FFmpeg دليل ليباف


بحثت طويلًا عن كتاب يمكن مضغه لاستخدام مكتبة تشبه FFmpeg تُعرف باسم libav (يشير الاسم إلى lib rary a udio v ideo ). العثور على كتاب مدرسي " كيفية كتابة مشغل فيديو وملاءمته في أقل من ألف سطر ". لسوء الحظ ، فإن المعلومات الموجودة هناك قديمة ، لذلك كان علي إنشاء دليل بمفردي.

ستكون معظم التعليمات البرمجية باللغة C ، ولكن لا تقلق: ستفهم بسهولة كل شيء ويمكنك تطبيقه بلغتك المفضلة. لدى FFmpeg libav الكثير من الارتباطات بالعديد من اللغات (بما في ذلك Python و Go). ولكن حتى إذا لم تكن لغتك متوافقة بشكل مباشر ، فلا يزال بإمكانك الحصول على إرفاق عبر ffi (إليك مثال معلوا ).

لنبدأ باستطراد قصير عن ماهية الفيديو والصوت وبرامج الترميز والحاويات. ثم ننتقل إلى دورة التعطل باستخدام سطر الأوامر FFmpeg ، وأخيرًا نكتب الرمز. لا تتردد في الذهاب مباشرة إلى قسم "The Thorny Path to Learning FFmpeg libav".

هناك رأي (وليس رأيي فقط) بأن بث الفيديو عبر الإنترنت قد أخذ العصا من التلفزيون التقليدي. كن على هذا النحو ، فإن FFmpeg libav يستحق بالتأكيد استكشافه.

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


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

, , .

! ;-)


— , !


إذا تم تغيير تسلسل الصور بتردد معين (على سبيل المثال ، 24 صورة في الثانية) ، يتم إنشاء وهم الحركة. هذه هي الفكرة الرئيسية للفيديو: سلسلة من الصور (الإطارات) تتحرك بسرعة معينة.

1886 توضيح.

الصوت هو ما تسمعه!


على الرغم من أن الفيديو الصامت يمكن أن يسبب مجموعة متنوعة من المشاعر ، إلا أن إضافة الصوت يزيد بشكل كبير من درجة المتعة.

الصوت هو موجات اهتزازية تنتشر في الهواء أو في أي وسيط إرسال آخر (مثل الغاز أو السائل أو الصلب).

في نظام الصوت الرقمي ، يحول الميكروفون الصوت إلى إشارة كهربائية تناظرية. بعد ذلك ، يقوم المحول التناظري إلى الرقمي ( ADC ) - عادة باستخدام تعديل شفرة النبض ( PCM ) - بتحويل إشارة تناظرية إلى إشارة رقمية.


برنامج الترميز - ضغط البيانات


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

ولكن إذا قررنا حزم ملايين الصور في ملف واحد وسميناها فيلمًا ، فيمكننا الحصول على ملف ضخم. دعونا نحسب:

لنفترض أننا أنشأنا فيديو بدقة 1080 × 1920 (ارتفاع × عرض). نحن ننفق 3 بايت لكل بكسل (الحد الأدنى للنقطة على الشاشة) لترميز الألوان (ألوان 24 بت ، مما يمنحنا 16777216 لونًا مختلفًا). يعمل هذا الفيديو بسرعة 24 لقطة في الثانية ، ومدتها 30 دقيقة.

toppf = 1080 * 1920 //    
cpp = 3 //  
tis = 30 * 60 //   
fps = 24 //   

required_storage = tis * fps * toppf * cpp

سيتطلب هذا الفيديو حوالي 250.28 جيجا بايت من الذاكرة ، أو 1.11 جيجابايت / ثانية! لهذا السبب يجب عليك استخدام برنامج ترميز.

الحاوية هي طريقة ملائمة لتخزين الصوت / الفيديو


تنسيق الحاوية (المجمع) هو تنسيق ملف تعريف تصف مواصفاته كيف تتعايش عناصر البيانات والبيانات الوصفية المختلفة في ملف كمبيوتر.

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

بشكل عام ، يتم تحديد تنسيق الملف من خلال امتداده: على سبيل المثال ، video.webm هو الفيديو على الأرجح باستخدام حاوية webm.


سطر الأوامر FFmpeg


حل مستقل عبر منصة لتسجيل وتحويل وتدفق الصوت / الفيديو.

للعمل مع الوسائط المتعددة ، لدينا أداة رائعة - مكتبة تسمى FFmpeg . حتى إذا لم تستخدمه في رمز البرنامج الخاص بك ، فما زلت تستخدمه (هل تستخدم Chrome؟).

تحتوي المكتبة على برنامج وحدة تحكم لإدخال سطر أوامر يسمى ffmpeg (بأحرف صغيرة ، على عكس اسم المكتبة نفسها). هذا هو ثنائي بسيط وقوي. على سبيل المثال ، يمكنك التحويل من mp4 إلى avi ببساطة عن طريق كتابة هذا الأمر:

$ ffmpeg -i input.mp4 output.avi

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

أداة سطر الأوامر FFmpeg 101


لدى FFmpeg وثائق حيث يتم شرح كل شيء تمامًا كيف يعمل.

