[Partie 1/2] Guide de FFmpeg et SDL ou Comment écrire un lecteur vidéo en moins de 1000 lignes


Bien que ces informations soient déjà obsolètes, le matériel d'origine est toujours une source d'inspiration populaire pour divers contenus utiles sur le thème de FFmpeg. Cependant, il n'y a toujours pas de traduction complète de l'original en russe. Nous corrigeons l'omission ennuyeuse, car il vaut mieux tard que jamais.

Et bien que nous ayons essayé, les difficultés de traduction sont inévitables dans un texte aussi volumineux . Signalez les bugs (de préférence dans les messages privés) - ensemble, nous ferons mieux.

Table des matières

Logiciel EDISON - développement web
EDISON.

, C C++.

! ;-)


UPD: Ce guide a été mis à jour en février 2015.

FFmpeg est une excellente bibliothèque pour créer des applications vidéo ainsi que des utilitaires à usage général. FFmpeg prend en charge l'intégralité de la routine de traitement vidéo, réalisant tous les décodages, encodages, multiplexages et démultiplexages. Ce qui simplifie grandement la création d'applications multimédias. Tout est assez simple et rapide, écrit en C, vous pouvez décoder presque tous les codecs disponibles aujourd'hui, ainsi que coder dans d'autres formats.

Le seul hic, c'est que la documentation est généralement manquante. Il y a un tutoriel ( dans l'original, voici un lien vers une page web déjà inexistante - traducteur de notes), qui couvre les bases de FFmpeg et la génération automatique de docks doxygen. Et rien de plus. Par conséquent, j'ai décidé de trouver de manière indépendante comment utiliser FFmpeg pour créer des applications vidéo et audio numériques fonctionnelles, et en même temps documenter le processus et le présenter sous la forme d'un manuel.

Il existe un programme FFplay fourni avec FFmpeg. Il est simple, écrit en C, implémente un lecteur vidéo à part entière en utilisant FFmpeg. Ma première leçon est une version mise à jour de la leçon originale de Martin Boehme ( dans l'original, un lien vers une page Web déjà disparue - une note du traducteur ) - j'en ai tiré quelques morceaux. Et aussi dans une série de mes leçons, je montrerai le processus de création d'un lecteur vidéo fonctionnel basé sur ffplay.cFabrice Bellard. Chaque leçon présentera une nouvelle idée (voire deux) avec une explication de sa mise en œuvre. Chaque chapitre est accompagné d'une liste C, que vous pouvez compiler et exécuter par vous-même. Les fichiers sources montreront comment ce programme fonctionne, comment ses différentes parties fonctionnent, ainsi que des détails techniques mineurs qui ne sont pas traités dans ce guide. Lorsque nous aurons terminé, nous aurons un lecteur vidéo fonctionnel écrit en moins de 1000 lignes de code!

Lors de la création du lecteur, nous utiliserons SDL pour sortir un fichier multimédia audio et vidéo. SDL est une excellente bibliothèque multimédia multiplateforme utilisée dans les programmes de lecture MPEG, les émulateurs et de nombreux jeux vidéo. Vous devrez télécharger et installer les bibliothèques SDL sur votre système afin de compiler les programmes de ce guide.

Ce tutoriel est destiné aux personnes ayant une bonne expérience de programmation. À tout le moins, vous devez connaître le C et avoir une compréhension des concepts tels que les files d'attente, les mutex, etc. Il devrait y avoir une certaine compréhension du multimédia; par exemple, des choses comme les formes d'onde et similaires. Cependant, être un gourou dans ces domaines n'est pas nécessaire, car de nombreux concepts seront expliqués au cours des leçons.

N'hésitez pas à m'envoyer des messages d'erreur, des questions, des commentaires, des idées, des fonctionnalités, peu importe, à Dranger Doggy Gmail dot Com.







A lire également sur le blog de la
société EDISON:


FFmpeg libav manual






Leçon 1: Création de captures d'écran


Liste complète: tutorial01.c
// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101 
// on GCC 4.7.2 in Debian February 2015

// A small sample program that shows how to use libavformat and libavcodec to
// read video from a file.
//
// Use
//
// gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lswscale -lz
//
// to build (assuming libavformat and libavcodec are correctly installed
// your system).
//
// Run using
//
// tutorial01 myvideofile.mpg
//
// to write the first five frames from "myvideofile.mpg" to disk in PPM
// format.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
  FILE *pFile;
  char szFilename[32];
  int  y;
  
  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile=fopen(szFilename, "wb");
  if(pFile==NULL)
    return;
  
  // Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
  
  // Write pixel data
  for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
  
  // Close file
  fclose(pFile);
}

int main(int argc, char *argv[]) {
  // Initalizing these to NULL prevents segfaults!
  AVFormatContext   *pFormatCtx = NULL;
  int               i, videoStream;
  AVCodecContext    *pCodecCtxOrig = NULL;
  AVCodecContext    *pCodecCtx = NULL;
  AVCodec           *pCodec = NULL;
  AVFrame           *pFrame = NULL;
  AVFrame           *pFrameRGB = NULL;
  AVPacket          packet;
  int               frameFinished;
  int               numBytes;
  uint8_t           *buffer = NULL;
  struct SwsContext *sws_ctx = NULL;

  if(argc < 2) {
    printf("Please provide a movie file\n");
    return -1;
  }
  // Register all formats and codecs
  av_register_all();
  
  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
  
  // Find the first video stream
  videoStream=-1;
  for(i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
  if(videoStream==-1)
    return -1; // Didn't find a video stream
  
  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }
  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();
  
  // Allocate an AVFrame structure
  pFrameRGB=av_frame_alloc();
  if(pFrameRGB==NULL)
    return -1;

  // Determine required buffer size and allocate buffer
  numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
			      pCodecCtx->height);
  buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
  
  // Assign appropriate parts of buffer to image planes in pFrameRGB
  // Note that pFrameRGB is an AVFrame, but AVFrame is a superset
  // of AVPicture
  avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
		 pCodecCtx->width, pCodecCtx->height);
  
  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_RGB24,
			   SWS_BILINEAR,
			   NULL,
			   NULL,
			   NULL
			   );

  // Read frames and save first five frames to disk
  i=0;
  while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
      
      // Did we get a video frame?
      if(frameFinished) {
	// Convert the image from its native format to RGB
	sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pFrameRGB->data, pFrameRGB->linesize);
	
	// Save the frame to disk
	if(++i<=5)
	  SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, 
		    i);
      }
    }
    
    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
  }
  
  // Free the RGB image
  av_free(buffer);
  av_frame_free(&pFrameRGB);
  
  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codecs
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);

  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

Aperçu


Les fichiers vidéo ont plusieurs composants principaux. Tout d'abord, le fichier lui-même est appelé conteneur , et le type de conteneur détermine la façon dont les données sont représentées dans le fichier. AVI et Quicktime sont des exemples de conteneurs . En outre, il existe plusieurs threads dans le fichier; en particulier, il existe généralement un flux audio et un flux vidéo . («Stream» est un mot amusant pour «une séquence d'éléments de données disponibles selon la chronologie».) Les éléments de données dans un flux sont appelés images . Chaque flux est codé par l'un ou l'autre type de codec . Le codec détermine comment les données réelles à diruyutsya et décembreAudité - d'où le nom du codec. Des exemples de codecs sont DivX et MP3. Les paquets sont ensuite lus à partir du flux. Les paquets sont des éléments de données qui peuvent contenir des bits de données qui sont décodés en trames brutes, que nous pouvons enfin manipuler dans notre application. Pour nos besoins, chaque paquet contient des images complètes (ou plusieurs images s'il s'agit d'audio).

Travailler avec des flux vidéo et audio est très simple, même au niveau le plus élémentaire:

10 OPEN video_stream FROM video.avi
20 READ packet FROM video_stream INTO frame
30 IF frame NOT COMPLETE GOTO 20
40 DO SOMETHING WITH frame
50 GOTO 20

Travailler avec du multimédia à l'aide de FFmpeg est presque aussi simple que dans ce programme, bien que dans certains programmes, l'étape «MAKE [...]» puisse être très difficile. Dans ce tutoriel, nous allons ouvrir le fichier, compter le flux vidéo à l'intérieur, et notre "MAKE [...]" écrira l'image dans le fichier PPM.

Fichier ouvert


Tout d'abord, voyons ce qui se passe en premier lorsque vous ouvrez le fichier. En utilisant FFmpeg, nous initialisons d'abord la bibliothèque souhaitée:

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <ffmpeg/swscale.h>
...
int main(int argc, charg *argv[]) {
av_register_all();

Cela enregistre tous les formats de fichiers et codecs disponibles dans la bibliothèque, ils seront donc utilisés automatiquement lors de l'ouverture d'un fichier avec le format / codec approprié. Notez que vous devez appeler av_register_all () une seule fois, nous le faisons donc ici dans main (). Si vous le souhaitez, vous ne pouvez enregistrer que des formats de fichiers et des codecs sélectifs, mais il n'y a généralement aucune raison particulière de le faire.

Ouvrez maintenant le fichier:

AVFormatContext *pFormatCtx = NULL;

// Open video file
if(avformat_open_input(&pFormatCtx, argv[1], NULL, 0, NULL)!=0)
  return -1; // Couldn't open file

Obtenez le nom du fichier à partir du premier argument. Cette fonction lit l'en-tête du fichier et stocke les informations de format de fichier dans la structure AVFormatContext que nous avons transmise. Les trois derniers arguments sont utilisés pour spécifier le format de fichier, la taille de la mémoire tampon et les paramètres de format. En les définissant sur NULL ou 0, libavformat détectera tout automatiquement.

Cette fonction ne regarde que l'en-tête, nous devons donc maintenant vérifier les informations de flux dans le fichier:

// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
  return -1; // Couldn't find stream information

Cette fonction transmet des données valides à pFormatCtx -> flux . Nous nous familiarisons avec une fonction de débogage pratique, nous montrant ce qu'il y a à l'intérieur:

// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, argv[1], 0);

Maintenant pFormatCtx -> streams est juste un tableau de pointeurs de taille pFormatCtx -> nb_streams . Nous allons le parcourir jusqu'à ce que nous trouvions le flux vidéo:

int i;
AVCodecContext *pCodecCtxOrig = NULL;
AVCodecContext *pCodecCtx = NULL;

// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
  if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
    videoStream=i;
    break;
  }
if(videoStream==-1)
  return -1; // Didn't find a video stream

// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;

Les informations sur le codec dans le flux sont situées dans un endroit appelé " contexte du codec ". Il contient toutes les informations sur le codec utilisé par le flux, et nous avons maintenant un pointeur sur celui-ci. Mais nous devons encore trouver le vrai codec et l'ouvrir:

AVCodec *pCodec = NULL;

// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1; // Codec not found
}
// Copy context
pCodecCtx = avcodec_alloc_context3(pCodec);
if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // Error copying codec context
}
// Open codec
if(avcodec_open2(pCodecCtx, pCodec)<0)
  return -1; // Could not open codec

Veuillez noter que vous ne pouvez pas utiliser directement AVCodecContext à partir du flux vidéo! Par conséquent, vous devez utiliser un vcodec_copy_context () pour copier le contexte vers un nouvel emplacement (bien sûr, une fois que la mémoire lui est allouée).

Stockage de données


Maintenant, nous avons besoin d'un endroit pour stocker le cadre:

AVFrame *pFrame = NULL;

// Allocate video frame
pFrame=av_frame_alloc();

Puisque nous prévoyons de sortir des fichiers PPM qui sont stockés en RVB 24 bits, nous devrons convertir notre cadre de son propre format en RVB. FFmpeg le fera pour nous. Pour la plupart des projets (y compris celui-ci), vous devez convertir l'image de départ dans un format spécifique. Sélectionnez un cadre pour le cadre converti:

// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();
if(pFrameRGB==NULL)
  return -1;

Malgré le fait que nous avons sélectionné le cadre, nous avons toujours besoin d'un endroit pour accueillir les données brutes lors de la conversion. Nous utilisons avpicture_get_size pour obtenir les bonnes tailles et allouer manuellement l'espace nécessaire:

uint8_t *buffer = NULL;
int numBytes;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
                            pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

av_malloc est un analogue de la fonction C malloc de FFmpeg, qui est un simple wrapper autour de malloc qui fournit l'alignement des adresses mémoire, etc. Soit dit en passant, cela ne protège pas contre les fuites de mémoire, la double libération ou d'autres problèmes qui se produisent avec malloc .

Maintenant, nous utilisons avpicture_fill pour associer la trame à notre tampon nouvellement alloué. Concernant AVPicture : la structure AVPicture est un sous-ensemble de la structure AVFrame - le début de la structure AVFrame est identique à la structure AVPicture .

// Assign appropriate parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
                pCodecCtx->width, pCodecCtx->height);

Nous sommes déjà à la ligne d'arrivée! Maintenant, nous sommes prêts à lire le flux!

Lecture des données


Maintenant, pour lire l'intégralité du flux vidéo, nous lisons le paquet suivant, le déchiffrons dans notre cadre, et dès que le déchiffrement est terminé, convertissez le cadre et enregistrez-le:

struct SwsContext *sws_ctx = NULL;
int frameFinished;
AVPacket packet;
// initialize SWS context for software scaling
sws_ctx = sws_getContext(pCodecCtx->width,
    pCodecCtx->height,
    pCodecCtx->pix_fmt,
    pCodecCtx->width,
    pCodecCtx->height,
    PIX_FMT_RGB24,
    SWS_BILINEAR,
    NULL,
    NULL,
    NULL
    );

