मैंने यह pipeline किसी नए codec के साथ प्रयोग करने के लिए नहीं बनाई। असली समस्या delivery cost थी: Animated WebP, GIF और APNG अक्सर व्यवहार में कुछ सेकंड के silent video की तरह इस्तेमाल हो रहे थे, लेकिन एक ही asset को बार-बार web पर भेजने के लिए वे हमेशा सबसे किफायती representation नहीं थे।
मेरे workload में animation आम तौर पर छोटे होते हैं, कुछ सेकंड लंबे, कुछ दर्जन displayed frames वाले, और लगातार frames के बीच काफी temporal redundancy होती है। Encoding एक बार होती है; delivery बार-बार। इसलिए encode speed की तुलना में final file size और predictable browser playback ज्यादा महत्वपूर्ण हैं।
“Animation को MP4 में convert कर दो” सुनने में आसान है। Production version इतना सरल नहीं है। Animated image जरूरी नहीं कि साफ-सुथरे full-size frames की fixed-FPS sequence हो। उसमें partial rectangles, blend/disposal rules, alpha, irregular delays, zero-duration frames और misleading probe metadata हो सकते हैं।
आखिर में मेरा pipeline इस तरह बना:
animated WebP / GIF / APNG
↓
decode + reconstruct displayed canvas states
↓
recover source timeline
↓
select per-source CFR
↓
resize / pad without upscaling
↓
encode one H.264 segment
↓
repeat independently for every source
↓
concat with stream copy
↓
normalize final packet timeline
↓
ffprobe + packet checks + full decode
↓
atomic publish
Codec इस समस्या का केवल एक हिस्सा है। असली engineering यह है कि representation बदलते समय animation का अर्थ न बदले।
पहला नियम: stored rectangle नहीं, वास्तव में दिखा हुआ canvas निकालो
सबसे खतरनाक shortcut यह मानना है कि animated image के अंदर हर stored frame पूरी तस्वीर है जो पिछले frame को replace करता है।
Animated WebP में frame position, rectangle size, blend mode और disposal mode हो सकते हैं। APNG में offsets, dimensions, dispose और blend operations होते हैं। GIF में Graphic Control Extension disposal behavior बताता है।
इसलिए stored frame केवल एक छोटा patch हो सकता है जो पिछले canvas state पर निर्भर करता है। ऐसे patch को full frame मानकर encode करने से छोटी animation नहीं, गलत animation बनती है।
मेरी extraction boundary displayed canvas state है: वह पूरी composited image जो सही decoder पिछले disposal और वर्तमान blending के बाद user को दिखाता।
यह पूरी pipeline की पहली correctness guarantee है। यहाँ pixels गलत हो गए तो बाद में कोई CRF या H.264 option उन्हें ठीक नहीं कर सकता।
Frame timing source data है, guessed FPS नहीं
- Animated WebP हर frame का duration 1 ms units में रखता है।
- GIF delay को second के hundredths में रखता है।
- APNG हर frame के लिए delay numerator/denominator रखता है; denominator 0 होने पर PNG specification उसे 100 मानती है।
यही per-frame delays असली timeline हैं। Average FPS केवल summary है।
एक वास्तविक WebP 1264×720 था, उसमें 49 displayed frames थे, delays 62 और 63 ms के बीच बदल रहे थे, और total duration 3.063 s था। यह लगभग 16 fps cadence है क्योंकि 16 fps पर एक frame 62.5 ms होता है।
एक probe ने उसी source को 25 fps बताया। अगर मैं उस नंबर पर भरोसा करता और 25 या 30 fps में encode करता, तो source timing बदल सकती थी या unnecessary duplicate frames जुड़ जाते।
CFR चुनने से पहले खराब timing normalize करता हूँ
Zero या बहुत छोटे delays अलग implementations में अलग playback दे सकते हैं। इसलिए production policy जरूरी है। मैं timeline को millisecond precision में रखता हूँ, minimum frame delay 10 ms लगाता हूँ, और useful timing न मिलने पर 100 ms fallback इस्तेमाल करता हूँ।
ये universal numbers नहीं हैं। महत्वपूर्ण बात है valid timing को preserve करना और normalization को frame-rate selection से पहले करना।
हर animation को 30 fps क्यों नहीं बनाता
अगर source लगभग 12 या 16 बार प्रति सेकंड state बदलता है, तो उसे 30 fps में store करना आम तौर पर redundant temporal samples जोड़ता है। H.264 duplicate frames compress कर सकता है, लेकिन जो temporal information है ही नहीं उसे पैदा करना बेकार है।
हर source के लिए CFR अलग से इन candidates में से चुनता हूँ:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
Selector source timeline को देखकर सबसे कम candidate चुनता है जो displayed changes को साफ represent कर सके। अगर कोई lower candidate acceptable नहीं है, fallback 30 fps है। 62/63 ms example में 16 fps लगभग perfect fit है।
Final MP4 को एक global CFR की जरूरत नहीं
हर animation independent segment है। एक 12 fps, दूसरा 16, तीसरा 24 या 30 हो सकता है। मैं पूरी collection की सुविधा के लिए हर source की natural cadence नहीं बदलता।
हर segment के अंदर CFR है, लेकिन final concatenated timeline में segment boundaries पर packet duration बदल सकता है। यह intentional design है।
90,000 Hz time base क्यों
90,000 का फायदा है कि मेरे सभी allowed FPS exact integer ticks/frame देते हैं:
| FPS | ticks/frame |
|---|---|
| 10 | 9000 |
| 12 | 7500 |
| 15 | 6000 |
| 16 | 5625 |
| 18 | 5000 |
| 20 | 4500 |
| 24 | 3750 |
| 25 | 3600 |
| 30 | 3000 |
इससे concat और packet validation के लिए साफ mathematical grid मिलता है।
Geometry: जरूरत हो तो downscale, कभी upscale नहीं
- landscape: अधिकतम 1280×720
- portrait: अधिकतम 720×1280
- mixed/square: लगभग 960×960
छोटा source छोटा ही रहता है। 900×600 को 1280×720 करने से नई detail नहीं मिलती; encoder को सिर्फ ज्यादा pixels मिलते हैं।
Aspect ratio stretch नहीं करता। जरूरत पर proportional scale और padding करता हूँ। Alpha को H.264/yuv420p output के लिए तय background पर flatten किया जाता है।
इस workload में H.264 MP4 अक्सर बहुत छोटा क्यों हो सकता है
GIF, APNG और Animated WebP खुद भी partial updates और disposal जैसे mechanisms इस्तेमाल कर सकते हैं, इसलिए “MP4 हमेशा छोटा है” कहना गलत होगा।
लेकिन H.264 video temporal redundancy के लिए डिजाइन किया गया है। Mostly-static illustrated scene में inter prediction, reference pictures और P/B frames लगातार frames के बीच समानता का अच्छा फायदा उठाते हैं।
Apple static web video के लिए H.264 MP4 recommend करता है और animated GIF की जगह MP4 इस्तेमाल करने की सलाह देता है। Safari documentation में GIF के लिए modern video codec की तुलना में up to 12× bandwidth और लगभग 2× energy का उदाहरण दिया गया है। यह 12× मेरा benchmark नहीं है; यह Apple का उदाहरण है।
Animated WebP के लिए nuance जरूरी है। अगर source WebP lossy है, तो H.264 दूसरी lossy generation है। File काफी छोटा हो सकता है, पर quality validation जरूरी है।
H.264 profile जानबूझकर conservative है
| Setting | Value |
|---|---|
| Container | MP4 |
| Codec | H.264 / libx264 |
| Sample entry | avc1 |
| Profile / level | Main @ 3.1 |
| Pixel format | 8-bit yuv420p |
| Rate control | CRF 28 |
| Preset | veryslow |
| Tune | animation |
| Reference frames | 4 |
| Maximum B-frames | 5 |
| Open GOP | off |
| B-pyramid | normal |
| Maximum GOP | लगभग 5 s |
| VBV | 4M maxrate / 8M bufsize |
| Color | BT.709, limited range |
| Track/movie timescale | 90,000 |
| Audio/subtitles/data | none |
Main@3.1 modern devices की maximum capability नहीं है। यह मेरी conservative one-file compatibility baseline है। Android 6.0 से Main Profile decoder required है और 1280×720@30 fps को H.264 HD recommendation के रूप में दिखाता है। Apple भी static web video के लिए H.264 MP4 recommend करता है।
CPU एक बार खर्च, bandwidth बार-बार बचत
-preset veryslow
Encoding offline और one-time है; delivery repeated है। इसलिए x264 को ज्यादा CPU देकर efficient representation खोजने देना इस workload में तर्कसंगत है। Decoder complexity अलग से profile, level, refs और B-frame structure से सीमित है।
CRF 28 भी मेरे illustrated bandwidth-first content का baseline है, universal setting नहीं।
हर animation अलग H.264 segment बनता है
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
इस command से पहले timeline-aware frame sequence और final geometry तैयार रहती है। GOP लगभग selectedFps * 5 है।
Concat सुरक्षित तभी है जब segment contract सख्त हो
FFmpeg concat demuxer compatible streams चाहता है और हर file की duration से अगले timestamps बनाता है। इसलिए incompatibility को concat पर patch नहीं करता; bad segment पहले reject होता है।
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy -an -sn -dn -movflags +faststart final.mp4
-c:v copy दूसरी H.264 lossy encode से बचाता है।
Stream copy का मतलब perfect timestamps नहीं
90000 / 24 = 3750 ticks
एक वास्तविक concatenated output में validator ने 3750 की जगह 3751-tick packet पकड़ा। Video visually ठीक लग सकता था, लेकिन exact grid invariant टूट गया था।
Compressed payload copy होना timing correctness का proof नहीं है। Final concat अब setts bitstream filter से PTS, DTS और duration को known 90-kHz grid पर normalize करता है, जबकि -c:v copy बना रहता है। Exact expression segment timeline से बनती है, generic recipe नहीं।
Validation encoder का हिस्सा है
FFmpeg exit code 0 पर्याप्त नहीं। मैं streams, profile/level, pixel format, dimensions, time base, frame/packet counts, exact durations, PTS/DTS और final duration check करता हूँ, फिर पूरा file decode करता हूँ।
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 -
सभी checks पास होने के बाद atomic publish होता है।
यह pipeline किन गलतियों को रोकता है
- raw subframe को visible canvas मान लेना
- guessed FPS पर भरोसा
- हर source को 30 fps करना
- छोटे source का upscale
- aspect ratio stretch
- concat के बाद दूसरी lossy encode
-c copyको timing proof मानना- full decode के बिना केवल header check
जो चीजें जानबूझकर preserve नहीं होतीं
Alpha flatten होता है। Arbitrary VFR source-specific CFR approximation बनता है। बहुत बड़ी resolution downscale होती है। Lossy WebP एक extra lossy generation से गुजरता है। Audio scope से बाहर है। ये explicit trade-offs हैं।
कब मैं यह तरीका इस्तेमाल नहीं करूँगा
Transparency जरूरी हो, exact irregular timing semantic रूप से महत्वपूर्ण हो, archival master बनाना हो, या पहले से adaptive multi-codec video stack हो, तो यह single-file pipeline सही choice नहीं होगी। पहले से tiny optimized Animated WebP के लिए benchmark जरूरी है।
मेरा practical pipeline अब
- Real animation metadata पढ़ना।
- Blend/disposal से visible canvas states reconstruct करना।
- Delays normalize करके ms timeline बनाना।
- 10/12/15/16/18/20/24/25/30 से per-source CFR चुनना।
- No upscale; proportional resize/pad; alpha flatten।
- H.264 Main@3.1/yuv420p/avc1,
veryslow, CRF 28। - 90-kHz time base।
- हर segment validate करना।
-c:v copyconcat।- Final packet timeline exact grid पर normalize करना।
- Packet inspection + full decode।
- फिर publish।
सबसे महत्वपूर्ण सीख: असली asset timeline है
Animated WebP, GIF और APNG केवल “बहुत सारी images” नहीं हैं। वे समय से जुड़ी canvas states की sequence हैं।
Timeline को source of truth मानने पर बाकी design स्पष्ट हो जाता है: fake motion बनाए बिना cadence घट सकती है, fake pixels बनाए बिना resolution घट सकती है, हर source का अपना CFR हो सकता है, और concat के लिए दूसरी lossy encode जरूरी नहीं रहती।
H.264 temporal redundancy का फायदा उठाता है और MP4 delivery आसान करता है। लेकिन extraction या timing गलत हो जाए तो codec बाद में उसे नहीं बचा सकता।