กลับไปที่บล็อก
13 สิงหาคม 2569Sergei Solod27 นาทีในการอ่าน

หนึ่ง tick ที่ทำให้ CFR ของผมพัง: ทำไม 5580 ไม่ใช่ 5625 และ 3751 ไม่ใช่ 3750

ตัวตรวจสอบของผมปฏิเสธ MP4 16 fps ซ้ำ ๆ เพราะแพ็กเก็ตหนึ่งมีระยะเวลา 5580 tick แทนที่จะเป็น 5625 ต่อมายังพบ 3751 tick ในจุดที่ 24 fps ควรเป็น 3750 พอดี สองเหตุการณ์นี้ทำให้ผมแยกการกำหนดเวลาของต้นทาง การควอนไทซ์เป็น CFR ฐานเวลาของ MP4, PTS/DTS, การมัลติเพล็กซ์ และการตรวจสอบระดับแพ็กเก็ตออกจากกันอย่างชัดเจน

FFmpegH.264CFRไทม์สแตมป์วิดีโอPTS และ DTSการตรวจสอบ MP4

วันหนึ่ง ระบบประมวลผลสื่อของผมหยุดเผยแพร่ผลลัพธ์บางส่วน เพราะเจอข้อผิดพลาดที่ตอนแรกดูแทบจะไร้สาระ:

Invalid CFR packet duration: 5580 ticks, expected 5625

ตัวถอดรหัสไม่ได้ล้ม และ FFmpeg ก็สร้างไฟล์ H.264 ได้สำเร็จ แต่ตัวตรวจสอบหลังการเข้ารหัสปฏิเสธไฟล์ เพราะเอาต์พุตควรเป็นวิดีโออัตราเฟรมคงที่ ทว่าแพ็กเก็ตหนึ่งไม่อยู่บนกริดเวลาที่ผมกำหนดไว้

ผมรันงานใหม่ ได้ 5580 เหมือนเดิม รันอีกครั้งก็ยังเป็น 5580 ต่อมาซอร์สอีกไฟล์ก็ล้มเหลวด้วยค่าเดียวกัน นั่นเป็นหลักฐานสำคัญว่าไม่ใช่ปัญหาเครือข่ายชั่วคราวหรือ race condition ที่เกิดขึ้นนาน ๆ ครั้ง แต่เป็นการละเมิดข้อกำหนดที่เกิดซ้ำแบบกำหนดแน่นอน

ต่อมาผมเจอความล้มเหลวอีกแบบหนึ่งที่ดูคล้ายกัน แต่ทางเทคนิคต่างกัน ที่ 24 fps และวิดีโอแทร็กซึ่งมี timescale 90,000 ตัวอย่างปกติควรมีระยะเวลาเท่ากับ 3750 ทิกพอดี แต่ตัวตรวจสอบพบ 3751

สองกรณีนี้ไม่ควรถูกเหมารวมด้วยประโยคว่า “FFmpeg แค่ปัดเศษ” 5580 กับ 5625 ต่างกัน 45 ทิก หรือ 0.5 ms พอดี ส่วน 3751 กับ 3750 ต่างกันเพียง 1 ทิก หรือประมาณ 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

การตรวจแบบเท่ากันเป๊ะนี้สมเหตุผล เพราะผมตั้งใจเลือกอัตราเฟรมที่หาร timescale ลงตัว หากจังหวะเป้าหมายไม่สามารถแทนด้วยระยะเวลาจำนวนเต็มเพียงค่าเดียว ตัวตรวจสอบที่ถูกต้องต้องตรวจรูปแบบจำนวนเต็มที่อนุญาต ไม่ใช่บังคับค่าที่เป็นไปไม่ได้

อัตราเฟรม, time base และ MP4 timescale เป็นคนละเรื่อง

  • อัตราเฟรม บอกจังหวะการแสดงผล CFR 16 หมายถึงแสดงหนึ่งเฟรมทุก 62.5 ms
  • FFmpeg time base คือระยะเวลาของหนึ่งหน่วย timestamp ที่เป็นจำนวนเต็ม เช่น 1/90000 วินาที
  • MP4 track timescale บอกจำนวนหน่วยเวลาต่อวินาที ถ้า timescale เท่ากับ 90,000 หนึ่งหน่วยจะยาว 1/90000 วินาที
  • PTS บอกว่าภาพควรถูกแสดงเมื่อไร
  • DTS บอกว่าแพ็กเก็ตที่เข้ารหัสแล้วต้องถูกถอดรหัสเมื่อไร
  • ระยะเวลาแพ็กเก็ต บอกระยะเวลาของตัวอย่างใน time base ของสตรีม

