این فرایند را برای آزمایش کُدکهای مختلف نساختم. دلیلش این بود که تصاویر متحرک به روشی پرهزینه برای رساندن چیزی تبدیل شده بودند که در عمل یک ویدیوی کوتاه و بیصدا بود.
بیشتر محتوایی که پردازش میکنم 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 قابِ نمایشدادهشده
خروجی
1 فایل 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 برمیگردد. این اعداد سیاست من هستند، نه استاندارد جهانی.
برای کل MP4 نهایی یک CFR انتخاب میکنم، نه اینکه همهچیز را بهطور پیشفرض 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، تبدیل در 16 fps به 50 قاب و در 30 fps به 94 قاب رسید. با -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 متحرکِ از قبل فشردهشده با اتلاف، یک نسل اتلافی دیگر دریافت میکند. صدا نیز عمداً وجود ندارد.
وقتی شفافیت باید روی هر پسزمینهای قابلترکیب بماند، یا زمانبندی دقیق و نامنظم هر قاب بخشی از معنای محتوا باشد، یا نسخهٔ آرشیوی میسازم، یا سامانه از قبل تحویل تطبیقیِ چندکُدکی دارد، از همین فرایند استفاده نمیکنم.
همچنین WebPهای متحرک بسیار کوچک و از پیش بهینهشده را اندازهگیری میکنم و فرض نمیکنم MP4 حتماً برنده است.
توالی تولیدی که اکنون استفاده میکنم
- قالب متحرک را تشخیص میدهم و اطلاعات واقعی کنترل قاب را میخوانم.
- حالتهای کامل نمایشدادهشده را با رعایت قواعد ترکیب و پاکسازی قالب بازسازی میکنم.
- مدت هر قاب را بازیابی و مقادیر نامعتبر را پاکسازی میکنم.
- خط زمانی معتبر منبع را با دقت میلیثانیه میسازم.
- همهٔ منابعی را که در یک MP4 نهایی قرار میگیرند تحلیل میکنم.
- از 10/12/15/16/18/20/24/25/30 یک CFR مشترک برای کل مجموعه انتخاب میکنم.
- حالتهای نمایشدادهشده را روی خط زمانی همان 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.