لم أبنِ هذا المسار لتجربة برامج ترميز مختلفة. بنيته لأن الصور المتحركة أصبحت وسيلة مكلفة لتقديم محتوى يتصرف عمليًا كفيديو قصير صامت.
معظم المواد التي أعالجها هي ملفات WebP متحركة وGIF وAPNG قصيرة: غالبًا لا تتجاوز بضع ثوانٍ، وتحتوي عادةً على عشرات قليلة من الإطارات المعروضة، وفيها قدر كبير من التشابه بين اللحظات المتتالية. أغلب المشاهدة تتم على الهواتف، وقد يُطلب الملف نفسه مرارًا، لذلك تهمني وحدات البيانات التي سأرسلها لاحقًا أكثر بكثير من كلفة الترميز التي أدفعها مرة واحدة.
الجزء الصعب ليس تشغيل FFmpeg. فالصورة المتحركة ليست بالضرورة مجموعة مرتبة من صور كاملة الحجم بمعدل إطارات منتظم. قد تحتوي على مستطيلات جزئية، وقواعد للمزج والتخلص من الإطار، وشفافية، وتأخيرات غير منتظمة، وإطارات بمدة صفرية، واتجاهات مختلفة، وبيانات توقيت يمكن لأداة فحص عامة أن تلخصها بصورة مضللة.
لذلك أتعامل مع التحويل بوصفه مجموعة من الثوابت التي يجب الحفاظ عليها، لا أمرًا واحدًا:
WebP متحرك / GIF / APNG
↓
إعادة بناء حالات اللوحة الكاملة التي يراها المستخدم
↓
استعادة توقيت المصدر وتنقيحه
↓
تحليل جميع المصادر في التسلسل النهائي
↓
اختيار CFR واحد لملف MP4 النهائي
↓
حساب أصغر لوحة مشتركة من دون تكبير
↓
ترميز مقاطع H.264 متوافقة
↓
التحقق من عقد البث
↓
الدمج بنسخ البث
↓
تطبيع الخط الزمني للحزم والتحقق منه
↓
التحقق من التسليم عبر HTTP
↓
نشر ذري
برنامج الترميز مهم، لكن الحفاظ على ما عرضته الحركة فعليًا أهم.
نتيجة إنتاج مقاسة: 217 ملف WebP متحرك أصبحت ملف MP4 واحدًا بحجم 78.49 MB
لم يكن الإدخال فيديو واحدًا بحجم 1.49 GB. كان 217 ملف WebP متحركًا منفصلًا، تحتوي معًا على 10,633 إطارًا معروضًا. وكان الحجم الإجمالي لهذه الرسوم المتحركة نحو 1.49 GB.
المدخل
217 ملف WebP متحرك
1.49 GB إجمالًا
10,633 إطارًا معروضًا
الناتج
ملف H.264 MP4 واحد
78.49 MB
0.98 Mbps
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow
بلغ حجم ملف H.264 الناتج 78.49 MB بمعدل يقارب 0.98 Mbps. وبالمقارنة مع مجموع وحدات بيانات الرسوم المتحركة المصدر، أصبح الناتج أصغر بنحو 19 مرة، أي أقل بحوالي 94.7% من البيانات.
هذه نتيجة للمسار كاملًا من البداية إلى النهاية، وليست اختبار A/B نظيفًا بين «H.264 قديم» و«H.264 جديد». فقد تغير التمثيل من مئات ملفات الصور المتحركة إلى فيديو واحد يستفيد من الضغط الزمني، لذلك لا أنسب كامل فرق 19 مرة إلى CRF 28 أو veryslow أو خيار واحد في برنامج الترميز.
أولًا: أعيد بناء الصور التي يراها المشاهد فعلًا
أخطر اختصار هو افتراض أن كل إطار مخزن داخل ملف الحركة يمثل صورة كاملة تستبدل ما قبلها.
يمكن لإطار WebP المتحرك أن يصف مستطيلًا في موضع محدد مع سلوك للمزج والتخلص. وتملك إطارات APNG إزاحات وأبعادًا ومددًا وعمليات للمزج والتخلص. كما يستطيع GIF الاحتفاظ باللوحة السابقة أو مسح منطقة أو استعادة حالة أقدم.
لذلك قد يكون الإطار المخزن مجرد رقعة صغيرة يعتمد معناها على اللوحة التي أنشأتها الإطارات السابقة. وإذا رُمّزت هذه الرقع كما لو كانت صورًا كاملة، فالنتيجة ليست نسخة أصغر من الحركة الصحيحة، بل حركة خاطئة.
وحدة الاستخراج عندي هي حالة اللوحة الكاملة المعروضة: كل البكسلات المركبة التي سيعرضها عارض صحيح بعد تطبيق قاعدة التخلص للإطار السابق وقاعدة المزج للإطار الحالي.
هذا هو أول ضمان لصحة المسار. فإذا سُطّح إطار جزئي خاطئ داخل H.264، فلن يستطيع CRF أو الإعداد المسبق أو خيار التغليف إصلاحه لاحقًا.
توقيت الإطارات من بيانات المصدر، لا قيمة FPS نخمنها
تخزن صيغ الحركة الزمن بطرق مختلفة. يستخدم WebP المتحرك مدة مستقلة لكل إطار بوحدات 1 ms. ويخزن GIF التأخير بأجزاء من مئة من الثانية. أما APNG فيخزن بسطًا ومقامًا لكل تأخير؛ وإذا كان المقام صفرًا فمواصفة PNG تتعامل معه على أنه 100.
هذه التأخيرات هي الخط الزمني الحقيقي. أما قيمة FPS البارزة التي تعرضها أداة فحص عامة فهي مجرد تلخيص وقد تكون مضللة.
كان أحد ملفات WebP الحقيقية في مساري بدقة 1264×720 ويحتوي على 49 إطارًا معروضًا. وتناوبت مدد الإطارات بين 62 و63 ms، بإجمالي 3.063 ثانية. وهذا يعادل عمليًا إيقاع 16 fps لأن مدة الإطار عند 16 fps هي 62.5 ms.
لكن أداة فحص عامة أبلغت عن 25 fps لذلك المصدر. ولو وثقت بهذا الرقم لغيّرت توقيت المصدر أو صنعت صورًا مكررة لا حاجة إليها.
أحتاج أيضًا إلى سياسة للتأخيرات المشوهة أو الملتبسة. تترك مواصفة WebP تفسير مدة الإطار الصفرية، وغالبًا القيم الصغيرة جدًا، للتنفيذ. ويمكن أن يحتوي GIF على تأخير صفري. ويسمح APNG ببسط صفري، أي عرض الإطار التالي بأسرع ما يمكن، مع إمكان فرض العارض حدًا أدنى عمليًا.
سياستي في التطبيع تحفظ التوقيت بدقة المللي ثانية، وتستخدم حدًا أدنى صغيرًا قدره 10 ms للمدد الصفرية أو الصغيرة بصورة غير معقولة، ولا تلجأ إلى 100 ms إلا عندما لا يتوفر توقيت مفيد أصلًا. هذه أرقام تخص سياستي وليست معايير عالمية.
أختار CFR واحدًا لملف MP4 النهائي كله، ولا أفرض 30 fps تلقائيًا
بعد إعادة بناء الحالات المعروضة ومددها، أحوّل خط المصدر الزمني إلى خط زمني للفيديو. ولا أرمّز كل شيء عند 30 fps بصورة عمياء.
لكل مصدر سيظهر في ملف MP4 النهائي نفسه، أفحص مجموعة صغيرة من القيم المرشحة:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
تختار الخوارزمية أقل CFR يمثل التسلسل النهائي كاملًا بصورة مقبولة. يمكن لملفات MP4 نهائية مختلفة أن تختار معدلات مختلفة، لكن جميع المقاطع المرمزة مستقلًا داخل ملف MP4 واحد تستخدم CFR المختار نفسه.
يوضح مثال 3.063 ثانية الفائدة مباشرة. عند 16 fps يحتاج إلى نحو 49 إطار خرج، بينما يحتاج عند 30 fps إلى نحو 92. وإذا كان مصدر آخر في ملف MP4 نفسه يحتاج فعلًا إلى 30 fps، يستخدم التسلسل كله 30. لا أخلط معدلات إطارات مختلفة بين المقاطع داخل البث النهائي الواحد.
أستخدم مقياسًا زمنيًا لمسار الفيديو قدره 90,000 Hz لأن كل CFR مسموح به ينتج مدة إطار صحيحة بعدد صحيح من النبضات:
10 fps → 9000 ticks
12 fps → 7500 ticks
15 fps → 6000 ticks
16 fps → 5625 ticks
18 fps → 5000 ticks
20 fps → 4500 ticks
24 fps → 3750 ticks
25 fps → 3600 ticks
30 fps → 3000 ticks
مقياس زمن مسار الفيديو هو الذي يمنحني هذه الشبكة الدقيقة للإطارات. وأضبط أيضًا مقياس زمن الفيلم في MP4 على 90,000 للاتساق، لكنه ساعة منفصلة في الحاوية. يتحقق المدقق من حزم الفيديو مقابل شبكة الأعداد الصحيحة بدل الاعتماد على مدد عشرية مقربة.
حدود الدقة سقوف وليست لوحات إلزامية
حدود التسليم لدي تقارب 1280×720 للأفقي، و720×1280 للرأسي، ولا يتجاوز العرض أو الارتفاع 960 للمحتوى ذي الاتجاهات المختلطة.
القاعدة غير القابلة للتفاوض هي عدم التكبير مطلقًا. فمصدر 900×600 لا يصبح أفضل عند 1280×720؛ بل ينتج فقط بكسلات مستنبطة إضافية يجب على برنامج الترميز وصفها.
والقاعدة الثانية أقل وضوحًا: 960×960 حد خارجي، وليس لوحة مربعة مفروضة.
أحسب أولًا الأبعاد الفعلية لكل مصدر بعد التصغير فقط. ثم يحصل التسلسل النهائي على أصغر لوحة مشتركة ذات أبعاد زوجية تستطيع احتواء جميع المستطيلات الفعلية بعد التصغير.
مثلًا، إذا احتاج التسلسل إلى صورة أفقية 960×540 وصورة رأسية 500×900، فيمكن أن تكون اللوحة المشتركة 960×900 بدل 960×960. وتبقى الأبعاد المرمزة متطابقة في كل المقاطع، فيظل الدمج بنسخ البث ممكنًا، لكنني أتجنب ترميز مساحة سوداء لا تؤدي أي وظيفة.
أحافظ على نسبة الأبعاد وأملأ المساحة غير المستخدمة بدل تمديد الصورة. الحشو في هذا المسار أسود. وبما أن H.264/yuv420p العادي لا يحفظ قناة الشفافية الأصلية، أركب الشفافية عمدًا على هذه الخلفية بدل فقدها مصادفة.
لماذا يناسب H.264 داخل MP4 مشكلة التسليم هذه
GIF وAPNG وWebP المتحرك ليست صيغًا بدائية. تستطيع تجنب إعادة رسم المناطق التي لم تتغير، لذلك سيكون من الخطأ القول إن «الفيديو أصغر دائمًا».
لكن H.264 مصمم للتنبؤ الزمني بين الصور. والحلقات المرسومة القصيرة ذات الخلفيات الثابتة والمناطق الصغيرة المتحركة تناسب جدًا الصور المرجعية والتنبؤ بين الإطارات وإطارات P وB.
توصي إرشادات Safari الحالية من Apple باستخدام MP4 المشفر بـ H.264 للفيديو الثابت، وتذكر أن GIF المتحرك قد يستهلك حتى 12 ضعفًا من عرض النطاق وضعفي الطاقة مقارنة ببرنامج ترميز فيديو حديث. رقم 12 ضعفًا مثال من Apple، وليس نتيجة قياس خاصة بي.
لذلك تعد نتيجة 19 مرة التي قستها مفيدة كملاحظة إنتاج حقيقية، لكنني أظل أقيس بدل افتراض أن كل ملف WebP متحرك صغير أصلًا سيخسر أمام MP4.
أرمّز كل حركة كمقطع ضمن عقد بث واحد
عند وصول العمل إلى برنامج الترميز يكون المسار قد عرف بالفعل الإطارات المعروضة، وCFR الموحد للتسلسل، واللوحة النهائية، والمدة المتوقعة.
الجزء المركزي من أمر ترميز المقطع لدي يقارب ما يلي:
ffmpeg -framerate "$COLLECTION_FPS" -i frame-%06d.png \
-c:v libx264 \
-preset veryslow \
-tune animation \
-crf 28 \
-profile:v main \
-level:v 3.1 \
-pix_fmt yuv420p \
-tag:v avc1 \
-refs 4 \
-bf 5 \
-g "$GOP_FRAMES" \
-maxrate:v 4M \
-bufsize:v 8M \
-x264-params "stitchable=1:open-gop=0:b-pyramid=normal:nal-hrd=none" \
-color_range tv \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-fps_mode passthrough \
-map_metadata -1 \
-map_chapters -1 \
-an -sn -dn \
-video_track_timescale 90000 \
-movie_timescale 90000 \
-t "$EXPECTED_DURATION" \
segment.mp4
الحد الأقصى لـ GOP يقارب خمس ثوانٍ ويُشتق من CFR المختار: 80 إطارًا عند 16 fps، و120 عند 24 fps، و150 عند 30 fps.
الخيار -t "$EXPECTED_DURATION" ليس للزينة. في مساري المبني على قائمة الإطارات أكرر الصورة الأخيرة كعلامة ختامية كي تُحترم مدة الإطار السابق. ومن دون حد المدة الصريح قد تتحول هذه العلامة إلى عينة إضافية عند النهاية.
أعدت إنتاج ذلك على حالة 49 إطارًا و3.063 ثانية. من دون -t نتج 50 إطارًا عند 16 fps و94 عند 30 fps. ومع -t 3.063 حصلت على العدد المقصود: 49 إطارًا عند 16 fps و92 عند 30 fps.
الدمج بنسخ البث آمن فقط بعد فحوص توافق صارمة
يتوقع مُجمّع concat في FFmpeg أن تكون الملفات ذات تدفقات متطابقة، بما فيها برنامج الترميز وقاعدة الزمن، كما يستخدم مدة كل ملف لوضع الملف التالي. لذلك يمكن لبيانات مدة خاطئة أن تصنع عيوبًا في الخط الزمني.
لا أستخدم الدمج لجعل ملفات غير متوافقة متوافقة. يجب أن يحقق المقطع العقد قبل قبوله:
CFR للمجموعة = متطابق
قاعدة زمن البث = متطابقة
المقياس الزمني لمسار MP4 = متطابق
أبعاد اللوحة / SAR = متطابقة
الملف التعريفي / المستوى / تنسيق البكسل = متطابقة
إشارات اللون = متطابقة
avcC / البيانات الإضافية لـ AVC = متطابقة بايتًا ببايت
أستخدم stitchable=1 في x264 لأن هذا المسار يرمّز المقاطع مستقلًا، لكنني لا أتعامل مع هذا الخيار كدليل على تطابق إعداد AVC. ما زلت أقارن بايتات الإعداد الفعلية قبل الدمج.
بعد تحقق العقد، يمكن أن يبقى الدمج النهائي بلا خسارة على مستوى تدفق الفيديو:
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy \
-an -sn -dn \
-movflags +faststart \
final.mp4
يمنع -c:v copy فك ضغط مقاطع H.264 المرمزة ثم ضغطها مرة ثانية.
التحقق يشمل ملف MP4 وطريقة تسليمه معًا
لا أنشر ملفًا لمجرد أن FFmpeg أنهى التنفيذ برمز 0.
اكتشف المدقق أخطاء توقيت حتمية حقيقية: رأيت عند 16 fps مقدار 5580 نبضة حيث يتطلب العقد 5625، ثم ظهر لاحقًا في خرج 24 fps مقدار 3751 حيث تتطلب الشبكة الدقيقة 3750. التحقيق التفصيلي في فرق النبضة الواحدة موضوع منفصل؛ والدرس هنا أن إعادة المحاولة لا تصلح خطأ زمنيًا حتميًا.
بالنسبة إلى كائن الوسائط نفسه أتحقق من عدد التدفقات المتوقع، وملف H.264 ومستواه، وصيغة البكسل، والأبعاد الدقيقة المخططة، وSAR، وإشارات اللون، ومقياس زمن مسار الفيديو 90 kHz، ومدد الحزم، وأعداد الإطارات والحزم، والمدة، وعلاقات PTS/DTS، وتطابق إعداد AVC بين المقاطع، ووجود moov قبل mdat، وفك ترميز الملف كاملًا مع معالجة صارمة للأخطاء.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json \
final.mp4
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
لكن ملف MP4 الصحيح محليًا قد يُسلَّم بطريقة خاطئة. لذلك أتحقق أيضًا من مسار الويب: قيمة Content-Type المتوقعة، وصحة Content-Length، ودعم طلبات نطاق البايتات، واستجابة صحيحة 206 Partial Content، وقيمة صحيحة لـ Content-Range.
وعندما أغيّر عقد برنامج الترميز، أجري اختبارًا صغيرًا على أجهزة ومتصفحات حقيقية بدل افتراض أن ffprobe يثبت توافق العتاد. أختبر البدء، والانتقال داخل الفيديو، والتكرار، والخروج إلى الخلفية ثم الاستئناف، والتشغيل عبر النطاقات على مسار iPhone/Safari حديث، وجهاز Android متواضع، والمتصفحات المكتبية الشائعة.
ولا أستبدل ملف الإنتاج ذريًا إلا بعد نجاح الملف نفسه ومسار تسليمه في العقد كله.
المعلومات التي يفقدها هذا المسار عمدًا
هذا تحويل مخصص للتسليم، وليس نسخة أرشيفية رئيسية. تُسطح الشفافية، ويُكمم توقيت المصدر غير المنتظم على CFR واحد لملف MP4 النهائي، وقد تُخفض المصادر الكبيرة، ويمر WebP المتحرك الفاقد بجيل فاقد إضافي، ولا توجد مسار صوتي عمدًا.
لن أستخدم هذا المسار نفسه عندما يجب الاحتفاظ بشفافية قابلة للتركيب فوق أي خلفية، أو عندما يكون التوقيت غير المنتظم الدقيق بين الإطارات جزءًا من معنى المحتوى، أو عندما أصنع أصلًا أرشيفيًا، أو عندما يملك التطبيق أصلًا منظومة فيديو تكيفية متعددة برامج الترميز تعالج مشكلة التسليم بطريقة أخرى.
كما أقيس الملفات المتحركة الصغيرة والمحسنة أصلًا بدل افتراض أن MP4 يجب أن يفوز دائمًا.
تسلسل الإنتاج الذي أستخدمه الآن
- أكتشف صيغة الحركة وأقرأ بيانات التحكم الحقيقية في الإطارات.
- أعيد بناء حالات اللوحة الكاملة المعروضة وفق قواعد المزج والتخلص في الصيغة.
- أستعيد مدة كل إطار وأنقح القيم غير الصالحة.
- أبني الخط الزمني الموثوق للمصدر بدقة المللي ثانية.
- أحلل كل مصدر سيظهر في ملف MP4 النهائي نفسه.
- أختار CFR واحدًا للتسلسل كله من 10/12/15/16/18/20/24/25/30.
- أحوّل الحالات المعروضة إلى شبكة CFR المختارة.
- أحسب الأبعاد الفعلية بالتصغير فقط، ولا أكبر أبدًا.
- أبني أصغر لوحة مشتركة ذات أبعاد زوجية يحتاجها التسلسل النهائي.
- أضيف الحشو من دون تمديد الصورة وأسطح الشفافية عمدًا.
- أرمّز كل مصدر على هيئة H.264 Main@3.1 / yuv420p / avc1 وفق عقد البث نفسه.
- أقيد كل مقطع بمدته المتوقعة.
- أرفض أي مقطع يخالف إعداده الفعلي لـ AVC أو توقيته العقد.
- أجمع المقاطع المقبولة باستخدام
-c:v copy. - أطبع الخط الزمني النهائي للحزم وأتحقق منه.
- أفك ترميز النتيجة كاملة.
- أتحقق من ترويسات HTTP ونطاقات البايتات وسلوك المحتوى الجزئي.
- أجري اختبار أجهزة ومتصفحات بعد تغييرات ملف الترميز.
- أنشر ذريًا بعد نجاح جميع الفحوص فقط.
الخط الزمني، لا امتداد الملف، هو مصدر الحقيقة
ملف WebP المتحرك أو GIF أو APNG هو تسلسل زمني من حالات اللوحة المعروضة، وليس مجرد مجلد صور تحمل امتدادًا صوريًا.
يستطيع H.264 استغلال التشابه الزمني بكفاءة كبيرة، لكنه لا يستطيع إصلاح تركيب خاطئ للإطارات أو توقيت مخترع أو بيانات مقاطع غير متوافقة. معظم الهندسة التي جعلت هذا المسار موثوقًا تقع قبل x264 وبعده.
والقاعدة التي بقيت معي هي: لا تغيّر طريقة التمثيل إلا بعد أن تستطيع وصف ما يجب أن يبقى ثابتًا وصفًا دقيقًا.
الوثائق الأساسية
- مواصفة حاوية WebP من Google — مستطيلات الإطارات ومددها والمزج والتخلص.
- مواصفة PNG، الإصدار الثالث، من W3C — توقيت إطارات APNG والإزاحات وعمليات المزج والتخلص.
- مواصفة GIF89a — تأخيرات GIF وسلوك التخلص.
- توثيق صيغ FFmpeg — متطلبات مُجمّع concat وسلوك تغليف MP4.
- توثيق مرشحات تدفق البتات في FFmpeg —
setts. - توثيق ffprobe — فحص التدفقات والحزم.
- Apple: تقديم محتوى الفيديو في Safari — H.264 MP4 للفيديو الثابت وإرشادات استبدال GIF المتحرك.
- صيغ الوسائط المدعومة في Android — دعم H.264 ومتطلبات البث عبر HTTP.