بازگشت به بلاگ
۲۲ مرداد ۱۴۰۵Sergei Solod13 دقیقه مطالعه

چطور با H.264 یک ویدیوی واقعی را از حدود 280 MB به حدود 50 MB رساندم

یک ویدیوی واقعی از سامانهٔ عملیاتی پس از بازطراحی تنظیمات H.264 با CRF 28، x264 veryslow، سقف وضوح در ردهٔ 720p، نرخ فریم مفید و الزامات متعادل برای رمزگشا، از حدود 280 MB به حدود 50 MB رسید. در مرحله‌ای قدیمی‌تر همان نمونه از حدود 350 به 238 MB کاهش یافته بود و بررسی کتابخانهٔ قدیمی نیز نشان داد فایل‌های H.264 با نرخ چند مگابیت بر ثانیه رایج بوده‌اند.

H.264FFmpegx264فشرده‌سازی ویدیوکارایی وببهینه‌سازی رسانه

عددی که بالاخره این بهینه‌سازی را ملموس کرد ساده بود: یک ویدیوی واقعی عملیاتی که دنبال می‌کردم، بعد از سیاست جدید H.264 از حدود 280 MB به 50 MB رسید. یعنی تقریباً 5.6 برابر کوچک‌تر، با صرفه‌جویی حدود 230 MB یا نزدیک به 82% از اندازهٔ اولیه.

برای رسیدن به این نتیجه به AV1، HEVC یا VP9 مهاجرت نکردم. خروجی عملیاتی همچنان H.264 داخل MP4 بود. چیزی که تغییر کرد سیاست اطراف کُدِک بود: پیکسل‌های غیرضروری کمتر، نمونه‌های زمانی اضافه کمتر، هدف کیفیت بسیار کمتر محافظه‌کارانه، صرف CPU بیشتر در مرحلهٔ یک‌بارهٔ رمزگذاری، و یک قرارداد عمداً محدود برای رمزگشا.

این یک سناریوی کاری بسیار مشخص بود: کلیپ‌های کوتاه تصویرسازی‌شده و متحرک، حدود 80% ترافیک موبایل، پهنای باند به‌عنوان هزینه‌ای تکرارشونده، و رمزگذاری آفلاین که زمان آن در مقایسه با سرو مکرر فایل‌های بیش از حد بزرگ ارزان است.

سیاست قدیمی و جدید تقریباً چنین بودند:

تنظیمات قبلی
H.264 Main @ Level 4.0
CRF 19
پیش‌تنظیم slow
تا 1920x1080 / 1080x1920
30 fps
refs = 3
فریم‌های B = 3
GOP ≈ 2 ثانیه
VBV ≈ 10M / 20M

تنظیمات جدید
H.264 Main @ Level 3.1
CRF 28
پیش‌تنظیم veryslow
سقف ردهٔ 720p، بدون بزرگ‌نمایی
CFR مفید، معمولاً <= 30 fps
refs = 4
فریم‌های B = 5
GOP ≈ 5 ثانیه
VBV ≈ 4M / 8M
yuv420p / avc1 / faststart

نتیجهٔ اندازه‌گیری‌شده: از حدود 280 MB به حدود 50 MB

چند اندازه‌گیری واقعی عملیاتی دارم، اما همهٔ آن‌ها یک آزمایش واحد نیستند. جدا نگه داشتن این دسته‌ها از پیدا کردن بزرگ‌ترین درصد جذاب مهم‌تر است.

اندازه‌گیریپیش از تغییرپس از تغییرکاهش
همان ویدیوی مشخص~280 MB~50 MB~5.6× کوچک‌تر / ~82% کمتر
مرحلهٔ قدیمی‌تر همان فایل~350 MB~238 MB~1.47× کوچک‌تر / 32% کمتر

