[Teil 1/2] Anleitung zu FFmpeg und SDL oder Schreiben eines Videoplayers in weniger als 1000 Zeilen


Obwohl diese Informationen bereits veraltet sind, ist das Originalmaterial immer noch eine beliebte Inspirationsquelle für verschiedene nützliche Inhalte zum Thema FFmpeg. Es gibt jedoch noch keine vollständige Übersetzung des Originals ins Russische. Wir korrigieren das nervige Auslassen, denn es ist besser spät als nie.

Und obwohl wir es versucht haben, sind Schwierigkeiten bei der Übersetzung in einem so umfangreichen Text unvermeidlich . Fehler melden (vorzugsweise in privaten Nachrichten) - gemeinsam werden wir es besser machen.

Inhaltsverzeichnis

EDISON Software - Webentwicklung
EDISON.

, C C++.

! ;-)


UPD: Dieser Leitfaden wurde ab Februar 2015 aktualisiert.

FFmpeg ist eine großartige Bibliothek zum Erstellen von Videoanwendungen sowie allgemeinen Dienstprogrammen. FFmpeg kümmert sich um die gesamte Videoverarbeitungsroutine und führt alle Decodierungen, Codierungen, Multiplexe und Demultiplexe durch. Dies vereinfacht die Erstellung von Medienanwendungen erheblich. Alles ist ganz einfach und schnell, in C geschrieben, Sie können fast jeden heute verfügbaren Codec dekodieren sowie in einige andere Formate kodieren.

Der einzige Haken ist, dass die Dokumentation meistens fehlt. Es gibt ein Tutorial ( im Original befindet sich hier ein Link zu einer bereits nicht vorhandenen Webseite - Notizübersetzer), das die Grundlagen von FFmpeg und die automatische Erzeugung von Sauerstoffdocks behandelt. Und nichts weiter. Aus diesem Grund habe ich mich entschlossen, unabhängig herauszufinden, wie FFmpeg zum Erstellen funktionierender digitaler Video- und Audioanwendungen verwendet werden kann, und gleichzeitig den Prozess zu dokumentieren und in Form eines Lehrbuchs darzustellen.

Es gibt ein FFplay-Programm, das mit FFmpeg geliefert wird. Es ist einfach, in C geschrieben, implementiert einen vollwertigen Videoplayer mit FFmpeg. Meine erste Lektion ist eine aktualisierte Version der Originalstunde von Martin Böhme ( im Original ein Link zu einer bereits nicht mehr existierenden Webseite - eine Anmerkung des Übersetzers ) - ich habe einige Teile von dort gezogen. Außerdem werde ich in einer Reihe meiner Lektionen den Prozess der Erstellung eines funktionierenden Videoplayers auf der Basis von ffplay.c zeigenFabrice Bellard. In jeder Lektion wird eine neue Idee (oder sogar zwei) mit einer Erläuterung ihrer Umsetzung vorgestellt. Jedes Kapitel enthält eine C-Liste, die Sie selbst kompilieren und ausführen können. Die Quelldateien zeigen, wie dieses Programm funktioniert, wie seine einzelnen Teile funktionieren und zeigen kleinere technische Details, die in diesem Handbuch nicht behandelt werden. Wenn wir fertig sind, haben wir einen funktionierenden Videoplayer, der in weniger als 1000 Codezeilen geschrieben ist!

Beim Erstellen des Players verwenden wir SDL, um Audio- und Videomediendateien auszugeben. SDL ist eine hervorragende plattformübergreifende Multimedia-Bibliothek, die in MPEG-Wiedergabeprogrammen, Emulatoren und vielen Videospielen verwendet wird. Sie müssen die SDL-Bibliotheken herunterladen und auf Ihrem System installieren, um die Programme aus diesem Handbuch zu kompilieren.

Dieses Tutorial richtet sich an Personen mit guter Programmiererfahrung. Zumindest müssen Sie C kennen und Konzepte wie Warteschlangen, Mutexe usw. verstehen. Es sollte ein gewisses Verständnis für Multimedia geben. Zum Beispiel Dinge wie Wellenformen und dergleichen. Es ist jedoch nicht notwendig, in diesen Angelegenheiten ein Guru zu sein, da viele Konzepte im Verlauf des Unterrichts erklärt werden.

Bitte senden Sie mir Fehlermeldungen, Fragen, Kommentare, Ideen, Funktionen usw. an Dranger Doggy Gmail dot Com.







Lesen Sie auch im Blog der
Firma EDISON:


FFmpeg libav Handbuch






Lektion 1: Erstellen von Screencaps


Vollständige Auflistung: 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;
}

Überblick


Filmdateien bestehen aus mehreren Hauptkomponenten. Zunächst wird die Datei selbst als Container bezeichnet , und der Containertyp bestimmt, wie Daten in der Datei dargestellt werden. Beispiele für Container sind AVI und Quicktime . Darüber hinaus enthält die Datei mehrere Threads. Insbesondere gibt es normalerweise einen Audiostream und einen Videostream . ("Stream" ist ein lustiges Wort für "eine Folge von Datenelementen, die gemäß der Zeitachse verfügbar sind".) Datenelemente in einem Stream werden als Frames bezeichnet . Jeder Stream wird von dem einen oder anderen Codec- Typ codiert . Der Codec bestimmt, wie die tatsächlichen Daten zu diruyutsya und DezemberGeprüft - daher der Name des Codecs. Beispiele für Codecs sind DivX und MP3. Pakete werden dann aus dem Stream gelesen. Pakete sind Datenstücke , die Datenbits enthalten können , die in Rohrahmen dekodiert werden, die wir schließlich in unserer Anwendung bearbeiten können. Für unsere Zwecke enthält jedes Paket Vollbilder (oder mehrere Bilder, wenn es sich um Audio handelt).

Das Arbeiten mit Video- und Audiostreams ist selbst auf der einfachsten Ebene sehr einfach:

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

Das Arbeiten mit Multimedia mit FFmpeg ist fast so einfach wie in diesem Programm, obwohl in einigen Programmen der Schritt "MAKE [...]" sehr schwierig sein kann. In diesem Tutorial öffnen wir die Datei, zählen den darin enthaltenen Videostream und unser "MAKE [...]" schreibt den Frame in die PPM-Datei.

Datei öffnen


Lassen Sie uns zuerst sehen, was zuerst passiert, wenn Sie die Datei öffnen. Mit FFmpeg initialisieren wir zuerst die gewünschte Bibliothek:

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

Dadurch werden alle verfügbaren Dateiformate und Codecs in der Bibliothek registriert, sodass sie beim Öffnen einer Datei mit dem entsprechenden Format / Codec automatisch verwendet werden. Beachten Sie, dass Sie av_register_all () nur einmal aufrufen müssen , daher tun wir dies hier in main (). Wenn Sie möchten, können Sie nur ausgewählte Dateiformate und Codecs registrieren. In der Regel gibt es jedoch keinen besonderen Grund dafür.

Öffnen Sie nun die Datei:

AVFormatContext *pFormatCtx = NULL;

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

Holen Sie sich den Dateinamen aus dem ersten Argument. Diese Funktion liest den Dateikopf und speichert die Dateiformatinformationen in der von uns übergebenen AVFormatContext- Struktur . Die letzten drei Argumente werden verwendet, um das Dateiformat, die Puffergröße und die Formatparameter anzugeben. Wenn Sie sie auf NULL oder 0 setzen, erkennt libavformat alles automatisch.

Diese Funktion betrachtet nur den Header, daher müssen wir jetzt die Stream-Informationen in der Datei überprüfen:

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

Diese Funktion übergibt gültige Daten an pFormatCtx -> Streams . Wir lernen eine praktische Debugging-Funktion kennen, die uns zeigt, was drin ist:

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

Jetzt ist pFormatCtx -> Streams nur ein Array von Zeigern der Größe pFormatCtx -> nb_streams . Wir werden es durchgehen, bis wir den Videostream finden:

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;

Informationen zum Codec im Stream befinden sich an einem Ort, der als " Codec-Kontext " bezeichnet wird. Es enthält alle Informationen über den Codec, den der Stream verwendet, und jetzt haben wir einen Zeiger darauf. Aber wir müssen noch den richtigen Codec finden und öffnen:

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

Bitte beachten Sie, dass Sie AVCodecContext nicht direkt aus dem Videostream verwenden können! Daher müssen Sie einen vcodec_copy_context () verwenden, um den Kontext an einen neuen Speicherort zu kopieren (natürlich nachdem Speicher dafür zugewiesen wurde).

