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:
parent
11709186ea
commit
0c1b413595
|
|
@ -1264,6 +1264,7 @@ dependencies = [
|
|||
"filmcraft-bitstream",
|
||||
"filmcraft-color",
|
||||
"filmcraft-frame",
|
||||
"filmcraft-h264",
|
||||
"filmcraft-isobmff",
|
||||
"filmcraft-media",
|
||||
"filmcraft-time",
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue