Wróć do bloga
13 sierpnia 2026Sergei Solod12 min czytania

Jak zmniejszyłem produkcyjne wideo z ~280 MB do ~50 MB, pozostając przy H.264

Jeden konkretny film z systemu produkcyjnego po przebudowie konfiguracji H.264 zmniejszył się z około 280 MB do około 50 MB. Nowe podejście łączy CRF 28, x264 veryslow, limit rozdzielczości klasy 720p, użyteczną liczbę klatek na sekundę i umiarkowane wymagania wobec dekodera. Na wcześniejszym etapie ten sam przykład zmniejszył się już z około 350 do 238 MB, a przegląd starej biblioteki pokazał, że pliki H.264 o przepływności kilku megabitów na sekundę były częste.

H.264FFmpegx264Kompresja wideoWydajność WWWOptymalizacja multimediów

Liczba, która ostatecznie uczyniła tę optymalizację konkretną, była prosta: jedno rzeczywiste wideo produkcyjne po wdrożeniu nowej polityki H.264 zmniejszyło się z około 280 MB do 50 MB. To około 5,6× mniej, czyli oszczędność mniej więcej 230 MB lub 82% pierwotnego rozmiaru.

Nie osiągnąłem tego, przechodząc na AV1, HEVC czy VP9. Wynik produkcyjny pozostał H.264 w MP4. Zmieniła się polityka wokół kodeka: mniej zbędnych pikseli, mniej niepotrzebnych próbek czasowych, znacznie mniej zachowawczy cel jakościowy, więcej pracy enkodera podczas jednorazowego kodowania i świadomie ograniczony kontrakt dekodera.

To bardzo konkretne zastosowanie: krótkie materiały ilustracyjne i animowane, około 80% ruchu mobilnego, przepustowość jako koszt powtarzalny i kodowanie wstępne, w którym czas CPU jest tani w porównaniu z wielokrotnym wysyłaniem zbyt dużych plików.

W uproszczeniu stara i nowa polityka wyglądały tak:

poprzednia konfiguracja
H.264 Main @ Level 4.0
CRF 19
ustawienie slow
do 1920x1080 / 1080x1920
30 fps
refs = 3
klatki B = 3
GOP ≈ 2 sekundy
VBV ≈ 10M / 20M

nowa konfiguracja
H.264 Main @ Level 3.1
CRF 28
ustawienie veryslow
limit klasy 720p, bez powiększania
użyteczny CFR, zwykle <= 30 fps
refs = 4
klatki B = 5
GOP ≈ 5 sekund
VBV ≈ 4M / 8M
yuv420p / avc1 / faststart

Zmierzony wynik: z ~280 MB do ~50 MB

Mam kilka rzeczywistych pomiarów produkcyjnych, ale nie wszystkie są tym samym eksperymentem. Rozdzielenie ich jest ważniejsze niż znalezienie najbardziej efektownego procentu.

PomiarPrzedPoRedukcja
Ten sam konkretny film~280 MB~50 MB~5,6× mniej / ~82% mniej
Wcześniejszy etap tego samego pliku~350 MB~238 MB~1,47× mniej / 32% mniej

Pierwszy wiersz jest najczystszym dowodem dla tytułu: to samo konkretne wideo przed i po nowszej polityce, około 280 MB wobec około 50 MB. Dla dokładnie tej pary w odzyskanych starych logach nie zachowała się linia z czasem trwania i przepływnością, więc jej nie wymyślam. Sama zmiana rozmiaru daje około 5,6×.

Przypadek ~350→238 MB pochodził z wcześniejszego etapu optymalizacji tego samego przykładu na tamtym etapie łańcucha przetwarzania. Wynik miał około 1264×720, 30 fps, ~500 sekund, bez dźwięku i około 3,8 Mbps. Matematyka się zgadza: 3,8 Mbps przez około 500 sekund daje około 238 MB. To już było 32% mniej niż ~350 MB, ale nadal zdecydowanie za dużo dla mojego celu dotyczącego przepustowości.

Stary korpus również pokazywał, że duże H.264 nie były pojedynczym wyjątkiem. Jeden audyt obejmował 238 plików wideo z systemu produkcyjnego o łącznym rozmiarze 6,37 GB: 117 H.264 i 121 AV1. 101 plików miało co najmniej 20 MB, a 34 co najmniej 50 MB. Kilka dużych H.264 wyglądało tak:

RozmiarCzas trwaniaŚrednia przepływność
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

To różne materiały, więc tabela daje kontekst, a nie test A/B. Pokazuje jednak, że dawny przykład 3,8 Mbps nie był przypadkowym wyjątkiem: kilka dużych plików H.264 ze starej biblioteki rzeczywiście mieściło się mniej więcej w zakresie 2,6–3,8 Mbps.

Najważniejsza optymalizacja nie była opcją FFmpeg