من الناحية التخطيطية ، يتوقع برنامج سطر الأوامر FFmpeg أن يقوم تنسيق الوسيطة التالي بعمله - ffmpeg {1} {2} -i {3} {4} {5}حيث:

{1} - المعلمات العامة
{2} - معلمات ملف الإدخال
{3} - عنوان URL الوارد
{4} - معلمات ملف الإخراج
{5} - الصادر

تحدد أجزاء عنوان URL {2} و {3} و {4} و {5} العديد من الوسائط حسب الحاجة. من الأسهل فهم تنسيق وسيطات التمرير باستخدام مثال:

تحذير: الملف حسب المرجع يزن 300 ميغابايت

$ wget -O bunny_1080p_60fps.mp4 http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4

$ ffmpeg \
-y \ #  
-c: libfdk_aac -c: v libx264 \ #  
-i bunny_1080p_60fps.mp4 \ #  URL
-c: v libvpx-vp9 -c: libvorbis \ #  
bunny_1080p_60fps_vp9.webm #  URL

يأخذ هذا الأمر ملف mp4 وارد يحتوي على دفقين (ترميز الصوت باستخدام ترميز aac ، وترميز الفيديو باستخدام ترميز h264) ، وتحويله إلى webm ، وتغيير أيضًا ترميز الصوت والفيديو.

إذا قمت بتبسيط الأمر أعلاه ، يجب مراعاة أن FFmpeg سيقبل القيم الافتراضية بدلاً منك. على سبيل المثال ، إذا كتبت ببساطة

ffmpeg -i input.avi output.mp4

ما هو برنامج ترميز الصوت / الفيديو الذي يستخدمه لإنشاء output.mp4؟

كتب Werner Robitz دليل الترميز والتحرير للقراءة / التنفيذ مع FFmpeg.

عمليات الفيديو الأساسية


عند العمل مع الصوت / الفيديو ، نقوم عادةً بتنفيذ عدد من المهام المتعلقة بالوسائط المتعددة.

تحويل الشفرة (تحويل الشفرة)




ما هذا؟ عملية تحويل الدفق أو الصوت أو الفيديو (أو كليهما في نفس الوقت) من برنامج ترميز إلى آخر. لا يتغير تنسيق الملف (الحاوية).

على ماذا؟ يحدث أن بعض الأجهزة (أجهزة التلفزيون والهواتف الذكية ووحدات التحكم وما إلى ذلك) لا تدعم تنسيق الصوت / الفيديو X ، ولكنها تدعم تنسيق الصوت / الفيديو Y. أو يفضل استخدام برامج الترميز الأحدث لأنها توفر نسبة ضغط أفضل.

كيف؟ تحويل ، على سبيل المثال ، فيديو H264 (AVC) إلى H265 (HEVC):

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c:v libx265 \
bunny_1080p_60fps_h265.mp4

Transmultiplexing



ما هذا؟ تحويل من تنسيق (حاوية) إلى آخر.

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

كيف؟ تحويل mp4 إلى webm:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-c copy \ # just saying to ffmpeg to skip encoding
bunny_1080p_60fps.webm

ترجمة



ما هذا؟ تغيير معدل البيانات أو إنشاء طريقة عرض أخرى.

على ماذا؟ يمكن للمستخدم مشاهدة الفيديو الخاص بك على شبكة 2G على هاتف ذكي منخفض الطاقة وعبر اتصال الألياف الضوئية بالإنترنت على تلفزيون 4K. لذلك ، يجب أن تقدم أكثر من خيار لتشغيل نفس الفيديو بمعدلات بيانات مختلفة.

كيف؟ ينتج التشغيل بمعدل بت بين 3856K و 2000K.

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-minrate 964K -maxrate 3856K -bufsize 2000K \
bunny_1080p_60fps_transrating_964_3856.mp4

عادة ، يتم إجراء الترجمة بالاقتران مع إعادة المعايرة. كتب Werner Robitz مقالًا إلزاميًا آخر حول التحكم في السرعة FFmpeg.

التحويل (إعادة المعايرة)



ما هذا؟ تغيير القرار. كما ذكر أعلاه ، غالبًا ما يتم إجراء التحويل في وقت واحد مع transrating.

على ماذا؟ لنفس الأسباب كما هو الحال مع transrating.

كيف؟ تقليل الدقة من 1080 إلى 480:

$ ffmpeg \
-i bunny_1080p_60fps.mp4 \
-vf scale=480:-1 \
bunny_1080p_60fps_transsizing_480.mp4

المكافأة: التدفق التكيفي



ما هذا؟ إنشاء العديد من الأذونات (معدلات البت) وتقسيم الوسائط إلى أجزاء وإرسالها عبر بروتوكول http.

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

كيف؟ إنشاء WebM سريع الاستجابة باستخدام DASH:

# video streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 160x90 -b:v 250k -keyint_min 150 -g 150 -an -f webm -dash 1 video_160x90_250k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 320x180 -b:v 500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_320x180_500k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 750k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_750k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 640x360 -b:v 1000k -keyint_min 150 -g 150 -an -f webm -dash 1 video_640x360_1000k.webm

$ ffmpeg -i bunny_1080p_60fps.mp4 -c:v libvpx-vp9 -s 1280x720 -b:v 1500k -keyint_min 150 -g 150 -an -f webm -dash 1 video_1280x720_1500k.webm

