العودة إلى المدونة
13 أغسطس 2026Sergei Solod12 دقائق قراءة

النبضة الواحدة التي كسرت معدل الإطارات الثابت لدي: لماذا لم تكن 5580 تساوي 5625، ولم تكن 3751 تساوي 3750

رفض المدقّق ملف MP4 بمعدل 16 إطارًا/ثانية مرارًا لأن مدة إحدى الحزم كانت 5580 نبضة بدل 5625، ثم اكتشف لاحقًا 3751 نبضة حيث يتطلب 24 إطارًا/ثانية القيمة 3750 بالضبط. علّمتني الحالتان الفصل بين توقيت المصدر، وتكميم CFR، وقاعدة الزمن، وPTS/DTS، والدمج، والتحقق على مستوى الحزم.

FFmpegH.264CFRالطوابع الزمنية للفيديوPTS وDTSالتحقق من MP4

توقف خط معالجة الوسائط لدي يومًا عن نشر جزء من النتائج بسبب خطأ بدا سخيفًا تقريبًا:

Invalid CFR packet duration: 5580 ticks, expected 5625

لم يتعطل فك الترميز. وقد أنتج FFmpeg ملف H.264 بالفعل. الذي رفض الملف كان المدقّق بعد انتهاء الترميز، لأن الناتج كان يفترض أن يكون ذا معدل إطارات ثابت، بينما لم تقع مدة إحدى الحزم على شبكة الزمن التي صممتها.

أعدت المهمة. ظهرت القيمة نفسها 5580. أعدتها مرة أخرى، وبقيت 5580. ثم فشل مصدر آخر لاحقًا بالقيمة نفسها. كانت هذه معلومة مهمة: لم يكن الأمر عطل شبكة عابرًا أو سباقًا نادرًا، بل خرقًا حتميًا للعقد.

بعد ذلك وجدت مشكلة ثانية تشبه الأولى ظاهريًا لكنها مختلفة تقنيًا. عند 24 إطارًا/ثانية ومسار فيديو مقياسه 90,000 نبضة في الثانية، يجب أن تكون مدة العينة العادية 3750 نبضة بالضبط. وجد المدقّق 3751.

لا يصح تفسير الرقمين بعبارة فضفاضة واحدة من نوع «FFmpeg يقرّب الأرقام». الفرق بين 5580 و5625 هو 45 نبضة، أي 0.5 مللي ثانية تمامًا. أما الفرق بين 3751 و3750 فهو نبضة واحدة فقط، أي نحو 11.1 ميكروثانية.

أجبرتني الحالتان على فصل مفاهيم كنت أضعها سابقًا تحت كلمة «FPS»: معدل الإطارات، وقاعدة الزمن، وPTS/DTS، ومدة الحزمة.

معدل الإطارات الثابت ليس مجرد وسم يقول «16 إطارًا/ثانية»

حين أقول اليوم إن ملفًا مولدًا يعمل بمعدل إطارات ثابت، لا أقصد فقط أن ffprobe يطبع قيمة مريحة مثل 16/1 أو 24/1.

في هذا الخط، CFR عقد أشد صرامة: أزمنة العرض تقع على شبكة منتظمة، ومدة كل عينة فيديو عادية تساوي خطوة واحدة من تلك الشبكة.

عند 16 إطارًا/ثانية تستغرق الصورة الواحدة:

1 / 16 = 0.0625 s = 62.5 ms

ومع مسار زمني قدره 90,000 نبضة/ثانية تصبح المدة عددًا صحيحًا بلا تقريب:

90000 / 16 = 5625 ticks

إذًا 5625 ليست ثابتًا اعتباطيًا في المدقّق. إنها نتيجة مباشرة لخيارين في عقد الناتج: 16 fps و90,000 ticks/s.

ولا يصح هذا الشرط الصارم إلا لأنني اخترت عمدًا معدلات تقسم مقياس المسار دون باقٍ. إذا كان الإيقاع المستهدف لا يمكن تمثيله بمدة صحيحة واحدة، فعلى المدقّق الصحيح التحقق من نمط الأعداد الصحيحة المسموح به بدل فرض قيمة مستحيلة.