Największa zmiana dotyczyła sposobu, w jaki patrzyłem na koszty.

Kodowanie wykonuję raz. Plik przesyłam za każdym razem, gdy ktoś go odtwarza.

W przypadku wideo kodowanego w czasie rzeczywistym zużycie dużo większej ilości CPU po to, by oszczędzić trochę przepływności, może być złym kompromisem. Moje pliki koduję wstępnie, a potem wielokrotnie je serwuję. W takim modelu zaoszczędzenie dziesięciu minut przy kodowaniu może nie mieć ekonomicznego znaczenia, jeśli szybsze kodowanie zwiększa każdy późniejszy transfer.

Dlatego -preset veryslow ma dla mnie sens. Mogę raz zapłacić za CPU, jeśli x264 wykorzysta ten czas do znalezienia bardziej efektywnej reprezentacji. Przeglądarka nie powtarza pracy kodera; tylko dekoduje gotowy strumień bitów.

Zasada stała się prosta: więcej obliczeń na etapie wykonywanym raz, mniej bajtów na etapie powtarzanym.

Dlaczego zostałem przy H.264 zamiast gonić za najnowszym kodekiem

Nie twierdzę, że H.264 zapewnia najlepszą dostępną kompresję. Nie zapewnia. Nowsze kodeki mogą być bardzo atrakcyjne, gdy system dystrybucji może przechowywać kilka wersji i wybierać najlepszą dla danego urządzenia.

Moje ograniczenie było inne: jeden URL, jeden plik, jeden kodek i możliwie mało problemów z odtwarzaniem u odbiorców korzystających głównie z urządzeń mobilnych.

Do tego zadania H.264 w MP4 nadal jest bardzo bezpieczną bazą. Apple zaleca obecnie twórcom stron używanie plików MP4 zakodowanych w H.264 dla statycznego wideo w Safari. Aktualna dokumentacja Androida wymienia H.264 w MP4 i od Androida 6.0 wymaga dekodera profilu Main; w zaleceniach odtwarzania H.264 dla HD podaje także 1280×720 przy 30 fps, zaznaczając, że HD nie jest dostępne na każdym urządzeniu. Zobacz obsługiwane formaty multimedialne w Androidzie.

Nie oznacza to, że współczesne urządzenia są ograniczone do profilu Main albo poziomu 3.1. Na przykład w zaleceniach Apple dla HLS zwykle preferowany jest profil High zamiast Main lub profil Baseline. Main@3.1 wybrałem dlatego, że dla jednego statycznego pliku MP4 chciałem świadomie utrzymać umiarkowane wymagania wobec dekodera, a nie dlatego, że Apple tego wymaga.

Przestałem kodować piksele, których nie potrzebowałem

Rozdzielczość okazała się jedną z największych dźwigni. Ustaliłem górny limit mniej więcej na 1280×720 dla obrazu poziomego, 720×1280 dla pionowego i około 960×960 dla materiału kwadratowego lub o mieszanej orientacji.

Ważniejsza jest zasada: nigdy nie powiększać obrazu tylko po to, by dojść do limitu.

Jeśli źródło ma 900×600, powiększenie go do 1280×720 nie odzyska żadnych szczegółów. Powstanie tylko więcej próbek, które koder musi opisać. Źródło 1920×1080 można zmniejszyć do klasy 720p, natomiast 900×600 może pozostać w okolicach 900×600. Limit jest maksimum, a nie celem.

Brzmi banalnie, ale usunięcie niepotrzebnych pikseli może dać więcej niż wiele egzotycznych ustawień kodera.

Ten limit nie jest przypadkową okrągłą liczbą. Obraz 1920×1080 zawiera 2 073 600 pikseli, a 1280×720 — 921 600. Zejście z 1080p do 720p usuwa więc około 55,6% próbek przestrzennych jeszcze zanim koder zacznie podejmować decyzje o kompresji.

Rozważałem też 540p jako ustawienie ogólne. Jednak 960×540 to tylko 518 400 pikseli: o 43,75% mniej niż 1280×720, więc zostaje 56,25% próbek 720p. W materiale ilustracyjnym te próbki opisują cienkie linie, oczy, włosy, palce, twarze i ostre kontury. Jeśli nadal potrzebuję mniej bajtów, wolę najpierw sprawdzić nieco wyższy CRF, zamiast w ciemno wyrzucać kolejne 43,75% informacji przestrzennej. Kwantyzację można zmienić przy następnym kodowaniu; szczegół usunięty przez zmniejszenie rozdzielczości już nie wróci.

Dlatego klasa 720p jest moim ostrożnym limitem ogólnym, a nie twierdzeniem, że 540p jest złe. Dla konkretnego pliku zmierzona wersja 540p może wygrać. Po prostu nie robię z takiej nieodwracalnej utraty szczegółów reguły dla całej biblioteki bez danych.

Przestałem płacić za klatki, których źródło tak naprawdę nie miało

Liczba klatek na sekundę jest kolejnym mnożnikiem. Jeśli animacja zawiera około 16 rzeczywiście użytecznych stanów obrazu na sekundę, zapisanie jej jako 30 lub 60 fps nie tworzy automatycznie lepszego ruchu. Często dodaje przede wszystkim powtórzone lub syntetyzowane próbki czasowe, które i tak trzeba zakodować.

Staram się zachować rzeczywisty rytm źródła i zwykle nie przekraczać 30 fps. W takim materiale 12, 15, 16, 18, 20, 24, 25 albo 30 fps może być rozsądne, jeśli faktycznie odpowiada źródłu.

W wygenerowanym pliku wolę też czystą stałą liczbę klatek na sekundę. VFR samo w sobie nie jest problemem; CFR po prostu upraszcza znaczniki czasu, liczenie klatek, sprawdzanie długości, przewijanie i późniejsze sprawdzanie w moim procesie.

Ogólna zasada jest ważniejsza niż konkretna wartość FPS: nie płacić transferem za informację czasową, której źródło nie zawiera.

CRF 28 to wybór dla mojego materiału, nie magiczna liczba

Nie chciałem zmuszać każdego klipu do tej samej docelowej przepływności. Prawie statyczna ilustracja i scena ze złożonym ruchem nie potrzebują tej samej liczby bitów, żeby wyglądać akceptowalnie.

Dlatego używam trybu CRF w x264 i dla tego ilustrowanego materiału, w którym priorytetem jest oszczędzanie transferu, zatrzymałem się w okolicy -crf 28. FFmpeg opisuje CRF w libx264 jako sterowanie przepływnością o stałej jakości; zobacz dokumentację kodeków FFmpeg.

Wartość CRF 28 jest celowo agresywna. Nie kopiowałbym tej wartości bez sprawdzenia do ziarna filmowego, zaszumionego materiału z kamery, bardzo drobnego tekstu na ekranie ani zastosowań, w których wierność obrazu jest ważniejsza niż transfer.

Nie mam też uniwersalnej miary percepcyjnej dowodzącej, że degradacja przy CRF 28 jest zawsze niewidoczna. Mogę powiedzieć coś węższego: w moim materiale pliki stały się znacznie mniejsze, a podczas zwykłego oglądania nadal wyglądały normalnie. To praktyczna obserwacja, nie twierdzenie o wizualnej bezstratności.

Veryslow jest kosztowne dla kodera, nie automatycznie dla dekodera

Moje ustawienie wstępne to -preset veryslow. Wolniejsze ustawienie wstępne daje x264 więcej czasu na szukanie efektywnych decyzji dotyczących predykcji i kodowania. Kosztem są czas i CPU po stronie kodera.

Najważniejsze rozróżnienie jest takie: wysiłek kodera i złożoność dekodera to nie to samo.

Mogę pozwolić x264 długo szukać dobrego rozwiązania, a jednocześnie osobno ograniczyć gotowy strumień. Moje zachowawcze wymagania dla wyjścia są następujące:

H.264, profil Main
Level 3.1
8-bitowy yuv420p
avc1
refs = 4
klatki B = 5
B-pyramid = normal
otwarty GOP = wyłączony

FFmpeg pozwala osobno ustawiać CRF, ustawienia wstępne, strojenie, ograniczenia profilu, klatki referencyjne i klatki B. Tak właśnie na to patrzę: koder może szukać bardzo dokładnie, ale strona odtwarzania ma pozostać zwyczajna.

GOP i VBV są ograniczeniami bezpieczeństwa, a nie głównym sterowaniem jakością

Dla tych krótkich progresywnych klipów używam maksymalnego GOP-u wynoszącego około pięciu sekund: mniej więcej -g 150 przy 30 fps, -g 120 przy 24 fps albo -g 80 przy 16 fps.

To wybór dla mojego zastosowania, nie uniwersalna reguła. Adaptacyjne przesyłanie strumieniowe ma inne ograniczenia; na przykład zalecenia Apple dotyczące HLS mówią o IDR co dwie sekundy. Nie przenoszę tej reguły HLS bezpośrednio na krótkie statyczne pliki MP4 odtwarzane progresywnie.

Używam również mniej więcej:

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

Te wartości są górnym limitem chroniącym przed nietypowymi skokami przepływności. Nie oznaczają „koduj wszystko z 4 Mbps”. CRF nadal odpowiada za zwykły przydział bitów, więc łatwe klipy mogą pozostać bardzo małe.

Sam kontener MP4 też zrobiłem możliwie zwyczajny