# audio streams
$ ffmpeg -i bunny_1080p_60fps.mp4 -c:a libvorbis -b:a 128k -vn -f webm -dash 1 audio_128k.webm

# the DASH manifest
$ ffmpeg \
 -f webm_dash_manifest -i video_160x90_250k.webm \
 -f webm_dash_manifest -i video_320x180_500k.webm \
 -f webm_dash_manifest -i video_640x360_750k.webm \
 -f webm_dash_manifest -i video_640x360_1000k.webm \
 -f webm_dash_manifest -i video_1280x720_500k.webm \
 -f webm_dash_manifest -i audio_128k.webm \
 -c copy -map 0 -map 1 -map 2 -map 3 -map 4 -map 5 \
 -f webm_dash_manifest \
 -adaptation_sets "id=0,streams=0,1,2,3,4 id=1,streams=5" \
 manifest.mpd

ملاحظة: لقد سحبت هذا المثال من تعليمات تشغيل Adapmive WebM باستخدام DASH .

تجاوز


لا توجد استخدامات أخرى لـ FFmpeg. أستخدمه مع iMovie لإنشاء / تحرير بعض مقاطع فيديو YouTube. وبالطبع ، لا شيء يمنعك من استخدامه بشكل احترافي.

المسار الشائك للتعلم FFmpeg libav

أليس من المدهش من وقت لآخر أن ينظر إليه من خلال السمع والبصر؟

عالم الأحياء ديفيد روبرت جونز
FFmpeg مفيد للغاية كأداة سطر أوامر لأداء العمليات الهامة مع ملفات الوسائط المتعددة. ربما يمكن استخدامه في البرامج أيضًا؟

يتكون FFmpeg من العديد من المكتبات التي يمكن دمجها في برامجنا الخاصة. عادة ، عند تثبيت FFmpeg ، يتم تثبيت كل هذه المكتبات تلقائيًا. سأشير إلى مجموعة من هذه المكتبات باسم FFmpeg libav .

عنوان القسم هو تكريم لسلسلة زيد شو مسار التعلم الشائك [...] ، ولا سيما كتابه مسار الشائكة للتعلم ج.

الفصل 0 - عالم الترحيب البسيط


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

FFmpeg العمارة libav


ولكن قبل أن نبدأ في كتابة الشفرة ، دعنا نرى كيف تعمل بنية FFmpeg libav بشكل عام وكيف تتفاعل مكوناتها مع الآخرين.

فيما يلي رسم تخطيطي لعملية فك تشفير الفيديو:

أولاً ، يتم تحميل ملف الوسائط في مكون يسمى AVFormatContext (حاوية الفيديو هي أيضًا تنسيق). في الواقع ، لا يقوم بتنزيل الملف بالكامل: غالبًا ما تتم قراءة الرأس فقط.

بمجرد تنزيل الحد الأدنى لرأس الحاوية الخاصة بنا ، يمكنك الوصول إلى تدفقاتها (يمكن تمثيلها كبيانات صوتية وفيديو أولية). سيتوفر كل دفق في مكون AVStream .

افترض أن الفيديو الخاص بنا يحتوي على دفقين: ترميز الصوت باستخدام ترميز AAC ، وترميز الفيديو باستخدام ترميز H264 ( AVC ). من كل تيار يمكننا استخراج أجزاء من البيانات تسمى الحزمالتي يتم تحميلها في مكونات تسمى AVPacket .

البيانات داخل الحزم لا تزال مشفرة (مضغوطة) ، وللفك شفرة الحزم نحتاج لتمريرها إلى AVCodec محددة . يقوم

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

المتطلبات


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

في الواقع ، رمز


TLDR أرني مثالاً على التعليمات البرمجية القابلة للتنفيذ ، إخوانه:

$ make run_hello

سنحذف بعض التفاصيل ، لكن لا تقلق: شفرة المصدر متاحة على github.

سنقوم بتخصيص ذاكرة لمكون AVFormatContext ، والذي سيحتوي على معلومات حول التنسيق (الحاوية).

AVFormatContext *pFormatContext = avformat_alloc_context();

سنقوم الآن بفتح الملف ، وقراءة عنوانه وملء AVFormatContext بأقل قدر من المعلومات عن التنسيق (لاحظ أن برامج الترميز عادة لا تفتح). للقيام بذلك ، استخدم الدالة avformat_open_input . يتوقع AVFormatContext ، واسم ملف ، ووسيطتين اختياريتين : AVInputFormat (إذا قمت بتمرير NULL ، سيحدد FFmpeg التنسيق) و AVDictionary (وهي خيارات demultiplexer).

avformat_open_input(&pFormatContext, filename, NULL, NULL);

يمكنك أيضًا طباعة اسم التنسيق ومدة الوسائط:

printf("Format %s, duration %lld us", pFormatContext->iformat->long_name, pFormatContext->duration);

للوصول إلى التدفقات ، نحتاج إلى قراءة البيانات من الوسائط. يتم ذلك عن طريق وظيفة avformat_find_stream_info . الآن pFormatContext-> nb_streams سوف تحتوي على عدد من المواضيع، و pFormatContext-> تيارات [أنا] سيعطينا ط ال تدفق على التوالي ( AVStream ).

avformat_find_stream_info(pFormatContext,  NULL);