i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
  // Is this a packet from the video stream?
  if(packet.stream_index==videoStream) {
	// Decode video frame
    avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
    
    // Did we get a video frame?
    if(frameFinished) {
    // Convert the image from its native format to RGB
        sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pFrameRGB->data, pFrameRGB->linesize);
	
        // Save the frame to disk
        if(++i<=5)
          SaveFrame(pFrameRGB, pCodecCtx->width, 
                    pCodecCtx->height, i);
    }
  }
    
  // Free the packet that was allocated by av_read_frame
  av_free_packet(&packet);
}

Rien de compliqué: av_read_frame () lit le paquet et l'enregistre dans la structure AVPacket . Veuillez noter que nous distribuons uniquement la structure du package - FFmpeg nous fournit les données internes vers lesquelles pointe packet.data . Cela libère av_free_packet () un peu plus tard . avcodec_decode_video () convertit le paquet en trame. Cependant, nous pouvons ne pas avoir toutes les informations dont nous avons besoin pour la trame après décodage du paquet, donc avcodec_decode_video () définit frameFinished lorsque nous avons la trame suivante. Enfin, nous utilisons sws_scale () pour convertir à partir de notre propre format ( pCodecCtx ->pix_fmt ) en RVB. N'oubliez pas que vous pouvez convertir unpointeur AVFrame en un pointeur AVPicture . Enfin, nous transmettons les informations sur le cadre, la hauteur et la largeur de notre fonction SaveFrame .

En parlant de packages. Techniquement, un paquet ne peut contenir qu'une partie d'une trame, ainsi que d'autres bits de données. Cependant, l'analyseur FFmpeg garantit que les paquets que nous recevons contiennent soit une trame complète, soit même plusieurs trames.

Il ne reste plus qu'à utiliser la fonction SaveFrame pour écrire les informations RVB dans un fichier PPM. Bien que nous traitions superficiellement le format PPM lui-même; croyez-moi, tout fonctionne ici:

void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
  FILE *pFile;
  char szFilename[32];
  int  y;
  
  // Open file
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile=fopen(szFilename, "wb");
  if(pFile==NULL)
    return;
  
  // Write header
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);
  
  // Write pixel data
  for(y=0; y<height; y++)
    fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
  
  // Close file
  fclose(pFile);
}

Nous effectuons un fichier standard ouvert, etc., puis enregistrons les données RVB. Le fichier est écrit ligne par ligne. Un fichier PPM est simplement un fichier dans lequel les informations RVB sont présentées sous forme de longue ligne. Si vous connaissez les couleurs du HTML, ce sera comme marquer les couleurs de chaque pixel de la première à la dernière, quelque chose comme # ff0000 # ff0000 .... , comme pour un écran rouge. (En fait, il est stocké au format binaire et sans séparateur, mais j'espère que vous comprendrez l'idée.) Le titre indique la largeur et la hauteur de l'image, ainsi que la taille maximale des valeurs RVB.

Revenons maintenant à notre fonction main (). Dès que nous avons fini de lire le flux vidéo, nous avons juste besoin de tout effacer:

// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);

// Free the YUV frame
av_free(pFrame);

// Close the codecs
avcodec_close(pCodecCtx);
avcodec_close(pCodecCtxOrig);

// Close the video file
avformat_close_input(&pFormatCtx);

return 0;

Comme vous pouvez le voir, nous utilisons av_free pour la mémoire allouée en utilisant avcode_alloc_frame et av_malloc .

C'est tout le code! Maintenant, si vous utilisez Linux ou une plate-forme similaire, exécutez:

gcc -o tutorial01 tutorial01.c -lavutil -lavformat -lavcodec -lz -lavutil -lm

Si vous avez une ancienne version de FFmpeg, vous devrez peut-être supprimer -lavutil :

gcc -o tutorial01 tutorial01.c -lavformat -lavcodec -lz -lm

La plupart des programmes graphiques doivent ouvrir le format PPM. Découvrez-le sur certains fichiers vidéo dont les captures d'écran ont été réalisées à l'aide de notre programme.






Leçon 2: Affichage de l'écran


Liste complète: tutorial02.c
// tutorial02.c
// A pedagogical video player that will stream through every video frame as fast as it can.
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
//
// Use
// 
// gcc -o tutorial02 tutorial02.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial02 myvideofile.mpg
//
// to play the video stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

int main(int argc, char *argv[]) {
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream;
  AVCodecContext  *pCodecCtxOrig = NULL;
  AVCodecContext  *pCodecCtx = NULL;
  AVCodec         *pCodec = NULL;
  AVFrame         *pFrame = NULL;
  AVPacket        packet;
  int             frameFinished;
  float           aspect_ratio;
  struct SwsContext *sws_ctx = NULL;

  SDL_Overlay     *bmp;
  SDL_Surface     *screen;
  SDL_Rect        rect;
  SDL_Event       event;

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
  
  // Find the first video stream
  videoStream=-1;
  for(i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
  if(videoStream==-1)
    return -1; // Didn't find a video stream
  
  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }

  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
#else
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }
  
  // Allocate a place to put our YUV image on that screen
  bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
				 pCodecCtx->height,
				 SDL_YV12_OVERLAY,
				 screen);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_YUV420P,
			   SWS_BILINEAR,
			   NULL,
			   NULL,
			   NULL
			   );



  // Read frames and save first five frames to disk
  i=0;
  while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
      
      // Did we get a video frame?
      if(frameFinished) {
	SDL_LockYUVOverlay(bmp);

	AVPicture pict;
	pict.data[0] = bmp->pixels[0];
	pict.data[1] = bmp->pixels[2];
	pict.data[2] = bmp->pixels[1];

	pict.linesize[0] = bmp->pitches[0];
	pict.linesize[1] = bmp->pitches[2];
	pict.linesize[2] = bmp->pitches[1];

	// Convert the image into YUV format that SDL uses
	sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pict.data, pict.linesize);

	SDL_UnlockYUVOverlay(bmp);
	
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
      
      }
    }
    
    // Free the packet that was allocated by av_read_frame
    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

  }
  
  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codec
  avcodec_close(pCodecCtx);
  avcodec_close(pCodecCtxOrig);
  
  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

SDL et vidéo


Pour dessiner sur l'écran, nous utiliserons SDL. SDL signifie Simple Direct Layer . Il s'agit d'une excellente bibliothèque multimédia multiplateforme utilisée dans de nombreux projets. Vous pouvez obtenir la bibliothèque sur le site officiel ou télécharger le package développeur pour votre système d'exploitation, le cas échéant. Vous aurez besoin de bibliothèques pour compiler le code de cette leçon (toutes les autres leçons, d'ailleurs, cela s'applique également).

SDL propose de nombreuses méthodes pour dessiner à l'écran. Une façon d'afficher des films est ce qu'on appelle la superposition YUV .

Formellement, pas même YUV, mais YCbCr. Soit dit en passant, certaines personnes sont très brûlées lorsque «YCbCr» est appelé «YUV». D'une manière générale, YUV est un format analogique et YCbCr est un format numérique. FFmpeg et SDL dans leur code et dans les macros désignent YCbCr comme YUV, mais c'est le cas.

YUV est une méthode de stockage de données d'image brutes telles que RVB. En gros, Y est une composante de la luminosité et U et V sont des composantes de la couleur . (C'est plus compliqué que RVB car une partie des informations de couleur est supprimée et vous ne pouvez avoir qu'une mesure de U et V pour 2 mesures de Y ). Superposition YUVdans SDL accepte un jeu de données YUV brut et l'affiche. Il accepte 4 types de formats YUV différents, mais le YV12 est le plus rapide d'entre eux. Il existe un autre format YUV appelé YUV420P qui correspond à YV12, sauf que les tableaux de U et V sont échangés. 420 signifie qu'il est échantillonné selon un rapport de 4: 2: 0, c'est-à-dire que pour 4 mesures de luminosité, il y a 1 mesure de couleur, de sorte que les informations de couleur sont réparties en quartiers. C'est un bon moyen d'économiser de la bande passante car l'œil humain ne remarque toujours pas ces changements. La lettre latine «P» dans le nom indique que le format est «planaire», ce qui signifie simplement que les composants sont Y ,U et V sont dans des tableaux séparés. FFmpeg peut convertir des images en YUV420P , ce qui est très utile, car de nombreux flux vidéo sont déjà stockés dans ce format ou y sont facilement convertis.

Ainsi, notre plan actuel est de remplacer la fonction SaveFrame () de la leçon précédente et d'afficher notre cadre à la place. Mais vous devez d'abord vous familiariser avec les fonctionnalités de base de la bibliothèque SDL. Pour commencer, connectez les bibliothèques et initialisez le SDL:

#include <SDL.h>
#include <SDL_thread.h>

if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
  fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
  exit(1);
}

SDL_Init () indique essentiellement à la bibliothèque les fonctions que nous utiliserons. SDL_GetError (), bien sûr, c'est notre fonction pratique pour le débogage.

Création d'affichage


Maintenant, nous avons besoin d'une place sur l'écran pour organiser les éléments. La zone principale d'affichage des images avec SDL est appelée la surface :

SDL_Surface *screen;

screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
if(!screen) {
  fprintf(stderr, "SDL: could not set video mode - exiting\n");
  exit(1);
}

Nous avons donc mis en place un écran avec une largeur et une hauteur données. L'option suivante est la profondeur de bits de l'écran - 0 - il s'agit d'une valeur spéciale qui signifie «la même chose que l'affichage actuel».

Maintenant, nous créons une superposition YUV sur cet écran afin que nous puissions y produire de la vidéo et configurer notre SWSContext pour convertir les données d'image en YUV420 :

SDL_Overlay     *bmp = NULL;
struct SWSContext *sws_ctx = NULL;

bmp = SDL_CreateYUVOverlay(pCodecCtx->width, pCodecCtx->height,
                           SDL_YV12_OVERLAY, screen);

// initialize SWS context for software scaling
sws_ctx = sws_getContext(pCodecCtx->width,
                         pCodecCtx->height,
			 pCodecCtx->pix_fmt,
			 pCodecCtx->width,
			 pCodecCtx->height,
			 PIX_FMT_YUV420P,
			 SWS_BILINEAR,
			 NULL,
			 NULL,
			 NULL
			 );

Comme mentionné, nous utilisons YV12 pour afficher l'image et obtenir les données YUV420 de FFmpeg.

Affichage d'image


