Saya tidak membuat alur ini untuk bereksperimen dengan kodek. Saya membuatnya karena gambar animasi sudah menjadi cara mahal untuk mengirim sesuatu yang pada praktiknya berperilaku seperti video pendek tanpa suara.
Materi saya terutama WebP animasi, GIF, dan APNG pendek: biasanya hanya beberapa detik, sering kali beberapa puluh bingkai yang benar-benar tampil, dengan banyak kemiripan antarwaktu. Sebagian besar tontonan berasal dari perangkat seluler, berkas yang sama bisa diminta berulang kali, dan biaya enkode sekali jauh kurang penting daripada byte yang terus dikirim pada setiap penayangan.
Bagian sulitnya bukan menjalankan FFmpeg. Gambar animasi tidak selalu berupa urutan rapi gambar penuh pada satu laju bingkai tetap. Isinya dapat berupa persegi panjang sebagian, aturan pencampuran dan pembersihan, kanal alfa, jeda tidak teratur, bingkai berdurasi nol, orientasi berbeda, serta metadata waktu yang dapat diringkas secara menyesatkan oleh alat pemeriksa umum.
Karena itu saya memperlakukan konversi sebagai kumpulan syarat tetap, bukan satu perintah:
WebP animasi / GIF / APNG
↓
merekonstruksi keadaan kanvas lengkap yang benar-benar tampil
↓
memulihkan dan menormalkan waktu sumber
↓
menganalisis semua sumber dalam urutan akhir
↓
memilih satu CFR untuk MP4 akhir
↓
menghitung kanvas bersama terkecil tanpa memperbesar
↓
mengode segmen H.264 yang kompatibel
↓
memeriksa kontrak aliran
↓
menggabungkan dengan menyalin aliran
↓
menormalkan dan memvalidasi garis waktu paket
↓
memvalidasi pengiriman HTTP
↓
mempublikasikan secara atomik
Kodek tetap penting, tetapi mempertahankan apa yang benar-benar ditampilkan animasi jauh lebih penting.
Hasil produksi yang terukur: 217 WebP animasi menjadi satu MP4 78,49 MB
Masukannya bukan satu video 1,49 GB. Masukannya adalah 217 berkas WebP animasi terpisah dengan 10.633 bingkai yang ditampilkan. Total ukuran semua animasi sumber sekitar 1,49 GB.
MASUKAN
217 berkas WebP animasi
total 1,49 GB
10.633 bingkai yang ditampilkan
KELUARAN
1 H.264 MP4
78,49 MB
0,98 Mbit/dtk
1264×720
30 fps CFR
Main@3.1 / CRF 28 / veryslow
Berkas H.264 hasil akhirnya berukuran 78,49 MB dengan bitrate rata-rata sekitar 0,98 Mbit/dtk. Dibandingkan total byte animasi sumber, hasilnya sekitar 19 kali lebih kecil, atau kira-kira 94,7% lebih sedikit data.
Ini hasil nyata dari keseluruhan alur, bukan uji A/B bersih “H.264 lama melawan H.264 baru”. Representasinya berubah dari ratusan berkas gambar animasi menjadi satu video yang dikompresi secara temporal, jadi saya tidak menganggap seluruh pengurangan 19× berasal dari CRF 28, veryslow, atau satu opsi pengode saja.
Pertama, saya merekonstruksi gambar yang benar-benar dilihat pengguna
Jalan pintas paling berbahaya adalah menganggap setiap bingkai yang tersimpan merupakan gambar lengkap yang menggantikan bingkai sebelumnya.
Bingkai WebP animasi dapat mendeskripsikan persegi panjang pada posisi tertentu beserta perilaku pencampuran dan pembersihan. APNG memiliki offset, dimensi, durasi, serta operasi pencampuran dan pembersihan. GIF juga dapat mempertahankan kanvas sebelumnya, membersihkan suatu area, atau mengembalikan keadaan sebelumnya.
Karena itu bingkai yang tersimpan bisa saja hanya patch kecil yang bergantung pada kanvas yang sudah dibangun oleh bingkai sebelumnya. Mengenkode patch tersebut sebagai gambar penuh menghasilkan animasi yang salah, bukan versi lebih kecil dari animasi yang benar.
Batas ekstraksi saya adalah keadaan kanvas lengkap yang tampil: piksel hasil komposisi penuh yang akan ditunjukkan pemutar yang benar setelah menerapkan pembersihan bingkai sebelumnya dan pencampuran bingkai saat ini.
Ini adalah jaminan kebenaran pertama dalam seluruh alur. Setelah patch yang direkonstruksi secara salah diratakan ke H.264, CRF, prasetel, atau opsi kontainer apa pun tidak dapat memperbaikinya.
Durasi bingkai adalah data sumber, bukan angka FPS untuk ditebak
Format animasi menyimpan waktu dengan cara berbeda. WebP animasi memakai durasi per bingkai dalam satuan 1 ms. GIF menyimpan jeda dalam seperseratus detik. APNG memakai pembilang dan penyebut untuk setiap jeda; bila penyebut nol, spesifikasi PNG menyatakan nilainya diperlakukan sebagai 100.
Jeda-jeda itulah garis waktu sebenarnya. Nilai FPS yang ditampilkan alat umum hanyalah ringkasan dan bisa menyesatkan.
Satu WebP nyata dalam alur saya berukuran 1264×720 dengan 49 bingkai yang ditampilkan. Jeda bergantian antara 62 dan 63 ms, dengan durasi total 3,063 detik. Secara praktis ini adalah ritme 16 fps, karena satu bingkai pada 16 fps berlangsung 62,5 ms.
Alat pemeriksa umum justru melaporkan 25 fps untuk sumber itu. Jika saya mempercayainya, pengaturan waktu sumber akan berubah atau akan muncul bingkai pengulangan yang tidak perlu.
Saya juga memerlukan aturan eksplisit untuk jeda yang tidak valid atau ambigu. WebP menyerahkan penafsiran durasi nol dan sering kali nilai yang sangat kecil kepada implementasi. GIF dapat memiliki jeda nol. APNG mengizinkan pembilang nol, yang berarti bingkai berikutnya seharusnya dirender secepat mungkin, walaupun pemutar dapat menerapkan batas minimum yang masuk akal.
Kebijakan normalisasi saya mempertahankan ketelitian milidetik, memakai minimum kecil 10 ms untuk durasi nol atau terlalu kecil secara tidak masuk akal, dan menggunakan 100 ms hanya sebagai cadangan saat informasi pengaturan waktu yang berguna memang tidak tersedia. Itu kebijakan produksi, bukan standar universal.
Saya memilih satu CFR untuk seluruh MP4 akhir, bukan memaksa 30 fps
Setelah keadaan yang tampil dan durasinya direkonstruksi, saya memetakan garis waktu sumber ke garis waktu video. Saya tidak mengenkode semuanya secara membabi buta pada 30 fps.
Untuk semua sumber yang akan masuk ke MP4 akhir yang sama, saya mengevaluasi kumpulan kecil berikut:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
Pemilih mengambil CFR terendah yang masih merepresentasikan seluruh urutan akhir dengan baik. MP4 akhir yang berbeda boleh memilih laju berbeda, tetapi semua segmen yang dienkode terpisah di dalam satu MP4 menggunakan CFR yang sama persis.
Contoh 3,063 detik membuat penghematan mudah terlihat: pada 16 fps dibutuhkan sekitar 49 bingkai keluaran; pada 30 fps sekitar 92 bingkai. Jika sumber lain dalam MP4 yang sama memang membutuhkan 30 fps, seluruh koleksi memakai 30. Saya tidak mencampur laju bingkai berbeda di satu aliran akhir.
Untuk trek video saya memakai skala waktu 90.000 Hz, karena setiap CFR yang diizinkan menghasilkan durasi bingkai berupa bilangan bulat:
10 fps → 9000 tick
12 fps → 7500 tick
15 fps → 6000 tick
16 fps → 5625 tick
18 fps → 5000 tick
20 fps → 4500 tick
24 fps → 3750 tick
25 fps → 3600 tick
30 fps → 3000 tick
Skala waktu trek video inilah yang membentuk kisi bingkai tepat tersebut. Skala waktu umum MP4 juga saya set ke 90.000 demi konsistensi, tetapi itu adalah jam kontainer yang terpisah. Pemvalidasi memeriksa paket video terhadap kisi bilangan bulat yang pasti, bukan bergantung pada durasi desimal yang dibulatkan.
Batas resolusi adalah plafon, bukan kanvas wajib
Batas pengiriman saya kira-kira 1280×720 untuk lanskap, 720×1280 untuk potret, dan maksimum 960 piksel baik lebar maupun tinggi untuk urutan dengan orientasi campuran.
Aturan yang tidak dapat ditawar adalah jangan pernah memperbesar. Sumber 900×600 tidak memperoleh detail baru ketika dijadikan 1280×720; hanya tercipta piksel hasil interpolasi yang harus dijelaskan pengode.
Aturan kedua kurang jelas: 960×960 adalah batas maksimum, bukan kanvas persegi yang wajib.
Pertama saya hitung dimensi aktif setiap sumber dengan hanya mengizinkan pengecilan. Setelah itu urutan akhir mendapat kanvas bersama terkecil dengan dimensi genap yang mampu menampung semua persegi panjang aktif yang sudah diperkecil.
Contohnya, jika urutan akhir membutuhkan gambar lanskap 960×540 dan potret 500×900, kanvas bersama dapat berupa 960×900, bukan 960×960. Semua segmen tetap memiliki dimensi terkode yang sama sehingga penggabungan dengan penyalinan aliran tetap aman, tanpa membuang bit untuk area hitam yang tidak diperlukan.
Rasio aspek tetap dijaga dan ruang kosong diisi, bukan gambar diregangkan. Dalam alur saya, latarnya hitam. Karena H.264/yuv420p biasa tidak menyimpan kanal alfa sumber, transparansi sengaja dikomposisikan ke latar tersebut.
Mengapa H.264 MP4 cocok untuk masalah pengiriman ini
GIF, APNG, dan WebP animasi bukan format primitif. Mereka juga dapat menghindari menggambar ulang area yang tidak berubah, jadi pernyataan “video selalu lebih kecil” tidak benar.
Namun H.264 dirancang untuk prediksi temporal antargambar. Perulangan ilustrasi pendek dengan latar statis dan hanya wilayah kecil yang berubah merupakan kasus yang cocok untuk gambar acuan, prediksi antarbingkai, serta bingkai P dan B.
Dokumentasi Safari Apple saat ini merekomendasikan MP4 yang dienkode H.264 untuk video statis dan menyebut GIF animasi dapat menggunakan bandwidth hingga dua belas kali lebih besar serta energi sekitar dua kali lebih banyak daripada kodek video modern. Angka 12× itu contoh dari Apple, bukan hasil pengukuran saya.
Hasil 19× yang saya ukur adalah observasi produksi yang berguna, tetapi saya tetap membandingkan berkas nyata daripada menganggap setiap WebP animasi yang sudah kecil pasti kalah dari MP4.
Setiap animasi dienkode sebagai segmen di bawah satu kontrak aliran
Saat pengode berjalan, alur sudah mengetahui bingkai yang tampil, CFR bersama, kanvas akhir, dan durasi yang diharapkan.
Bagian inti perintah segmen saya kira-kira seperti ini:
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
Batas GOP sekitar lima detik dan diturunkan dari CFR terpilih: 80 bingkai pada 16 fps, 120 pada 24 fps, dan 150 pada 30 fps.
Pengaman -t "$EXPECTED_DURATION" bukan hiasan. Dalam daftar bingkai saya, gambar terakhir diulang sebagai penanda akhir agar durasi bingkai nyata sebelumnya diterapkan. Tanpa batas durasi eksplisit, penanda itu bisa menjadi bingkai tambahan di ujung.
Saya mereproduksi ini pada kasus 49 bingkai dan 3,063 detik. Tanpa -t, hasilnya 50 bingkai pada 16 fps dan 94 pada 30 fps. Dengan -t 3.063, hasilnya sesuai harapan: 49 dan 92 bingkai.
Penggabungan dengan aliran salin hanya aman setelah pemeriksaan kompatibilitas yang ketat
Demultiplekser penggabungan FFmpeg mengharapkan aliran yang sama, termasuk kodek dan dasar waktu, lalu memakai durasi setiap berkas untuk menempatkan berkas berikutnya. Durasi yang salah dapat menyebabkan artefak garis waktu.
Saya tidak memakai tahap penggabungan untuk membuat berkas yang tidak kompatibel menjadi kompatibel. Sebelum diterima, setiap segmen harus sudah memenuhi kontrak:
CFR seluruh koleksi = identik
dasar waktu aliran = identik
skala waktu trek video MP4 = identik
dimensi kanvas / SAR = identik
profil / tingkat / format piksel = identik
pensinyalan warna = identik
avcC / data tambahan AVC = identik byte demi byte
Saya memakai stitchable=1 dari x264 karena segmen dienkode secara independen, tetapi saya tidak menganggap opsi itu sebagai bukti konfigurasi AVC benar-benar sama. Sebelum penggabungan, byte konfigurasi aktual tetap dibandingkan.
Jika kontrak terpenuhi, penggabungan akhir tidak membutuhkan kompresi kedua:
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy \
-an -sn -dn \
-movflags +faststart \
final.mp4
-c:v copy menghindari dekode dan kompresi ulang terhadap segmen H.264 yang sudah selesai.
Validasi mencakup berkas MP4 dan cara berkas itu dikirim
Saya tidak mempublikasikan berkas hanya karena FFmpeg selesai dengan kode 0.
Pemvalidasi pernah menemukan kesalahan waktu deterministik nyata: pada 16 fps saya melihat 5580 tick ketika kontrak menuntut 5625; kemudian keluaran 24 fps berisi 3751 padahal kisi yang tepat menuntut 3750. Investigasi mendalam tentang selisih satu tick adalah topik lain; pelajaran di sini adalah mengulangi proses yang sama tidak memperbaiki kesalahan garis waktu deterministik.
Pada objek media saya memeriksa jumlah aliran, profil dan tingkat H.264, format piksel, dimensi tepat yang direncanakan, SAR, sinyal warna, skala waktu trek video 90 kHz, durasi paket, jumlah bingkai dan paket, durasi total, hubungan PTS/DTS, konfigurasi AVC identik antarsegmen, moov sebelum mdat, serta dekode penuh dengan penanganan kesalahan yang ketat.
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 -
Tetapi MP4 lokal yang benar masih bisa dikirim secara salah lewat jaringan. Karena itu saya juga memeriksa jalur HTTP: Content-Type yang diharapkan, Content-Length yang benar, dukungan rentang byte, respons 206 Partial Content yang valid, serta Content-Range yang benar.
Saat mengubah kontrak pengode, saya juga melakukan uji kecil pada perangkat dan peramban nyata, bukan menganggap ffprobe membuktikan kompatibilitas perangkat keras. Saya menguji mulai, pencarian posisi, perulangan, pindah ke latar belakang dan kembali, serta pemutaran rentang pada iPhone/Safari terkini, perangkat Android kelas bawah, dan peramban komputer utama.
Baru setelah berkas dan jalur pengirimannya memenuhi kontrak, aset produksi diganti secara atomik.
Bagian yang sengaja kehilangan informasi
Ini transformasi untuk distribusi, bukan master arsip. Alfa diratakan. Pengaturan waktu sumber yang tidak teratur dikuantisasi ke satu CFR untuk MP4 akhir. Sumber besar dapat diperkecil. WebP animasi yang sudah dengan kehilangan data menerima satu generasi dengan kehilangan data tambahan. Audio sengaja tidak disertakan.
Saya tidak akan memakai alur yang sama bila transparansi harus tetap dapat dikomposisikan di atas latar apa pun, bila pengaturan waktu per bingkai yang tidak teratur secara tepat memiliki makna, bila saya sedang membuat sumber arsip, atau bila aplikasi sudah memiliki sistem video adaptif multikodek yang memecahkan masalah distribusi dengan cara lain.
WebP animasi yang sangat kecil dan sudah sangat teroptimasi juga saya uji terlebih dahulu; saya tidak berasumsi MP4 pasti lebih kecil.
Urutan produksi yang saya gunakan sekarang
- Mendeteksi format animasi dan membaca metadata kontrol bingkai sebenarnya.
- Merekonstruksi keadaan kanvas lengkap sesuai aturan pencampuran dan pembersihan format.
- Memulihkan dan menormalkan durasi setiap bingkai.
- Membangun garis waktu sumber acuan dengan presisi milidetik.
- Menganalisis semua sumber yang akan masuk ke MP4 akhir yang sama.
- Memilih satu CFR bersama dari 10/12/15/16/18/20/24/25/30.
- Memetakan keadaan yang tampil ke garis waktu CFR tersebut.
- Menghitung dimensi aktif hanya dengan pengecilan; tidak pernah memperbesar.
- Membangun kanvas bersama terkecil yang diperlukan dengan dimensi genap.
- Mengisi tanpa meregangkan dan mengomposisikan alfa secara sengaja.
- Mengenkode setiap sumber sebagai H.264 Main@3.1 / yuv420p / avc1 dengan kontrak aliran yang sama.
- Membatasi setiap segmen pada durasi yang diharapkan.
- Menolak segmen bila konfigurasi AVC aktual atau pengaturan waktu melanggar kontrak.
- Menggabungkan segmen yang diterima dengan
-c:v copy. - Menormalkan dan memvalidasi garis waktu paket akhir.
- Mendekode hasil sepenuhnya.
- Memeriksa header HTTP, rentang byte, dan respons sebagian.
- Setelah perubahan profil pengode, menjalankan uji perangkat dan peramban.
- Mempublikasikan secara atomik hanya setelah semua pemeriksaan berhasil.
Sumber kebenaran adalah garis waktu, bukan ekstensi berkas
WebP animasi, GIF, atau APNG adalah urutan keadaan kanvas yang ditampilkan dengan pengaturan waktu tertentu, bukan sekadar folder gambar dengan suatu ekstensi.
H.264 dapat memanfaatkan redundansi temporal dengan sangat baik, tetapi tidak dapat memperbaiki komposisi yang salah, pengaturan waktu yang dibuat-buat, atau metadata segmen yang tidak kompatibel. Banyak pekerjaan teknis yang membuat alur ini dapat diandalkan terjadi sebelum dan sesudah x264.
Aturan yang saya pegang: ubah representasi hanya setelah dapat menjelaskan dengan tepat syarat apa saja yang harus tetap tidak berubah.
Dokumentasi utama
- Spesifikasi kontainer WebP dari Google — persegi panjang bingkai, durasi, pencampuran, dan pembersihan.
- Spesifikasi PNG W3C, edisi ketiga — pengaturan waktu APNG, offset, pencampuran, dan pembersihan.
- Spesifikasi GIF89a — jeda dan perilaku pembersihan.
- Dokumentasi format FFmpeg — persyaratan penggabungan dan perilaku MP4.
- Dokumentasi filter aliran bit FFmpeg —
setts. - Dokumentasi ffprobe — pemeriksaan aliran dan paket.
- Apple: mengirim konten video untuk Safari — H.264 MP4 untuk video statis dan penggantian GIF animasi.
- Format media yang didukung Android — dukungan H.264 dan persyaratan pengaliran HTTP.