دعنا نذهب من خلال الحلقة في جميع المواضيع:

for(int i = 0; i < pFormatContext->nb_streams; i++) {
  //
}

لكل تيار، ونحن نذهب لإنقاذ AVCodecParameters ، الذي يصف خصائص الترميز التي يستخدمها أولا: تيار ال:

AVCodecParameters *pLocalCodecParameters = pFormatContext->streams[i]->codecpar;


باستخدام خصائص برامج الترميز ، يمكننا العثور على الأداة المقابلة عن طريق طلب وظيفة avcodec_find_decoder ، كما يمكننا العثور على وحدة فك الترميز المسجلة لمعرف برنامج الترميز وإرجاع AVCodec ، وهو مكون يعرف كيفية ترميز الدفق وفك ترميزه:

AVCodec *pLocalCodec = avcodec_find_decoder(pLocalCodecParameters->codec_id);

الآن يمكننا طباعة معلومات الترميز:

// specific for video and audio
if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
  printf("Video Codec: resolution %d x %d", pLocalCodecParameters->width, pLocalCodecParameters->height);
} else if (pLocalCodecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
  printf("Audio Codec: %d channels, sample rate %d", pLocalCodecParameters->channels, pLocalCodecParameters->sample_rate);
}
// general
printf("\tCodec %s ID %d bit_rate %lld", pLocalCodec->long_name, pLocalCodec->id, pCodecParameters->bit_rate);

باستخدام برنامج الترميز ، نقوم بتخصيص ذاكرة لـ AVCodecContext ، والتي ستحتوي على سياق عملية فك التشفير / التشفير. ولكنك تحتاج بعد ذلك إلى ملء سياق برنامج الترميز هذا بمعلمات برنامج الترميز - نقوم بذلك باستخدام avcodec_parameters_to_context .

بعد ملء سياق برنامج الترميز ، تحتاج إلى فتح برنامج الترميز. نسمي وظيفة avcodec_open2 ثم يمكننا استخدامها:

AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters);
avcodec_open2(pCodecContext, pCodec, NULL);

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

AVPacket *pPacket = av_packet_alloc();
AVFrame *pFrame = av_frame_alloc();

دعونا نطعم حزمنا من تيارات وظيفة av_read_frame بينما تحتوي على الحزم:

while(av_read_frame(pFormatContext, pPacket) >= 0) {
  //...
}

الآن سنرسل حزمة البيانات الأولية (إطار مضغوط) إلى وحدة فك الترميز من خلال سياق برنامج الترميز باستخدام وظيفة avcodec_send_packet :

avcodec_send_packet(pCodecContext, pPacket);

ودعنا نحصل على إطار من البيانات الأولية (إطار غير مضغوط) من وحدة فك الترميز من خلال سياق برنامج الترميز نفسه باستخدام دالة avcodec_receive_frame :

avcodec_receive_frame(pCodecContext, pFrame);

يمكننا طباعة رقم الإطار ، PTS ، DTS ، نوع الإطار ، إلخ:

printf(
    "Frame %c (%d) pts %d dts %d key_frame %d [coded_picture_number %d, display_picture_number %d]",
    av_get_picture_type_char(pFrame->pict_type),
    pCodecContext->frame_number,
    pFrame->pts,
    pFrame->pkt_dts,
    pFrame->key_frame,
    pFrame->coded_picture_number,
    pFrame->display_picture_number
);

وأخيرًا ، يمكننا حفظ إطارنا الذي تم فك ترميزه في صورة رمادية بسيطة. العملية بسيطة للغاية: سنستخدم بيانات pFrame-> ، حيث يرتبط الفهرس بمسافات الألوان Y و Cb و Cr . ما عليك سوى اختيار 0 (Y) لحفظ صورتنا الرمادية:

save_gray_frame(pFrame->data[0], pFrame->linesize[0], pFrame->width, pFrame->height, frame_filename);

static void save_gray_frame(unsigned char *buf, int wrap, int xsize, int ysize, char *filename)
{
    FILE *f;
    int i;
    f = fopen(filename,"w");
    // writing the minimal required header for a pgm file format
    // portable graymap format -> https://en.wikipedia.org/wiki/Netpbm_format#PGM_example
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);

    // writing line by line
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

وفويلا! الآن لدينا صورة 2 ميجا بايت ذات تدرج رمادي:


الفصل 1 - مزامنة الصوت والفيديو

التواجد في اللعبة هو عندما يكتب مطور JS صغير مشغل فيديو MSE جديد.
قبل أن نبدأ في كتابة كود التحويل ، دعنا نتحدث عن المزامنة أو كيف يكتشف مشغل الفيديو الوقت المناسب لتشغيل الإطار.

في المثال السابق ، قمنا بحفظ عدة إطارات:


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

لذلك ، نحتاج إلى تحديد بعض المنطق للتشغيل السلس لكل إطار. في هذا الصدد ، لكل إطار علامة تمثيل زمني ( PTS - من p resentation t ime s tamp) ، وهو رقم متزايد يؤخذ في الاعتبار في المتغيرtimebase ، وهو رقم منطقي (حيث يُعرف المقام بالمقياس الزمني - مقياس الوقت ) مقسومًا على معدل الإطارات ( fps ).