معدل الإطارات وقاعدة الزمن ومقياس MP4 ليست الشيء نفسه

  • معدل الإطارات يصف إيقاع العرض. CFR 16 يعني صورة معروضة كل 62.5 مللي ثانية.
  • قاعدة الزمن في FFmpeg تحدد مدة وحدة الطابع الزمني الصحيحة، مثل 1/90000 ثانية.
  • مقياس مسار MP4 يعبّر عن الفكرة المعاكسة: عدد الوحدات في الثانية. القيمة 90,000 تجعل مدة الوحدة 1/90000 ثانية.
  • PTS يحدد متى تُعرض الصورة.
  • DTS يحدد متى يجب فك ترميز الحزمة المضغوطة.
  • مدة الحزمة تصف مدة العينة وفق قاعدة زمن التدفق.

مع إطارات B يمكن أن يختلف PTS عن DTS بشكل صحيح تمامًا. لذلك فإن «إصلاح» التوقيت بوضع PTS = DTS ببساطة أمر خطر؛ بل إن توثيق setts الرسمي ينبه إلى أن هذا الأسلوب غير موصى به عند وجود إطارات B.

لذلك يتحقق المدقّق لدي من علاقة قانونية بين ترتيب فك الترميز وترتيب العرض، ولا يشترط تطابق الطابعين.

لماذا اخترت مسارًا بمقياس 90,000 نبضة

ليست 90,000 رقمًا سحريًا عامًا. كانت مفيدة لأن كل معدل إطارات أسمح به يتحول إلى مدة صحيحة تمامًا:

المعدلمدة الإطار عند 90,000 نبضة/ثانية
10 fps9000 ticks
12 fps7500 ticks
15 fps6000 ticks
16 fps5625 ticks
18 fps5000 ticks
20 fps4500 ticks
24 fps3750 ticks
25 fps3600 ticks
30 fps3000 ticks

ولها أيضًا علاقة مريحة مع توقيت المصدر بالمللي ثانية:

1 ms = 90 ticks

يوفر FFmpeg خيار video_track_timescale في جامع MP4، لذلك أستطيع طلب هذه الشبكة صراحة. الشبكة وحدها لا تجعل التوقيت صحيحًا؛ لكنها تجعل العقد قابلًا للقياس.

القيمة 5580 أخبرتني أين أبحث

تصبح المشكلة الأولى أكثر وضوحًا إذا أعدنا الرقم إلى الزمن:

5580 / 90000 = 0.062 s = 62 ms

لم تكن مصادفة عديمة المعنى. كان لدي مصدر رسوم متحركة حقيقي يضم 49 حالة معروضة خلال 3.063 ثانية، وتتبادل مدد الإطارات فيه بين 62 و63 مللي ثانية:

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 مللي ثانية تقريب منطقي جدًا لإيقاع 16 إطارًا/ثانية على شبكة دقتها مللي ثانية واحدة.

لكن بعد اختيار CFR 16 يجب أن يعيش الناتج على الشبكة الجديدة: 62.5 مللي ثانية، أو 5625 نبضة، لكل إطار عادي.

لذلك كانت 5580 قرينة قوية على أن مدة مصدر قدرها 62 مللي ثانية نجت إلى مرحلة كان يفترض أن تحتوي توقيت CFR المكمّم بالفعل.

وأبقي حدود الدليل واضحة: السجل المحفوظ يثبت أن 5580 تساوي 62 مللي ثانية تمامًا عند 90 kHz، كما أن توقيت المصدر الحقيقي احتوى 62/63 مللي ثانية. لكنه لا يثبت وحده أي دالة بعينها سمحت لتلك المدة بالعبور. هذا استنتاج قوي عن الآلية، وليس ترخيصًا لاختراع سطر كود لم نرصده.

ساعة الإدخال 1000 Hz لم تكن الخطأ

أعدت اختبار هذا الجزء من المعمارية بصورة مستقلة باستخدام FFmpeg 7.1.5 وffconcat. مع:

