ब्लॉग पर वापस जाएं
13 अगस्त 2026Sergei Solod14 मिनट पढ़ें

एक टिक जिसने मेरा CFR तोड़ दिया: 5580 क्यों 5625 नहीं था और 3751 क्यों 3750 नहीं था

मेरे वैलिडेटर ने 16 fps MP4 को बार-बार इसलिए अस्वीकार किया क्योंकि एक पैकेट की अवधि 5625 के बजाय 5580 टिक थी। बाद में 24 fps पर 3750 की जगह 3751 टिक मिला। इन दोनों विफलताओं ने मुझे स्रोत समय-निर्धारण, CFR परिमाणीकरण, MP4 समय-आधार, PTS/DTS, मल्टीप्लेक्सिंग और पैकेट-स्तरीय सत्यापन को अलग-अलग समझने पर मजबूर किया।

FFmpegH.264CFRवीडियो टाइमस्टैम्पPTS और DTSMP4 सत्यापन

एक दिन मेरी मीडिया पाइपलाइन ने आउटपुट का कुछ हिस्सा प्रकाशित करना बंद कर दिया। कारण एक ऐसी त्रुटि थी जो पहली नज़र में लगभग हास्यास्पद लगी:

Invalid CFR packet duration: 5580 ticks, expected 5625

डिकोडर क्रैश नहीं हुआ था और FFmpeg ने H.264 फ़ाइल भी बना दी थी। एन्कोडिंग के बाद मेरा वैलिडेटर उसे अस्वीकार कर रहा था, क्योंकि आउटपुट को स्थिर फ्रेम दर पर होना था, लेकिन एक पैकेट उस समय-ग्रिड पर नहीं था जिसे मैंने तय किया था।

मैंने काम दोबारा चलाया। फिर वही 5580। एक बार और चलाया। फिर 5580। बाद में एक अलग स्रोत ने भी यही मान दिया। यह महत्वपूर्ण संकेत था: समस्या न तो अस्थायी नेटवर्क विफलता थी, न कोई दुर्लभ race condition। उल्लंघन नियतात्मक और दोहराने योग्य था।

बाद में मुझे दूसरी विफलता मिली जो दिखने में समान थी, लेकिन तकनीकी रूप से अलग। 24 fps और 90,000-टिक वीडियो ट्रैक पर हर सामान्य नमूने की अवधि ठीक 3750 टिक होनी चाहिए थी। वैलिडेटर ने 3751 पाया।

इन दोनों संख्याओं को केवल “FFmpeg राउंडिंग करता है” कहकर एक ही कारण से जोड़ना गलत होगा। 5580 और 5625 में 45 टिक का अंतर है, यानी ठीक 0.5 ms। 3751 और 3750 में सिर्फ एक टिक, लगभग 11.1 माइक्रोसेकंड का अंतर है।

इन दोनों बग ने मुझे उन चार अवधारणाओं को अलग-अलग समझने पर मजबूर किया जिन्हें मैं पहले “FPS” के नीचे मिला देता था: फ्रेम दर, time base, PTS/DTS और पैकेट की अवधि

CFR सिर्फ “16 fps” लिखे होने का नाम नहीं है

अब जब मैं कहता हूँ कि बनाई गई फ़ाइल CFR है, तो मेरा मतलब केवल इतना नहीं कि ffprobe कोई साफ 16/1 या 24/1 दिखा दे।

मेरी पाइपलाइन में CFR एक सख्त नियम है: प्रस्तुति समय नियमित ग्रिड पर हों और हर सामान्य वीडियो नमूने की अवधि उसी ग्रिड के एक चरण के बराबर हो।

16 fps पर एक फ़्रेम की अवधि:

1 / 16 = 0.0625 s = 62.5 ms

90,000-टिक वीडियो ट्रैक पर वही अंतराल एक सटीक पूर्णांक बनता है:

90000 / 16 = 5625 ticks

