[Bagian 1/2] Panduan untuk FFmpeg dan SDL atau Cara menulis pemutar video dalam waktu kurang dari 1000 baris


Meskipun informasi ini sudah usang, materi asli masih menjadi sumber inspirasi populer untuk berbagai konten yang berguna pada topik FFmpeg. Namun, masih belum ada terjemahan lengkap dari aslinya ke dalam bahasa Rusia. Kami memperbaiki kelalaian yang mengganggu, karena itu lebih baik terlambat daripada tidak sama sekali.

Dan meskipun kami sudah mencoba, kesulitan penerjemahan tidak dapat dihindari dalam teks yang begitu banyak . Laporkan bug (lebih disukai dalam pesan pribadi) - bersama-sama kami akan melakukan yang lebih baik.

Daftar Isi

Perangkat Lunak EDISON - pengembangan web
EDISON.

, C C++.

! ;-)


UPD: Panduan ini telah diperbarui pada Februari 2015.

FFmpeg adalah perpustakaan yang hebat untuk membuat aplikasi video serta utilitas untuk keperluan umum. FFmpeg menangani seluruh rutin pemrosesan video, melakukan semua decoding, encoding, multiplexing dan demultiplexing. Yang sangat menyederhanakan pembuatan aplikasi media. Semuanya cukup sederhana dan cepat, ditulis dalam C, Anda dapat mendekode hampir semua codec yang tersedia saat ini, serta menyandikan ke beberapa format lain.

Satu-satunya tangkapan adalah bahwa sebagian besar dokumentasi hilang. Ada satu tutorial ( dalam bahasa aslinya, ini adalah tautan ke halaman web yang sudah tidak ada - penerjemah catatan), yang mencakup dasar-dasar FFmpeg dan generasi otomatis dok dok. Dan tidak ada lagi. Oleh karena itu, saya memutuskan untuk mencari tahu cara menggunakan FFmpeg secara mandiri untuk membuat aplikasi video dan audio digital yang berfungsi, dan pada saat yang sama mendokumentasikan proses dan menyajikannya dalam bentuk buku teks.

Ada program FFplay yang dilengkapi dengan FFmpeg. Ini sederhana, ditulis dalam C, mengimplementasikan pemutar video lengkap menggunakan FFmpeg. Pelajaran pertama saya adalah versi terbaru dari pelajaran asli oleh Martin Boehme ( dalam aslinya, tautan ke halaman web yang sudah tidak ada - catatan penerjemah ) - Saya menyeret beberapa bagian dari sana. Dan juga dalam serangkaian pelajaran saya, saya akan menunjukkan proses membuat pemutar video yang berfungsi berdasarkan ffplay.cFabrice Bellard. Setiap pelajaran akan menyajikan ide baru (atau bahkan dua) dengan penjelasan penerapannya. Setiap bab dilengkapi dengan daftar C, yang dapat Anda kompilasi dan jalankan sendiri. File sumber akan menunjukkan bagaimana program ini bekerja, bagaimana bagian-bagiannya bekerja, dan juga menunjukkan detail teknis kecil yang tidak tercakup dalam panduan ini. Ketika kita selesai, kita akan memiliki pemutar video yang berfungsi ditulis dalam kurang dari 1000 baris kode!

Saat membuat pemutar, kami akan menggunakan SDL untuk mengeluarkan file media audio dan video. SDL adalah perpustakaan multimedia lintas platform luar biasa yang digunakan dalam program pemutaran MPEG, emulator, dan banyak video game. Anda perlu mengunduh dan menginstal perpustakaan SDL di sistem Anda untuk mengkompilasi program dari panduan ini.

Tutorial ini untuk orang-orang dengan pengalaman pemrograman yang baik. Paling tidak, Anda perlu tahu C, dan juga memiliki pemahaman konsep-konsep seperti antrian, mutex, dll. Harus ada pemahaman tentang multimedia; misalnya, hal-hal seperti bentuk gelombang dan sejenisnya. Namun, menjadi guru dalam hal-hal ini tidak perlu, karena banyak konsep akan dijelaskan dalam pelajaran.

Silakan mengirimi saya pesan kesalahan, pertanyaan, komentar, ide, fitur, apa pun, di Dranger Doggy Gmail dot Com.







Baca juga di blog
perusahaan EDISON:


Manual libav FFmpeg






Pelajaran 1: Membuat Screencaps


Daftar lengkap: 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;
}

Gambaran


File film memiliki beberapa komponen utama. Pertama, file itu sendiri disebut wadah , dan jenis wadah menentukan bagaimana data diwakili dalam file. Contoh wadah adalah AVI dan Quicktime . Selanjutnya, ada beberapa utas dalam file; khususnya, biasanya ada aliran audio dan aliran video . ("Stream" adalah kata yang lucu untuk "urutan item data yang tersedia sesuai dengan timeline.") Item data dalam aliran disebut frame . Setiap aliran dikodekan oleh satu atau lain jenis codec . Codec menentukan bagaimana data aktual untuk diruyutsya dan DesemberDiaudit - oleh karena itu nama codec. Contoh codec adalah DivX dan MP3. Paket kemudian dibaca dari aliran. Paket adalah potongan data yang dapat berisi bit data yang diterjemahkan ke dalam bingkai mentah, yang akhirnya dapat kita manipulasi dalam aplikasi kita. Untuk keperluan kami, setiap paket berisi frame penuh (atau beberapa frame jika itu audio).

Bekerja dengan stream video dan audio sangat sederhana bahkan pada tingkat paling dasar:

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

Bekerja dengan multimedia menggunakan FFmpeg hampir sesederhana dalam program ini, meskipun dalam beberapa program langkah "MAKE [...]" bisa sangat sulit. Dalam tutorial ini, kita akan membuka file, menghitung aliran video di dalamnya, dan "MAKE [...]" kami akan menulis bingkai ke file PPM.

Membuka file


Hal pertama yang pertama, mari kita lihat apa yang terjadi pertama kali ketika Anda membuka file. Menggunakan FFmpeg, kami pertama-tama menginisialisasi perpustakaan yang diinginkan:

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

Ini mencatat semua format file dan codec yang tersedia di perpustakaan, sehingga mereka akan digunakan secara otomatis saat membuka file dengan format / codec yang sesuai. Perhatikan bahwa Anda perlu memanggil av_register_all () hanya sekali, jadi kami melakukannya di sini di main (). Jika mau, Anda hanya dapat mendaftarkan format file dan codec selektif, tetapi biasanya tidak ada alasan khusus untuk melakukannya.

Sekarang buka file:

AVFormatContext *pFormatCtx = NULL;

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

Dapatkan nama file dari argumen pertama. Fungsi ini membaca header file dan menyimpan informasi format file dalam struktur AVFormatContext yang kami lewati. Tiga argumen terakhir digunakan untuk menentukan format file, ukuran buffer, dan parameter format. Dengan mengaturnya ke NULL atau 0, libavformat akan mendeteksi semuanya secara otomatis.

Fungsi ini hanya melihat header, jadi sekarang kita perlu memeriksa informasi aliran di file:

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

Fungsi ini meneruskan data yang valid ke pFormatCtx -> stream . Kami berkenalan dengan fungsi debugging yang nyaman, menunjukkan kepada kami apa yang ada di dalamnya:

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

Sekarang pFormatCtx -> stream hanyalah sebuah array pointer ukuran pFormatCtx -> nb_streams . Kami akan melewatinya sampai kami menemukan aliran video:

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;

Informasi tentang codec dalam aliran terletak di tempat yang disebut " konteks codec ". Ini berisi semua informasi tentang codec yang digunakan stream, dan sekarang kami memiliki pointer ke sana. Tetapi kita masih harus menemukan codec asli dan membukanya:

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

Harap dicatat bahwa Anda tidak dapat langsung menggunakan AVCodecContext dari aliran video! Oleh karena itu, Anda harus menggunakan vcodec_copy_context () untuk menyalin konteks ke lokasi baru (tentu saja, setelah memori dialokasikan untuk itu).

Penyimpanan data