Datenspeicher


Jetzt brauchen wir einen Platz zum Aufbewahren des Rahmens:

AVFrame *pFrame = NULL;

// Allocate video frame
pFrame=av_frame_alloc();

Da wir PPM-Dateien ausgeben möchten, die in 24-Bit-RGB gespeichert sind, müssen wir unseren Frame von seinem eigenen Format in RGB konvertieren. FFmpeg wird es für uns tun. Für die meisten Projekte (einschließlich dieses) müssen Sie den Startrahmen in ein bestimmtes Format konvertieren. Wählen Sie einen Frame für den konvertierten Frame aus:

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

Trotz der Tatsache, dass wir den Frame ausgewählt haben, benötigen wir beim Konvertieren noch einen Platz, an dem die Rohdaten untergebracht werden können. Wir verwenden avpicture_get_size , um die richtigen Größen zu erhalten und den erforderlichen Speicherplatz manuell zuzuweisen:

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 ist ein Analogon der C-Funktion malloc von FFmpeg, einem einfachen Wrapper um malloc , der die Ausrichtung von Speicheradressen usw. ermöglicht. Dies schützt übrigens nicht vor Speicherlecks, doppelter Freigabe oder anderen Problemen, die bei malloc auftreten .

Jetzt verwenden wir avpicture_fill, um den Frame unserem neu zugewiesenen Puffer zuzuordnen. In Bezug auf AVPicture : Die AVPicture- Struktur ist eine Teilmenge der AVFrame- Struktur - der Anfang der AVFrame- Struktur ist identisch mit der AVPicture- Struktur .

// 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);

Wir sind schon am Ziel! Jetzt können wir aus dem Stream lesen!

Daten lesen


Um nun den gesamten Videostream zu lesen, lesen wir das nächste Paket, entschlüsseln es in unserem Frame. Sobald die Entschlüsselung abgeschlossen ist, konvertieren wir den Frame und speichern ihn:

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);
}

Nichts kompliziertes: av_read_frame () liest das Paket und speichert es in der AVPacket- Struktur . Bitte beachten Sie, dass wir nur die Struktur des Pakets verteilen - FFmpeg liefert uns die internen Daten, auf die packet.data verweist . Dies gibt av_free_packet () etwas später frei . avcodec_decode_video () konvertiert das Paket in einen Frame. Möglicherweise verfügen wir jedoch nicht über alle Informationen, die wir nach dem Decodieren des Pakets für den Frame benötigen. Daher setzt avcodec_decode_video () frameFinished, wenn wir den nächsten Frame haben. Schließlich verwenden wir sws_scale (), um aus unserem eigenen Format ( pCodecCtx -> ) zu konvertierenpix_fmt ) in RGB. Denken Sie daran, dass Sie einen AVFrame- Zeiger in einen AVPicture- Zeiger umwandeln können . Schließlich übergeben wir die Informationen über den Rahmen, die Höhe und die Breite unserer SaveFrame- Funktion.

Apropos Pakete. Technisch gesehen kann ein Paket nur einen Teil eines Rahmens sowie andere Datenbits enthalten. Der FFmpeg-Parser garantiert jedoch, dass die empfangenen Pakete entweder einen vollständigen Frame oder sogar mehrere Frames enthalten.

Jetzt müssen Sie nur noch die SaveFrame- Funktion verwenden, um die RGB-Informationen in eine PPM-Datei zu schreiben. Obwohl wir uns oberflächlich mit dem PPM-Format selbst befassen; Glauben Sie mir, hier funktioniert alles:

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);
}

Wir führen ein Öffnen einer Standarddatei usw. durch und zeichnen dann die RGB-Daten auf. Die Datei wird Zeile für Zeile geschrieben. Eine PPM-Datei ist einfach eine Datei, in der RGB-Informationen als lange Zeile dargestellt werden. Wenn Sie die Farben von HTML kennen, ist dies so, als würden Sie die Farben jedes Pixels vom ersten bis zum letzten Ende markieren, etwa # ff0000 # ff0000 .... , wie bei einem roten Bildschirm. (Tatsächlich wird es im Binärformat und ohne Trennzeichen gespeichert, aber ich hoffe, Sie verstehen die Idee.) Der Titel gibt an, wie breit und hoch das Bild ist, sowie die maximale Größe der RGB-Werte.

Nun zurück zu unserer main () Funktion . Sobald wir mit dem Lesen aus dem Videostream fertig sind, müssen wir nur noch alles löschen:

// 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;

Wie Sie sehen können, verwenden wir av_free für den mit avcode_alloc_frame und av_malloc zugewiesenen Speicher .

Das ist der ganze Code! Wenn Sie nun Linux oder eine ähnliche Plattform verwenden, führen Sie Folgendes aus:

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

Wenn Sie eine ältere Version von FFmpeg haben, müssen Sie möglicherweise -lavutil entfernen :

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

Die meisten Grafikprogramme müssen das PPM-Format öffnen. Schauen Sie sich einige Filmdateien an, deren Screencaps mit unserem Programm erstellt wurden.






Lektion 2: Anzeigen des Bildschirms


Vollständige Auflistung: 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 und Video


Zum Zeichnen auf dem Bildschirm verwenden wir SDL. SDL steht für Simple Direct Layer . Es ist eine hervorragende plattformübergreifende Multimedia-Bibliothek, die in vielen Projekten verwendet wird. Sie können die Bibliothek auf der offiziellen Website herunterladen oder gegebenenfalls das Entwicklerpaket für Ihr Betriebssystem herunterladen. Sie benötigen Bibliotheken, um den Code aus dieser Lektion zu kompilieren (alle anderen Lektionen gelten übrigens auch).

SDL bietet viele Methoden zum Zeichnen auf dem Bildschirm. Eine Möglichkeit, Filme anzuzeigen, ist das sogenannte YUV-Overlay .

Formal nicht einmal YUV, sondern YCbCr. Einige Leute werden übrigens sehr verbrannt, wenn "YCbCr" als "YUV" bezeichnet wird. Im Allgemeinen ist YUV ein analoges Format und YCbCr ist ein digitales Format. FFmpeg und SDL bezeichnen in ihrem Code und in Makros YCbCr als YUV, aber das ist.

YUV ist eine Methode zum Speichern von Rohbilddaten wie RGB. Grob gesagt ist Y eine Komponente der Helligkeit , und U und V sind Komponenten der Farbe . (Dies ist komplizierter als RGB, da ein Teil der Farbinformationen verworfen wird und Sie nur 1 Messung von U und V für jeweils 2 Messungen von Y durchführen können. ) YUV-Overlayin SDL akzeptiert einen rohen YUV-Datensatz und zeigt ihn an. Es werden 4 verschiedene Arten von YUV-Formaten akzeptiert, aber YV12 ist das schnellste davon. Es gibt ein anderes YUV-Format namens YUV420P , das mit YV12 übereinstimmt, außer dass die Arrays von U und V vertauscht werden. 420 bedeutet, dass es in einem Verhältnis von 4: 2: 0 abgetastet wird, dh für jeweils 4 Helligkeitsmessungen gibt es 1 Farbmessung, sodass die Farbinformationen in Vierteln verteilt werden. Dies ist ein guter Weg, um Bandbreite zu sparen, da das menschliche Auge diese Änderungen immer noch nicht bemerkt. Der lateinische Buchstabe "P" im Namen zeigt an, dass das Format "planar" ist, was einfach bedeutet, dass die Komponenten Y sind.U und V befinden sich in getrennten Arrays. FFmpeg kann Bilder in YUV420P konvertieren , was sehr hilfreich ist, da viele Videostreams bereits in diesem Format gespeichert sind oder einfach in dieses konvertiert werden können.

Daher ist es derzeit geplant, die Funktion SaveFrame () aus der vorherigen Lektion zu ersetzen und stattdessen unseren Frame anzuzeigen. Zunächst müssen Sie sich jedoch mit den Grundfunktionen der SDL-Bibliothek vertraut machen. Verbinden Sie zunächst die Bibliotheken und initialisieren Sie die 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 () teilt der Bibliothek im Wesentlichen mit, welche Funktionen wir verwenden werden. SDL_GetError (), dies ist natürlich unsere praktische Funktion zum Debuggen.

Anzeigeerstellung


Jetzt brauchen wir einen Platz auf dem Bildschirm, um die Elemente anzuordnen. Der Hauptbereich für die Anzeige von Bildern mit SDL wird als Oberfläche bezeichnet :

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);
}

