filmcraft/crates/codecs/src/avi.rs

586 lines
26 KiB
Rust

//! AVI media source (`filmcraft-avi` demux, GOP-aware video via [`crate::gop`]).
//!
//! Video: Motion JPEG (frames without their own Huffman tables get the standard ones), H.264 and
//! HEVC (Annex B byte streams, our decoders), and uncompressed RGB / YUV ([`RawFormat`]). A frame
//! slot with an empty chunk (a "drop frame") repeats the picture before it. MPEG-4 Part 2
//! (DivX / Xvid) and DV have no decoder yet: their streams are listed and report the codec.
//!
//! Audio: PCM (integer and float) straight from the chunks; MPEG audio (MP3, MP2) and AC-3 frames,
//! which AVI chunks need not follow, are found again across chunk boundaries
//! ([`filmcraft_mpegts::frame_bytes`]) and decoded as packets.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use filmcraft_avi::{AviFile, BitmapInfo, StreamKind, WaveFormat};
use filmcraft_frame::{AudioBuffer, VideoFrame};
use filmcraft_isobmff::{PcmConfig, SampleEntry};
use filmcraft_media::{AudioStreamInfo, FrameRequest, MediaError, MediaInfo, MediaKind, MediaSource, SharedSource, VideoStreamInfo};
use filmcraft_time::{FrameRate, Tick};
use crate::audio::{PacketDecoder, decode_pcm};
use crate::gop::{GopCache, VideoSamples};
use crate::video::{RawFormat, RawVideoDecoder, VideoDecoder};
use crate::{CodecError, make_video_decoder};
pub fn sniff(b: &[u8]) -> bool {
filmcraft_avi::sniff(b)
}
#[derive(Clone, Debug, PartialEq)]
enum VCodec {
Jpeg,
H264,
Hevc,
Raw(RawFormat),
/// No decoder: the codec's name.
Unsupported(String),
}
fn fourcc_text(f: [u8; 4]) -> String {
let t: String = f.iter().map(|&c| if c.is_ascii_graphic() { c as char } else { '?' }).collect();
t.trim_end_matches('?').to_string()
}
/// The decoder for a video stream and its label.
fn video_codec(b: &BitmapInfo) -> (VCodec, String) {
if let Some(raw) = RawFormat::from_avi(b.compression, b.bit_count) {
return (VCodec::Raw(raw), raw.label().into());
}
let mut f = b.compression;
f.make_ascii_uppercase();
match &f {
b"MJPG" | b"AVRN" | b"LJPG" | b"JPGL" | b"DMB1" | b"JPEG" | b"MJPA" => (VCodec::Jpeg, "Motion JPEG".into()),
b"H264" | b"X264" | b"AVC1" | b"DAVC" | b"VSSH" => (VCodec::H264, "H.264".into()),
b"HEVC" | b"H265" | b"HEV1" | b"HVC1" | b"X265" => (VCodec::Hevc, "HEVC".into()),
b"DIVX" | b"DX50" | b"XVID" | b"FMP4" | b"MP4V" | b"M4S2" | b"3IV2" | b"DIV3" | b"MP43" | b"MP42" => {
(VCodec::Unsupported("MPEG-4 Part 2 (DivX / Xvid)".into()), "MPEG-4 Part 2 (DivX / Xvid)".into())
}
b"DVSD" | b"DV25" | b"DV50" | b"DVHD" | b"DVSL" | b"CDVC" | b"DVCP" | b"DVPP" => (VCodec::Unsupported("DV".into()), "DV".into()),
_ => {
let name = fourcc_text(b.compression);
(VCodec::Unsupported(format!("\"{name}\"")), name)
}
}
}
/// One picture of the video stream: its frame slot (in `scale / rate` units) and chunk.
#[derive(Clone, Copy, Debug)]
struct VFrame {
pts: i64,
chunk: usize,
key: bool,
}
#[derive(Clone, Debug)]
enum ACodec {
Pcm(PcmConfig),
/// MPEG audio (layer 1-3) or AC-3 frames, found again across chunks.
Framed {
ts: filmcraft_mpegts::Codec,
layer: u8,
},
Unsupported(String),
}
fn audio_codec(w: &WaveFormat) -> (ACodec, String) {
let pcm = |float: bool| {
let bits = w.bits_per_sample;
let cfg = PcmConfig { bits, float, big_endian: false, signed: bits > 8, channels: u32::from(w.channels.max(1)), sample_rate: f64::from(w.sample_rate) };
(ACodec::Pcm(cfg), format!("PCM {bits}-bit{}", if float { " float" } else { "" }))
};
match w.format_tag {
1 if matches!(w.bits_per_sample, 8 | 16 | 24 | 32) => pcm(false),
3 if matches!(w.bits_per_sample, 32 | 64) => pcm(true),
0x55 => (ACodec::Framed { ts: filmcraft_mpegts::Codec::MpegAudio, layer: 3 }, "MP3".into()),
0x50 => (ACodec::Framed { ts: filmcraft_mpegts::Codec::MpegAudio, layer: 2 }, "MPEG Audio".into()),
0x2000 => (ACodec::Framed { ts: filmcraft_mpegts::Codec::Ac3, layer: 0 }, "AC-3".into()),
tag => {
let name = format!("audio format 0x{tag:04X}");
(ACodec::Unsupported(name.clone()), name)
}
}
}
/// A slice of a chunk: (chunk, offset in it, length).
type Piece = (usize, usize, usize);
struct AudioState {
decoder: Option<PacketDecoder>,
packets: HashMap<usize, Arc<Vec<Vec<f32>>>>,
order: Vec<usize>,
last_decoded: Option<usize>,
}
struct AviAudio {
stream: usize,
codec: ACodec,
/// The pieces of packet `k` are `pieces[first[k]..first[k + 1]]`.
pieces: Vec<Piece>,
first: Vec<usize>,
/// Packet start positions in sample frames.
starts: Vec<i64>,
state: Mutex<AudioState>,
}
pub struct AviSource {
info: MediaInfo,
bytes: crate::Src,
file: AviFile,
vstream: Option<usize>,
vcodec: VCodec,
/// Pictures in stored (decode) order.
vframes: Vec<VFrame>,
/// `vframes` indices in presentation order (H.264 with B-frames is stored in decode order),
/// and their presentation times.
vorder: Vec<usize>,
vorder_pts: Vec<i64>,
/// Frame slots of the video stream (where the last picture ends).
vslots: i64,
rate: FrameRate,
bottom_up: bool,
video: GopCache,
audios: Vec<AviAudio>,
}
/// Whether an Annex B access unit holds an H.264 IDR or HEVC IRAP picture.
fn random_access(data: &[u8], hevc: bool) -> bool {
filmcraft_bitstream::annexb_nals(data).iter().any(|n| match n.first() {
Some(h) if hevc => (16..=23).contains(&((h >> 1) & 0x3F)),
Some(h) => h & 0x1F == 5,
None => false,
})
}
/// The packets of a framed audio stream: every frame header found in the stream of chunks, and
/// the samples each frame decodes to. Bytes that start no frame are skipped (resynchronisation).
fn split_frames(
file: &AviFile,
bytes: &crate::Src,
s: usize,
ts: &filmcraft_mpegts::Codec,
) -> (Vec<Piece>, Vec<usize>, Vec<u32>, Option<filmcraft_mpegts::FrameInfo>) {
let Some(st) = file.streams.get(s) else { return Default::default() };
// chunk start positions in the concatenated stream
let mut at = Vec::with_capacity(st.chunks.len());
let mut total = 0usize;
for c in &st.chunks {
at.push(total);
total = total.saturating_add(c.size as usize);
}
let locate = |pos: usize| -> Option<(usize, usize)> {
let k = at.partition_point(|&a| a <= pos).checked_sub(1)?;
Some((k, pos - at.get(k)?))
};
let (mut pieces, mut first, mut samples) = (Vec::new(), Vec::new(), Vec::new());
let mut info = None;
// a sliding window over the chunks: `win` holds the stream from `base`
let (mut win, mut base, mut next) = (Vec::<u8>::new(), 0usize, 0usize);
let mut pos = 0usize;
while pos < total {
// keep at least a header's worth (and the frame, when we know its length) in the window
while win.len() < (pos - base) + 4096 && next < st.chunks.len() {
win.extend(file.read_chunk(&bytes, s, next).unwrap_or_default());
next += 1;
}
let Some(here) = win.get(pos - base..) else { break };
match filmcraft_mpegts::frame_bytes(ts, here) {
Some(n) if pos + n <= total => {
if info.is_none() {
info = filmcraft_mpegts::frame_info(ts, here);
}
let k = filmcraft_mpegts::frame_info(ts, here).map_or(0, |f| f.samples);
first.push(pieces.len());
let mut p = pos;
while p < pos + n {
let Some((c, off)) = locate(p) else { break };
let len = (st.chunks.get(c).map_or(0, |ch| ch.size as usize) - off).min(pos + n - p);
if len == 0 {
break;
}
pieces.push((c, off, len));
p += len;
}
samples.push(k);
pos += n;
}
_ if here.len() < 8 && next >= st.chunks.len() => break,
_ => pos += 1,
}
// drop what is behind
if pos - base > 1 << 16 {
let cut = pos - base;
win.drain(..cut.min(win.len()));
base += cut;
}
}
(pieces, first, samples, info)
}
impl AviSource {
pub fn open(name: &str, bytes: Arc<[u8]>) -> crate::Result<Self> {
Self::open_reader(name, Arc::new(filmcraft_media::reader::MemReader(bytes)))
}
/// Open from a random-access reader: the index now, frames and audio on demand.
pub fn open_reader(name: &str, reader: filmcraft_media::SharedReader) -> crate::Result<Self> {
let bytes = crate::Src(reader);
let file = filmcraft_avi::open(&bytes).map_err(|e| CodecError::Container(e.to_string()))?;
let vstream = file.streams.iter().position(|s| s.kind == StreamKind::Video && s.video.is_some() && !s.chunks.is_empty());
let astreams: Vec<usize> = file
.streams
.iter()
.enumerate()
.filter(|(_, s)| s.kind == StreamKind::Audio && s.audio.is_some() && !s.chunks.is_empty())
.map(|(i, _)| i)
.take(filmcraft_media::MAX_AUDIO_STREAMS)
.collect();
if vstream.is_none() && astreams.is_empty() {
return Err(CodecError::Unsupported("no playable streams".into()));
}
// video: frame slots, pictures and key frames
let (mut vcodec, mut vframes, mut vslots, mut rate, mut bottom_up, mut video) =
(VCodec::Unsupported(String::new()), Vec::new(), 0i64, FrameRate::new(25, 1), false, None);
if let Some(vs) = vstream.and_then(|i| file.streams.get(i)) {
let b = vs.video.clone().unwrap_or_default();
let (codec, label) = video_codec(&b);
rate = FrameRate::new(i64::from(vs.rate), i64::from(vs.scale));
bottom_up = b.height > 0;
let (w, h) = (b.width.unsigned_abs(), b.height.unsigned_abs());
for (k, c) in vs.chunks.iter().enumerate() {
if c.size == 0 {
continue;
}
let key = match &codec {
VCodec::H264 | VCodec::Hevc if !vs.keyframes_known => {
file.read_chunk(&bytes, vstream.unwrap_or(0), k).is_ok_and(|d| random_access(&d, codec == VCodec::Hevc))
}
VCodec::H264 | VCodec::Hevc => c.key,
_ => true,
};
vframes.push(VFrame { pts: i64::from(vs.start) + k as i64, chunk: k, key });
}
vslots = i64::from(vs.start) + vs.chunks.len() as i64;
// AVI has no timestamps: H.264 with B-frames is stored in decode order, and the
// presentation order comes from the pictures' order counts. A first look at the start
// of the stream decides whether it reorders at all (reading every frame's header is the
// cost of a reordering stream only).
if codec == VCodec::H264
&& let Some(si) = vstream
{
let heads = |n: usize| -> Vec<Vec<u8>> {
vframes
.iter()
.take(n)
.map(|f| {
file.streams
.get(si)
.and_then(|s| s.chunks.get(f.chunk))
.and_then(|c| filmcraft_media::reader::read_range(&*bytes.0, c.offset, (c.size as usize).min(8 << 10)).ok())
.unwrap_or_default()
})
.collect()
};
let reorders = |p: &[i64]| p.windows(2).any(|w| w[1] < w[0]);
if crate::mxf::avc_presentation_order(&heads(300)).is_some_and(|p| reorders(&p))
&& let Some(ranks) = crate::mxf::avc_presentation_order(&heads(vframes.len()))
{
// the pictures keep the slots they occupy (empty drop-frame chunks between them
// stay), shown in their presentation order
let mut slots: Vec<i64> = vframes.iter().map(|f| f.pts).collect();
slots.sort_unstable();
for (f, r) in vframes.iter_mut().zip(ranks) {
if let Some(&p) = usize::try_from(r).ok().and_then(|r| slots.get(r)) {
f.pts = p;
}
}
}
}
let secs = vslots as f64 / rate.as_f64().max(1e-9);
// The rate the pictures play at: a writer that kept a fine time base (ffmpeg copying an
// MP4's 1/600) fills the slots between pictures with empty chunks, every 20th slot
// holding a picture at 30 fps. A regular spacing makes the rate; slots stay the units.
let shown = if vframes.len() > 1 {
let mut gaps: Vec<i64> = vframes.windows(2).map(|w| w[1].chunk as i64 - w[0].chunk as i64).filter(|g| *g > 0).take(1000).collect();
gaps.sort_unstable();
let gap = gaps.get(gaps.len() / 2).copied().unwrap_or(1).max(1);
let regular = gaps.iter().filter(|g| **g == gap).count() * 10 >= gaps.len() * 9;
if gap > 1 && regular { FrameRate::new(i64::from(vs.rate), i64::from(vs.scale).saturating_mul(gap)) } else { rate }
} else {
rate
};
let bitrate = (secs > 0.0).then(|| (vs.chunks.iter().map(|c| u64::from(c.size)).sum::<u64>() as f64 * 8.0 / secs) as u64);
video = Some(VideoStreamInfo {
width: w,
height: h,
frame_rate: shown,
par: (1, 1),
codec: label,
pixel_format: String::new(),
color: crate::stream_color::resolve(w, h, &[]),
has_alpha: false,
bitrate,
hdr: None,
});
vcodec = codec;
}
// audio: packets and their start positions
let mut audios = Vec::new();
let mut audio_streams = Vec::new();
for &s in &astreams {
let Some(st) = file.streams.get(s) else { continue };
let w = st.audio.clone().unwrap_or_default();
let (codec, label) = audio_codec(&w);
let mut sample_rate = w.sample_rate.max(1);
let mut channels = u32::from(w.channels.max(1));
let (pieces, first, starts) = match &codec {
ACodec::Pcm(cfg) => {
let block = usize::from(w.block_align.max(1)).max((usize::from(cfg.bits).div_ceil(8)) * channels as usize);
let mut t = 0i64;
let mut starts = Vec::with_capacity(st.chunks.len());
for c in &st.chunks {
starts.push(t);
t = t.saturating_add((c.size as usize / block) as i64);
}
let pieces: Vec<Piece> = st.chunks.iter().enumerate().map(|(k, c)| (k, 0, c.size as usize)).collect();
let first = (0..pieces.len()).collect();
(pieces, first, starts)
}
ACodec::Framed { ts, .. } => {
let (pieces, first, samples, info) = split_frames(&file, &bytes, s, ts);
if let Some(i) = info {
sample_rate = i.sample_rate.max(1);
channels = i.channels.max(1);
}
let mut t = 0i64;
let starts = samples
.iter()
.map(|&k| {
let s = t;
t = t.saturating_add(i64::from(k));
s
})
.collect();
(pieces, first, starts)
}
ACodec::Unsupported(_) => (Vec::new(), Vec::new(), Vec::new()),
};
// a stream that starts late (dwStart, in scale / rate units)
let delay = (i128::from(st.start) * i128::from(st.scale) * i128::from(sample_rate) / i128::from(st.rate.max(1))) as i64;
let starts: Vec<i64> = starts.into_iter().map(|s: i64| s.saturating_add(delay)).collect();
audio_streams.push(AudioStreamInfo {
sample_rate,
channels,
codec: label,
bits_per_sample: (w.bits_per_sample > 0).then_some(u32::from(w.bits_per_sample)),
});
let state = Mutex::new(AudioState { decoder: None, packets: HashMap::new(), order: Vec::new(), last_decoded: None });
audios.push(AviAudio { stream: s, codec, pieces, first, starts, state });
}
// the longer of the picture and the sound
let vdur = if vslots > 0 { rate.tick_of(vslots) } else { Tick::ZERO };
let adur = audios
.iter()
.zip(&audio_streams)
.filter_map(|(a, i)| {
let last = a.starts.last()?;
let tail = a.starts.len().checked_sub(2).and_then(|k| a.starts.get(k)).map_or(0, |p| last - p);
Some(Tick::from_rational(last + tail, 1, i64::from(i.sample_rate)))
})
.max()
.unwrap_or_default();
let info = MediaInfo {
name: name.to_string(),
kind: if video.is_some() { MediaKind::Movie } else { MediaKind::AudioOnly },
duration: vdur.max(adur),
video,
audio_streams,
container: "AVI".into(),
start_timecode: None,
file_size: Some(bytes.0.len()),
};
let mut vorder: Vec<usize> = (0..vframes.len()).collect();
vorder.sort_by_key(|&i| vframes.get(i).map_or(0, |f| f.pts));
let vorder_pts = vorder.iter().map(|&i| vframes.get(i).map_or(0, |f| f.pts)).collect();
Ok(Self { info, bytes, file, vstream, vcodec, vframes, vorder, vorder_pts, vslots, rate, bottom_up, video: GopCache::new(None), audios })
}
fn read(&self, stream: usize, chunk: usize) -> crate::Result<Vec<u8>> {
self.file.read_chunk(&self.bytes, stream, chunk).map_err(|e| CodecError::Container(e.to_string()))
}
/// The bytes of audio packet `k`.
fn packet(&self, a: &AviAudio, k: usize) -> crate::Result<Vec<u8>> {
let (from, to) = (
a.first.get(k).copied().ok_or_else(|| CodecError::Container("audio packet out of range".into()))?,
a.first.get(k + 1).copied().unwrap_or(a.pieces.len()),
);
let mut out = Vec::new();
for &(c, off, len) in a.pieces.get(from..to).unwrap_or_default() {
let chunk = self.read(a.stream, c)?;
out.extend_from_slice(chunk.get(off..off + len).ok_or_else(|| CodecError::Container("audio chunk shorter than its index".into()))?);
}
Ok(out)
}
fn audio_packet(&self, a: &AviAudio, st: &mut AudioState, ainfo: &AudioStreamInfo, i: usize) -> crate::Result<Arc<Vec<Vec<f32>>>> {
if let Some(p) = st.packets.get(&i) {
return Ok(p.clone());
}
let data = self.packet(a, i)?;
let decoded = match &a.codec {
ACodec::Pcm(cfg) => decode_pcm(&data, cfg),
ACodec::Unsupported(name) => return Err(CodecError::Unsupported(name.clone())),
ACodec::Framed { ts, layer } => {
if st.decoder.is_none() {
st.decoder = Some(match ts {
filmcraft_mpegts::Codec::Ac3 => PacketDecoder::ac3()?,
_ => PacketDecoder::mpeg_audio(*layer, ainfo.sample_rate)?,
});
}
let d = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?;
// non-sequential access: reset and prime with the packet before (the bit reservoir)
if st.last_decoded.is_none_or(|l| l + 1 != i) {
d.reset();
if let Some(prev) = i.checked_sub(1).and_then(|j| self.packet(a, j).ok()) {
let _ = d.decode(&prev, 0);
}
}
let start = a.starts.get(i).copied().unwrap_or(0).max(0) as u64;
let r = d.decode(&data, start);
st.last_decoded = Some(i);
r.unwrap_or_default()
}
};
let p = Arc::new(decoded);
st.packets.insert(i, p.clone());
st.order.push(i);
if st.order.len() > 4096 {
let old = st.order.remove(0);
st.packets.remove(&old);
}
Ok(p)
}
}
/// The video stream as a [`VideoSamples`] table.
struct AviVideo<'a> {
src: &'a AviSource,
stream: usize,
}
impl VideoSamples for AviVideo<'_> {
fn count(&self) -> usize {
self.src.vframes.len()
}
fn pts(&self, i: usize) -> i64 {
self.src.vframes.get(i).map_or(0, |f| f.pts)
}
fn sync_before(&self, i: usize) -> usize {
let i = i.min(self.src.vframes.len().saturating_sub(1));
self.src.vframes.get(..=i).and_then(|f| f.iter().rposition(|f| f.key)).unwrap_or(0)
}
fn sample_at(&self, t: i64) -> Option<usize> {
// the picture shown in slot `t`: the last one at or before it (empty chunks repeat it)
if t >= self.src.vslots {
return None;
}
let k = self.src.vorder_pts.partition_point(|&p| p <= t).checked_sub(1)?;
self.src.vorder.get(k).copied()
}
fn read(&self, i: usize) -> crate::Result<Vec<u8>> {
let f = self.src.vframes.get(i).ok_or_else(|| CodecError::Container("frame out of range".into()))?;
self.src.read(self.stream, f.chunk)
}
fn make_decoder(&self) -> crate::Result<Box<dyn VideoDecoder>> {
let v = self.src.info.video.as_ref().ok_or_else(|| CodecError::Unsupported("no video stream".into()))?;
let (w, h) = (v.width.min(u32::from(u16::MAX)) as u16, v.height.min(u32::from(u16::MAX)) as u16);
match &self.src.vcodec {
VCodec::Jpeg => make_video_decoder(&SampleEntry::jpeg(w, h)),
VCodec::H264 => Ok(Box::new(crate::video::H264Decoder::annexb())),
VCodec::Hevc => Ok(Box::new(crate::video::HevcDecoder::annexb())),
VCodec::Raw(f) => Ok(Box::new(RawVideoDecoder::new(*f, v.width, v.height, self.src.bottom_up)?)),
VCodec::Unsupported(name) => Err(CodecError::Unsupported(format!("no decoder for {name} video"))),
}
}
}
impl MediaSource for AviSource {
fn info(&self) -> &MediaInfo {
&self.info
}
fn video_frame(&self, req: FrameRequest) -> Result<Arc<VideoFrame>, MediaError> {
let s = self.vstream.ok_or(MediaError::NoStream("video"))?;
let t = req.time.max(Tick::ZERO);
let slot = |t: Tick| self.rate.frame_at(t);
let late = filmcraft_media::cancel::catch_up().map(|m| slot(t - m));
Ok(self.video.frame_late(&AviVideo { src: self, stream: s }, slot(t), late)?)
}
fn audio(&self, start: i64, frames: usize, sample_rate: u32) -> Result<AudioBuffer, MediaError> {
self.audio_stream(0, start, frames, sample_rate)
}
fn audio_stream(&self, stream: usize, start: i64, frames: usize, sample_rate: u32) -> Result<AudioBuffer, MediaError> {
let a = self.audios.get(stream).ok_or(MediaError::NoStream("audio"))?;
let ainfo = self.info.audio_streams.get(stream).ok_or(MediaError::NoStream("audio"))?;
if let ACodec::Unsupported(name) = &a.codec {
return Err(MediaError::Unsupported(format!("{name} audio")));
}
let ch = ainfo.channels.max(1) as usize;
let ratio = f64::from(ainfo.sample_rate) / f64::from(sample_rate.max(1));
let s0 = (start as f64 * ratio).floor() as i64;
let need = (frames as f64 * ratio).ceil() as i64 + 2;
let mut src: Vec<Vec<f32>> = vec![vec![0.0; need.max(0) as usize]; ch];
let mut st = a.state.lock().unwrap_or_else(|e| e.into_inner());
let mut i = a.starts.partition_point(|&x| x <= s0).saturating_sub(1);
while let Some(&pk_start) = a.starts.get(i) {
if pk_start >= s0 + need {
break;
}
let pk = self.audio_packet(a, &mut st, ainfo, i)?;
for (c, dst) in src.iter_mut().enumerate() {
let Some(chan) = pk.get(c.min(pk.len().saturating_sub(1))) else { continue };
for (k, v) in chan.iter().enumerate() {
let pos = pk_start + k as i64 - s0;
if let Some(d) = usize::try_from(pos).ok().and_then(|p| dst.get_mut(p)) {
*d = *v;
}
}
}
i += 1;
}
drop(st);
let frac = s0 as f64 - start as f64 * ratio;
let mut out = AudioBuffer::silence(sample_rate, ch, frames);
for (c, o) in out.channels.iter_mut().enumerate() {
let Some(s) = src.get(c) else { continue };
for (k, d) in o.iter_mut().enumerate() {
let pos = k as f64 * ratio - frac;
let i0 = pos.floor().max(0.0) as usize;
let f = (pos - i0 as f64) as f32;
let a = s.get(i0).copied().unwrap_or(0.0);
let b = s.get(i0 + 1).copied().unwrap_or(a);
*d = a + (b - a) * f;
}
}
Ok(out)
}
}
pub fn opener(name: &str, bytes: Arc<[u8]>) -> Option<Result<SharedSource, MediaError>> {
if !sniff(&bytes) {
return None;
}
Some(AviSource::open(name, bytes).map(|s| Arc::new(s) as SharedSource).map_err(Into::into))
}
/// [`filmcraft_media::ReaderOpener`] for AVI.
pub fn reader_opener(name: &str, head: &[u8], reader: &filmcraft_media::SharedReader) -> Option<Result<SharedSource, MediaError>> {
if !sniff(head) {
return None;
}
Some(AviSource::open_reader(name, reader.clone()).map(|s| Arc::new(s) as SharedSource).map_err(Into::into))
}