[الجزء 1/2] دليل FFmpeg و SDL أو كيفية كتابة مشغل فيديو في أقل من 1000 سطر


على الرغم من أن هذه المعلومات قديمة بالفعل ، إلا أن المادة الأصلية لا تزال مصدر إلهام شائع لمحتوى مفيد مختلف حول موضوع FFmpeg. ومع ذلك ، لا توجد حتى الآن ترجمة كاملة للأصل إلى الروسية. نحن نصحح الإغفال المزعج ، لأنه متأخر أفضل من عدمه.

وعلى الرغم من محاولتنا ، إلا أن صعوبات الترجمة أمر لا مفر منه في مثل هذا النص الضخم . الإبلاغ عن الأخطاء (يُفضل أن يكون ذلك في الرسائل الخاصة) - سنعمل معًا بشكل أفضل.

جدول المحتويات

إديسون برمجيات - تطوير الويب
EDISON.

, C C++.

! ;-)


UPD: تم تحديث هذا الدليل اعتبارًا من فبراير 2015.

FFmpeg هي مكتبة رائعة لإنشاء تطبيقات الفيديو بالإضافة إلى الأدوات المساعدة للأغراض العامة. يتولى FFmpeg روتين معالجة الفيديو بالكامل ، حيث يقوم بجميع عمليات فك التشفير والتشفير والمضاعفة وإزالة تعدد الإرسال. مما يبسط إلى حد كبير إنشاء تطبيقات الوسائط. كل شيء بسيط وسريع ، مكتوب بلغة C ، يمكنك فك تشفير أي برنامج ترميز متوفر اليوم تقريبًا ، بالإضافة إلى ترميز بعض التنسيقات الأخرى.

الصيد الوحيد هو أن الوثائق مفقودة في الغالب. هناك برنامج تعليمي واحد ( في الأصل ، هنا رابط لصفحة ويب غير موجودة بالفعل - مترجم الملاحظات) ، والتي تغطي أساسيات FFmpeg والتوليد التلقائي لأحواض doxygen. ولا أكثر. لذلك ، قررت بشكل مستقل معرفة كيفية استخدام FFmpeg لإنشاء تطبيقات فيديو وصوت رقمية عاملة ، وفي نفس الوقت توثيق العملية وتقديمها في شكل كتاب مدرسي.

هناك برنامج FFplay الذي يأتي مع FFmpeg. إنه بسيط ، مكتوب في C ، ينفذ مشغل فيديو كامل باستخدام FFmpeg. درسي الأول هو نسخة محدثة من الدرس الأصلي لمارتن بوهيم ( في الأصل ، رابط إلى صفحة ويب معطلة بالفعل - ملاحظة مترجم ) - قمت بسحب بعض القطع من هناك. وأيضًا في سلسلة من دروسي سأعرض عملية إنشاء مشغل فيديو يعمل على أساس ffplay.cفابريس بيلارد. سيقدم كل درس فكرة جديدة (أو حتى فكرتين) مع شرح لتطبيقها. يأتي كل فصل مع قائمة C ، والتي يمكنك تجميعها وتشغيلها بنفسك. ستعرض ملفات المصدر كيفية عمل هذا البرنامج ، وكيفية عمل أجزائه الفردية ، وستوضح أيضًا التفاصيل الفنية الثانوية التي لم يتم تناولها في هذا الدليل. عندما ننتهي ، سيكون لدينا مشغل فيديو يعمل مكتوبًا في أقل من 1000 سطر من التعليمات البرمجية!

عند إنشاء المشغل ، سنستخدم SDL لإخراج ملف وسائط الصوت والفيديو. SDL هي مكتبة وسائط متعددة ممتازة عبر الأنظمة الأساسية تُستخدم في برامج تشغيل MPEG وبرامج المحاكاة والعديد من ألعاب الفيديو. ستحتاج إلى تنزيل مكتبات SDL وتثبيتها على نظامك لتجميع البرامج من هذا الدليل.

هذا البرنامج التعليمي للأشخاص الذين لديهم خبرة برمجة جيدة. على الأقل ، تحتاج إلى معرفة C ، وأن يكون لديك أيضًا فهم لمفاهيم مثل قوائم الانتظار ، وكائنات المزامنة ، وما إلى ذلك. يجب أن يكون هناك بعض الفهم للوسائط المتعددة ؛ على سبيل المثال ، أشياء مثل الطول الموجي وما شابه. ومع ذلك ، ليس من الضروري أن تكون معلمًا في هذه الأمور ، حيث سيتم شرح العديد من المفاهيم في سياق الدروس.

لا تتردد في أن ترسل لي رسائل خطأ ، أسئلة ، تعليقات ، أفكار ، ميزات ، أيا كان ، في Dranger Doggy Gmail dot Com.







اقرأ أيضا على مدونة
شركة إديسون:


FFmpeg دليل ليباف






الدرس 1: إنشاء Screencaps


القائمة الكاملة: 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;
}

نظرة عامة


تحتوي ملفات الأفلام على العديد من المكونات الرئيسية. أولاً ، يسمى الملف نفسه حاوية ، ويحدد نوع الحاوية كيفية تمثيل البيانات في الملف. أمثلة الحاويات هي AVI و Quicktime . علاوة على ذلك ، هناك العديد من سلاسل المحادثات في الملف ؛ على وجه الخصوص، عادة ما يكون هناك دفق الصوت و دفق الفيديو . ("دفق" هي كلمة مضحكة لـ "سلسلة من عناصر البيانات المتاحة وفقًا للمخطط الزمني.") تسمى عناصر البيانات في دفق الإطارات . يتم ترميز كل دفق بواسطة نوع واحد أو آخر من برنامج الترميز . يحدد برنامج الترميز كيفية البيانات الفعلية إلى diruyutsya وديسمبرمراجعة - ومن هنا جاء اسم برنامج الترميز. أمثلة برامج الترميز هي DivX و MP3. ثم تتم قراءة الحزم من الدفق. الحزم عبارة عن أجزاء من البيانات التي يمكن أن تحتوي على أجزاء من البيانات التي تم فك تشفيرها في إطارات خام ، والتي يمكننا معالجتها أخيرًا في تطبيقنا. لأغراضنا ، تحتوي كل حزمة على إطارات كاملة (أو عدة إطارات إذا كانت صوتية).

العمل مع تدفقات الفيديو والصوت بسيط للغاية حتى على المستوى الأساسي:

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

يعد العمل مع الوسائط المتعددة باستخدام FFmpeg أمرًا بسيطًا للغاية كما هو الحال في هذا البرنامج ، على الرغم من أن خطوة "MAKE [...]" في بعض البرامج يمكن أن تكون صعبة للغاية. في هذا البرنامج التعليمي ، سنفتح الملف ونحسب دفق الفيديو بداخله ، وسيقوم "MAKE [...]" بكتابة الإطار إلى ملف PPM.

افتح الملف


أولاً وقبل كل شيء ، دعنا نرى ما يحدث أولاً عند فتح الملف. باستخدام FFmpeg ، نقوم أولاً بتهيئة المكتبة المطلوبة:

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

يسجل هذا جميع تنسيقات الملفات وبرامج الترميز المتاحة في المكتبة ، لذلك سيتم استخدامها تلقائيًا عند فتح ملف بالتنسيق / برنامج الترميز المناسب. لاحظ أنك تحتاج إلى الاتصال بـ av_register_all () مرة واحدة فقط ، لذلك نقوم بذلك هنا بشكل رئيسي (). إذا كنت ترغب في ذلك ، يمكنك تسجيل تنسيقات الملفات وبرامج الترميز الانتقائية فقط ، ولكن عادةً لا يوجد سبب محدد للقيام بذلك.

افتح الملف الآن:

AVFormatContext *pFormatCtx = NULL;

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

احصل على اسم الملف من الوسيطة الأولى. تقوم هذه الوظيفة بقراءة رأس الملف وتخزين معلومات تنسيق الملف في بنية AVFormatContext التي مررنا بها. يتم استخدام الوسيطات الثلاثة الأخيرة لتحديد تنسيق الملف وحجم المخزن المؤقت ومعلمات التنسيق. بضبطها على NULL أو 0 ، يكتشف libavformat كل شيء تلقائيًا.

تنظر هذه الوظيفة فقط في الرأس ، لذا نحتاج الآن إلى التحقق من معلومات التدفق في الملف:

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

تقوم هذه الوظيفة بتمرير بيانات صالحة إلى تيارات pFormatCtx -> . نتعرف على وظيفة تصحيح أخطاء مناسبة ، توضح لنا ما بداخلها:

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

الآن تيارات pFormatCtx -> هي مجرد مجموعة من مؤشرات حجم pFormatCtx -> nb_streams . سنستمر في ذلك حتى نجد دفق الفيديو:

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;

توجد معلومات حول برنامج الترميز في الدفق في مكان يسمى " سياق برنامج الترميز ". يحتوي على جميع المعلومات حول برنامج الترميز الذي يستخدمه الدفق ، ولدينا الآن مؤشر عليه. ولكن لا يزال يتعين علينا العثور على برنامج الترميز الحقيقي وفتحه:

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

يرجى ملاحظة أنه لا يمكنك استخدام AVCodecContext مباشرة من دفق الفيديو! لذلك ، يجب عليك استخدام vcodec_copy_context () لنسخ السياق إلى موقع جديد (بالطبع ، بعد تخصيص الذاكرة له).

مخزن البيانات


الآن نحن بحاجة إلى مكان لتخزين الإطار:

AVFrame *pFrame = NULL;

// Allocate video frame
pFrame=av_frame_alloc();

نظرًا لأننا نخطط لإخراج ملفات PPM المخزنة في RGB 24 بت ، فسنحتاج إلى تحويل إطارنا من تنسيقه إلى RGB. سوف تفعل FFmpeg لنا. بالنسبة لمعظم المشاريع (بما في ذلك هذا المشروع) ، تحتاج إلى تحويل إطار البدء إلى تنسيق معين. حدد إطارًا للإطار المحول:

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

على الرغم من أننا اخترنا الإطار ، ما زلنا بحاجة إلى مكان لاستيعاب البيانات الأولية عند تحويله. نستخدم avpicture_get_size للحصول على الأحجام المناسبة وتخصيص المساحة اللازمة يدويًا:

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 هو نظير لـ C-function malloc من FFmpeg ، وهو عبارة عن غلاف بسيط حول malloc يوفر محاذاة لعناوين الذاكرة ، إلخ. بالمناسبة ، هذا لا يحمي ضد تسرب الذاكرة أو التحرير المزدوج أو المشاكل الأخرى التي تحدث مع malloc .

نستخدم الآن avpicture_fill لربط الإطار بالمخزن المؤقت المخصص حديثًا. فيما يتعلق بـ AVPicture : بنية AVPicture هي مجموعة فرعية من هيكل AVFrame - بداية بنية AVFrame مماثلة لبنية 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);

نحن بالفعل على خط النهاية! الآن نحن مستعدون للقراءة من الدفق!

قراءة البيانات


الآن ، لقراءة دفق الفيديو بأكمله ، نقرأ الحزمة التالية ، ونفك تشفيرها في إطارنا ، وبمجرد اكتمال فك التشفير ، قم بتحويل الإطار وحفظه:

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

لا شيء معقد: يقرأ av_read_frame () الحزمة ويحفظها في بنية AVPacket . يرجى ملاحظة أننا نقوم فقط بتوزيع بنية الحزمة - حيث تزودنا FFmpeg بالبيانات الداخلية التي تشير إليها packet.data . يؤدي هذا إلى تحرير av_free_packet () بعد ذلك بقليل . يقوم avcodec_decode_video () بتحويل الحزمة إلى إطار. ومع ذلك ، قد لا تتوفر لدينا جميع المعلومات التي نحتاجها للإطار بعد فك تشفير الحزمة ، وبالتالي فإن avcodec_decode_video () يعين frameFinished عندما يكون لدينا الإطار التالي. أخيرًا ، نستخدم sws_scale () للتحويل من تنسيقنا الخاص ( pCodecCtx ->pix_fmt ) في RGB. تذكر أنه يمكنك إرسال مؤشر AVFrame إلى مؤشر AVPicture . أخيرًا ، نقوم بتمرير المعلومات حول الإطار والارتفاع والعرض لوظيفة SaveFrame .

يتحدث عن الحزم. من الناحية الفنية ، يمكن أن تحتوي الحزمة على جزء فقط من الإطار ، بالإضافة إلى بتات البيانات الأخرى. ومع ذلك ، يضمن محلل FFmpeg أن الحزم التي نتلقاها تحتوي إما على إطار كامل أو حتى عدة إطارات.

الآن كل ما تبقى القيام به هو استخدام وظيفة SaveFrame لكتابة معلومات RGB إلى ملف PPM. على الرغم من أننا نتعامل بشكل سطحي مع تنسيق PPM نفسه ؛ صدقوني ، كل شيء يعمل هنا:

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

نقوم بتنفيذ ملف قياسي مفتوح ، وما إلى ذلك ، ثم نسجل بيانات RGB. الملف مكتوب سطرا. ملف PPM هو ببساطة ملف يتم فيه تقديم معلومات RGB كخط طويل. إذا كنت تعرف ألوان HTML ، فسيكون الأمر مثل وضع علامة على ألوان كل بكسل من النهاية الأولى إلى النهاية ، مثل # ff0000 # ff0000 .... ، مثل شاشة حمراء. (في الواقع ، يتم تخزينها بتنسيق ثنائي وبدون فاصل ، ولكن آمل أن تكون قد استوعبت الفكرة.) يشير العنوان إلى مدى اتساع الصورة وارتفاعها ، بالإضافة إلى الحجم الأقصى لقيم RGB.

الآن عد إلى وظيفتنا الرئيسية (). بمجرد الانتهاء من القراءة من دفق الفيديو ، نحتاج فقط لمسح كل شيء:

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

كما ترى ، نستخدم av_free للذاكرة المخصصة باستخدام avcode_alloc_frame و av_malloc .

هذا كل رمز! الآن ، إذا كنت تستخدم Linux أو منصة مشابهة ، فقم بتشغيل:

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

إذا كان لديك إصدار قديم من FFmpeg ، فقد تحتاج إلى إزالة -lavutil :

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

يجب أن تفتح معظم برامج الرسومات تنسيق PPM. تحقق من بعض ملفات الأفلام التي تم عمل لقطات الشاشة باستخدام برنامجنا.






الدرس 2: عرض الشاشة


القائمة الكاملة: 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 والفيديو


للرسم على الشاشة سنستخدم SDL. SDL تعني طبقة مباشرة بسيطة . إنها مكتبة ممتازة متعددة الوسائط عبر الأنظمة الأساسية تستخدم في العديد من المشاريع. يمكنك الحصول على المكتبة على الموقع الرسمي أو تنزيل حزمة المطور لنظام التشغيل الخاص بك ، إن وجد. ستحتاج إلى مكتبات لتجميع الكود من هذا الدرس (جميع الدروس الأخرى ، بالمناسبة ، هذا ينطبق أيضًا).

لدى SDL العديد من الطرق للرسم على الشاشة. إحدى الطرق لعرض الأفلام هي ما يسمى بتراكب YUV .

رسميا ، ولا حتى YUV ، ولكن YCbCr. بعض الناس ، بالمناسبة ، يصابون بحروق شديدة عندما يطلق على "YCbCr" اسم "YUV". بشكل عام ، YUV هو تنسيق تمثيلي ، و YCbCr هو تنسيق رقمي. يعين FFmpeg و SDL في التعليمات البرمجية ووحدات الماكرو YCbCr كـ YUV ، ولكن هذا هو.

YUV هي طريقة لتخزين بيانات الصورة الأولية مثل RGB. بشكل تقريبي ، Y هي مكون من السطوع ، و U و V هي مكونات اللون . (هذا أكثر تعقيدًا من RGB لأنه يتم تجاهل جزء من معلومات اللون ، ويمكنك الحصول على قياس واحد فقط لـ U و V لكل قياسين من Y ). تراكب YUVفي SDL يقبل مجموعة بيانات YUV الخام ويعرضها. يقبل 4 أنواع مختلفة من تنسيقات YUV ، لكن YV12 هو الأسرع منها. يوجد تنسيق YUV آخر يسمى YUV420P يتطابق مع YV12 ، باستثناء أنه يتم تبديل صفائف U و V. 420 يعني أنه تم أخذ عينات منه بنسبة 4: 2: 0 ، أي أنه لكل 4 قياسات للسطوع هناك قياس لون واحد ، لذلك يتم توزيع معلومات الألوان على أرباع. هذه طريقة جيدة لتوفير عرض النطاق الترددي لأن العين البشرية لا تزال لا تلاحظ هذه التغييرات. يشير الحرف اللاتيني "P" في الاسم إلى أن التنسيق هو "مستوي" ، مما يعني ببساطة أن المكونات هي Y ،U و V في صفائف منفصلة. يمكن لـ FFmpeg تحويل الصور إلى YUV420P ، وهو أمر مفيد للغاية ، لأن العديد من تدفقات الفيديو مخزنة بالفعل بهذا التنسيق أو يتم تحويلها بسهولة إليها.

وبالتالي ، فإن خطتنا الحالية هي استبدال دالة SaveFrame () من الدرس السابق وعرض إطارنا بدلاً من ذلك. ولكن عليك أولاً التعرف على الميزات الأساسية لمكتبة SDL. للبدء ، قم بتوصيل المكتبات وتهيئة 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 () المكتبة بشكل أساسي بالوظائف التي سنستخدمها . SDL_GetError () ، بالطبع ، هذه هي وظيفتنا المناسبة لتصحيح الأخطاء.

إنشاء العرض


الآن نحن بحاجة إلى مكان على الشاشة لترتيب العناصر. تسمى المنطقة الرئيسية لعرض الصور باستخدام SDL السطح :

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

لذلك قمنا بإعداد شاشة بعرض وارتفاع معينين. الخيار التالي هو عمق البت للشاشة - 0 - هذه قيمة خاصة تعني "نفس العرض الحالي".