इसलिए 5625 वैलिडेटर में मनमाना स्थिरांक नहीं था। यह आउटपुट नियम के दो निर्णयों—16 fps और 90,000 ticks/s—से सीधे निकलता था।

इतनी सख्ती तभी उचित है क्योंकि मैंने जानबूझकर ऐसी फ्रेम दरें चुनीं जो टाइमस्केल को पूरी तरह विभाजित करती हैं। यदि लक्ष्य cadence को एक ही पूर्णांक अवधि से व्यक्त नहीं किया जा सकता, तो वैलिडेटर को सही पूर्णांक पैटर्न मॉडल करना चाहिए, कोई असंभव एकल मान नहीं थोपना चाहिए।

फ्रेम दर, time base और MP4 timescale अलग चीज़ें हैं

  • फ्रेम दर प्रस्तुति की लय बताती है। CFR 16 का अर्थ है हर 62.5 ms में एक फ़्रेम दिखना।
  • FFmpeg time base एक पूर्णांक timestamp इकाई की अवधि है, जैसे 1/90000 सेकंड।
  • MP4 ट्रैक का timescale उसी विचार को प्रति सेकंड इकाइयों की संख्या के रूप में व्यक्त करता है। 90,000 का अर्थ है कि एक इकाई 1/90000 सेकंड की है।
  • PTS बताता है कि चित्र कब प्रस्तुत होना चाहिए।
  • DTS बताता है कि एन्कोड किया गया पैकेट कब डिकोड होना चाहिए।
  • पैकेट की अवधि स्ट्रीम के time base में नमूने की अवधि बताती है।

B-फ़्रेम के साथ PTS और DTS का अलग होना पूरी तरह वैध हो सकता है। इसलिए timestamps “ठीक” करने के लिए सीधे PTS = DTS कर देना खतरनाक है; setts की आधिकारिक दस्तावेज़ीकरण भी B-फ़्रेम होने पर इसे अनुशंसित नहीं मानती।

इसलिए मेरा वैलिडेटर दोनों को बराबर नहीं करता; वह डिकोड क्रम और प्रस्तुति क्रम के बीच वैध संबंध जाँचता है।

मैंने 90,000-टिक ट्रैक क्यों चुना

90,000 कोई सार्वभौमिक जादुई संख्या नहीं है। मेरे लिए यह इसलिए उपयोगी था क्योंकि पाइपलाइन में अनुमत हर फ्रेम दर एक सटीक पूर्णांक अवधि देती थी:

फ्रेम दर90,000 टिक/s पर फ़्रेम की अवधि
10 fps9000 टिक
12 fps7500 टिक
15 fps6000 टिक
16 fps5625 टिक
18 fps5000 टिक
20 fps4500 टिक
24 fps3750 टिक
25 fps3600 टिक
30 fps3000 टिक

मिलीसेकंड स्तर के स्रोत timing से भी इसका संबंध सरल है:

1 ms = 90 ticks

FFmpeg के MP4 multiplexer में video_track_timescale उपलब्ध है, इसलिए मैं यह ट्रैक ग्रिड स्पष्ट रूप से निर्धारित कर सकता हूँ। ग्रिड समय-निर्धारण को अपने-आप सही नहीं बनाता; वह नियम को मापने और जाँचने योग्य बनाता है।

5580 ने बताया कि कहाँ देखना चाहिए

पहली विफलता को समय में वापस बदलते ही वह ज्यादा अर्थपूर्ण हो जाती है:

5580 / 90000 = 0.062 s = 62 ms

यह कोई यादृच्छिक मान नहीं था। मेरे पास एक वास्तविक एनिमेटेड स्रोत था: 3.063 सेकंड में 49 दिखाए गए फ़्रेम, जिनकी देरी 62 और 63 ms के बीच बारी-बारी से बदलती थी:

49 / 3.063 ≈ 15.997 frames/s

62 ms + 63 ms = 125 ms
2 frames at 16 fps = 2 × 62.5 ms = 125 ms

स्रोत स्तर पर 62/63 ms का बारी-बारी का क्रम, मिलीसेकंड ग्रिड पर 16 fps का बहुत स्वाभाविक सन्निकटन है।

