Browse / github.com/storytold/filmcraft / patch
codecs: read 2 KiB instead of 16 KiB of each picture for AVC reordering at open
codecs: read 2 KiB instead of 16 KiB of each picture for AVC reordering at open Part of #3261 (the mxf crate parts are #3276 and #3279) ## What `MxfSource::open_reader` derives the presentation order of AVC long GOP without temporal offsets from the slice headers (POC). It read `min(size, 16 KiB)` of every picture for that, up to 240 MB for 15k frames, although only the slice header is needed. Now it reads 2 KiB of every picture first. If the order cannot be derived from those heads (a slice header or parameter set cut by the short read), it falls back to the previous 16 KiB heads, so the outcome is the same as before. Pictures that are all 2 KiB or shorter are not read
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- vangel76 (imported from pull request #3283, open)
- Base
a504f3c5adc4cd60d9c1120916cddb50c9b24b86- Size
- +60 −7 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
3b1861e68d4c2debc97c9db6
How it went
No other operator has reported using it yet.
Diff
raw · crates/codecs/src/mxf.rs crates/codecs/tests/it/mxf_oracle.rs
@@ -191,6 +191,12 @@ fn start_frames(tc: &Timecode, rate: FrameRate) -> i64 { ((n + d / 2) / d) as i64 } +/// Bytes read from the front of every picture to find its slice header when the index has no+/// temporal offsets (`avc_presentation_order`), and the longer read for pictures where that is+/// not enough.+const AVC_ORDER_HEAD: usize = 2 << 10;+const AVC_ORDER_HEAD_MAX: usize = 16 << 10;+ impl MxfSource { pub fn open(name: &str, bytes: Arc<[u8]>) -> crate::Result<Self> { Self::open_reader(name, Arc::new(filmcraft_media::reader::MemReader(bytes)))@@ -229,13 +235,17 @@ impl MxfSource { vpts = t.samples.iter().map(|s| s.pts).collect(); vorder = t.display_order.clone(); if t.needs_reorder && matches!(t.codec, Codec::Avc { .. }) {- let heads: Vec<Vec<u8>> = (0..t.samples.len())- .map(|i| {- let s = t.samples[i];- filmcraft_media::reader::read_range(&*bytes.0, s.offset, (s.size as usize).min(16 << 10)).unwrap_or_default()- })- .collect();- match avc_presentation_order(&heads) {+ // Only the slice headers are needed. The first picture of a stream has its+ // parameter sets and SEI in front of the slice, so read a short head of every+ // picture first and the long one only if some slice header does not fit.+ let heads = |cap: usize| -> Vec<Vec<u8>> {+ t.samples.iter().map(|s| filmcraft_media::reader::read_range(&*bytes.0, s.offset, (s.size as usize).min(cap)).unwrap_or_default()).collect()+ };+ let order = avc_presentation_order(&heads(AVC_ORDER_HEAD)).or_else(|| {+ let all_short = t.samples.iter().all(|s| s.size as usize <= AVC_ORDER_HEAD);+ if all_short { None } else { avc_presentation_order(&heads(AVC_ORDER_HEAD_MAX)) }+ });+ match order { Some(pts) => { vorder = vec![0; pts.len()]; for (s, &d) in pts.iter().enumerate() {@@ -5,6 +5,7 @@ use crate::common; use std::path::{Path, PathBuf};+use std::sync::Arc; use common::*; use filmcraft_codecs::MxfSource;@@ -24,6 +25,12 @@ fn spec(name: &str) -> Vec<String> { NOISE, &["-t", "2", "-c:v", "libx264", "-bf", "2", "-g", "12", "-pix_fmt", "yuv420p", "-c:a", "pcm_s16le", "-ac", "2", "-timecode", "01:00:00:00"], ]),+ // the same at 720p and a high rate: pictures far longer than the head read for the order+ "mxf_h264_bf2_720p.mxf" => cat(&[+ &["-f", "lavfi", "-i", "testsrc2=size=1280x720:rate=25"],+ NOISE,+ &["-t", "2", "-c:v", "libx264", "-bf", "2", "-g", "12", "-b:v", "12M", "-pix_fmt", "yuv420p", "-c:a", "pcm_s16le", "-ac", "2"],+ ]), "mxf_h264_2997df.mxf" => cat(&[ &["-f", "lavfi", "-i", "testsrc2=size=320x240:rate=30000/1001"], NOISE,@@ -111,6 +118,7 @@ fn spec(name: &str) -> Vec<String> { pub const ALL: &[&str] = &[ "mxf_h264_bf2.mxf",+ "mxf_h264_bf2_720p.mxf", "mxf_h264_2997df.mxf", "mxf_dnxhr_lb.mxf", "mxf_prores.mxf",@@ -225,6 +233,41 @@ fn h264_long_gop_with_b_frames_bit_exact() { check_audio_exact(&ff, &f, &[]); } +/// Reordering B pictures reads only a short head of every picture, not the pictures.+#[test]+fn h264_long_gop_order_needs_only_the_slice_headers() {+ use std::sync::Mutex;++ /// Remembers the length of every read.+ struct Counting(filmcraft_media::SharedReader, Arc<Mutex<Vec<usize>>>);+ impl filmcraft_media::reader::ByteReader for Counting {+ fn len(&self) -> u64 {+ self.0.len()+ }+ fn read_at(&self, offset: u64, buf: &mut [u8]) -> std::io::Result<()> {+ self.1.lock().unwrap().push(buf.len());+ self.0.read_at(offset, buf)+ }+ }++ let ff = filmcraft_testkit::require_ffmpeg!();+ let f = make(&ff, "mxf_h264_bf2_720p.mxf");+ let data = bytes(&f);+ let reads = Arc::new(Mutex::new(Vec::new()));+ let reader = Arc::new(Counting(Arc::new(filmcraft_media::reader::MemReader(data.clone())), reads.clone()));+ let src = MxfSource::open_reader("bf2_720p.mxf", reader).unwrap();+ let t = &src.file().tracks[src.file().track_of_kind(filmcraft_mxf::TrackKind::Picture).unwrap()];+ assert!(t.needs_reorder && !t.temporal_offsets);+ assert!(t.samples.iter().all(|s| s.size > 16 << 10), "the pictures must be longer than the long head read");+ // one 2 KiB head per picture, and no 16 KiB read (the fallback for a slice header that does+ // not fit in the short head)+ let reads = reads.lock().unwrap();+ assert_eq!(reads.iter().filter(|&&n| n == 2 << 10).count(), t.samples.len());+ assert!(!reads.contains(&(16 << 10)));+ // the order found from the short heads is the right one+ check_video(&ff, &f, "yuv420p", (2, 2), 1, 0, 10);+}+ #[test] fn h264_2997_drop_frame_timecode_and_24_bit_audio() { let ff = filmcraft_testkit::require_ffmpeg!();