الآن نقوم بإنشاء تراكب YUV على هذه الشاشة حتى نتمكن من إخراج الفيديو إليه ، وتكوين SWSContext لدينا لتحويل بيانات الصورة إلى 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
			 );

كما ذكرنا ، نستخدم YV12 لعرض الصورة والحصول على بيانات YUV420 من FFmpeg.

عرض الصور


حسنا ، كان ذلك سهلا بما فيه الكفاية! الآن نحن بحاجة فقط لإظهار الصورة. دعنا نذهب على طول الطريق إلى المكان الذي حصلنا على اللقطة النهائية. يمكننا التخلص من كل شيء لدينا لإطار RGB وسنقوم باستبدال SaveFrame () برمز العرض الخاص بنا. لعرض الصورة ، سنقوم بإنشاء بنية AVPicture وتعيين مؤشرات البيانات وحجم الخط لها لتراكب YUV الخاص بنا :

  if(frameFinished) {
    SDL_LockYUVOverlay(bmp);

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

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

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

في البداية ، نحظر التراكب ، لأننا نخطط للكتابة عليه. هذه عادة جيدة بحيث لا توجد مشاكل في وقت لاحق. تحتوي بنية AVPicture ، كما هو موضح أعلاه ، على مؤشر بيانات ، وهو عبارة عن صفيف من 4 مؤشرات. نظرًا لأننا نتعامل هنا مع YUV420P ، فلدينا 3 قنوات فقط وبالتالي 3 مجموعات بيانات فقط. قد تحتوي التنسيقات الأخرى على مؤشر رابع لقناة ألفا أو أي شيء آخر. حجم الخط هو ما يبدو. الهياكل المتشابهة في تراكب YUV هي متغيرات للبكسل والارتفاعات. (نغمات ، نغمات - إذا تم التعبير عنها من حيث SDL للإشارة إلى عرض خط بيانات معين.) لذا ، فإننا نشير إلى ثلاث صفائف pict.data على التراكب لدينا ، لذلك عندما نكتبفي الصورة ، نحن نسجل بالفعل في تراكبنا ، والذي بالطبع لديه بالفعل المساحة اللازمة المخصصة له على وجه التحديد. بنفس الطريقة ، نحصل على معلومات حجم الخط مباشرة من تراكبنا. نغير تنسيق التحويل إلى PIX_FMT_YUV420P ونستخدم sws_scale كما كان من قبل.

رسم صورة


لكننا ما زلنا بحاجة إلى تحديد SDL بحيث يعرض حقًا البيانات التي قدمناها له. نقوم أيضًا بتمرير مستطيل إلى هذه الوظيفة ، مما يشير إلى المكان الذي يجب أن يذهب إليه الفيلم ، إلى أي عرض وارتفاع يجب أن يتم تحجيمه. وبالتالي ، فإن مقياس SDL يتطور بالنسبة لنا ، وهذا يمكن أن يساعد GPU الخاص بك على التوسع بشكل أسرع:

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

الآن يتم عرض الفيديو لدينا!

دعنا نظهر ميزة أخرى لـ SDL: نظام الأحداث . يتم تكوين SDL بطريقة تؤدي إلى إدخال حدث عند إدخال الماوس أو تحريكه في تطبيق SDL أو إرسال إشارة إليه. يتحقق البرنامج بعد ذلك من هذه الأحداث إذا كان الغرض منه معالجة إدخال المستخدم. يمكن لبرنامجك أيضًا إنشاء أحداث لإرسال أحداث SDL إلى النظام. هذا مفيد بشكل خاص للبرمجة متعددة الخيوط باستخدام SDL ، والذي سنراه في الدرس رقم 4. في برنامجنا ، سنقوم بفحص الأحداث فورًا بعد معالجة الحزمة. في الوقت الحالي ، سنتعامل مع حدث SDL_QUIT حتى نتمكن من الخروج:

SDL_Event       event;

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

وهكذا نعيش! نتخلص من كل القمامة القديمة ونحن على استعداد لتجميعها. إذا كنت تستخدم Linux أو شيء مثل Linux ، فإن أفضل طريقة للترجمة باستخدام مكتبات SDL هي:

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

يعرض sdl-config ببساطة العلامات اللازمة لـ gcc لتمكين مكتبات SDL بشكل صحيح. قد تضطر إلى القيام بشيء آخر لجعل هذا التجميع على نظامك ؛ يرجى التحقق من وثائق SDL لنظامك عن أي رجل إطفاء. بمجرد تجميعها ، استمر في التشغيل.

ماذا يحدث عند تشغيل هذا البرنامج؟ يبدو أن الفيديو بالجنون! في الواقع ، نحن ببساطة نعرض جميع إطارات الفيديو بأسرع ما يمكن استخراجها من ملف الفيلم. ليس لدينا الرمز الآن لمعرفة متى نحتاج إلى عرض الفيديو. في النهاية (في الدرس رقم 5) سنبدأ بمزامنة الفيديو. لكن في الوقت الحاضر نفتقد شيئًا مهمًا بنفس القدر: الصوت!






الدرس 3: تشغيل الصوت


القائمة الكاملة: 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;
}

صوت


نود الآن تشغيل الصوت في التطبيق. توفر لنا SDL أيضًا طرقًا لتشغيل الصوت. يتم استخدام الوظيفة SDL_OpenAudio () لفتح جهاز الصوت نفسه. يتطلب الأمر بنية SDL_AudioSpec ، والتي تحتوي على جميع المعلومات حول الصوت الذي سنقوم بتشغيله .

قبل أن نوضح كيفية تكوين هذا ، نوضح أولاً كيفية معالجة الكمبيوتر للصوت بشكل عام. يتكون الصوت الرقمي من دفق طويل من العيناتكل منها يمثل معنى محدد لموجة صوتية. يتم تسجيل الأصوات بمعدل أخذ عينات محدد ، والذي يشير ببساطة إلى مدى سرعة تشغيل كل عينة ، ويقاس بعدد العينات في الثانية. ترددات أخذ العينات التقريبية هي 22050 و 44100 عينة في الثانية، وهي سرعة المستخدمة للإذاعة وCD، على التوالي. بالإضافة إلى ذلك ، يمكن أن تحتوي معظم الصوت على أكثر من قناة واحدة لصوت الاستريو أو الصوت المحيطي ، لذلك ، على سبيل المثال ، إذا كانت العينة في الاستريو ، فستأتي العينات اثنين في كل مرة. عندما نحصل على البيانات من ملف الفيلم ، لا نعرف عدد العينات التي سنحصل عليها ، لكن FFmpeg لا ينتج عينات مكسورة - وهذا يعني أيضًا أنه لن يفصل عينة الاستريو أيضًا.

طريقة تشغيل الصوت في SDL هي كما يلي. يتم تكوين معلمات الصوت: تردد أخذ العينات ، عدد القنوات ، إلخ. وكذلك تعيين وظيفة رد الاتصال وبيانات المستخدم. عندما نبدأ في تشغيل الصوت ، ستستدعي SDL باستمرار وظيفة رد الاتصال هذه وتطلب منها ملء المخزن المؤقت الصوتي بعدد معين من وحدات البايت. بعد وضع هذه المعلومات في بنية SDL_AudioSpec ، نسمي SDL_OpenAudio () ، والذي سيفتح جهاز الصوت ويعيد لنا بنية AudioSpec أخرى . هذه هي الخصائص التي سنستخدمها بالفعل - ليس هناك ما يضمن أننا سنحصل بالضبط على ما طلبناه!

إعداد الصوت


فقط ضعها في الاعتبار في الوقت الحالي ، لأنه ليس لدينا أي معلومات حول دفق الصوت حتى الآن! دعنا نعود إلى المكان في الكود الخاص بنا حيث وجدنا دفق الفيديو ومعرفة أي دفق هو دفق الصوت:

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

هنا يمكننا الحصول على جميع المعلومات التي نريدها من AVCodecContext من الدفق ، تمامًا كما فعلنا مع دفق الفيديو:

AVCodecContext *aCodecCtxOrig;
AVCodecContext *aCodecCtx;

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

إذا كنت تتذكر ، في الدروس السابقة ، لا يزال يتعين علينا فتح برنامج ترميز الصوت نفسه. انه سهل:

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

في سياق برنامج الترميز يحتوي على جميع المعلومات اللازمة لتكوين الصوت لدينا:

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

لنستعرض كل عنصر:
  • freq ( التكرار ): معدل أخذ العينات ، كما هو موضح سابقًا.
  • format (): SDL , . «S» «S16SYS» «», 16 , 16 , «SYS» , , . , avcodec_decode_audio2 .
  • channels (): .
  • silence (): , . 0.
  • samples (): , , SDL , . - 512 8192; FFplay, , 1024.
  • رد الاتصال (callback): هنا نقوم بتمرير وظيفة رد الاتصال الحقيقية. سنتحدث أكثر عن وظيفة رد الاتصال لاحقًا.
  • بيانات المستخدم : ستعطي SDL رد الاتصال الخاص بنا بمؤشر خالٍ لأي بيانات مستخدم نريدها. نريد أن نعلمه بسياق الترميز الخاص بنا ؛ أقل قليلا سيكون من الواضح لماذا.

أخيرًا ، افتح الصوت باستخدام SDL_OpenAudio .