Sekarang kita membutuhkan tempat untuk menyimpan bingkai:

AVFrame *pFrame = NULL;

// Allocate video frame
pFrame=av_frame_alloc();

Karena kami berencana untuk menghasilkan file PPM yang disimpan dalam RGB 24-bit, kami perlu mengubah frame kami dari formatnya sendiri ke RGB. FFmpeg akan melakukannya untuk kita. Untuk sebagian besar proyek (termasuk yang ini), Anda perlu mengonversi kerangka awal ke format tertentu. Pilih bingkai untuk bingkai yang dikonversi:

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

Terlepas dari kenyataan bahwa kami memilih bingkai, kami masih membutuhkan tempat untuk menampung data mentah saat mengonversinya. Kami menggunakan avpicture_get_size untuk mendapatkan ukuran yang tepat dan mengalokasikan ruang yang diperlukan secara manual:

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 adalah analog dari malloc C-function dari FFmpeg, yang merupakan pembungkus sederhana di sekitar malloc yang menyediakan perataan alamat memori, dll. Omong-omong, ini tidak melindungi terhadap kebocoran memori, pembebasan ganda atau masalah lain yang terjadi dengan malloc .

Sekarang kita menggunakan avpicture_fill untuk mengaitkan frame dengan buffer yang baru dialokasikan. Mengenai AVPicture : struktur AVPicture adalah bagian dari struktur AVFrame - awal dari struktur AVFrame identik dengan struktur AVPicture .

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

Kami sudah berada di garis finish! Sekarang kita siap membaca dari aliran!

Membaca data


Sekarang, untuk membaca seluruh aliran video, kita membaca paket berikutnya, mendekripsi dalam bingkai kita, dan segera setelah dekripsi selesai, konversi bingkai dan simpan:

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

Tidak ada yang rumit: av_read_frame () membaca paket dan menyimpannya dalam struktur AVPacket . Harap dicatat bahwa kami hanya mendistribusikan struktur paket - FFmpeg memberi kami data internal yang ditunjukkan oleh packet.data . Ini membebaskan av_free_packet () sedikit kemudian . avcodec_decode_video () mengubah paket menjadi bingkai. Namun, kami mungkin tidak memiliki semua informasi yang kami butuhkan untuk frame setelah mendekode paket, oleh karena itu avcodec_decode_video () menetapkan frameFinished ketika kami memiliki frame berikutnya. Akhirnya, kami menggunakan sws_scale () untuk mengkonversi dari format kami sendiri ( pCodecCtx ->pix_fmt ) di RGB. Ingatlah bahwa Anda dapat melemparkanpenunjuk AVFrame ke penunjuk AVPicture . Akhirnya, kami meneruskan informasi tentang bingkai, tinggi dan lebar fungsi SaveFrame kami.

Berbicara tentang paket. Secara teknis, sebuah paket hanya dapat berisi bagian dari sebuah frame, serta bit data lainnya. Namun, parser FFmpeg menjamin bahwa paket yang kami terima mengandung frame penuh atau bahkan beberapa frame.

Sekarang yang masih harus dilakukan adalah menggunakan fungsi SaveFrame untuk menulis informasi RGB ke file PPM. Meskipun kami secara dangkal berurusan dengan format PPM itu sendiri; percayalah, semuanya bekerja di sini:

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

Kami melakukan file standar terbuka, dll., Dan kemudian merekam data RGB. File ini ditulis baris demi baris. File PPM hanyalah sebuah file di mana informasi RGB disajikan sebagai garis panjang. Jika Anda tahu warna HTML, itu akan seperti menandai warna setiap piksel dari ujung pertama ke terakhir, sesuatu seperti # ff0000 # ff0000 .... , seperti untuk layar merah. (Sebenarnya, ini disimpan dalam format biner dan tanpa pemisah, tapi saya harap Anda menangkap idenya.) Judul menunjukkan seberapa lebar dan tinggi gambar, serta ukuran maksimum nilai RGB.

Sekarang kembali ke fungsi () utama kami . Segera setelah kami selesai membaca dari streaming video, kami hanya perlu menghapus semuanya:

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

Seperti yang Anda lihat, kami menggunakan av_free untuk memori yang dialokasikan menggunakan avcode_alloc_frame dan av_malloc .

Itu saja kodenya! Sekarang, jika Anda menggunakan Linux atau platform serupa, jalankan:

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

Jika Anda memiliki versi FFmpeg yang lebih lama, Anda mungkin perlu menghapus -lavutil :

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

Sebagian besar program grafis harus membuka format PPM. Lihatlah beberapa file film yang skreencaps-nya dibuat menggunakan program kami.






Pelajaran 2: Menampilkan layar


Daftar lengkap: 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 dan video


Untuk menggambar di layar kita akan menggunakan SDL. SDL singkatan dari Simple Direct Layer . Ini adalah perpustakaan multimedia lintas platform yang sangat baik yang digunakan dalam banyak proyek. Anda bisa mendapatkan perpustakaan di situs web resmi atau mengunduh paket pengembang untuk sistem operasi Anda, jika ada. Anda akan memerlukan perpustakaan untuk mengkompilasi kode dari pelajaran ini (semua pelajaran lain, omong-omong, ini juga berlaku)

SDL memiliki banyak metode untuk menggambar di layar. Salah satu cara untuk menampilkan film adalah apa yang disebut overlay YUV .

Secara formal, bahkan YUV, tetapi YCbCr. Ngomong-ngomong, beberapa orang menjadi sangat terbakar ketika "YCbCr" disebut sebagai "YUV". Secara umum, YUV adalah format analog, dan YCbCr adalah format digital. FFmpeg dan SDL dalam kode mereka dan di makro menunjuk YCbCr sebagai YUV, tapi itu.

YUV adalah metode penyimpanan data gambar mentah seperti RGB. Secara kasar, Y adalah komponen kecerahan , dan U dan V adalah komponen warna . (Ini lebih rumit daripada RGB karena bagian dari informasi warna dibuang, dan Anda hanya dapat memiliki 1 pengukuran U dan V untuk setiap 2 pengukuran Y ). Hamparan YUVdi SDL menerima dataset YUV mentah dan menampilkannya. Ia menerima 4 jenis format YUV yang berbeda, tetapi YV12 adalah yang tercepat. Ada format YUV lain yang disebut YUV420P yang cocok dengan YV12, kecuali bahwa array U dan V ditukar. 420 berarti sampel diambil pada rasio 4: 2: 0, yaitu, untuk setiap 4 pengukuran kecerahan terdapat 1 pengukuran warna, sehingga informasi warna didistribusikan dalam kuartal. Ini adalah cara yang baik untuk menghemat bandwidth karena mata manusia masih tidak melihat perubahan ini. Huruf Latin "P" dalam nama menunjukkan bahwa formatnya adalah "planar", yang berarti bahwa komponennya adalah Y ,U dan V berada dalam array yang terpisah. FFmpeg dapat mengonversi gambar ke YUV420P , yang sangat membantu, karena banyak aliran video sudah disimpan dalam format ini atau mudah dikonversi ke dalamnya.

Dengan demikian, rencana kami saat ini adalah untuk menggantikan fungsi SaveFrame () dari pelajaran sebelumnya dan menampilkan bingkai kami sebagai gantinya. Tetapi pertama-tama Anda harus berkenalan dengan fitur-fitur dasar perpustakaan SDL. Untuk memulai, hubungkan perpustakaan dan inisialisasi 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 () pada dasarnya memberi tahu perpustakaan fungsi mana yang akan kita gunakan. SDL_GetError (), tentu saja, ini adalah fungsi nyaman kami untuk debugging.

Pembuatan tampilan


Sekarang kita membutuhkan tempat di layar untuk mengatur elemen. Area utama untuk menampilkan gambar dengan SDL disebut permukaan :

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

Jadi kami mengatur layar dengan lebar dan tinggi yang diberikan. Opsi berikutnya adalah kedalaman bit layar - 0 - ini adalah nilai khusus yang berarti "sama dengan tampilan saat ini."

Sekarang kami membuat overlay YUV pada layar ini sehingga kami dapat menampilkan video, dan mengkonfigurasi SWSContext kami untuk mengonversi data gambar ke YUV420 :

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

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

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

Seperti yang disebutkan, kami menggunakan YV12 untuk menampilkan gambar dan mendapatkan data YUV420 dari FFmpeg.

Tampilan gambar


Yah, itu cukup mudah! Sekarang kita hanya perlu menunjukkan gambarnya. Mari kita pergi ke tempat di mana kita memiliki tembakan selesai. Kami dapat menyingkirkan semua yang kami miliki untuk bingkai RGB dan kami akan mengganti SaveFrame () dengan kode tampilan kami. Untuk menampilkan gambar, kita akan membuat struktur AVPicture dan mengatur pointer data dan ukuran garis untuknya untuk overlay YUV kami :

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

Pada awalnya, kami memblokir overlay, karena kami berencana untuk menulisnya. Ini adalah kebiasaan yang baik sehingga nantinya tidak ada masalah. Struktur AVPicture , seperti yang ditunjukkan di atas, memiliki pointer data, yang merupakan array dari 4 pointer. Karena di sini kita berurusan dengan YUV420P , kita hanya memiliki 3 saluran dan karenanya hanya 3 set data. Format lain mungkin memiliki pointer keempat untuk saluran alpha atau yang lainnya. Ukuran garis seperti apa. Struktur serupa dalam hamparan YUV kami adalah variabel untuk piksel dan tinggi. (Pitch, pitches - jika dinyatakan dalam SDL untuk menunjukkan lebar dari suatu baris data yang diberikan.) Jadi, kami menunjukkan tiga array pict.data pada overlay kami, jadi ketika kami menulis dalamfoto , kami sebenarnya merekam dalam overlay kami, yang, tentu saja, sudah memiliki ruang yang diperlukan dialokasikan khusus untuk itu. Dengan cara yang sama, kami mendapatkan informasi ukuran garis langsung dari hamparan kami. Kami mengubah format konversi ke PIX_FMT_YUV420P dan menggunakan sws_scale seperti sebelumnya.

Gambar gambar


Tapi kami masih perlu menentukan untuk SDL sehingga benar-benar menunjukkan data yang kami berikan kepadanya. Kami juga meneruskan persegi panjang ke fungsi ini, yang menunjukkan ke mana film harus pergi, dengan lebar dan tinggi apa yang harus diskalakan. Dengan demikian, SDL berskala untuk kami, dan ini dapat membantu GPU Anda untuk skala lebih cepat:

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

Sekarang video kami ditampilkan!

Mari kita tunjukkan satu lagi fitur SDL: sistem acara . SDL dikonfigurasi sedemikian rupa sehingga ketika Anda memasukkan atau memindahkan mouse dalam aplikasi SDL atau mengirim sinyal ke sana, suatu peristiwa dihasilkan. Program Anda kemudian memeriksa peristiwa ini jika dimaksudkan untuk memproses input pengguna. Program Anda juga dapat membuat acara untuk mengirim acara SDL ke sistem. Ini sangat berguna untuk pemrograman multi-berulir dengan SDL, yang akan kita lihat dalam pelajaran nomor 4. Dalam program kami, kami akan memeriksa acara segera setelah pemrosesan paket. Saat ini, kami akan menangani acara SDL_QUIT sehingga kami dapat keluar:

SDL_Event       event;

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

Jadi kita hidup! Kami membuang semua sampah lama dan kami siap untuk dikompilasi. Jika Anda menggunakan Linux atau sesuatu seperti Linux, cara terbaik untuk mengompilasi menggunakan perpustakaan SDL adalah:

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

sdl-config hanya menampilkan flag yang diperlukan untuk gcc agar dapat mengaktifkan pustaka SDL dengan benar. Anda mungkin harus melakukan sesuatu yang lain untuk membuat kompilasi ini di sistem Anda; silakan periksa dokumentasi SDL untuk sistem Anda untuk petugas pemadam kebakaran. Setelah dikompilasi, lanjutkan dan jalankan.

Apa yang terjadi ketika Anda menjalankan program ini? Video itu sepertinya menjadi gila! Bahkan, kami cukup menampilkan semua frame video secepat kami dapat mengekstraknya dari file film. Kami tidak memiliki kode sekarang untuk mencari tahu kapan kami perlu menampilkan video. Pada akhirnya (dalam pelajaran nomor 5) kita akan mulai menyinkronkan video. Tetapi saat ini kita kehilangan sesuatu yang sama pentingnya: suara!






Pelajaran 3: Mainkan Suara


Daftar lengkap: 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


Sekarang kami ingin suara diputar di aplikasi. SDL juga memberi kami metode untuk memutar suara. Fungsi SDL_OpenAudio () digunakan untuk membuka perangkat audio itu sendiri. Dibutuhkan argumen struktur SDL_AudioSpec , yang berisi semua informasi tentang audio yang akan kita putar.

Sebelum kami menunjukkan cara mengkonfigurasi ini, kami terlebih dahulu menjelaskan bagaimana komputer memproses audio secara umum. Audio digital terdiri dari aliran sampel yang panjang, yang masing-masing mewakili makna spesifik dari gelombang suara. Suara direkam pada laju sampling tertentu, yang hanya menunjukkan seberapa cepat setiap sampel dimainkan, dan diukur dengan jumlah sampel per detik. Frekuensi pengambilan sampel perkiraan adalah 22.050 dan 44.100 sampel per detik, yang merupakan kecepatan yang digunakan untuk radio dan CD. Selain itu, sebagian besar audio dapat memiliki lebih dari satu saluran untuk stereo atau surround sound, jadi, misalnya, jika sampel dalam stereo, sampel akan datang dua sekaligus. Ketika kami mendapatkan data dari file film, kami tidak tahu berapa banyak sampel yang akan kami dapatkan, tetapi FFmpeg tidak menghasilkan sampel yang rusak - ini juga berarti bahwa itu tidak akan memisahkan sampel stereo juga.

Metode untuk memutar audio di SDL adalah sebagai berikut. Parameter suara dikonfigurasikan: frekuensi pengambilan sampel, jumlah saluran, dll. Dan juga mengatur fungsi panggilan balik dan data pengguna. Ketika kita mulai memutar suara, SDL akan terus memanggil fungsi panggilan balik ini dan memintanya untuk mengisi buffer audio dengan sejumlah byte. Setelah kami memasukkan informasi ini ke dalam struktur SDL_AudioSpec , kami memanggil SDL_OpenAudio (), yang akan membuka perangkat audio dan mengembalikan kami struktur AudioSpec lainnya . Ini adalah karakteristik yang benar-benar akan kita gunakan - tidak ada jaminan bahwa kita akan mendapatkan apa yang kita minta!

Pengaturan audio


Ingatlah itu untuk saat ini, karena kami belum memiliki informasi tentang stream audio! Mari kita kembali ke tempat dalam kode kita di mana kita menemukan aliran video dan mencari tahu aliran mana yang merupakan aliran audio:

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

Di sini kita bisa mendapatkan semua informasi yang kita inginkan dari AVCodecContext dari stream, sama seperti yang kita lakukan dengan streaming video:

AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;

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

Jika Anda ingat, dalam pelajaran sebelumnya, kami masih harus membuka codec audio itu sendiri. Itu mudah:

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

Dalam konteks codec berisi semua informasi yang diperlukan untuk mengonfigurasi audio kami:

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

Mari kita membahas setiap item:
  • freq (frekuensi): laju sampling, seperti yang dijelaskan sebelumnya.
  • format (): SDL , . «S» «S16SYS» «», 16 , 16 , «SYS» , , . , avcodec_decode_audio2 .
  • channels (): .
  • silence (): , . 0.
  • samples (): , , SDL , . - 512 8192; FFplay, , 1024.
  • callback (panggilan balik): di sini kita melewati fungsi panggilan balik yang sebenarnya. Kami akan berbicara lebih banyak tentang fungsi panggilan balik nanti.
  • userdata : SDL akan memberikan callback kita sebuah pointer nol ke semua data pengguna yang kita inginkan. Kami ingin memberi tahu dia tentang konteks codec kami; sedikit lebih rendah akan jelas mengapa.