लेकिन CFR 16 चुनने के बाद आउटपुट को लक्ष्य ग्रिड पर होना चाहिए: हर सामान्य फ़्रेम के लिए 62.5 ms या 5625 टिक।

इसलिए 5580 एक मजबूत संकेत था कि स्रोत की 62 ms अवधि उस चरण तक बची रही जहाँ timing को पहले ही CFR ग्रिड पर क्वांटाइज़ हो जाना चाहिए था।

मैं प्रमाण की सीमा साफ रखता हूँ। संग्रहीत लॉग साबित करता है कि 90 kHz पर 5580 बिल्कुल 62 ms है, और वास्तविक स्रोत में 62/63 ms की देरी थी। अकेला लॉग यह साबित नहीं करता कि किस सटीक फ़ंक्शन ने उस अवधि को आगे जाने दिया। यह तंत्र के बारे में मजबूत निष्कर्ष है, बाद में कोई काल्पनिक कोड-पंक्ति गढ़ने का आधार नहीं।

1000 Hz इनपुट घड़ी गलती नहीं थी

मैंने आर्किटेक्चर के इस हिस्से को FFmpeg 7.1.5 और ffconcat के साथ अलग से पुनरुत्पादित किया। इस इनपुट के साथ:

duration 0.010
option framerate 1000

पैकेट timestamps ने अपेक्षित मिलीसेकंड स्थितियाँ बनाए रखीं:

0 ms
10 ms
20 ms
30 ms

उसी शुरुआती चरण पर इनपुट दर 30 रखने से वही timing लगभग 0 और 33.3 ms पर क्वांटाइज़ हो जाती थी।

इसलिए 1000 Hz इनपुट घड़ी सही काम कर रही थी: वह प्रामाणिक स्रोत देरी को 1 ms सटीकता पर सुरक्षित रख रही थी। इसका मतलब अंतिम वीडियो को 1000 fps बनाना नहीं था।

millisecond-accurate source delays
        ↓
authoritative source timeline
        ↓
choose target CFR
        ↓
explicitly quantize onto CFR grid
        ↓
preserve that grid through encoding and muxing

इनपुट की अधिक सटीकता समस्या नहीं है। समस्या स्रोत timing से लक्ष्य CFR तक जाने की सीमा को स्पष्ट न करना है।

CFR नियंत्रित समय-परिमाणीकरण है

62/63 ms वाला वास्तविक स्रोत दो-दो फ़्रेम के जोड़ों पर 16 fps से स्वाभाविक रूप से मेल खाता है: दोनों 125 ms कवर करते हैं। लेकिन अंतिम फ़ाइल 62, 63, 62, 63 ms की अवधियाँ रखकर केवल “16 fps” घोषित नहीं कर सकती; वे तब भी परिवर्ती अवधियाँ हैं।

62 ms, 63 ms, 62 ms, 63 ms
              ↓
62.5 ms, 62.5 ms, 62.5 ms, 62.5 ms

FFmpeg का fps फ़िल्टर इस रूपांतरण के लिए उचित जगह है। वह इनपुट timestamps और चुनी गई rounding policy के आधार पर फ़्रेम हटाकर या दोहराकर लक्ष्य फ्रेम दर बनाता है।

एक बार यह चरण अपेक्षित ग्रिड बना दे, तो बाद के चरण को स्वतंत्र रूप से दूसरा फ्रेम-दर रूपांतरण नहीं करना चाहिए। पाइपलाइन में एक जानबूझकर तय की गई quantization सीमा होनी चाहिए, कई स्वतंत्र rounding परतें नहीं।

तीन दोबारा प्रयासों ने कुछ क्यों नहीं बदला

FAIL: duration 5580, expected 5625
retry 1/3
FAIL: duration 5580, expected 5625
retry 2/3
FAIL: duration 5580, expected 5625