เมื่อมี B-frame ค่า PTS และ DTS สามารถต่างกันได้อย่างถูกต้อง ดังนั้นการ “แก้ timestamp” ด้วยการตั้ง PTS = DTS ตรง ๆ อาจทำให้ลำดับการถอดรหัสเสีย เอกสารทางการของ setts ก็ระบุว่าไม่แนะนำวิธีนี้เมื่อมี B-frame

ตัวตรวจสอบของผมจึงตรวจว่าความสัมพันธ์ระหว่างลำดับการถอดรหัสกับลำดับการแสดงผลถูกต้องหรือไม่ แทนที่จะบังคับให้ PTS และ DTS เท่ากัน

เหตุผลที่ผมเลือก timescale 90,000 ทิก/s

90,000 ไม่ใช่ตัวเลขวิเศษที่เหมาะกับทุกวิดีโอ สำหรับระบบของผม มันมีประโยชน์เพราะอัตราเฟรมทุกค่าที่อนุญาตให้ระยะเวลาเฟรมเป็นจำนวนเต็มพอดี:

อัตราเฟรมระยะเวลาเฟรมที่ 90,000 ทิก/s
10 fps9000 ทิก
12 fps7500 ทิก
15 fps6000 ทิก
16 fps5625 ทิก
18 fps5000 ทิก
20 fps4500 ทิก
24 fps3750 ทิก
25 fps3600 ทิก
30 fps3000 ทิก

มันยังสัมพันธ์กับเวลาต้นทางระดับมิลลิวินาทีอย่างสะดวกด้วย:

1 ms = 90 ticks

MP4 muxer ของ FFmpeg มีตัวเลือก video_track_timescale จึงสามารถกำหนดกริดของแทร็กนี้ได้อย่างชัดเจน แต่การตั้ง timescale ไม่ได้ทำให้ timing ถูกต้องโดยอัตโนมัติ มันเพียงทำให้ข้อกำหนดตรวจวัดได้อย่างแม่นยำ

5580 บอกผมว่าควรเริ่มตรวจตรงไหน

เมื่อแปลงความล้มเหลวครั้งแรกกลับเป็นหน่วยเวลา ตัวเลขก็เริ่มมีความหมาย:

5580 / 90000 = 0.062 s = 62 ms

ค่านี้ไม่ใช่ตัวเลขสุ่ม ผมมีซอร์สแอนิเมชันจริงที่แสดง 49 เฟรมในเวลา 3.063 วินาที โดยดีเลย์ของเฟรมสลับระหว่าง 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 ที่สมเหตุผลมากบนกริดความละเอียด 1 ms

แต่หลังจากเลือก CFR 16 แล้ว เอาต์พุตต้องอยู่บนกริดเป้าหมาย: เฟรมปกติแต่ละเฟรมต้องยาว 62.5 ms หรือ 5625 ทิก

ดังนั้น 5580 จึงเป็นเบาะแสที่ชัดว่าระยะเวลา 62 ms จากซอร์สอาจหลุดรอดมาถึงขั้นตอนที่ timing ควรถูกควอนไทซ์เป็น CFR เรียบร้อยแล้ว

ผมแยกสิ่งที่พิสูจน์ได้ออกจากข้อสันนิษฐานอย่างชัดเจน: บันทึกยืนยันว่า 5580 ที่ 90 kHz เท่ากับ 62 ms พอดี และซอร์สจริงมีดีเลย์ 62/63 ms แต่บรรทัดในล็อกเพียงบรรทัดเดียวไม่ได้พิสูจน์ว่าฟังก์ชันใดเป็นจุดแรกที่ปล่อยค่า 62 ms ผ่านมา

นาฬิกาอินพุต 1000 Hz ไม่ใช่ต้นเหตุ

ผมทำซ้ำส่วนนี้แยกต่างหากด้วย FFmpeg 7.1.5 และ ffconcat โดยใช้อินพุต:

duration 0.010
option framerate 1000

timestamp ของแพ็กเก็ตยังคงตำแหน่งมิลลิวินาทีตามที่ตั้งใจไว้:

0 ms
10 ms
20 ms
30 ms

แต่ถ้าใช้ input rate 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

อินพุตที่ละเอียดไม่ใช่บั๊ก ปัญหาคือการไม่มีขอบเขตที่ชัดเจนระหว่างการรักษาเวลาต้นทางกับการแปลงไปเป็น 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