من السهل الفهم بالأمثلة. دعونا محاكاة بعض السيناريوهات.

ل إطارا في الثانية = 60/1 و القاعدة الزمنية = 1/60000، كل PTS سيزيد الزمني / إطارا في الثانية = 1000 ، وبالتالي فإن الحقيقي PTS الوقت لكل إطار يمكن (شريطة أن يبدأ في 0):

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1000, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2000, PTS_TIME = PTS * timebase = 0.033

تقريبا نفس السيناريو ، ولكن مع مقياس زمني يساوي 1/60:

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 1, PTS_TIME = PTS * timebase = 0.016
frame=2, PTS = 2, PTS_TIME = PTS * timebase = 0.033
frame=3, PTS = 3, PTS_TIME = PTS * timebase = 0.050

بالنسبة إلى fps = 25/1 و timebase = 1/75 ، ستزيد كل PTS مقياس الوقت / fps = 3 ، ويمكن أن يكون وقت PTS :

frame=0, PTS = 0, PTS_TIME = 0
frame=1, PTS = 3, PTS_TIME = PTS * timebase = 0.04
frame=2, PTS = 6, PTS_TIME = PTS * timebase = 0.08
frame=3, PTS = 9, PTS_TIME = PTS * timebase = 0.12
...
frame=24, PTS = 72, PTS_TIME = PTS * timebase = 0.96
...
frame=4064, PTS = 12192, PTS_TIME = PTS * timebase = 162.56

الآن مع pts_time يمكننا إيجاد طريقة لتصور هذا بالتزامن مع صوت pts_time أو مع ساعة النظام. يوفر FFmpeg libav هذه المعلومات من خلال API الخاص به:

fps = AVStream->avg_frame_rate
tbr = AVStream->r_frame_rate
tbn = AVStream->time_base


فقط من باب الفضول ، تم إرسال الإطارات التي حفظناها بترتيب DTS (الإطارات: 1 ، 6 ، 4 ، 2 ، 3 ، 5) ، ولكن تم نسخها بترتيب PTS (الإطارات: 1 ، 2 ، 3 ، 4 ، 5). لاحظ أيضًا مقدار الإطارات B الأرخص مقارنةً بالإطارات P أو I :

LOG: AVStream->r_frame_rate 60/1
LOG: AVStream->time_base 1/60000
...
LOG: Frame 1 (type=I, size=153797 bytes) pts 6000 key_frame 1 [DTS 0]
LOG: Frame 2 (type=B, size=8117 bytes) pts 7000 key_frame 0 [DTS 3]
LOG: Frame 3 (type=B, size=8226 bytes) pts 8000 key_frame 0 [DTS 4]
LOG: Frame 4 (type=B, size=17699 bytes) pts 9000 key_frame 0 [DTS 2]
LOG: Frame 5 (type=B, size=6253 bytes) pts 10000 key_frame 0 [DTS 5]
LOG: Frame 6 (type=P, size=34992 bytes) pts 11000 key_frame 0 [DTS 1]

الفصل 2 - إعادة تعدد


إعادة الإرسال (إعادة الترتيب ، إعادة التحويل) - الانتقال من تنسيق (حاوية) إلى آخر. على سبيل المثال ، يمكننا بسهولة استبدال فيديو MPEG-4 بـ MPEG-TS باستخدام FFmpeg:

ffmpeg input.mp4 -c copy output.ts

سيتم إلغاء تعدد ملفات ملف MP4 ، بينما لن يتم فك تشفير الملف أو تشفيره ( نسخة -c ) ، وفي النهاية ، نحصل على ملف mpegts. إذا لم تحدد التنسيق -f ، فسيحاول ffmpeg تخمينه بناءً على امتداد الملف.

يتبع الاستخدام العام لـ FFmpeg أو libav مثل هذا النمط / البنية أو سير العمل:

  • مستوى البروتوكول - قبول بيانات الإدخال (على سبيل المثال ، ملف ، ولكن يمكن أن يكون أيضًا rtmp أو تنزيل HTTP)
  • — , , ,
  • — (, ),
  • … :
  • — ( )
  • — ( ) ( )
  • — , , ( , , )


(هذا الرسم البياني مستوحى بشدة من عمل Leixiaohua و Slhck )

الآن دعنا ننشئ مثالًا باستخدام libav لتوفير نفس التأثير عند تنفيذ هذا الأمر:

ffmpeg input.mp4 -c copy output.ts

سنقوم بقراءة من الإدخال ( input_format_context ) وتغييره إلى إخراج آخر ( output_format_context ):

AVFormatContext *input_format_context = NULL;
AVFormatContext *output_format_context = NULL;

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

if ((ret = avformat_open_input(&input_format_context, in_filename, NULL, NULL)) < 0) {
  fprintf(stderr, "Could not open input file '%s'", in_filename);
  goto end;
}
if ((ret = avformat_find_stream_info(input_format_context, NULL)) < 0) {
  fprintf(stderr, "Failed to retrieve input stream information");
  goto end;
}

avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
if (!output_format_context) {
  fprintf(stderr, "Could not create output context\n");
  ret = AVERROR_UNKNOWN;
  goto end;
}

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

number_of_streams = input_format_context->nb_streams;
streams_list = av_mallocz_array(number_of_streams, sizeof(*streams_list));

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