यह विश्वसनीयता का सबक वीडियो से बड़ा है। दोबारा प्रयास उन विफलताओं के लिए ठीक है जो अगले प्रयास में गायब हो सकती हैं: नेटवर्क त्रुटि, अस्थायी स्टोरेज समस्या, संसाधन दबाव या अनुपलब्ध dependency।

लेकिन उसी इनपुट और उसी एल्गोरिदम से बनने वाले नियतात्मक नियम-उल्लंघन को दोबारा प्रयास ठीक नहीं करता।

  • क्षणिक विफलता — दोबारा प्रयास मदद कर सकता है;
  • खराब इनपुट — उसे दूसरे रास्ते पर भेजें या अस्वीकार करें;
  • नियतात्मक invariant विफलता — दोबारा प्रयास रोकें और पाइपलाइन का निदान करें।

बार-बार मिला 5580 तीसरी श्रेणी में था।

फिर 3750 की जगह 3751 मिला

24 fps पर अपेक्षित मान सटीक पूर्णांक है:

90000 / 24 = 3750 ticks

फिर भी एक वास्तविक concat किए गए आउटपुट में एक पैकेट की अवधि थी:

3751 ticks

अंतर केवल:

1 / 90000 s ≈ 11.111 µs

कोई दर्शक एक टिक महसूस नहीं करेगा। इसलिए वैलिडेटर को ±1 सहिष्णुता देना आकर्षक था।

मैंने ऐसा नहीं किया। इस ग्रिड पर 24 fps के लिए 3750 सटीक रूप से दर्शाने योग्य है। इसलिए 3751 कोई अपरिहार्य प्रतिनिधित्व-समझौता नहीं था; वह प्रमाण था कि सटीक ग्रिड का invariant कहीं खो गया।

स्ट्रीम कॉपी का अर्थ timestamps अछूते रहना नहीं है

पाइपलाइन संगत H.264 खंडों को दूसरी lossy encoding के बिना concat करती थी:

ffmpeg -f concat -safe 0 -i segments.ffconcat \
  -c:v copy \
  final.mp4

-c:v copy का अर्थ है कि संपीड़ित H.264 डेटा को डिकोड करके दोबारा एन्कोड नहीं किया जाता। इसका अर्थ यह नहीं कि multiplexing परत timing पर कोई काम नहीं करती।

concat demuxer की दस्तावेज़ीकरण साफ कहती है कि हर फ़ाइल की अवधि अगले फ़ाइल के timestamps को समायोजित करने में उपयोग होती है। FFmpeg पूर्णांक timestamps को rational time bases के बीच rescale भी करता है, और libavutil स्पष्ट rounding modes वाली rescaling functions देता है।

यह मेरे 3751 वाले विशिष्ट मामले में concat को अकेला कारण साबित नहीं करता। लेकिन “bitstream कॉपी हुआ, इसलिए पैकेट timestamps बदल नहीं सकते” वाली सोच गलत है।

संपीड़ित चित्रों की समानता और timestamp ग्रिड की समानता अलग गुण हैं।

PTS और DTS को एक सुंदर समीकरण से ठीक नहीं किया जा सकता

एक लुभावना सुधार ऐसा दिख सकता है:

PTS = N * frame_duration
DTS = N * frame_duration

H.264 में B-फ़्रेम होने पर यह गलत हो सकता है, क्योंकि चित्रों का डिकोड क्रम और प्रस्तुति क्रम अलग हो सकता है।

सही लक्ष्य अधिक सीमित है: प्रामाणिक segment timeline से ज्ञात प्रस्तुति ग्रिड और पैकेट अवधियाँ बहाल करना, जबकि वैध decode-order संबंध बना रहे।

इसीलिए मैं संदर्भ-विहीन “जादुई setts expression” प्रकाशित नहीं करता। सही expression segment boundaries, चुनी गई rates और उस timing metadata पर निर्भर है जिससे अंतिम timeline बनी।

मैंने setts क्यों इस्तेमाल किया

FFmpeg का setts bitstream filter वीडियो को डिकोड और दोबारा एन्कोड किए बिना पैकेट PTS, DTS, duration और output time base बदल सकता है।