ฟิลเตอร์ fps ของ FFmpeg เป็นจุดที่เหมาะสำหรับการแปลงนี้ มันสร้างอัตราเฟรมเป้าหมายโดยทิ้งหรือทำซ้ำเฟรมตาม timestamp ของอินพุตและนโยบายการปัดเศษที่เลือก

เมื่อขั้นตอนนี้สร้างกริดเป้าหมายแล้ว ผมไม่ต้องการให้ขั้นตอนถัดไปทำการแปลงอัตราเฟรมอีกครั้งแบบอิสระ ระบบควรมีขอบเขตการควอนไทซ์ที่ตั้งใจไว้เพียงจุดเดียว

ทำไมลองใหม่สามครั้งก็ไม่เปลี่ยนอะไร

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

นี่เป็นบทเรียนด้านความน่าเชื่อถือที่ใช้ได้มากกว่าเรื่องวิดีโอ การลองใหม่เหมาะกับความล้มเหลวที่อาจหายไปในการลองครั้งถัดไป เช่น ปัญหาเครือข่ายชั่วคราว ที่เก็บข้อมูลไม่พร้อมใช้งานชั่วคราว ความกดดันด้านทรัพยากร หรือ dependency ที่ล่มชั่วคราว

แต่การลองใหม่ไม่สามารถแก้การละเมิดข้อกำหนดแบบกำหนดแน่นอนที่เกิดจากอินพุตเดิมและอัลกอริทึมเดิมได้

  • ความล้มเหลวชั่วคราว — ลองใหม่อาจช่วยได้
  • อินพุตไม่ถูกต้อง — แยกไปใช้เส้นทางอื่นหรือปฏิเสธอินพุต
  • การละเมิด invariant แบบกำหนดแน่นอน — หยุดลองใหม่แล้ววิเคราะห์ระบบประมวลผล

5580 ที่เกิดซ้ำอยู่ในกลุ่มที่สาม

จากนั้นผมเจอ 3751 แทน 3750

ที่ 24 fps ค่าที่คาดไว้เป็นจำนวนเต็มพอดี:

90000 / 24 = 3750 ticks

แต่ในเอาต์พุตจริงที่ผ่านการ concat แล้ว มีแพ็กเก็ตหนึ่งที่มีระยะเวลา:

3751 ticks

ความต่างมีเพียง:

1 / 90000 s ≈ 11.111 µs

ไม่มีผู้ชมคนไหนสังเกตเห็นความต่าง 1 ทิกนี้ได้ นั่นทำให้การผ่อนตัวตรวจสอบเป็น ±1 ดูน่าดึงดูด

ผมไม่ทำเช่นนั้น เพราะบนกริดนี้ 24 fps แทนได้พอดีด้วย 3750 ดังนั้น 3751 ไม่ใช่ความคลาดเคลื่อนที่หลีกเลี่ยงไม่ได้จากการแทนเวลา แต่เป็นหลักฐานว่า invariant ของกริดที่ควรแม่นยำได้หายไประหว่างทาง

การคัดลอกสตรีมไม่ได้หมายความว่า timestamp จะไม่ถูกแตะ

ระบบประมวลผล concat เซกเมนต์ H.264 ที่เข้ากันได้โดยไม่ทำการเข้ารหัสแบบสูญเสียรอบที่สอง:

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

-c:v copy หมายถึงข้อมูล H.264 ที่บีบอัดแล้วจะไม่ถูกถอดรหัสและเข้ารหัสใหม่ ไม่ได้หมายความว่าชั้น muxing ไม่มีงานด้าน timing ต้องทำ

เอกสารของ concat demuxer ระบุชัดว่าระยะเวลาของแต่ละไฟล์ถูกใช้เพื่อปรับ timestamp ของไฟล์ถัดไป FFmpeg ยัง rescale timestamp จำนวนเต็มระหว่าง time base แบบจำนวนตรรกยะ และ libavutil ก็มีฟังก์ชันที่กำหนดโหมดการปัดเศษได้อย่างชัดเจน

ข้อมูลนี้ไม่ได้พิสูจน์ว่า concat เป็นสาเหตุเดียวของกรณี 3751 ของผม แต่เพียงพอที่จะบอกว่าแนวคิด “คัดลอก bitstream แล้ว timestamp ของแพ็กเก็ตจึงเปลี่ยนไม่ได้” นั้นผิด

ความเหมือนของข้อมูลภาพที่บีบอัดกับความเหมือนของกริด timestamp เป็นคนละคุณสมบัติ

PTS และ DTS แก้ด้วยสมการสวย ๆ สมการเดียวไม่ได้

PTS = N * frame_duration
DTS = N * frame_duration