Terakhir, buka audio dengan SDL_OpenAudio .

Antrian


Dan itu perlu! Sekarang kami siap untuk mengekstrak informasi audio dari aliran. Tetapi apa yang harus dilakukan dengan informasi ini? Kami akan terus menerima paket dari file film, tetapi pada saat yang sama, SDL akan memanggil fungsi panggilan balik! Solusinya adalah dengan membuat semacam struktur global di mana kita dapat memasukkan paket audio sehingga audio_callback kita memiliki sesuatu untuk menerima data audio! Jadi, inilah yang akan kita lakukan untuk membuat antrian paket. FFmpeg bahkan memiliki struktur untuk membantu dengan ini: AVPacketList , yang hanya daftar yang ditautkan untuk paket. Berikut adalah struktur antrian kami:

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

Pertama, kita harus mengindikasikan bahwa nb_packets berbeda dalam ukuran - ukurannya mengacu pada ukuran byte yang kita dapatkan dari ukuran paket-> . Perhatikan bahwa kita memiliki variabel mutex dan kondisi. Ini karena SDL melakukan proses audio sebagai aliran terpisah. Jika kami tidak memblokir antrian dengan benar, kami dapat merusak data kami. Mari kita lihat bagaimana antrian diimplementasikan. Setiap programmer yang menghargai diri sendiri harus tahu cara membuat antrian, tetapi kami juga akan menunjukkan cara melakukannya sehingga lebih mudah bagi Anda untuk mempelajari fungsi-fungsi SDL.

Pertama, kami membuat fungsi untuk menginisialisasi antrian:

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

Kemudian buat fungsi untuk menempatkan objek dalam antrian kami:

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 () memblokir mutex dalam antrian sehingga kami dapat menambahkan sesuatu, dan kemudian SDL_CondSignal () mengirimkan sinyal ke fungsi get kami (jika mengharapkannya) melalui variabel kondisional kami untuk memberi tahu bahwa ada data dan dapat dilanjutkan, untuk selanjutnya membuka kunci mutex.

Berikut adalah fungsi get yang sesuai . Perhatikan bagaimana SDL_CondWait () membuat blok fungsi (mis. Berhenti sampai kami mendapatkan data) jika kami memintanya untuk melakukan ini:

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

Seperti yang Anda lihat, kami membungkus fungsi dalam siklus abadi, jadi kami pasti akan mendapatkan beberapa data jika kami ingin memblokirnya. Kami menghindari perulangan selamanya menggunakan fungsi SDL_CondWait (). Intinya, semua yang dilakukan CondWait adalah menunggu sinyal dari SDL_CondSignal () (atau SDL_CondBroadcast ()) dan kemudian melanjutkan. Namun, sepertinya kami menangkapnya dalam sebuah mutex - jika kami memegang kunci, fungsi put kami tidak dapat mengantri apa pun! Namun, apa yang SDL_CondWait () juga lakukan untuk kita adalah membuka blokir mutex yang kita berikan, dan kemudian mencoba lagi untuk menguncinya segera setelah kami menerima sinyal.

Untuk setiap pemadam kebakaran


Anda juga melihat bahwa kami memiliki variabel berhenti global yang kami periksa untuk memastikan bahwa kami tidak menetapkan sinyal keluaran dalam program (SDL secara otomatis memproses sinyal TERM , dll.). Jika tidak, utas akan berlanjut selamanya, dan kita harus mematikan program dengan kill -9 :

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

Kami akan mengatur bendera keluar ke 1.

Kami memberi makan paket


Tetap hanya untuk mengkonfigurasi antrian kami:

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

  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

SDL_PauseAudio () akhirnya memulai unit audio. Ini mereproduksi keheningan jika tidak menerima data; tetapi ini tidak terjadi segera.

Jadi, kita memiliki antrian yang dikonfigurasi, sekarang kita siap untuk mengirim paket kepadanya. Kami beralih ke siklus pembacaan paket kami:

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

Harap dicatat bahwa kami tidak merilis paket setelah mengantri. Kami akan merilisnya nanti ketika kami mendekripsi.

Mengambil paket


Sekarang mari kita buat fungsi audio_callback untuk mengambil paket dari antrian. Callback akan terlihat seperti:

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

userdata , adalah pointer yang kami berikan SDL, stream adalah buffer di mana kami akan menulis data audio, dan len adalah ukuran buffer ini. Ini kodenya:

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

Sebenarnya, ini adalah loop sederhana yang mengekstraksi data dari fungsi lain yang kami tulis, audio_decode_frame (), menyimpan hasilnya dalam buffer perantara, mencoba menulis len bytes ke stream dan menerima lebih banyak data jika kami masih belum memiliki cukup atau menyimpannya untuk nanti, jika kita memiliki sesuatu yang tersisa. Ukuran audio_buf adalah 1,5 kali ukuran bingkai audio terbesar yang FFmpeg akan berikan kepada kami, yang memberi kami margin yang baik.

Dekripsi Audio Akhir


Mari kita lihat bagian dalam decoder audio_decode_frame :

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

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

  int len1, data_size = 0;

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

    if(quit) {
      return -1;
    }

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

Seluruh proses sebenarnya dimulai di dekat akhir fungsi, tempat kami memanggil packet_queue_get (). Kami mengambil paket dari antrian dan menyimpan informasi darinya. Kemudian, ketika kita memiliki paket untuk berfungsi, kita memanggil avcodec_decode_audio4 (), yang sangat mirip dengan fungsi saudaranya avcodec_decode_video (), kecuali bahwa dalam kasus ini paket dapat memiliki lebih dari satu bingkai. Karena itu, Anda mungkin perlu memanggilnya beberapa kali untuk mendapatkan semua data dari paket. Setelah menerima bingkai, kami cukup menyalinnya ke buffer audio kami, memastikan bahwa data_size lebih kecil dari buffer audio kami. Juga, ingat tentang casting audio_bufke tipe yang benar, karena SDL memberikan buffer int 8-bit, dan FFmpeg memberi kita data dalam buffer int 16-bit. Anda juga harus mempertimbangkan perbedaan antara len1 dan data_size . len1 adalah ukuran paket yang kami gunakan, dan data_size adalah jumlah data mentah yang dikembalikan.

Ketika kami memiliki beberapa data, kami segera kembali untuk mencari tahu apakah kami perlu mendapatkan lebih banyak data dari antrian atau kami selesai. Jika kita masih perlu memproses paket, maka patuhi itu. Jika Anda telah menyelesaikan paket, maka akhirnya lepaskan.

Dan itu semua! Kami memiliki audio yang ditransfer dari loop baca utama ke antrian, yang kemudian dibaca oleh fungsi audio_callback, yang mentransfer data ini ke SDL, dan SDL mentransfer ke kartu suara Anda. Silakan dan kompilasi:

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

Gip-gip-hore! Video masih dibawa pada kecepatan maksimum, tetapi suara sudah diputar sebagaimana mestinya. Mengapa demikian? Ya, karena informasi audio memiliki frekuensi pengambilan sampel - kami memompa informasi audio secepatnya, tetapi audio hanya diputar dalam aliran ini sesuai dengan frekuensi pengambilan sampelnya.

Kami hampir matang untuk sinkronisasi video dan audio, tetapi pertama-tama kita perlu melakukan reorganisasi kecil program. Metode antrian suara dan memainkannya menggunakan aliran terpisah bekerja sangat baik: itu membuat kode lebih mudah dikelola dan lebih modular. Sebelum kita mulai menyinkronkan video dengan audio, kita perlu menyederhanakan kodenya. Dalam seri berikutnya kami akan menghasilkan aliran kontrol!






Pelajaran 4: Banyak Utas


Daftar lengkap 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;

}

Gambaran