Jawnie używam avc1. Aktualna dokumentacja HLS Apple zaleca formaty próbek takie jak avc1 zamiast avc3. To nie jest powód, dla którego moje pliki stały się mniejsze, ale pasuje do celu: zwyczajny H.264 w MP4.

Używam też -movflags +faststart. Dokumentacja formatów FFmpeg mówi, że faststart przenosi indeks MP4 moov na początek pliku. Wymagania Androida dla strumieniowania przez HTTP również mówią, że w MPEG-4 moov musi znajdować się przed mdat, po ftyp.

ftyp
moov
mdat

Faststart nie poprawia kompresji. Po prostu upraszcza progresywne odtwarzanie przez HTTP.

Dla zwykłego SDR używam 8-bit yuv420p i sygnalizuję BT.709 z ograniczonym zakresem wideo. Jeśli klip nie ma dźwięku, nie tworzę sztucznie ścieżki dźwiękowej. Dla tego ilustrowanego materiału używam też -tune animation; traktuję to jako wybór zależny od rodzaju treści, a nie część uniwersalnych wymagań zgodności.

Główny profil FFmpeg

Dla ilustrowanego źródła 30 fps centralna część polecenia wygląda mniej więcej tak:

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

Skalowanie i zmiana liczby klatek na sekundę celowo nie są tu wpisane na sztywno. Źródła 900×600 nie trzeba powiększać tylko dlatego, że limit wynosi 1280×720, a animacji z naturalnie niską liczbą klatek nie trzeba wymuszać do 30 fps tylko dlatego, że przykład używa -g 150.

Polecenie jest implementacją tych zasad, a nie samymi zasadami.

Dlaczego pliki stały się kilkukrotnie mniejsze

Nie było jednej cudownej opcji.

Zmniejszenie rozmiaru wynikało z połączenia kilku decyzji, z których każda usuwała inny rodzaj marnotrawstwa: niepotrzebne piksele, niepotrzebne klatki, zbyt zachowawczy cel jakościowy, ustawienia kodera nastawione bardziej na szybkość niż wydajność, zbyt częste klatki kluczowe i strumienie, których nie potrzebowałem.

Dlatego stwierdzenie „ten plik jest H.264” mówi zaskakująco mało o jego rozmiarze. Dwa pliki H.264 zakodowane z tego samego źródła mogą znacznie się różnić, bo nazwa kodeka nie określa rozdzielczości, liczby klatek na sekundę, sposobu sterowania przepływnością, ustawienia wstępnego, struktury GOP, profilu ani przygotowania źródła.

W moim przypadku te decyzje wokół kodeka miały większe znaczenie niż zmiana samego kodeka.

Czego ten wynik nie dowodzi

Nie izolowałem każdego ustawienia w kontrolowanym eksperymencie, więc nie mogę uczciwie przypisać dokładnego procentu oszczędności osobno do veryslow, CRF 28, zmniejszenia rozdzielczości ani zmniejszenia liczby klatek na sekundę.

Nie mogę też stwierdzić, że każdy wynik CRF 28 jest percepcyjnie przezroczysty. „Brak widocznej utraty jakości” to moja obserwacja dla tego ilustrowanego materiału przy normalnych rozmiarach wyświetlania, a nie naukowa gwarancja dla dowolnego wideo.

Nie twierdzę również, że jeden plik H.264 jest właściwą architekturą dla każdej strony. Wiele wersji, adaptacyjne przesyłanie strumieniowe, HDR, 4K i negocjacja kodeka zmieniają kompromisy.

To, co naprawdę mogę stwierdzić, jest węższe: w bibliotece krótkich ilustrowanych i animowanych klipów, gdzie priorytetem jest transfer, większość odbiorców korzysta z urządzeń mobilnych, czas kodowania jest tani, a przewidywalne odtwarzanie ważne, ten profil zmniejszył moje pliki kilkukrotnie, a przy zwykłym odtwarzaniu nadal wyglądały normalnie.

Jak ten wynik zmienił moje podejście do optymalizacji

Kiedyś myślałem o optymalizacji wideo głównie jako o problemie ustawień kodera. Dziś patrzę na nią jak na koszt całego życia pliku.

Koder może uruchomić się raz. Bajty mogą przejść przez sieć tysiące lub miliony razy.

To zmienia znaczenie słowa „drogo”.

Chętnie wydam CPU jeden raz. Znacznie mniej chętnie będę przy każdym przyszłym żądaniu wysyłał piksele stworzone przez powiększanie, klatki bez dodatkowego użytecznego ruchu albo przepływność, której treść nie potrzebuje.

Kodek pozostał nudny: H.264 w MP4. Optymalizacja wydarzyła się wokół niego.

Dla mojego zastosowania najważniejsza zasada jest bardziej użyteczna niż dowolna opcja FFmpeg: optymalizuj koszt, który ponosisz wielokrotnie, a nie ten, który ponosisz raz.