สูตรนี้อาจผิดสำหรับ H.264 ที่มี B-frame เพราะลำดับการถอดรหัสกับลำดับการแสดงผลอาจไม่เหมือนกัน

เป้าหมายที่ถูกต้องแคบกว่านั้น: ใช้ timeline ของเซกเมนต์ที่เชื่อถือได้เพื่อคืนกริดการแสดงผลและระยะเวลาแพ็กเก็ตที่ทราบอยู่แล้ว โดยยังรักษาความสัมพันธ์ของลำดับการถอดรหัสที่ถูกต้อง

นี่คือเหตุผลที่ผมไม่เผยแพร่ “นิพจน์ setts วิเศษ” แบบไม่สนบริบท นิพจน์ที่ถูกต้องขึ้นอยู่กับขอบเขตเซกเมนต์ อัตราที่เลือก และ metadata ด้าน timing ที่ใช้สร้าง timeline สุดท้าย

ทำไมผมใช้ setts

ฟิลเตอร์ bitstream setts ของ FFmpeg สามารถเปลี่ยน PTS, DTS, ระยะเวลา และ output time base ของแพ็กเก็ตได้โดยไม่ต้องถอดรหัสและเข้ารหัสวิดีโอใหม่

จึงเหมาะกับการ normalize หลัง concat: เก็บข้อมูล H.264 ที่บีบอัดไว้เหมือนเดิม แต่บังคับ timeline ของแพ็กเก็ตใหม่จาก metadata ของเซกเมนต์ที่ทราบแน่นอน

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

นี่ต่างจาก “ถ้าระยะเวลาเป็น 3751 ให้ลบ 1” อย่างสิ้นเชิง การแก้ต้องมาจากโมเดล timing ไม่ใช่จากข้อความผิดพลาดที่บังเอิญเห็นในวันนี้

ทำไมผมไม่ยอมรับค่าคลาดเคลื่อน ±1 ทิก

ระบบจำนวนมากควรมีค่าคลาดเคลื่อนเล็กน้อย หากจังหวะไม่สามารถแทนได้อย่างแม่นยำใน time base จำนวนเต็มที่เลือก ตัวตรวจสอบต้องเข้าใจรูปแบบการปัดเศษที่จำเป็น

แต่ข้อกำหนดของผมต่างออกไป ผมจำกัดอัตราที่อนุญาตไว้ให้ 90000 / fps เป็นจำนวนเต็ม:

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

เมื่อระยะเวลาที่คาดไว้แทนได้อย่างแม่นยำ การยอม ±1 แบบทั่วไปเพียงเปลี่ยนการละเมิด invariant ที่ยังอธิบายไม่ได้ให้กลายเป็นสถานะที่ยอมรับ

หนึ่งทิกไม่สำคัญต่อภาพ แต่การสูญเสียข้อกำหนดที่ตั้งใจออกแบบไว้โดยไม่รู้สาเหตุเป็นเรื่องสำคัญ

  • ถ้ากริดจำเป็นต้องสลับระยะเวลาจำนวนเต็มหลายค่า ให้ตรวจรูปแบบที่ถูกต้อง
  • ถ้าระยะเวลาต้องเป็นจำนวนเต็มค่าเดียวแบบแม่นยำ ให้บังคับค่าจำนวนเต็มนั้น
  • อย่าใช้ ±1 เป็นสวิตช์สากลเพื่อทำให้ตัวตรวจสอบผ่าน

ผมตรวจ CFR ที่ระดับแพ็กเก็ตอย่างไร

avg_frame_rate และ metadata ระดับสตรีมที่คล้ายกันมีประโยชน์ในฐานะข้อมูลสรุป แต่ยังไม่เพียงพอสำหรับข้อกำหนดนี้

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

ผมตรวจ 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 list, การตัดแต่ง, ตัวอย่างสุดท้ายที่ตั้งใจให้พิเศษ หรือพฤติกรรมอื่นของคอนเทนเนอร์ อาจต้องถูกจำลองไว้อย่างชัดเจน ผมไม่ได้เสนอเงื่อนไขนี้เป็นกฎสากลสำหรับ MP4 ทุกไฟล์

แต่สำหรับตัวสร้างไฟล์ที่ผมควบคุมโครงสร้างเอง การตรวจแบบเข้มงวดมีประโยชน์กว่าการบอกว่า “ดูเหมือนประมาณ 16 fps” มาก

การตรวจ metadata กับการถอดรหัสทั้งไฟล์ตอบคนละคำถาม

timestamp ที่สมบูรณ์ไม่ได้พิสูจน์ว่า H.264 stream ทั้งหมดถอดรหัสได้ และไฟล์ที่ถอดรหัสได้ครบก็ยังอาจละเมิดข้อกำหนดด้าน timing

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 -