Also haben wir einen Bildschirm mit einer bestimmten Breite und Höhe eingerichtet. Die nächste Option ist die Bittiefe des Bildschirms - 0 - dies ist ein spezieller Wert, der "dasselbe wie die aktuelle Anzeige" bedeutet.

Jetzt erstellen wir ein YUV-Overlay auf diesem Bildschirm, damit wir Videos darauf ausgeben können, und konfigurieren unseren SWSContext so, dass Bilddaten in YUV420 konvertiert werden :

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
			 );

Wie bereits erwähnt, verwenden wir YV12 , um das Bild anzuzeigen und die YUV420- Daten von FFmpeg abzurufen .

Bildschirm


Das war einfach genug! Jetzt müssen wir nur noch das Bild zeigen. Gehen wir den ganzen Weg zu dem Ort, an dem wir den fertigen Schuss hatten. Wir können alles loswerden, was wir für den RGB-Frame hatten, und wir werden SaveFrame () durch unseren Anzeigecode ersetzen . Um das Bild anzuzeigen, erstellen wir eine AVPicture- Struktur und legen die Datenzeiger und die Liniengröße für unser YUV-Overlay fest :

  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);

Zuerst blockieren wir das Overlay, weil wir vorhaben, darauf zu schreiben. Dies ist eine gute Angewohnheit, damit später keine Probleme auftreten. Die AVPicture- Struktur hat , wie oben gezeigt, einen Datenzeiger, der ein Array von 4 Zeigern ist. Da es sich hier um den YUV420P handelt , haben wir nur 3 Kanäle und damit nur 3 Datensätze. Andere Formate haben möglicherweise einen vierten Zeiger für den Alphakanal oder etwas anderes. Die Zeilengröße sieht so aus. Ähnliche Strukturen in unserer YUV-Überlagerung sind Variablen für Pixel und Höhen. (Tonhöhen, Tonhöhen - wenn in SDL ausgedrückt, um die Breite einer bestimmten Datenzeile anzugeben.) Wir geben also drei Bilddaten-Arrays in unserer Überlagerung an, also wenn wir schreibenIm Bild nehmen wir tatsächlich in unserem Overlay auf, dem natürlich bereits der dafür erforderliche Speicherplatz zugewiesen ist. Auf die gleiche Weise erhalten wir Informationen zur Zeilengröße direkt aus unserer Überlagerung. Wir ändern das Konvertierungsformat in PIX_FMT_YUV420P und verwenden sws_scale wie zuvor.

Bildzeichnung


Wir müssen jedoch noch die SDL angeben, damit sie wirklich die Daten anzeigt, die wir ihr zur Verfügung gestellt haben. Wir übergeben dieser Funktion auch ein Rechteck, das angibt, wohin der Film gehen soll, auf welche Breite und Höhe er skaliert werden soll. Daher skaliert die SDL für uns, und dies kann Ihrer GPU helfen, schneller zu skalieren:

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);
  }

Jetzt wird unser Video angezeigt!

Lassen Sie uns eine weitere Funktion von SDL zeigen: das Ereignissystem . SDL ist so konfiguriert, dass beim Eingeben oder Bewegen der Maus in der SDL-Anwendung oder beim Senden eines Signals ein Ereignis generiert wird. Ihr Programm prüft dann auf diese Ereignisse, ob es Benutzereingaben verarbeiten soll. Ihr Programm kann auch Ereignisse erstellen, um SDL-Ereignisse an das System zu senden. Dies ist besonders nützlich für die Multithread-Programmierung mit SDL, die wir in Lektion 4 sehen werden. In unserem Programm werden wir Ereignisse unmittelbar nach der Verarbeitung des Pakets überprüfen. Im Moment werden wir das SDL_QUIT- Ereignis behandeln, damit wir beenden können:

SDL_Event       event;

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

Und so leben wir! Wir werden den ganzen alten Müll los und sind bereit zu kompilieren. Wenn Sie Linux oder ähnliches verwenden, können Sie am besten mit den SDL-Bibliotheken kompilieren:

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

sdl-config zeigt einfach die notwendigen Flags an, damit gcc die SDL-Bibliotheken korrekt aktiviert. Möglicherweise müssen Sie etwas anderes tun, um diese Kompilierung auf Ihrem System durchzuführen. Bitte überprüfen Sie die SDL-Dokumentation für Ihr System für jeden Feuerwehrmann. Nach dem Kompilieren fortfahren und ausführen.

Was passiert, wenn Sie dieses Programm ausführen? Das Video scheint verrückt zu werden! Tatsächlich zeigen wir einfach alle Videobilder so schnell an, wie wir sie aus der Filmdatei extrahieren können. Wir haben derzeit nicht den Code, um herauszufinden, wann wir das Video zeigen müssen. Am Ende (in Lektion 5) werden wir beginnen, das Video zu synchronisieren. Aber im Moment fehlt uns etwas ebenso Wichtiges: der Sound!






Lektion 3: Sound abspielen


Vollständige Auflistung: 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;
}

Audio


Jetzt möchten wir, dass der Sound in der Anwendung abgespielt wird. SDL bietet uns auch Methoden zum Abspielen von Sound. Mit der Funktion SDL_OpenAudio () wird das Audiogerät selbst geöffnet. Als Argumente wird die SDL_AudioSpec- Struktur verwendet , die alle Informationen zu dem Audio enthält, das abgespielt werden soll.

Bevor wir zeigen, wie dies konfiguriert wird, erklären wir zunächst, wie der Computer Audio im Allgemeinen verarbeitet. Digitales Audio besteht aus einem langen Strom von Samples, von denen jede eine bestimmte Bedeutung einer Schallwelle darstellt. Sounds werden mit einer bestimmten Abtastrate aufgenommen, die lediglich angibt, wie schnell jedes Sample abgespielt wird, und anhand der Anzahl der Samples pro Sekunde gemessen. Die ungefähren Abtastfrequenzen betragen 22.050 und 44.100 Abtastfrequenzen pro Sekunde. Dies sind die Geschwindigkeiten, die für Radio bzw. CD verwendet werden. Darüber hinaus können die meisten Audiodaten mehr als einen Kanal für Stereo- oder Surround-Sound haben. Wenn sich das Sample beispielsweise in Stereo befindet, werden zwei Samples gleichzeitig ausgegeben. Wenn wir die Daten aus der Filmdatei erhalten, wissen wir nicht, wie viele Samples wir erhalten werden, aber FFmpeg erzeugt keine defekten Samples - dies bedeutet auch, dass das Stereo-Sample nicht getrennt wird.

Die Methode zum Abspielen von Audio in SDL ist wie folgt. Die Soundparameter sind konfiguriert: Abtastfrequenz, Anzahl der Kanäle usw. Stellen Sie außerdem die Rückruffunktion und die Benutzerdaten ein. Wenn wir mit der Tonwiedergabe beginnen, ruft die SDL diese Rückruffunktion ständig auf und fordert sie auf, den Audiopuffer mit einer bestimmten Anzahl von Bytes zu füllen. Nachdem wir diese Informationen in die SDL_AudioSpec- Struktur eingefügt haben , rufen wir SDL_OpenAudio () auf, wodurch das Audiogerät geöffnet und eine weitere AudioSpec- Struktur zurückgegeben wird . Dies sind die Eigenschaften, die wir tatsächlich verwenden werden - es gibt keine Garantie dafür, dass wir genau das bekommen, wonach wir gefragt haben!

Audioeinstellung


Denken Sie vorerst daran, denn wir haben noch keine Informationen zu Audio-Streams! Kehren wir zu der Stelle in unserem Code zurück, an der wir den Videostream gefunden haben, und finden Sie heraus, welcher Stream der Audiostream ist:

// 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;

Hier können wir alle gewünschten Informationen aus dem AVCodecContext aus dem Stream abrufen , genau wie wir es mit dem Videostream getan haben:

AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;

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

Wenn Sie sich erinnern, müssen wir in früheren Lektionen den Audio-Codec selbst noch öffnen. Das ist einfach:

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);

Im Kontext des Codecs enthält alle Informationen, die zur Konfiguration unseres Audios erforderlich sind:

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;
}