ردیف اول تمیزترین مدرک برای عنوان مقاله است: همان ویدیوی مشخص قبل و بعد از سیاست جدید، حدود 280 MB به حدود 50 MB رسید. خط نرخ بیت/مدت دقیق آن جفت را در لاگ‌های بازیابی‌شده ندارم، بنابراین چیزی اختراع نمی‌کنم. خود تغییر اندازه کافی است: این فایل مشخص حدود 5.6 برابر کوچک‌تر شد.

نمونهٔ ~350→238 MB مرحلهٔ قدیمی‌تری از بهینه‌سازی همان فایل بود. خروجی تقریباً 1264×720، 30 fps، حدود 500 ثانیه، بدون صدا و نزدیک 3.8 Mbps بود. حساب با اندازهٔ مشاهده‌شده جور درمی‌آید: 3.8 Mbps برای حدود 500 ثانیه تقریباً 238 MB می‌شود. این مرحله از ~350 MB حدود 32% صرفه‌جویی داده بود، اما برای هدف پهنای باند من هنوز بیش از حد بزرگ بود.

ممیزی کتابخانهٔ قدیمی هم نشان داد H.264های بزرگ فقط یک نمونهٔ پرت عجیب نبودند. در یک ممیزی، 238 ویدیوی عملیاتی در مجموع 6.37 GB بودند: 117 H.264 و 121 AV1. تعداد 101 فایل حداقل 20 MB و 34 فایل حداقل 50 MB بودند. چند نمونهٔ بزرگ H.264:

حجممدتمیانگین نرخ بیت
121.0 MB4:253.83 Mbps
101.2 MB5:192.659 Mbps
92.78 MB4:442.735 Mbps
90.10 MB3:553.206 Mbps
89.74 MB4:412.676 Mbps

این‌ها محتوای متفاوتی هستند، پس جدول فقط زمینه می‌دهد و آزمون مقایسهٔ مستقیم قبل و بعد نیست. با این حال نشان می‌دهد نمونهٔ قدیمی 3.8 Mbps یک استثنای تصادفی نبود؛ چند فایل بزرگ H.264 در کتابخانهٔ قبلی واقعاً در محدودهٔ تقریبی 2.6 تا 3.8 Mbps قرار داشتند.

مهم‌ترین بهینه‌سازی یک گزینهٔ FFmpeg نبود

بزرگ‌ترین تغییر، نگاه من به هزینه بود.

کدگذاری یک بار انجام می‌شود. انتقال فایل هر بار که کسی آن را درخواست کند تکرار می‌شود.

برای ویدیوی بلادرنگ، صرف CPU بسیار بیشتر برای کمی کاهش نرخ بیت می‌تواند معاملهٔ بدی باشد. فایل‌های من آفلاین کدگذاری می‌شوند و بعد بارها تحویل داده می‌شوند. در این مدل، ده دقیقه صرفه‌جویی در زمان کدگذاری ممکن است از نظر اقتصادی تقریباً بی‌ارزش باشد اگر کدگذاری سریع‌تر باعث شود هر درخواست بعدی فایل بزرگ‌تری دریافت کند.

به همین دلیل -preset veryslow برای من منطقی است. حاضرم یک بار CPU بیشتری خرج کنم اگر x264 بتواند با آن نمایش فشرده‌تری پیدا کند. مرورگر جست‌وجوی انکودر را دوباره انجام نمی‌دهد؛ فقط بیت‌استریم نهایی را رمزگشایی می‌کند.

قاعده ساده شد: محاسبه را در مرحله‌ای خرج کن که یک بار انجام می‌شود و در مرحله‌ای که بارها تکرار می‌شود روی بایت‌ها سخت‌گیر باش.

چرا به‌جای دنبال کردن جدیدترین کدک، H.264 را نگه داشتم

نمی‌گویم H.264 کارآمدترین کدک فشرده‌سازی موجود است. نیست. کدک‌های جدیدتر وقتی جذاب‌اند که سامانهٔ تحویل بتواند چند نسخه نگه دارد و برای هر دستگاه نسخهٔ مناسب را انتخاب کند.

محدودیت من چیز دیگری بود: یک URL، یک فایل، یک کدک و تا حد ممکن دردسر کمتر برای پخش در میان کاربرانی که عمدتاً موبایل دارند.