for (i = 0; i < input_format_context->nb_streams; i++) {
  AVStream *out_stream;
  AVStream *in_stream = input_format_context->streams[i];
  AVCodecParameters *in_codecpar = in_stream->codecpar;
  if (in_codecpar->codec_type != AVMEDIA_TYPE_AUDIO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_VIDEO &&
      in_codecpar->codec_type != AVMEDIA_TYPE_SUBTITLE) {
    streams_list[i] = -1;
    continue;
  }
  streams_list[i] = stream_index++;
  out_stream = avformat_new_stream(output_format_context, NULL);
  if (!out_stream) {
    fprintf(stderr, "Failed allocating output stream\n");
    ret = AVERROR_UNKNOWN;
    goto end;
  }
  ret = avcodec_parameters_copy(out_stream->codecpar, in_codecpar);
  if (ret < 0) {
    fprintf(stderr, "Failed to copy codec parameters\n");
    goto end;
  }
}

الآن قم بإنشاء ملف الإخراج:

if (!(output_format_context->oformat->flags & AVFMT_NOFILE)) {
  ret = avio_open(&output_format_context->pb, out_filename, AVIO_FLAG_WRITE);
  if (ret < 0) {
    fprintf(stderr, "Could not open output file '%s'", out_filename);
    goto end;
  }
}

ret = avformat_write_header(output_format_context, NULL);
if (ret < 0) {
  fprintf(stderr, "Error occurred when opening output file\n");
  goto end;
}

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