Terakhir kali, kami menambahkan dukungan audio menggunakan fitur audio SDL. SDL meluncurkan utas membuat panggilan balik untuk fungsi yang kami tentukan setiap kali diperlukan suara. Sekarang kita akan melakukan hal yang sama dengan tampilan video. Ini membuat kode lebih modular dan lebih mudah digunakan - terutama jika Anda ingin menambahkan sinkronisasi. Jadi dari mana kita mulai?

Perhatikan bahwa fungsi utama kami banyak menangani: ia melewati perulangan acara, membaca paket dan menerjemahkan video. Apa yang akan kita lakukan adalah membagi semuanya menjadi beberapa bagian: kita akan memiliki aliran yang bertanggung jawab atas penguraian paket; kemudian paket-paket ini ditambahkan ke antrian dan dibaca oleh aliran audio dan video yang sesuai. Kami telah menyetel streaming audio sesuai kebutuhan; dengan streaming video akan sedikit lebih sulit, karena kami harus memastikan bahwa video ditampilkan sendiri. Kami akan menambahkan kode tampilan aktual ke loop utama. Tetapi alih-alih menampilkan video setiap kali kami menjalankan loop, kami mengintegrasikan tampilan video ke dalam loop acara. Idenya adalah untuk memecahkan kode video, simpan bingkai yang diterima dalam antrian lain, lalu buat acara Anda sendiri ( FF_REFRESH_EVENT), yang kita tambahkan ke sistem acara, maka ketika loop acara kami melihat acara ini, itu akan menampilkan bingkai berikutnya dalam antrian. Berikut adalah ilustrasi ASCII yang nyaman tentang apa yang terjadi:


Alasan utama untuk memindahkan kontrol tampilan video melalui loop acara adalah bahwa dengan aliran SDL_Delay kita dapat mengontrol dengan tepat kapan bingkai video berikutnya muncul di layar. Ketika kami akhirnya menyinkronkan video dalam pelajaran berikutnya, cukup tambahkan kode yang akan menjadwalkan pembaruan video berikutnya sehingga gambar yang benar muncul di layar pada waktu yang tepat.

Sederhanakan kodenya


Mari kita hapus kodenya sedikit. Kami memiliki semua informasi ini tentang codec audio dan video, dan kami akan menambahkan antrian, buffer, dan Tuhan tahu apa lagi. Semua hal ini adalah untuk unit logis tertentu, yaitu - untuk film. Jadi, kami bermaksud membuat struktur besar yang berisi semua informasi ini yang disebut VideoState .