Lassen Sie uns jeden Punkt durchgehen:
  • Frequenz (Frequenz): Abtastrate, wie zuvor erläutert.
  • format (): SDL , . «S» «S16SYS» «», 16 , 16 , «SYS» , , . , avcodec_decode_audio2 .
  • channels (): .
  • silence (): , . 0.
  • samples (): , , SDL , . - 512 8192; FFplay, , 1024.
  • Rückruf (Rückruf): Hier übergeben wir die eigentliche Rückruffunktion. Wir werden später mehr über die Rückruffunktion sprechen.
  • Benutzerdaten : Die SDL gibt unserem Rückruf einen Nullzeiger auf alle gewünschten Benutzerdaten. Wir möchten ihn über unseren Codec-Kontext informieren. etwas tiefer wird klar sein warum.

Öffnen Sie zum Schluss das Audio mit SDL_OpenAudio .

Warteschlangen


Und es ist notwendig! Jetzt können wir Audioinformationen aus dem Stream extrahieren. Aber was tun mit diesen Informationen? Wir werden kontinuierlich Pakete aus der Filmdatei empfangen, aber gleichzeitig ruft die SDL die Rückruffunktion auf! Die Lösung wird darin bestehen, eine Art globale Struktur zu erstellen, in die wir Audiopakete einfügen können, damit unser audio_callback etwas zum Empfangen von Audiodaten hat! Hier ist also, was wir tun werden, um die Paketwarteschlange zu erstellen. FFmpeg hat sogar eine Struktur, die dabei hilft: AVPacketList , eine verknüpfte Liste für Pakete. Hier ist unsere Warteschlangenstruktur:

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

Zunächst müssen wir angeben, dass nb_packets unterschiedlich groß ist - die Größe bezieht sich auf die Größe des Bytes, das wir von package-> size erhalten . Beachten Sie, dass wir einen Mutex und eine Bedingungsvariable haben. Dies liegt daran, dass die SDL den Audioprozess als separaten Stream ausführt. Wenn wir die Warteschlange nicht richtig blockieren, können wir unsere Daten wirklich ruinieren. Mal sehen, wie die Warteschlange implementiert wird. Jeder Programmierer mit Selbstachtung sollte wissen, wie man Warteschlangen erstellt. Wir zeigen Ihnen jedoch auch, wie dies funktioniert, damit Sie die SDL-Funktionen leichter erlernen können.

Zuerst erstellen wir eine Funktion zum Initialisieren der Warteschlange:

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

Erstellen Sie dann eine Funktion, um Objekte in unsere Warteschlange zu stellen:

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 () blockiert den Mutex in der Warteschlange, damit wir etwas hinzufügen können, und dann sendet SDL_CondSignal () ein Signal an unsere get- Funktion (falls erwartet) über unsere bedingte Variable, um ihr mitzuteilen, dass Daten vorhanden sind, und kann für weitere Zwecke fortgesetzt werden Mutex entsperren.

Hier ist die entsprechende Get- Funktion . Beachten Sie, wie SDL_CondWait () den Funktionsblock erstellt (d. H. Pausiert, bis wir die Daten erhalten), wenn wir ihn dazu auffordern:

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;
}

Wie Sie sehen können, haben wir die Funktion in einen ewigen Zyklus eingeschlossen, sodass wir definitiv einige Daten erhalten, wenn wir sie blockieren möchten. Mit der Funktion SDL_CondWait () vermeiden wir eine Schleife für immer . Im Wesentlichen wartet CondWait lediglich auf ein Signal von SDL_CondSignal () (oder SDL_CondBroadcast ()) und fährt dann fort. Es sieht jedoch so aus, als hätten wir es in einem Mutex gefangen - wenn wir die Sperre gedrückt halten, kann unsere Put- Funktion nichts in die Warteschlange stellen! Was SDL_CondWait () jedoch auch für uns tut, ist, den Mutex, den wir ihm geben, zu entsperren und dann erneut zu versuchen, ihn zu sperren, sobald wir das Signal empfangen.

Für jeden Feuerwehrmann


Sie sehen auch, dass wir eine globale Beendigungsvariable haben , die wir überprüfen, um sicherzustellen, dass wir das Ausgangssignal nicht im Programm eingestellt haben (SDL verarbeitet automatisch TERM- Signale usw.). Andernfalls wird der Thread für immer fortgesetzt und wir müssen das Programm mit kill -9 beenden :

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

Wir setzen das Exit-Flag auf 1.

Wir füttern Pakete


Es bleibt nur die Konfiguration unserer Warteschlange:

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

  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

SDL_PauseAudio () startet schließlich die Audioeinheit. Es reproduziert Stille, wenn es keine Daten empfängt. Dies geschieht jedoch nicht sofort.

Wir haben also eine Warteschlange konfiguriert und können nun Pakete an sie senden. Wir fahren mit unserem Paketlesezyklus fort:

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);
  }

Bitte beachten Sie, dass wir das Paket nach dem Einreihen nicht freigeben. Wir werden es später veröffentlichen, wenn wir entschlüsseln.

Pakete abrufen


Lassen Sie uns nun endlich unsere audio_callback- Funktion ausführen , um Pakete aus der Warteschlange abzurufen. Der Rückruf sollte folgendermaßen aussehen:

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

Benutzerdaten sind der Zeiger, den wir der SDL gegeben haben, Stream ist der Puffer, in den Audiodaten geschrieben werden, und len ist die Größe dieses Puffers. Hier ist der 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;
  }
}

Tatsächlich ist dies eine einfache Schleife, die Daten aus einer anderen von uns geschriebenen Funktion, audio_decode_frame () , extrahiert , das Ergebnis in einem Zwischenpuffer speichert, versucht, len Bytes in den Stream zu schreiben und mehr Daten empfängt, wenn wir noch nicht genug haben oder sie für später speichern. wenn wir noch etwas übrig haben. Die Größe von audio_buf ist 1,5-mal so groß wie der größte Audio-Frame, den FFmpeg uns geben wird, was uns einen guten Spielraum gibt.

Endgültige Audio-Entschlüsselung


Schauen wir uns die Innenseiten des audio_decode_frame- Decoders an :

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;
  }
}

Der gesamte Prozess beginnt tatsächlich gegen Ende der Funktion, wo wir packet_queue_get () aufrufen . Wir nehmen das Paket aus der Warteschlange und speichern die Informationen daraus. Wenn das Paket funktioniert, rufen wir avcodec_decode_audio4 () auf, das der Schwesterfunktion avcodec_decode_video () sehr ähnlich ist , außer dass das Paket in diesem Fall mehr als einen Frame haben kann. Daher müssen Sie es möglicherweise mehrmals aufrufen, um alle Daten aus dem Paket abzurufen. Nachdem wir den Frame erhalten haben, kopieren wir ihn einfach in unseren Audiopuffer und stellen sicher, dass data_size kleiner als unser Audiopuffer ist . Denken Sie auch an das Casting von audio_bufauf den richtigen Typ, da SDL einen 8-Bit-Int-Puffer und FFmpeg Daten in einem 16-Bit-Int-Puffer liefert. Sie sollten auch den Unterschied zwischen len1 und data_size berücksichtigen . len1 ist die Größe des von uns verwendeten Pakets und data_size ist die Menge der zurückgegebenen Rohdaten.

Wenn wir Daten haben, kehren wir sofort zurück, um herauszufinden, ob wir mehr Daten aus der Warteschlange abrufen müssen oder ob wir fertig sind. Wenn wir das Paket noch bearbeiten müssen, bleiben Sie dabei. Wenn Sie das Paket fertiggestellt haben, geben Sie es schließlich frei.

Und das ist alles! Wir haben Audio von der Hauptleseschleife in die Warteschlange übertragen, die dann von der Funktion audio_callback gelesen wird, der diese Daten an die SDL und die SDL an Ihre Soundkarte überträgt. Gehen Sie voran und kompilieren Sie:

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

Gip-gip-Hurra! Das Video wird immer noch mit maximaler Geschwindigkeit übertragen, aber der Ton wird bereits so abgespielt, wie er sollte. Warum so? Ja, da die Audioinformationen eine Abtastfrequenz haben - wir pumpen die Audioinformationen so schnell aus, wie sich herausstellt, aber das Audio wird in diesem Stream einfach entsprechend seiner Abtastfrequenz abgespielt.

Wir sind fast reif für die Video- und Audiosynchronisation, aber zuerst müssen wir eine kleine Neuorganisation des Programms durchführen. Die Methode, Sound in die Warteschlange zu stellen und ihn mit einem separaten Stream abzuspielen, funktionierte sehr gut: Sie machte den Code übersichtlicher und modularer. Bevor wir mit der Synchronisierung von Video und Audio beginnen, müssen wir den Code vereinfachen. In der nächsten Serie werden wir Kontrollflüsse erzeugen!






Lektion 4: Mehrere Themen