duration 0.010
option framerate 1000

حافظت الطوابع الزمنية للحزم على مواضع المللي ثانية المقصودة:

0 ms
10 ms
20 ms
30 ms

أما استخدام معدل إدخال 30 في تلك المرحلة المبكرة فكمّم التوقيت نفسه تقريبًا إلى 0 و33.3 مللي ثانية.

إذًا كان إدخال 1000 Hz يؤدي وظيفته الصحيحة: الحفاظ على مدد المصدر الموثوقة بدقة 1 ms. ولم يكن يعني أن الفيديو النهائي سيعمل عند 1000 إطار/ثانية.

المعمارية الصحيحة تحتاج حدًا صريحًا:

source delays with 1 ms precision
        ↓
authoritative source timeline
        ↓
choose target CFR
        ↓
quantize explicitly to CFR grid
        ↓
preserve that grid through encode and mux

الدقة العالية في الإدخال ليست المشكلة. المشكلة هي ترك الانتقال من توقيت المصدر إلى CFR ضمنيًا أو موزعًا على أكثر من مرحلة.

CFR هو تكميم زمني متحكم به

المصدر الحقيقي ذو 62/63 مللي ثانية يطابق 16 إطارًا/ثانية طبيعيًا على أزواج الإطارات: كلاهما يغطي 125 مللي ثانية. لكن ذلك لا يعني أن الملف النهائي يستطيع الاحتفاظ بالمدد 62، 63، 62، 63 مللي ثانية ثم يكتفي بالإعلان أنه «16 fps». هذه مدد متغيرة.

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

مرشح fps في FFmpeg مكان منطقي لتنفيذ هذا التحويل. فهو يبني معدل الإطارات المطلوب بإسقاط الإطارات أو تكرارها وفق طوابعها الزمنية وسياسة التقريب المختارة.

وبعد إنشاء الشبكة المقصودة لا أريد مرحلة لاحقة تقوم من تلقاء نفسها بتحويل آخر لمعدل الإطارات. ينبغي أن يكون في الخط حد واحد مقصود للتكميم، لا طبقات متعددة تتخذ قرارات تقريب مستقلة.

لماذا لم تغيّر ثلاث محاولات إعادة أي شيء

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

هذه أيضًا قاعدة موثوقية تتجاوز الفيديو. إعادة المحاولة مناسبة للأعطال التي قد تختفي بين محاولة وأخرى: مشكلة شبكة، تعذر مؤقت في التخزين، ضغط موارد، خدمة خارجية غير متاحة.

لكنها لا تصلح خرقًا حتميًا للعقد ينتجه الإدخال نفسه والخوارزمية نفسها.

أفصل الآن بين ثلاث فئات على الأقل:

  • عطل عابر — قد تساعد إعادة المحاولة؛
  • إدخال غير صالح — يعاد توجيهه أو يرفض؛
  • خرق حتمي للثوابت — أوقف الإعادة وافحص خط المعالجة.

القيمة المتكررة 5580 تنتمي إلى الفئة الثالثة.

ثم ظهرت 3751 بدل 3750

عند 24 إطارًا/ثانية تكون القيمة المتوقعة دقيقة بلا تقريب:

90000 / 24 = 3750 ticks

لكن خرجًا حقيقيًا بعد الدمج احتوى حزمة مدتها:

3751 ticks

الفرق لم يتجاوز:

1 / 90000 s ≈ 11.111 µs

لن يلاحظ أي مشاهد نبضة واحدة. ولهذا كان إغراء جعل المدقّق يقبل ±1 قويًا.

لم أفعل ذلك. عند 24 إطارًا/ثانية على هذه الشبكة يمكن تمثيل 3750 بالضبط، لذا لم تكن 3751 تنازلًا ضروريًا بسبب حدود التمثيل؛ كانت دليلًا على أن ثابت الشبكة الدقيقة ضاع في مكان ما.

نسخ التدفق لا يعني أن الطوابع الزمنية لم تُمس

كان الخط يدمج مقاطع H.264 المتوافقة من دون جيل ترميز ضياعي ثانٍ:

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