برای این هدف، H.264 داخل MP4 هنوز یک انتخاب پایهٔ بسیار امن است. Apple در حال حاضر به توسعه‌دهندگان وب توصیه می‌کند برای ویدیوی ثابت در Safari از فایل‌های MP4 کدگذاری‌شده با H.264 استفاده کنند. مستندات فعلی Android نیز H.264 در MP4 را فهرست می‌کند و در Android 6.0 و بالاتر وجود دیکودر پروفایل Main را الزامی می‌داند؛ در پیشنهادهای پخش H.264 نیز 1280×720 با 30 fps برای HD آمده، با این توضیح که HD روی همهٔ دستگاه‌ها در دسترس نیست. به فرمت‌های رسانه‌ای پشتیبانی‌شده در Android مراجعه کنید.

این به آن معنا نیست که دستگاه‌های جدید به پروفایل Main یا سطح 3.1 محدودند. راهنمای HLS اپل، برای نمونه، معمولاً پروفایل High را به Main یا پروفایل Baseline ترجیح می‌دهد. من Main@3.1 را انتخاب کردم چون برای یک فایل MP4 ثابت، عمداً نیازهای رمزگشایی متواضعی می‌خواستم؛ نه چون Apple آن را الزام کرده است.

دیگر پیکسل‌هایی را که لازم نبود وجود داشته باشند کدگذاری نکردم

وضوح تصویر یکی از بزرگ‌ترین اهرم‌ها بود. سقف من تقریباً 1280×720 برای افقی، 720×1280 برای عمودی و حدود 960×960 برای محتوای مربعی یا با جهت‌های ترکیبی شد.

قاعدهٔ مهم‌تر این است: فقط برای رسیدن به سقف، تصویر را بزرگ نکن.

اگر منبع 900×600 باشد، تبدیل آن به 1280×720 جزئیات از دست‌رفته را برنمی‌گرداند. فقط نمونه‌های بیشتری می‌سازد که انکودر باید توصیفشان کند. منبع 1920×1080 را می‌توان به کلاس 720p کاهش داد، اما منبع 900×600 می‌تواند تقریباً همان 900×600 بماند. سقف، حداکثر است نه هدف.

ساده به نظر می‌رسد، اما حذف پیکسل‌های غیرضروری می‌تواند از بسیاری از ریزتنظیم‌های پیچیدهٔ انکودر مهم‌تر باشد.

این سقف فقط به‌خاطر گرد بودن عدد انتخاب نشده است. یک فریم 1920×1080 شامل 2,073,600 پیکسل است، در حالی که 1280×720 فقط 921,600 پیکسل دارد. بنابراین گذار از 1080p به 720p پیش از آن‌که رمزگذار تصمیم‌های فشرده‌سازی را آغاز کند، حدود 55.6٪ از نمونه‌های مکانی را حذف می‌کند.

540p را هم به‌عنوان انتخاب عمومی بررسی کردم. اما 960×540 فقط 518,400 پیکسل دارد؛ یعنی 43.75٪ کمتر از 1280×720 و تنها 56.25٪ از نمونه‌های 720p باقی می‌ماند. در محتوای تصویرسازی‌شده این نمونه‌ها خطوط نازک، چشم‌ها، مو، انگشتان، چهره‌ها و مرزهای تیز را توصیف می‌کنند. اگر هنوز حجم کمتر بخواهم، ترجیح می‌دهم پیش از حذف کورکورانهٔ 43.75٪ دیگر از اطلاعات مکانی، CRF کمی بالاتر را آزمایش کنم. کوانتیزه‌سازی را می‌توان در رمزگذاری بعدی تغییر داد؛ جزئیاتی که با کاهش وضوح حذف شده‌اند دیگر بازنمی‌گردند.