Eh bien, c'était assez facile! Maintenant, nous avons juste besoin de montrer l'image. Allons jusqu'à l'endroit où nous avons eu le tir final. Nous pouvons nous débarrasser de tout ce que nous avions pour le cadre RVB et nous allons remplacer SaveFrame () par notre code d'affichage. Pour afficher l'image, nous allons créer une structure AVPicture et définir les pointeurs de données et la taille de la ligne pour notre superposition YUV :

  if(frameFinished) {
    SDL_LockYUVOverlay(bmp);

    AVPicture pict;
    pict.data[0] = bmp->pixels[0];
    pict.data[1] = bmp->pixels[2];
    pict.data[2] = bmp->pixels[1];

    pict.linesize[0] = bmp->pitches[0];
    pict.linesize[1] = bmp->pitches[2];
    pict.linesize[2] = bmp->pitches[1];

    // Convert the image into YUV format that SDL uses
    sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, pCodecCtx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(bmp);

Au début, nous bloquons la superposition, car nous prévoyons d'y écrire. C'est une bonne habitude pour qu'il n'y ait plus de problèmes par la suite. La structure AVPicture , comme indiqué ci-dessus, a un pointeur de données, qui est un tableau de 4 pointeurs. Puisqu'il s'agit ici du YUV420P , nous n'avons que 3 canaux et donc seulement 3 ensembles de données. D'autres formats peuvent avoir un quatrième pointeur pour le canal alpha ou autre chose. La taille de la ligne est à quoi elle ressemble. Des structures similaires dans notre superposition YUV sont des variables pour les pixels et les hauteurs. (Pitches, pitches - s'ils sont exprimés en SDL pour indiquer la largeur d'une ligne de données donnée.) Donc, nous indiquons trois tableaux de données illustrés sur notre superposition, donc lorsque nous écrivons danspict , nous enregistrons en fait dans notre superposition, qui, bien sûr, dispose déjà de l'espace nécessaire spécifiquement pour cela. De la même manière, nous obtenons des informations de taille de ligne directement à partir de notre superposition. Nous changeons le format de conversion en PIX_FMT_YUV420P et utilisons sws_scale comme précédemment.

Dessin d'image


Mais nous devons encore spécifier pour le SDL afin qu'il montre vraiment les données que nous lui avons fournies. Nous passons également un rectangle à cette fonction, qui indique où le film doit aller, à quelle largeur et quelle hauteur il doit être mis à l'échelle. Ainsi, le SDL évolue pour nous, ce qui peut aider votre GPU à évoluer plus rapidement:

SDL_Rect rect;

  if(frameFinished) {
    /* ... code ... */
    // Convert the image into YUV format that SDL uses
    sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
              pFrame->linesize, 0, pCodecCtx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(bmp);
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
  }

Maintenant, notre vidéo est affichée!

Montrons une autre fonctionnalité de SDL: le système d'événements . SDL est configuré de telle manière que lorsque vous entrez ou déplacez la souris dans l'application SDL ou lui envoyez un signal, un événement est généré. Votre programme vérifie ensuite ces événements s'il est destiné à traiter les entrées utilisateur. Votre programme peut également créer des événements pour envoyer des événements SDL au système. Ceci est particulièrement utile pour la programmation multi-thread avec SDL, que nous verrons dans la leçon numéro 4. Dans notre programme, nous allons vérifier les événements immédiatement après le traitement du colis. Pour le moment, nous allons gérer l'événement SDL_QUIT afin de pouvoir quitter:

SDL_Event       event;

    av_free_packet(&packet);
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

Et donc nous vivons! Nous nous débarrassons de toutes les vieilles ordures et nous sommes prêts à compiler. Si vous utilisez Linux ou quelque chose comme Linux, la meilleure façon de compiler à l'aide des bibliothèques SDL est:

gcc -o tutorial02 tutorial02.c -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

sdl-config affiche simplement les drapeaux nécessaires pour que gcc active correctement les bibliothèques SDL. Vous devrez peut-être faire autre chose pour effectuer cette compilation sur votre système; veuillez consulter la documentation SDL de votre système pour tout pompier. Une fois compilé, continuez et exécutez.

Que se passe-t-il lorsque vous exécutez ce programme? La vidéo semble devenir folle! En fait, nous affichons simplement toutes les images vidéo aussi rapidement que nous pouvons les extraire du fichier vidéo. Nous n'avons pas le code pour le moment pour savoir quand nous devons montrer la vidéo. À la fin (dans la leçon numéro 5), nous commencerons à synchroniser la vidéo. Mais pour le moment il nous manque quelque chose d'égale importance: le son!






Leçon 3: Jouer le son


Liste complète: tutorial03.c
// tutorial03.c
// A pedagogical video player that will stream through every video frame as fast as it can
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
//
// Use
//
// gcc -o tutorial03 tutorial03.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial03 myvideofile.mpg
//
// to play the stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;

PacketQueue audioq;

int quit = 0;

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  
  SDL_LockMutex(q->mutex);
  
  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;
  
  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf, int buf_size) {

  static AVPacket pkt;
  static uint8_t *audio_pkt_data = NULL;
  static int audio_pkt_size = 0;
  static AVFrame frame;

  int len1, data_size = 0;

  for(;;) {
    while(audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	audio_pkt_size = 0;
	break;
      }
      audio_pkt_data += len1;
      audio_pkt_size -= len1;
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       aCodecCtx->channels,
					       frame.nb_samples,
					       aCodecCtx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, frame.data[0], data_size);
      }
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt.data)
      av_free_packet(&pkt);

    if(quit) {
      return -1;
    }

    if(packet_queue_get(&audioq, &pkt, 1) < 0) {
      return -1;
    }
    audio_pkt_data = pkt.data;
    audio_pkt_size = pkt.size;
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
  int len1, audio_size;

  static uint8_t audio_buf[(MAX_AUDIO_FRAME_SIZE * 3) / 2];
  static unsigned int audio_buf_size = 0;
  static unsigned int audio_buf_index = 0;

  while(len > 0) {
    if(audio_buf_index >= audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(aCodecCtx, audio_buf, sizeof(audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	audio_buf_size = 1024; // arbitrary?
	memset(audio_buf, 0, audio_buf_size);
      } else {
	audio_buf_size = audio_size;
      }
      audio_buf_index = 0;
    }
    len1 = audio_buf_size - audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
    len -= len1;
    stream += len1;
    audio_buf_index += len1;
  }
}

int main(int argc, char *argv[]) {
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream, audioStream;
  AVCodecContext  *pCodecCtxOrig = NULL;
  AVCodecContext  *pCodecCtx = NULL;
  AVCodec         *pCodec = NULL;
  AVFrame         *pFrame = NULL;
  AVPacket        packet;
  int             frameFinished;
  struct SwsContext *sws_ctx = NULL;
  
  AVCodecContext  *aCodecCtxOrig = NULL;
  AVCodecContext  *aCodecCtx = NULL;
  AVCodec         *aCodec = NULL;

  SDL_Overlay     *bmp;
  SDL_Surface     *screen;
  SDL_Rect        rect;
  SDL_Event       event;
  SDL_AudioSpec   wanted_spec, spec;

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Open video file
  if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
    return -1; // Couldn't open file
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, argv[1], 0);
    
  // Find the first video stream
  videoStream=-1;
  audioStream=-1;
  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       videoStream < 0) {
      videoStream=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audioStream < 0) {
      audioStream=i;
    }
  }
  if(videoStream==-1)
    return -1; // Didn't find a video stream
  if(audioStream==-1)
    return -1;
   
  aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;
  aCodec = avcodec_find_decoder(aCodecCtxOrig->codec_id);
  if(!aCodec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  // Copy context
  aCodecCtx = avcodec_alloc_context3(aCodec);
  if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Set audio settings from codec info
  wanted_spec.freq = aCodecCtx->sample_rate;
  wanted_spec.format = AUDIO_S16SYS;
  wanted_spec.channels = aCodecCtx->channels;
  wanted_spec.silence = 0;
  wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
  wanted_spec.callback = audio_callback;
  wanted_spec.userdata = aCodecCtx;
  
  if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
    fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
    return -1;
  }

  avcodec_open2(aCodecCtx, aCodec, NULL);

  // audio_st = pFormatCtx->streams[index]
  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

  // Get a pointer to the codec context for the video stream
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  
  // Find the decoder for the video stream
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1; // Codec not found
  }

  // Copy context
  pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }

  // Open codec
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1; // Could not open codec
  
  // Allocate video frame
  pFrame=av_frame_alloc();

  // Make a screen to put our video

#ifndef __DARWIN__
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 0, 0);
#else
        screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->height, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }
  
  // Allocate a place to put our YUV image on that screen
  bmp = SDL_CreateYUVOverlay(pCodecCtx->width,
				 pCodecCtx->height,
				 SDL_YV12_OVERLAY,
				 screen);

  // initialize SWS context for software scaling
  sws_ctx = sws_getContext(pCodecCtx->width,
			   pCodecCtx->height,
			   pCodecCtx->pix_fmt,
			   pCodecCtx->width,
			   pCodecCtx->height,
			   PIX_FMT_YUV420P,
			   SWS_BILINEAR,
			   NULL,
			   NULL,
			   NULL
			   );

  // Read frames and save first five frames to disk
  i=0;
  while(av_read_frame(pFormatCtx, &packet)>=0) {
    // Is this a packet from the video stream?
    if(packet.stream_index==videoStream) {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
      
      // Did we get a video frame?
      if(frameFinished) {
	SDL_LockYUVOverlay(bmp);

	AVPicture pict;
	pict.data[0] = bmp->pixels[0];
	pict.data[1] = bmp->pixels[2];
	pict.data[2] = bmp->pixels[1];

	pict.linesize[0] = bmp->pitches[0];
	pict.linesize[1] = bmp->pitches[2];
	pict.linesize[2] = bmp->pitches[1];

	// Convert the image into YUV format that SDL uses	
	sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,
		  pFrame->linesize, 0, pCodecCtx->height,
		  pict.data, pict.linesize);
	
	SDL_UnlockYUVOverlay(bmp);
	
	rect.x = 0;
	rect.y = 0;
	rect.w = pCodecCtx->width;
	rect.h = pCodecCtx->height;
	SDL_DisplayYUVOverlay(bmp, &rect);
	av_free_packet(&packet);
      }
    } else if(packet.stream_index==audioStream) {
      packet_queue_put(&audioq, &packet);
    } else {
      av_free_packet(&packet);
    }
    // Free the packet that was allocated by av_read_frame
    SDL_PollEvent(&event);
    switch(event.type) {
    case SDL_QUIT:
      quit = 1;
      SDL_Quit();
      exit(0);
      break;
    default:
      break;
    }

  }

  // Free the YUV frame
  av_frame_free(&pFrame);
  
  // Close the codecs
  avcodec_close(pCodecCtxOrig);
  avcodec_close(pCodecCtx);
  avcodec_close(aCodecCtxOrig);
  avcodec_close(aCodecCtx);
  
  // Close the video file
  avformat_close_input(&pFormatCtx);
  
  return 0;
}

l'audio


Maintenant, nous aimerions que le son soit lu dans l'application. SDL nous fournit également des méthodes de lecture du son. La fonction SDL_OpenAudio () est utilisée pour ouvrir le périphérique audio lui-même. Il prend comme arguments la structure SDL_AudioSpec , qui contient toutes les informations sur l'audio que nous allons lire.

Avant de montrer comment configurer cela, nous expliquons d'abord comment l'ordinateur traite l'audio en général. L'audio numérique se compose d'un long flux d' échantillons, chacun représentant une signification spécifique d'une onde sonore. Les sons sont enregistrés à une fréquence d'échantillonnage spécifique, qui indique simplement la vitesse de lecture de chaque échantillon et mesurée par le nombre d'échantillons par seconde. Les fréquences d'échantillonnage approximatives sont de 22 050 et 44 100 échantillons par seconde, qui sont les vitesses utilisées pour la radio et le CD, respectivement. De plus, la plupart des fichiers audio peuvent avoir plus d'un canal pour le son stéréo ou surround, donc, par exemple, si l'échantillon est en stéréo, les échantillons viendront deux à la fois. Lorsque nous obtenons les données du fichier vidéo, nous ne savons pas combien d'échantillons nous obtiendrons, mais FFmpeg ne produit pas d'échantillons cassés - cela signifie également qu'il ne séparera pas l'échantillon stéréo.

La méthode de lecture audio en SDL est la suivante. Les paramètres sonores sont configurés: fréquence d'échantillonnage, nombre de canaux, etc. Et définissez également la fonction de rappel et les données utilisateur. Lorsque nous commençons à jouer du son, le SDL appellera constamment cette fonction de rappel et lui demandera de remplir le tampon audio avec un certain nombre d'octets. Après avoir mis ces informations dans la structure SDL_AudioSpec , nous appelons SDL_OpenAudio (), ce qui ouvrira le périphérique audio et nous renverra une autre structure AudioSpec . Ce sont les caractéristiques que nous utiliserons réellement - il n'y a aucune garantie que nous obtiendrons exactement ce que nous avons demandé!

Réglage audio


Gardez cela à l'esprit pour l'instant, car nous n'avons pas encore d'informations sur les flux audio! Revenons à l'endroit de notre code où nous avons trouvé le flux vidéo et découvrons quel flux est le flux audio:

// Find the first video stream
videoStream=-1;
audioStream=-1;
for(i=0; i < pFormatCtx->nb_streams; i++) {
  if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO
     &&
       videoStream < 0) {
    videoStream=i;
  }
  if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
     audioStream < 0) {
    audioStream=i;
  }
}
if(videoStream==-1)
  return -1; // Didn't find a video stream
if(audioStream==-1)
  return -1;

Ici, nous pouvons obtenir toutes les informations que nous voulons de l' AVCodecContext du flux, tout comme nous l'avons fait avec le flux vidéo:

AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;

aCodecCtxOrig=pFormatCtx->streams[audioStream]->codec;

Si vous vous souvenez, dans les leçons précédentes, nous devons encore ouvrir le codec audio lui-même. C'est simple:

AVCodec         *aCodec;

aCodec = avcodec_find_decoder(aCodecCtx->codec_id);
if(!aCodec) {
  fprintf(stderr, "Unsupported codec!\n");
  return -1;
}
// Copy context
aCodecCtx = avcodec_alloc_context3(aCodec);
if(avcodec_copy_context(aCodecCtx, aCodecCtxOrig) != 0) {
  fprintf(stderr, "Couldn't copy codec context");
  return -1; // Error copying codec context
}
/* set up SDL Audio here */

avcodec_open2(aCodecCtx, aCodec, NULL);

Dans le cadre du codec contient toutes les informations nécessaires pour configurer notre audio:

wanted_spec.freq = aCodecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = aCodecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = aCodecCtx;

if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
  fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
  return -1;
}

Passons en revue chaque élément:
  • freq (fréquence): taux d'échantillonnage, comme expliqué précédemment.
  • format (): SDL , . «S» «S16SYS» «», 16 , 16 , «SYS» , , . , avcodec_decode_audio2 .
  • channels (): .
  • silence (): , . 0.
  • samples (): , , SDL , . - 512 8192; FFplay, , 1024.
  • callback (callback): ici on passe la vraie fonction de callback. Nous parlerons plus en détail de la fonction de rappel plus tard.
  • userdata : Le SDL donnera à notre rappel un pointeur nul vers toutes les données utilisateur que nous voulons. Nous voulons lui faire connaître notre contexte de codec; un peu plus bas, on comprendra pourquoi.

Enfin, ouvrez l'audio avec SDL_OpenAudio .

Files d'attente


Et c'est nécessaire! Nous sommes maintenant prêts à extraire des informations audio du flux. Mais que faire de ces informations? Nous recevrons en continu des paquets du fichier vidéo, mais en même temps, le SDL appellera la fonction de rappel! La solution sera de créer une sorte de structure globale dans laquelle nous pouvons insérer des paquets audio afin que notre audio_callback ait quelque chose pour recevoir des données audio! Voici donc ce que nous allons faire pour créer la file d'attente de paquets. FFmpeg a même une structure pour aider à cela: AVPacketList , qui est juste une liste chaînée pour les packages. Voici notre structure de file d'attente:

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;

