Bloga dön
13 Ağustos 2026Sergei Solod7 dk okuma

Animated WebP, GIF ve APNG'yi küçük ve tarayıcı-dostu H.264 MP4'e nasıl dönüştürüyorum

Production pipeline'ım gerçekten görüntülenen frame'leri ve timing'i yeniden kuruyor, source başına CFR seçiyor, upscale yapmıyor, konservatif H.264 segmentler encode ediyor, ikinci lossy encode olmadan concat ediyor ve yayınlamadan önce exact packet timeline'ı validate ediyor.

H.264FFmpegMP4Animated WebPGIFAPNGVideo sıkıştırmaMedya pipeline

Bu pipeline'ı codec'lerle oynamak istediğim için kurmadım. Animasyonlu görsellerin, pratikte kısa ve sessiz video olan içeriği sunmanın pahalı bir yoluna dönüşmesi yüzünden kurdum.

Workload'ım çoğunlukla birkaç saniyelik animated WebP, GIF ve APNG dosyalarından oluşuyor: onlarca görüntülenen frame ve yüksek temporal redundancy. Trafik mobil ağırlıklı, aynı asset'ler tekrar tekrar sunuluyor ve encoding süresinin maliyeti, dosyanın ömrü boyunca aktarılacak byte'lardan çok daha az önemli.

“Animasyonu MP4'e çevir” basit görünüyor. Production'da öyle değil. Animated image tek FPS'li tam frame dizisi olmak zorunda değil; partial rectangle, disposal kuralları, alpha blending, düzensiz delay, sıfır duration, farklı orientation ve yanıltıcı timing metadata içerebilir.

Bu yüzden pipeline bir FFmpeg komutundan değil, invariants zincirinden oluşuyor:

animated WebP / GIF / APNG
        ↓
gerçekte görüntülenen canvas state'lerini decode edip yeniden oluştur
        ↓
source timeline'ı çıkar
        ↓
source başına CFR seç
        ↓
upscale yapmadan geometriyi normalize et
        ↓
konservatif H.264 segment encode et
        ↓
her source için bağımsız tekrarla
        ↓
uyumlu segmentleri stream copy ile concat et
        ↓
final packet timeline'ı normalize et
        ↓
probe + packet kontrolü + tam decode
        ↓
atomik yayınla

İlk kural: depolanan dikdörtgenleri değil, gösterilen frame'leri çıkar

Animated WebP, APNG ve GIF küçük frame bölgeleri ile blend/disposal kuralları kullanabilir. Depolanan frame, önceki canvas olmadan tek başına anlamlı olmayabilir.

Bu yüzden extraction birimim doğru viewer'ın gerçekten göstereceği tam composited canvas state. Yanlış compose edilen bir frame H.264'e girdikten sonra sonraki encoder ayarları onu düzeltemez.

Timing source verisidir; FPS tahmin değildir

  • Animated WebP frame duration'ı 1 ms biriminde tutar.
  • GIF delay'i saniyenin yüzde biri olarak tutar.
  • APNG numerator/denominator kullanır; denominator 0 ise 100 kabul edilir.

Gerçek bir WebP örneği 1264×720, 49 görünen frame, sırayla 62/63 ms delay ve toplam 3,063 saniyeydi: yaklaşık 16 fps. Probe ise 25 fps bildirdi.

Doğru source of truth per-frame delay'lerdi.

CFR seçmeden önce bozuk timing'i normalize ediyorum

WebP zero ve çok küçük duration değerlerinin implementation-dependent olabileceğini belirtiyor. GIF zero delay'e izin veriyor, APNG de zero numerator kullanabiliyor.

Ben millisecond precision koruyorum, minimum 10 ms uyguluyorum ve sadece anlamlı timing yoksa 100 ms fallback kullanıyorum. Bu sayılar evrensel değil; asıl nokta geçerli source timing'i default ile ezmemek.

Neden her şeyi 30 fps yapmıyorum

10, 12, 15, 16, 18, 20, 24, 25, 30 fps

Her source için gerçek timeline'ı en düşük maliyetle düzgün temsil eden CFR seçiliyor; 30 fps fallback. 62/63 ms örneği 16 fps'e doğal olarak oturuyor çünkü frame başına 62,5 ms.

Her source farklı CFR kullanabilir

source A → 12 fps CFR H.264 segment
source B → 16 fps CFR H.264 segment
source C → 24 fps CFR H.264 segment
source D → 30 fps CFR H.264 segment

segments → stream-copy concat → tek MP4

Segmentler ayrı ayrı CFR, final dosya ise parça parça CFR ve genel olarak variable-rate olabilir. Tek global FPS için ikinci encode yapmıyorum.

Neden 90 kHz time base

FPS90 kHz'te tam frame duration
109000 ticks
127500 ticks
156000 ticks
165625 ticks
185000 ticks
204500 ticks
243750 ticks
253600 ticks
303000 ticks

Tüm adaylar 90.000'i tam böler. Böylece packet duration için exact integer grid kullanabiliyorum.

Geometriyi upscale yapmadan normalize ediyorum

SınıfLimit
Landscape1280×720
Portrait720×1280
Mixed orientationyaklaşık 960×960 canvas

Küçük source büyütülmez. Farklı aspect ratio'lar stretch edilmez; ortak canvas içine fit edilir ve padding kullanılır. Background siyah, alpha bu background üzerine flattened.