پس ردهٔ 720p سقف عمومی محتاطانهٔ من است، نه این ادعا که 540p بد است. برای یک فایل مشخص ممکن است نسخهٔ اندازه‌گیری‌شدهٔ 540p بهتر باشد. فقط بدون داده چنین کاهش فضایی برگشت‌ناپذیری را به قانون کل کتابخانه تبدیل نمی‌کنم.

دیگر برای فریم‌هایی که واقعاً در منبع نبودند هزینه ندادم

نرخ فریم هم یک ضریب دیگر است. اگر یک انیمیشن در هر ثانیه حدود 16 حالت بصری واقعاً مفید داشته باشد، ذخیره کردنش با 30 یا 60 fps خودبه‌خود حرکت را بهتر نمی‌کند. اغلب فقط نمونه‌های زمانی تکراری یا ساخته‌شده اضافه می‌شوند که باز هم باید در فایل نمایش داده شوند.

سیاست من حفظ آهنگ مفید منبع و معمولاً ماندن روی 30 fps یا کمتر است. برای این نوع محتوا، 12، 15، 16، 18، 20، 24، 25 یا 30 fps همگی می‌توانند منطقی باشند، اگر واقعاً منبع را درست توصیف کنند.

در خروجی تولیدشده نرخ فریم ثابت و تمیزی را هم ترجیح می‌دهم. VFR ذاتاً مشکل‌دار نیست؛ CFR فقط کار با زمان‌نشان‌ها، شمارش فریم‌ها، بررسی مدت، جابه‌جایی در ویدیو و اعتبارسنجی بعدی را در خط پردازش من ساده‌تر می‌کند.

اصل کلی از هر عدد FPS مهم‌تر است: برای اطلاعات زمانی‌ای که در منبع وجود ندارد پهنای باند مصرف نکن.

CRF 28 انتخابی برای این کاربرد است، نه یک عدد جادویی

نمی‌خواستم همهٔ کلیپ‌ها به یک نرخ بیت هدف یکسان رانده شوند. یک تصویر تقریباً ثابت و صحنه‌ای با حرکت پیچیده برای قابل‌قبول دیده شدن به تعداد یکسانی بیت نیاز ندارند.

بنابراین از حالت CRF در x264 استفاده می‌کنم و برای این محتوای مصور با اولویت پهنای باند، حدود -crf 28 را انتخاب کردم. FFmpeg مقدار CRF در libx264 را به‌عنوان کنترل نرخ در حالت کیفیت ثابت مستند کرده است؛ مستندات کدک FFmpeg را ببینید.

CRF 28 عمداً تهاجمی است. آن را بدون آزمایش برای دانه‌های فیلم، ویدیوی دوربین پرنویز، متن بسیار ریز روی صفحه یا کاری که وفاداری تصویر از پهنای باند مهم‌تر است کپی نمی‌کنم.

همچنین معیار ادراکی عمومی‌ای ندارم که ثابت کند CRF 28 همیشه از نظر بصری نامحسوس است. دربارهٔ کار خودم فقط می‌توانم محدودتر بگویم: فایل‌ها خیلی کوچک‌تر شدند و در پخش عادی هنوز برای من طبیعی به نظر می‌رسیدند. این یک مشاهدهٔ عملی است، نه ادعای بی‌اتلاف بودن CRF 28.

veryslow برای انکودر گران است، نه لزوماً برای دیکودر

تنظیم ازپیش‌تعریف‌شدهٔ من -preset veryslow است. تنظیم ازپیش‌تعریف‌شدهٔ کندتر به x264 فرصت بیشتری می‌دهد تا تصمیم‌های کارآمدتر برای پیش‌بینی و کدگذاری پیدا کند. هزینه‌اش CPU و زمان کدگذاری است.

تفاوت مهم این است که تلاش انکودر و پیچیدگی دیکودر یک چیز نیستند.

می‌توانم اجازه بدهم x264 بسیار سخت کار کند و در عین حال جریان نهایی را جداگانه محدود کنم. قرارداد محافظه‌کارانهٔ خروجی من این است:

H.264، پروفایل Main
Level 3.1
yuv420p هشت‌بیتی
avc1
refs = 4
فریم‌های B = 5
B-pyramid = normal
GOP باز = غیرفعال