Tout d'abord, nous devons indiquer que nb_packets est de taille différente - la taille fait référence à la taille de l'octet que nous obtenons du paquet-> taille . Notez que nous avons un mutex et une variable de condition. En effet, le SDL exécute le processus audio en tant que flux distinct. Si nous ne bloquons pas la file d'attente correctement, nous pouvons vraiment ruiner nos données. Voyons comment la file d'attente est implémentée. Chaque programmeur qui se respecte devrait savoir comment créer des files d'attente, mais nous montrerons également comment procéder afin qu'il vous soit plus facile d'apprendre les fonctions SDL.

Tout d'abord, nous créons une fonction pour initialiser la file d'attente:

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}

Créez ensuite une fonction pour placer des objets dans notre file d'attente:

int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  
  SDL_LockMutex(q->mutex);
  
  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}

SDL_LockMutex () bloque le mutex dans la file d'attente afin que nous puissions ajouter quelque chose, puis SDL_CondSignal () envoie un signal à notre fonction get (si elle s'y attend) via notre variable conditionnelle pour lui dire qu'il existe des données et peut être poursuivi, pour plus loin débloquer mutex.

Voici la fonction get correspondante . Remarquez comment SDL_CondWait () crée le bloc fonction (c'est-à-dire fait une pause jusqu'à ce que nous obtenions les données) si nous lui demandons de faire ceci:

int quit = 0;

static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block) {
  AVPacketList *pkt1;
  int ret;
  
  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

Comme vous pouvez le voir, nous avons enveloppé la fonction dans un cycle éternel, donc nous obtiendrons certainement des données si nous voulons la bloquer. Nous évitons de boucler pour toujours en utilisant la fonction SDL_CondWait (). Essentiellement, CondWait ne fait qu'attendre un signal de SDL_CondSignal () (ou SDL_CondBroadcast ()) puis continuer. Cependant, il semble que nous l'avons attrapé dans un mutex - si nous maintenons le verrou, notre fonction put ne peut rien mettre en file d'attente! Cependant, ce que SDL_CondWait () fait également pour nous est de débloquer le mutex que nous lui donnons, puis d'essayer à nouveau de le verrouiller dès que nous recevons le signal.

Pour chaque pompier


Vous voyez également que nous avons une variable globale de sortie que nous vérifions pour nous assurer que nous n'avons pas défini de signal de sortie dans le programme (SDL traite automatiquement les signaux TERM , etc.). Sinon, le thread continuera indéfiniment et nous devrons tuer le programme avec kill -9 :

  SDL_PollEvent(&event);
  switch(event.type) {
  case SDL_QUIT:
    quit = 1;

Nous allons mettre le drapeau de sortie à 1.

Nous alimentons les colis


Il ne reste plus qu'à configurer notre file d'attente:

PacketQueue audioq;
main() {
...
  avcodec_open2(aCodecCtx, aCodec, NULL);

  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

SDL_PauseAudio () démarre enfin l'unité audio. Il reproduit le silence s'il ne reçoit pas de données; mais cela ne se produit pas immédiatement.

Donc, nous avons une file d'attente configurée, maintenant nous sommes prêts à lui envoyer des paquets. Nous passons à notre cycle de lecture de colis:

while(av_read_frame(pFormatCtx, &packet)>=0) {
  // Is this a packet from the video stream?
  if(packet.stream_index==videoStream) {
    // Decode video frame
    ....
    }
  } else if(packet.stream_index==audioStream) {
    packet_queue_put(&audioq, &packet);
  } else {
    av_free_packet(&packet);
  }

Veuillez noter que nous ne publions pas le package après l'avoir mis en file d'attente. Nous le publierons plus tard lorsque nous décrypterons.

Récupération des packages


Maintenant, faisons enfin notre fonction audio_callback pour récupérer les paquets de la file d'attente. Le rappel devrait ressembler à:

void callback(void *userdata, Uint8 *stream, int len)

userdata , est le pointeur que nous avons donné à la SDL, stream est le tampon dans lequel nous allons écrire des données audio, et len est la taille de ce tampon. Voici le code:

void audio_callback(void *userdata, Uint8 *stream, int len) {

  AVCodecContext *aCodecCtx = (AVCodecContext *)userdata;
  int len1, audio_size;

  static uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  static unsigned int audio_buf_size = 0;
  static unsigned int audio_buf_index = 0;

  while(len > 0) {
    if(audio_buf_index >= audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(aCodecCtx, audio_buf,
                                      sizeof(audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	audio_buf_size = 1024;
	memset(audio_buf, 0, audio_buf_size);
      } else {
	audio_buf_size = audio_size;
      }
      audio_buf_index = 0;
    }
    len1 = audio_buf_size - audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)audio_buf + audio_buf_index, len1);
    len -= len1;
    stream += len1;
    audio_buf_index += len1;
  }
}

En fait, il s'agit d'une simple boucle qui extrait les données d'une autre fonction que nous avons écrite, audio_decode_frame (), enregistre le résultat dans un tampon intermédiaire, essaie d'écrire len octets dans le flux et reçoit plus de données si nous n'en avons toujours pas assez ou l'enregistrons pour plus tard, s'il nous reste quelque chose. La taille de audio_buf est 1,5 fois la taille de la plus grande trame audio que FFmpeg nous donnera, ce qui nous donne une bonne marge.

Décryptage audio final


Regardons l'intérieur du décodeur audio_decode_frame :

int audio_decode_frame(AVCodecContext *aCodecCtx, uint8_t *audio_buf,
                       int buf_size) {

  static AVPacket pkt;
  static uint8_t *audio_pkt_data = NULL;
  static int audio_pkt_size = 0;
  static AVFrame frame;

  int len1, data_size = 0;

  for(;;) {
    while(audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(aCodecCtx, &frame, &got_frame, &pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	audio_pkt_size = 0;
	break;
      }
      audio_pkt_data += len1;
      audio_pkt_size -= len1;
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       aCodecCtx->channels,
					       frame.nb_samples,
					       aCodecCtx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, frame.data[0], data_size);
      }
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt.data)
      av_free_packet(&pkt);

    if(quit) {
      return -1;
    }

    if(packet_queue_get(&audioq, &pkt, 1) < 0) {
      return -1;
    }
    audio_pkt_data = pkt.data;
    audio_pkt_size = pkt.size;
  }
}

L'ensemble du processus commence en fait vers la fin de la fonction, où nous appelons packet_queue_get (). Nous prenons le paquet de la file d'attente et en sauvegardons les informations. Ensuite, lorsque nous avons le package à travailler, nous appelons avcodec_decode_audio4 (), qui est très similaire à sa fonction sœur avcodec_decode_video (), sauf que dans ce cas, le package peut avoir plus d'une image. Par conséquent, vous devrez peut-être l'appeler plusieurs fois pour obtenir toutes les données du paquet. Après avoir reçu la trame, nous la copions simplement dans notre tampon audio, en nous assurant que data_size est plus petit que notre tampon audio. N'oubliez pas non plus de diffuser audio_bufau type correct, car SDL donne un tampon int 8 bits et FFmpeg nous donne des données dans un tampon int 16 bits. Vous devez également prendre en compte la différence entre len1 et data_size . len1 est la taille du package que nous avons utilisé, et data_size est la quantité de données brutes retournées.

Lorsque nous avons des données, nous revenons immédiatement pour savoir si nous devons obtenir plus de données de la file d'attente ou nous avons terminé. Si nous devons encore traiter le colis, respectez-le. Si vous avez terminé le package, libérez-le enfin.

Et c'est tout! Nous avons transféré l'audio de la boucle de lecture principale vers la file d'attente, qui est ensuite lue par la fonction audio_callback, qui transfère ces données vers le SDL, et le SDL les transfère vers votre carte son. Allez-y et compilez:

gcc -o tutorial03 tutorial03.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

Gip-gip-hourra! La vidéo est toujours diffusée à la vitesse maximale, mais le son est déjà lu comme il se doit. Pourquoi donc? Oui, car les informations audio ont une fréquence d'échantillonnage - nous pompons les informations audio aussi vite qu'elles se révèlent, mais l'audio est simplement lu dans ce flux en fonction de sa fréquence d'échantillonnage.

Nous sommes presque mûrs pour la synchronisation vidéo et audio, mais nous devons d'abord effectuer une petite réorganisation du programme. La méthode de mise en file d'attente du son et de lecture à l'aide d'un flux séparé a très bien fonctionné: elle a rendu le code plus facile à gérer et plus modulaire. Avant de commencer à synchroniser la vidéo avec l'audio, nous devons simplifier le code. Dans la prochaine série, nous produirons des flux de contrôle!






Leçon 4: plusieurs threads


Liste complète tutorial04.c
// tutorial04.c
// A pedagogical video player that will stream through every video frame as fast as it can,
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial04 tutorial04.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>
#include <math.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)

#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)

#define VIDEO_PICTURE_QUEUE_SIZE 1

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;


typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
} VideoPicture;

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVFrame         audio_frame;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;
  struct SwsContext *sws_ctx;

  VideoPicture    pictq[VIDEO_PICTURE_QUEUE_SIZE];
  int             pictq_size, pictq_rindex, pictq_windex;
  SDL_mutex       *pictq_mutex;
  SDL_cond        *pictq_cond;
  
  SDL_Thread      *parse_tid;
  SDL_Thread      *video_tid;

  char            filename[1024];
  int             quit;
} VideoState;

SDL_Surface     *screen;
SDL_mutex       *screen_mutex;

/* Since we only have one decoding thread, the Big Struct
   can be global in case we need it. */
VideoState *global_video_state;

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  SDL_LockMutex(q->mutex);

  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;

  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(global_video_state->quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size) {

  int len1, data_size = 0;
  AVPacket *pkt = &is->audio_pkt;

  for(;;) {
    while(is->audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	is->audio_pkt_size = 0;
	break;
      }
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;
      is->audio_pkt_size -= len1;
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt->data)
      av_free_packet(pkt);

    if(is->quit) {
      return -1;
    }
    /* next packet */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
      return -1;
    }
    is->audio_pkt_data = pkt->data;
    is->audio_pkt_size = pkt->size;
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  VideoState *is = (VideoState *)userdata;
  int len1, audio_size;

  while(len > 0) {
    if(is->audio_buf_index >= is->audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf));
      if(audio_size < 0) {
	/* If error, output silence */
	is->audio_buf_size = 1024;
	memset(is->audio_buf, 0, is->audio_buf_size);
      } else {
	is->audio_buf_size = audio_size;
      }
      is->audio_buf_index = 0;
    }
    len1 = is->audio_buf_size - is->audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;
    stream += len1;
    is->audio_buf_index += len1;
  }
}

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_ctx->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
	is->video_ctx->width / is->video_ctx->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_ctx->width /
	(float)is->video_ctx->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);

  }
}

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];
      /* Now, normally here goes a ton of code
	 about timing, etc. we're just going to
	 guess at a delay for now. You can
	 increase and decrease this value and hard code
	 the timing - but I don't suggest that ;)
	 We'll learn how to do it for real later.
      */
      schedule_refresh(is, 40);
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}
      
void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
				 is->video_ctx->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);

  vp->width = is->video_ctx->width;
  vp->height = is->video_ctx->height;
  vp->allocated = 1;

}

int queue_picture(VideoState *is, AVFrame *pFrame) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_ctx->width ||
     vp->height != is->video_ctx->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_ctx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    // Decode video frame
    avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);
    // Did we get a video frame?
    if(frameFinished) {
      if(queue_picture(is, pFrame) < 0) {
	break;
      }      
    }
    av_free_packet(packet);
  }
  av_frame_free(&pFrame);
  return 0;
}

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx = NULL;
  AVCodec *codec = NULL;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codecCtx->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
				 is->video_ctx->pix_fmt, is->video_ctx->width,
				 is->video_ctx->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

int decode_thread(void *arg) {

  VideoState *is = (VideoState *)arg;
  AVFormatContext *pFormatCtx;
  AVPacket pkt1, *packet = &pkt1;

  int video_index = -1;
  int audio_index = -1;
  int i;

  is->videoStream=-1;
  is->audioStream=-1;

  global_video_state = is;

  // Open video file
  if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
    return -1; // Couldn't open file

  is->pFormatCtx = pFormatCtx;
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, is->filename, 0);
  
  // Find the first video stream

  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       video_index < 0) {
      video_index=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audio_index < 0) {
      audio_index=i;
    }
  }
  if(audio_index >= 0) {
    stream_component_open(is, audio_index);
  }
  if(video_index >= 0) {
    stream_component_open(is, video_index);
  }   

  if(is->videoStream < 0 || is->audioStream < 0) {
    fprintf(stderr, "%s: could not open codecs\n", is->filename);
    goto fail;
  }

  // main decode loop

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if(is->pFormatCtx->pb->error == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }
  /* all done - wait for it */
  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;
}

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
        screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }

  screen_mutex = SDL_CreateMutex();

  av_strlcpy(is->filename, argv[1], sizeof(is->filename));

  is->pictq_mutex = SDL_CreateMutex();
  is->pictq_cond = SDL_CreateCond();

  schedule_refresh(is, 40);

  is->parse_tid = SDL_CreateThread(decode_thread, is);
  if(!is->parse_tid) {
    av_free(is);
    return -1;
  }
  for(;;) {

    SDL_WaitEvent(&event);
    switch(event.type) {
    case FF_QUIT_EVENT:
    case SDL_QUIT:
      is->quit = 1;
      SDL_Quit();
      return 0;
      break;
    case FF_REFRESH_EVENT:
      video_refresh_timer(event.user.data1);
      break;
    default:
      break;
    }
  }
  return 0;

}

