Efekt wyglądał początkowo jak coś, co zwykle wymaga nowszego kodeka: moje pliki wideo stały się kilkukrotnie mniejsze, a zwykłe odtwarzanie nadal wyglądało normalnie i przy typowych rozmiarach obrazu nie widziałem wyraźnej utraty jakości.
Nie przeszedłem jednak na AV1, HEVC ani VP9. Nadal używałem H.264 w kontenerze MP4.
Zmieniło się wszystko wokół kodeka. Przebudowałem politykę kodowania pod konkretny workload: krótkie ilustrowane i animowane klipy, duży udział użytkowników mobilnych, transfer jako główny koszt powtarzalny i praktycznie brak presji na czas jednorazowego kodowania offline.
Mój baseline stał się więc celowo konserwatywny po stronie playbacku i celowo kosztowny po stronie encodera: H.264 Main Profile @ Level 3.1, avc1, 8-bit yuv420p, CRF 28, x264 veryslow, limit rozdzielczości około klasy 720p, tylko użyteczny frame rate zwykle do 30 fps, ograniczone reference frames i B-frames oraz faststart dla progresywnego MP4.
Najważniejsza optymalizacja nie była flagą FFmpeg
Największa zmiana dotyczyła sposobu, w jaki liczyłem koszt.
Kodowanie odbywa się raz. Transfer pliku odbywa się przy każdym odtworzeniu.
W wideo real-time wydawanie dużo większej ilości CPU, żeby oszczędzić trochę bitrate, może być złym kompromisem. Moje pliki są kodowane offline, a potem serwowane wielokrotnie. W takim modelu oszczędzenie dziesięciu minut przy encode może być ekonomicznie bez znaczenia, jeśli szybsze kodowanie powiększa każdy przyszły request.
Dlatego -preset veryslow ma dla mnie sens. Mogę raz zapłacić CPU, jeśli x264 wykorzysta je do znalezienia bardziej efektywnej reprezentacji. Przeglądarka nie powtarza kosztownego wyszukiwania encodera; tylko dekoduje gotowy bitstream.
Zasada stała się prosta: wydawać moc obliczeniową na etap wykonywany raz i oszczędzać bajty na etapie powtarzanym.
Dlaczego zostałem przy H.264 zamiast gonić za najnowszym kodekiem
Nie twierdzę, że H.264 daje najlepszą dostępną kompresję. Nie daje. Nowsze kodeki są atrakcyjne, gdy system delivery może utrzymywać kilka renditions i dobierać najlepszą dla klienta.
Moje ograniczenie było inne: jeden URL, jeden plik, jeden kodek i jak najmniej problemów z playbackiem dla mobile-heavy publiczności.
Do tego zadania H.264 w MP4 pozostaje bardzo bezpiecznym baseline. Apple obecnie zaleca webdeveloperom używanie plików MP4 kodowanych H.264 dla statycznego wideo w Safari. Aktualna dokumentacja Androida wymienia H.264 w MP4 i wymaga dekodera Main Profile od Androida 6.0; rekomendacje playbacku zawierają też 1280×720 przy 30 fps jako konfigurację H.264 HD. Zobacz obsługiwane formaty multimedialne Androida.
Nie oznacza to, że współczesne urządzenia są ograniczone do Main Profile albo Level 3.1. Apple w HLS na przykład zwykle preferuje High Profile zamiast Main czy Baseline. Main@3.1 wybrałem dlatego, że chciałem celowo umiarkowane wymagania dekodera dla pojedynczego statycznego MP4, a nie dlatego, że Apple tego wymaga.
Przestałem kodować piksele, których nie potrzebowałem
Rozdzielczość była jedną z największych dźwigni. Mój limit stał się mniej więcej 1280×720 dla poziomego obrazu, 720×1280 dla pionowego oraz około 960×960 dla materiału kwadratowego lub mieszanego.
Ważniejsza zasada brzmi: nigdy nie robić upscale tylko po to, żeby osiągnąć ten limit.
Jeśli źródło ma 900×600, zwiększenie go do 1280×720 nie odzyskuje detali. Tworzy tylko więcej próbek do opisania przez encoder. Źródło 1920×1080 można zmniejszyć do klasy 720p, natomiast 900×600 może pozostać około 900×600. Limit jest maksimum, a nie celem.
Brzmi prosto, ale usuwanie niepotrzebnych pikseli może dać więcej niż wiele egzotycznych tweaków encodera.
Przestałem płacić za klatki, których źródło właściwie nie miało
Frame rate jest kolejnym mnożnikiem. Jeśli animacja ma około 16 rzeczywiście użytecznych stanów wizualnych na sekundę, przechowywanie jej jako 30 lub 60 fps nie tworzy automatycznie lepszego ruchu. Często dodaje głównie powtórzone lub syntetyzowane próbki czasowe, które i tak trzeba zakodować.
Moja polityka to zachowanie użytecznej kadencji źródła i zwykle nieprzekraczanie 30 fps. Dla tego rodzaju materiału 12, 15, 16, 18, 20, 24, 25 lub 30 fps może mieć sens, jeśli faktycznie opisuje źródło.
W generowanym output preferuję też czysty CFR. VFR nie jest z natury uszkodzony; CFR po prostu upraszcza timestamps, liczbę klatek, sprawdzanie czasu trwania, seeking i późniejszą walidację w moim pipeline.
Ogólna zasada jest ważniejsza niż konkretne FPS: nie płacić transferem za informację czasową, której nie ma w źródle.
CRF 28 to decyzja dla workloadu, a 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 bandwidth-first ilustrowanego workloadu zatrzymałem się w okolicy -crf 28. FFmpeg opisuje CRF w libx264 jako rate control o stałej jakości; zobacz dokumentację kodeków FFmpeg.
CRF 28 jest celowo agresywne. Nie kopiowałbym tej wartości bezmyślnie do film grain, zaszumionego obrazu z kamery, drobnego tekstu w screen recording albo workloadu, w którym fidelity jest ważniejsza niż bandwidth.
Nie mam też uniwersalnej metryki percepcyjnej dowodzącej, że CRF 28 jest transparentne. Mogę powiedzieć coś węższego: dla mojego materiału pliki stały się dużo mniejsze, a podczas zwykłego oglądania nadal wyglądały normalnie. To obserwacja praktyczna, nie twierdzenie o wizualnej bezstratności.
Veryslow jest drogie dla encodera, nie automatycznie dla dekodera
Mój preset to -preset veryslow. Wolniejszy preset daje x264 więcej możliwości szukania efektywnych decyzji prediction i coding. Kosztem są CPU oraz czas encodera.
Kluczowe rozróżnienie: wysiłek encodera i złożoność dekodera to nie to samo.
Mogę pozwolić x264 długo pracować i osobno ograniczyć gotowy stream. Mój konserwatywny kontrakt wyjściowy to:
H.264 Main Profile
Level 3.1
8-bit yuv420p
avc1
refs = 4
B-frames = 5
B-pyramid = normal
open GOP = disabled
FFmpeg udostępnia osobno CRF, presety, tuning, ograniczenia profilu, reference frames i B-frames. Tak właśnie je traktuję: encoder może szukać bardzo dokładnie, ale strona playbacku powinna pozostać zwyczajna.
GOP i VBV są barierami bezpieczeństwa, nie głównym sterowaniem jakością
Dla tych krótkich progresywnych klipów używam maksymalnego GOP 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 workloadu, a nie uniwersalna reguła. Adaptive streaming ma inne ograniczenia; Apple w HLS rekomenduje na przykład IDR co dwie sekundy. Nie przenoszę tej reguły HLS automatycznie na krótkie statyczne progresywne MP4.
Używam również około:
-maxrate:v 4M
-bufsize:v 8M
Te wartości są limitami przeciw nietypowym skokom bitrate. Nie oznaczają „koduj wszystko w 4 Mbps”. CRF nadal steruje normalnym przydziałem bitów, więc łatwe klipy mogą być bardzo małe.
Sam kontener MP4 też zrobiłem możliwie nudny
Jawnie używam avc1. Aktualna dokumentacja HLS Apple zaleca sample formats takie jak avc1 zamiast avc3. To nie zmniejszyło moich plików, ale pasuje do celu tworzenia zwyczajnego H.264-in-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 HTTP streaming również mówią, że w MPEG-4 moov musi poprzedzać mdat po ftyp.
ftyp
moov
mdat
Faststart nie poprawia kompresji. Ułatwia progresywne odtwarzanie przez HTTP.
Dla zwykłego SDR używam 8-bit yuv420p i sygnalizuję BT.709 z limited/video range. Jeśli klip nie potrzebuje audio, nie tworzę ścieżki audio. Dla ilustrowanego contentu używam również -tune animation; traktuję to jako wybór specyficzny dla contentu, a nie część uniwersalnego kontraktu kompatybilności.
Główny profil FFmpeg
Dla ilustrowanego źródła 30 fps centralna część komendy 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
Scaling i frame rate celowo nie są tu zahardkodowane. Źródła 900×600 nie trzeba powiększać tylko dlatego, że limit to 1280×720, a animacji o naturalnie niskim FPS nie trzeba wymuszać do 30 fps tylko dlatego, że przykład używa -g 150.
Komenda implementuje politykę; nie jest samą polityką.
Dlaczego pliki stały się kilkukrotnie mniejsze
Nie było jednej cudownej flagi.
Redukcja wynikła z nałożenia kilku decyzji usuwających różne rodzaje marnotrawstwa: niepotrzebne piksele, niepotrzebne klatki, myślenie stałym bitrate, tanie ustawienia encodera, zbyt częste keyframes i streamy, których nie potrzebowałem.
Dlatego zdanie „ten plik jest H.264” mówi zaskakująco mało o jego rozmiarze. Dwa encodes H.264 tego samego źródła mogą różnić się ogromnie, bo nazwa kodeka nie opisuje rozdzielczości, FPS, rate control, presetu, 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 czy zmniejszenia FPS.
Nie mogę też stwierdzić, że każdy output CRF 28 jest percepcyjnie transparentny. „Brak widocznej utraty jakości” to moja obserwacja dla tego ilustrowanego workloadu 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. Multiple renditions, adaptive streaming, HDR, 4K i codec negotiation zmieniają trade-offs.
Wniosek jest węższy i bardziej użyteczny: dla bandwidth-first, mobile-heavy biblioteki krótkich ilustrowanych i animowanych klipów, gdzie czas kodowania jest tani, a przewidywalny playback ważny, ten profil zmniejszył moje pliki kilkukrotnie, zachowując normalny wygląd przy zwykłym odtwarzaniu.
Zasada, której używam teraz
Kiedyś myślałem o optymalizacji wideo głównie jako o problemie ustawień encodera. Dziś myślę o niej jako o koszcie całego życia pliku.
Encoder 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 request wysyłał piksele stworzone przez upscale, klatki bez dodatkowego użytecznego ruchu czy bitrate, którego content nie potrzebuje.
Kodek pozostał nudny: H.264 w MP4. Optymalizacja wydarzyła się wokół niego.
Dla mojego workloadu lekcja jest bardziej użyteczna niż dowolna flaga FFmpeg: optymalizuj koszt, który ponosisz wielokrotnie, a nie ten, który ponosisz raz.