Vollständige Auflistung 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;

}

Überblick


Beim letzten Mal haben wir Audio-Unterstützung mithilfe der SDL-Audiofunktionen hinzugefügt. SDL hat einen Thread gestartet, der Rückrufe für die Funktion erstellt, die wir jedes Mal definiert haben, wenn ein Sound benötigt wurde. Jetzt machen wir dasselbe mit der Videoanzeige. Dies macht den Code modularer und einfacher zu bearbeiten - insbesondere, wenn Sie die Synchronisierung hinzufügen möchten. Wo fangen wir an?

Beachten Sie, dass unsere Hauptfunktion viel erledigt: Sie durchläuft die Ereignisschleife, liest Pakete und decodiert das Video. Was wir tun werden, ist, alles in Teile zu teilen: Wir werden einen Stream haben, der für das Decodieren von Paketen verantwortlich ist; Dann werden diese Pakete zur Warteschlange hinzugefügt und von den entsprechenden Audio- und Videostreams gelesen. Wir haben den Audiostream bereits nach Bedarf eingestellt. Mit einem Videostream wird es etwas schwieriger, da wir sicherstellen müssen, dass das Video selbst gezeigt wird. Wir werden den eigentlichen Anzeigecode zur Hauptschleife hinzufügen. Anstatt das Video jedes Mal anzuzeigen, wenn wir die Schleife ausführen, integrieren wir die Videoanzeige in die Ereignisschleife. Die Idee ist, das Video zu dekodieren, den empfangenen Frame in einer anderen Warteschlange zu speichern und dann ein eigenes Ereignis ( FF_REFRESH_EVENT) zu erstellen), die wir dem Ereignissystem hinzufügen. Wenn unsere Ereignisschleife dieses Ereignis sieht, wird der nächste Frame in der Warteschlange angezeigt. Hier ist eine praktische ASCII-Darstellung des Geschehens:


Der Hauptgrund für das Verschieben der Videoanzeigesteuerung durch die Ereignisschleife besteht darin, dass wir mit dem SDL_Delay- Stream genau steuern können, wann das nächste Videobild auf dem Bildschirm angezeigt wird. Wenn wir das Video in der nächsten Lektion endlich synchronisieren, fügen Sie einfach einen Code hinzu, der das nächste Video-Update so plant, dass das richtige Bild zum richtigen Zeitpunkt auf dem Bildschirm angezeigt wird.

Vereinfachen Sie den Code


Lassen Sie uns den Code ein wenig löschen. Wir haben all diese Informationen über Audio- und Video-Codecs und werden Warteschlangen, Puffer und Gott weiß was noch hinzufügen. All diese Dinge sind für eine bestimmte logische Einheit, nämlich - für den Film. Wir beabsichtigen daher, eine große Struktur mit all diesen Informationen namens VideoState zu erstellen .

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;

Hier sehen wir Hinweise darauf, was wir am Ende bekommen werden. Zunächst sehen wir die grundlegenden Informationen - den Kontext des Formats und die Indizes des Audio- und Videostreams sowie die entsprechenden AVStream- Objekte . Dann sehen wir, dass einige dieser Audiopuffer in diese Struktur verschoben werden. Sie ( audio_buf , audio_buf_size usw.) waren für Informationen zu Audio gedacht, das noch vorhanden war (oder fehlte). Wir haben eine weitere Warteschlange für Videos und einen Puffer (der als Warteschlange verwendet wird; dafür benötigen wir keine extravaganten Warteschlangen) für dekodierte Frames hinzugefügt (als Overlay gespeichert). VideoPicture- Struktur- Dies ist unsere eigene Schöpfung (wir werden sehen, was darin sein wird, wenn wir dazu kommen). Sie können auch feststellen, dass wir Zeiger für zwei zusätzliche Streams zugewiesen haben, die wir erstellen werden, sowie ein Exit-Flag und einen Filmdateinamen.

Nun kehren wir zur Hauptfunktion zurück, um zu sehen, wie dies unser Programm ändert. Lassen Sie uns unsere VideoState- Struktur einrichten :

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

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

av_mallocz () ist eine gute Funktion, die Speicher für uns reserviert und auf Null setzt.

Dann initialisieren wir unsere Sperren für den Anzeigepuffer ( pictq ), da die Anzeigeschleife vordecodierte Frames aus pictq abruft , da die Ereignisschleife unsere Anzeigefunktion aufruft . Gleichzeitig wird unser Videodecoder Informationen einfügen - wir wissen nicht, wer zuerst dort ankommt. Ich hoffe du verstehst, dass dies eine klassische Rennbedingung ist. Deshalb verteilen wir es jetzt, bevor wir mit Themen beginnen. Kopieren wir auch den Namen unseres Films in VideoState :

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

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

av_strlcpy ist eine Funktion von FFmpeg, die neben strncpy einige zusätzliche Randprüfungen durchführt .

Unser erster Thread


Lassen Sie uns unsere Threads ausführen und etwas Reales tun:

schedule_refresh(is, 40);

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

Schedule_Refresh ist eine Funktion, die wir später definieren werden. Sie weist das System an, FF_REFRESH_EVENT nach der angegebenen Anzahl von Millisekunden zu erzeugen . Dadurch wird die Videoaktualisierungsfunktion aufgerufen, wenn sie in der Ereigniswarteschlange angezeigt wird. Aber jetzt schauen wir uns SDL_CreateThread () an.

SDL_CreateThread () macht genau das - es erzeugt einen neuen Thread, der vollen Zugriff auf den gesamten Speicher des ursprünglichen Prozesses hat, und startet den Thread, der von der Funktion ausgeführt wird, die wir ihm geben. Diese Funktion überträgt auch benutzerdefinierte Daten. In diesem Fall rufen wir decode_thread () auf und hängen unsere VideoState- Struktur an. In der ersten Hälfte der Funktion gibt es nichts Neues. Es öffnet lediglich die Datei und findet den Index der Audio- und Videostreams. Das einzige, was wir anders machen, ist, den Formatkontext in unserer großen Struktur beizubehalten. Nachdem wir unsere Stream-Indizes gefunden haben, rufen wir eine andere von uns definierte Funktion auf, stream_component_open (). Dies ist eine ziemlich natürliche Art der Trennung. Da wir viele ähnliche Dinge tun, um den Video- und Audio-Codec einzurichten, verwenden wir einen Code wieder, wodurch er zu einer Funktion wird. Stream_component_open-

Funktion() Ist der Ort, an dem wir unseren Codec-Decoder entdecken, die Soundparameter konfigurieren, wichtige Informationen in unserer großen Struktur speichern und unsere Audio- und Videostreams starten. Hier fügen wir auch andere Parameter ein, z. B. die erzwungene Verwendung des Codecs anstelle seiner automatischen Erkennung usw. So:

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;
  }
}

Dies ist fast das gleiche wie der Code, den wir zuvor hatten, außer dass er jetzt für Audio und Video verallgemeinert ist. Beachten Sie, dass wir anstelle von aCodecCtx unsere große Struktur als Benutzerdaten für unseren Audio-Rückruf konfiguriert haben. Wir haben die Streams auch selbst als audio_st und video_st gespeichert . Wir haben auch unsere Video-Warteschlange hinzugefügt und genau wie unsere Audio-Warteschlange eingerichtet. Unter dem Strich werden Video- und Audio-Streams ausgeführt. Diese Bits tun dies:

    SDL_PauseAudio(0);
    break;

/* ...... */

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

Denken Sie an SDL_PauseAudio () aus der letzten Lektion. SDL_CreateThread () wird auf die gleiche Weise verwendet. Zurück zu unserer Funktion video_thread (). Kehren

wir vorher zur zweiten Hälfte unserer Funktion decode_thread () zurück. Im Wesentlichen ist es nur eine for-Schleife, die ein Paket liest und in die richtige Warteschlange stellt:

  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);
    }
  }

Hier gibt es nichts wirklich Neues, außer dass wir jetzt eine maximale Größe für unsere Audio- und Video-Warteschlange haben und die Lesefehlerprüfung hinzugefügt haben. Der Formatkontext hat eine ByteIOContext- Struktur namens pb . ByteIOContext ist eine Struktur, in der im Wesentlichen alle Informationen zu Dateien auf niedriger Ebene gespeichert werden.

Nach unserer for-Schleife haben wir den gesamten Code, um zu warten, bis der Rest des Programms abgeschlossen ist, oder um darüber zu informieren. Dieser Code ist aufschlussreich, da er zeigt, wie wir Ereignisse pushen - etwas, das wir später benötigen, um das Video anzuzeigen:

  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;