Aperçu


La dernière fois, nous avons ajouté la prise en charge audio à l'aide des fonctionnalités audio SDL. SDL a lancé un thread effectuant des rappels pour la fonction que nous avons définie chaque fois qu'un son était nécessaire. Maintenant, nous allons faire de même avec l'affichage vidéo. Cela rend le code plus modulaire et plus facile à utiliser, surtout si vous souhaitez ajouter la synchronisation. Alors par où commencer?

Notez que notre fonction principale gère beaucoup: elle passe par la boucle d'événement, lit les paquets et décode la vidéo. Ce que nous allons faire, c'est tout diviser en parties: nous aurons un flux responsable du décodage des paquets; puis ces paquets sont ajoutés à la file d'attente et lus par les flux audio et vidéo correspondants. Nous avons déjà réglé le flux audio comme requis; avec un flux vidéo, ce sera un peu plus difficile, car nous devrons nous assurer que la vidéo est diffusée par nous-mêmes. Nous ajouterons le code d'affichage réel à la boucle principale. Mais au lieu d'afficher la vidéo à chaque exécution de la boucle, nous intégrons l'affichage vidéo dans la boucle d'événement. L'idée est de décoder la vidéo, d'enregistrer l'image reçue dans une autre file d'attente, puis de créer votre propre événement ( FF_REFRESH_EVENT), que nous ajoutons au système d'événements, puis lorsque notre boucle d'événements voit cet événement, il affiche la trame suivante dans la file d'attente. Voici une illustration ASCII pratique de ce qui se passe:


La principale raison du déplacement du contrôle d'affichage vidéo à travers la boucle d'événement est qu'avec le flux SDL_Delay , nous pouvons contrôler avec précision quand la prochaine image vidéo apparaît à l'écran. Lorsque nous synchroniserons enfin la vidéo dans la leçon suivante, ajoutez simplement un code qui planifiera la prochaine mise à jour vidéo afin que l'image correcte apparaisse à l'écran au bon moment.

Simplifiez le code


Effaçons un peu le code. Nous avons toutes ces informations sur les codecs audio et vidéo, et nous allons ajouter des files d'attente, des tampons, et Dieu sait quoi d'autre. Toutes ces choses sont pour une certaine unité logique, à savoir - pour le film. Nous avons donc l'intention de créer une grande structure contenant toutes ces informations appelée VideoState .

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;

  VideoPicture    pictq[VIDEO_PICTURE_QUEUE_SIZE];
  int             pictq_size, pictq_rindex, pictq_windex;
  SDL_mutex       *pictq_mutex;
  SDL_cond        *pictq_cond;
  
  SDL_Thread      *parse_tid;
  SDL_Thread      *video_tid;

  char            filename[1024];
  int             quit;
} VideoState;

Ici, nous voyons des indices de ce que nous allons obtenir à la fin. Tout d'abord, nous voyons les informations de base - le contexte du format et des indices du flux audio et vidéo, ainsi que les objets AVStream correspondants . Ensuite, nous voyons que certains de ces tampons audio sont déplacés vers cette structure. Ils ( audio_buf , audio_buf_size , etc.) étaient destinés à des informations sur l'audio qui était toujours là (ou manquait). Nous avons ajouté une autre file d'attente pour la vidéo et un tampon (qui sera utilisé comme file d'attente; pour cela, nous n'avons pas besoin de files d'attente extravagantes) pour les images décodées (enregistrées en superposition). Structure de VideoPicture- c'est notre propre création (nous verrons ce qu'il y aura quand nous y arriverons). Vous pouvez également remarquer que nous avons alloué des pointeurs pour deux flux supplémentaires que nous allons créer, ainsi qu'un indicateur de sortie et un nom de fichier de film.

Donc, maintenant nous revenons à la fonction principale afin de voir comment cela change notre programme. Configurons notre structure VideoState :

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

av_mallocz () est une bonne fonction qui va nous allouer de la mémoire et la mettre à zéro.

Ensuite, nous initialisons nos verrous pour le tampon d'affichage ( pictq ), car puisque la boucle d'événement appelle notre fonction d'affichage - rappelez-vous, la fonction d'affichage récupérera les images pré-décodées de pictq . Dans le même temps, notre décodeur vidéo y mettra des informations - nous ne savons pas qui y arrivera en premier. J'espère que vous comprenez qu'il s'agit d'une condition de course classique. Par conséquent, nous le distribuons maintenant avant de commencer tout sujet. Copions également le nom de notre film dans VideoState :

av_strlcpy(is->filename, argv[1], sizeof(is->filename));

is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();

av_strlcpy est une fonction de FFmpeg qui effectue des vérifications de bordure supplémentaires en plus de strncpy .

Notre premier fil


Lançons nos discussions et faisons quelque chose de réel:

schedule_refresh(is, 40);

is->parse_tid = SDL_CreateThread(decode_thread, is);
if(!is->parse_tid) {
  av_free(is);
  return -1;
}

Schedule_refresh est une fonction que nous définirons plus tard. Ce qu'elle fait, c'est dire au système de produire FF_REFRESH_EVENT après le nombre de millisecondes spécifié. Ceci, à son tour, appellera la fonction de mise à jour vidéo lorsque nous la verrons dans la file d'attente des événements. Mais maintenant, regardons SDL_CreateThread ().

SDL_CreateThread () fait exactement cela - il génère un nouveau thread qui a un accès complet à toute la mémoire du processus d'origine, et démarre le thread exécuté par la fonction que nous lui donnons. Cette fonction transmettra également des données définies par l'utilisateur. Dans ce cas, nous appelons decode_thread () et attachons notre structure VideoState. Il n'y a rien de nouveau dans la première moitié de la fonction; il fait juste le travail d'ouvrir le fichier et de trouver l'index des flux audio et vidéo. La seule chose que nous faisons différemment est de conserver le contexte de format dans notre grande structure. Après avoir trouvé nos index de flux, nous appelons une autre fonction que nous définissons, stream_component_open (). C'est une façon assez naturelle de séparer, et comme nous faisons beaucoup de choses similaires pour configurer le codec vidéo et audio, nous réutilisons du code, ce qui en fait une fonction.

Fonction Stream_component_open() Est l'endroit où nous découvrons notre décodeur de codec, configurons les paramètres sonores, enregistrons des informations importantes dans notre grande structure et lançons nos flux audio et vidéo. Ici, nous insérons également d'autres paramètres, tels que l'utilisation forcée du codec au lieu de sa détection automatique, etc. Comme ça:

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx;
  AVCodec *codec;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    /* ...etc... */
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;
    
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_st->codec->width, is->video_st->codec->height,
				 is->video_st->codec->pix_fmt, is->video_st->codec->width,
				 is->video_st->codec->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

C'est presque le même que le code que nous avions avant, sauf qu'il est maintenant généralisé pour l'audio et la vidéo. Notez qu'au lieu de aCodecCtx, nous avons configuré notre grande structure en tant que données utilisateur pour notre rappel audio. Nous avons également enregistré les flux eux-mêmes en tant que audio_st et video_st . Nous avons également ajouté notre file d'attente vidéo et l'avons configurée comme notre file d'attente audio. L'essentiel est d'exécuter des flux vidéo et audio. Ces bits font ceci:

    SDL_PauseAudio(0);
    break;

/* ...... */

    is->video_tid = SDL_CreateThread(video_thread, is);

N'oubliez pas SDL_PauseAudio () de la dernière leçon. SDL_CreateThread () est utilisé de la même manière. Revenons à notre fonction video_thread ().

Avant cela, revenons à la seconde moitié de notre fonction decode_thread (). Essentiellement, c'est juste une boucle for qui lit un paquet et le place dans la bonne file d'attente:

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if((is->pFormatCtx->pb->error) == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }

Il n'y a rien de vraiment nouveau ici, sauf que nous avons maintenant une taille maximale pour notre file d'attente audio et vidéo, et nous avons ajouté une vérification des erreurs de lecture. Le contexte de format a une structure ByteIOContext à l' intérieur appelée pb . ByteIOContext est une structure qui stocke essentiellement toutes les informations sur les fichiers de bas niveau.

Après notre boucle for, nous avons tout le code pour attendre que le reste du programme se termine ou pour en informer. Ce code est instructif car il montre comment nous poussons les événements - quelque chose dont nous aurons besoin plus tard pour afficher la vidéo:

  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;

Nous obtenons des valeurs pour les événements personnalisés à l'aide de la constante SDL SDL_USEREVENT . Le premier événement utilisateur doit être défini sur SDL_USEREVENT , le prochain SDL_USEREVENT + 1 , etc. FF_QUIT_EVENT est défini dans notre programme comme SDL_USEREVENT + 1 . Nous pouvons également transmettre des données utilisateur si nécessaire, et ici nous passons notre pointeur à une grande structure. Enfin, nous appelons SDL_PushEvent (). Dans notre commutateur de boucle d'événement, nous venons de mettre cela dans la section SDL_QUIT_EVENTque nous avions avant. Nous verrons notre cycle d'événements plus en détail; pour l'instant, assurez-vous simplement que lorsque nous appuyez sur FF_QUIT_EVENT, nous l' attrapons plus tard et basculons le drapeau de sortie.

Image de réception: video_thread


Après avoir préparé le codec, vous pouvez démarrer le flux vidéo. Ce flux lit les paquets de la file d'attente vidéo, décode la vidéo en images, puis appelle la fonction queue_picture pour placer l'image traitée dans la file d'attente d'images:

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    // Decode video frame
    avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);

    // Did we get a video frame?
    if(frameFinished) {
      if(queue_picture(is, pFrame) < 0) {
	break;
      }
    }
    av_free_packet(packet);
  }
  av_free(pFrame);
  return 0;
}

La plupart de cette fonction doit être comprise maintenant. Nous venons de copier la fonction avcodec_decode_video2 ici , en remplaçant simplement certains arguments; par exemple, nous avons un AVStream stocké dans notre grande structure, nous obtenons donc notre codec à partir de là. Nous continuons simplement à recevoir des paquets de notre file d'attente vidéo jusqu'à ce que quelqu'un nous dise de quitter ou que nous trouvions une erreur.

Cadre de file d'attente


Jetons un coup d'œil à la fonction qui stocke notre pFrame décodé dans notre file d'attente d'images. Étant donné que notre file d'attente d'images est une superposition de SDL (vraisemblablement pour permettre à la fonction d'affichage vidéo d'effectuer le moins de calcul possible), nous devons convertir notre image en elle. Les données que nous stockons dans la file d'attente d'images sont la structure que nous avons créée:

typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
} VideoPicture;

Notre grande structure contient un tampon de ces fichiers, où nous pouvons les stocker. Cependant, nous devons distribuer SDL_Overlay nous-mêmes (faites attention au drapeau attribué, qui indique si nous l'avons fait ou non).

Pour utiliser cette file d'attente, nous avons deux pointeurs - l'index d'écriture et l'index de lecture. Nous suivons également le nombre d'images réelles dans le tampon. Pour écrire dans la file d'attente, nous attendons d'abord que notre tampon soit effacé, afin d'avoir un endroit pour stocker notre VideoPicture . Ensuite, nous vérifions si nous avons défini la superposition dans notre index d'enregistrement? Sinon, vous devez allouer de la mémoire. Nous devons également réallouer le tampon si la taille de la fenêtre a changé!

int queue_picture(VideoState *is, AVFrame *pFrame) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_st->codec->width ||
     vp->height != is->video_st->codec->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

Jetons un œil à la fonction alloc_picture ():

void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width,
				 is->video_st->codec->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);
  vp->width = is->video_st->codec->width;
  vp->height = is->video_st->codec->height;  
  vp->allocated = 1;
}

Vous devez reconnaître la fonction SDL_CreateYUVOverlay , que nous avons déplacée de notre boucle principale vers cette section. Ce code devrait être raisonnablement clair maintenant. Cependant, nous avons maintenant un verrou mutex, car deux threads ne peuvent pas simultanément écrire des informations sur l'écran! Cela ne permettra pas à notre fonction alloc_picture d' interférer avec une autre fonction qui affichera l'image. (Nous avons créé ce verrou en tant que variable globale et l'avons initialisé dans main (); voir le code.) N'oubliez pas que nous conservons la largeur et la hauteur dans la structure VideoPicture , car nous devons nous assurer que la taille de notre vidéo ne change pas pour une raison quelconque.
Ok, nous l'avons réglé, et nous avons notre superposition YUV, dédié et prêt à recevoir l'image. Revenons à queue_picture et regardons le code pour copier le cadre dans la superposition. Cette partie devrait vous être familière:

int queue_picture(VideoState *is, AVFrame *pFrame) {

  /* Allocate a frame if we need it... */
  /* ... */
  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_st->codec->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

Ici, la majeure partie est juste le code que nous avons utilisé plus tôt pour remplir la superposition YUV avec notre cadre. Le dernier bit «ajoute» simplement notre valeur à la file d'attente. La file d'attente fonctionne, des valeurs y sont ajoutées jusqu'à ce qu'elle soit pleine, et sa lecture se produit tant qu'elle contient au moins quelque chose. Par conséquent, tout dépend de la valeur de is -> pictq_size , ce qui nous oblige à le bloquer. Alors, que faisons-nous ici: augmenter le pointeur d'enregistrement (et si nécessaire, recommencer), puis bloquer la file d'attente et augmenter sa taille. Maintenant, notre lecteur saura qu'il y a plus d'informations sur la file d'attente, et si cela rend notre file d'attente pleine, et notre enregistreur le saura.

Affichage vidéo


C'est tout pour notre fil vidéo! Maintenant, nous avons terminé tous les threads gratuits, sauf un - rappelez-vous comment nous avons appelé la fonction schedule_refresh () il y a longtemps ? Jetez un œil à ce qui s'est réellement passé:

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

SDL_AddTimer () est une fonction SDL qui effectue simplement un rappel vers une fonction définie par l'utilisateur après un certain nombre de millisecondes (et, si nécessaire, transfère certaines données définies par l'utilisateur). Nous utiliserons cette fonction pour planifier des mises à jour vidéo - chaque fois que nous l'appellerons, elle définira une minuterie qui déclenchera un événement, ce qui, à son tour, amènera notre fonction main () à appeler une fonction qui extrait une image de notre image de file d'attente et affiche sa! Phew! Trois «qui / quoi / qui» en une phrase! Alors, faisons la première chose à faire: déclencher cet événement. Cela nous envoie à:

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

L'événement est lancé par notre vieil ami. FF_REFRESH_EVENT est défini ici comme SDL_USEREVENT + 1 . Il convient de noter que lorsque nous renvoyons 0, le SDL arrête le temporisateur, de sorte que le rappel ne s'exécute pas à nouveau.

Maintenant que nous avons de nouveau appelé FF_REFRESH_EVENT , nous devons le traiter dans notre boucle d'événements:

for(;;) {

  SDL_WaitEvent(&event);
  switch(event.type) {
  /* ... */
  case FF_REFRESH_EVENT:
    video_refresh_timer(event.user.data1);
    break;

ce qui nous envoie ici à cette fonction, qui extrait en fait les données de notre file d'attente d'images:

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];
      /* Timing code goes here */

      schedule_refresh(is, 80);
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}

Pour le moment, cette fonction est assez simple: elle traite la file d'attente pendant que nous avons quelque chose, définit une minuterie pour afficher la prochaine image vidéo, appelle video_display pour afficher la vidéo à l'écran, puis augmente le compteur dans la file d'attente, tout en réduisant sa taille. Vous remarquerez peut-être que nous ne faisons rien avec vp dans cette fonction, et voici pourquoi: c'est en avance. Mais un peu plus tard. Nous allons l'utiliser pour accéder aux informations de temps lorsque nous commencerons à synchroniser la vidéo avec l'audio. Ici, jetez un oeil à l'endroit dans le code où le commentaire «Le code de synchronisation va ici» est écrit. Dans cette section, nous allons découvrir dans combien de temps nous devons afficher la prochaine image vidéo, puis entrer cette valeur dans la fonction schedule_refresh(). Pour le moment, nous entrons simplement une valeur fictive de 80. Techniquement, vous pouvez deviner et vérifier cette valeur et la recompiler pour chaque film, mais: 1) elle commencera à ralentir après un certain temps et 2) c'est assez stupide. Bien que, à l'avenir, nous reviendrons sur ce point.

On a presque fini. Il ne reste qu'une chose à faire: montrer la vidéo! Voici la fonction video_display :

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_st->codec->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) *
	is->video_st->codec->width / is->video_st->codec->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_st->codec->width /
	(float)is->video_st->codec->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);
  }
}

Étant donné que l'écran peut être de n'importe quelle taille (nous avons installé 640x480 et qu'il existe des moyens de le configurer pour que l'utilisateur redimensionne), vous devez déterminer dynamiquement la taille de la zone rectangulaire pour notre film. Donc, vous devez d'abord connaître le rapport d'aspect de notre film, juste la largeur divisée par la hauteur. Certains codecs auront un rapport d'aspect impair de l'échantillon, qui est simplement la largeur / hauteur d'un pixel ou d'un échantillon. Étant donné que les valeurs de hauteur et de largeur dans notre contexte de codec sont mesurées en pixels, le rapport d'aspect réel est égal au rapport d'aspect multiplié par le rapport d'aspect de l'échantillon. Certains codecs affichent un rapport d'aspect de 0, ce qui signifie que chaque pixel a simplement une taille de 1x1. Ensuite, nous mettons le film à l'échelle de telle manièreafin qu'il s'adapte autant que possible à l'écran. Inversion de bits& -3 arrondit simplement la valeur au multiple de quatre le plus proche. Centrez ensuite le film et appelez SDL_DisplayYUVOverlay () pour vous assurer que le mutex d'écran est utilisé pour y accéder.

Et c'est tout? Avons-nous fini? Vous devez toujours réécrire le code audio pour utiliser le nouveau VideoStruct , mais ce sont des changements triviaux qui peuvent être vus dans l'exemple de code. La dernière chose que nous devons faire est de changer notre rappel pour la fonction de rappel de sortie interne dans FFmpeg:

VideoState *global_video_state;

int decode_interrupt_cb(void) {
  return (global_video_state && global_video_state->quit);
}

Définissez global_video_state sur une grande structure dans main ().

Alors c'est tout! Nous compilons:

gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

et profitez du film sans synchronisation! Dans la prochaine étape, nous allons enfin créer un lecteur vidéo vraiment fonctionnel !






Leçon 5: Synchronisation vidéo


Liste complète tutorial05.c
// tutorial05.c
// A pedagogical video player that really works!
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard, 
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial05 tutorial05.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed, 
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include <SDL.h>
#include <SDL_thread.h>

#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif

#include <stdio.h>
#include <assert.h>
#include <math.h>

// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif

#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000

#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)

#define AV_SYNC_THRESHOLD 0.01
#define AV_NOSYNC_THRESHOLD 10.0

#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)

#define VIDEO_PICTURE_QUEUE_SIZE 1

typedef struct PacketQueue {
  AVPacketList *first_pkt, *last_pkt;
  int nb_packets;
  int size;
  SDL_mutex *mutex;
  SDL_cond *cond;
} PacketQueue;


typedef struct VideoPicture {
  SDL_Overlay *bmp;
  int width, height; /* source height & width */
  int allocated;
  double pts;
} VideoPicture;

typedef struct VideoState {

  AVFormatContext *pFormatCtx;
  int             videoStream, audioStream;

  double          audio_clock;
  AVStream        *audio_st;
  AVCodecContext  *audio_ctx;
  PacketQueue     audioq;
  uint8_t         audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
  unsigned int    audio_buf_size;
  unsigned int    audio_buf_index;
  AVFrame         audio_frame;
  AVPacket        audio_pkt;
  uint8_t         *audio_pkt_data;
  int             audio_pkt_size;
  int             audio_hw_buf_size;  
  double          frame_timer;
  double          frame_last_pts;
  double          frame_last_delay;
  double          video_clock; ///<pts of last decoded frame / predicted pts of next decoded frame
  AVStream        *video_st;
  AVCodecContext  *video_ctx;
  PacketQueue     videoq;
  struct SwsContext *sws_ctx;

  VideoPicture    pictq[VIDEO_PICTURE_QUEUE_SIZE];
  int             pictq_size, pictq_rindex, pictq_windex;
  SDL_mutex       *pictq_mutex;
  SDL_cond        *pictq_cond;
  
  SDL_Thread      *parse_tid;
  SDL_Thread      *video_tid;

  char            filename[1024];
  int             quit;
} VideoState;

SDL_Surface     *screen;
SDL_mutex       *screen_mutex;

/* Since we only have one decoding thread, the Big Struct
   can be global in case we need it. */
VideoState *global_video_state;

void packet_queue_init(PacketQueue *q) {
  memset(q, 0, sizeof(PacketQueue));
  q->mutex = SDL_CreateMutex();
  q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {

  AVPacketList *pkt1;
  if(av_dup_packet(pkt) < 0) {
    return -1;
  }
  pkt1 = av_malloc(sizeof(AVPacketList));
  if (!pkt1)
    return -1;
  pkt1->pkt = *pkt;
  pkt1->next = NULL;
  
  SDL_LockMutex(q->mutex);

  if (!q->last_pkt)
    q->first_pkt = pkt1;
  else
    q->last_pkt->next = pkt1;
  q->last_pkt = pkt1;
  q->nb_packets++;
  q->size += pkt1->pkt.size;
  SDL_CondSignal(q->cond);
  
  SDL_UnlockMutex(q->mutex);
  return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
  AVPacketList *pkt1;
  int ret;

  SDL_LockMutex(q->mutex);
  
  for(;;) {
    
    if(global_video_state->quit) {
      ret = -1;
      break;
    }

    pkt1 = q->first_pkt;
    if (pkt1) {
      q->first_pkt = pkt1->next;
      if (!q->first_pkt)
	q->last_pkt = NULL;
      q->nb_packets--;
      q->size -= pkt1->pkt.size;
      *pkt = pkt1->pkt;
      av_free(pkt1);
      ret = 1;
      break;
    } else if (!block) {
      ret = 0;
      break;
    } else {
      SDL_CondWait(q->cond, q->mutex);
    }
  }
  SDL_UnlockMutex(q->mutex);
  return ret;
}

double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_ctx->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_ctx->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size, double *pts_ptr) {

  int len1, data_size = 0;
  AVPacket *pkt = &is->audio_pkt;
  double pts;
  int n;

  for(;;) {
    while(is->audio_pkt_size > 0) {
      int got_frame = 0;
      len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
      if(len1 < 0) {
	/* if error, skip frame */
	is->audio_pkt_size = 0;
	break;
      }
      data_size = 0;
      if(got_frame) {
	data_size = av_samples_get_buffer_size(NULL, 
					       is->audio_ctx->channels,
					       is->audio_frame.nb_samples,
					       is->audio_ctx->sample_fmt,
					       1);
	assert(data_size <= buf_size);
	memcpy(audio_buf, is->audio_frame.data[0], data_size);
      }
      is->audio_pkt_data += len1;
      is->audio_pkt_size -= len1;
      if(data_size <= 0) {
	/* No data yet, get more frames */
	continue;
      }
      pts = is->audio_clock;
      *pts_ptr = pts;
      n = 2 * is->audio_ctx->channels;
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_ctx->sample_rate);
      /* We have data, return it and come back for more later */
      return data_size;
    }
    if(pkt->data)
      av_free_packet(pkt);

    if(is->quit) {
      return -1;
    }
    /* next packet */
    if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
      return -1;
    }
    is->audio_pkt_data = pkt->data;
    is->audio_pkt_size = pkt->size;
    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE) {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }
  }
}

void audio_callback(void *userdata, Uint8 *stream, int len) {

  VideoState *is = (VideoState *)userdata;
  int len1, audio_size;
  double pts;

  while(len > 0) {
    if(is->audio_buf_index >= is->audio_buf_size) {
      /* We have already sent all our data; get more */
      audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf), &pts);
      if(audio_size < 0) {
	/* If error, output silence */
	is->audio_buf_size = 1024;
	memset(is->audio_buf, 0, is->audio_buf_size);
      } else {
	is->audio_buf_size = audio_size;
      }
      is->audio_buf_index = 0;
    }
    len1 = is->audio_buf_size - is->audio_buf_index;
    if(len1 > len)
      len1 = len;
    memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
    len -= len1;
    stream += len1;
    is->audio_buf_index += len1;
  }
}

static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
  SDL_Event event;
  event.type = FF_REFRESH_EVENT;
  event.user.data1 = opaque;
  SDL_PushEvent(&event);
  return 0; /* 0 means stop timer */
}

/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
  SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}

void video_display(VideoState *is) {

  SDL_Rect rect;
  VideoPicture *vp;
  float aspect_ratio;
  int w, h, x, y;
  int i;

  vp = &is->pictq[is->pictq_rindex];
  if(vp->bmp) {
    if(is->video_ctx->sample_aspect_ratio.num == 0) {
      aspect_ratio = 0;
    } else {
      aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
	is->video_ctx->width / is->video_ctx->height;
    }
    if(aspect_ratio <= 0.0) {
      aspect_ratio = (float)is->video_ctx->width /
	(float)is->video_ctx->height;
    }
    h = screen->h;
    w = ((int)rint(h * aspect_ratio)) & -3;
    if(w > screen->w) {
      w = screen->w;
      h = ((int)rint(w / aspect_ratio)) & -3;
    }
    x = (screen->w - w) / 2;
    y = (screen->h - h) / 2;
    
    rect.x = x;
    rect.y = y;
    rect.w = w;
    rect.h = h;
    SDL_LockMutex(screen_mutex);
    SDL_DisplayYUVOverlay(vp->bmp, &rect);
    SDL_UnlockMutex(screen_mutex);

  }
}

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];

      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
      if(delay <= 0 || delay >= 1.0) {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

      /* update delay to sync to audio */
      ref_clock = get_audio_clock(is);
      diff = vp->pts - ref_clock;

      /* Skip or repeat the frame. Take delay into account
	 FFPlay still doesn't "know if this is the best guess." */
      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
	if(diff <= -sync_threshold) {
	  delay = 0;
	} else if(diff >= sync_threshold) {
	  delay = 2 * delay;
	}
      }
      is->frame_timer += delay;
      /* computer the REAL delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
      if(actual_delay < 0.010) {
	/* Really it should skip the picture instead */
	actual_delay = 0.010;
      }
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
      
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}
      