typedef struct VideoState {

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

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

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

Di sini kita melihat petunjuk tentang apa yang akan kita dapatkan pada akhirnya. Pertama, kita melihat informasi dasar - konteks format dan indeks aliran audio dan video, serta objek AVStream yang sesuai . Kemudian kita melihat bahwa beberapa buffer audio ini dipindahkan ke struktur ini. Mereka ( audio_buf , audio_buf_size , dll.) Dimaksudkan untuk informasi tentang audio yang masih ada (atau hilang). Kami menambahkan antrian lain untuk video dan buffer (yang akan digunakan sebagai antrian; untuk ini kami tidak memerlukan antrian yang berlebihan) untuk frame yang diterjemahkan (disimpan sebagai overlay). VideoPicture Struktur- ini adalah ciptaan kita sendiri (kita akan melihat apa yang akan ada di dalamnya ketika kita sampai di sana). Anda juga dapat melihat bahwa kami telah mengalokasikan pointer untuk dua aliran tambahan yang akan kami buat, serta bendera keluar dan nama file film.

Jadi, sekarang kita kembali ke fungsi utama untuk melihat bagaimana ini mengubah program kita. Mari siapkan struktur VideoState kami :

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

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

av_mallocz () adalah fungsi yang baik yang akan mengalokasikan memori untuk kita dan nol itu.

Kemudian kami menginisialisasi kunci kami untuk buffer tampilan ( pictq ), karena sejak loop acara memanggil fungsi tampilan kami - ingat, fungsi tampilan akan mengambil frame yang telah didekodekan dari pictq . Pada saat yang sama, decoder video kami akan memasukkan informasi ke dalamnya - kami tidak tahu siapa yang lebih dulu ke sana. Saya harap Anda mengerti bahwa ini adalah kondisi balapan klasik. Karena itu, kami mendistribusikannya sekarang sebelum memulai topik apa pun. Mari juga menyalin nama film kami ke VideoState :

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

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

av_strlcpy adalah fungsi dari FFmpeg yang melakukan beberapa pemeriksaan perbatasan tambahan selain strncpy .

Utas pertama kami


Mari jalankan utas kami dan lakukan sesuatu yang nyata:

schedule_refresh(is, 40);

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

schedule_refresh adalah fungsi yang akan kita definisikan nanti. Apa yang dia lakukan adalah memberi tahu sistem untuk menghasilkan FF_REFRESH_EVENT setelah jumlah milidetik yang ditentukan. Ini, pada gilirannya, akan memanggil fungsi pembaruan video ketika kita melihatnya dalam antrian acara. Tapi sekarang mari kita lihat SDL_CreateThread ().

SDL_CreateThread () tidak hanya itu - ia memunculkan thread baru yang memiliki akses penuh ke semua memori dari proses asli, dan memulai utas yang dieksekusi oleh fungsi yang kami berikan. Fungsi ini juga akan mengirimkan data yang ditentukan pengguna. Dalam hal ini, kami memanggil decode_thread () dan melampirkan struktur VideoState kami. Tidak ada yang baru di paruh pertama fungsi; itu hanya melakukan pekerjaan membuka file dan menemukan indeks stream audio dan video. Satu-satunya hal yang kami lakukan secara berbeda adalah menjaga konteks format dalam struktur besar kami. Setelah kami menemukan indeks aliran kami, kami memanggil fungsi lain yang kami tentukan, stream_component_open (). Ini adalah cara yang cukup alami untuk memisahkan, dan karena kami melakukan banyak hal serupa untuk mengatur video dan audio codec, kami menggunakan kembali beberapa kode, menjadikannya fungsi.

Fungsi Stream_component_open() Adalah tempat di mana kami menemukan decoder codec kami, mengonfigurasi parameter suara, menyimpan informasi penting dalam struktur besar kami dan meluncurkan stream audio dan video kami. Di sini kami juga menyisipkan parameter lain, seperti penggunaan codec secara paksa alih-alih deteksi otomatisnya, dll. Seperti ini:

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

Ini hampir sama dengan kode yang kita miliki sebelumnya, kecuali sekarang kode ini digeneralisasi untuk audio dan video. Perhatikan bahwa alih-alih aCodecCtx, kami telah mengonfigurasi struktur besar kami sebagai data pengguna untuk panggilan balik audio kami. Kami juga menyimpan streaming sendiri sebagai audio_st dan video_st . Kami juga menambahkan antrean video kami dan mengaturnya seperti antrian audio kami. Intinya adalah untuk menjalankan streaming video dan audio. Bit-bit ini melakukan ini:

    SDL_PauseAudio(0);
    break;

/* ...... */

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

Ingat SDL_PauseAudio () dari pelajaran terakhir. SDL_CreateThread () digunakan dengan cara yang sama. Kembali ke fungsi video_thread () kami.

Sebelum itu, mari kita kembali ke bagian kedua dari fungsi decode_thread () kami. Pada dasarnya, ini hanya untuk loop yang membaca paket dan meletakkannya di antrian yang tepat:

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

Tidak ada yang benar-benar baru di sini, kecuali bahwa kami sekarang memiliki ukuran maksimum untuk antrian audio dan video kami, dan kami telah menambahkan pemeriksaan kesalahan baca. Konteks format memiliki struktur ByteIOContext di dalam yang disebut pb . ByteIOContext adalah struktur yang pada dasarnya menyimpan semua informasi tentang file tingkat rendah.

Setelah loop for kami, kami memiliki semua kode untuk menunggu sisa program untuk menyelesaikan atau menginformasikannya. Kode ini bersifat instruktif karena menunjukkan bagaimana kita mendorong acara - sesuatu yang nanti kita perlukan untuk menampilkan video:

  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;

Kami mendapatkan nilai untuk acara khusus menggunakan konstanta SDL SDL_USEREVENT . Acara pengguna pertama harus disetel ke SDL_USEREVENT , SDL_USEREVENT + 1 berikutnya , dll. FF_QUIT_EVENT didefinisikan dalam program kami sebagai SDL_USEREVENT + 1 . Kami juga dapat meneruskan data pengguna jika perlu, dan di sini kami meneruskan pointer kami ke struktur besar. Akhirnya, kami memanggil SDL_PushEvent (). Di sakelar perulangan acara kami, kami hanya meletakkan ini di bagian SDL_QUIT_EVENTyang kita miliki sebelumnya. Kami akan melihat siklus acara kami secara lebih rinci; untuk saat ini, pastikan saja ketika kita menekan FF_QUIT_EVENT kita menangkapnya nanti dan mengganti flag keluar.

Terima bingkai: video_thread


Setelah menyiapkan codec, Anda dapat memulai streaming video. Aliran ini membaca paket dari antrian video, menerjemahkan video ke dalam bingkai, dan kemudian memanggil fungsi queue_picture untuk menempatkan bingkai yang diproses dalam antrian gambar:

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

Sebagian besar fungsi ini harus dipahami sekarang. Kami baru saja menyalin fungsi avcodec_decode_video2 di sini , cukup mengganti beberapa argumen; misalnya, kami memiliki AVStream yang tersimpan di struktur besar kami, jadi kami mendapatkan codec kami dari sana. Kami hanya terus menerima paket dari antrean video kami sampai seseorang memberitahu kami untuk keluar atau kami menemukan kesalahan.

Bingkai antrian


Mari kita lihat fungsi yang menyimpan pFrame yang di- decode dalam antrian gambar kita. Karena antrian gambar kita adalah overlay SDL (mungkin untuk memungkinkan fungsi tampilan video untuk melakukan komputasi sesedikit mungkin), kita perlu mengkonversi frame kita ke dalamnya. Data yang kami simpan di antrian gambar adalah struktur yang kami buat:

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

Struktur besar kami berisi buffer file-file ini, tempat kami dapat menyimpannya. Namun, kami perlu mendistribusikan SDL_Overlay sendiri (perhatikan flag yang diberikan, yang menunjukkan apakah kami melakukannya atau tidak).

Untuk menggunakan antrian ini, kami memiliki dua petunjuk - indeks tulis dan indeks baca. Kami juga melacak berapa banyak gambar aktual di buffer. Untuk menulis ke antrean, pertama-tama kita tunggu hingga buffer kita dihapus, sehingga kita punya tempat untuk menyimpan VideoPicture kita . Lalu kami memeriksa untuk melihat apakah kami menetapkan overlay dalam indeks rekaman kami? Jika tidak, Anda perlu mengalokasikan memori. Kami juga harus merealokasi buffer jika ukuran jendela telah berubah!

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

Mari kita lihat fungsi alokasi_picture ():

void alloc_picture(void *userdata) {

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

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

Anda harus mengenali fungsi SDL_CreateYUVOverlay , yang telah kami pindahkan dari loop utama kami ke bagian ini. Kode ini seharusnya sudah cukup jelas sekarang. Namun, sekarang kami memiliki kunci mutex, karena dua utas tidak dapat secara bersamaan menulis informasi ke layar! Ini tidak akan mengizinkan fungsi alokasi_picture kami mengganggu fungsi lain yang akan menampilkan gambar. (Kami membuat kunci ini sebagai variabel global dan menginisialisasi di main (); lihat kode.) Ingatlah bahwa kami menjaga lebar dan tinggi dalam struktur VideoPicture , karena kami perlu memastikan bahwa ukuran video kami tidak berubah karena alasan tertentu.
Oke, kami sudah menyelesaikannya, dan kami memiliki YUV overlay kami, berdedikasi dan siap menerima gambar. Mari kita kembali ke queue_picture dan melihat kode untuk menyalin bingkai ke overlay. Bagian ini harus familier bagi Anda:

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

Di sini, sebagian besar hanya kode yang kami gunakan sebelumnya untuk mengisi overlay YUV dengan bingkai kami. Bit terakhir hanya "menambahkan" nilai kami ke antrian. Antrian berfungsi, nilai ditambahkan ke dalamnya sampai penuh, dan pembacaan dari itu terjadi saat setidaknya ada sesuatu di dalamnya. Karena itu, semuanya tergantung pada nilai is -> pictq_size , yang mengharuskan kita untuk memblokirnya. Jadi, apa yang kita lakukan di sini: tingkatkan penunjuk catatan (dan jika perlu, mulai lagi dari awal), lalu blokir antrean dan tambah ukurannya. Sekarang pembaca kita akan tahu bahwa ada lebih banyak informasi tentang antrian, dan jika ini membuat antrian kita penuh, dan perekam kita akan mengetahuinya.

Tampilan video


Itu semua untuk utas video kami! Sekarang kita telah menyelesaikan semua utas gratis, kecuali satu - ingat bagaimana kita memanggil fungsi schedule_refresh () dulu ? Lihatlah apa yang sebenarnya terjadi:

/* 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 () adalah fungsi SDL yang hanya melakukan panggilan balik ke fungsi yang ditentukan pengguna setelah beberapa milidetik (dan, jika perlu, mentransfer beberapa data yang ditentukan pengguna). Kami akan menggunakan fungsi ini untuk menjadwalkan pembaruan video - setiap kali kami menyebutnya, ia menetapkan timer yang akan memicu suatu peristiwa, yang, pada gilirannya, akan menyebabkan fungsi utama kami () memanggil fungsi yang mengekstrak bingkai dari gambar antrian kami dan menampilkan nya! Fiuh! Tiga "yang / mana / yang" dalam satu kalimat! Jadi, mari kita lakukan hal pertama yang harus dilakukan - jalankan acara ini. Ini mengirim kami ke:

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

Acara ini diluncurkan oleh teman lama kami. FF_REFRESH_EVENT didefinisikan di sini sebagai SDL_USEREVENT + 1 . Perlu dicatat bahwa ketika kita mengembalikan 0, SDL menghentikan timer, sehingga panggilan balik tidak dieksekusi lagi.

Sekarang kami telah memanggil FF_REFRESH_EVENT lagi , kami perlu memprosesnya dalam loop acara kami:

for(;;) {

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

apa yang mengirim kami ke sini ke fungsi ini, yang sebenarnya mengekstrak data dari antrian gambar kami:

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

Saat ini, fungsi ini cukup sederhana: ia memproses antrian sementara kami memiliki sesuatu, mengatur timer untuk menampilkan bingkai video berikutnya, memanggil video_display untuk benar-benar menampilkan video di layar, kemudian meningkatkan penghitung pada antrian, sambil mengurangi ukurannya. Anda mungkin memperhatikan bahwa kami tidak benar-benar melakukan apa pun dengan vp dalam fungsi ini, dan inilah alasannya: ini ada di depan. Tetapi sedikit kemudian. Kami akan menggunakannya untuk mengakses informasi waktu ketika kami mulai menyinkronkan video dengan audio. Di sini, lihat tempat di kode di mana komentar "Kode waktu di sini" ditulis. Di bagian ini kita akan mengetahui seberapa cepat kita harus menunjukkan bingkai video berikutnya, dan kemudian masukkan nilai ini dalam fungsi schedule_refresh(). Saat ini, kami hanya memasukkan nilai fiktif 80. Secara teknis, Anda dapat menebak dan memeriksa nilai ini dan mengkompilasi ulang untuk setiap film, tetapi: 1) itu akan mulai melambat setelah beberapa saat dan 2) itu cukup bodoh. Meskipun, di masa depan kami akan kembali ke titik ini.

Kami hampir selesai. Hanya ada satu hal yang harus dilakukan: tampilkan videonya! Inilah fungsi 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);
  }
}

Karena layar dapat memiliki ukuran berapa pun (kami memasang 640x480, dan ada beberapa cara untuk menyesuaikannya sehingga pengguna dapat mengubah ukuran), Anda perlu secara dinamis menentukan seberapa besar area persegi panjang seharusnya untuk film kami. Jadi, pertama-tama Anda perlu mengetahui aspek rasio film kami, hanya lebarnya dibagi dengan tinggi. Beberapa codec akan memiliki rasio aspek yang aneh dari sampel, yang hanya lebar / tinggi satu piksel atau sampel. Karena nilai tinggi dan lebar dalam konteks codec kami diukur dalam piksel, rasio aspek aktual sama dengan rasio aspek dikalikan dengan rasio aspek untuk sampel. Beberapa codec akan menampilkan rasio aspek 0, yang berarti bahwa setiap piksel hanya memiliki ukuran 1x1. Lalu kami membuat skala film sedemikian rupasehingga pas di layar sebanyak mungkin. Pembalikan bit& -3 hanya membulatkan nilainya ke kelipatan empat terdekat. Kemudian pusatkan film dan panggil SDL_DisplayYUVOverlay () untuk memastikan bahwa mutex layar digunakan untuk mengaksesnya.

Dan itu semua? Sudahkah kita selesai? Anda masih perlu menulis ulang kode audio untuk menggunakan VideoStruct baru, tetapi ini adalah perubahan sepele yang dapat dilihat pada kode sampel. Hal terakhir yang perlu kita lakukan adalah mengubah panggilan balik kita untuk fungsi panggilan balik keluar internal di FFmpeg:

VideoState *global_video_state;

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

Set global_video_state ke struktur besar di main ().

Jadi begitu! Kami mengkompilasi:

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

dan nikmati filmnya tanpa sinkronisasi! Pada langkah selanjutnya, kita akhirnya akan membuat pemutar video yang benar - benar berfungsi !






Pelajaran 5: Sinkronisasi Video


Daftar lengkap 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;

}

PERINGATAN


Ketika saya baru saja menulis panduan ini, semua kode sinkronisasi saya diambil dari versi ffplay.c . Hari ini adalah program yang sama sekali berbeda, dan pembaruan di perpustakaan FFmpeg (dan di ffplay.c sendiri) telah menyebabkan perubahan mendasar. Meskipun kode ini masih berfungsi, sudah usang, dan ada banyak perbaikan lain yang bisa digunakan dalam panduan ini.

Bagaimana video disinkronkan


Sampai sekarang, kami memiliki pemutar film yang hampir tidak berguna. Ya, itu memutar video, dan ya, itu memutar audio, tapi itu tidak cukup seperti yang kita sebut film. Jadi apa yang kita lakukan?

PTS dan DTS


Untungnya, streaming audio dan video berisi informasi tentang seberapa cepat dan pada titik kapan mereka harus diputar. Streaming audio memiliki laju pengambilan sampel, dan streaming video memiliki bingkai per detik. Namun, jika kami hanya menyinkronkan video dengan menghitung jumlah frame dan mengalikannya dengan frame rate, ada kemungkinan besar video itu tidak disinkronkan dengan suara. Karena itu, kita akan pergi ke arah lain. Paket dari sungai dapat memiliki apa yang disebut decoding waktu cap ( DTS - dari d ecoding t ime s memadatkan ) dan waktu presentasi cap ( PTS - dari p resentation t ime stamp ). Untuk memahami kedua arti ini, Anda perlu tahu bagaimana film disimpan. Beberapa format, seperti MPEG, menggunakan apa yang mereka sebut B-frame ( Bed dan dua arah, Inggris. Bidirectional ). Dua jenis lain dari frame disebut I-frame dan P-frame ( I adalah internal yang , saya nner , dan P berarti diprediksi , p redicted ). I-frame berisi gambar penuh. Bingkai P.tergantung pada I-dan P-frame sebelumnya dan berbeda dari frame sebelumnya, atau Anda juga dapat memberi nama - delta. B-frame mirip dengan P-frame, tetapi tergantung pada informasi yang terkandung dalam frame sebelumnya dan berikutnya! Fakta bahwa suatu bingkai mungkin tidak mengandung gambar itu sendiri, tetapi perbedaan dengan bingkai lainnya - menjelaskan mengapa kita mungkin tidak memiliki bingkai yang selesai setelah memanggil avcodec_decode_video2 .

Katakanlah kita memiliki film di mana 4 frame dalam urutan ini: IBBP . Kemudian kita perlu mencari tahu informasi dari frame-P terakhir sebelum kita dapat menampilkan salah satu dari dua frame-B sebelumnya. Karena itu, frame dapat disimpan dalam urutan yang tidak sesuai dengan urutan tampilan aktual: IPBB. Itulah tujuan cap waktu pengodean dan waktu presentasi untuk setiap frame. Cap waktu pengodean memberi tahu kami kapan kami harus memecahkan kode sesuatu, dan cap waktu presentasi memberitahu kami kapan kami perlu menampilkan sesuatu. Jadi, dalam hal ini, streaming kami mungkin terlihat seperti ini:

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

Sebagai aturan, PTS dan DTS hanya berbeda ketika streaming yang dimainkan mengandung B-frame.

Ketika kami menerima paket dari av_read_frame (), itu berisi nilai PTS dan DTS untuk informasi yang ada di dalam paket. Tetapi yang benar-benar kita butuhkan adalah PTS dari kerangka mentah yang baru diterjemahkan, dalam hal ini kita tahu kapan itu perlu ditampilkan.

Untungnya, FFmpeg memberi kita "cap waktu terbaik" yang bisa kita peroleh dengan menggunakan fungsi av_frame_get_best_effort_timestamp ().

Sinkronisasi


Agar frame ditampilkan pada gilirannya, alangkah baiknya jika tahu kapan harus menampilkan frame video tertentu. Tetapi bagaimana tepatnya kita melakukannya? Idenya adalah ini: setelah kami menunjukkan bingkai, kami mencari tahu kapan bingkai berikutnya harus ditampilkan. Kemudian cukup jeda, setelah itu kami memperbarui video setelah periode waktu ini. Seperti yang diharapkan, kami memeriksa nilai PTS dari frame berikutnya pada jam sistem untuk melihat berapa lama waktu tunggu kami. Pendekatan ini berhasil, tetapi ada dua masalah yang perlu diatasi.

Pertama, pertanyaannya adalah, kapan PTS berikutnya? Anda akan mengatakan bahwa Anda cukup menambahkan frekuensi video ke PTS saat ini - dan pada prinsipnya Anda akan benar. Namun, beberapa jenis video akan membutuhkan bingkai berulang. Ini berarti Anda harus mengulang bingkai saat ini beberapa kali. Ini dapat menyebabkan program untuk menampilkan frame berikutnya terlalu cepat. Ini harus diperhitungkan.

Masalah kedua adalah bahwa, dalam program yang kami tulis saat ini, video dan audio dengan senang hati bergerak maju sampai mereka bersusah payah menyinkronkan sama sekali. Kita tidak perlu khawatir tentang itu jika semuanya dengan sendirinya bekerja dengan sempurna. Tetapi komputer Anda tidak sempurna, seperti banyak file video. Jadi, kami memiliki tiga opsi: menyinkronkan audio dengan video, menyinkronkan video dengan audio atau menyinkronkan audio dan video dengan jam eksternal (misalnya, dengan komputer Anda). Sekarang kita akan menyinkronkan video dengan audio.

Pengodean: menerima bingkai PTS


Sekarang mari kita menulis sesuatu secara langsung. Kita perlu menambahkan beberapa bagian lagi ke struktur besar kita, dan kita akan melakukannya dengan cara yang kita butuhkan. Pertama, mari kita lihat utas video kami. Ingat bahwa di sini kami mengumpulkan paket-paket yang telah diantri oleh aliran decoding kami? Di bagian kode ini, kita perlu mendapatkan PTS untuk kerangka yang diberikan avcodec_decode_video2 kepada kita . Cara pertama yang kita bicarakan adalah mendapatkan DTS dari paket yang diproses terakhir, yang cukup sederhana:

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

Kami menetapkan PTS ke nol jika kami tidak dapat menentukan nilainya.

Yah, itu mudah. Catatan teknis: seperti yang Anda lihat, kami menggunakan int64 untuk PTS. Ini karena PTS disimpan sebagai integer. Nilai ini adalah cap waktu yang sesuai dengan dimensi waktu di timebb . Misalnya, jika streaming memiliki 24 frame per detik, PTS dari 42 akan menunjukkan bahwa frame harus digunakan di mana frame 42 seharusnya, asalkan kita memiliki frame diganti setiap 1/24 detik (tentu saja, ini tidak akan selalu begitu faktanya).

Kami dapat mengonversi nilai ini ke detik dengan membaginya dengan frame rate. Nilai time_basestreaming akan sama dengan 1 dibagi dengan frame rate (untuk konten dengan frame rate tetap), oleh karena itu, untuk mendapatkan PTS dalam hitungan detik, kami mengalikannya dengan time_base .

Kode selanjutnya: sinkronisasi dan penggunaan PTS


Jadi sekarang kita memiliki semua PTS yang sudah jadi. Sekarang kita akan menangani dua masalah sinkronisasi yang dibahas sedikit lebih tinggi. Kami akan mendefinisikan fungsi syncize_video yang akan memperbarui PTS untuk disinkronkan dengan semuanya. Fungsi ini, akhirnya, juga akan menangani kasus-kasus di mana kita tidak mendapatkan nilai PTS untuk bingkai kita. Pada saat yang sama, kita perlu melacak kapan frame berikutnya diharapkan sehingga kita dapat mengatur kecepatan refresh dengan benar. Kita dapat melakukan ini menggunakan nilai jam video internal , yang melacak berapa banyak waktu yang telah berlalu untuk video. Kami menambahkan nilai ini ke struktur besar kami:

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

Berikut adalah fungsi syncize_video , yang cukup jelas:

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

Seperti yang Anda lihat, kami memperhitungkan frame berulang dalam fungsi ini.

Sekarang, mari kita dapatkan PTS kita yang benar dan antri bingkai menggunakan queue_picture dengan menambahkan argumen Poin baru :

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

Satu-satunya hal yang berubah dalam queue_picture adalah bahwa kami menyimpan nilai Poin ini dalam struktur VideoPicture yang kami antri. Jadi, kita harus menambahkan variabel pts ke struktur dan menambahkan baris kode ini:

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

Jadi sekarang kita memiliki gambar yang di-antri dengan nilai PTS yang benar, jadi mari kita lihat fitur pembaruan video kami. Anda dapat mengingat dari pelajaran terakhir bahwa kami hanya memalsukannya dan menginstal pembaruan 80 ms. Nah, sekarang kita akan mencari tahu apa yang seharusnya ada di sana.

Strategi kami adalah untuk memprediksi waktu PTS berikutnya dengan hanya mengukur waktu antara Poin saat ini dan yang sebelumnya. Pada saat yang sama, kita perlu menyinkronkan video dengan audio. Kami akan membuat jam audio.: Nilai internal yang melacak posisi audio yang kami putar. Ini seperti pembacaan digital pada pemutar mp3 mana pun. Karena kami menyinkronkan video dengan suara, aliran video menggunakan nilai ini untuk mencari tahu apakah itu terlalu jauh di depan atau terlalu jauh di belakang.

Kami akan kembali ke implementasi nanti; Sekarang mari kita asumsikan bahwa kita memiliki fungsi get_audio_clockyang akan memberi kita waktu pada jam audio. Segera setelah kami mendapatkan nilai ini, apa yang perlu dilakukan jika video dan audio tidak disinkronkan? Akan bodoh jika mencoba melompat ke paket yang tepat melalui pencarian atau sesuatu yang lain. Sebagai gantinya, kami hanya menyesuaikan nilai yang kami hitung untuk pembaruan berikutnya: jika PTS terlalu jauh di belakang waktu audio, kami menggandakan perkiraan keterlambatan kami. Jika PTS terlalu jauh dari waktu bermain, kami hanya memperbarui secepat mungkin. Sekarang kita memiliki pembaruan atau waktu tunda yang terkonfigurasi, kita akan membandingkannya dengan jam komputer kita, membiarkan frame_timer berjalan . Pengatur waktu bingkai ini merangkum semua perkiraan keterlambatan kami selama pemutaran film. Dengan kata lain, frame_timer ini- Ini adalah waktu yang menunjukkan kapan akan menampilkan bingkai berikutnya. Kami cukup menambahkan penundaan baru ke pengatur waktu bingkai, membandingkannya dengan waktu pada jam komputer kami, dan menggunakan nilai ini untuk merencanakan pembaruan berikutnya. Ini bisa sedikit membingungkan, jadi baca kodenya dengan cermat:

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

Kami melakukan beberapa pemeriksaan: pertama, kami memastikan bahwa penundaan antara PTS saat ini dan PTS sebelumnya masuk akal. Jika tidak perlu menunda, maka audio dan video hanya bertepatan pada titik ini dan cukup gunakan penundaan terakhir. Kemudian kami memastikan bahwa ambang sinkronisasi dipenuhi, karena sinkronisasi sempurna tidak pernah terjadi. FFplay menggunakan nilai 0,01 untuk ambang. Kami juga memastikan bahwa ambang sinkronisasi tidak pernah kurang dari interval antara nilai PTS. Akhirnya, tetapkan nilai pembaruan minimum menjadi 10 milidetik (memang, sepertinya mereka harus melewatkan frame di sini, tapi jangan khawatir tentang itu).

Kami menambahkan banyak variabel ke struktur besar, jadi jangan lupa untuk memeriksa kode. Juga, jangan lupa untuk menginisialisasi timer bingkai dan penundaan awal dari frame sebelumnya di stream_component_open :

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

Sinkronkan: jam audio


Waktunya telah tiba untuk mewujudkan jam audio. Kami dapat memperbarui waktu dalam fungsi audio_decode_frame kami , tempat kami mendekode audio. Sekarang ingat bahwa kami tidak selalu memproses paket baru setiap kali kami memanggil fungsi ini, jadi ada dua area di mana Anda perlu memperbarui jam. Tempat pertama adalah tempat kami mendapatkan paket baru: cukup instal jam suara pada paket PTS. Kemudian, jika paket memiliki beberapa bingkai, kami menghemat waktu pemutaran audio dengan menghitung jumlah sampel dan mengalikannya dengan frekuensi sampling yang diberikan per detik. Jadi, ketika kita memiliki paket:

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

Dan segera setelah kami memproses paket:

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

Beberapa nuansa minor: templat fungsi telah diubah dan sekarang menyertakan pts_ptr , jadi pastikan untuk mengubahnya. pts_ptr adalah pointer yang kami gunakan untuk memberi tahu audio_callback Poin paket audio . Ini akan digunakan lain kali untuk menyinkronkan audio dengan video.

Sekarang kita akhirnya dapat mengimplementasikan fungsi get_audio_clock kami . Hal ini tidak sesederhana mendapatkan nilai yang -> audio_clock , jika Anda berpikir tentang hal itu. Harap perhatikan bahwa kami mengatur audio PTS setiap kali kami memprosesnya, tetapi jika Anda melihat fungsi audio_callback, akan butuh waktu untuk memindahkan semua data dari paket audio kami ke buffer output kami. Ini berarti bahwa nilai dalam jam audio kami mungkin terlalu jauh ke depan. Karena itu, kita perlu memeriksa seberapa banyak kita harus menulis. Ini kode lengkapnya:

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

Anda sekarang harus mengerti mengapa fungsi ini bekerja;)

Jadi, itu dia! Kami mengkompilasi:

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

Itu terjadi! Anda dapat menonton film di pemutar buatan sendiri. Dalam pelajaran berikutnya, kita akan melihat sinkronisasi audio, dan kemudian belajar cara mencari.

Panduan FFmpeg dan SDL atau Cara Menulis Video Player dalam Kurang dari 1000 Baris - Bagian 2



Terjemahan di Blog Edison:


All Articles