قوائم الانتظار


ومن الضروري! الآن نحن على استعداد لاستخراج المعلومات الصوتية من الدفق. ولكن ماذا تفعل بهذه المعلومات؟ سنستقبل الحزم باستمرار من ملف الفيلم ، ولكن في نفس الوقت ، ستستدعي SDL وظيفة رد الاتصال! سيكون الحل هو إنشاء نوع من الهيكل العالمي الذي يمكننا من خلاله إدراج الحزم الصوتية بحيث يكون لاستدعاء الصوت لدينا شيء لاستقبال البيانات الصوتية! لذا ، هذا ما سنفعله لإنشاء قائمة انتظار الحزم. يحتوي FFmpeg على بنية للمساعدة في هذا: AVPacketList ، وهو مجرد قائمة مرتبطة للحزم. فيما يلي هيكل قائمة الانتظار لدينا:

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

أولاً ، يجب أن نشير إلى أن nb_packets يختلف في الحجم - يشير الحجم إلى حجم البايت الذي نحصل عليه من حجم الحزمة -> . لاحظ أن لدينا متغير ومتغير شرط. وذلك لأن SDL يقوم بعملية الصوت كدفق منفصل. إذا لم نقم بحظر قائمة الانتظار بشكل صحيح ، فيمكننا بالفعل إتلاف بياناتنا. دعونا نرى كيف يتم تنفيذ قائمة الانتظار. يجب أن يعرف كل مبرمج يحترم نفسه كيفية إنشاء قوائم الانتظار ، ولكننا سنوضح أيضًا كيفية القيام بذلك بحيث يسهل عليك تعلم وظائف SDL.

أولاً ، نقوم بإنشاء دالة لتهيئة قائمة الانتظار:

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

يقوم SDL_LockMutex () بحظر كائن المزامنة في قائمة الانتظار حتى نتمكن من إضافة شيء ما ، ثم يقوم SDL_CondSignal () بإرسال إشارة إلى دالة get الخاصة بنا (إذا كانت تتوقع ذلك) من خلال المتغير الشرطي لإخباره بوجود بيانات ويمكن استمرارها ، لمزيد من فتح المؤخر.

إليك دالة get المقابلة . لاحظ كيف يقوم SDL_CondWait () بإنشاء كتلة الوظائف (أي توقف مؤقتًا حتى نحصل على البيانات) إذا أخبرناها بذلك:

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

كما ترون ، قمنا بلف الوظيفة في دورة أبدية ، لذلك سنحصل بالتأكيد على بعض البيانات إذا أردنا منعها. نتجنب التكرار إلى الأبد باستخدام الدالة SDL_CondWait (). بشكل أساسي ، كل ما تفعله CondWait هو انتظار إشارة من SDL_CondSignal () (أو SDL_CondBroadcast ()) ثم المتابعة . ومع ذلك ، يبدو أننا وقعناها في كائن متحرك - إذا أمسكنا بالقفل ، فلن تتمكن وظيفة الوضع لدينا من وضع أي شيء في قائمة الانتظار! ومع ذلك ، ما يفعله SDL_CondWait () أيضًا بالنسبة لنا هو إلغاء حظر كائن المزامنة الذي نقدمه له ، ثم حاول مرة أخرى قفله بمجرد استلامنا للإشارة.

لكل إطفائي


ترى أيضًا أن لدينا متغيرًا عامًا للإقلاع نتحقق منه للتأكد من أننا لم نقم بتعيين إشارة خرج في البرنامج (يعالج SDL إشارات TERM تلقائيًا ، وما إلى ذلك). خلاف ذلك ، سوف يستمر الخيط إلى الأبد ، وعلينا أن نقتل البرنامج بقتل -9 :

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

سنضع علامة الخروج على 1.

نحن نطعم الحزم


يبقى فقط لتكوين قائمة الانتظار لدينا:

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

  packet_queue_init(&audioq);
  SDL_PauseAudio(0);

يبدأ SDL_PauseAudio () أخيرًا وحدة الصوت. تستنسخ الصمت إذا لم تتلقى البيانات ؛ لكن هذا لا يحدث على الفور.

لذلك ، لدينا قائمة انتظار تم تكوينها ، ونحن الآن على استعداد لإرسال حزم لها. ننتقل إلى دورة قراءة الحزمة الخاصة بنا:

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

يرجى ملاحظة أننا لا نطلق الحزمة بعد وضعها في قائمة الانتظار. سنقوم بإصداره لاحقًا عندما نقوم بفك التشفير.

استرداد الحزم


الآن دعنا نجعل أخيرًا وظيفة تسجيل الصوت لجلب الحزم من قائمة الانتظار. يجب أن يبدو رد الاتصال كالتالي:

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

بيانات المستخدم ، هي المؤشر الذي قدمناه SDL ، الدفق هو المخزن المؤقت الذي سنكتب فيه البيانات الصوتية ، و len هو حجم هذا المخزن المؤقت. هذا هو الرمز:

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

في الواقع ، هذه حلقة بسيطة تستخرج البيانات من وظيفة أخرى كتبناها ، audio_decode_frame () ، وتحفظ النتيجة في مخزن مؤقت متوسط ​​، وتحاول كتابة len bytes في الدفق وتتلقى المزيد من البيانات إذا لم يكن لدينا ما يكفي من البيانات أو حفظها لوقت لاحق ، إذا بقي لدينا شيء. يبلغ حجم ملف audio_buf 1.5 ضعف حجم أكبر إطار صوتي سيعطيناه FFmpeg ، مما يمنحنا هامشًا جيدًا.

فك تشفير الصوت النهائي


دعونا ننظر إلى الدواخل من وحدة فك ترميز 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;
  }
}

تبدأ العملية برمتها في الواقع بالقرب من نهاية الوظيفة ، حيث نسمي packet_queue_get (). نأخذ الحزمة من قائمة الانتظار ونحفظ المعلومات منها. بعد ذلك ، عندما يكون لدينا الحزمة للعمل ، فإننا نسمي avcodec_decode_audio4 () ، والذي يشبه إلى حد كبير الوظيفة الشقيقة avcodec_decode_video () ، باستثناء أنه في هذه الحالة يمكن أن تحتوي الحزمة على أكثر من إطار واحد. لذلك ، قد تحتاج إلى الاتصال به عدة مرات للحصول على جميع البيانات من الحزمة. بعد استلام الإطار ، نقوم ببساطة بنسخه إلى المخزن المؤقت الصوتي الخاص بنا ، مع التأكد من أن حجم البيانات أصغر من المخزن المؤقت الصوتي الخاص بنا. وتذكر أيضًا عن إرسال audio_bufإلى النوع الصحيح ، لأن SDL يعطي مخزنًا مؤقتًا int 8 بت ، ويعطينا FFmpeg بيانات في مخزن مؤقت int 16 بت. يجب عليك أيضًا مراعاة الفرق بين len1 و data_size . len1 هو حجم الحزمة التي استخدمناها ، و data_size هو مقدار البيانات الخام التي تم إرجاعها.

عندما يكون لدينا بعض البيانات ، نعود على الفور لمعرفة ما إذا كنا بحاجة للحصول على المزيد من البيانات من قائمة الانتظار أو انتهينا. إذا ما زلنا بحاجة إلى معالجة الحزمة ، فالتزم بها. إذا أكملت الحزمة ، فأطلقها أخيرًا.

وكل شيء! لقد نقلنا الصوت من حلقة القراءة الرئيسية إلى قائمة الانتظار ، والتي تتم قراءتها بعد ذلك بواسطة وظيفة audio_callback، الذي ينقل هذه البيانات إلى SDL ، وينقل SDL إلى بطاقة الصوت الخاصة بك. المضي قدما وتجميع:

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

Gip-gip-hooray! لا يزال الفيديو يحمل بأقصى سرعة ، ولكن الصوت قيد التشغيل بالفعل كما ينبغي. لماذا هذا؟ نعم ، لأن معلومات الصوت لها تردد أخذ عينات - نقوم بضخ معلومات الصوت بالسرعة التي يتضح بها ، ولكن يتم تشغيل الصوت ببساطة في هذا الدفق وفقًا لتردد أخذ العينات.

نحن جاهزون تقريبًا لمزامنة الفيديو والصوت ، ولكن أولاً نحتاج إلى إجراء إعادة تنظيم صغيرة للبرنامج. نجحت طريقة طابور الصوت وتشغيله باستخدام دفق منفصل بشكل جيد للغاية: فقد جعلت الشفرة أكثر قابلية للإدارة وأكثر نمطيًا. قبل أن نبدأ مزامنة الفيديو مع الصوت ، نحتاج إلى تبسيط الرمز. في السلسلة التالية سوف ننتج تدفقات التحكم!






الدرس 4: مواضيع متعددة


القائمة الكاملة للبرنامج التعليمي .04 ج
// 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;

}

نظرة عامة


في المرة الأخيرة ، أضفنا دعم الصوت باستخدام ميزات الصوت SDL. أطلق SDL خيطًا لإجراء عمليات رد للوظيفة التي حددناها في كل مرة كانت هناك حاجة إلى صوت. الآن سنفعل الشيء نفسه مع عرض الفيديو. وهذا يجعل الشفرة أكثر نموذجية وأسهل للعمل معها - خاصة إذا كنت ترغب في إضافة التزامن. إذن، أين نبدأ؟