void alloc_picture(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;

  vp = &is->pictq[is->pictq_windex];
  if(vp->bmp) {
    // we already have one make another, bigger/smaller
    SDL_FreeYUVOverlay(vp->bmp);
  }
  // Allocate a place to put our YUV image on that screen
  SDL_LockMutex(screen_mutex);
  vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
				 is->video_ctx->height,
				 SDL_YV12_OVERLAY,
				 screen);
  SDL_UnlockMutex(screen_mutex);

  vp->width = is->video_ctx->width;
  vp->height = is->video_ctx->height;
  vp->allocated = 1;

}

int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {

  VideoPicture *vp;
  int dst_pix_fmt;
  AVPicture pict;

  /* wait until we have space for a new pic */
  SDL_LockMutex(is->pictq_mutex);
  while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
	!is->quit) {
    SDL_CondWait(is->pictq_cond, is->pictq_mutex);
  }
  SDL_UnlockMutex(is->pictq_mutex);

  if(is->quit)
    return -1;

  // windex is set to 0 initially
  vp = &is->pictq[is->pictq_windex];

  /* allocate or resize the buffer! */
  if(!vp->bmp ||
     vp->width != is->video_ctx->width ||
     vp->height != is->video_ctx->height) {
    SDL_Event event;

    vp->allocated = 0;
    alloc_picture(is);
    if(is->quit) {
      return -1;
    }
  }

  /* We have a place to put our picture on the queue */

  if(vp->bmp) {

    SDL_LockYUVOverlay(vp->bmp);
    vp->pts = pts;
    
    dst_pix_fmt = PIX_FMT_YUV420P;
    /* point pict at the queue */

    pict.data[0] = vp->bmp->pixels[0];
    pict.data[1] = vp->bmp->pixels[2];
    pict.data[2] = vp->bmp->pixels[1];
    
    pict.linesize[0] = vp->bmp->pitches[0];
    pict.linesize[1] = vp->bmp->pitches[2];
    pict.linesize[2] = vp->bmp->pitches[1];
    
    // Convert the image into YUV format that SDL uses
    sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
	      pFrame->linesize, 0, is->video_ctx->height,
	      pict.data, pict.linesize);
    
    SDL_UnlockYUVOverlay(vp->bmp);
    /* now we inform our display thread that we have a pic ready */
    if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
      is->pictq_windex = 0;
    }
    SDL_LockMutex(is->pictq_mutex);
    is->pictq_size++;
    SDL_UnlockMutex(is->pictq_mutex);
  }
  return 0;
}

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

  double frame_delay;

  if(pts != 0) {
    /* if we have pts, set video clock to it */
    is->video_clock = pts;
  } else {
    /* if we aren't given a pts, set it to the clock */
    pts = is->video_clock;
  }
  /* update the video clock */
  frame_delay = av_q2d(is->video_ctx->time_base);
  /* if we are repeating a frame, adjust clock accordingly */
  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  is->video_clock += frame_delay;
  return pts;
}

int video_thread(void *arg) {
  VideoState *is = (VideoState *)arg;
  AVPacket pkt1, *packet = &pkt1;
  int frameFinished;
  AVFrame *pFrame;
  double pts;

  pFrame = av_frame_alloc();

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    pts = 0;

    // Decode video frame
    avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);

    if((pts = av_frame_get_best_effort_timestamp(pFrame)) == AV_NOPTS_VALUE) {
      pts = 0;
    }
    pts *= av_q2d(is->video_st->time_base);

    // Did we get a video frame?
    if(frameFinished) {
      pts = synchronize_video(is, pFrame, pts);
      if(queue_picture(is, pFrame, pts) < 0) {
	break;
      }
    }
    av_free_packet(packet);
  }
  av_frame_free(&pFrame);
  return 0;
}

int stream_component_open(VideoState *is, int stream_index) {

  AVFormatContext *pFormatCtx = is->pFormatCtx;
  AVCodecContext *codecCtx = NULL;
  AVCodec *codec = NULL;
  SDL_AudioSpec wanted_spec, spec;

  if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
    return -1;
  }

  codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
  if(!codec) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  codecCtx = avcodec_alloc_context3(codec);
  if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1; // Error copying codec context
  }


  if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
    // Set audio settings from codec info
    wanted_spec.freq = codecCtx->sample_rate;
    wanted_spec.format = AUDIO_S16SYS;
    wanted_spec.channels = codecCtx->channels;
    wanted_spec.silence = 0;
    wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
    wanted_spec.callback = audio_callback;
    wanted_spec.userdata = is;
    
    if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
      fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
      return -1;
    }
    is->audio_hw_buf_size = spec.size;
  }
  if(avcodec_open2(codecCtx, codec, NULL) < 0) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }

  switch(codecCtx->codec_type) {
  case AVMEDIA_TYPE_AUDIO:
    is->audioStream = stream_index;
    is->audio_st = pFormatCtx->streams[stream_index];
    is->audio_ctx = codecCtx;
    is->audio_buf_size = 0;
    is->audio_buf_index = 0;
    memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
    packet_queue_init(&is->audioq);
    SDL_PauseAudio(0);
    break;
  case AVMEDIA_TYPE_VIDEO:
    is->videoStream = stream_index;
    is->video_st = pFormatCtx->streams[stream_index];
    is->video_ctx = codecCtx;

    is->frame_timer = (double)av_gettime() / 1000000.0;
    is->frame_last_delay = 40e-3;
    
    packet_queue_init(&is->videoq);
    is->video_tid = SDL_CreateThread(video_thread, is);
    is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
				 is->video_ctx->pix_fmt, is->video_ctx->width,
				 is->video_ctx->height, PIX_FMT_YUV420P,
				 SWS_BILINEAR, NULL, NULL, NULL
				 );
    break;
  default:
    break;
  }
}

int decode_thread(void *arg) {

  VideoState *is = (VideoState *)arg;
  AVFormatContext *pFormatCtx;
  AVPacket pkt1, *packet = &pkt1;

  int video_index = -1;
  int audio_index = -1;
  int i;

  is->videoStream=-1;
  is->audioStream=-1;

  global_video_state = is;

  // Open video file
  if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
    return -1; // Couldn't open file

  is->pFormatCtx = pFormatCtx;
  
  // Retrieve stream information
  if(avformat_find_stream_info(pFormatCtx, NULL)<0)
    return -1; // Couldn't find stream information
  
  // Dump information about file onto standard error
  av_dump_format(pFormatCtx, 0, is->filename, 0);
  
  // Find the first video stream

  for(i=0; i<pFormatCtx->nb_streams; i++) {
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
       video_index < 0) {
      video_index=i;
    }
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
       audio_index < 0) {
      audio_index=i;
    }
  }
  if(audio_index >= 0) {
    stream_component_open(is, audio_index);
  }
  if(video_index >= 0) {
    stream_component_open(is, video_index);
  }   

  if(is->videoStream < 0 || is->audioStream < 0) {
    fprintf(stderr, "%s: could not open codecs\n", is->filename);
    goto fail;
  }

  // main decode loop

  for(;;) {
    if(is->quit) {
      break;
    }
    // seek stuff goes here
    if(is->audioq.size > MAX_AUDIOQ_SIZE ||
       is->videoq.size > MAX_VIDEOQ_SIZE) {
      SDL_Delay(10);
      continue;
    }
    if(av_read_frame(is->pFormatCtx, packet) < 0) {
      if(is->pFormatCtx->pb->error == 0) {
	SDL_Delay(100); /* no error; wait for user input */
	continue;
      } else {
	break;
      }
    }
    // Is this a packet from the video stream?
    if(packet->stream_index == is->videoStream) {
      packet_queue_put(&is->videoq, packet);
    } else if(packet->stream_index == is->audioStream) {
      packet_queue_put(&is->audioq, packet);
    } else {
      av_free_packet(packet);
    }
  }
  /* all done - wait for it */
  while(!is->quit) {
    SDL_Delay(100);
  }

 fail:
  if(1){
    SDL_Event event;
    event.type = FF_QUIT_EVENT;
    event.user.data1 = is;
    SDL_PushEvent(&event);
  }
  return 0;
}

int main(int argc, char *argv[]) {

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

  if(argc < 2) {
    fprintf(stderr, "Usage: test <file>\n");
    exit(1);
  }
  // Register all formats and codecs
  av_register_all();
  
  if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
    fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
    exit(1);
  }

  // Make a screen to put our video
#ifndef __DARWIN__
        screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
        screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
  if(!screen) {
    fprintf(stderr, "SDL: could not set video mode - exiting\n");
    exit(1);
  }

  screen_mutex = SDL_CreateMutex();

  av_strlcpy(is->filename, argv[1], sizeof(is->filename));

  is->pictq_mutex = SDL_CreateMutex();
  is->pictq_cond = SDL_CreateCond();

  schedule_refresh(is, 40);

  is->parse_tid = SDL_CreateThread(decode_thread, is);
  if(!is->parse_tid) {
    av_free(is);
    return -1;
  }
  for(;;) {

    SDL_WaitEvent(&event);
    switch(event.type) {
    case FF_QUIT_EVENT:
    case SDL_QUIT:
      is->quit = 1;
      SDL_Quit();
      return 0;
      break;
    case FF_REFRESH_EVENT:
      video_refresh_timer(event.user.data1);
      break;
    default:
      break;
    }
  }
  return 0;

}

ATTENTION


Quand je viens d'écrire ce guide, tout mon code de synchronisation provenait de la version d'alors de ffplay.c . Aujourd'hui, c'est un programme complètement différent, et les mises à jour dans les bibliothèques FFmpeg (et dans ffplay.c lui-même) ont conduit à des changements fondamentaux. Bien que ce code fonctionne toujours, il est déjà obsolète et de nombreuses autres améliorations pourraient être utilisées dans ce guide.

Comment la vidéo se synchronise


Jusqu'à présent, nous avions un lecteur de film presque inutile. Oui, il lit de la vidéo et oui, il lit de l'audio, mais ce n'est pas tout à fait ce que nous appellerions un film. Alors que faisons-nous alors?

PTS et DTS


Heureusement, les flux audio et vidéo contiennent des informations sur la vitesse et le moment de leur lecture. Les flux audio ont un taux d'échantillonnage et les flux vidéo ont des images par seconde. Cependant, si nous synchronisons simplement la vidéo en comptant le nombre d'images et en multipliant par la fréquence d'images, il y a de fortes chances qu'elle ne se synchronise pas avec le son. Par conséquent, nous irons dans l'autre sens. Packets du flux peuvent avoir les soi-disant temps de décodage timbre ( DTS - de d ecoding t ime s TAMP ) et le temps de présentation tampon ( PTS - de p sentation t ime stamponner ). Pour comprendre ces deux significations, vous devez savoir comment les films sont stockés. Certains formats, tels que MPEG, utilisent ce qu'ils appellent des trames B ( Bed et bidirectionnel, Angleterre. Bidirectionnel ). Deux autres types de trames sont appelées trames-I et des images P ( I est interne , i nner , et P un moyen prédit , p redicted ). Les cadres en I contiennent l'image complète. Cadres en Pdépendent des images I et P précédentes et sont différentes des images précédentes, ou vous pouvez également nommer - deltas. Les trames B sont similaires aux trames P, mais dépendent des informations contenues dans les trames précédentes et suivantes! Le fait qu'un cadre ne contienne pas l'image elle-même, mais des différences avec d'autres cadres - explique pourquoi nous ne pouvons pas avoir un cadre fini après avoir appelé avcodec_decode_video2 .

Disons que nous avons un film dans lequel 4 images dans cette séquence: IBBP . Ensuite, nous devons trouver les informations de la dernière trame P avant de pouvoir afficher l'une des deux trames B précédentes. Pour cette raison, les trames peuvent être stockées dans une séquence qui ne correspond pas à l'ordre d'affichage réel: IPBB. C'est à cela que servent l'horodatage de décodage et l'horodatage de présentation pour chaque image. L'horodatage de décodage nous indique quand nous devons décoder quelque chose, et l'horodatage de présentation nous indique quand nous devons afficher quelque chose. Donc, dans ce cas, notre flux peut ressembler à ceci:

   PTS: 1 4 2 3
   DTS: 1 2 3 4
Stream: IPBB

En règle générale, PTS et DTS sont différents uniquement lorsque le flux en cours de lecture contient des images B.

Lorsque nous recevons un package de av_read_frame (), il contient les valeurs PTS et DTS pour les informations qui se trouvent à l'intérieur du package. Mais ce dont nous avons vraiment besoin, c'est du PTS de notre cadre brut nouvellement décodé, auquel cas nous savons quand il doit être affiché.

Heureusement, FFmpeg nous fournit le «meilleur horodatage possible» que nous pouvons obtenir en utilisant la fonction av_frame_get_best_effort_timestamp ().

Synchronisation


Pour que les images soient affichées tour à tour, il serait bon de savoir quand afficher une image vidéo spécifique. Mais comment le faisons-nous exactement? L'idée est la suivante: après avoir montré l'image, nous déterminons quand l'image suivante doit être affichée. Ensuite, faites une pause, après quoi nous mettons à jour la vidéo après cette période. Comme prévu, nous vérifions la valeur PTS de la trame suivante sur l'horloge système pour voir combien de temps notre temps d'attente devrait être. Cette approche fonctionne, mais deux problèmes doivent être résolus.