ไฟล์จะเผยแพร่ได้ก็ต่อเมื่อผ่านทั้งสองขั้นตอนแล้วเท่านั้น exit code 0 ของ encoder ไม่ได้แปลว่า “เสร็จ” สำหรับผมอีกต่อไป

สองความล้มเหลวนี้พิสูจน์อะไรได้จริง

สำหรับ 5580 ผมยืนยันได้ว่าค่าเดิมเกิดซ้ำแบบกำหนดแน่นอนในการลองใหม่; 5580 ที่ 90 kHz เท่ากับ 62 ms พอดี; ซอร์สจริงมีดีเลย์ 62/63 ms; CFR 16 ต้องใช้ 62.5 ms หรือ 5625 ทิก; และการทดลองอินพุต 1000 Hz รักษาดีเลย์ระดับมิลลิวินาทีได้ถูกต้อง

ข้อมูลเหล่านี้ทำให้คำอธิบายว่า “ระยะเวลาจากซอร์สรั่วมาถึงขั้นตอนที่ควรเป็น CFR แล้ว” มีน้ำหนักมาก แต่บรรทัด error ที่เก็บไว้เพียงอย่างเดียวไม่ได้พิสูจน์ว่าฟังก์ชันใดเป็นต้นเหตุ

สำหรับ 3751 ผมยืนยันได้ว่าเอาต์พุตจริงมีแพ็กเก็ต 3751 ทิกในจุดที่ 24 fps ต้องเป็น 3750; ระบบใช้ stream-copy concat; หลัง concat การ normalize timeline ของแพ็กเก็ตด้วย setts กลายเป็นส่วนหนึ่งของวิธีแก้; และเอกสาร FFmpeg ระบุทั้งการปรับ timestamp ของ concat และการเขียนค่า timestamp ระดับแพ็กเก็ตใหม่

ข้อเท็จจริงเหล่านี้สอดคล้องกับการ rescale จำนวนเต็มหรือการปัดเศษที่ขอบเขต muxing/concat แต่ไม่พอจะอ้างว่า “concat เพิ่ม 1 ทิกเสมอ”

ขั้นตอนงานที่ผมใช้ตอนนี้

  1. สร้าง timeline ของซอร์สที่เชื่อถือได้ แทนการเดาจากฟิลด์ FPS เพียงค่าเดียว
  2. เก็บดีเลย์ต้นฉบับระดับมิลลิวินาทีไว้บนอินพุต clock ที่ละเอียดพอ
  3. เลือก CFR เป้าหมายเป็นการตัดสินใจแยกต่างหาก
  4. ควอนไทซ์ timeline ของซอร์สลงบนกริด CFR อย่างชัดเจน
  5. ใช้ track timescale ที่แทนอัตราที่อนุญาตได้อย่างแม่นยำเมื่อทำได้
  6. เข้ารหัสโดยไม่ให้ขั้นตอนถัดไปทำการแปลงอัตราเฟรมซ้ำเองอย่างอิสระ
  7. ก่อน concat ตรวจ time base, จำนวนเฟรม, PTS/DTS และระยะเวลาแพ็กเก็ต
  8. ก่อน stream-copy concat ตรวจการตั้งค่า codec และความเข้ากันได้ด้าน timing
  9. หลัง concat ตรวจกริดแพ็กเก็ตอีกครั้ง และไม่ถือว่า -c:v copy เป็นหลักฐานว่า timestamp ไม่เปลี่ยน
  10. ถ้าต้อง normalize ให้คำนวณจาก timeline ที่ทราบอยู่แล้วและทำในระดับแพ็กเก็ต
  11. ตรวจทุกแพ็กเก็ตอีกครั้ง
  12. ถอดรหัสไฟล์สุดท้ายทั้งหมด
  13. เผยแพร่แบบ atomic หลังจากข้อกำหนดทั้งหมดผ่านแล้วเท่านั้น

กฎที่ผมยึดไว้: CFR คือข้อกำหนด timing แบบจำนวนเต็ม

เมื่อก่อน 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 สำคัญเพราะแทบมองไม่เห็น: แค่ 1 ทิกก็พอพิสูจน์ว่าระบบหยุดรักษา invariant ที่ถูกออกแบบให้ต้องคงอยู่

กฎของผมตอนนี้ง่ายมาก: อย่าตรวจเพียงป้าย “CFR” ให้ตรวจ timing ที่ควรทำให้ป้ายนั้นเป็นจริง

เอกสารหลัก