আমি এই pipeline বানানো শুরু করিনি নতুন codec নিয়ে পরীক্ষা করার জন্য। সমস্যা ছিল delivery cost: Animated WebP, GIF আর APNG অনেক ক্ষেত্রেই বাস্তবে কয়েক সেকেন্ডের silent video-এর কাজ করছিল, কিন্তু একই asset বারবার web-এ পাঠানোর জন্য সেগুলো সবসময় সবচেয়ে সাশ্রয়ী representation ছিল না।
আমার workload-এ animation সাধারণত ছোট, কয়েক সেকেন্ডের, কয়েক ডজন displayed frame নিয়ে, আর পরপর frame-এর মধ্যে প্রচুর temporal redundancy থাকে। Encoding একবার হয়; bytes পরে বহুবার পাঠানো হয়। তাই encoding time-এর চেয়ে final file size এবং browser playback আমার কাছে বেশি গুরুত্বপূর্ণ।
“Animation-টা MP4 বানিয়ে দাও” কথাটা সহজ। Production implementation সহজ নয়। Animated image সবসময় full-size frame-এর পরিষ্কার sequence নয়। এতে partial rectangle, blend/disposal rule, alpha, irregular delay, zero-duration frame, orientation difference, এমনকি misleading probe FPS-ও থাকতে পারে।
শেষ পর্যন্ত pipeline-টি আমি এই invariants-এর ওপর দাঁড় করিয়েছি:
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 এখানে পুরো সমস্যার ছোট অংশ। আসল কাজ হলো representation বদলালেও animation যেভাবে দেখা যেত সেই অর্থটি না হারানো।
প্রথম নিয়ম: stored rectangle নয়, displayed frame extract করতে হবে
সবচেয়ে বিপজ্জনক shortcut হলো ধরে নেওয়া যে animated image-এর প্রতিটি stored frame পুরো canvas replace করে।
Animated WebP frame-এর position, rectangle size, blend mode এবং disposal mode থাকতে পারে। APNG-তে offset, dimensions, dispose operation এবং blend operation থাকে। GIF-এর Graphic Control Extension-ও decoder-কে বলে frame-এর পরে affected area কীভাবে handle করতে হবে।
তাই stored frame অনেক সময় শুধু ছোট patch, যার অর্থ আগের canvas state-এর ওপর নির্ভর করে। Patch-গুলোকে full image ধরে encode করলে ছোট video নয়, ভুল animation তৈরি হয়।
আমার extraction boundary তাই displayed canvas state: সঠিক decoder disposal এবং blending প্রয়োগ করার পরে user যে পূর্ণ composited image দেখত সেটাই একটি video frame-এর source।
এই ধাপ ভুল হলে পরে CRF, preset বা H.264 setting দিয়ে আর ঠিক করা সম্ভব নয়।
Frame timing source data; FPS অনুমান নয়
আরেকটি ভুল ছিল generic probe-এর FPS-কে সত্য ধরে নেওয়া।
- Animated WebP প্রতিটি frame-এর duration 1 ms unit-এ রাখে।
- GIF delay রাখে second-এর hundredths-এ।
- APNG প্রতি frame-এর delay numerator/denominator রাখে; denominator 0 হলে PNG specification সেটিকে 100 ধরে।
এই per-frame delay-গুলোই timeline। Average FPS শুধু summary।
একটি বাস্তব WebP ছিল 1264×720, 49 displayed frame, frame delay 62 ও 63 ms করে পাল্টাচ্ছিল, total duration ছিল 3.063 s। বাস্তবে এটি প্রায় 16 fps cadence, কারণ 16 fps-এ frame duration 62.5 ms।
একটি probe একই source-কে 25 fps বলেছিল।
আমি যদি 25 বা 30 fps ধরে conversion করতাম, source timing বদলে যেত বা অপ্রয়োজনীয় duplicate frame তৈরি হতো। তাই headline FPS নয়, original frame durations-ই authoritative।
CFR বেছে নেওয়ার আগে খারাপ timing normalize করি
Zero বা অত্যন্ত ছোট delay সব implementation-এ একই playback behavior দেয় না। WebP-তে zero duration থাকতে পারে, GIF-এ zero delay সম্ভব, APNG-তেও numerator zero হলে “next frame as quickly as possible” ধরনের semantics আসে।
Production pipeline-এ তাই explicit policy দরকার। আমি timeline millisecond precision-এ রাখি, 10 ms minimum frame delay ব্যবহার করি, আর useful timing অনুপস্থিত হলে 100 ms fallback ব্যবহার করি।
এই সংখ্যা universal নয়। গুরুত্বপূর্ণ হলো valid timing না ফেলে normalization আগে করা এবং তার পরে CFR quantization করা।
সব animation-কে 30 fps করি না কেন
Displayed states এবং durations পাওয়ার পরে image timeline-কে video timeline-এ বসাতে হয়। 30 fps সবকিছুর জন্য সহজ, কিন্তু 12–16 fps-এর animation-এর জন্য wasteful।
H.264 duplicate frame ভালো compress করে, কিন্তু এমন frame encode করাই অপ্রয়োজনীয় যদি source motion-এ সেগুলো না থাকে।
প্রতিটি source-এর জন্য আমি এই candidate set থেকে CFR বেছে নিই:
10, 12, 15, 16, 18, 20, 24, 25, 30 fps
Selector source timeline দেখে সর্বনিম্ন candidate বেছে নেয় যেটি displayed transitions যথেষ্ট পরিষ্কারভাবে represent করে। নিচের candidate গ্রহণযোগ্য না হলে fallback 30 fps।
62/63 ms example-এ 16 fps সরাসরি fit করে, কারণ 62.5 ms per frame।
Final MP4-এর একটাই global CFR থাকা জরুরি নয়
প্রতিটি animation স্বাধীন H.264 segment হিসেবে encode হয়। একটি 12 fps, অন্যটি 16, আরেকটি 24 বা 30 হতে পারে। Collection-এর সুবিধার জন্য source cadence নষ্ট করি না।
প্রতিটি segment ভিতরে CFR, কিন্তু final concatenated timeline segment boundary-তে packet duration বদলাতে পারে। অর্থাৎ পুরো file piecewise-CFR ধরনের timeline বহন করে। এটি intentional।
90,000 Hz time base কেন
Video এবং movie timescale 90,000 রাখার বড় সুবিধা হলো আমার সব allowed FPS exact integer tick duration দেয়:
| FPS | ticks/frame |
|---|---|
| 10 | 9000 |
| 12 | 7500 |
| 15 | 6000 |
| 16 | 5625 |
| 18 | 5000 |
| 20 | 4500 |
| 24 | 3750 |
| 25 | 3600 |
| 30 | 3000 |
এই exact grid concat এবং packet validation অনেক সহজ করে।
Geometry: downscale করি, upscale নয়
আমার output envelope:
- landscape: সর্বোচ্চ 1280×720
- portrait: সর্বোচ্চ 720×1280
- mixed/square: প্রায় 960×960
Source ছোট হলে ছোটই থাকে। 900×600 source-কে 1280×720 বানালে detail বাড়ে না, শুধু encoder-এর pixel count বাড়ে।
Aspect ratio stretch করি না। প্রয়োজনে proportional scale + padding ব্যবহার করি। Alpha থাকলে H.264/yuv420p output-এর জন্য একটি নির্দিষ্ট background-এ flatten করি।
এই ধরনের content-এ H.264 MP4 কেন অনেক ছোট হতে পারে
GIF, APNG এবং Animated WebP নিজেরাও partial updates এবং disposal দিয়ে redundancy কমাতে পারে। তাই “MP4 সবসময় ছোট” বলা ভুল।
কিন্তু H.264 video temporal compression-এর জন্য তৈরি। Background স্থির থেকে ছোট অঞ্চল নড়লে reference pictures, inter prediction, P-frames এবং B-frames এই workload-এর সাথে খুব ভালো মেলে।
Apple static web video-এর জন্য H.264 MP4 recommend করে এবং animated GIF-এর বদলে MP4 ব্যবহারের কথা বলে। Safari documentation-এ GIF modern video codec-এর তুলনায় সর্বোচ্চ 12× bandwidth এবং প্রায় 2× energy ব্যবহার করতে পারে বলে উল্লেখ আছে। 12× আমার benchmark নয়; এটি Apple-এর documentation example।
Animated WebP-এর ক্ষেত্রে nuance দরকার। Source যদি lossy WebP হয়, H.264 conversion একটি দ্বিতীয় lossy generation। File ছোট হতে পারে, কিন্তু quality validate করা বাধ্যতামূলক।
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 আধুনিক device-এর maximum capability নয়। এটি আমার one-file compatibility baseline। Android 6.0 থেকে Main Profile decoder required এবং Android 1280×720@30 fps-কে H.264 HD recommendation-এর উদাহরণ দেয়। Apple-ও static web video-এর জন্য H.264 MP4 recommend করে।
Encoding CPU একবার খরচ, bandwidth বারবার
-preset veryslow
এই architecture-এ encoding offline এবং একবার। Delivery বহুবার। তাই x264-কে বেশি CPU দিয়ে ছোট representation খুঁজতে দেওয়া যুক্তিসঙ্গত। Decoder complexity আলাদা করে profile, level, refs এবং B-frame structure দিয়ে সীমিত থাকে।
CRF 28-ও আমার illustrated bandwidth-first workload-এর baseline; universal quality setting নয়।
প্রতিটি animation আলাদা segment হিসেবে encode হয়
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 resize/pad geometry তৈরি থাকে। GOP প্রায় selectedFps * 5।
Concat-এর আগে segment contract কঠোর হতে হয়
FFmpeg concat demuxer compatible streams আশা করে এবং প্রতিটি input file-এর duration ব্যবহার করে পরের file-এর timestamp স্থাপন করে। তাই mismatch concat-এর সময় “ঠিক” করি না; segment acceptance-এর আগেই 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 মানেই timestamps নিখুঁত নয়
90000 / 24 = 3750 ticks
একটি বাস্তব concatenated output-এ validator 3750-এর বদলে 3751-tick packet ধরেছিল। Video চোখে ঠিক লাগতে পারে, কিন্তু exact 90-kHz grid invariant ভেঙেছিল।
অর্থাৎ compressed payload copy হওয়া এবং timestamp correctness এক জিনিস নয়। Segment boundary adjustment-এর পরে muxed timing-এ rounding ঢুকতে পারে।
Final concat এখন setts bitstream filter ব্যবহার করে PTS, DTS এবং duration-কে পরিচিত grid-এ normalize করে, তবুও -c:v copy থাকে। Exact expression segment timeline থেকে তৈরি হয়; এটি generic paste-able formula নয়।
Validation encoder-এরই অংশ
FFmpeg exit code 0 যথেষ্ট নয়। আমি stream metadata, profile/level, pixel format, dimensions, time base, frame/packet counts, exact durations, PTS/DTS এবং total duration যাচাই করি, তারপর পুরো 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 -
সব check pass করার পরেই atomic publish হয়।
এই pipeline কোন ভুলগুলো আটকায়
- raw subframe-কে visible canvas ধরে encode করা
- guessed FPS বিশ্বাস করা
- সব source 30 fps করা
- ছোট source upscale করা
- aspect ratio stretch করা
- concat-এর পরে আবার lossy encode করা
-c copy-কে timing correctness-এর proof ধরা- full decode ছাড়া শুধু header check করা
যা ইচ্ছাকৃতভাবে preserve করি না
Alpha flatten হয়। Arbitrary irregular VFR source-specific CFR approximation-এ যায়। বড় source resolution envelope-এর মধ্যে downscale হয়। Lossy WebP আরেকটি lossy generation পায়। Audio এই pipeline-এর scope নয়।
এসব hidden defect নয়; এগুলো explicit trade-off।
কখন এই approach ব্যবহার করব না
Transparency preserve করা বাধ্যতামূলক হলে, irregular timing-এর exact semantics গুরুত্বপূর্ণ হলে, archival master বানাতে হলে, বা existing adaptive multi-codec video stack থাকলে এই single-file pipeline সরাসরি ব্যবহার করব না।
আগেই খুব ছোট optimized Animated WebP থাকলে আমি benchmark করব; MP4 ছোট হবেই ধরে নেব না।
এখনকার practical pipeline
- Real animation metadata পড়া।
- Blend/disposal দিয়ে visible canvas states reconstruct করা।
- Delay normalize করে millisecond timeline বানানো।
- 10/12/15/16/18/20/24/25/30 থেকে source-specific 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 copyদিয়ে concat।- Final packet timeline exact grid-এ normalize করা।
- Packet inspection + full decode।
- সব pass করলে publish।
সবচেয়ে গুরুত্বপূর্ণ শিক্ষা: asset আসলে timeline
Animated WebP, GIF আর APNG শুধু “অনেকগুলো ছবি” নয়। এগুলো time-ordered canvas state-এর sequence।
Timeline-কে source of truth ধরলে design সহজ হয়: motion বানিয়ে না দিয়ে cadence কমানো যায়, pixels বানিয়ে না দিয়ে resolution কমানো যায়, প্রতি source-এর নিজের CFR থাকতে পারে, এবং final concat-এর জন্য আবার lossy encode দরকার হয় না।
H.264 temporal redundancy কমাতে সাহায্য করে, MP4 delivery সহজ করে। কিন্তু extraction বা timing ভুল হলে কোনো codec পরে সেটা উদ্ধার করতে পারবে না।
Codec তুলনামূলক সহজ অংশ। আসল engineering হলো representation বদলালেও animation-এর meaning ধরে রাখা।