Premièrement, la question est, quand sera le prochain PTS? Vous direz que vous pouvez simplement ajouter la fréquence vidéo au PTS actuel - et vous aurez en principe raison. Cependant, certaines variétés de vidéos nécessiteront des images répétées. Cela signifie que vous devez répéter l'image actuelle un certain nombre de fois. Cela peut amener le programme à afficher la trame suivante trop tôt. Il faut en tenir compte.

Le deuxième problème est que, dans le programme que nous avons écrit en ce moment, la vidéo et l'audio se précipitent joyeusement jusqu'à ce qu'ils prennent la peine de se synchroniser. Nous n'aurions pas à nous en préoccuper si tout fonctionnait parfaitement. Mais votre ordinateur n'est pas parfait, tout comme de nombreux fichiers vidéo. Ainsi, nous avons trois options: synchroniser l'audio avec la vidéo, synchroniser la vidéo avec l'audio ou synchroniser l'audio et la vidéo avec une horloge externe (par exemple, avec votre ordinateur). Maintenant, nous allons synchroniser la vidéo avec l'audio.

Codage: réception d'une trame PTS


Écrivons maintenant quelque chose directement. Nous devons ajouter quelques pièces supplémentaires à notre grande structure, et nous le ferons comme nous en avons besoin. Jetons d'abord un coup d'œil à notre fil vidéo. N'oubliez pas qu'ici, nous collectons les paquets qui ont été mis en file d'attente par notre flux de décodage? Dans cette partie du code, nous devons obtenir le PTS pour le cadre que nous a donné avcodec_decode_video2 . La première façon dont nous avons parlé est d'obtenir le DTS du dernier paquet traité, ce qui est assez simple:

  double pts;

  for(;;) {
    if(packet_queue_get(&is->videoq, packet, 1) < 0) {
      // means we quit getting packets
      break;
    }
    pts = 0;
    // Decode video frame
    len1 = avcodec_decode_video2(is->video_st->codec,
                                pFrame, &frameFinished, packet);
    if(packet->dts != AV_NOPTS_VALUE) {
      pts = av_frame_get_best_effort_timestamp(pFrame);
    } else {
      pts = 0;
    }
    pts *= av_q2d(is->video_st->time_base);

Nous mettons le PTS à zéro si nous ne pouvons pas déterminer sa valeur.

Eh bien, c'était facile. Note technique: comme vous pouvez le voir, nous utilisons int64 pour PTS. En effet, le PTS est stocké sous forme d'entier. Cette valeur est un horodatage qui correspond à la dimension de temps dans timebb . Par exemple, si un flux a 24 images par seconde, PTS de 42 indiquera que l'image doit être utilisée là où la 42e image doit être, à condition que nous ayons des images remplacées toutes les 1/24 de seconde (bien sûr, ce ne sera pas nécessairement le cas En réalité).

Nous pouvons convertir cette valeur en secondes en divisant par la fréquence d'images. Valeur Time_basele flux sera égal à 1 divisé par la fréquence d'images (pour le contenu avec une fréquence d'images fixe), donc, pour obtenir le PTS en quelques secondes, nous multiplions par time_base .

Coder davantage: synchronisation et utilisation de PTS


Alors maintenant, nous avons tous les PTS prêts à l'emploi. Nous allons maintenant nous occuper de ces deux problèmes de synchronisation, qui ont été discutés un peu plus haut. Nous allons définir une fonction synchronize_video qui mettra à jour le PTS pour se synchroniser avec tout. Cette fonction, enfin, traitera également les cas où nous n'obtenons pas la valeur PTS pour notre trame. Dans le même temps, nous devons suivre quand la prochaine image est attendue afin que nous puissions correctement définir le taux de rafraîchissement. Nous pouvons le faire en utilisant la valeur interne video_clock , qui suit le temps écoulé pour la vidéo. Nous ajoutons cette valeur à notre grande structure:

typedef struct VideoState {
  double          video_clock; // pts of last decoded frame / predicted pts of next decoded frame

Voici la fonction synchronize_video , qui est assez claire:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

  double frame_delay;

  if(pts != 0) {
    /* if we have pts, set video clock to it */
    is->video_clock = pts;
  } else {
    /* if we aren't given a pts, set it to the clock */
    pts = is->video_clock;
  }
  /* update the video clock */
  frame_delay = av_q2d(is->video_st->codec->time_base);
  /* if we are repeating a frame, adjust clock accordingly */
  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  is->video_clock += frame_delay;
  return pts;
}

Comme vous pouvez le voir, nous prenons en compte les images répétées dans cette fonction.

Maintenant, obtenons notre PTS correct et mettons en file d'attente la trame en utilisant queue_picture en ajoutant un nouvel argument pts :

    // Did we get a video frame?
    if(frameFinished) {
      pts = synchronize_video(is, pFrame, pts);
      if(queue_picture(is, pFrame, pts) < 0) {
	break;
      }
    }

La seule chose qui change dans queue_picture est que nous stockons cette valeur pts dans la structure VideoPicture que nous mettons en file d'attente. Ainsi, nous devons ajouter la variable pts à la structure et ajouter ces lignes de code:

typedef struct VideoPicture {
  ...
  double pts;
}
int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {
  ... stuff ...
  if(vp->bmp) {
    ... convert picture ...
    vp->pts = pts;
    ... alert queue ...
  }

Alors maintenant, nous avons les images en file d'attente avec les valeurs PTS correctes, alors jetons un œil à notre fonctionnalité de mise à jour vidéo. Vous vous souvenez de la dernière leçon que nous l'avons simplement simulé et installé une mise à jour de 80 ms. Eh bien, maintenant nous allons découvrir ce qui devrait vraiment y être.

Notre stratégie consiste à prédire l'heure du prochain PTS en mesurant simplement le temps entre les pts actuels et le précédent. Dans le même temps, nous devons synchroniser la vidéo avec l'audio. Nous allons faire une horloge audio.: Une valeur interne qui garde la position de l'audio que nous lisons. C'est comme une lecture numérique sur n'importe quel lecteur mp3. Puisque nous synchronisons la vidéo avec le son, le flux vidéo utilise cette valeur pour savoir si elle est trop en avance ou trop en arrière.

Nous reviendrons à la mise en œuvre plus tard; Supposons maintenant que nous avons la fonction get_audio_clockce qui nous donnera du temps sur l'horloge audio. Dès que nous obtenons cette valeur, que faut-il faire si la vidéo et l'audio ne sont pas synchronisés? Il serait stupide d'essayer de passer directement au bon package via une recherche ou autre chose. Au lieu de cela, nous ajustons simplement la valeur que nous avons calculée pour la prochaine mise à jour: si le PTS est trop loin derrière le temps audio, nous doublons notre retard estimé. Si le PTS est trop en avance sur le temps de jeu, nous mettons à jour le plus rapidement possible. Maintenant que nous avons le temps de mise à jour ou de retard configuré, nous allons le comparer avec l'horloge de notre ordinateur, en laissant frame_timer en cours d'exécution . Cette minuterie d'images résume tous nos retards estimés pendant la lecture d'un film. En d'autres termes, ce frame_timer- C'est l'heure qui indique quand afficher la prochaine image. Nous ajoutons simplement un nouveau délai au temporisateur de trame, le comparons avec l'heure de l'horloge de notre ordinateur et utilisons cette valeur pour planifier la prochaine mise à jour. Cela peut être un peu déroutant, alors lisez attentivement le code:

void video_refresh_timer(void *userdata) {

  VideoState *is = (VideoState *)userdata;
  VideoPicture *vp;
  double actual_delay, delay, sync_threshold, ref_clock, diff;
  
  if(is->video_st) {
    if(is->pictq_size == 0) {
      schedule_refresh(is, 1);
    } else {
      vp = &is->pictq[is->pictq_rindex];

      delay = vp->pts - is->frame_last_pts; /* the pts from last time */
      if(delay <= 0 || delay >= 1.0) {
	/* if incorrect delay, use previous one */
	delay = is->frame_last_delay;
      }
      /* save for next time */
      is->frame_last_delay = delay;
      is->frame_last_pts = vp->pts;

      /* update delay to sync to audio */
      ref_clock = get_audio_clock(is);
      diff = vp->pts - ref_clock;

      /* Skip or repeat the frame. Take delay into account
	 FFPlay still doesn't "know if this is the best guess." */
      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {
	if(diff <= -sync_threshold) {
	  delay = 0;
	} else if(diff >= sync_threshold) {
	  delay = 2 * delay;
	}
      }
      is->frame_timer += delay;
      /* computer the REAL delay */
      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
      if(actual_delay < 0.010) {
	/* Really it should skip the picture instead */
	actual_delay = 0.010;
      }
      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));
      /* show the picture! */
      video_display(is);
      
      /* update queue for next picture! */
      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
	is->pictq_rindex = 0;
      }
      SDL_LockMutex(is->pictq_mutex);
      is->pictq_size--;
      SDL_CondSignal(is->pictq_cond);
      SDL_UnlockMutex(is->pictq_mutex);
    }
  } else {
    schedule_refresh(is, 100);
  }
}

Nous effectuons plusieurs vérifications: premièrement, nous nous assurons que le délai entre le PTS actuel et le PTS précédent a du sens. S'il n'y a pas besoin de retard, alors l'audio et la vidéo coïncident à ce point et utilisent simplement le dernier retard. Ensuite, nous nous assurons que le seuil de synchronisation est atteint, car une synchronisation parfaite ne se produit jamais. FFplay utilise une valeur de 0,01 pour le seuil. Nous veillons également à ce que le seuil de synchronisation ne soit jamais inférieur aux intervalles entre les valeurs PTS. Enfin, définissez la valeur de mise à jour minimale à 10 millisecondes (en effet, il semble qu'ils devraient sauter le cadre ici, mais ne nous inquiétons pas à ce sujet).

Nous avons ajouté un tas de variables à la grande structure, alors n'oubliez pas de vérifier le code. N'oubliez pas non plus d'initialiser le temporisateur de trame et le délai initial de la trame précédente dans stream_component_open :

    is->frame_timer = (double)av_gettime() / 1000000.0;
    is->frame_last_delay = 40e-3;

Sync: horloge audio


Le moment est venu de réaliser l'horloge audio. Nous pouvons mettre à jour l'heure dans notre fonction audio_decode_frame , où nous décodons l'audio. Rappelez-vous maintenant que nous ne traitons pas toujours un nouveau package à chaque fois que nous appelons cette fonction, il y a donc deux zones où vous devez mettre à jour l'horloge. La première place est celle où nous obtenons le nouveau package: il suffit d'installer l'horloge audio sur le package PTS. Ensuite, si le paquet a plusieurs images, nous économisons le temps de lecture audio en comptant le nombre d'échantillons et en les multipliant par une fréquence d'échantillonnage donnée par seconde. Donc, quand nous avons le paquet:

    /* if update, update the audio clock w/pts */
    if(pkt->pts != AV_NOPTS_VALUE) {
      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;
    }

Et dès que nous traitons le colis:

      /* Keep audio_clock up-to-date */
      pts = is->audio_clock;
      *pts_ptr = pts;
      n = 2 * is->audio_st->codec->channels;
      is->audio_clock += (double)data_size /
	(double)(n * is->audio_st->codec->sample_rate);

Quelques nuances mineures: le modèle de fonction a été modifié et inclut désormais pts_ptr , alors assurez-vous de le changer. pts_ptr est le pointeur que nous utilisons pour indiquer à audio_callback les pts du paquet audio . Ce sera utilisé la prochaine fois pour synchroniser l'audio avec la vidéo.

Maintenant, nous pouvons enfin implémenter notre fonction get_audio_clock . Ce n'est pas aussi simple que d'obtenir la valeur est -> audio_clock , si vous y pensez. Veuillez noter que nous définissons l'audio PTS à chaque fois que nous le traitons, mais si vous regardez la fonction audio_callback, il faudra du temps pour déplacer toutes les données de notre paquet audio vers notre tampon de sortie. Cela signifie que la valeur de notre horloge audio peut être trop avancée. Par conséquent, nous devons vérifier combien nous devons écrire. Voici le code complet:

double get_audio_clock(VideoState *is) {
  double pts;
  int hw_buf_size, bytes_per_sec, n;
  
  pts = is->audio_clock; /* maintained in the audio thread */
  hw_buf_size = is->audio_buf_size - is->audio_buf_index;
  bytes_per_sec = 0;
  n = is->audio_st->codec->channels * 2;
  if(is->audio_st) {
    bytes_per_sec = is->audio_st->codec->sample_rate * n;
  }
  if(bytes_per_sec) {
    pts -= (double)hw_buf_size / bytes_per_sec;
  }
  return pts;
}

Vous devez maintenant comprendre pourquoi cette fonction fonctionne;)

Alors, c'est tout! Nous compilons:

gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lswscale -lz -lm \
`sdl-config --cflags --libs`

C'est arrivé! Vous pouvez regarder le film sur un lecteur self-made. Dans la leçon suivante, nous allons examiner la synchronisation audio, puis apprendre à rechercher.

Guide FFmpeg et SDL ou Comment écrire un lecteur vidéo en moins de 1000 lignes - Partie 2



Traductions sur le blog Edison:


All Articles