Wir erhalten Werte für benutzerdefinierte Ereignisse mit der SDL-Konstante SDL_USEREVENT . Das erste Benutzerereignis muss auf SDL_USEREVENT , das nächste SDL_USEREVENT + 1 usw. gesetzt sein. FF_QUIT_EVENT ist in unserem Programm als SDL_USEREVENT + 1 definiert . Bei Bedarf können wir auch Benutzerdaten übergeben, und hier übergeben wir unseren Zeiger auf eine große Struktur. Schließlich rufen wir SDL_PushEvent () auf. In unserem Ereignisschleifenschalter haben wir dies einfach in den Abschnitt SDL_QUIT_EVENT eingefügtdas hatten wir vorher. Wir werden unseren Zyklus der Ereignisse detaillierter sehen; Stellen Sie vorerst sicher, dass wir FF_QUIT_EVENT später abfangen und das Exit-Flag wechseln.

Bild empfangen: video_thread


Nachdem Sie den Codec vorbereitet haben, können Sie den Videostream starten. Dieser Stream liest Pakete aus der Video-Warteschlange, decodiert das Video in Frames und ruft dann die Funktion queue_picture auf, um den verarbeiteten Frame in die Bildwarteschlange zu stellen:

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;
}

Der größte Teil dieser Funktion sollte inzwischen verstanden werden. Wir haben gerade die Funktion avcodec_decode_video2 hier kopiert und einfach einige Argumente ersetzt. Zum Beispiel haben wir einen AVStream in unserer großen Struktur gespeichert, sodass wir unseren Codec von dort erhalten. Wir empfangen nur weiterhin Pakete aus unserer Video-Warteschlange, bis uns jemand zum Beenden auffordert oder wir einen Fehler finden.

Warteschlangenrahmen


Werfen wir einen Blick auf die Funktion, mit der unser dekodierter pFrame in unserer Bildwarteschlange gespeichert wird . Da es sich bei unserer Bildwarteschlange um eine Überlagerung von SDL handelt (vermutlich, damit die Videoanzeigefunktion so wenig Berechnungen wie möglich durchführen kann), müssen wir unseren Frame in diese konvertieren. Die Daten, die wir in der Bildwarteschlange speichern, sind die Struktur, die wir erstellt haben:

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

Unsere große Struktur enthält einen Puffer dieser Dateien, in dem wir sie speichern können. Wir müssen SDL_Overlay jedoch selbst verteilen (achten Sie auf das zugewiesene Flag, das anzeigt, ob wir es getan haben oder nicht).

Um diese Warteschlange zu verwenden, haben wir zwei Zeiger - den Schreibindex und den Leseindex. Wir verfolgen auch, wie viele tatsächliche Bilder sich im Puffer befinden. Um in die Warteschlange zu schreiben, warten wir zuerst, bis unser Puffer gelöscht ist, damit wir einen Platz zum Speichern unseres VideoPicture haben . Dann prüfen wir, ob wir das Overlay in unserem Datensatzindex festgelegt haben. Wenn nicht, müssen Sie Speicher zuweisen. Wir müssen auch den Puffer neu zuweisen, wenn sich die Fenstergröße geändert hat!

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;
    }
  }

Schauen wir uns die Funktion alloc_picture () an:

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;
}

Sie sollten die Funktion SDL_CreateYUVOverlay erkennen , die wir von unserer Hauptschleife in diesen Abschnitt verschoben haben. Dieser Code sollte inzwischen einigermaßen klar sein. Jetzt haben wir jedoch eine Mutex-Sperre, da zwei Threads nicht gleichzeitig Informationen auf den Bildschirm schreiben können! Dadurch kann unsere Funktion alloc_picture keine andere Funktion stören, die das Bild anzeigt. (Wir haben diese Sperre als globale Variable erstellt und in main () initialisiert . Siehe Code.) Beachten Sie, dass wir die Breite und Höhe in der VideoPicture- Struktur beibehalten , da wir sicherstellen müssen, dass sich die Größe unseres Videos aus irgendeinem Grund nicht ändert.
Ok, wir haben es geregelt und wir haben unser Overlay YUV, engagiert und bereit, das Bild zu empfangen. Kehren wir zu queue_picture zurück und sehen uns den Code an, um den Frame in das Overlay zu kopieren. Dieser Teil sollte Ihnen vertraut sein:

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;
}

Hier ist das meiste nur der Code, den wir zuvor verwendet haben, um das YUV- Overlay mit unserem Frame zu füllen . Das letzte Bit "fügt" einfach unseren Wert zur Warteschlange hinzu. Die Warteschlange funktioniert, Werte werden hinzugefügt, bis sie voll ist, und das Lesen erfolgt, solange sich mindestens etwas darin befindet. Daher hängt alles vom Wert von is -> pictq_size ab , weshalb wir ihn blockieren müssen. Also, was machen wir hier: Erhöhen Sie den Datensatzzeiger (und beginnen Sie gegebenenfalls von vorne), blockieren Sie dann die Warteschlange und vergrößern Sie sie. Jetzt wird unser Leser wissen, dass es mehr Informationen über die Warteschlange gibt, und wenn dies unsere Warteschlange voll macht, wird unser Rekorder davon erfahren.

Videoanzeige


Das ist alles für unseren Videothread! Jetzt haben wir alle freien Threads bis auf einen abgeschlossen - erinnern Sie sich, wie wir die Funktion sched_refresh () vor langer Zeit aufgerufen haben ? Schauen Sie sich an, was tatsächlich passiert ist:

/* 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 () ist eine SDL-Funktion, die nach einer bestimmten Anzahl von Millisekunden einfach einen Rückruf an eine benutzerdefinierte Funktion ausführt (und bei Bedarf einige benutzerdefinierte Daten überträgt). Wir werden diese Funktion verwenden, um Videoaktualisierungen zu planen. Jedes Mal, wenn wir sie aufrufen, wird ein Timer festgelegt, der ein Ereignis auslöst, wodurch unsere main () - Funktion eine Funktion aufruft, die einen Frame aus unserem Warteschlangenbild extrahiert und anzeigt ihr! Puh! Drei "welche / welche / welche" in einem Satz! Lassen Sie uns also als Erstes dieses Ereignis auslösen. Dies schickt uns zu:

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 */
}

Die Veranstaltung wird von unserem alten Freund ins Leben gerufen. FF_REFRESH_EVENT ist hier als SDL_USEREVENT + 1 definiert . Es ist zu beachten, dass die SDL den Timer stoppt, wenn wir 0 zurückgeben, sodass der Rückruf nicht erneut ausgeführt wird. Nachdem

wir FF_REFRESH_EVENT erneut aufgerufen haben , müssen wir es in unserer Ereignisschleife verarbeiten:

for(;;) {

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

Was schickt uns hierher zu dieser Funktion, die tatsächlich Daten aus unserer Bildwarteschlange extrahiert:

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);
  }
}

Im Moment ist diese Funktion ziemlich einfach: Sie verarbeitet die Warteschlange, während wir etwas haben, stellt einen Timer für die Anzeige des nächsten Videobilds ein, ruft video_display auf, um das Video tatsächlich auf dem Bildschirm anzuzeigen, erhöht dann den Zähler in der Warteschlange und verringert gleichzeitig seine Größe. Sie werden vielleicht bemerken, dass wir in dieser Funktion nicht wirklich etwas mit vp machen , und hier ist der Grund: Dies liegt vor uns. Aber etwas später. Wir werden es verwenden, um auf Zeitinformationen zuzugreifen, wenn wir beginnen, Video mit Audio zu synchronisieren. Schauen Sie sich hier die Stelle im Code an, an der der Kommentar „Timing-Code geht hierher“ geschrieben ist. In diesem Abschnitt werden wir , wie schnell um herauszufinden wir den nächsten Video - Frame anzeigen sollen, und dann diesen Wert in der Eingabe schedule_refresh Funktion(). Im Moment geben wir nur einen fiktiven Wert von 80 ein. Technisch gesehen können Sie diesen Wert erraten und überprüfen und für jeden Film neu kompilieren, aber: 1) er wird nach einer Weile langsamer und 2) er ist ziemlich dumm. Obwohl wir in Zukunft auf diesen Punkt zurückkommen werden.

Wir sind fast fertig. Es bleibt nur noch eines zu tun: Zeigen Sie das Video! Hier ist die Funktion 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);
  }
}