تعني -c:v copy أن حمولة H.264 المضغوطة لا تُفك ثم تُرمز من جديد. لكنها لا تعني أن طبقة الدمج لا تقوم بأي عمل زمني.

يوضح توثيق concat صراحة أن مدة كل ملف تستخدم لضبط طوابع الملف التالي. كما يعيد FFmpeg تحجيم الطوابع الصحيحة بين قواعد زمن كسرية، وتوفر libavutil دوال إعادة التحجيم مع أوضاع تقريب صريحة.

هذا لا يثبت أن concat كان السبب الوحيد الممكن لقيمة 3751 التي رأيتها. لكنه يثبت أن الاستنتاج «نُسخ البت ستريم، إذًا لا يمكن أن تتغير طوابع الحزم» نموذج ذهني خاطئ.

تطابق الصور المضغوطة وتطابق شبكة الطوابع خاصيتان منفصلتان.

لا يمكن إصلاح PTS وDTS بمعادلة جميلة واحدة

من المغري كتابة إصلاح من هذا النوع:

PTS = N * frame_duration
DTS = N * frame_duration

وقد يكون ذلك خطأ مع H.264 الذي يستخدم إطارات B، لأن ترتيب فك الصور قد يختلف عن ترتيب عرضها.

الهدف الصحيح أضيق: استخدام المخطط الزمني الموثوق للمقطع لاستعادة شبكة العرض ومدد الحزم المعروفة، مع الحفاظ على علاقة قانونية لترتيب فك الترميز.

لهذا أيضًا لا أنشر «صيغة setts سحرية» منزوعة من سياقها. الصيغة الصحيحة تعتمد على حدود المقاطع، والمعدلات المختارة، وبيانات الزمن التي بُني منها المخطط النهائي.

لماذا استخدمت setts

يمكن لمرشح setts في FFmpeg تغيير PTS وDTS ومدة الحزمة وقاعدة الزمن من دون فك الفيديو وإعادة ترميزه.

لذلك كان طبقة مناسبة للتطبيع بعد الدمج: اترك حمولة H.264 كما هي، وأعد فرض مخطط الحزم المشتق من بيانات المقاطع المعروفة.

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

هذا مختلف جذريًا عن قاعدة «إذا كانت المدة 3751 فاطرح واحدًا». الإصلاح يجب أن ينبع من نموذج الزمن، لا من رسالة الخطأ التي ظهرت اليوم.

لماذا رفضت هامش ±1 نبضة

الهوامش الصغيرة صحيحة في أنظمة كثيرة. إذا تعذر تمثيل إيقاع ما بدقة في قاعدة زمن صحيحة معينة، فعلى المدقّق نمذجة نمط التقريب الضروري.

لكن عقدي مختلف: قيدت المعدلات عمدًا بحيث تكون 90000 / fps عددًا صحيحًا:

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

حين تكون المدة المتوقعة قابلة للتمثيل بدقة، يحول هامش ±1 العام خرقًا غير مفسر للثابت إلى حالة مقبولة.

النبضة الواحدة لا تهم بصريًا. فقدان العقد من دون تفسير هو المهم.

  • إذا كانت الشبكة المختارة تتطلب بالضرورة تناوب مدد صحيحة، تحقق من ذلك النمط؛
  • إذا وجب أن تكون المدة عددًا صحيحًا واحدًا، فاطلبه بدقة؛
  • ولا تستخدم ±1 كزر عام لإسكات المدقّق.

كيف أتحقق من CFR على مستوى الحزم

حقول مثل avg_frame_rate ملخصات مفيدة، لكنها غير كافية لهذا العقد.

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

أفحص قاعدة زمن التدفق الحقيقية، وقيم pts وdts وduration على مستوى كل حزمة.

وبصورة مبسطة، مدقّق CFR 16 لهذا التصميم يشبه:

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

كلمة «عادية» مهمة. قوائم التحرير، والقص، وعينة نهائية خاصة مقصودة، أو سلوك حاوية آخر قد يتطلب نمذجة صريحة. هذه ليست قاعدة عامة لكل ملف MP4.

