use std::collections::VecDeque;
use std::ffi::{CStr, CString};
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use crate::audio_timeline::SourceTimeline;
use crate::recovery::{DecodeDiagnostics, DecodePolicy, Recovery, WorkBudget, submit_packet};
use crate::convert::{
adapter_error, codec_thread_budget, color_metadata, convert_video_frame, declared_sar,
frame_color_metadata, guess_sar, orientation_from_display, reject_unused_decode_options,
stream_rotation, tick_base, timestamp, video_decoder_options,
};
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
use crate::video_frame::{HardwareDecoderSpec, HardwareOpenError};
use alpha2_ffmpeg_bridge::{DecodedFrame, DemuxInput, OpenInputError};
use alpha2_media::{
AudioBlock, CancelToken, ColorMetadata, DecodeError, DecodeEvent, DecodeRequest, DecodeResult,
DecodedVideoFrame, Decoder, MAX_AUDIO_CHANNELS, Orientation, SampleAspectRatio, Speaker,
SpeakerLayout, StreamInfo, StreamKind,
};
use alpha2_time::{FrameRate, MediaTime, TICKS_PER_SECOND};
use rsmpeg::avcodec::{AVCodec, AVCodecContext, AVCodecRef, AVPacket};
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
use rsmpeg::avutil::AVHWDeviceContext;
use rsmpeg::avutil::{AVChannelLayout, AVFrame, AVRational, av_rescale_q, av_rescale_q_rnd, ra};
use rsmpeg::error::RsmpegError;
use rsmpeg::ffi;
use rsmpeg::swresample::SwrContext;
#[derive(Clone, Debug, PartialEq)]
pub struct ProbeResult {
pub format: String,
pub duration: Option<MediaTime>,
pub start: Option<MediaTime>,
pub streams: Vec<StreamInfo>,
pub selected_video: Option<usize>,
pub selected_audio: Option<usize>,
}
impl ProbeResult {
#[must_use]
pub fn stream(&self, index: usize) -> Option<&StreamInfo> {
self.streams.iter().find(|stream| stream.index == index)
}
#[must_use]
pub fn selected_video_stream(&self) -> Option<&StreamInfo> {
self.selected_video.and_then(|index| self.stream(index))
}
#[must_use]
pub fn selected_audio_stream(&self) -> Option<&StreamInfo> {
self.selected_audio.and_then(|index| self.stream(index))
}
}
pub fn probe(path: impl AsRef<Path>) -> Result<ProbeResult, DecodeError> {
let input = open_input(path.as_ref())?;
probe_context(&input, path.as_ref())
}
pub fn visit_packet_times(
path: impl AsRef<Path>,
stream: usize,
mut visit: impl FnMut(f64) -> bool,
) -> Result<(), DecodeError> {
let mut input = open_input(path.as_ref())?;
let time_base = input
.streams()
.get(stream)
.map(|found| found.time_base)
.ok_or_else(|| DecodeError::Unsupported(format!("the file has no stream {stream}")))?;
input.discard_all_but(stream);
while let Some(packet) = input.read_packet().map_err(adapter_error)? {
if usize::try_from(packet.stream_index).ok() != Some(stream) {
continue;
}
let time = [packet.pts, packet.dts]
.into_iter()
.find(|time| *time != ffi::AV_NOPTS_VALUE);
let Some(time) = time else { continue };
#[allow(clippy::cast_precision_loss)] let seconds = time as f64 * f64::from(time_base.num) / f64::from(time_base.den);
if !visit(seconds) {
break;
}
}
Ok(())
}
pub struct NativeDecoder {
pub(crate) input: DemuxInput,
pub(crate) streams: Vec<StreamInfo>,
pub(crate) container_duration: Option<MediaTime>,
pub(crate) video_index: usize,
pub(crate) time_base: AVRational,
pub(crate) decoder: AVCodecContext,
pub(crate) orientation: Orientation,
pub(crate) sample_aspect_ratio: SampleAspectRatio,
pub(crate) color: Option<ColorMetadata>,
pub(crate) declared_color: Option<ColorMetadata>,
pub(crate) video_scaler: crate::convert::FrameScaler,
pub(crate) pending: VecDeque<DecodedFrame>,
pub(crate) demux_eof: bool,
pub(crate) decoder_flushed: bool,
pub(crate) stages: DecodeStageTimings,
first_video_pts: Option<MediaTime>,
recovery: Recovery,
slack: MediaTime,
prefetched: Prefetched,
}
type Prefetched = VecDeque<Result<Option<AVPacket>, RsmpegError>>;
fn read_first_packets(input: &mut DemuxInput, index: usize) -> (Prefetched, Vec<i64>) {
let mut read = VecDeque::new();
let mut timestamps = Vec::with_capacity(alpha2_time::ROUNDING_SCAN_FRAMES);
let (mut packets, mut bytes) = (0, 0_u64);
while packets < alpha2_time::ROUNDING_SCAN_FRAMES && bytes < alpha2_time::ROUNDING_SCAN_BYTES {
let packet = input.read_packet();
let more = match &packet {
Ok(Some(packet)) => {
if usize::try_from(packet.stream_index).ok() == Some(index) {
packets += 1;
bytes = bytes.saturating_add(u64::try_from(packet.size).unwrap_or(0));
if packet.pts != ffi::AV_NOPTS_VALUE {
timestamps.push(packet.pts);
}
}
true
}
Ok(None) | Err(_) => false,
};
read.push_back(packet);
if !more {
break;
}
}
(read, timestamps)
}
fn read_presentation_slack(
input: &mut DemuxInput,
video_index: usize,
time_base: AVRational,
) -> (Prefetched, MediaTime) {
let (read, timestamps) = read_first_packets(input, video_index);
let slack = alpha2_time::presentation_slack(
i64::from(time_base.num),
i64::from(time_base.den),
alpha2_time::rounding_quantum(×tamps),
);
(read, slack)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DecodeStageTimings {
pub demux: Duration,
pub send_packet: Duration,
pub receive_frame: Duration,
pub convert: Duration,
pub seek: Duration,
}
impl DecodeStageTimings {
#[must_use]
pub fn merged(self, other: Self) -> Self {
Self {
demux: self.demux.saturating_add(other.demux),
send_packet: self.send_packet.saturating_add(other.send_packet),
receive_frame: self.receive_frame.saturating_add(other.receive_frame),
convert: self.convert.saturating_add(other.convert),
seek: self.seek.saturating_add(other.seek),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum InputPosition {
Opened,
Moved,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum AudioContinuity {
Continuous,
Damaged,
}
pub struct NativeAudioDecoder {
pub(crate) path: PathBuf,
pub(crate) input: DemuxInput,
input_position: InputPosition,
pub(crate) streams: Vec<StreamInfo>,
pub(crate) container_duration: Option<MediaTime>,
pub(crate) audio_index: usize,
pub(crate) time_base: AVRational,
pub(crate) decoder: AVCodecContext,
converter: Option<SwrContext>,
pub(crate) sample_rate: u32,
pub(crate) channel_count: usize,
speakers: SpeakerLayout,
speakers_assumed: bool,
output_layout: AVChannelLayout,
timeline: SourceTimeline,
pub(crate) pending: Vec<VecDeque<f32>>,
pub(crate) next_sample: Option<i64>,
pub(crate) block_frames: usize,
pub(crate) demux_eof: bool,
pub(crate) decoder_flushed: bool,
recovery: Recovery,
repair_audio: AudioContinuity,
made_up: Option<MadeUpTimestamps>,
grid: Option<(u32, alpha2_time::AudioFrameGrid)>,
prefetched: Prefetched,
frames_placed: u64,
seek_preroll: i64,
}
struct MadeUpTimestamps {
rate: u32,
drift: i64,
rate_changed: bool,
}
const AUDIO_SEEK_PREROLL_TICKS: i64 = TICKS_PER_SECOND / 10;
const OPUS_SEEK_PREROLL_TICKS: i64 = TICKS_PER_SECOND * 2 / 5;
impl NativeAudioDecoder {
pub fn set_decode_policy(&mut self, policy: DecodePolicy) {
self.recovery.policy = policy;
}
pub fn set_decode_reporter(
&mut self,
reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
) {
self.recovery.reporter = Some(Box::new(reporter));
}
#[must_use]
pub fn decode_diagnostics(&self) -> &DecodeDiagnostics {
&self.recovery.diagnostics
}
pub fn open(path: impl AsRef<Path>, project_sample_rate: u32) -> Result<Self, DecodeError> {
if project_sample_rate == 0 {
return Err(DecodeError::OutOfRange);
}
let path = path.as_ref().to_owned();
let mut input = open_input(&path)?;
let probe = probe_context(&input, &path)?;
let selected = probe.selected_audio.ok_or_else(|| {
DecodeError::Unsupported("source has no audio stream this runtime decodes".into())
})?;
let stream = probe
.stream(selected)
.ok_or_else(|| DecodeError::Adapter("selected audio stream disappeared".into()))?;
check_source_sample_rate(i64::from(stream.sample_rate.unwrap_or(0)))?;
let source_channels =
usize::try_from(stream.channels.unwrap_or(0)).map_err(|_| DecodeError::OutOfRange)?;
if source_channels == 0 {
return Err(DecodeError::Unsupported(
"audio stream declares no channels".into(),
));
}
check_audio_channels(source_channels)?;
let decoder = open_audio_decoder(&mut input, selected)?;
let (prefetched, timestamps) = read_first_packets(&mut input, selected);
let stream = input
.streams()
.get(selected)
.ok_or(DecodeError::OutOfRange)?;
let time_base = stream.time_base;
let seek_preroll = if stream.codecpar().codec_id == ffi::AV_CODEC_ID_OPUS {
OPUS_SEEK_PREROLL_TICKS
} else {
AUDIO_SEEK_PREROLL_TICKS
};
let made_up = (input.iformat().name() == c"aac")
.then(|| u32::try_from(stream.codecpar().sample_rate).ok())
.flatten()
.filter(|rate| *rate > 0)
.map(|rate| MadeUpTimestamps {
rate,
drift: 0,
rate_changed: false,
});
let grid = u32::try_from(stream.codecpar().sample_rate)
.ok()
.filter(|_| made_up.is_none())
.and_then(|rate| {
alpha2_time::audio_frame_grid(
×tamps,
i64::from(time_base.num),
i64::from(time_base.den),
rate,
)
.map(|grid| (rate, grid))
});
let declared = [decoder.ch_layout(), stream.codecpar().ch_layout()]
.into_iter()
.find(|layout| {
layout.order != ffi::AV_CHANNEL_ORDER_UNSPEC
&& usize::try_from(layout.nb_channels).ok() == Some(source_channels)
})
.map_or_else(
|| vec![Speaker::Unnamed; source_channels],
|layout| layout_speakers(&layout),
);
let (speakers, speakers_assumed) = SpeakerLayout::declared_or_default(&declared)
.ok_or_else(|| unsupported_channels(source_channels))?;
let channel_count = speakers.channels();
let output_layout = ffmpeg_layout(&speakers)?;
Ok(Self {
path,
input,
input_position: InputPosition::Opened,
container_duration: probe.duration,
streams: probe.streams,
audio_index: selected,
time_base,
decoder,
converter: None,
sample_rate: project_sample_rate,
channel_count,
speakers,
speakers_assumed,
output_layout,
timeline: SourceTimeline::new(project_sample_rate, channel_count, None),
pending: (0..channel_count).map(|_| VecDeque::new()).collect(),
next_sample: None,
block_frames: 1_024,
demux_eof: false,
decoder_flushed: false,
recovery: Recovery::new(time_base),
repair_audio: AudioContinuity::Continuous,
made_up,
grid,
prefetched,
frames_placed: 0,
seek_preroll,
})
}
pub fn set_block_frames(&mut self, frames: usize) -> Result<(), DecodeError> {
if frames == 0 {
return Err(DecodeError::OutOfRange);
}
self.block_frames = frames;
Ok(())
}
#[must_use]
pub const fn channels(&self) -> usize {
self.channel_count
}
#[must_use]
pub const fn speakers(&self) -> SpeakerLayout {
self.speakers
}
#[must_use]
pub const fn speakers_assumed(&self) -> bool {
self.speakers_assumed
}
#[must_use]
pub fn audio_duration(&self) -> Option<MediaTime> {
effective_media_duration(
self.streams
.iter()
.find(|stream| stream.index == self.audio_index)
.and_then(|stream| stream.duration),
self.container_duration,
)
}
fn stream_start(&self) -> i64 {
self.input
.streams()
.get(self.audio_index)
.map(|stream| stream.start_time)
.filter(|start| *start != ffi::AV_NOPTS_VALUE)
.unwrap_or(0)
}
#[cfg(test)]
pub(crate) const fn peak_held_samples(&self) -> usize {
self.timeline.peak_held()
}
pub(crate) fn reset_to_start(&mut self) -> Result<(), DecodeError> {
if self.input_position == InputPosition::Opened {
self.decoder.flush_buffers();
} else {
let mut input = open_input(&self.path)?;
self.decoder = open_audio_decoder(&mut input, self.audio_index)?;
self.input = input;
self.input_position = InputPosition::Opened;
self.prefetched.clear();
}
self.restart(None);
Ok(())
}
fn restart(&mut self, first: Option<i64>) {
self.converter = None;
self.timeline = SourceTimeline::new(self.sample_rate, self.channel_count, first);
self.pending.iter_mut().for_each(VecDeque::clear);
self.next_sample = None;
self.demux_eof = false;
self.decoder_flushed = false;
self.repair_audio = AudioContinuity::Continuous;
self.recovery.reset_progress();
if let Some(made_up) = &mut self.made_up {
made_up.drift = 0;
}
}
fn made_up_rate_changed(&self) -> bool {
self.made_up
.as_ref()
.is_some_and(|made_up| made_up.rate_changed)
}
fn true_timestamp(&mut self, pts: Option<i64>, rate: u32, samples: i32) -> Option<i64> {
let time_base = self.time_base;
let Some(made_up) = &mut self.made_up else {
return pts;
};
let declared = made_up.rate;
made_up.rate_changed |= rate != declared;
let corrected = pts.map(|pts| pts.saturating_sub(made_up.drift));
let (num, den) = (
i128::from(time_base.num.max(1)),
i128::from(time_base.den.max(1)),
);
let samples = i128::from(samples.max(0));
let counted = samples * den / (i128::from(declared) * num);
let lasted = (2 * samples * den + i128::from(rate) * num) / (2 * i128::from(rate) * num);
made_up.drift = made_up
.drift
.saturating_add(i64::try_from(counted - lasted).unwrap_or(0));
corrected
}
pub(crate) fn receive_audio_frame(&mut self) -> Result<bool, DecodeError> {
let mut frame = match self.decoder.receive_frame() {
Ok(frame) => frame,
Err(RsmpegError::DecoderDrainError | RsmpegError::DecoderFlushedError) => {
return Ok(false);
}
Err(error) => {
self.recovery.error(error, "audio receive", None)?;
self.repair_audio = AudioContinuity::Damaged;
return Ok(true);
}
};
if frame.flags & i32::try_from(ffi::AV_FRAME_FLAG_CORRUPT).expect("flag fits i32") != 0
|| frame.decode_error_flags != 0
{
self.recovery.corrupt("audio frame", Some(frame.pts))?;
self.repair_audio = AudioContinuity::Damaged;
return Ok(true);
}
let pts = Some(if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
frame.pts
} else {
frame.best_effort_timestamp
})
.filter(|pts| *pts != ffi::AV_NOPTS_VALUE);
if pts.is_some_and(|pts| !audio_timestamp_in_range(pts, self.time_base)) {
self.recovery.corrupt("audio timestamp", pts)?;
self.repair_audio = AudioContinuity::Damaged;
return Ok(true);
}
self.recovery.progress(pts.unwrap_or(ffi::AV_NOPTS_VALUE))?;
normalize_audio_frame_layout(&mut frame, &self.output_layout)?;
check_source_sample_rate(i64::from(frame.sample_rate))?;
let rate = u32::try_from(frame.sample_rate).map_err(|_| DecodeError::OutOfRange)?;
let pts = self.true_timestamp(pts, rate, frame.nb_samples);
let planes = self.convert_audio_frame(&frame)?;
self.place_audio(rate, pts, planes)?;
Ok(true)
}
fn convert_audio_frame(&mut self, frame: &AVFrame) -> Result<Vec<Vec<f32>>, DecodeError> {
let mut packed = audio_output_frame(frame.sample_rate, &self.output_layout);
let mut converter = match self.converter.take() {
Some(converter) => converter,
None => audio_converter(frame, &self.output_layout)?,
};
if let Err(error) = converter.convert_frame(Some(frame), &mut packed) {
if error.raw_error() != Some(ffi::AVERROR_INPUT_CHANGED) {
return Err(adapter_error(error));
}
converter = audio_converter(frame, &self.output_layout)?;
packed = audio_output_frame(frame.sample_rate, &self.output_layout);
converter
.convert_frame(Some(frame), &mut packed)
.map_err(adapter_error)?;
}
self.converter = Some(converter);
if packed.nb_samples != frame.nb_samples {
return Err(DecodeError::Adapter(format!(
"converting {} decoded audio samples gave {}",
frame.nb_samples, packed.nb_samples
)));
}
let mut planes = (0..self.channel_count)
.map(|_| VecDeque::new())
.collect::<Vec<_>>();
append_interleaved_audio(&packed, &mut planes)?;
Ok(planes.into_iter().map(Vec::from).collect())
}
fn place_audio(
&mut self,
rate: u32,
pts: Option<i64>,
mut planes: Vec<Vec<f32>>,
) -> Result<(), DecodeError> {
let rate_i32 = i32::try_from(rate).map_err(|_| DecodeError::OutOfRange)?;
let at = pts.map(|pts| {
av_rescale_q_rnd(
pts,
self.time_base,
ra(1, rate_i32),
ffi::AV_ROUND_NEAR_INF | ffi::AV_ROUND_PASS_MINMAX,
)
});
let damaged = self.repair_audio == AudioContinuity::Damaged;
self.repair_audio = AudioContinuity::Continuous;
self.frames_placed = self.frames_placed.wrapping_add(1);
let frames = i64::try_from(planes.first().map_or(0, Vec::len)).unwrap_or(i64::MAX);
let Some(expected) = self.timeline.expected(rate) else {
let at = match (at, self.grid) {
(Some(at), Some((grid_rate, grid))) if !damaged && grid_rate == rate => {
grid.place(at, frames)
}
(at, _) => at.unwrap_or(0),
};
return self.timeline.place(rate, at, planes);
};
let Some(at) = at else {
if damaged {
return Err(DecodeError::Adapter(
"cannot locate damaged audio gap without PTS".into(),
));
}
return self.timeline.place(rate, expected, planes);
};
let ahead = at
.checked_sub(expected)
.ok_or_else(|| DecodeError::Adapter("audio timestamps are out of range".into()))?;
if damaged {
if ahead < 0 || ahead > i64::from(rate) * 10 {
return Err(DecodeError::Adapter(
"damaged audio gap is ambiguous or exceeds 10 seconds".into(),
));
}
if ahead > 0 {
self.recovery.audio_gap(
self.timeline.output_at(rate, expected),
self.timeline.output_at(rate, at),
self.sample_rate,
"corrupt input",
)?;
}
return self.timeline.place(rate, at, planes);
}
let den = i64::from(self.time_base.den.max(1));
let unit = (i64::from(rate) * i64::from(self.time_base.num) + den - 1) / den;
let tolerance = frames
.max((i64::from(rate) + 499) / 500)
.max(unit.saturating_mul(2));
if ahead.abs() < tolerance {
return self.timeline.place(rate, expected, planes);
}
if ahead > 0 {
self.recovery.timestamp_gap(
self.timeline.output_at(rate, expected),
self.timeline.output_at(rate, at),
self.sample_rate,
);
return self.timeline.place(rate, at, planes);
}
let repeated = usize::try_from(-ahead).unwrap_or(usize::MAX);
self.recovery.timestamp_overlap(
self.timeline.output_at(rate, at),
self.timeline
.output_at(rate, at.saturating_add(frames.min(-ahead))),
self.sample_rate,
);
for plane in &mut planes {
plane.drain(..repeated.min(plane.len()));
}
self.timeline.place(rate, expected, planes)
}
pub(crate) fn fill_pending(
&mut self,
frames: usize,
cancel: &CancelToken,
) -> Result<(), DecodeError> {
interrupt_on_cancel(&self.input, cancel);
let mut budget = WorkBudget::new();
let mut foreign = false;
let mut placed = self.frames_placed;
loop {
let held = self.pending.first().map_or(0, VecDeque::len);
if held >= frames {
return Ok(());
}
if self.frames_placed != placed {
placed = self.frames_placed;
budget.progressed();
}
if std::mem::take(&mut foreign) {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
} else {
budget.step(cancel)?;
}
if let Some((start, _)) = self.timeline.produce(&mut self.pending, frames - held) {
if held == 0 {
self.next_sample = Some(start);
}
continue;
}
if self.receive_audio_frame()? {
continue;
}
if self.demux_eof {
if !self.decoder_flushed {
match self.decoder.send_packet(None) {
Ok(()) | Err(RsmpegError::DecoderFlushedError) => {}
Err(RsmpegError::DecoderFullError) => continue,
Err(error) => {
self.recovery.error(error, "flush", None)?;
continue;
}
}
self.decoder_flushed = true;
continue;
}
if self.repair_audio == AudioContinuity::Damaged {
self.recovery.damaged_tail("audio")?;
self.repair_audio = AudioContinuity::Continuous;
}
if !self.timeline.finished() {
self.timeline.finish();
continue;
}
return Ok(());
}
self.input_position = InputPosition::Moved;
let packet = match self
.prefetched
.pop_front()
.unwrap_or_else(|| self.input.read_packet())
{
Ok(Some(packet)) => packet,
Ok(None) => {
self.demux_eof = true;
continue;
}
Err(error) => {
let error = demux_failure(error, cancel)?;
self.recovery.error(error, "audio demux", None)?;
self.repair_audio = AudioContinuity::Damaged;
continue;
}
};
if usize::try_from(packet.stream_index).ok() != Some(self.audio_index) {
foreign = true;
continue;
}
if packet.flags & i32::try_from(ffi::AV_PKT_FLAG_CORRUPT).expect("flag fits i32") != 0 {
self.recovery.corrupt("audio packet", Some(packet.pts))?;
self.repair_audio = AudioContinuity::Damaged;
continue;
}
submit_packet(
|drain| {
if drain {
return self.receive_audio_frame();
}
match self.decoder.send_packet(Some(&packet)) {
Ok(()) => Ok(true),
Err(RsmpegError::DecoderFullError) => Ok(false),
Err(error) => {
self.recovery.error(error, "audio send", Some(packet.pts))?;
self.repair_audio = AudioContinuity::Damaged;
Ok(true)
}
}
},
&mut budget,
cancel,
)?;
}
}
}
impl Decoder for NativeAudioDecoder {
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn flush(&mut self) -> Result<(), DecodeError> {
self.reset_to_start()
}
fn seek(
&mut self,
request: DecodeRequest,
cancel: &CancelToken,
) -> Result<DecodeResult, DecodeError> {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
let target = request
.time
.ticks()
.saturating_mul(i64::from(self.sample_rate))
.div_euclid(TICKS_PER_SECOND);
let timestamp = av_rescale_q(
request.time.ticks().saturating_sub(self.seek_preroll),
tick_base(),
self.time_base,
);
let rate_changed = self.made_up_rate_changed();
let from_start = timestamp <= self.stream_start() || rate_changed;
if from_start {
self.reset_to_start()?;
} else {
interrupt_on_cancel(&self.input, cancel);
self.input_position = InputPosition::Moved;
self.prefetched.clear();
self.input
.seek(
i32::try_from(self.audio_index).map_err(|_| DecodeError::OutOfRange)?,
timestamp,
i32::try_from(ffi::AVSEEK_FLAG_BACKWARD).expect("seek flag fits i32"),
)
.map_err(|error| seek_failure(error, cancel))?;
}
self.decoder.flush_buffers();
self.restart(Some(target));
let mut event = self.decode_next(cancel)?;
if !from_start && !rate_changed && self.made_up_rate_changed() {
self.reset_to_start()?;
self.decoder.flush_buffers();
self.restart(Some(target));
event = self.decode_next(cancel)?;
}
if matches!(event, DecodeEvent::EndOfStream) {
return Err(DecodeError::OutOfRange);
}
Ok(DecodeResult { request, event })
}
fn decode_next(&mut self, cancel: &CancelToken) -> Result<DecodeEvent, DecodeError> {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
self.fill_pending(self.block_frames, cancel)?;
let frames = self
.pending
.first()
.map_or(0, VecDeque::len)
.min(self.block_frames);
if frames == 0 {
return Ok(DecodeEvent::EndOfStream);
}
let start_sample = self.next_sample.unwrap_or(0);
let block = AudioBlock {
start_sample,
sample_rate: self.sample_rate,
speakers: self.speakers,
channels: self
.pending
.iter_mut()
.map(|channel| channel.drain(..frames).collect())
.collect(),
};
self.next_sample =
Some(start_sample.saturating_add(i64::try_from(frames).unwrap_or(i64::MAX)));
Ok(DecodeEvent::Audio(block))
}
}
fn open_audio_decoder(
input: &mut DemuxInput,
stream_index: usize,
) -> Result<AVCodecContext, DecodeError> {
input.discard_all_but(stream_index);
let stream = input
.streams()
.get(stream_index)
.ok_or(DecodeError::OutOfRange)?;
let parameters = stream.codecpar();
let codec = AVCodec::find_decoder(parameters.codec_id)
.ok_or_else(|| DecodeError::Unsupported("audio decoder is unavailable".into()))?;
let mut decoder = AVCodecContext::new(&codec);
decoder.apply_codecpar(¶meters).map_err(adapter_error)?;
decoder.set_pkt_timebase(stream.time_base);
decoder.open(None).map_err(adapter_error)?;
Ok(decoder)
}
const AUDIO_TIMESTAMP_LIMIT_SECONDS: i128 = 1 << 31;
fn audio_timestamp_in_range(pts: i64, time_base: AVRational) -> bool {
let (num, den) = (i128::from(time_base.num), i128::from(time_base.den));
num > 0 && den > 0 && (i128::from(pts) * num).abs() <= AUDIO_TIMESTAMP_LIMIT_SECONDS * den
}
pub const SOURCE_SAMPLE_RATES: std::ops::RangeInclusive<u32> = 1_000..=768_000;
fn check_source_sample_rate(rate: i64) -> Result<(), DecodeError> {
if u32::try_from(rate).is_ok_and(|rate| SOURCE_SAMPLE_RATES.contains(&rate)) {
return Ok(());
}
Err(DecodeError::Unsupported(format!(
"the source's audio is {rate} Hz, and alpha2 reads audio from {} Hz to {} Hz",
SOURCE_SAMPLE_RATES.start(),
SOURCE_SAMPLE_RATES.end()
)))
}
pub(crate) fn audio_converter(
frame: &AVFrame,
output: &AVChannelLayout,
) -> Result<SwrContext, DecodeError> {
if frame.sample_rate <= 0 || frame.format == ffi::AV_SAMPLE_FMT_NONE {
return Err(DecodeError::Adapter(
"decoded audio frame has an incomplete format".into(),
));
}
let input_layout = frame.ch_layout();
let mut converter = SwrContext::new(
output,
ffi::AV_SAMPLE_FMT_FLT,
frame.sample_rate,
&input_layout,
frame.format,
frame.sample_rate,
)
.map_err(adapter_error)?;
converter.init().map_err(adapter_error)?;
Ok(converter)
}
pub(crate) fn layout_speakers(layout: &AVChannelLayout) -> Vec<Speaker> {
let channels = alpha2_ffmpeg_bridge::channel_layout_channels(layout);
if channels.is_empty() {
return vec![Speaker::Unnamed; usize::try_from(layout.nb_channels).unwrap_or(0)];
}
channels
.into_iter()
.map(|channel| Speaker::from_ffmpeg_channel(i64::from(channel)))
.collect()
}
pub(crate) fn ffmpeg_layout(speakers: &SpeakerLayout) -> Result<AVChannelLayout, DecodeError> {
let channels = speakers
.speakers()
.iter()
.map(|speaker| {
ffi::AVChannel::try_from(speaker.ffmpeg_channel()).unwrap_or(ffi::AV_CHAN_UNKNOWN)
})
.collect::<Vec<_>>();
alpha2_ffmpeg_bridge::channel_layout_of(&channels).map_err(|error| {
DecodeError::Adapter(format!(
"could not lay out audio channels {}: {error:?}",
speakers.describe()
))
})
}
fn unsupported_channels(channels: usize) -> DecodeError {
DecodeError::Unsupported(format!(
"audio with {channels} channels is unsupported: the engine takes 1 to {MAX_AUDIO_CHANNELS}"
))
}
fn check_audio_channels(channels: usize) -> Result<(), DecodeError> {
if (1..=MAX_AUDIO_CHANNELS).contains(&channels) {
Ok(())
} else {
Err(unsupported_channels(channels))
}
}
pub(crate) fn normalize_audio_frame_layout(
frame: &mut AVFrame,
stream: &AVChannelLayout,
) -> Result<(), DecodeError> {
let (channels, named) = {
let layout = frame.ch_layout();
let named = matches!(
layout.order,
ffi::AV_CHANNEL_ORDER_NATIVE | ffi::AV_CHANNEL_ORDER_CUSTOM
) && !layout_speakers(&layout).contains(&Speaker::Unnamed);
(layout.nb_channels, named)
};
if channels <= 0 {
return Err(DecodeError::Adapter(
"decoded audio frame has no channels".into(),
));
}
if named {
return Ok(());
}
if channels == stream.nb_channels {
let mut owned = AVChannelLayout::from_nb_channels(1);
owned.copy(stream);
frame.set_ch_layout(owned.into_inner());
return Ok(());
}
let count = usize::try_from(channels).map_err(|_| DecodeError::OutOfRange)?;
let speakers = SpeakerLayout::default_for(count).ok_or_else(|| unsupported_channels(count))?;
frame.set_ch_layout(ffmpeg_layout(&speakers)?.into_inner());
Ok(())
}
pub(crate) fn audio_output_frame(sample_rate: i32, layout: &AVChannelLayout) -> AVFrame {
let mut frame = AVFrame::new();
frame.set_format(ffi::AV_SAMPLE_FMT_FLT);
frame.set_sample_rate(sample_rate);
let mut owned = AVChannelLayout::from_nb_channels(1);
owned.copy(layout);
frame.set_ch_layout(owned.into_inner());
frame
}
pub(crate) fn append_interleaved_audio(
frame: &AVFrame,
output: &mut [VecDeque<f32>],
) -> Result<(), DecodeError> {
let packed = alpha2_ffmpeg_bridge::audio_f32_interleaved(frame).map_err(adapter_error)?;
if output.is_empty() || packed.len() % output.len() != 0 {
return Err(DecodeError::Adapter(
"FFmpeg returned a partial audio frame".into(),
));
}
for frame in packed.chunks_exact(output.len()) {
for (channel, sample) in output.iter_mut().zip(frame) {
channel.push_back(*sample);
}
}
Ok(())
}
pub(crate) fn has_hw_config_method(methods: i32, method: impl Into<i64>) -> bool {
i64::from(methods) & method.into() != 0
}
const STREAM_START_SEEK: i64 = i64::MIN / 2;
enum Landing {
Presented(DecodedFrame),
Late(DecodedFrame),
Empty,
}
impl NativeDecoder {
pub fn set_decode_policy(&mut self, policy: DecodePolicy) {
self.recovery.policy = policy;
}
pub fn declare_color(&mut self, color: Option<ColorMetadata>) {
self.declared_color = color;
}
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
pub(crate) fn picture_chroma_location(&self, frame: &AVFrame) -> alpha2_media::ChromaLocation {
crate::convert::decoded_chroma_location(self.decoder.codec_id, frame.chroma_location)
}
pub(crate) fn picture_color(&self, frame: &AVFrame) -> Option<ColorMetadata> {
self.declared_color
.or_else(|| frame_color_metadata(frame))
.or(self.color)
}
pub fn set_decode_reporter(
&mut self,
reporter: impl Fn(crate::DecodeDiagnostic) + Send + Sync + 'static,
) {
self.recovery.reporter = Some(Box::new(reporter));
}
#[must_use]
pub fn decode_diagnostics(&self) -> &DecodeDiagnostics {
&self.recovery.diagnostics
}
pub fn open(path: impl AsRef<Path>) -> Result<Self, DecodeError> {
Self::open_with_threads(path, codec_thread_budget())
}
pub fn open_with_threads(path: impl AsRef<Path>, threads: i64) -> Result<Self, DecodeError> {
if !(1..=64).contains(&threads) {
return Err(DecodeError::OutOfRange);
}
let mut input = open_input(path.as_ref())?;
let probed = probe_context(&input, path.as_ref())?;
let video_index = probed.selected_video.ok_or_else(|| {
DecodeError::Unsupported("resource has no decodable video stream".into())
})?;
input.discard_all_but(video_index);
let selected = probed
.streams
.iter()
.find(|stream| stream.index == video_index)
.ok_or(DecodeError::OutOfRange)?;
let orientation = selected.orientation;
let sample_aspect_ratio = selected.sample_aspect_ratio;
let color = selected.color;
let (time_base, decoder) = {
let stream = &input.streams()[video_index];
let parameters = stream.codecpar();
let codec = software_video_decoder(parameters.codec_id)?;
let mut decoder = AVCodecContext::new(&codec);
decoder.apply_codecpar(¶meters).map_err(adapter_error)?;
decoder.set_pkt_timebase(stream.time_base);
let unused = alpha2_ffmpeg_bridge::open_codec_context(
&mut decoder,
video_decoder_options(threads),
)
.map_err(adapter_error)?;
reject_unused_decode_options(unused)?;
(stream.time_base, decoder)
};
let (prefetched, slack) = read_presentation_slack(&mut input, video_index, time_base);
Ok(Self {
time_base,
input,
container_duration: probed.duration,
streams: probed.streams,
video_index,
decoder,
orientation,
sample_aspect_ratio,
color,
declared_color: None,
video_scaler: crate::convert::FrameScaler::default(),
pending: VecDeque::new(),
demux_eof: false,
decoder_flushed: false,
stages: DecodeStageTimings::default(),
first_video_pts: None,
recovery: Recovery::new(time_base),
slack,
prefetched,
})
}
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
#[allow(clippy::too_many_lines)]
pub(crate) fn open_hardware(
path: impl AsRef<Path>,
threads: i64,
device: AVHWDeviceContext,
spec: &HardwareDecoderSpec,
) -> Result<Self, HardwareOpenError> {
use crate::video_frame::HardwareDecodeFallback::{
DecoderOpenFailed, NoHardwareConfig, SourceUnavailable, UnsupportedColor,
};
let unavailable = |error| HardwareOpenError::new(SourceUnavailable, error);
let mut input = open_input(path.as_ref()).map_err(unavailable)?;
let probed = probe_context(&input, path.as_ref()).map_err(unavailable)?;
let video_index = probed.selected_video.ok_or_else(|| {
HardwareOpenError::new(
SourceUnavailable,
DecodeError::Unsupported("resource has no decodable video stream".into()),
)
})?;
input.discard_all_but(video_index);
let selected = probed
.streams
.iter()
.find(|stream| stream.index == video_index)
.ok_or_else(|| HardwareOpenError::new(SourceUnavailable, DecodeError::OutOfRange))?;
let orientation = selected.orientation;
let sample_aspect_ratio = selected.sample_aspect_ratio;
let color = selected.color;
if selected.color.is_some_and(ColorMetadata::is_hdr) {
return Err(HardwareOpenError::new(
UnsupportedColor,
DecodeError::Unsupported("hardware export currently supports SDR only".into()),
));
}
let (time_base, decoder) = {
let stream = &input.streams()[video_index];
let parameters = stream.codecpar();
let preferred = AVCodec::find_decoder(parameters.codec_id).ok_or_else(|| {
HardwareOpenError::new(
NoHardwareConfig,
DecodeError::Unsupported("selected video codec has no decoder".into()),
)
})?;
let supported = |codec: &AVCodecRef<'_>| {
(0..)
.map_while(|index| codec.hw_config(index))
.any(|config| {
config.device_type == spec.device_type
&& config.pix_fmt == spec.pixel_format
&& (!spec.require_device_context
|| has_hw_config_method(
config.methods,
ffi::AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX,
))
})
};
let Some(codec) =
registered_video_decoders(parameters.codec_id).find(|codec| supported(codec))
else {
return Err(HardwareOpenError::new(
NoHardwareConfig,
DecodeError::Unsupported(format!(
"decoder {} has no {} hardware configuration",
preferred.name().to_string_lossy(),
spec.backend
)),
));
};
check_hardware_sample_profile(parameters.format, spec)?;
check_hardware_crop(¶meters, spec)?;
let open_failed = |error| HardwareOpenError::new(DecoderOpenFailed, error);
let mut decoder = AVCodecContext::new(&codec);
decoder
.apply_codecpar(¶meters)
.map_err(adapter_error)
.map_err(open_failed)?;
decoder.set_pkt_timebase(stream.time_base);
decoder.set_get_format(Some(spec.get_format));
decoder.set_hw_device_ctx(device);
let unused = alpha2_ffmpeg_bridge::open_codec_context(
&mut decoder,
video_decoder_options(threads),
)
.map_err(adapter_error)
.map_err(open_failed)?;
reject_unused_decode_options(unused).map_err(open_failed)?;
(stream.time_base, decoder)
};
let (prefetched, slack) = read_presentation_slack(&mut input, video_index, time_base);
Ok(Self {
time_base,
input,
container_duration: probed.duration,
streams: probed.streams,
video_index,
decoder,
orientation,
sample_aspect_ratio,
color,
declared_color: None,
video_scaler: crate::convert::FrameScaler::default(),
pending: VecDeque::new(),
demux_eof: false,
decoder_flushed: false,
stages: DecodeStageTimings::default(),
first_video_pts: None,
recovery: Recovery::hardware(time_base),
slack,
prefetched,
})
}
#[must_use]
pub fn video_duration(&self) -> Option<MediaTime> {
effective_media_duration(
self.streams
.iter()
.find(|stream| stream.index == self.video_index)
.and_then(|stream| stream.duration),
self.container_duration,
)
}
pub(crate) fn take_stage_timings(&mut self) -> DecodeStageTimings {
std::mem::take(&mut self.stages)
}
pub(crate) fn receive_available_raw(
&mut self,
cancel: &CancelToken,
) -> Result<Option<DecodedFrame>, DecodeError> {
let mut budget = WorkBudget::new();
loop {
budget.step(cancel)?;
let started = Instant::now();
let received = DecodedFrame::receive(&mut self.decoder);
self.stages.receive_frame = self.stages.receive_frame.saturating_add(started.elapsed());
match received {
Ok(frame) => {
if frame.flags
& i32::try_from(ffi::AV_FRAME_FLAG_CORRUPT).expect("flag fits i32")
!= 0
|| frame.decode_error_flags != 0
{
self.recovery.corrupt("video frame", Some(frame.pts))?;
continue;
}
#[cfg(test)]
VIDEO_FRAMES_DECODED.with(|decoded| decoded.set(decoded.get() + 1));
self.recovery.progress(
if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
frame.pts
} else {
frame.best_effort_timestamp
},
)?;
return Ok(Some(frame));
}
Err(RsmpegError::DecoderDrainError | RsmpegError::DecoderFlushedError) => {
return Ok(None);
}
Err(error) => {
self.recovery.error(error, "video receive", None)?;
}
}
}
}
pub(crate) fn convert_received(
&mut self,
frame: &DecodedFrame,
) -> Result<DecodedVideoFrame, DecodeError> {
let declared = self.picture_color(frame);
let started = Instant::now();
let converted = convert_video_frame(
frame,
self.decoder.codec_id,
self.time_base,
declared.unwrap_or(ColorMetadata::UNDECLARED),
&mut self.video_scaler,
);
self.stages.convert = self.stages.convert.saturating_add(started.elapsed());
let mut converted = converted?;
if declared.is_none() && converted.color == Some(ColorMetadata::UNDECLARED) {
converted.color = None;
}
converted.orientation = self.orientation;
converted.sample_aspect_ratio = self.picture_sample_aspect_ratio(frame);
Ok(converted)
}
pub(crate) fn picture_sample_aspect_ratio(&self, frame: &AVFrame) -> SampleAspectRatio {
if declared_sar(self.input.streams()[self.video_index].sample_aspect_ratio).is_some() {
return self.sample_aspect_ratio;
}
declared_sar(frame.sample_aspect_ratio).unwrap_or(self.sample_aspect_ratio)
}
pub(crate) fn raw_frame_pts(&self, frame: &AVFrame) -> Result<MediaTime, DecodeError> {
let pts = if frame.best_effort_timestamp == ffi::AV_NOPTS_VALUE {
frame.pts
} else {
frame.best_effort_timestamp
};
timestamp(pts, self.time_base).ok_or(DecodeError::OutOfRange)
}
pub(crate) fn flush_video(&mut self) {
let started = Instant::now();
self.recovery.reset_progress();
self.decoder.flush_buffers();
self.stages.seek = self.stages.seek.saturating_add(started.elapsed());
self.pending.clear();
self.prefetched.clear();
self.demux_eof = false;
self.decoder_flushed = false;
}
pub(crate) fn seek_raw(
&mut self,
target: MediaTime,
cancel: &CancelToken,
) -> Result<DecodedFrame, DecodeError> {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
let start_time = self.input.streams()[self.video_index].start_time;
let stream_start = timestamp(start_time, self.time_base).unwrap_or(MediaTime::ZERO);
let deadline = self.presentation_deadline(target);
let mut back_off = 0_i64;
loop {
let from = MediaTime::from_ticks(deadline.ticks().saturating_sub(back_off));
let from_start =
from <= stream_start || self.first_video_pts.is_some_and(|first| deadline < first);
if !from_start {
let position = av_rescale_q_rnd(
from.ticks(),
tick_base(),
self.time_base,
ffi::AV_ROUND_DOWN,
);
self.seek_video_to(position, cancel)?;
} else if let Err(error) = self.seek_video_to(STREAM_START_SEEK, cancel) {
if error == DecodeError::Cancelled {
return Err(error);
}
let start = if start_time == ffi::AV_NOPTS_VALUE {
0
} else {
start_time
};
self.seek_video_to(start, cancel)?;
}
let mut first = None;
let landing = self.present_after_seek(target, &mut first, cancel)?;
if from_start {
self.first_video_pts = first.or(self.first_video_pts);
}
match landing {
Landing::Presented(frame) => return Ok(frame),
Landing::Late(frame) if from_start || first == self.first_video_pts => {
return Ok(frame);
}
Landing::Empty if from_start => return Err(DecodeError::OutOfRange),
Landing::Late(_) | Landing::Empty => {}
}
back_off = if back_off == 0 {
TICKS_PER_SECOND
} else {
back_off.saturating_mul(2)
};
}
}
fn seek_video_to(&mut self, position: i64, cancel: &CancelToken) -> Result<(), DecodeError> {
interrupt_on_cancel(&self.input, cancel);
let seek_started = Instant::now();
let sought = self.input.seek(
i32::try_from(self.video_index).map_err(|_| DecodeError::OutOfRange)?,
position,
i32::try_from(ffi::AVSEEK_FLAG_BACKWARD).expect("seek flag fits i32"),
);
self.stages.seek = self.stages.seek.saturating_add(seek_started.elapsed());
sought.map_err(|error| seek_failure(error, cancel))?;
self.flush_video();
Ok(())
}
fn present_after_seek(
&mut self,
target: MediaTime,
first: &mut Option<MediaTime>,
cancel: &CancelToken,
) -> Result<Landing, DecodeError> {
let deadline = self.presentation_deadline(target);
let mut presented: Option<DecodedFrame> = None;
let mut budget = WorkBudget::new();
loop {
budget.step(cancel)?;
let Some(raw) = self.decode_raw_next(cancel)? else {
return Ok(presented.map_or(Landing::Empty, Landing::Presented));
};
budget.progressed();
#[cfg(test)]
crate::recovery::take_frame_delay();
let pts = self.raw_frame_pts(&raw)?;
first.get_or_insert(pts);
if pts > deadline {
return Ok(match presented {
Some(frame) => {
self.pending.push_front(raw);
Landing::Presented(frame)
}
None => Landing::Late(raw),
});
}
if pts >= target {
return Ok(Landing::Presented(raw));
}
presented = Some(raw);
}
}
#[must_use]
pub fn presentation_deadline(&self, time: MediaTime) -> MediaTime {
time.presentation_deadline(self.slack)
}
pub fn peek_video_pts(
&mut self,
cancel: &CancelToken,
) -> Result<Option<MediaTime>, DecodeError> {
self.peek_raw_pts(cancel)
}
pub(crate) fn peek_raw_pts(
&mut self,
cancel: &CancelToken,
) -> Result<Option<MediaTime>, DecodeError> {
let Some(raw) = self.decode_raw_next(cancel)? else {
return Ok(None);
};
let pts = self.raw_frame_pts(&raw);
self.pending.push_front(raw);
pts.map(Some)
}
pub(crate) fn decode_raw_next(
&mut self,
cancel: &CancelToken,
) -> Result<Option<DecodedFrame>, DecodeError> {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
if let Some(frame) = self.pending.pop_front() {
return Ok(Some(frame));
}
interrupt_on_cancel(&self.input, cancel);
let mut budget = WorkBudget::new();
let mut foreign = false;
loop {
if std::mem::take(&mut foreign) {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
} else {
budget.step(cancel)?;
}
if let Some(frame) = self.pending.pop_front() {
return Ok(Some(frame));
}
if let Some(frame) = self.receive_available_raw(cancel)? {
return Ok(Some(frame));
}
if self.demux_eof {
if !self.decoder_flushed {
match self.decoder.send_packet(None) {
Ok(()) | Err(RsmpegError::DecoderFlushedError) => {}
Err(RsmpegError::DecoderFullError) => continue,
Err(error) => {
self.recovery.error(error, "flush", None)?;
continue;
}
}
self.decoder_flushed = true;
continue;
}
self.recovery.video_eof()?;
return Ok(None);
}
let demux_started = Instant::now();
let packet = self
.prefetched
.pop_front()
.unwrap_or_else(|| self.input.read_packet());
self.stages.demux = self.stages.demux.saturating_add(demux_started.elapsed());
let packet = match packet {
Ok(Some(packet)) => packet,
Ok(None) => {
self.demux_eof = true;
continue;
}
Err(error) => {
let error = demux_failure(error, cancel)?;
self.recovery.error(error, "video demux", None)?;
continue;
}
};
if usize::try_from(packet.stream_index).ok() != Some(self.video_index) {
foreign = true;
continue;
}
if packet.flags & i32::try_from(ffi::AV_PKT_FLAG_CORRUPT).expect("flag fits i32") != 0 {
self.recovery.corrupt("video packet", Some(packet.pts))?;
continue;
}
submit_packet(
|drain| {
if drain {
let corrupt_before = self.recovery.diagnostics.corrupt_packets.len();
if let Some(frame) = self.receive_available_raw(cancel)? {
self.pending.push_back(frame);
return Ok(true);
}
return Ok(self.recovery.diagnostics.corrupt_packets.len() > corrupt_before);
}
let started = Instant::now();
let sent = self.decoder.send_packet(Some(&packet));
self.stages.send_packet += started.elapsed();
match sent {
Ok(()) => Ok(true),
Err(RsmpegError::DecoderFullError) => Ok(false),
Err(error) => {
self.recovery.error(error, "video send", Some(packet.pts))?;
Ok(true)
}
}
},
&mut budget,
cancel,
)?;
}
}
}
impl Decoder for NativeDecoder {
fn streams(&self) -> &[StreamInfo] {
&self.streams
}
fn flush(&mut self) -> Result<(), DecodeError> {
self.flush_video();
Ok(())
}
fn seek(
&mut self,
request: DecodeRequest,
cancel: &CancelToken,
) -> Result<DecodeResult, DecodeError> {
let raw = self.seek_raw(request.time, cancel)?;
let frame = self.convert_received(&raw)?;
Ok(DecodeResult {
request,
event: DecodeEvent::Video(frame),
})
}
fn decode_next(&mut self, cancel: &CancelToken) -> Result<DecodeEvent, DecodeError> {
if cancel.is_cancelled() {
return Err(DecodeError::Cancelled);
}
match self.decode_raw_next(cancel)? {
Some(frame) => Ok(DecodeEvent::Video(self.convert_received(&frame)?)),
None => Ok(DecodeEvent::EndOfStream),
}
}
}
const HWACCEL_ONLY_DECODERS: &[&str] = &["av1"];
pub(crate) fn registered_video_decoders(
codec_id: ffi::AVCodecID,
) -> impl Iterator<Item = AVCodecRef<'static>> {
AVCodec::iterate().filter(move |codec| {
codec.id == codec_id
&& AVCodec::find_decoder_by_name(codec.name())
.is_some_and(|found| found.as_ptr() == codec.as_ptr())
})
}
pub(crate) fn software_video_decoder(
codec_id: ffi::AVCodecID,
) -> Result<AVCodecRef<'static>, DecodeError> {
let Some(preferred) = AVCodec::find_decoder(codec_id) else {
return Err(DecodeError::Unsupported(
"selected video codec has no decoder".into(),
));
};
let hardware_only =
|codec: &AVCodecRef<'_>| HWACCEL_ONLY_DECODERS.contains(&&*codec.name().to_string_lossy());
if !hardware_only(&preferred) {
return Ok(preferred);
}
let experimental = i32::try_from(ffi::AV_CODEC_CAP_EXPERIMENTAL).unwrap_or(0);
AVCodec::iterate()
.find(|codec| {
codec.id == codec_id
&& AVCodec::find_decoder_by_name(codec.name())
.is_some_and(|found| found.as_ptr() == codec.as_ptr())
&& codec.capabilities & experimental == 0
&& !hardware_only(codec)
})
.ok_or_else(|| DecodeError::Unsupported(hardware_only_reason(&preferred)))
}
fn hardware_only_reason(decoder: &AVCodecRef<'_>) -> String {
let name = decoder.name().to_string_lossy();
format!(
"{name} needs a hardware decoder; this runtime has no software {} decoder",
name.to_uppercase()
)
}
#[must_use]
pub fn software_decode_unsupported(stream: &StreamInfo) -> Option<String> {
if stream.kind != StreamKind::Video {
return None;
}
let name = CString::new(stream.codec.as_str()).ok()?;
let decoder = AVCodec::find_decoder_by_name(&name)?;
software_video_decoder(decoder.id)
.err()
.map(|error| match error {
DecodeError::Unsupported(reason) => reason,
error => error.to_string(),
})
}
pub(crate) fn effective_media_duration(
stream_duration: Option<MediaTime>,
container_duration: Option<MediaTime>,
) -> Option<MediaTime> {
stream_duration.into_iter().chain(container_duration).max()
}
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
fn check_hardware_crop(
parameters: &rsmpeg::avcodec::AVCodecParameters,
spec: &HardwareDecoderSpec,
) -> Result<(), HardwareOpenError> {
if spec.keeps_leading_crop {
return Ok(());
}
let extradata = alpha2_ffmpeg_bridge::codec_parameters_extradata(parameters);
match crate::parameter_sets::leading_crop(parameters.codec_id, &extradata) {
Some((left, top)) if left > 0 || top > 0 => Err(HardwareOpenError::new(
crate::video_frame::HardwareDecodeFallback::UnsupportedCrop,
DecodeError::Unsupported(format!(
"{} decode does not show a picture cropped {left} columns from the left and {top} rows from the top",
spec.backend
)),
)),
_ => Ok(()),
}
}
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
fn check_hardware_sample_profile(
format: ffi::AVPixelFormat,
spec: &HardwareDecoderSpec,
) -> Result<(), HardwareOpenError> {
let Some(profile) = alpha2_ffmpeg_bridge::sample_profile(format) else {
return Ok(());
};
let maximum_depth = if spec.ten_bit { 10 } else { 8 };
if profile.is_yuv_420 && !profile.has_alpha && profile.bit_depth <= maximum_depth {
return Ok(());
}
Err(HardwareOpenError::new(
crate::video_frame::HardwareDecodeFallback::UnsupportedProfile,
DecodeError::Unsupported(format!(
"{} decode does not present {}-bit {} input",
spec.backend,
profile.bit_depth,
if profile.is_yuv_420 {
"4:2:0"
} else {
"non-4:2:0"
}
)),
))
}
pub(crate) const SOURCE_CONTAINERS: &CStr =
c"mov,matroska,mpegts,avi,mp3,aac,wav,flac,ogg,png_pipe,jpeg_pipe,webp_pipe,gif";
pub(crate) fn open_input(path: &Path) -> Result<DemuxInput, DecodeError> {
#[cfg(test)]
INPUTS_OPENED.with(|opened| opened.set(opened.get() + 1));
let text = path
.to_str()
.ok_or_else(|| DecodeError::Unsupported("media path is not UTF-8".into()))?;
let path = CString::new(text)
.map_err(|_| DecodeError::Unsupported("media path contains a NUL byte".into()))?;
alpha2_ffmpeg_bridge::open_input(&path, SOURCE_CONTAINERS).map_err(|error| match error {
OpenInputError::Refused(format) => DecodeError::Unsupported(format!(
"the file is {format} input, which is not a container alpha2 reads"
)),
OpenInputError::Ffmpeg(error) => adapter_error(error),
})
}
#[cfg(test)]
thread_local! {
pub(crate) static INPUTS_OPENED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
pub(crate) static VIDEO_FRAMES_DECODED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(crate) fn interrupt_on_cancel(input: &DemuxInput, cancel: &CancelToken) {
let cancel = cancel.clone();
input.interrupt_when(move || cancel.is_cancelled());
}
fn demux_failure(error: RsmpegError, cancel: &CancelToken) -> Result<RsmpegError, DecodeError> {
if cancel.is_cancelled() {
Err(DecodeError::Cancelled)
} else {
Ok(error)
}
}
fn seek_failure(error: RsmpegError, cancel: &CancelToken) -> DecodeError {
demux_failure(error, cancel).map_or_else(|cancelled| cancelled, adapter_error)
}
const STILL_IMAGE_FORMATS: [&CStr; 3] = [c"jpeg_pipe", c"png_pipe", c"webp_pipe"];
fn is_still_image_format(input: &DemuxInput) -> bool {
STILL_IMAGE_FORMATS.contains(&input.iformat().name())
}
fn still_image_orientation(path: &Path, index: usize) -> Option<Orientation> {
let mut input = open_input(path).ok()?;
let (codec, mut decoder) = {
let stream = input.streams().get(index)?;
let parameters = stream.codecpar();
let codec = software_video_decoder(parameters.codec_id).ok()?;
let mut decoder = AVCodecContext::new(&codec);
decoder.apply_codecpar(¶meters).ok()?;
decoder.set_pkt_timebase(stream.time_base);
(codec, decoder)
};
let mut options = video_decoder_options(1);
if codec.max_lowres > 0 {
options = options.set_int(c"lowres", i64::from(codec.max_lowres), 0);
}
alpha2_ffmpeg_bridge::open_codec_context(&mut decoder, options).ok()?;
let mut sent = false;
for _ in 0..16 {
match decoder.receive_frame() {
Ok(frame) => {
return alpha2_ffmpeg_bridge::frame_display_rotation(&frame)
.and_then(orientation_from_display);
}
Err(RsmpegError::DecoderDrainError) => {}
Err(_) => return None,
}
match input.read_packet().ok()? {
Some(packet) if usize::try_from(packet.stream_index).ok() == Some(index) => {
decoder.send_packet(Some(&packet)).ok()?;
sent = true;
}
Some(_) => {}
None if sent => {
decoder.send_packet(None).ok()?;
sent = false;
}
None => return None,
}
}
None
}
#[allow(clippy::too_many_lines)]
const PARSED_END_READ: u64 = 250_000;
fn parsed_end(input: &DemuxInput, path: &Path) -> Option<i64> {
if input.duration_estimation_method != ffi::AVFMT_DURATION_FROM_PTS {
return None;
}
let size = std::fs::metadata(path).ok()?.len();
let mut tail = open_input(path).ok()?;
let streams = tail
.streams()
.iter()
.map(|stream| {
let kind = stream.codecpar().codec_type();
(kind.is_audio() || kind.is_video()).then_some(stream.time_base)
})
.collect::<Vec<_>>();
let av_time_base = ra(1, i32::try_from(ffi::AV_TIME_BASE).ok()?);
for doubling in 0..6 {
let from = size.saturating_sub(PARSED_END_READ << doubling);
tail.seek(
-1,
i64::try_from(from).ok()?,
i32::try_from(ffi::AVSEEK_FLAG_BYTE).ok()?,
)
.ok()?;
let mut end = None::<i64>;
while let Ok(Some(packet)) = tail.read_packet() {
let Some(Some(time_base)) = usize::try_from(packet.stream_index)
.ok()
.and_then(|index| streams.get(index))
else {
continue;
};
if packet.pts == ffi::AV_NOPTS_VALUE {
continue;
}
let packet_end = av_rescale_q(
packet.pts.saturating_add(packet.duration.max(0)),
*time_base,
av_time_base,
);
end = Some(end.map_or(packet_end, |end: i64| end.max(packet_end)));
}
if end.is_some() || from == 0 {
return end;
}
}
None
}
pub(crate) fn probe_context(input: &DemuxInput, path: &Path) -> Result<ProbeResult, DecodeError> {
let selected_video = input
.best_stream(ffi::AVMEDIA_TYPE_VIDEO)
.map_err(adapter_error)?;
let selected_audio = input
.best_stream(ffi::AVMEDIA_TYPE_AUDIO)
.map_err(adapter_error)?;
let streams = input
.streams()
.iter()
.enumerate()
.filter_map(|(index, stream)| {
let parameters = stream.codecpar();
let kind = if parameters.codec_type().is_video() {
StreamKind::Video
} else if parameters.codec_type().is_audio() {
StreamKind::Audio
} else {
return None;
};
let codec = AVCodec::find_decoder(parameters.codec_id).map_or_else(
|| "unknown".to_owned(),
|codec| codec.name().to_string_lossy().into_owned(),
);
let dimensions = (kind == StreamKind::Video).then(|| {
(
u32::try_from(parameters.width).unwrap_or(0),
u32::try_from(parameters.height).unwrap_or(0),
)
});
let frame_rate = stream
.guess_framerate()
.filter(|rate| rate.num > 0 && rate.den > 0)
.and_then(|rate| {
FrameRate::new(u32::try_from(rate.num).ok()?, u32::try_from(rate.den).ok()?)
.ok()
});
Some(StreamInfo {
index,
kind,
codec,
duration: timestamp(stream.duration, stream.time_base),
dimensions,
frame_rate,
sample_rate: (parameters.sample_rate > 0)
.then(|| u32::try_from(parameters.sample_rate).unwrap_or(0)),
channels: (kind == StreamKind::Audio)
.then(|| u32::try_from(parameters.ch_layout.nb_channels).unwrap_or(0)),
orientation: alpha2_ffmpeg_bridge::display_rotation(¶meters)
.and_then(orientation_from_display)
.or_else(|| {
(kind == StreamKind::Video
&& Some(index) == selected_video
&& is_still_image_format(input))
.then(|| still_image_orientation(path, index))
.flatten()
})
.unwrap_or_else(|| Orientation::rotated(stream_rotation(stream.metadata()))),
sample_aspect_ratio: guess_sar(
stream.sample_aspect_ratio,
parameters.sample_aspect_ratio,
),
color: if kind == StreamKind::Video {
color_metadata(¶meters)
} else {
None
},
})
})
.collect();
let av_time_base = ra(
1,
i32::try_from(ffi::AV_TIME_BASE).expect("time base fits i32"),
);
let duration = parsed_end(input, path)
.filter(|_| input.start_time != ffi::AV_NOPTS_VALUE)
.map(|end| end.saturating_sub(input.start_time))
.map_or(input.duration, |parsed| parsed.max(input.duration));
Ok(ProbeResult {
format: input.iformat().name().to_string_lossy().into_owned(),
duration: (duration > 0)
.then(|| MediaTime::from_ticks(av_rescale_q(duration, av_time_base, tick_base()))),
start: (input.start_time != ffi::AV_NOPTS_VALUE).then(|| {
MediaTime::from_ticks(av_rescale_q(
input.start_time,
ra(
1,
i32::try_from(ffi::AV_TIME_BASE).expect("time base fits i32"),
),
tick_base(),
))
}),
streams,
selected_video,
selected_audio,
})
}