while (1) {
  AVStream *in_stream, *out_stream;
  ret = av_read_frame(input_format_context, &packet);
  if (ret < 0)
    break;
  in_stream  = input_format_context->streams[packet.stream_index];
  if (packet.stream_index >= number_of_streams || streams_list[packet.stream_index] < 0) {
    av_packet_unref(&packet);
    continue;
  }
  packet.stream_index = streams_list[packet.stream_index];
  out_stream = output_format_context->streams[packet.stream_index];
  /* copy packet */
  packet.pts = av_rescale_q_rnd(packet.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.dts = av_rescale_q_rnd(packet.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
  packet.duration = av_rescale_q(packet.duration, in_stream->time_base, out_stream->time_base);
  // https://ffmpeg.org/doxygen/trunk/structAVPacket.html#ab5793d8195cf4789dfb3913b7a693903
  packet.pos = -1;

  //https://ffmpeg.org/doxygen/trunk/group__lavf__encoding.html#ga37352ed2c63493c38219d935e71db6c1
  ret = av_interleaved_write_frame(output_format_context, &packet);
  if (ret < 0) {
    fprintf(stderr, "Error muxing packet\n");
    break;
  }
  av_packet_unref(&packet);
}

للإكمال ، نحتاج إلى كتابة مقطع دعائي إلى ملف وسائط الإخراج باستخدام وظيفة av_write_trailer :

av_write_trailer(output_format_context);

الآن نحن جاهزون لاختبار الكود. وسيكون الاختبار الأول تحويل تنسيق (حاوية الفيديو) من MP4 إلى ملف فيديو MPEG-TS. أساسا نقوم بإنشاء سطر الأوامر ffmpeg input.mp4 -c لنسخ output.ts باستخدام libav.

make run_remuxing_ts

إنها تعمل! لا تصدقني ؟! تحقق مع ffprobe :

ffprobe -i remuxed_small_bunny_1080p_60fps.ts

Input #0, mpegts, from 'remuxed_small_bunny_1080p_60fps.ts':
  Duration: 00:00:10.03, start: 0.000000, bitrate: 2751 kb/s
  Program 1
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 1920x1080 [SAR 1:1 DAR 16:9], 60 fps, 60 tbr, 90k tbn, 120 tbc
    Stream #0:1[0x101]: Audio: ac3 ([129][0][0][0] / 0x0081), 48000 Hz, 5.1(side), fltp, 320 kb/s

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


قبل أن ننتهي من هذا الفصل ، أود أن أعرض هذا الجزء المهم من عملية إعادة الإرسال المتعدد ، حيث يمكنك تمرير المعلمات إلى معدد الإرسال. افترض أنك تريد توفير تنسيق MPEG-DASH ، لذلك تحتاج إلى استخدام mp4 المجزأ (يسمى أحيانًا fmp4 ) بدلاً من MPEG-TS أو MPEG-4 العادي.

يعد استخدام سطر الأوامر أمرًا سهلاً:

ffmpeg -i non_fragmented.mp4 -movflags frag_keyframe+empty_moov+default_base_moof fragmented.mp4

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

AVDictionary* opts = NULL;
av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
ret = avformat_write_header(output_format_context, &opts);

يمكننا الآن إنشاء ملف mp4 المجزأ:

make run_remuxing_fragmented_mp4

للتأكد من أن كل شيء عادل ، يمكنك استخدام موقع أداة gpac / mp4box.js المذهل أو http://mp4parser.com/ لمعرفة الاختلافات - قم بتنزيل mp4 أولاً.

كما ترون ، يحتوي على كتلة mdat واحدة غير قابلة للتجزئة - هذا هو المكان الذي توجد فيه إطارات الفيديو والصوت. الآن قم بتنزيل MP4 المجزأ لترى كيف تمد كتل mdat:

الفصل 3 - تحويل الشفرة


TLDR أرني الرمز والتنفيذ:

$ make run_transcoding

سوف نتخطى بعض التفاصيل ، لكن لا تقلق: كود المصدر متاح على github.

في هذا الفصل، سوف نستحدث اشارات الحد الأدنى مكتوب في C، والتي يمكن تحويل ملفات الفيديو من H264 H265 لاستخدام مكتبات FFMPEG libav، ولا سيما يبافكوديك ، libavformat و libavutil .


AVFormatContext عبارة عن تجريد لتنسيق ملف الوسائط ، أي للحاوية (MKV ، MP4 ،
Webm ، TS) يمثل AVStream كل نوع بيانات لصيغة معينة (على سبيل المثال: الصوت والفيديو والترجمات والبيانات الوصفية)
AVPacket هو جزء من البيانات المضغوطة التي يتم تلقيها من AVStream والتي يمكن فك تشفيرها باستخدام AVCodec (على سبيل المثال : av1 ، h264 ، vp9 ، hevc) توليد بيانات أولية تسمى AVFrame .

Transmultiplexing


لنبدأ بتحويل بسيط ، ثم تحميل ملف الإدخال.

// Allocate an AVFormatContext
avfc = avformat_alloc_context();
// Open an input stream and read the header.
avformat_open_input(avfc, in_filename, NULL, NULL);
// Read packets of a media file to get stream information.
avformat_find_stream_info(avfc, NULL);

الآن تكوين وحدة فك الترميز. سيتيح لنا AVFormatContext الوصول إلى جميع مكونات AVStream ، ولكل منها يمكننا الحصول على AVCodec وإنشاء AVCodecContext محدد . وأخيرًا ، يمكننا فتح برنامج الترميز هذا للانتقال إلى عملية فك الترميز. يحتوي

AVCodecContext على بيانات تكوين الوسائط ، مثل معدل البيانات ، ومعدل الإطارات ، ومعدل العينة ، والقنوات ، والملعب ، وغيرها الكثير.

for(int i = 0; i < avfc->nb_streams; i++) {
  AVStream *avs = avfc->streams[i];
  AVCodec *avc = avcodec_find_decoder(avs->codecpar->codec_id);
  AVCodecContext *avcc = avcodec_alloc_context3(*avc);
  avcodec_parameters_to_context(*avcc, avs->codecpar);
  avcodec_open2(*avcc, *avc, NULL);
}

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

العلامة AV_CODEC_FLAG_GLOBAL_HEADER ، والتي تخبر برنامج التشفير أنه يمكنه استخدام رؤوس عامة ، وأخيرًا فتح ملف الإخراج لكتابة الرؤوس وحفظها:

avformat_alloc_output_context2(&encoder_avfc, NULL, NULL, out_filename);

AVStream *avs = avformat_new_stream(encoder_avfc, NULL);
avcodec_parameters_copy(avs->codecpar, decoder_avs->codecpar);

if (encoder_avfc->oformat->flags & AVFMT_GLOBALHEADER)
  encoder_avfc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

avio_open(&encoder_avfc->pb, encoder->filename, AVIO_FLAG_WRITE);
avformat_write_header(encoder->avfc, &muxer_opts);

نحصل على AVPacket من مفكك التشفير ، ونضبط الطوابع الزمنية ونكتب الحزمة بشكل صحيح في ملف الإخراج. على الرغم من حقيقة أن وظيفة av_interleaved_write_frame تشير إلى " إطار الكتابة " ، فإننا نحفظ الحزمة. نكمل عملية التبادل من خلال كتابة مقطع دعائي إلى ملف.

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while(av_read_frame(decoder_avfc, input_packet) >= 0) {
  av_packet_rescale_ts(input_packet, decoder_video_avs->time_base, encoder_video_avs->time_base);
  av_interleaved_write_frame(*avfc, input_packet) < 0));
}

av_write_trailer(encoder_avfc);

تحويل الشفرة


في القسم السابق ، كان هناك برنامج بسيط للتحويل ، الآن سنضيف القدرة على ترميز الملفات ، على وجه الخصوص ، تحويل الفيديو من h264 إلى h265 .

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

  • قم بإنشاء AVStream للفيديو في برنامج التشفير avformat_new_stream .
  • نستخدم AVCodec باسم libx265 ، avcodec_find_encoder_by_name .
  • قم بإنشاء AVCodecContext بناءً على برنامج ترميز avcodec_alloc_context3 الذي تم إنشاؤه .
  • تعيين السمات الأساسية لجلسة تحويل الشفرة و ...
  • ... افتح برنامج الترميز وانسخ المعلمات من السياق إلى الدفق ( avcodec_open2 و avcodec_parameters_from_context ).

AVRational input_framerate = av_guess_frame_rate(decoder_avfc, decoder_video_avs, NULL);
AVStream *video_avs = avformat_new_stream(encoder_avfc, NULL);

char *codec_name = "libx265";
char *codec_priv_key = "x265-params";
// we're going to use internal options for the x265
// it disables the scene change detection and fix then
// GOP on 60 frames.
char *codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

