M1/M2: wire filmcraft-h264 into the codec hub (MP4/MOV H.264 with B-frames, GOP-aware seeking)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-09-30 07:01:36 -04:00
parent 11709186ea
commit 0c1b413595
5 changed files with 102 additions and 1 deletions

1
Cargo.lock generated
View File

@ -1264,6 +1264,7 @@ dependencies = [
"filmcraft-bitstream",
"filmcraft-color",
"filmcraft-frame",
"filmcraft-h264",
"filmcraft-isobmff",
"filmcraft-media",
"filmcraft-time",

View File

@ -16,6 +16,7 @@ filmcraft-frame = { workspace = true }
filmcraft-media = { workspace = true }
filmcraft-isobmff = { workspace = true }
filmcraft-bitstream = { workspace = true }
filmcraft-h264 = { workspace = true }
thiserror = { workspace = true }
log = { workspace = true }
image = { version = "0.25", default-features = false, features = ["jpeg"] }

View File

@ -43,7 +43,7 @@ impl From<CodecError> for filmcraft_media::MediaError {
fn factories() -> &'static RwLock<Vec<VideoDecoderFactory>> {
static F: std::sync::OnceLock<RwLock<Vec<VideoDecoderFactory>>> = std::sync::OnceLock::new();
F.get_or_init(|| RwLock::new(vec![video::mjpeg_factory]))
F.get_or_init(|| RwLock::new(vec![video::h264_factory, video::mjpeg_factory]))
}
/// Register a video decoder factory (tried before previously registered ones).

View File

@ -99,3 +99,26 @@ fn seek_backwards_and_forwards_mjpeg() {
assert_eq!(a.to_rgba8(), c.to_rgba8());
assert_ne!(a.to_rgba8(), b.to_rgba8());
}
#[test]
fn h264_mp4_decodes_and_seeks() {
let Some(b) = fixture(
"mandel_h264.mp4",
&["-f", "lavfi", "-i", "mandelbrot=s=640x360:r=25", "-t", "4", "-c:v", "libx264", "-preset", "fast", "-bf", "3", "-g", "25", "-pix_fmt", "yuv420p"],
) else {
return;
};
let src = crate::open_bytes("mandel_h264.mp4", b).unwrap();
assert!(src.info().video.as_ref().unwrap().codec.contains("H.264"));
let rate = src.info().frame_rate();
let late = src.video_frame(FrameRequest::full(rate.tick_of(70))).unwrap();
assert_eq!((late.width, late.height), (640, 360));
let early = src.video_frame(FrameRequest::full(rate.tick_of(3))).unwrap();
let again = src.video_frame(FrameRequest::full(rate.tick_of(70))).unwrap();
assert_eq!(late.to_rgba8(), again.to_rgba8(), "random access is deterministic");
assert_ne!(late.to_rgba8(), early.to_rgba8());
// sequential access after a seek
for f in 71..90 {
src.video_frame(FrameRequest::full(rate.tick_of(f))).unwrap();
}
}

View File

@ -48,3 +48,79 @@ impl VideoDecoder for MjpegDecoder {
pub fn mjpeg_factory(e: &SampleEntry) -> Option<Result<Box<dyn VideoDecoder>>> {
matches!(e.codec, CodecConfig::Jpeg { .. }).then(|| Ok(Box::new(MjpegDecoder) as Box<dyn VideoDecoder>))
}
/// Our pure-Rust H.264 decoder (frame-threaded).
pub struct H264Decoder {
avcc: Vec<u8>,
dec: filmcraft_h264::Decoder,
}
impl H264Decoder {
pub fn new(avcc: Vec<u8>) -> Result<Self> {
let dec = filmcraft_h264::Decoder::from_avcc(&avcc).map_err(|e| CodecError::Decode(e.to_string()))?;
Ok(Self { avcc, dec })
}
fn convert(p: filmcraft_h264::Picture) -> DecodedFrame {
use std::sync::Arc;
let (w, h) = (p.width as usize, p.height as usize);
let (cw, ch) = (p.chroma_width as usize, p.chroma_height as usize);
let tight = |src: &[u8], stride: usize, w: usize, h: usize| -> Vec<u8> {
if stride == w && src.len() >= w * h {
return src[..w * h].to_vec();
}
let mut out = Vec::with_capacity(w * h);
for y in 0..h {
out.extend_from_slice(&src[y * stride..y * stride + w]);
}
out
};
let y = tight(&p.y, p.y_stride, w, h);
let u = tight(&p.u, p.uv_stride, cw, ch);
let v = tight(&p.v, p.uv_stride, cw, ch);
let mut color = filmcraft_color::ColorInfo { matrix: filmcraft_frame::default_matrix(p.width, p.height), ..filmcraft_color::ColorInfo::REC709 };
if let Some(m) = filmcraft_color::Matrix::from_code(p.color.matrix) {
color.matrix = m;
}
if let Some(t) = filmcraft_color::Transfer::from_code(p.color.transfer) {
color.transfer = t;
}
if p.color.full_range {
color.range = filmcraft_color::Range::Full;
}
let par = if p.sar.0 > 0 && p.sar.1 > 0 { (p.sar.0 as u32, p.sar.1 as u32) } else { (1, 1) };
let frame = VideoFrame {
width: p.width,
height: p.height,
data: filmcraft_frame::PixelData::Yuv8 { planes: [Arc::new(y), Arc::new(u), Arc::new(v)], chroma: filmcraft_frame::Chroma::C420, alpha: None },
color,
par,
pts: filmcraft_time::Tick::ZERO,
};
DecodedFrame { pts: p.pts, frame }
}
}
impl VideoDecoder for H264Decoder {
fn decode(&mut self, sample: &[u8], pts: i64) -> Result<Vec<DecodedFrame>> {
let pics = self.dec.decode(sample, pts).map_err(|e| CodecError::Decode(e.to_string()))?;
Ok(pics.into_iter().map(Self::convert).collect())
}
fn flush(&mut self) -> Vec<DecodedFrame> {
self.dec.flush().into_iter().map(Self::convert).collect()
}
fn reset(&mut self) {
if let Ok(d) = filmcraft_h264::Decoder::from_avcc(&self.avcc) {
self.dec = d;
}
}
fn name(&self) -> &str {
"FilmCraft H.264"
}
}
pub fn h264_factory(e: &SampleEntry) -> Option<Result<Box<dyn VideoDecoder>>> {
match &e.codec {
CodecConfig::Avc(a) => Some(H264Decoder::new(a.to_bytes()).map(|d| Box::new(d) as Box<dyn VideoDecoder>)),
_ => None,
}
}