concat के बाद normalization के लिए यही सही परत थी: H.264 का संपीड़ित डेटा वैसा ही रहे, लेकिन ज्ञात segment metadata से निकली packet timeline को फिर लागू किया जाए।

known segment timeline
+ known CFR
+ 90000-tick grid
        ↓
known valid packet positions and durations
        ↓
normalize packet timing
        ↓
validate again

यह “duration 3751 हो तो एक घटा दो” से बिल्कुल अलग है। सुधार timing model से निकलना चाहिए, आज के error message से नहीं।

मैंने ±1 टिक सहिष्णुता क्यों नहीं स्वीकार की

कई प्रणालियों में छोटी सहिष्णुता सही है। यदि cadence चुने गए पूर्णांक time base पर सटीक नहीं दर्शाई जा सकती, तो वैलिडेटर को आवश्यक rounding pattern मॉडल करना चाहिए।

मेरा नियम अलग था। अनुमत rates ऐसी चुनी थीं कि 90000 / fps पूर्णांक हो:

16 fps → 5625
24 fps → 3750
30 fps → 3000

जब अपेक्षित duration सटीक दर्शाने योग्य हो, सामान्य ±1 किसी अस्पष्ट invariant violation को चुपचाप स्वीकार्य स्थिति बना देता है।

एक टिक दृश्य रूप से अप्रासंगिक है। लेकिन यह अप्रासंगिक नहीं कि डिज़ाइन किया हुआ नियम बिना कारण समझे क्यों खो गया।

  • यदि ग्रिड को पूर्णांक अवधियों के बीच बारी-बारी करनी पड़ती है, तो सही pattern सत्यापित करें;
  • यदि अवधि एक सटीक पूर्णांक होनी चाहिए, तो वही पूर्णांक आवश्यक मानें;
  • वैलिडेटर को पास कराने के लिए ±1 को सार्वभौमिक शॉर्टकट न बनाएं।

मैं CFR को पैकेट स्तर पर कैसे सत्यापित करता हूँ

avg_frame_rate जैसी stream metadata उपयोगी सारांश है, लेकिन इस नियम के लिए पर्याप्त नहीं।

ffprobe -v error \
  -select_streams v:0 \
  -show_streams \
  -show_packets \
  -of json \
  final.mp4

मैं वास्तविक stream time base और हर पैकेट के pts, dts और duration जाँचता हूँ।

expected = 90000 / 16   // 5625

for each normal video packet:
    assert packet.duration == 5625

assert decode ordering is legal
assert PTS/DTS relationship is legal for the codec
assert presentation timeline matches planned frame count and duration

“सामान्य पैकेट” महत्वपूर्ण है। edit lists, trimming, जानबूझकर विशेष terminal sample या container का कोई और व्यवहार स्पष्ट modeling मांग सकता है। मैं इसे हर MP4 पर लागू होने वाला सार्वभौमिक नियम नहीं बता रहा।

लेकिन जिस generator की संरचना मेरे नियंत्रण में है, वहाँ कठोर सत्यापन “लगभग 16 fps जैसा है” कहने से कहीं अधिक उपयोगी है।

मेटाडेटा सत्यापन और पूर्ण डिकोडिंग अलग सवालों के जवाब देते हैं

सही timestamps यह साबित नहीं करते कि पूरा H.264 stream डिकोड होगा। और साफ़ डिकोड यह साबित नहीं करता कि timing contract सही है।

ffprobe / packet validation
→ structure, timestamps, durations, stream parameters

full decode
→ whether the complete compressed stream can actually be decoded

पैकेट जाँच के बाद भी मैं यह चलाता हूँ:

ffmpeg -v error -xerror -err_detect explode \
  -i final.mp4 -f null -

दोनों चरण पार होने के बाद ही फ़ाइल प्रकाशित की जाती है। encoder का exit code 0 अब मेरे लिए “पूरा” होने की परिभाषा नहीं है।

