use crate::error::{
DecodingOperationError, DemuxingOperationError, EncodingOperationError, Error,
FilterGraphOperationError, MuxingOperationError, OpenDecoderOperationError,
OpenEncoderOperationError, OpenOutputError, PacketSinkError,
};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobFailureKind {
Decode,
Encode,
Filter,
Mux,
Io,
Callback,
Other,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct JobFailureSummary {
kind: JobFailureKind,
stream_index: Option<usize>,
ffmpeg_code: Option<i32>,
message: String,
}
impl JobFailureSummary {
pub fn kind(&self) -> JobFailureKind {
self.kind
}
pub fn stream_index(&self) -> Option<usize> {
self.stream_index
}
pub fn ffmpeg_code(&self) -> Option<i32> {
self.ffmpeg_code
}
pub fn message(&self) -> &str {
&self.message
}
pub(crate) fn from_error(error: &Error) -> Self {
let (kind, stream_index, ffmpeg_code) = classify(error);
Self {
kind,
stream_index,
ffmpeg_code,
message: error.to_string(),
}
}
pub(crate) fn formatting_panicked() -> Self {
Self {
kind: JobFailureKind::Other,
stream_index: None,
ffmpeg_code: None,
message: String::from(
"job error message unavailable: formatting the recorded error panicked",
),
}
}
pub(crate) fn into_message(self) -> String {
self.message
}
}
impl std::fmt::Display for JobFailureSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
macro_rules! unknown_code_fn {
($name:ident, $leaf:ident) => {
fn $name(e: &crate::error::$leaf) -> Option<i32> {
match e {
crate::error::$leaf::UnknownError(code) => Some(*code),
_ => None,
}
}
};
}
unknown_code_fn!(muxing_leaf_code, MuxingError);
unknown_code_fn!(write_header_code, WriteHeaderError);
unknown_code_fn!(encoding_leaf_code, EncodingError);
unknown_code_fn!(encode_subtitle_code, EncodeSubtitleError);
unknown_code_fn!(open_encoder_leaf_code, OpenEncoderError);
unknown_code_fn!(decoding_leaf_code, DecodingError);
unknown_code_fn!(open_decoder_leaf_code, OpenDecoderError);
unknown_code_fn!(filter_graph_leaf_code, FilterGraphError);
unknown_code_fn!(filter_parse_code, FilterGraphParseError);
unknown_code_fn!(demuxing_leaf_code, DemuxingError);
unknown_code_fn!(open_input_code, OpenInputError);
unknown_code_fn!(find_stream_code, FindStreamError);
unknown_code_fn!(open_output_code, OpenOutputError);
unknown_code_fn!(alloc_output_code, AllocOutputContextError);
fn classify(error: &Error) -> (JobFailureKind, Option<usize>, Option<i32>) {
match error {
Error::Decoder(_) => (JobFailureKind::Decode, None, None),
Error::OpenDecoder(e) => (JobFailureKind::Decode, None, open_decoder_code(e)),
Error::Decoding(e) => (JobFailureKind::Decode, None, decoding_code(e)),
Error::OpenEncoder(e) => (JobFailureKind::Encode, None, open_encoder_code(e)),
Error::Encoding(e) => (JobFailureKind::Encode, None, encoding_code(e)),
Error::FilterGraphParse(e) => (JobFailureKind::Filter, None, filter_parse_code(e)),
Error::FilterGraph(e) => (JobFailureKind::Filter, None, filter_graph_code(e)),
Error::FilterDescUtf8
| Error::FilterNameUtf8
| Error::FilterZeroOutputs
| Error::FilterZeroInputs
| Error::FrameFilterInit(_)
| Error::FrameFilterProcess(_)
| Error::FrameFilterRequest(_)
| Error::FrameFilterTypeNoMatched(_, _)
| Error::FrameFilterDstFinished
| Error::FrameFilterFrameDuplicateFailed
| Error::FrameFilterThreadExited => (JobFailureKind::Filter, None, None),
Error::FrameFilterStreamTypeNoMatched(_, stream_index, _) => {
(JobFailureKind::Filter, Some(*stream_index), None)
}
#[cfg(feature = "wgpu")]
Error::WgpuFilter(_) => (JobFailureKind::Filter, None, None),
#[cfg(feature = "opengl")]
Error::OpenGLFilter(_) => (JobFailureKind::Filter, None, None),
Error::Muxing(e) => (JobFailureKind::Mux, None, muxing_code(e)),
Error::AllocOutputContext(e) => (JobFailureKind::Mux, None, alloc_output_code(e)),
Error::PacketSink(e) => classify_packet_sink(e),
Error::OpenInputStream(e) => (JobFailureKind::Io, None, open_input_code(e)),
Error::FindStream(e) => (JobFailureKind::Io, None, find_stream_code(e)),
Error::OpenOutput(OpenOutputError::UnknownFrameFormat) => {
(JobFailureKind::Encode, None, None)
}
Error::OpenOutput(e) => (JobFailureKind::Io, None, open_output_code(e)),
Error::Demuxing(e) => (JobFailureKind::Io, None, demuxing_code(e)),
Error::IO(_) => (JobFailureKind::Io, None, None),
_ => (JobFailureKind::Other, None, None),
}
}
fn classify_packet_sink(e: &PacketSinkError) -> (JobFailureKind, Option<usize>, Option<i32>) {
match e {
PacketSinkError::StreamInfoCallbackFailed { .. } => (JobFailureKind::Callback, None, None),
PacketSinkError::PacketCallbackFailed { stream_index, .. } => {
(JobFailureKind::Callback, Some(*stream_index), None)
}
PacketSinkError::MissingExtradata { stream_index }
| PacketSinkError::InvalidExtradata { stream_index, .. }
| PacketSinkError::InvalidTimeBase { stream_index, .. }
| PacketSinkError::PacketTimeBaseMismatch { stream_index, .. }
| PacketSinkError::MissingTimestamp { stream_index, .. }
| PacketSinkError::NonMonotonicDts { stream_index, .. }
| PacketSinkError::DuplicatePts { stream_index, .. }
| PacketSinkError::PtsBeforeDts { stream_index, .. }
| PacketSinkError::TimestampOverflow { stream_index }
| PacketSinkError::MissingDuration { stream_index }
| PacketSinkError::MalformedPacket { stream_index, .. }
| PacketSinkError::PhaseViolation { stream_index }
| PacketSinkError::ConfigChange { stream_index, .. }
| PacketSinkError::InBandParameterSets { stream_index } => {
(JobFailureKind::Mux, Some(*stream_index), None)
}
_ => (JobFailureKind::Mux, None, None),
}
}
fn muxing_code(e: &MuxingOperationError) -> Option<i32> {
match e {
MuxingOperationError::WriteHeader(w) => write_header_code(w),
MuxingOperationError::BitstreamFilterInit(_, m)
| MuxingOperationError::InterleavedWriteError(m)
| MuxingOperationError::TrailerWriteError(m)
| MuxingOperationError::IOCloseError(m) => muxing_leaf_code(m),
_ => None,
}
}
fn encoding_code(e: &EncodingOperationError) -> Option<i32> {
match e {
EncodingOperationError::SendFrameError(x)
| EncodingOperationError::ReceivePacketError(x)
| EncodingOperationError::ReceiveAudioError(x) => encoding_leaf_code(x),
EncodingOperationError::EncodeSubtitle(s) => encode_subtitle_code(s),
_ => None,
}
}
fn open_encoder_code(e: &OpenEncoderOperationError) -> Option<i32> {
match e {
OpenEncoderOperationError::FrameSideDataCloneError(x)
| OpenEncoderOperationError::ChannelLayoutCopyError(x)
| OpenEncoderOperationError::CodecOpenError(x)
| OpenEncoderOperationError::CodecParametersError(x)
| OpenEncoderOperationError::SettingSubtitleError(x)
| OpenEncoderOperationError::HwSetupError(x)
| OpenEncoderOperationError::ContextAllocationError(x) => open_encoder_leaf_code(x),
_ => None,
}
}
fn decoding_code(e: &DecodingOperationError) -> Option<i32> {
match e {
DecodingOperationError::FrameRefError(x)
| DecodingOperationError::FrameCopyPropsError(x)
| DecodingOperationError::DecodeSubtitleError(x)
| DecodingOperationError::CopySubtitleError(x)
| DecodingOperationError::SendPacketError(x)
| DecodingOperationError::ReceiveFrameError(x)
| DecodingOperationError::FrameAllocationError(x)
| DecodingOperationError::PacketAllocationError(x)
| DecodingOperationError::SubtitleAllocationError(x)
| DecodingOperationError::HWRetrieveDataError(x)
| DecodingOperationError::CroppingError(x) => decoding_leaf_code(x),
_ => None,
}
}
fn open_decoder_code(e: &OpenDecoderOperationError) -> Option<i32> {
match e {
OpenDecoderOperationError::ContextAllocationError(x)
| OpenDecoderOperationError::ParameterApplicationError(x)
| OpenDecoderOperationError::DecoderOpenError(x)
| OpenDecoderOperationError::ChannelLayoutCopyError(x)
| OpenDecoderOperationError::HwSetupError(x) => open_decoder_leaf_code(x),
_ => None,
}
}
fn filter_graph_code(e: &FilterGraphOperationError) -> Option<i32> {
match e {
FilterGraphOperationError::RequestOldestError(x)
| FilterGraphOperationError::ProcessFramesError(x)
| FilterGraphOperationError::SendFramesError(x)
| FilterGraphOperationError::ChannelLayoutCopyError(x)
| FilterGraphOperationError::BufferSourceAddFrameError(x)
| FilterGraphOperationError::BufferSourceCloseError(x)
| FilterGraphOperationError::BufferReplaceoseError(x)
| FilterGraphOperationError::FrameSideDataCloneError(x) => filter_graph_leaf_code(x),
FilterGraphOperationError::ParseError(p) => filter_parse_code(p),
_ => None,
}
}
fn demuxing_code(e: &DemuxingOperationError) -> Option<i32> {
match e {
DemuxingOperationError::ReadFrameError(x)
| DemuxingOperationError::PacketRefError(x)
| DemuxingOperationError::SeekFileError(x) => demuxing_leaf_code(x),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::packet_sink::PacketCallbackError;
use crate::error::{
AllocOutputContextError, DecoderError, DecodingError, DecodingOperationError,
DemuxingError, DemuxingOperationError, EncodeSubtitleError, EncodingError,
EncodingOperationError, Error, FilterGraphOperationError, FilterGraphParseError,
MuxingError, MuxingOperationError, OpenDecoderOperationError, OpenEncoderOperationError,
OpenInputError, OpenOutputError, PacketSinkError, WriteHeaderError,
};
fn classified(e: &Error) -> (JobFailureKind, Option<usize>, Option<i32>) {
let s = JobFailureSummary::from_error(e);
(s.kind(), s.stream_index(), s.ffmpeg_code())
}
#[test]
fn maps_error_variants_to_coarse_kinds() {
assert_eq!(
classified(&Error::Decoder(DecoderError::NotFound("h264".into()))).0,
JobFailureKind::Decode
);
assert_eq!(
classified(&Error::OpenDecoder(OpenDecoderOperationError::InvalidName)).0,
JobFailureKind::Decode
);
assert_eq!(
classified(&Error::Decoding(DecodingOperationError::CorruptFrame)).0,
JobFailureKind::Decode
);
assert_eq!(
classified(&Error::OpenEncoder(OpenEncoderOperationError::NoFramesReceived)).0,
JobFailureKind::Encode
);
assert_eq!(
classified(&Error::Encoding(EncodingOperationError::MuxerFinished)).0,
JobFailureKind::Encode
);
assert_eq!(
classified(&Error::FilterGraph(FilterGraphOperationError::InvalidData)).0,
JobFailureKind::Filter
);
assert_eq!(
classified(&Error::FilterZeroInputs).0,
JobFailureKind::Filter
);
assert_eq!(
classified(&Error::FrameFilterProcess(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
"user filter failed",
))))
.0,
JobFailureKind::Filter
);
assert_eq!(
classified(&Error::FrameFilterStreamTypeNoMatched(
"video".into(),
2,
"audio".into()
)),
(JobFailureKind::Filter, Some(2), None)
);
assert_eq!(
classified(&Error::Muxing(MuxingOperationError::ThreadExited)).0,
JobFailureKind::Mux
);
assert_eq!(
classified(&Error::AllocOutputContext(
AllocOutputContextError::OutOfMemory
))
.0,
JobFailureKind::Mux
);
assert_eq!(
classified(&Error::OpenInputStream(OpenInputError::Timeout)).0,
JobFailureKind::Io
);
assert_eq!(
classified(&Error::Demuxing(DemuxingOperationError::ThreadExited)).0,
JobFailureKind::Io
);
assert_eq!(
classified(&Error::IO(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"peer gone",
)))
.0,
JobFailureKind::Io
);
assert_eq!(
classified(&Error::WorkerPanicked("muxer1:mpegts".into())).0,
JobFailureKind::Other
);
assert_eq!(classified(&Error::StartFailed).0, JobFailureKind::Other);
assert_eq!(classified(&Error::Bug).0, JobFailureKind::Other);
assert_eq!(classified(&Error::NotStarted).0, JobFailureKind::Other);
}
#[test]
fn sibling_packet_sink_errors_split_callback_from_delivery() {
assert_eq!(
classified(&Error::PacketSink(PacketSinkError::PacketCallbackFailed {
stream_index: 3,
error: PacketCallbackError::new("consumer said no"),
})),
(JobFailureKind::Callback, Some(3), None)
);
assert_eq!(
classified(&Error::PacketSink(
PacketSinkError::StreamInfoCallbackFailed {
error: PacketCallbackError::new("config rejected"),
}
)),
(JobFailureKind::Callback, None, None)
);
assert_eq!(
classified(&Error::PacketSink(PacketSinkError::NonMonotonicDts {
stream_index: 1,
prev: 5,
current: 5,
})),
(JobFailureKind::Mux, Some(1), None)
);
assert_eq!(
classified(&Error::PacketSink(PacketSinkError::ChannelDisconnected)),
(JobFailureKind::Mux, None, None)
);
}
#[test]
fn extracts_raw_codes_only_from_unknown_error_leaves() {
let ext = ffmpeg_sys_next::AVERROR_EXTERNAL;
assert_eq!(
classified(&Error::Muxing(MuxingOperationError::InterleavedWriteError(
MuxingError::UnknownError(ext)
)))
.2,
Some(ext)
);
assert_eq!(
classified(&Error::Muxing(MuxingOperationError::WriteHeader(
WriteHeaderError::UnknownError(-77)
)))
.2,
Some(-77)
);
assert_eq!(
classified(&Error::Encoding(EncodingOperationError::SendFrameError(
EncodingError::UnknownError(-1)
)))
.2,
Some(-1)
);
assert_eq!(
classified(&Error::Decoding(DecodingOperationError::ReceiveFrameError(
DecodingError::UnknownError(-2)
)))
.2,
Some(-2)
);
assert_eq!(
classified(&Error::FilterGraph(FilterGraphOperationError::ParseError(
FilterGraphParseError::UnknownError(-3)
)))
.2,
Some(-3)
);
assert_eq!(
classified(&Error::Demuxing(DemuxingOperationError::ReadFrameError(
DemuxingError::UnknownError(-4)
)))
.2,
Some(-4)
);
assert_eq!(
classified(&Error::Muxing(MuxingOperationError::InterleavedWriteError(
MuxingError::IOError
)))
.2,
None
);
assert_eq!(
classified(&Error::IO(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"peer gone",
)))
.2,
None
);
}
#[test]
fn runtime_encoder_open_failure_classifies_as_encode_not_io() {
assert_eq!(
classified(&Error::OpenOutput(OpenOutputError::UnknownFrameFormat)).0,
JobFailureKind::Encode
);
assert_eq!(
classified(&Error::OpenOutput(OpenOutputError::Timeout)).0,
JobFailureKind::Io
);
}
#[test]
fn subtitle_encode_unknown_code_is_extracted() {
assert_eq!(
classified(&Error::Encoding(EncodingOperationError::EncodeSubtitle(
EncodeSubtitleError::UnknownError(-77)
))),
(JobFailureKind::Encode, None, Some(-77))
);
}
#[test]
fn message_is_the_error_display_and_the_summary_displays_it() {
let recorded = Error::WorkerPanicked("muxer1:mpegts".to_string());
let summary = JobFailureSummary::from_error(&recorded);
assert_eq!(summary.message(), recorded.to_string());
assert_eq!(summary.to_string(), recorded.to_string());
assert_eq!(
summary.message(),
"Worker thread 'muxer1:mpegts' panicked; output may be incomplete"
);
}
#[test]
fn formatting_panic_fallback_matches_the_fixed_message() {
let summary = JobFailureSummary::formatting_panicked();
assert_eq!(summary.kind(), JobFailureKind::Other);
assert_eq!(summary.stream_index(), None);
assert_eq!(summary.ffmpeg_code(), None);
assert_eq!(
summary.message(),
"job error message unavailable: formatting the recorded error panicked"
);
}
}