Da der Bildschirm eine beliebige Größe haben kann (wir haben 640 x 480 installiert und es gibt Möglichkeiten, ihn so anzupassen, dass die Größe des Benutzers geändert wird), müssen Sie dynamisch bestimmen, wie groß der rechteckige Bereich für unseren Film sein soll. Zuerst müssen Sie das Seitenverhältnis unseres Films herausfinden, nur die Breite geteilt durch die Höhe. Einige Codecs haben ein ungerades Seitenverhältnis des Samples, dh einfach die Breite / Höhe eines Pixels oder Samples. Da die Höhen- und Breitenwerte in unserem Codec-Kontext in Pixel gemessen werden, entspricht das tatsächliche Seitenverhältnis dem Seitenverhältnis multipliziert mit dem Seitenverhältnis für die Stichprobe. Einige Codecs zeigen ein Seitenverhältnis von 0, was bedeutet, dass jedes Pixel einfach eine Größe von 1x1 hat. Dann skalieren wir den Film sodamit es so gut wie möglich auf den Bildschirm passt. Bitumkehr& -3 rundet den Wert einfach auf das nächste Vielfache von vier. Zentrieren Sie dann den Film und rufen Sie SDL_DisplayYUVOverlay () auf, um sicherzustellen, dass der Bildschirmmutex für den Zugriff verwendet wird.

Und das ist alles? Sind wir fertig? Sie müssen den Audiocode noch neu schreiben, um das neue VideoStruct verwenden zu können. Dies sind jedoch triviale Änderungen, die im Beispielcode zu sehen sind. Als letztes müssen wir unseren Rückruf für die interne Exit-Rückruffunktion in FFmpeg ändern:

VideoState *global_video_state;

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

Setzen Sie global_video_state in main () auf eine große Struktur .

So, das war es! Wir kompilieren:

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

und genieße den Film ohne zu synchronisieren! Im nächsten Schritt werden wir endlich einen wirklich funktionierenden Videoplayer erstellen !






Lektion 5: Videosynchronisation


Vollständige Auflistung 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;

}

WARNUNG


Als ich gerade dieses Handbuch geschrieben habe, wurde mein gesamter Synchronisationscode aus der damaligen Version von ffplay.c übernommen . Heute ist es ein völlig anderes Programm, und Aktualisierungen in den FFmpeg-Bibliotheken (und in ffplay.c selbst) haben zu grundlegenden Änderungen geführt. Obwohl dieser Code immer noch funktioniert, ist er bereits veraltet und es gibt viele andere Verbesserungen, die in diesem Handbuch verwendet werden könnten.

Wie Video synchronisiert


Bis jetzt hatten wir einen fast nutzlosen Movie Player. Ja, es wird Video abgespielt, und ja, es wird Audio abgespielt, aber das ist nicht ganz das, was wir einen Film nennen würden. Was machen wir dann?

PTS und DTS


Glücklicherweise enthalten Audio- und Videostreams Informationen darüber, wie schnell und zu welchen Zeitpunkten sie abgespielt werden sollen. Audio-Streams haben eine Abtastrate und Video-Streams haben Bilder pro Sekunde. Wenn wir das Video jedoch nur synchronisieren, indem wir die Anzahl der Bilder zählen und mit der Bildrate multiplizieren, besteht eine gute Chance, dass es nicht mit dem Ton synchronisiert wird. Deshalb werden wir den anderen Weg gehen. Pakete aus dem Strom können die sogenannten haben Dekodierungszeitstempel ( DTS - von d ecoding t ime s Stampf ) und Präsentationszeitstempel ( PTS - aus p RÄSENTATION t ime sTamp ). Um diese beiden Bedeutungen zu verstehen, müssen Sie wissen, wie Filme gespeichert werden. Einige Formate, wie z. B. MPEG, verwenden sogenannte B-Frames ( Bed and is bidirektional, England. Bidirektional ). Zwei andere Arten von Frames werden als I-Frames und P-Frames ( I ist intern , i nner und P Mittel vorhergesagt , p redicted ). I-Frames enthalten das Vollbild. P-Frameshängen von vorherigen I- und P-Frames ab und unterscheiden sich von vorherigen Frames, oder Sie können auch - Deltas benennen. B-Frames ähneln P-Frames, hängen jedoch von den Informationen in vorherigen und nachfolgenden Frames ab! Die Tatsache, dass ein Frame möglicherweise nicht das Bild selbst enthält, sondern sich von anderen Frames unterscheidet, erklärt, warum wir nach dem Aufruf von avcodec_decode_video2 möglicherweise keinen fertigen Frame haben .

Nehmen wir an, wir haben einen Film mit 4 Bildern in dieser Reihenfolge: IBBP . Dann müssen wir die Informationen aus dem letzten P-Frame herausfinden, bevor wir einen der beiden vorherigen B-Frames anzeigen können. Aus diesem Grund können Frames in einer Reihenfolge gespeichert werden, die nicht der tatsächlichen Anzeigereihenfolge entspricht: IPBB. Dafür sind der Dekodierungszeitstempel und der Präsentationszeitstempel für jeden Frame vorgesehen. Der Dekodierungszeitstempel sagt uns, wann wir etwas dekodieren müssen, und der Präsentationszeitstempel sagt uns, wann wir etwas anzeigen müssen. In diesem Fall sieht unser Stream also möglicherweise folgendermaßen aus:

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

In der Regel unterscheiden sich PTS und DTS nur, wenn der abgespielte Stream B-Frames enthält.

Wenn wir ein Paket von av_read_frame () erhalten, enthält es die PTS- und DTS-Werte für die Informationen, die sich im Paket befinden. Was wir aber wirklich brauchen, ist das PTS unseres neu dekodierten Rohrahmens. In diesem Fall wissen wir, wann es angezeigt werden muss.

Glücklicherweise liefert uns FFmpeg den „bestmöglichen Zeitstempel“, den wir mit der Funktion av_frame_get_best_effort_timestamp () erhalten können .

Synchronisation


Damit Frames der Reihe nach angezeigt werden, ist es schön zu wissen, wann ein bestimmtes Videobild angezeigt werden soll. Aber wie genau machen wir das? Die Idee ist folgende: Nachdem wir den Rahmen gezeigt haben, finden wir heraus, wann der nächste Rahmen gezeigt werden soll. Dann machen Sie einfach eine Pause, danach aktualisieren wir das Video nach dieser Zeit. Wie erwartet überprüfen wir den PTS-Wert des nächsten Frames auf der Systemuhr, um festzustellen, wie lange unsere Wartezeit sein sollte. Dieser Ansatz funktioniert, aber es gibt zwei Probleme, die angegangen werden müssen.

Zunächst stellt sich die Frage, wann der nächste PTS sein wird. Sie werden sagen, dass Sie einfach die Videofrequenz zum aktuellen PTS hinzufügen können - und Sie haben im Prinzip Recht. Für einige Arten von Videos müssen jedoch Frames wiederholt werden. Dies bedeutet, dass Sie den aktuellen Frame einige Male wiederholen müssen. Dies kann dazu führen, dass das Programm das nächste Bild zu früh anzeigt. Dies muss berücksichtigt werden.

Das zweite Problem ist, dass in dem Programm, das wir gerade geschrieben haben, Video und Audio freudig vorwärts eilen, bis sie sich überhaupt die Mühe machen, sich zu synchronisieren. Wir müssten uns keine Sorgen machen, wenn alles für sich perfekt funktionieren würde. Ihr Computer ist jedoch nicht perfekt, ebenso wie viele Videodateien. Daher haben wir drei Möglichkeiten: Audio mit Video synchronisieren, Video mit Audio synchronisieren oder Audio und Video mit einer externen Uhr synchronisieren (z. B. mit Ihrem Computer). Jetzt werden wir das Video mit Audio synchronisieren.

Codierung: Empfang eines PTS-Frames


Jetzt schreiben wir etwas direkt. Wir müssen unserer großen Struktur noch ein paar Teile hinzufügen, und wir werden es so machen, wie wir es brauchen. Schauen wir uns zunächst unseren Videothread an. Denken Sie daran, dass wir hier Pakete sammeln, die von unserem Dekodierungsstrom in die Warteschlange gestellt wurden? In diesem Teil des Codes müssen wir den PTS für den Frame abrufen , den avcodec_decode_video2 uns gegeben hat . Der erste Weg, über den wir gesprochen haben, besteht darin, die DTS des zuletzt verarbeiteten Pakets zu erhalten, was ziemlich einfach ist:

  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);