H.264 MP4 neden bu workload'da genellikle daha ucuz

GIF, APNG ve animated WebP'nin kendi optimizasyonları var. Ancak H.264 inter prediction, reference pictures, P-frames ve B-frames için tasarlanmış bir video codec. Static background ve küçük hareketli bölgeleri olan illustrated clips için bu yapı çok iyi çalışıyor.

Apple static web video için H.264 MP4 öneriyor ve animated GIF yerine MP4 kullanılmasını söylüyor; Safari dokümanında GIF'in modern video codec'e göre 12 kata kadar daha fazla bandwidth ve yaklaşık iki kat enerji kullanabileceği belirtiliyor. Bu 12× benim benchmark'ım değil.

Lossy animated WebP zaten sıkıştırılmıştır; H.264'e dönüşüm ikinci lossy generation anlamına gelir. Bu yüzden görsel kalite kontrolü şart.

H.264 profili bilerek sıkıcı

AyarDeğer
ContainerMP4
CodecH.264 / libx264
Sample entryavc1
Profile / levelMain @ 3.1
Pixel format8-bit yuv420p
Rate controlCRF 28
Presetveryslow
Tuneanimation
Refs4
B-frames5
Open GOPoff
B-pyramidnormal
GOPyaklaşık 5 saniye
VBV4M / 8M
ColorBT.709 limited
Timescale90,000
Audio/subtitles/datayok

Main@3.1 modern cihazların sınırı değil, konservatif single-file envelope. Android 6.0'dan itibaren Main Profile decode zorunlu ve 1280×720@30 fps H.264 HD önerisi olarak veriliyor. Apple static web video için H.264 MP4 öneriyor.

CPU'yu bir kez harcıyorum

-preset veryslow

Encode bir kez, delivery çok kez olur. Bu yüzden x264 CPU harcayabilir. CRF 28 de illustrated ve bandwidth-first workload'ıma özeldir.

Her animasyon bağımsız bir segment olur

ffmpeg -framerate "$SELECTED_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

Frame timeline ve geometry daha önce hazırlanır. GOP yaklaşık selectedFps * 5.

Concat sıkı segment contract sayesinde çalışır

FFmpeg concat uyumlu stream'ler ister ve bir dosyanın duration'ını sonraki dosyanın başlangıcı için kullanır. Bu nedenle uyumluluk concat sırasında değil önce sağlanır.

ffmpeg -f concat -safe 0 -i segments.ffconcat \
  -c:v copy -an -sn -dn -movflags +faststart final.mp4

İkinci lossy encode yok.

Stream copy tek başına perfect timestamp demek değil

90000 / 24 = 3750 ticks

Validator gerçek bir output'ta 3751 tick packet buldu. Görsel olarak önemsiz olabilir, fakat exact grid invariant'ını bozuyordu.

Final concat artık setts ile PTS, DTS ve duration'ı bilinen 90-kHz grid'e normalize ediyor ve -c:v copy korunuyor.

Validation encoder'ın parçasıdır

Stream, profile/level, pixel format, dimensions, time base, frame/packet count, exact duration, PTS/DTS, final duration ve full decode kontrol edilir.

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 -

Pipeline'ın önlediği hatalar

  • Visible canvas yerine raw subframe encode etmek.
  • FPS tahminine güvenmek.
  • Her şeyi 30 fps yapmak.
  • Upscale.
  • Aspect ratio stretch.
  • Concat sonrası re-encode.
  • -c copy'yi doğru timing sanmak.
  • Full decode yapmamak.

Bilerek korumadığım şeyler

Alpha flattened, arbitrary VFR source başına CFR ile yaklaşık temsil edilir, yüksek resolution küçültülür, lossy WebP başka bir lossy generation alır ve audio yoktur. Bunlar gizli değil, design contract'ın parçasıdır.

Bu yaklaşımı ne zaman kullanmam

Transparency korunmalıysa, exact irregular timing kritikse, archival master ise veya zaten adaptive multi-codec stack varsa bu exact pipeline'ı kullanmam. Zaten çok küçük bir animated WebP için de önce benchmark yaparım.

Bugünkü pratik pipeline

  1. Animation metadata oku.
  2. Tam visible canvas state'lerini reconstruct et.
  3. Delay'leri sanitize edip ms timeline kur.
  4. 10/12/15/16/18/20/24/25/30 arasından source CFR seç.
  5. No-upscale geometry, padding, alpha flatten.
  6. H.264 Main@3.1/yuv420p/avc1, veryslow, CRF 28.
  7. 90-kHz time base.
  8. Her segmenti validate et.
  9. -c:v copy concat.
  10. Final timeline normalize.
  11. Packet inspection ve full decode.
  12. Sonra publish.

Asıl ders: asset timeline'dır

Animated WebP, GIF ve APNG sadece “çok sayıda resim” değildir; zamanlanmış canvas state dizileridir. Bunu source of truth kabul ettiğimde hareket icat etmeden cadence'i, piksel icat etmeden resolution'ı düşürebildim, source başına CFR kullanabildim ve ek lossy generation olmadan concat yapabildim.

H.264 dosyaları küçülttü, MP4 delivery'yi kolaylaştırdı. Codec kolay kısımdı; gerçek mühendislik animasyonun anlamını representation değişirken korumaktı.

Birincil dokümantasyon