दोनों विफलताओं ने वास्तव में क्या साबित किया

5580 के लिए मैं पुष्टि कर सकता हूँ कि दोबारा प्रयास में वही नियतात्मक मान आया; 90 kHz पर 5580 ठीक 62 ms है; वास्तविक स्रोत में 62/63 ms की देरी थी; CFR 16 को 62.5 ms या 5625 टिक चाहिए; और 1000 Hz इनपुट प्रयोग ने मिलीसेकंड देरी को सही रूप से सुरक्षित रखा।

इससे यह व्याख्या मजबूत होती है कि स्रोत की duration उस चरण तक रिस गई जहाँ CFR पहले ही बन जाना चाहिए था। लेकिन संग्रहीत error line सटीक फ़ंक्शन को साबित नहीं करती।

3751 के लिए मैं पुष्टि कर सकता हूँ कि वास्तविक आउटपुट में 24 fps के अपेक्षित 3750 की जगह एक 3751-टिक पैकेट था; पाइपलाइन stream-copy concat करती थी; concat के बाद packet normalization में setts समाधान का हिस्सा बना; और FFmpeg concat timestamp adjustment तथा packet-level timestamp rewriting दोनों दस्तावेज़ करता है।

ये तथ्य integer rescaling या multiplexing/concat boundary rounding के साथ संगत हैं, लेकिन “concat हमेशा एक टिक जोड़ता है” साबित नहीं करते।

अब मैं यह कार्यप्रवाह इस्तेमाल करता हूँ

  1. अनुमानित FPS फ़ील्ड पर भरोसा करने की जगह प्रामाणिक स्रोत timeline पुनर्निर्मित करता हूँ।
  2. मूल मिलीसेकंड देरी को पर्याप्त रूप से सटीक input clock पर सुरक्षित रखता हूँ।
  3. लक्ष्य CFR अलग से चुनता हूँ।
  4. स्रोत timeline को स्पष्ट रूप से CFR ग्रिड पर क्वांटाइज़ करता हूँ।
  5. जहाँ संभव हो, ऐसा track timescale चुनता हूँ जो अनुमत rates को सटीक रूप से दर्शाए।
  6. एन्कोडिंग के बाद किसी बाद के चरण को स्वतंत्र रूप से frame-rate conversion दोबारा नहीं करने देता।
  7. concat से पहले time base, frame count, PTS/DTS और packet duration सत्यापित करता हूँ।
  8. stream-copy concat से पहले codec configuration और timing compatibility सत्यापित करता हूँ।
  9. concat के बाद packet grid फिर जाँचता हूँ; -c:v copy को timestamp identity का प्रमाण नहीं मानता।
  10. normalization चाहिए तो उसे ज्ञात timeline से निकालकर packet level पर लागू करता हूँ।
  11. हर पैकेट दोबारा सत्यापित करता हूँ।
  12. अंतिम फ़ाइल को पूरा डिकोड करता हूँ।
  13. नियम पास होने के बाद ही atomically प्रकाशित करता हूँ।

जो नियम बचा: CFR एक पूर्णांक timing contract है

पहले 16 fps मुझे लगभग स्वतः स्पष्ट लगता था। अब नहीं।

time base = 1/90000
normal frame duration = 5625 ticks
presentation cadence = 62.5 ms
packet timeline follows that grid
PTS/DTS remain legal for H.264 reordering

5580 महत्वपूर्ण था क्योंकि उसने पुरानी मिलीसेकंड ग्रिड उजागर की। 3751 इसलिए महत्वपूर्ण था क्योंकि वह लगभग अदृश्य था: एक टिक यह बताने के लिए पर्याप्त था कि प्रणाली ने वह invariant सुरक्षित रखना बंद कर दिया है जिसे डिज़ाइन के अनुसार बनाए रखना था।

अब मेरा नियम सरल है: “CFR” लेबल को सत्यापित मत करो; उस timing को सत्यापित करो जिससे वह लेबल सही सिद्ध होना चाहिए।

मुख्य दस्तावेज़