لكن في مولد أتحكم في بنيته، تكون الصرامة أكثر فائدة بكثير من القول «يبدو قريبًا من 16 fps».

فحص البيانات الوصفية وفك الترميز الكامل يجيبان عن سؤالين مختلفين

الطوابع المثالية لا تثبت أن كامل تدفق H.264 قابل للفك. والعكس صحيح: قد يفك الملف تمامًا ومع ذلك يخالف عقد الزمن لدي.

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 -

ولا يصبح الملف صالحًا للنشر إلا بعد اجتياز المرحلتين. نجاح FFmpeg برمز خروج صفر لم يعد تعريفي لكلمة «انتهى».

ما الذي أثبتته الحالتان فعلًا

بالنسبة إلى 5580 أستطيع تأكيد ظهور القيمة الحتمية نفسها عبر المحاولات؛ وأن 5580 عند 90 kHz تساوي 62 مللي ثانية تمامًا؛ وأن توقيت المصدر الحقيقي احتوى مدد 62/63 مللي ثانية؛ وأن CFR 16 يحتاج 62.5 مللي ثانية أو 5625 نبضة؛ وأن تجربة إدخال 1000 Hz حافظت على مدد المللي ثانية بصورة صحيحة.

لذلك فإن تسرب مدة المصدر إلى مرحلة كان ينبغي أن تكون CFR بالفعل تفسير قوي. لكن سطر الخطأ المحفوظ وحده لا يثبت أي دالة سببت ذلك.

أما 3751 فأستطيع تأكيد أن خرجًا حقيقيًا احتوى حزمة 3751 حيث يتطلب 24 fps القيمة 3750؛ وأن الخط استخدم دمجًا مع نسخ التدفق؛ وأن تطبيع الحزم بعد الدمج بواسطة setts صار جزءًا من الحل؛ وأن FFmpeg يوثق تعديل concat للطوابع وإعادة كتابة توقيت الحزم.

هذه الوقائع تتفق مع تقريب يحدث أثناء إعادة تحجيم الأعداد الصحيحة أو عند حدود الدمج/التغليف، لكنها لا تسمح بقاعدة عامة من نوع «concat يضيف دائمًا نبضة».

الخط الذي أستخدمه الآن

  1. استرجاع مخطط زمني موثوق للمصدر بدل الثقة بحقل FPS ملخص.
  2. الحفاظ على مدد المصدر بالمللي ثانية على ساعة إدخال دقيقة بما يكفي.
  3. اختيار CFR الهدف بصورة منفصلة.
  4. تكميم مخطط المصدر صراحة على شبكة CFR.
  5. استخدام مقياس مسار يمثل المعدلات المسموح بها بدقة كلما أمكن.
  6. الترميز من دون مطالبة مراحل لاحقة بإعادة تحويل معدل الإطارات مستقلًا.
  7. فحص قاعدة الزمن وعدد الإطارات وPTS/DTS ومدد الحزم قبل الدمج.
  8. فحص توافق إعدادات الترميز والزمن قبل concat مع نسخ التدفق.
  9. فحص شبكة الحزم مجددًا بعد الدمج، وعدم اعتبار -c:v copy دليلًا على ثبات الطوابع.
  10. إذا لزم التطبيع، اشتقاقه من المخطط المعروف وتطبيقه على مستوى الحزم.
  11. إعادة التحقق من كل الحزم.
  12. فك الملف النهائي كاملًا.
  13. النشر الذري فقط بعد نجاح العقد.

القاعدة التي بقيت معي: CFR عقد زمني صحيح الأعداد

كنت أتعامل مع 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 مهمة تحديدًا لأنها تكاد لا تُرى: نبضة واحدة كانت كافية لإثبات أن النظام لم يعد يحافظ على ثابت صمم أصلًا للحفاظ عليه.

قاعدتي الآن بسيطة: لا تتحقق من وسم «CFR». تحقق من الزمن الذي يفترض أن يجعل ذلك الوسم صحيحًا.

المراجع الأساسية