Bir gün medya işleme hattım, ilk bakışta neredeyse saçma görünen bir hata yüzünden bazı videoları yayımlamayı bıraktı:
Geçersiz CFR paket süresi: 5580 tick, beklenen 5625
FFmpeg çökmemişti ve H.264 dosyası oluşmuştu. Dosyayı kodlamadan sonra benim doğrulayıcım reddediyordu: çıktı sabit kare hızlı olmalıydı fakat bir paket tasarladığım zaman ızgarasına oturmuyordu.
Aynı işi yeniden çalıştırdım. Yine 5580. Bir kez daha: yine 5580. Daha sonra başka bir kaynak da aynı değerde hata verdi. Bu, geçici bir ağ sorunu değil, deterministik bir sözleşme ihlaliydi.
Sonra ikinci bir olay çıktı. 24 kare/sn ve saniyede 90 000 birim kullanan bir video izinde tam 3750 tick bekliyordum. Doğrulayıcı 3751 buldu.
Bu iki hata aynı değildir. 5580 ile 5625 arasındaki fark 45 tick, yani tam 0,5 ms'dir. 3751 ile 3750 arasındaki fark ise tek tick, yaklaşık 11,1 mikrosaniyedir. İkisine de “FFmpeg yuvarladı” demek asıl bilgiyi kaybettirirdi.
Bu olaylar FPS kelimesi altında fazla kolay birleştirdiğim kavramları ayırmamı sağladı: kare hızı, zaman tabanı, PTS/DTS ve paket süresi.
CFR yalnızca “16 fps” yazan bir metadata alanı değildir
Benim üreticimde ffprobe çıktısında 16/1 görmek yeterli değildir. CFR bir zaman sözleşmesidir: sunum anları düzenli bir ızgaraya oturmalı, her normal örneğin süresi bu ızgaranın tek adımı olmalıdır.
1 / 16 = 0.0625 s = 62.5 ms
90000 / 16 = 5625 tick
5625 doğrulayıcının uydurduğu bir sayı değil; 16 kare/sn ve 90 000 tick/sn kararlarının doğrudan sonucudur.
Bu kesinlik, seçtiğim hızların ölçeği tam bölmesi sayesinde mümkündür. Bir hız tek bir tam sayı süreyle gösterilemiyorsa doğrulayıcı doğru tam sayı desenini modellemelidir.
Kare hızı, zaman tabanı ve MP4 zaman ölçeği farklı kavramlardır
- Kare hızı: sunum ritmi; 16 fps için her 62,5 ms'de bir kare.
- Zaman tabanı: tek zaman damgası biriminin süresi; örneğin
1/90000sn. - MP4 iz zaman ölçeği: saniyedeki birim sayısı; 90 000 aynı çözünürlüğü verir.
- PTS: görüntünün ne zaman sunulacağı.
- DTS: kodlanmış paketin ne zaman çözüleceği.
- Paket süresi: örneğin akış zaman tabanındaki uzunluğu.
B-kareleri olduğunda PTS ve DTS'nin farklı olması normaldir. Bu nedenle doğrudan PTS = DTS yapmak genel bir düzeltme değildir; setts belgeleri de buna dikkat çeker.
Neden 90 000 kullandım
| Hız | Kare başına tick |
|---|---|
| 10 | 9000 |
| 12 | 7500 |
| 15 | 6000 |
| 16 | 5625 |
| 18 | 5000 |
| 20 | 4500 |
| 24 | 3750 |
| 25 | 3600 |
| 30 | 3000 |
Ayrıca:
1 ms = 90 tick
Kaynak animasyonların gecikmeleri milisaniye olduğu için bu ilişki çok kullanışlıydı. FFmpeg MP4 muxer içinde video_track_timescale değerini açıkça ayarlayabiliyor. Ölçek zamanlamayı kendi başına düzeltmez; sözleşmeyi ölçülebilir yapar.
5580 sayısı nereye bakacağımı söylüyordu
5580 / 90000 = 0.062 s = 62 ms
Gerçek bir kaynağım 3,063 saniyede 49 görüntülenen kare içeriyor, gecikmeler 62 ve 63 ms arasında değişiyordu:
49 / 3.063 ≈ 15.997 fps
62 + 63 = 125 ms
2 × 62.5 ms = 125 ms
Kaynağın milisaniye ızgarasında 62/63 ms, 16 fps için mantıklı bir yaklaşımdır. Fakat CFR 16 seçildikten sonra her normal karenin 62,5 ms yani 5625 tick olması gerekir.
Bu yüzden 5580, 62 ms'lik kaynak süresinin CFR'a kuantize edilmiş olması gereken bir aşamaya kadar taşındığına dair güçlü bir ipucuydu.
Yine de kanıt sınırını koruyorum: log sayıyı ve matematiksel bağı kanıtlar; tam olarak hangi fonksiyonun bunu geçirdiğini tek başına kanıtlamaz.
1000 Hz giriş saati hata değildi
FFmpeg 7.1.5 ve ffconcat ile şu girişi sınadım:
duration 0.010
option framerate 1000
Paket zaman damgaları amaçlanan milisaniye konumlarını koruyordu:
0 ms
10 ms
20 ms
30 ms
Zaman damgaları 0, 10, 20 ve 30 ms olarak korunuyordu. Aynı erken aşamada 30 kullanınca zaman yaklaşık 33,3-ms adımlara kuantize oluyordu.
Yani 1000 Hz kaynak gecikmesini 1 ms hassasiyetle korumak için doğruydu. Son çıktının 1000 fps olacağı anlamına gelmiyordu.
hassas kaynak gecikmeleri
→ güvenilir kaynak zaman çizgisi
→ hedef CFR seçimi
→ CFR ızgarasına açık kuantizasyon
→ bu ızgarayı son MP4'e kadar koruma
Girişteki yüksek hassasiyet sorun değildi. Sorun, kaynak zamanlamasını koruma aşaması ile onu CFR ızgarasına niceleme aşaması arasındaki geçişi açıkça tanımlamamaktı.
CFR kontrollü zaman kuantizasyonudur
62, 63, 62, 63 ms
↓
62.5, 62.5, 62.5, 62.5 ms
FFmpeg'in fps filtresi bu dönüşüm için uygun bir noktadır: giriş PTS değerlerine ve yuvarlama kuralına göre kareleri tekrarlar veya düşürür.
Bu sınırdan sonra başka bir katmanın bağımsız biçimde ikinci kez kare hızı dönüşümü yapmasını istemem.
Neden üç yeniden deneme hiçbir şeyi değiştirmedi
FAIL 5580, beklenen 5625
retry 1/3
FAIL 5580, beklenen 5625
retry 2/3
FAIL 5580, beklenen 5625
Yeniden deneme, iki deneme arasında ortadan kalkabilecek sorunlarda işe yarar. Aynı algoritmanın aynı girdiden ürettiği deterministik sözleşme ihlalini düzeltmez.
- Geçici hata: yeniden deneme mantıklı olabilir.
- Bozuk giriş: farklı işlem yolu veya red.
- Deterministik invariant hatası: durdur ve teşhis et.
Sonra 3750 yerine 3751 çıktı
90000 / 24 = 3750 tick
Gerçek bir birleştirilmiş çıktı buna rağmen 3751 içeriyordu. Bir tick:
1 / 90000 s ≈ 11.111 µs
Gözle önemsiz, ama sözleşme açısından önemli; çünkü 24 fps bu ölçekte tam olarak 3750 ile gösterilebilir.
Akış kopyalama zaman damgalarının dokunulmaz olduğu anlamına gelmez
ffmpeg -f concat -safe 0 -i segments.ffconcat \
-c:v copy final.mp4
-c:v copy H.264 görüntülerini tekrar kodlamaz. Fakat konteyner paketleri yeni bir zaman çizgisine yerleştirmek zorundadır.
Concat demuxer belgeleri her dosyanın süresinin sonraki dosyanın zaman damgası'lerini ayarlamak için kullanıldığını söyler. FFmpeg ayrıca tam sayı zaman damgalarını rasyonel zaman tabanları arasında belirli yuvarlama kurallarıyla yeniden ölçekler.
Bu, concat’ın benim 3751 değerimin tek olası sebebi olduğunu kanıtlamaz. Ancak “bit akışı aynıysa zaman damgaları da aynıdır” düşüncesinin yanlış olduğunu gösterir.
PTS ve DTS tek güzel formülle düzeltilemez
PTS = N * süre
DTS = N * süre
B-karelerinde bu yanlış olabilir; çözme ve sunma sıraları farklıdır.
Normalizasyon, bilinen zaman çizgisinden türetilmeli ve geçerli PTS/DTS ilişkisini bozmadan sunum ızgarasını geri kurmalıdır. Bu yüzden bağlamsız bir setts sihirli formülü yayımlamıyorum.
Neden setts doğru katmandı
setts, videoyu yeniden kodlamadan paketlerin PTS, DTS, süre ve çıkış zaman tabanı alanlarını değiştirebilir.
bilinen segment zaman çizgisi + bilinen CFR + 90000 ızgarası
→ beklenen paket konumları ve süreleri
→ paket zamanını normalize et
→ tekrar doğrula
Düzeltme modelden gelir; “3751 görürsen bir çıkar” kuralından değil.
Neden ±1 tick toleransı eklemedim
Bir hız tam temsil edilemiyorsa kontrollü yuvarlama deseni gereklidir. Benim seçtiğim hızlar bilerek tamdı:
16 fps → 5625
24 fps → 3750
30 fps → 3000
Tam değer varken ±1 kabul etmek, açıklanamayan ihlali izin verilen duruma dönüştürürdü.
- \n
- seçilen ızgara zorunlu olarak farklı tam sayı sürelerini dönüşümlü gerektiriyorsa doğru örüntüyü doğrula; \n
- sürenin tek ve kesin bir tam sayı olması gerekiyorsa tam olarak o değeri iste; \n
- başarısız bir doğrulayıcıyı yeşile çevirmek için ±1 değerini evrensel bir kestirme olarak kullanma. \n
CFR'ı paket seviyesinde nasıl doğruluyorum
\navg_frame_rate ve benzeri akış meta verileri yararlı özetlerdir, ancak bu sözleşmeyi doğrulamak için tek başlarına yeterli değildir.
ffprobe -v error \
-select_streams v:0 \
-show_streams \
-show_packets \
-of json final.mp4
Gerçek zaman tabanı ile her paketin pts, dts ve duration alanlarını kontrol ediyorum.
expected = 90000 / 16 // 5625
for each normal video packet:
assert packet.duration == 5625
assert legal decode order
assert legal PTS/DTS relationship
assert frame count and duration match
Edit list, trimming veya bilinçli başka istisnalar açıkça modellenmelidir. Bu, kontrol ettiğim üretici için sözleşmedir; tüm MP4 dosyaları için evrensel yasa değildir.
Meta veri doğrulaması ile tam kod çözme farklı sorulara yanıt verir
ffprobe/paketler → yapı ve zaman
tam decode → sıkıştırılmış akışın bütünlüğü
ffmpeg -v error -xerror -err_detect explode \
-i final.mp4 -f null -
İki kontrol de geçmeden yayımlamıyorum.
Kodlayıcının 0 çıkış kodu tek başına artık dosyanın hazır olduğu anlamına gelmiyor.
İki hata gerçekten neyi kanıtladı
5580: tekrar denemelerde aynı deterministik değer; 90 kHz'te tam 62 ms; gerçek kaynakta 62/63 ms; CFR 16 için gerekli 62,5 ms; ve 1000-Hz deneyinde milisaniyelerin doğru korunması. Bu, kaynak timing'inin fazla ileri taşındığı açıklamasını güçlü biçimde destekler; fakat tek başına kesin kod satırını göstermez.
3751: gerçek çıktıda tek tick fazlalığı vardı; süreç stream-copy concat kullanıyordu ve ardından setts ile normalizasyon yapılıyordu. Belgeler ilgili mekanizmaları doğrular, ancak “concat her zaman bir tick ekler” sonucunu desteklemez.
Bugün kullandığım akış
- Gerçek kaynak zaman çizgisini kur.
- Kaynak gecikmelerini yeterli hassasiyetle koru.
- Hedef CFR'ı ayrı seç.
- Bu ızgaraya açıkça kuantize et.
- Mümkün olduğunda izin verilen hızları tam gösteren zaman ölçeği kullan.
- Sonraki katmanlarda ikinci bağımsız fps dönüşümü yapma.
- Concat öncesi paket zamanını doğrula.
- Akış kopyalama öncesi tam uyumluluğu kontrol et.
- Concat sonrası ızgarayı yeniden incele.
- Gerekirse bilinen zaman çizgisinden normalize et.
- Tekrar doğrula.
- Nihai dosyanın tamamını baştan sona çöz.
- Yalnızca tüm sözleşme geçerse atomik yayımla.
Kalan kural: CFR tam sayılı bir zaman sözleşmesidir
time base = 1/90000
normal süre = 5625 tick
sunum ritmi = 62.5 ms
paket zaman çizgisi bu ızgaraya uyar
PTS/DTS H.264 reorder için geçerli kalır
5580 eski milisaniye ızgarasını ele verdi. 3751 ise tam tersine neredeyse görünmez olduğu için değerliydi: tek tick, tasarım gereği kesin olması gereken bir invariant'ın kaybolduğunu gösterdi.
Artık “CFR” etiketini doğrulamıyorum. O etiketin türemesi gereken zamanı doğruluyorum.