Wir setzen das PTS auf Null, wenn wir seinen Wert nicht bestimmen können.

Das war einfach. Technischer Hinweis: Wie Sie sehen, verwenden wir int64 für PTS. Dies liegt daran, dass der PTS als Ganzzahl gespeichert ist. Dieser Wert ist ein Zeitstempel, der der Zeitdimension in timebb entspricht . Wenn der Stream beispielsweise 24 Frames pro Sekunde hat, gibt PTS von 42 an, dass der Frame dort verwendet werden soll, wo der 42. Frame sein soll, vorausgesetzt, wir haben Frames alle 1/24 Sekunde ersetzt (dies muss natürlich nicht so sein tatsächlich).

Wir können diesen Wert in Sekunden umwandeln, indem wir ihn durch die Bildrate dividieren. Time_base WertDer Stream ist gleich 1 geteilt durch die Framerate (für Inhalte mit einer festen Framerate) . Um den PTS in Sekunden zu erhalten, multiplizieren wir ihn mit time_base .

Code weiter: Synchronisation und Verwendung von PTS


Jetzt haben wir also alle fertigen PTS. Jetzt werden wir uns um die beiden Synchronisationsprobleme kümmern, die etwas höher diskutiert wurden. Wir werden eine Funktion synchronize_video definieren , die den PTS aktualisiert, um mit allem zu synchronisieren. Diese Funktion behandelt schließlich auch Fälle, in denen wir den PTS-Wert für unseren Frame nicht erhalten. Gleichzeitig müssen wir verfolgen, wann der nächste Frame erwartet wird, damit wir die Aktualisierungsrate korrekt einstellen können. Wir können dies mit dem internen video_clock- Wert tun , der verfolgt, wie viel Zeit für das Video vergangen ist. Diesen Wert fügen wir unserer großen Struktur hinzu:

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

Hier ist die Funktion synchronize_video , die ziemlich klar ist:

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;
}

Wie Sie sehen können, berücksichtigen wir in dieser Funktion wiederholte Frames.

Jetzt wollen wir unsere richtige PTS erhalten und den Rahmen Warteschlange mit queue_picture durch einen neuen Zusatz - Punkte Argument :

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

Das einzige , was Veränderungen in queue_picture ist , dass wir diese speichern pts Wert im Videobild Struktur , dass wir Warteschlange. Daher müssen wir die Variable pts zur Struktur hinzufügen und diese Codezeilen hinzufügen:

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 ...
  }

Jetzt haben wir die Bilder mit den richtigen PTS-Werten in die Warteschlange gestellt. Schauen wir uns also unsere Video-Update-Funktion an. Sie können sich aus der letzten Lektion daran erinnern, dass wir es einfach gefälscht und ein Update von 80 ms installiert haben. Nun werden wir herausfinden, was wirklich da sein sollte.

Unsere Strategie ist es durch einfaches Messen der Zeit zwischen den aktuellen Zeit der nächsten PTS zur Vorhersage Punkte und die vorherigen. Gleichzeitig müssen wir das Video mit Audio synchronisieren. Wir werden eine Audiouhr machen .: Ein interner Wert, der die Position des abgespielten Audios verfolgt. Es ist wie eine digitale Anzeige auf jedem MP3-Player. Da wir das Video mit dem Ton synchronisieren, verwendet der Videostream diesen Wert, um herauszufinden, ob er zu weit vorne oder zu weit hinten liegt.

Wir werden später zur Implementierung zurückkehren. Nehmen wir nun an, wir haben die Funktion get_audio_clockDas gibt uns Zeit auf der Audio-Uhr. Was ist zu tun, wenn Video und Audio nicht synchronisiert sind, sobald wir diesen Wert erhalten? Es wäre dumm, nur zu versuchen, durch eine Suche oder etwas anderes zum richtigen Paket zu springen. Stattdessen passen wir einfach den Wert an, den wir für das nächste Update berechnet haben: Wenn der PTS zu weit hinter der Audiozeit liegt, verdoppeln wir unsere geschätzte Verzögerung. Wenn der PTS der Spielzeit zu weit voraus ist, aktualisieren wir ihn einfach so schnell wie möglich. Nachdem wir die konfigurierte Aktualisierungs- oder Verzögerungszeit haben, werden wir sie mit der Uhr unseres Computers vergleichen und frame_timer laufen lassen . Dieser Frame-Timer fasst alle geschätzten Verzögerungen während der Filmwiedergabe zusammen. Mit anderen Worten, dieser frame_timer- Dies ist die Zeit, die angibt, wann das nächste Bild angezeigt werden soll. Wir fügen dem Frame-Timer einfach eine neue Verzögerung hinzu, vergleichen sie mit der Uhrzeit auf unserem Computer und verwenden diesen Wert, um das nächste Update zu planen. Dies kann etwas verwirrend sein. Lesen Sie den Code daher sorgfältig durch:

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);
  }
}

Wir führen mehrere Überprüfungen durch: Erstens stellen wir sicher, dass die Verzögerung zwischen dem aktuellen PTS und dem vorherigen PTS sinnvoll ist. Wenn keine Verzögerung erforderlich ist, stimmen Audio und Video zu diesem Zeitpunkt überein und verwenden nur die letzte Verzögerung. Dann stellen wir sicher, dass die Synchronisationsschwelle erfüllt ist, da eine perfekte Synchronisation niemals stattfindet. FFplay verwendet einen Wert von 0,01 für den Schwellenwert. Wir stellen auch sicher, dass der Synchronisationsschwellenwert niemals kleiner als die Intervalle zwischen den PTS-Werten ist. Stellen Sie schließlich den minimalen Aktualisierungswert auf 10 Millisekunden ein (tatsächlich sollten sie den Frame hier überspringen, aber machen wir uns darüber keine Sorgen).

Wir haben der großen Struktur eine Reihe von Variablen hinzugefügt. Vergessen Sie also nicht, den Code zu überprüfen. Vergessen Sie auch nicht, den Frame-Timer und die anfängliche Verzögerung des vorherigen Frames in stream_component_open zu initialisieren :

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

Sync: Audiouhr


Es ist an der Zeit, die Audiouhr zu realisieren. Wir können die Zeit in unserer Funktion audio_decode_frame aktualisieren , in der wir das Audio dekodieren. Denken Sie jetzt daran, dass wir nicht jedes Mal, wenn wir diese Funktion aufrufen, ein neues Paket verarbeiten. Es gibt also zwei Bereiche, in denen Sie die Uhr aktualisieren müssen. Als erstes erhalten wir das neue Paket: Installieren Sie einfach die Sound Clock auf dem PTS-Paket. Wenn das Paket dann mehrere Frames enthält, sparen wir die Audiowiedergabezeit, indem wir die Anzahl der Samples zählen und diese mit einer bestimmten Sampling-Frequenz pro Sekunde multiplizieren. Also, wenn wir das Paket haben:

    /* 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;
    }

Und sobald wir das Paket bearbeiten:

      /* 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);

Ein paar kleine Nuancen: Die Funktionsvorlage wurde geändert und enthält jetzt pts_ptr. Ändern Sie sie daher unbedingt . pts_ptr ist der Zeiger , dass wir sagen , verwenden die audio_callback Audiopaket pts . Dies wird beim nächsten Mal verwendet, um Audio mit Video zu synchronisieren.

Jetzt können wir endlich unsere Funktion get_audio_clock implementieren . Es ist nicht so einfach , wie Sie den Wert bekommen wird -> audio_clock , wenn man darüber nachdenkt. Bitte beachten Sie, dass wir PTS-Audio jedes Mal einstellen, wenn wir es verarbeiten, aber wenn Sie sich die Funktion audio_callback ansehenEs wird einige Zeit dauern, bis alle Daten aus unserem Audiopaket in unseren Ausgabepuffer verschoben sind. Dies bedeutet, dass der Wert in unserer Audiouhr möglicherweise zu weit voraus ist. Daher müssen wir überprüfen, wie viel wir schreiben müssen. Hier ist der vollständige Code:

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;
}

Du solltest jetzt verstehen, warum diese Funktion funktioniert;)

Also, das war's! Wir kompilieren:

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

Es passierte! Sie können den Film auf einem selbst erstellten Player ansehen. In der nächsten Lektion werden wir uns mit der Audiosynchronisation befassen und dann lernen, wie man sucht.

FFmpeg- und SDL-Handbuch oder Schreiben eines Videoplayers in weniger als 1000 Zeilen - Teil 2



Übersetzungen im Edison Blog:


All Articles