FFmpeg مقدار CRF، تنظیم ازپیش‌تعریف‌شده، تنظیم محتوا، محدودیت پروفایل، فریم‌های مرجع و فریم B را جداگانه در اختیار می‌گذارد. من هم دقیقاً همین‌طور به آن‌ها نگاه می‌کنم: انکودر هرقدر لازم است جست‌وجو کند، اما سمت پخش معمولی و قابل‌پیش‌بینی بماند.

GOP و VBV حفاظ‌اند، نه کنترل اصلی کیفیت

برای این کلیپ‌های کوتاه با پخش تدریجی، حداکثر GOP را حدود پنج ثانیه می‌گذارم: تقریباً -g 150 در 30 fps، -g 120 در 24 fps و -g 80 در 16 fps.

این انتخاب مخصوص کار من است، نه یک قانون عمومی. پخش تطبیقی محدودیت‌های دیگری دارد؛ برای مثال، راهنمای تولید HLS اپل توصیه می‌کند هر دو ثانیه یک IDR وجود داشته باشد. من این قاعدهٔ HLS را بدون فکر روی فایل‌های MP4 کوتاه و ثابت با پخش تدریجی کپی نمی‌کنم.

همچنین تقریباً از این مقادیر استفاده می‌کنم:

-maxrate:v 4M
-bufsize:v 8M

این‌ها سقفی برای جهش‌های غیرعادی نرخ بیت هستند. معنایشان این نیست که «همه‌چیز را با 4 Mbps کدگذاری کن». توزیع معمول بیت‌ها همچنان بر عهدهٔ CRF است، بنابراین کلیپ‌های ساده می‌توانند بسیار کوچک شوند.

کانتینر MP4 را هم تا جای ممکن معمولی نگه داشتم

صریحاً از avc1 استفاده می‌کنم. مستندات فعلی HLS اپل قالب‌های نمونه مانند avc1 را به avc3 ترجیح می‌دهد. این دلیل کوچک‌تر شدن فایل‌های من نیست، اما با هدف تولید H.264 در MP4 به شکل متعارف هماهنگ است.

از -movflags +faststart هم استفاده می‌کنم. مستندات فرمت FFmpeg می‌گوید faststart ایندکس moov را به ابتدای فایل MP4 منتقل می‌کند. الزامات پخش HTTP در Android نیز برای MPEG-4 می‌گوید moov باید بعد از ftyp و قبل از mdat قرار بگیرد.

ftyp
moov
mdat

faststart فشرده‌سازی را بهتر نمی‌کند. فقط پخش تدریجی روی HTTP را ساده‌تر می‌کند.

برای خروجی SDR معمولی از ۸ بیتی yuv420p استفاده می‌کنم و BT.709 را با محدودهٔ ویدیویی محدود علامت می‌زنم. اگر کلیپ صدا نداشته باشد، ترک صوتی مصنوعی اضافه نمی‌کنم. برای این محتوای مصور از -tune animation هم استفاده می‌کنم؛ آن را انتخابی مخصوص نوع محتوا می‌دانم، نه بخشی از قرارداد عمومی سازگاری.

پروفایل اصلی FFmpeg

برای یک منبع مصور 30 fps، بخش مرکزی فرمان تقریباً این است:

ffmpeg -i input \
  -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 150 \
  -maxrate:v 4M \
  -bufsize:v 8M \
  -x264-params "open-gop=0:b-pyramid=normal:nal-hrd=none" \
  -color_range tv \
  -color_primaries bt709 \
  -color_trc bt709 \
  -colorspace bt709 \
  -an \
  -movflags +faststart \
  output.mp4

مراحل تغییر اندازه و نرخ فریم را عمداً در این مثال ثابت نکرده‌ام. منبع 900×600 نباید فقط چون سقف 1280×720 است بزرگ شود، و انیمیشنی با نرخ فریم طبیعی پایین نباید فقط چون مثال از -g 150 استفاده می‌کند به 30 fps مجبور شود.