AVCodec *video_avc = avcodec_find_encoder_by_name(codec_name);
AVCodecContext *video_avcc = avcodec_alloc_context3(video_avc);
// encoder codec params
av_opt_set(sc->video_avcc->priv_data, codec_priv_key, codec_priv_value, 0);
video_avcc->height = decoder_ctx->height;
video_avcc->width = decoder_ctx->width;
video_avcc->pix_fmt = video_avc->pix_fmts[0];
// control rate
video_avcc->bit_rate = 2 * 1000 * 1000;
video_avcc->rc_buffer_size = 4 * 1000 * 1000;
video_avcc->rc_max_rate = 2 * 1000 * 1000;
video_avcc->rc_min_rate = 2.5 * 1000 * 1000;
// time base
video_avcc->time_base = av_inv_q(input_framerate);
video_avs->time_base = sc->video_avcc->time_base;

avcodec_open2(sc->video_avcc, sc->video_avc, NULL);
avcodec_parameters_from_context(sc->video_avs->codecpar, sc->video_avcc);

من الضروري توسيع دورة فك التشفير لتحويل تدفق الفيديو:

  • نرسل AVPacket فارغة إلى وحدة فك الترميز ( avcodec_send_packet ).
  • احصل على AVFrame غير المضغوط ( avcodec_receive_frame ).
  • نبدأ في إعادة ترميز الإطار الخام.
  • نرسل الإطار الخام ( avcodec_send_frame ).
  • نحصل على ضغط بناءً على برنامج ترميز AVPacket الخاص بنا ( avcodec_receive_packet ).
  • قم بتعيين الطابع الزمني ( av_packet_rescale_ts ).
  • نكتب على ملف الإخراج ( av_interleaved_write_frame ).

AVFrame *input_frame = av_frame_alloc();
AVPacket *input_packet = av_packet_alloc();

while (av_read_frame(decoder_avfc, input_packet) >= 0)
{
  int response = avcodec_send_packet(decoder_video_avcc, input_packet);
  while (response >= 0) {
    response = avcodec_receive_frame(decoder_video_avcc, input_frame);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return response;
    }
    if (response >= 0) {
      encode(encoder_avfc, decoder_video_avs, encoder_video_avs, decoder_video_avcc, input_packet->stream_index);
    }
    av_frame_unref(input_frame);
  }
  av_packet_unref(input_packet);
}
av_write_trailer(encoder_avfc);

// used function
int encode(AVFormatContext *avfc, AVStream *dec_video_avs, AVStream *enc_video_avs, AVCodecContext video_avcc int index) {
  AVPacket *output_packet = av_packet_alloc();
  int response = avcodec_send_frame(video_avcc, input_frame);

  while (response >= 0) {
    response = avcodec_receive_packet(video_avcc, output_packet);
    if (response == AVERROR(EAGAIN) || response == AVERROR_EOF) {
      break;
    } else if (response < 0) {
      return -1;
    }

    output_packet->stream_index = index;
    output_packet->duration = enc_video_avs->time_base.den / enc_video_avs->time_base.num / dec_video_avs->avg_frame_rate.num * dec_video_avs->avg_frame_rate.den;

    av_packet_rescale_ts(output_packet, dec_video_avs->time_base, enc_video_avs->time_base);
    response = av_interleaved_write_frame(avfc, output_packet);
  }
  av_packet_unref(output_packet);
  av_packet_free(&output_packet);
  return 0;
}

قمنا بتحويل دفق الوسائط من h264 إلى h265 . كما هو متوقع ، فإن إصدار ملف الوسائط h265 أصغر من h264 ، بينما يتمتع البرنامج بفرص وافرة:

  /*
   * H264 -> H265
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx265";
  sp.codec_priv_key = "x265-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> remuxed (untouched)
   * MP4 - fragmented MP4
   */
  StreamingParams sp = {0};
  sp.copy_audio = 1;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.muxer_opt_key = "movflags";
  sp.muxer_opt_value = "frag_keyframe+empty_moov+default_base_moof";

  /*
   * H264 -> H264 (fixed gop)
   * Audio -> AAC
   * MP4 - MPEG-TS
   */
  StreamingParams sp = {0};
  sp.copy_audio = 0;
  sp.copy_video = 0;
  sp.video_codec = "libx264";
  sp.codec_priv_key = "x264-params";
  sp.codec_priv_value = "keyint=60:min-keyint=60:scenecut=0:force-cfr=1";
  sp.audio_codec = "aac";
  sp.output_extension = ".ts";

  /* WIP :P  -> it's not playing on VLC, the final bit rate is huge
   * H264 -> VP9
   * Audio -> Vorbis
   * MP4 - WebM
   */
  //StreamingParams sp = {0};
  //sp.copy_audio = 0;
  //sp.copy_video = 0;
  //sp.video_codec = "libvpx-vp9";
  //sp.audio_codec = "libvorbis";
  //sp.output_extension = ".webm";

أعترف أنه كان أكثر تعقيدًا مما بدا في البداية. كان علي اختيار رمز مصدر سطر الأوامر FFmpeg واختبار الكثير. ربما فاتني شيء ما في مكان ما ، لأنني اضطررت إلى استخدام force-cfr لـ h264 ، ولا تزال بعض رسائل التحذير تظهر ، على سبيل المثال ، أن نوع الإطار (5) قد تم تغييره قسريًا إلى نوع الإطار (3).

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


All Articles