لاحظ أن وظيفتنا الرئيسية تتعامل كثيرًا: فهي تمر عبر حلقة الأحداث ، وتقرأ الحزم وتقوم بفك تشفير الفيديو. ما سنفعله هو تقسيم كل شيء إلى أجزاء: سيكون لدينا تيار مسؤول عن فك حزم البيانات ؛ ثم تتم إضافة هذه الحزم إلى قائمة الانتظار وقراءتها من قبل تدفقات الصوت والفيديو المقابلة. لقد قمنا بالفعل بضبط دفق الصوت كما هو مطلوب ؛ مع تدفق الفيديو ، سيكون الأمر أكثر صعوبة إلى حد ما ، حيث سيتعين علينا التأكد من عرض الفيديو بمفردنا. سنضيف رمز العرض الفعلي إلى الحلقة الرئيسية. ولكن بدلاً من عرض الفيديو في كل مرة ننفذ فيها الحلقة ، ندمج عرض الفيديو في حلقة الحدث. تتمثل الفكرة في فك تشفير الفيديو ، وحفظ الإطار المستلم في قائمة انتظار أخرى ، ثم إنشاء حدث خاص بك ( FF_REFRESH_EVENT) ، التي نضيفها إلى نظام الأحداث ، فعندما ترى حلقة الحدث لدينا هذا الحدث ، ستعرض الإطار التالي في قائمة الانتظار. في ما يلي توضيح مناسب لـ ASCII لما يحدث:


السبب الرئيسي لنقل التحكم في عرض الفيديو من خلال حلقة الأحداث هو أنه من خلال تدفق SDL_Delay يمكننا التحكم بدقة عند ظهور إطار الفيديو التالي على الشاشة. عندما نزامن الفيديو أخيرًا في الدرس التالي ، ما عليك سوى إضافة رمز لجدولة تحديث الفيديو التالي بحيث تظهر الصورة الصحيحة على الشاشة في الوقت المناسب.

تبسيط الرمز


دعونا مسح الرمز قليلاً. لدينا كل هذه المعلومات حول برامج ترميز الصوت والفيديو ، وسنقوم بإضافة قوائم انتظار ، ومخازن مؤقتة ، والله أعلم ماذا أيضًا. كل هذه الأشياء هي لوحدة منطقية معينة ، وهي - للفيلم. لذا ، نعتزم إنشاء بنية كبيرة تحتوي على كل هذه المعلومات تسمى 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;

هنا نرى تلميحات عما سنحصل عليه في النهاية. أولاً ، نرى المعلومات الأساسية - سياق التنسيق ومؤشرات دفق الصوت والفيديو ، بالإضافة إلى كائنات AVStream المقابلة . ثم نرى أن بعض هذه المخازن المؤقتة الصوتية يتم نقلها إلى هذه البنية. كانت مخصصة ( audio_buf ، audio_buf_size ، وما إلى ذلك) للحصول على معلومات حول الصوت الذي كان لا يزال موجودًا (أو مفقودًا). أضفنا طابورًا آخر للفيديو ومخزنًا مؤقتًا (والذي سيتم استخدامه كقائمة انتظار ؛ لهذا لا نحتاج إلى أي قوائم انتظار باهظة) للإطارات التي تم فك تشفيرها (محفوظة كتراكب). هيكل صورة الفيديو- هذا من صنعنا (سنرى ما سيكون فيه عندما نصل إليه). يمكنك أيضًا ملاحظة أننا قد خصصنا مؤشرات لتيارين إضافيين سننشئهما ، بالإضافة إلى علامة خروج واسم ملف فيلم.

لذا ، نعود الآن إلى الوظيفة الرئيسية لنرى كيف يغير هذا برنامجنا. لنقم بإعداد هيكل VideoState الخاص بنا :

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

  SDL_Event       event;

  VideoState      *is;

  is = av_mallocz(sizeof(VideoState));

av_mallocz () هي وظيفة جيدة من شأنها تخصيص ذاكرة لنا وصفرها .

ثم نقوم بتهيئة أقفالنا لمخزن العرض المؤقت ( pictq ) ، نظرًا لأن حلقة الحدث تستدعي وظيفة العرض الخاصة بنا - تذكر ، ستقوم وظيفة العرض باسترداد الإطارات التي تم فك تشفيرها مسبقًا من pictq . في الوقت نفسه ، سيضع جهاز فك تشفير الفيديو لدينا معلومات فيه - لا نعرف من يصل أولاً. أتمنى أن تفهم أن هذه حالة سباق كلاسيكية. لذلك ، نقوم بتوزيعه الآن قبل البدء في أي مواضيع. دعونا أيضًا نسخ اسم الفيلم إلى VideoState :

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

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

av_strlcpy هي دالة من FFmpeg تقوم ببعض عمليات فحص الحدود الإضافية إلى جانب strncpy .

خيطنا الأول


دعنا ندير خيوطنا ونفعل شيئًا حقيقيًا:

schedule_refresh(is, 40);

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

Schedule_refresh هي وظيفة سنحددها لاحقًا. ما تفعله هو إخبار النظام بإنتاج FF_REFRESH_EVENT بعد العدد المحدد من المللي ثانية. سيؤدي هذا بدوره إلى استدعاء وظيفة تحديث الفيديو عندما نراها في قائمة انتظار الحدث. ولكن الآن دعونا نلقي نظرة على SDL_CreateThread ().

يقوم SDL_CreateThread () بذلك فقط - فهو يولد سلسلة رسائل جديدة لها حق الوصول الكامل إلى كل ذاكرة العملية الأصلية ، ويبدأ مؤشر الترابط الذي يتم تنفيذه بواسطة الوظيفة التي نمنحها لها. ستقوم هذه الوظيفة أيضًا بنقل البيانات المعرفة من قبل المستخدم. في هذه الحالة ، نسمي decode_thread () ونرفق بنية VideoState الخاصة بنا. لا يوجد شيء جديد في النصف الأول من الوظيفة ؛ يقوم فقط بفتح الملف والعثور على فهرس تدفقات الصوت والفيديو. الشيء الوحيد الذي نقوم به بشكل مختلف هو الحفاظ على سياق التنسيق في هيكلنا الكبير. بعد العثور على فهارس التدفق الخاصة بنا ، نسمي وظيفة أخرى نحددها ، stream_component_open (). هذه طريقة طبيعية إلى حد ما للفصل ، وبما أننا نقوم بالكثير من الأشياء المماثلة لإعداد برنامج ترميز الفيديو والصوت ، فإننا نعيد استخدام بعض التعليمات البرمجية ، مما يجعلها وظيفة.

دالة Stream_component_open() هو المكان الذي نكتشف فيه وحدة فك الترميز الخاصة بنا ، وتكوين معلمات الصوت ، وحفظ المعلومات المهمة في هيكلنا الكبير وإطلاق تدفقات الصوت والفيديو الخاصة بنا. نقوم هنا أيضًا بإدخال معلمات أخرى ، مثل الاستخدام القسري لبرنامج الترميز بدلاً من الكشف التلقائي ، إلخ. مثله:

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

هذا هو تقريبًا نفس الرمز الذي كان لدينا من قبل ، باستثناء أنه الآن معمم للصوت والفيديو. لاحظ أنه بدلاً من aCodecCtx ، قمنا بتكوين هيكلنا الكبير كبيانات مستخدم لاستدعاءنا الصوتي. قمنا أيضًا بحفظ التدفقات نفسها على أنها audio_st و video_st . أضفنا أيضًا قائمة انتظار الفيديو الخاصة بنا وقمنا بإعدادها تمامًا مثل قائمة انتظار الصوت الخاصة بنا. خلاصة القول هي تشغيل تدفقات الفيديو والصوت. تقوم هذه البتات بما يلي:

    SDL_PauseAudio(0);
    break;

/* ...... */

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

تذكر SDL_PauseAudio () من الدرس الأخير. يتم استخدام SDL_CreateThread () بنفس الطريقة. ارجع إلى وظيفة video_thread ().

قبل ذلك ، دعنا نعود إلى النصف الثاني من دالة decode_thread (). بشكل أساسي ، إنها مجرد حلقة لقراءة الحزمة ووضعها في قائمة الانتظار الصحيحة:

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

لا يوجد شيء جديد حقًا هنا ، باستثناء أن لدينا الآن أقصى حجم لقائمة انتظار الصوت والفيديو ، وقد أضفنا تدقيق أخطاء القراءة. يحتوي سياق التنسيق على بنية ByteIOContext داخل تسمى pb . ByteIOContext هو بنية تقوم بتخزين جميع المعلومات حول الملفات ذات المستوى المنخفض.

بعد التكرار ، لدينا كل التعليمات البرمجية التي تنتظر حتى يكتمل البرنامج أو يبلغ عنه. هذا الرمز مفيد لأنه يوضح كيفية دفع الأحداث - وهو شيء سنحتاجه لاحقًا لعرض الفيديو:

  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;

نحصل على قيم للأحداث المخصصة باستخدام ثابت SDL SDL_USEREVENT . يجب تعيين حدث المستخدم الأول على SDL_USEREVENT ، و SDL_USEREVENT + 1 التالي ، وما إلى ذلك. تم تعريف FF_QUIT_EVENT في برنامجنا على أنه SDL_USEREVENT + 1 . يمكننا أيضًا تمرير بيانات المستخدم إذا لزم الأمر ، وهنا نمرر مؤشرنا إلى هيكل كبير. أخيرًا ، نسمي SDL_PushEvent (). في تبديل حلقة الحدث ، قمنا بوضع هذا في قسم SDL_QUIT_EVENTالتي كان لدينا من قبل. سنرى دورة الأحداث لدينا بمزيد من التفصيل ؛ في الوقت الحالي ، تأكد فقط أننا عندما نضغط على FF_QUIT_EVENT ، فإننا نلتقطها لاحقًا ونبدل علامة الخروج.

إطار الاستلام: video_thread


بعد إعداد برنامج الترميز ، يمكنك بدء دفق الفيديو. يقرأ هذا الدفق الحزم من قائمة انتظار الفيديو ، ويفك تشفير الفيديو في إطارات ، ثم يستدعي وظيفة queue_picture لوضع الإطار الذي تمت معالجته في قائمة انتظار الصور:

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

يجب فهم معظم هذه الوظيفة الآن. لقد قمنا للتو بنسخ وظيفة avcodec_decode_video2 هنا ، ببساطة استبدال بعض الحجج ؛ على سبيل المثال ، لدينا AVStream مخزنة في هيكلنا الكبير ، لذلك نحصل على برنامج الترميز من هناك. نستمر فقط في تلقي الحزم من قائمة انتظار الفيديو حتى يخبرنا شخص ما بالخروج أو نجد خطأً.

إطار قائمة الانتظار


دعنا نلقي نظرة على الوظيفة التي تخزن pFrame الخاص بنا الذي تم فك ترميزه في قائمة انتظار الصور لدينا. نظرًا لأن قائمة انتظار الصور الخاصة بنا عبارة عن تراكب لـ SDL (من المفترض أن تسمح لوظيفة عرض الفيديو بأداء أقل قدر ممكن من الحساب) ، نحتاج إلى تحويل إطارنا إلى ذلك. البيانات التي نخزنها في قائمة انتظار الصور هي البنية التي أنشأناها:

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

يحتوي هيكلنا الكبير على مخزن مؤقت لهذه الملفات ، حيث يمكننا تخزينها. ومع ذلك ، نحتاج إلى توزيع SDL_Overlay بأنفسنا (انتبه إلى العلامة المعينة ، والتي توضح ما إذا قمنا بذلك أم لا).

لاستخدام قائمة الانتظار هذه ، لدينا مؤشرين - فهرس الكتابة وفهرس القراءة. نتتبع أيضًا عدد الصور الفعلية الموجودة في المخزن المؤقت. للكتابة إلى قائمة الانتظار ، ننتظر أولاً حتى يتم مسح المخزن المؤقت ، بحيث يكون لدينا مكان لتخزين VideoPicture لدينا . ثم نتحقق لنرى ما إذا قمنا بتعيين التراكب في فهرسنا القياسي؟ إذا لم يكن الأمر كذلك ، فأنت بحاجة إلى تخصيص ذاكرة. يجب علينا أيضًا إعادة تخصيص المخزن المؤقت إذا تغير حجم النافذة!

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

دعنا نلقي نظرة على الوظيفة المخصصة ( تصوير ):

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

يجب أن تتعرف على الوظيفة SDL_CreateYUVOverlay ، التي انتقلنا إليها من حلقتنا الرئيسية إلى هذا القسم. يجب أن يكون هذا الرمز واضحًا بشكل معقول حتى الآن. ومع ذلك ، لدينا الآن قفل mutex ، لأن موضوعين لا يمكنهما كتابة المعلومات على الشاشة في وقت واحد! لن يسمح ذلك للتدخل في وظيفة custom_picture بوظيفة أخرى تعرض الصورة. (لقد أنشأنا هذا القفل كمتغير عام وقمنا بتهيئته بشكل رئيسي () ؛ انظر الكود.) تذكر أننا نحافظ على العرض والارتفاع في بنية VideoPicture ، لأننا بحاجة للتأكد من أن حجم مقطع الفيديو الخاص بنا لا يتغير لسبب ما.
حسنًا ، لقد قمنا بتسويتها ، ولدينا تراكب YUVمكرسة وجاهزة لاستقبال الصورة. دعنا نعود إلى queue_picture ونلقي نظرة على الرمز لنسخ الإطار إلى التراكب. يجب أن يكون هذا الجزء مألوفًا لك:

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

هنا ، معظمها مجرد رمز استخدمناه سابقًا لملء تراكب YUV بإطارنا . الجزء الأخير ببساطة "يضيف" قيمتنا إلى قائمة الانتظار. تعمل قائمة الانتظار ، ويتم إضافة القيم إليها حتى تمتلئ ، وتحدث القراءة منها أثناء وجود شيء على الأقل فيها. لذلك، كل هذا يتوقف على قيمة غير -> pictq_size ، الأمر الذي يتطلب منا لمنع ذلك. لذا ، ماذا نفعل هنا: قم بزيادة مؤشر التسجيل (وإذا لزم الأمر ، ابدأ من جديد) ، ثم قم بحجب قائمة الانتظار وزيادة حجمها. الآن سيعرف القارئ أن هناك المزيد من المعلومات حول قائمة الانتظار ، وإذا كان هذا يجعل قائمة الانتظار لدينا ممتلئة ، وسيعرف مسجلنا عنها.

عرض الفيديو


هذا كل ما في موضوع الفيديو! لقد أكملنا الآن جميع سلاسل المحادثات المجانية ، باستثناء سلسلة واحدة - هل تتذكر كيف كنا نطلق على وظيفة Schedule_refresh () منذ وقت طويل ؟ ألق نظرة على ما حدث بالفعل:

/* 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 () هي وظيفة SDL تقوم ببساطة باستدعاء دالة معرفة من قبل المستخدم بعد عدد معين من المللي ثانية (وإذا لزم الأمر ، تنقل بعض البيانات المعرفة من قبل المستخدم). سنستخدم هذه الوظيفة لجدولة تحديثات الفيديو - في كل مرة نسميها ، تقوم بتعيين مؤقت يقوم بتشغيل حدث ، والذي بدوره سيؤدي إلى استدعاء وظيفتنا الرئيسية () لوظيفة تستخرج إطارًا من صورة قائمة الانتظار وتعرض لها! عفوا! ثلاثة "أي / أي / أي / أي" في جملة واحدة! لذا ، لنفعل أول شيء نفعله - أطلق هذا الحدث. وهذا يرسلنا إلى:

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

يتم إطلاق الحدث من قبل صديقنا القديم. يتم تعريف FF_REFRESH_EVENT هنا على أنه SDL_USEREVENT + 1 . وتجدر الإشارة إلى أنه عندما نرجع 0 ، يقوم SDL بإيقاف المؤقت ، لذلك لا يتم تنفيذ رد الاتصال مرة أخرى.

الآن بعد أن اتصلنا بـ FF_REFRESH_EVENT مرة أخرى ، نحتاج إلى معالجته في حلقة الأحداث لدينا:

for(;;) {

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

ما الذي يرسلنا هنا إلى هذه الوظيفة ، والتي تستخرج بالفعل البيانات من قائمة انتظار الصور الخاصة بنا:

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

في الوقت الحالي ، هذه الوظيفة بسيطة جدًا: فهي تعالج قائمة الانتظار عندما يكون لدينا شيء ما ، وتضبط مؤقتًا لعرض إطار الفيديو التالي ، وتستدعي video_display لعرض الفيديو على الشاشة بالفعل ، ثم تزيد العداد في قائمة الانتظار ، مع تقليل حجمه. قد تلاحظ أننا لا نقوم بأي شيء باستخدام vp في هذه الوظيفة ، وإليك السبب: هذا في المستقبل. ولكن بعد ذلك بقليل. سنستخدمه للوصول إلى معلومات الوقت عندما نبدأ في مزامنة الفيديو مع الصوت. هنا ، ألق نظرة على المكان في الرمز حيث تتم كتابة التعليق "رمز التوقيت هنا". في هذا القسم ، سنكتشف متى يجب أن نعرض إطار الفيديو التالي ، ثم ندخل هذه القيمة في دالة الجدول الزمني.(). في الوقت الحالي ، ندخل فقط قيمة وهمية 80. من الناحية الفنية ، يمكنك تخمين هذه القيمة والتحقق منها وإعادة تجميعها لكل فيلم ، ولكن: 1) ستبدأ في التباطؤ بعد فترة من الوقت و 2) إنها غبية جدًا. على الرغم من أننا في المستقبل سنعود إلى هذه النقطة.

نحن على وشك الإنتهاء. يبقى أن تفعل شيئًا واحدًا فقط: عرض الفيديو! فيما يلي وظيفة 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);
  }
}

نظرًا لأن الشاشة يمكن أن تكون بأي حجم (قمنا بتثبيت 640 × 480 ، وهناك طرق لتكوينها بحيث يتم تغيير حجم المستخدم) ، تحتاج إلى تحديد حجم المنطقة المستطيلة للفيلم بشكل ديناميكي. لذلك ، تحتاج أولاً إلى معرفة نسبة العرض إلى الارتفاع للفيلم ، فقط العرض مقسومًا على الارتفاع. سيكون لبعض برامج الترميز نسبة عرض إلى ارتفاع فردية للعينة ، وهي ببساطة عرض / ارتفاع بكسل واحد أو عينة. نظرًا لأن قيم الارتفاع والعرض في سياق برنامج الترميز الخاص بنا يتم قياسها بالبكسل ، فإن نسبة العرض إلى الارتفاع الفعلية تساوي نسبة العرض إلى الارتفاع مضروبة في نسبة العرض إلى الارتفاع للعينة. ستعرض بعض برامج الترميز نسبة عرض إلى ارتفاع 0 ، مما يعني أن حجم كل بكسل 1x1. ثم نقيس الفيلم بهذه الطريقةبحيث تناسبها على الشاشة قدر الإمكان. عكس البت& -3 ببساطة تقريب القيمة إلى أقرب مضاعف أربعة. ثم قم بتوسيط الفيلم واستدعي SDL_DisplayYUVOverlay () للتأكد من استخدام كائن الشاشة المتبقي للوصول إليه.

وكل شيء؟ هل انتهينا؟ لا تزال بحاجة إلى إعادة كتابة الرمز الصوتي لاستخدام VideoStruct الجديد، ولكن هذه تغييرات تافهة يمكن رؤيتها في نموذج الرمز. آخر شيء يتعين علينا القيام به هو تغيير رد الاتصال الخاص بنا لوظيفة رد اتصال الخروج الداخلي في FFmpeg:

VideoState *global_video_state;

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

قم بتعيين global_video_state على هيكل كبير في main ().

هذا كل ما في الأمر! نقوم بتجميع:

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

واستمتع بالفيلم دون مزامنة! في الخطوة التالية ، سنقوم في النهاية بإنشاء مشغل فيديو يعمل حقًا !






الدرس 5: مزامنة الفيديو


القائمة الكاملة للبرنامج التعليمي 05
// 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;

}

تحذير


عندما كتبت هذا الدليل للتو ، تم أخذ كل كود المزامنة الخاص بي من إصدار ffplay.c . اليوم هو برنامج مختلف تمامًا ، وأدت التحديثات في مكتبات FFmpeg (وفي ffplay.c نفسها) إلى تغييرات أساسية. على الرغم من أن هذا الرمز لا يزال يعمل ، إلا أنه قديم بالفعل ، وهناك العديد من التحسينات الأخرى التي يمكن استخدامها في هذا الدليل.

كيف يتزامن الفيديو


حتى الآن ، كان لدينا مشغل فيلم عديم الفائدة تقريبًا. نعم ، إنها تشغل الفيديو ، ونعم ، تشغل الصوت ، ولكن هذا ليس تمامًا ما نسميه فيلمًا. إذن ماذا نفعل بعد ذلك؟

PTS و DTS


لحسن الحظ ، تحتوي تدفقات الصوت والفيديو على معلومات حول مدى السرعة وفي أي وقت يجب تشغيلها. تدفقات الصوت لديها معدل أخذ العينات ، وتدفقات الفيديو لها إطارات في الثانية. ومع ذلك ، إذا قمنا فقط بمزامنة الفيديو عن طريق حساب عدد الإطارات والضرب في معدل الإطارات ، فهناك فرصة جيدة لعدم تزامنه مع الصوت. لذلك ، سنذهب في الاتجاه الآخر. يمكن أن تحتوي الحزم من القطار على ما يسمى ختم وقت فك التشفير ( DTS - من d ecoding t ime s tamp ) وختم وقت العرض ( PTS - من p resentation t ime sحشايا ). لفهم هذين المعنيين ، تحتاج إلى معرفة كيفية تخزين الأفلام. تستخدم بعض التنسيقات ، مثل MPEG ، ما يسمونه إطارات B ( Bed وهو ثنائي الاتجاه ، إنجلترا. ثنائي الاتجاه ). ويطلق على نوعين آخرين من الإطارات I-إطارات و P-إطارات ( I هو داخلي ، ط nner ، و P الوسائل توقع ، ص redicted ). تحتوي الإطارات I على الصورة الكاملة. إطارات Pتعتمد على إطارات I- و P السابقة وتختلف عن الإطارات السابقة ، أو يمكنك أيضًا تسمية - deltas. الإطارات B تشبه الإطارات P ، ولكنها تعتمد على المعلومات الواردة في كل من الإطارات السابقة واللاحقة! حقيقة أن الإطار قد لا يحتوي على الصورة نفسها ، ولكن الاختلافات مع الإطارات الأخرى - يشرح لماذا قد لا يكون لدينا إطار منتهي بعد استدعاء avcodec_decode_video2 .

لنفترض أن لدينا فيلمًا فيه 4 إطارات في هذا التسلسل: IBBP . ثم نحتاج إلى معرفة المعلومات من الإطار P الأخير قبل أن نتمكن من عرض أي من الإطارين السابقين B. ولهذا السبب ، يمكن تخزين الإطارات في تسلسل لا يطابق ترتيب العرض الفعلي: IPBB. هذا هو الغرض من الطابع الزمني لفك التشفير والطابع الزمني للعرض التقديمي لكل إطار. يخبرنا الطابع الزمني لفك الشفرة عندما نحتاج إلى فك شفرة شيء ما ، ويخبرنا الطابع الزمني للعرض التقديمي عندما نحتاج إلى عرض شيء ما. لذلك ، في هذه الحالة ، قد يبدو الدفق الخاص بنا كما يلي:

   PTS: 1 4 2 3
   DTS: 1 2 3 4
الدفق: IPBB

كقاعدة ، تختلف PTS و DTS فقط عندما يحتوي الدفق الجاري تشغيله على إطارات B.

عندما نتلقى حزمة من av_read_frame () ، فإنها تحتوي على قيم PTS و DTS للمعلومات الموجودة داخل الحزمة. ولكن ما نحتاجه حقًا هو PTS لإطارنا الخام الذي تم فك تشفيره حديثًا ، وفي هذه الحالة نعرف متى يجب عرضه.

لحسن الحظ ، يوفر لنا FFmpeg "أفضل طابع زمني ممكن" يمكننا الحصول عليه باستخدام وظيفة av_frame_get_best_effort_timestamp ().

التزامن


من أجل عرض الإطارات بدورها ، سيكون من الجيد معرفة متى يتم عرض إطار فيديو معين. ولكن كيف نفعل ذلك بالضبط؟ الفكرة هي: بعد أن نعرض الإطار ، نكتشف متى يجب إظهار الإطار التالي. ثم توقف فقط ، وبعد ذلك نقوم بتحديث الفيديو بعد هذه الفترة الزمنية. كما هو متوقع ، نتحقق من قيمة PTS للإطار التالي على ساعة النظام لنرى كم يجب أن يكون وقت الانتظار. يعمل هذا النهج ، ولكن هناك مشكلتان يجب معالجتهما.

أولاً ، السؤال هو ، متى ستكون PTS التالية؟ ستقول أنه يمكنك ببساطة إضافة تردد الفيديو إلى PTS الحالي - وستكون على صواب من حيث المبدأ. ومع ذلك ، ستتطلب بعض أنواع الفيديو إطارات متكررة. هذا يعني أنه يجب عليك تكرار الإطار الحالي لعدد معين من المرات. قد يؤدي ذلك إلى عرض البرنامج للإطار التالي في وقت قريب جدًا. يجب أن يؤخذ هذا في الاعتبار.

المشكلة الثانية هي أنه في البرنامج الذي كتبناه في الوقت الحالي ، يندفع الفيديو والصوت بفرح إلى الأمام حتى يزعجوا التزامن على الإطلاق. لا داعي للقلق حيال ذلك إذا عمل كل شيء في حد ذاته بشكل مثالي. لكن جهاز الكمبيوتر الخاص بك ليس مثاليًا ، مثل العديد من ملفات الفيديو. وبالتالي ، لدينا ثلاثة خيارات: مزامنة الصوت مع الفيديو ، ومزامنة الفيديو مع الصوت أو مزامنة كل من الصوت والفيديو مع ساعة خارجية (على سبيل المثال ، مع جهاز الكمبيوتر الخاص بك). الآن سنقوم بمزامنة الفيديو مع الصوت.

التشفير: استقبال إطار PTS


الآن دعنا نكتب شيئًا مباشرةً. نحتاج إلى إضافة بضعة أجزاء أخرى إلى هيكلنا الكبير ، وسنفعل ذلك بالطريقة التي نحتاجها. أولاً ، دعنا نلقي نظرة على سلسلة الفيديو الخاصة بنا. هل تتذكر أننا نجمع هنا الحزم التي تم وضعها في قائمة انتظار من قبل دفق فك التشفير الخاص بنا؟ في هذا الجزء من الشفرة ، نحتاج إلى الحصول على PTS للإطار الذي قدمه لنا avcodec_decode_video2 . الطريقة الأولى التي تحدثنا عنها هي الحصول على DTS لآخر حزمة تمت معالجتها ، وهي بسيطة جدًا:

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

نضع PTS على الصفر إذا لم نتمكن من تحديد قيمته.

حسنا ، كان ذلك سهلا. ملاحظة فنية: كما ترى ، نستخدم int64 لـ PTS. هذا لأنه يتم تخزين PTS كعدد صحيح. هذه القيمة هي طابع زمني يتوافق مع البعد الزمني في timebb . على سبيل المثال ، إذا كان الدفق يحتوي على 24 إطارًا في الثانية ، فستشير PTS من 42 إلى أنه يجب استخدام الإطار في المكان الذي يجب أن يكون فيه الإطار 42 ، بشرط أن يكون لدينا إطارات مستبدلة كل 1/24 ثانية (بالطبع ، لن يكون هذا بالضرورة كذلك في الحقيقة).

يمكننا تحويل هذه القيمة إلى ثوانٍ عن طريق القسمة على معدل الإطارات. قيمة قاعدة الوقتسيكون الدفق مساويًا 1 مقسومًا على معدل الإطارات (للمحتوى بمعدل إطارات ثابت) ، وبالتالي ، للحصول على PTS في ثوانٍ ، نضرب في time_base .

الكود كذلك: تزامن واستخدام PTS


حتى الآن لدينا كل PTS الجاهزة. الآن سوف نتعامل مع مشكلتي المزامنة هاتين ، التي تمت مناقشتها أعلى قليلاً. سنقوم بتحديد وظيفة Syncize_video التي ستقوم بتحديث PTS للمزامنة مع كل شيء. وأخيرًا ، ستتعامل هذه الوظيفة أيضًا مع الحالات التي لا نحصل فيها على قيمة PTS لإطارنا. في الوقت نفسه ، نحتاج إلى تتبع الموعد المتوقع للإطار التالي حتى نتمكن من تعيين معدل التحديث بشكل صحيح. يمكننا القيام بذلك باستخدام قيمة video_clock الداخلية ، التي تتعقب مقدار الوقت المنقضي للفيديو. نضيف هذه القيمة إلى هيكلنا الكبير:

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

فيما يلي وظيفة Syncize_video ، وهي واضحة جدًا:

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

كما ترى ، نأخذ في الاعتبار الإطارات المتكررة في هذه الوظيفة.

الآن ، دعنا نحصل على PTS الصحيح ونضع الإطار في قائمة الانتظار باستخدام queue_picture بإضافة وسيطة pts جديدة :

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

الشيء الوحيد الذي يتغير في queue_picture هو أننا نقوم بتخزين قيمة pts هذه في هيكل VideoPicture الذي ننتظره . وبالتالي ، يجب أن نضيف متغير pts إلى الهيكل ونضيف أسطر التعليمات البرمجية هذه:

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

الآن لدينا الصور في قائمة الانتظار مع قيم PTS الصحيحة ، لذلك دعونا نلقي نظرة على ميزة تحديث الفيديو لدينا. يمكنك أن تتذكر من الدرس الأخير أننا ببساطة زورناه وقمنا بتثبيت تحديث 80 مللي ثانية. حسنًا ، الآن سنكتشف ما يجب أن يكون هناك حقًا.

استراتيجيتنا هي التنبؤ بوقت PTS التالي ببساطة عن طريق قياس الوقت بين النقاط الحالية والسابقة. في الوقت نفسه ، نحتاج إلى مزامنة الفيديو مع الصوت. سنقوم بعمل ساعة صوتية.: قيمة داخلية تتعقب موضع الصوت الذي يتم تشغيله. إنه مثل قراءة رقمية على أي مشغل mp3. نظرًا لأننا نقوم بمزامنة الفيديو مع الصوت ، فإن دفق الفيديو يستخدم هذه القيمة لمعرفة ما إذا كان متقدمًا جدًا أو متأخرًا جدًا.

سنعود إلى التنفيذ في وقت لاحق ؛ لنفترض الآن أن لدينا وظيفة get_audio_clockمما يمنحنا الوقت على الساعة الصوتية. بمجرد حصولنا على هذه القيمة ، ما الذي يجب القيام به إذا لم تتم مزامنة الفيديو والصوت؟ سيكون من الحماقة محاولة القفز إلى الحزمة الصحيحة من خلال البحث أو أي شيء آخر. بدلاً من ذلك ، نقوم ببساطة بتعديل القيمة التي قمنا بحسابها للتحديث التالي: إذا كان PTS بعيدًا جدًا عن وقت الصوت ، فإننا نضاعف فترة التأخير المقدرة. إذا كان PTS متقدمًا جدًا على وقت اللعب ، فنحن نقوم بالتحديث بأسرع وقت ممكن. الآن بعد أن أصبح لدينا التحديث المكون أو وقت التأخير ، سنقوم بمقارنته مع ساعة جهاز الكمبيوتر الخاص بنا ، تاركين frame_timer قيد التشغيل . يلخص مؤقت الإطار هذا جميع حالات التأخير المقدرة أثناء تشغيل الفيلم. وبعبارة أخرى ، هذا الإطار- هذا هو الوقت الذي يشير إلى وقت عرض الإطار التالي. نضيف ببساطة تأخيرًا جديدًا إلى مؤقت الإطار ، ونقارنه بالوقت على ساعة جهاز الكمبيوتر الخاص بنا ، ونستخدم هذه القيمة لتخطيط التحديث التالي. قد يكون هذا مربكًا بعض الشيء ، لذا اقرأ التعليمات البرمجية بعناية:

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

نجري عدة فحوصات: أولاً ، نتأكد من أن التأخير بين PTS الحالي و PTS السابق منطقي. إذا لم تكن هناك حاجة إلى تأخير ، فقد تزامن الصوت والفيديو فقط في هذه المرحلة واستخدم التأخير الأخير فقط. ثم نتأكد من استيفاء حد التزامن ، لأن التزامن المثالي لا يحدث أبدًا. يستخدم FFplay قيمة 0.01 للعتبة. نتأكد أيضًا من أن عتبة المزامنة لا تقل أبدًا عن الفترات الفاصلة بين قيم PTS. أخيرًا ، قم بتعيين الحد الأدنى لقيمة التحديث على 10 مللي ثانية (في الواقع ، يبدو أنه يجب عليهم تخطي الإطار هنا ، ولكن لا داعي للقلق بشأن ذلك).

أضفنا مجموعة من المتغيرات إلى الهيكل الكبير ، لذلك لا تنس التحقق من الرمز. ولا تنس أيضًا تهيئة موقت الإطار والتأخير الأولي للإطار السابق في stream_component_open :

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

المزامنة: ساعة صوتية


حان الوقت لإدراك الساعة الصوتية. يمكننا تحديث الوقت في دالة audio_decode_frame ، حيث نقوم بفك تشفير الصوت. تذكر الآن أننا لا نقوم دائمًا بمعالجة حزمة جديدة في كل مرة نطلق عليها هذه الوظيفة ، لذلك هناك مجالان تحتاجان فيه إلى تحديث الساعة. المقام الأول هو المكان الذي نحصل فيه على الحزمة الجديدة: فقط قم بتثبيت ساعة الصوت على حزمة PTS. بعد ذلك ، إذا كانت الحزمة تحتوي على عدة إطارات ، فإننا نحفظ وقت تشغيل الصوت عن طريق حساب عدد العينات وضربها في تردد أخذ عينات معين في الثانية. لذلك ، عندما يكون لدينا الحزمة:

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

وبمجرد معالجة الحزمة:

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

بعض الفروق الطفيفة: تم تغيير قالب الوظيفة ويتضمن الآن pts_ptr ، لذا تأكد من تغييره. pts_ptr هو المؤشر الذي نستخدمه لإعلام audio_callback بحزمة الصوت pts . سيتم استخدام هذا في المرة القادمة لمزامنة الصوت مع الفيديو.

الآن يمكننا تنفيذ وظيفة get_audio_clock أخيرًا . انها ليست بهذه البساطة الحصول على قيمة و -> audio_clock ، إذا كنت تفكر في ذلك. يرجى ملاحظة أننا نقوم بتعيين صوت PTS في كل مرة نقوم بمعالجتها ، ولكن إذا نظرت إلى وظيفة audio_callback، سيستغرق نقل جميع البيانات من الحزمة الصوتية إلى المخزن المؤقت للإخراج بعض الوقت. هذا يعني أن القيمة في ساعة الصوت لدينا قد تكون بعيدة جدًا. لذلك ، نحتاج إلى التحقق من مقدار ما يتعين علينا كتابته. هنا هو الرمز الكامل:

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

يجب أن تفهم الآن سبب عمل هذه الوظيفة ؛)

هذا كل شيء! نقوم بتجميع:

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

لقد حدث! يمكنك مشاهدة الفيلم على لاعب عصامي. في الدرس التالي ، سنلقي نظرة على مزامنة الصوت ، ثم سنتعرف على كيفية البحث.

دليل FFmpeg و SDL أو كيفية كتابة مشغل فيديو في أقل من 1000 سطر - الجزء 2



ترجمات مدونة اديسون:


All Articles