فرمان، اجرای سیاست است؛ خود سیاست نیست.

چرا فایل‌ها چند برابر کوچک‌تر شدند

هیچ گزینهٔ جادویی واحدی وجود نداشت.

کاهش حجم حاصل کنار هم قرار گرفتن چند تصمیم بود که هرکدام نوعی اتلاف را حذف کردند: پیکسل‌های غیرضروری، فریم‌های غیرضروری، هدف کیفیت بیش از حد محافظه‌کارانه، تنظیمات رمزگذار متمایل به سرعت به‌جای کارایی، فریم‌های کلیدی بیش از حد و جریان‌هایی که به آن‌ها نیاز نداشتم.

به همین دلیل، جملهٔ «این فایل H.264 است» دربارهٔ اندازهٔ آن اطلاعات خیلی کمی می‌دهد. دو کدگذاری H.264 از یک منبع می‌توانند تفاوت زیادی داشته باشند، چون نام کدک چیزی دربارهٔ وضوح، نرخ فریم، کنترل نرخ، تنظیم ازپیش‌تعریف‌شده، ساختار GOP، پروفایل یا آماده‌سازی منبع نمی‌گوید.

در مورد من، تغییر این تصمیم‌های اطراف کدک مهم‌تر از عوض کردن خود کدک بود.

این نتیجه چه چیزی را ثابت نمی‌کند

هر تنظیم را در یک آزمایش کنترل‌شده جداگانه بررسی نکردم، بنابراین نمی‌توانم صادقانه درصد دقیقی از صرفه‌جویی را جداگانه به veryslow، CRF 28، کاهش وضوح یا کاهش نرخ فریم نسبت دهم.

همچنین نمی‌توانم بگویم هر خروجی CRF 28 از نظر ادراکی نامحسوس است. «افت کیفیت واضحی ندیدم» مشاهدهٔ من برای این نوع محتوای مصور در اندازه‌های معمول نمایش است، نه تضمین علمی برای هر نوع ویدیو.

و نمی‌گویم یک فایل H.264 معماری درست برای هر سایت است. چند نسخه، پخش تطبیقی، HDR، 4K و مذاکرهٔ کدک، موازنه‌ها را تغییر می‌دهند.

چیزی که واقعاً می‌توانم ادعا کنم محدودتر است: برای کتابخانه‌ای از کلیپ‌های کوتاه مصور و متحرک که پهنای باند در آن اولویت دارد، مخاطب عمدتاً موبایلی است، زمان کدگذاری ارزان است و پخش قابل‌پیش‌بینی اهمیت دارد، این پروفایل فایل‌های من را چند برابر کوچک‌تر کرد و در پخش معمولی همچنان طبیعی به نظر می‌رسید.

این نتیجه چگونه روش بهینه‌سازی من را تغییر داد

قبلاً به بهینه‌سازی ویدیو بیشتر به چشم مسئلهٔ تنظیمات انکودر نگاه می‌کردم. حالا آن را مسئلهٔ هزینه در طول عمر فایل می‌بینم.

انکودر شاید یک بار اجرا شود. بایت‌ها ممکن است هزاران یا میلیون‌ها بار از شبکه عبور کنند.

این نگاه معنای «گران» را عوض می‌کند.

خوشحالم یک بار CPU بیشتری خرج کنم. در عوض خیلی کمتر مایلم در هر درخواست آینده پیکسل‌های حاصل از بزرگ‌نمایی، فریم‌هایی که حرکت مفیدی اضافه نمی‌کنند یا نرخ بیتی را بفرستم که محتوا به آن نیاز ندارد.

کدک همان انتخاب معمولی ماند: H.264 داخل MP4. بهینه‌سازی در اطرافش اتفاق افتاد.

برای این کاربرد، نتیجه از هر گزینهٔ منفرد FFmpeg روشن‌تر است: هزینه‌ای را بهینه کن که بارها می‌پردازی، نه هزینه‌ای را که فقط یک بار می‌پردازی.