Một ngày, quy trình xử lý đa phương tiện của tôi ngừng xuất bản một phần đầu ra vì một lỗi nhìn qua gần như vô lý:
Invalid CFR packet duration: 5580 ticks, expected 5625
Bộ giải mã không bị lỗi. FFmpeg đã tạo tệp H.264. Chính bộ kiểm tra sau mã hóa từ chối tệp vì đầu ra được thiết kế là tốc độ khung hình cố định nhưng một gói không nằm trên lưới thời gian mà tôi đã đặt ra.
Tôi chạy lại tác vụ. Vẫn 5580. Chạy thêm lần nữa, vẫn 5580. Sau đó một nguồn khác cũng lỗi với đúng con số ấy. Đây là bằng chứng quan trọng: không phải mạng chập chờn hay một điều kiện tranh chấp hiếm gặp. Vi phạm có tính xác định.
Về sau tôi gặp lỗi thứ hai trông tương tự nhưng cơ chế khác. Với 24 fps và rãnh video có thang thời gian 90.000, thời lượng của mỗi mẫu bình thường phải đúng 3750 ticks. Bộ kiểm tra tìm thấy 3751.
Không nên gộp hai số này vào một câu mơ hồ kiểu “FFmpeg làm tròn”. 5580 và 5625 lệch 45 ticks, đúng 0,5 ms. 3751 và 3750 chỉ lệch một tick, khoảng 11,1 microgiây.
Hai lỗi buộc tôi tách bốn khái niệm mà trước đó dễ bị trộn dưới chữ “FPS”: tốc độ khung hình, cơ sở thời gian, PTS/DTS và gói thời lượng.
CFR không chỉ là một nhãn ghi “16 fps”
Bây giờ khi gọi một tệp được tạo là CFR, tôi không chỉ dựa vào việc ffprobe in ra 16/1 hay 24/1.
Trong chuỗi xử lý này, CFR là một quy ước chặt hơn: thời điểm trình chiếu nằm trên một lưới đều và thời lượng của mỗi mẫu video bình thường bằng đúng một bước trên lưới đó.
Ở 16 fps, một khung hình kéo dài:
1 / 16 = 0.0625 s = 62.5 ms
Trên rãnh video 90.000 ticks/s, cùng khoảng thời gian đó là số nguyên chính xác:
90000 / 16 = 5625 ticks
Vì vậy 5625 không phải hằng số tùy ý của bộ kiểm tra. Nó xuất phát trực tiếp từ hai lựa chọn trong quy ước đầu ra: 16 fps và 90,000 ticks/s.
Yêu cầu chính xác như vậy chỉ hợp lý vì tôi cố tình chọn những tốc độ chia thang thời gian không dư. Nếu nhịp hiển thị mục tiêu không thể biểu diễn bằng một số nguyên thời lượng, bộ kiểm tra đúng phải mô hình hóa mẫu số nguyên hợp lệ thay vì ép một giá trị không thể tồn tại.
Tốc độ khung hình, cơ sở thời gian và thang thời gian MP4 là những khái niệm khác nhau
- Tốc độ khung hình mô tả nhịp trình chiếu. CFR 16 nghĩa là một khung hình được hiển thị mỗi 62,5 ms.
- Cơ sở thời gian của FFmpeg là độ dài của một đơn vị số nguyên dấu thời gian, ví dụ
1/90000giây. - Thang thời gian của rãnh MP4 diễn đạt theo số đơn vị mỗi giây. 90.000 nghĩa là mỗi đơn vị dài
1/90000giây. - PTS nói hình ảnh được trình chiếu khi nào.
- DTS nói gói đã mã hóa cần giải mã khi nào.
- Thời lượng gói biểu diễn độ dài mẫu theo cơ sở thời gian của luồng.
Với khung B, PTS và DTS có thể khác nhau hoàn toàn hợp lệ. Vì vậy “sửa dấu thời gian” bằng cách đặt thẳng PTS = DTS là nguy hiểm; tài liệu chính thức của setts cũng nói không nên làm vậy khi có khung B.
Bộ kiểm tra của tôi kiểm tra quan hệ hợp lệ giữa giải mã và trình chiếu, chứ không buộc hai dấu thời gian phải bằng nhau.
Vì sao tôi chọn rãnh 90.000 ticks/s
90.000 không phải con số thần kỳ cho mọi video. Nó phù hợp với chuỗi xử lý của tôi vì mọi tốc độ khung hình được phép đều tạo thời lượng số nguyên chính xác:
| Tốc độ | Thời lượng khung hình ở 90.000 ticks/s |
|---|---|
| 10 fps | 9000 ticks |
| 12 fps | 7500 ticks |
| 15 fps | 6000 ticks |
| 16 fps | 5625 ticks |
| 18 fps | 5000 ticks |
| 20 fps | 4500 ticks |
| 24 fps | 3750 ticks |
| 25 fps | 3600 ticks |
| 30 fps | 3000 ticks |
Nó còn có quan hệ rất tiện với thời gian nguồn theo mili giây:
1 ms = 90 ticks
bộ ghép kênh MP4 của FFmpeg cung cấp video_track_timescale, nên tôi có thể yêu cầu lưới này rõ ràng. Lưới không tự làm thời gian đúng; nó làm quy ước trở thành thứ có thể đo và kiểm tra chính xác.
5580 chỉ cho tôi nơi cần điều tra
Lỗi đầu tiên trở nên có ý nghĩa khi đổi lại thành thời gian:
5580 / 90000 = 0.062 s = 62 ms
62 ms không phải một con số ngẫu nhiên. Tôi có một nguồn ảnh động thật với 49 khung hình được hiển thị trong 3.063 giây, với độ trễ khung hình luân phiên 62 và 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
Ở cấp nguồn, 62/63 ms là xấp xỉ rất tự nhiên của 16 fps trên lưới mili giây.
Nhưng sau khi chọn CFR 16, đầu ra phải sống trên lưới mục tiêu mới: 62,5 ms hay 5625 ticks cho mỗi khung hình bình thường.
Vì vậy 5580 là dấu hiệu mạnh rằng thời lượng nguồn 62 ms đã tồn tại tới một giai đoạn lẽ ra thời gian phải được lượng tử hóa thành CFR rồi.
Tôi giữ ranh giới bằng chứng thật rõ. Nhật ký lưu trữ chứng minh rằng 5580 ở 90 kHz đúng bằng 62 ms và nguồn thật có độ trễ 62/63 ms. Một dòng nhật ký không thể tự nó chứng minh chính xác hàm nào đã để thời lượng đó lọt qua. Đây là suy luận mạnh về cơ chế, không phải lý do để bịa ra nguyên nhân.
Đồng hồ đầu vào 1000 Hz không phải lỗi
Tôi tái hiện phần kiến trúc này riêng bằng FFmpeg 7.1.5 và ffconcat. Với:
duration 0.010
option framerate 1000
gói dấu thời gian giữ đúng các vị trí mili giây dự định:
0 ms
10 ms
20 ms
30 ms
Nếu dùng tốc độ đầu vào 30 ngay ở giai đoạn sớm này, cùng thời gian bị lượng tử hóa xấp xỉ thành 0 và 33,3 ms.
Vì vậy đồng hồ đầu vào 1000 Hz làm đúng nhiệm vụ: giữ độ trễ nguồn làm mốc với độ chính xác 1 ms. Điều đó không có nghĩa video đầu ra cuối cùng có 1000 fps.
millisecond-accurate source delays
↓
authoritative source timeline
↓
choose target CFR
↓
explicitly quantize onto CFR grid
↓
preserve that grid through encoding and muxing
Độ chính xác cao ở đầu vào không phải lỗi. Lỗi là không làm rõ ranh giới chuyển từ thời gian của nguồn sang CFR mục tiêu.
CFR là sự lượng tử hóa thời gian có kiểm soát
Nguồn 62/63 ms tự nhiên khớp 16 fps theo từng cặp khung hình: cả hai đều phủ 125 ms. Nhưng tệp cuối không thể giữ 62,63,62,63 ms rồi chỉ quảng cáo “16 fps”; đó vẫn là thời lượng biến thiên.
62 ms, 63 ms, 62 ms, 63 ms
↓
62.5 ms, 62.5 ms, 62.5 ms, 62.5 ms
Bộ lọc fps của FFmpeg là nơi hợp lý để thực hiện chuyển đổi này. Nó tạo tốc độ mục tiêu bằng cách bỏ hoặc lặp khung hình theo dấu thời gian đầu vào và quy tắc làm tròn đã chọn.
Khi bước này đã tạo ra lưới mong muốn, tôi không muốn một bước sau tự thực hiện thêm một phép chuyển đổi tốc độ khung hình độc lập. Chuỗi xử lý nên có một ranh giới lượng tử hóa có chủ ý, không phải nhiều lớp tự đưa ra quyết định làm tròn.
Vì sao ba lần thử lại không thay đổi gì
FAIL: duration 5580, expected 5625
retry 1/3
FAIL: duration 5580, expected 5625
retry 2/3
FAIL: duration 5580, expected 5625
Đây là bài học về độ tin cậy không chỉ áp dụng cho video. Thử lại phù hợp với những lỗi có thể biến mất ở lần thử kế tiếp: lỗi mạng, lưu trữ tạm thời không khả dụng, áp lực tài nguyên hoặc một phụ thuộc đang tạm thời không khả dụng.
Thử lại không sửa được một vi phạm quy ước có tính xác định do cùng đầu vào và cùng thuật toán tạo ra.
- Lỗi tạm thời — thử lại có thể giúp;
- Đầu vào lỗi — chuyển sang nhánh xử lý khác hoặc từ chối;
- Lỗi bất biến có tính xác định — dừng thử lại và chẩn đoán chuỗi xử lý.
5580 lặp lại thuộc nhóm thứ ba.
Rồi tôi gặp 3751 thay vì 3750
Ở 24 fps, giá trị mong đợi được biểu diễn chính xác:
90000 / 24 = 3750 ticks
Nhưng một đầu ra thực tế sau khi nối lại chứa một gói có thời lượng:
3751 ticks
Chênh lệch chỉ:
1 / 90000 s ≈ 11.111 µs
Không người xem nào nhận ra một tick. Vì thế nới bộ kiểm tra thành ±1 rất hấp dẫn.
Tôi không làm vậy. Trên lưới này, 24 fps có thể được biểu diễn chính xác bằng 3750. Vì vậy 3751 không phải hệ quả bắt buộc của việc biểu diễn; nó là bằng chứng rằng bất biến lưới chính xác đã bị mất ở đâu đó.
Sao chép luồng không có nghĩa dấu thời gian nguyên xi
Chuỗi xử lý nối các đoạn H.264 tương thích mà không phải mã hóa lại có tổn hao lần thứ hai:
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy \
final.mp4
-c:v copy nghĩa là dữ liệu H.264 đã nén không bị giải mã rồi mã hóa lại. Điều đó không có nghĩa lớp ghép kênh không còn phải xử lý thời gian.
Tài liệu concat nói rõ thời lượng của mỗi tệp được dùng để điều chỉnh dấu thời gian của tệp tiếp theo. FFmpeg cũng đổi thang các dấu thời gian nguyên giữa những cơ sở thời gian hữu tỉ, còn libavutil cung cấp các hàm đổi thang với chế độ làm tròn rõ ràng.
Điều đó không chứng minh concat là nguyên nhân duy nhất của gói 3751 cụ thể của tôi. Nhưng nó bác bỏ cách hiểu rằng “luồng bit đã được sao chép nên dấu thời gian của gói không thể thay đổi”.
Tính đồng nhất của ảnh nén và tính đồng nhất của lưới thời gian là hai thuộc tính khác nhau.
PTS và DTS không thể sửa bằng một phương trình đẹp
PTS = N * frame_duration
DTS = N * frame_duration
Với H.264 có khung B, công thức này có thể sai vì thứ tự giải mã và thứ tự trình chiếu có thể khác nhau.
Mục tiêu đúng hẹp hơn: dùng dòng thời gian đoạn làm mốc để khôi phục lưới trình chiếu và thời lượng gói đã biết, đồng thời giữ quan hệ thứ tự giải mã hợp lệ.
Vì thế tôi không đưa ra một biểu thức setts “thần kỳ” không có ngữ cảnh. Biểu thức đúng phụ thuộc vào ranh giới đoạn, tốc độ đã chọn và siêu dữ liệu thời gian dùng để xây dựng dòng thời gian cuối.
Vì sao tôi dùng setts
Bộ lọc bitstream setts của FFmpeg có thể thay đổi PTS, DTS, thời lượng và cơ sở thời gian đầu ra của gói mà không cần giải mã rồi mã hóa lại video.
Đó là lớp phù hợp để chuẩn hóa sau khi nối: giữ nguyên dữ liệu H.264 đã nén, nhưng thiết lập lại dòng thời gian gói dựa trên siêu dữ liệu đoạn đã biết.
known segment timeline
+ known CFR
+ 90000-tick grid
↓
known valid packet positions and durations
↓
normalize packet timing
↓
validate again
Điều này hoàn toàn khác với “nếu thời lượng là 3751 thì trừ một”. Việc sửa phải xuất phát từ mô hình thời gian, không phải từ thông báo lỗi hôm nay.
Vì sao tôi từ chối dung sai ±1 tick
Dung sai nhỏ là đúng trong nhiều hệ thống. Nếu một nhịp không thể được biểu diễn chính xác trên cơ sở thời gian nguyên đã chọn, bộ kiểm tra phải mô hình hóa mẫu làm tròn thực sự cần thiết.
Quy ước của tôi khác. Tôi giới hạn các tốc độ được phép để 90000 / fps là số nguyên:
16 fps → 5625
24 fps → 3750
30 fps → 3000
Khi thời lượng mong đợi có thể biểu diễn chính xác, dung sai ±1 chung biến vi phạm bất biến chưa được giải thích thành trạng thái được chấp nhận.
Một tick không quan trọng về thị giác. Mất quy ước mà không biết lý do thì quan trọng.
- Nếu lưới bắt buộc phải xen kẽ nhiều số nguyên thời lượng, kiểm tra đúng mẫu đó;
- nếu thời lượng phải là một số nguyên chính xác, yêu cầu số nguyên ấy;
- đừng dùng ±1 như công tắc phổ quát để làm bộ kiểm tra xanh.
Tôi kiểm tra CFR ở cấp gói như thế nào
Siêu dữ liệu luồng như avg_frame_rate là bản tóm tắt hữu ích, nhưng chưa đủ cho quy ước này.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json \
final.mp4
Tôi kiểm tra cơ sở thời gian thực tế của luồng và pts, dts, duration của từng gói.
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
Cụm “gói bình thường” rất quan trọng. Edit list, cắt xén, một mẫu cuối đặc biệt được tạo có chủ ý hoặc hành vi khác của bộ chứa có thể cần được mô hình hóa rõ ràng. Đây không phải quy luật phổ quát cho mọi tệp MP4.
Nhưng với một bộ tạo có cấu trúc do tôi kiểm soát, kiểm tra nghiêm ngặt hữu ích hơn rất nhiều so với nhận xét “gần giống 16 fps”.
Kiểm tra siêu dữ liệu và giải mã toàn bộ trả lời hai câu hỏi khác nhau
Dấu thời gian hoàn hảo không chứng minh toàn bộ luồng H.264 có thể giải mã được. Ngược lại, một tệp có thể giải mã sạch mà vẫn vi phạm quy ước thời gian của tôi.
ffprobe / packet validation
→ structure, timestamps, durations, stream parameters
full decode
→ whether the complete compressed stream can actually be decoded
Sau khi kiểm tra gói, tôi vẫn chạy:
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
Tệp chỉ được xuất bản sau khi cả hai bước đều vượt qua. Mã thoát 0 của bộ mã hóa không còn đồng nghĩa với “xong”.
Hai lỗi thực sự chứng minh điều gì
Với 5580, tôi có thể xác nhận cùng một giá trị có tính xác định lặp lại qua nhiều lần thử; 5580 ở 90 kHz chính xác bằng 62 ms; nguồn thật có độ trễ 62/63 ms; CFR 16 cần 62,5 ms hoặc 5625 ticks; và thử nghiệm đầu vào 1000 Hz giữ đúng các độ trễ mili giây.
Điều đó khiến việc thời lượng nguồn lọt vào một giai đoạn lẽ ra đã là CFR trở thành lời giải thích rất mạnh. Dòng lỗi được lưu trữ không tự nó chứng minh chính xác hàm nào gây ra lỗi.
Với 3751, tôi có thể xác nhận đầu ra thực tế có một gói 3751 tick nơi 24 fps yêu cầu 3750; chuỗi xử lý dùng concat với stream copy; chuẩn hóa gói sau concat bằng setts trở thành một phần của giải pháp; và FFmpeg ghi rõ cả việc concat điều chỉnh dấu thời gian lẫn khả năng ghi lại thời gian ở cấp gói.
Các dữ kiện phù hợp với việc đổi thang số nguyên hoặc làm tròn ở ranh giới mux/concat, nhưng không cho phép nói “concat luôn cộng một tick”.
Quy trình hiện tại của tôi
- Khôi phục dòng thời gian nguồn làm mốc thay vì tin vào một trường FPS được đoán.
- Giữ độ trễ mili giây gốc trên một đồng hồ đầu vào đủ chính xác.
- Chọn CFR mục tiêu riêng.
- Lượng tử hóa rõ ràng dòng thời gian nguồn lên lưới CFR đó.
- Nếu có thể, dùng thang thời gian của rãnh biểu diễn chính xác các tốc độ được phép.
- Mã hóa mà không yêu cầu các bước sau tự chuyển đổi tốc độ khung hình thêm lần nữa.
- Trước concat, kiểm tra cơ sở thời gian, số khung hình, PTS/DTS và thời lượng gói.
- Trước concat bằng stream copy, xác minh cấu hình codec và khả năng tương thích về thời gian.
- Sau concat, kiểm tra lại lưới gói; không coi
-c:v copylà bằng chứng rằng dấu thời gian giữ nguyên. - Nếu cần chuẩn hóa, hãy suy ra nó từ dòng thời gian đã biết và áp dụng ở cấp gói.
- Kiểm tra lại mọi gói.
- Giải mã toàn bộ tệp cuối.
- Chỉ xuất bản nguyên tử sau khi quy ước vượt qua kiểm tra.
Quy tắc tôi giữ lại: CFR là một quy ước thời gian nguyên
Trước đây 16 fps gần như tự giải thích với tôi. Bây giờ thì không.
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 quan trọng vì nó lộ ra mili giây lưới cũ của nguồn. 3751 quan trọng chính vì gần như vô hình: chỉ một tick đã đủ chứng minh hệ thống ngừng giữ bất biến mà nó được thiết kế để giữ.
Quy tắc của tôi giờ rất đơn giản: đừng kiểm tra nhãn “CFR”; hãy kiểm tra thời gian mà từ đó nhãn ấy phải đúng.
Tài liệu chính
- FFmpeg: bộ lọc fps — xây dựng tốc độ khung hình mục tiêu và quy tắc làm tròn PTS.
- FFmpeg: định dạng MOV/MP4 —
video_track_timescalevà các tùy chọn của bộ ghép kênh MP4. - FFmpeg: bộ tách kênh concat — thời lượng tệp và việc điều chỉnh dấu thời gian giữa các đầu vào.
- FFmpeg: bộ lọc bitstream setts — ghi lại PTS, DTS, thời lượng và cơ sở thời gian của gói.
- Tài liệu ffprobe — kiểm tra luồng và gói.
- FFmpeg libavutil: phép toán và đổi thang thời gian — đổi thang dấu thời gian hữu tỉ và các chế độ làm tròn rõ ràng.