use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EncodeError {
#[error("Cannot create output file: {path}")]
CannotCreateFile {
path: PathBuf,
},
#[error("Unsupported codec: {codec}")]
UnsupportedCodec {
codec: String,
},
#[error("No suitable encoder found for {codec} (tried: {tried:?})")]
NoSuitableEncoder {
codec: String,
tried: Vec<String>,
},
#[error("Encoding failed at frame {frame}: {reason}")]
EncodingFailed {
frame: u64,
reason: String,
},
#[error("Invalid configuration: {reason}")]
InvalidConfig {
reason: String,
},
#[error("Hardware encoder unavailable: {encoder}")]
HwEncoderUnavailable {
encoder: String,
},
#[error("encoder unavailable: codec={codec} hint={hint}")]
EncoderUnavailable {
codec: String,
hint: String,
},
#[error("Muxing failed: {reason}")]
MuxingFailed {
reason: String,
},
#[error("ffmpeg error: {message} (code={code})")]
Ffmpeg {
code: i32,
message: String,
},
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid option: {name} — {reason}")]
InvalidOption {
name: String,
reason: String,
},
#[error("codec {codec} is not supported by container {container} — {hint}")]
UnsupportedContainerCodecCombination {
container: String,
codec: String,
hint: String,
},
#[error("dimensions {width}x{height} out of range [2, 32768]")]
InvalidDimensions {
width: u32,
height: u32,
},
#[error("bitrate {bitrate} bps exceeds maximum 800 Mbps (800,000,000 bps)")]
InvalidBitrate {
bitrate: u64,
},
#[error("channel count {count} exceeds maximum 8")]
InvalidChannelCount {
count: u32,
},
#[error("sample rate {rate} Hz outside supported range [8000, 384000]")]
InvalidSampleRate {
rate: u32,
},
#[error("Encoding cancelled by user")]
Cancelled,
#[error("Async encoder worker panicked or disconnected")]
WorkerPanicked,
#[error("media operation failed: {reason}")]
MediaOperationFailed {
reason: String,
},
#[error("preset constraint violated: preset={preset} reason={reason}")]
PresetConstraintViolation {
preset: String,
reason: String,
},
}
impl EncodeError {
pub(crate) fn from_ffmpeg_error(errnum: i32) -> Self {
EncodeError::Ffmpeg {
code: errnum,
message: ff_sys::av_error_string(errnum),
}
}
}
#[cfg(test)]
mod tests {
use super::EncodeError;
#[test]
fn from_ffmpeg_error_should_return_ffmpeg_variant() {
let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
assert!(matches!(err, EncodeError::Ffmpeg { .. }));
}
#[test]
fn from_ffmpeg_error_should_carry_numeric_code() {
let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
match err {
EncodeError::Ffmpeg { code, .. } => assert_eq!(code, ff_sys::error_codes::EINVAL),
_ => panic!("expected Ffmpeg variant"),
}
}
#[test]
fn from_ffmpeg_error_should_format_with_code_in_display() {
let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EINVAL);
let msg = err.to_string();
assert!(msg.contains("code=-22"), "expected 'code=-22' in '{msg}'");
}
#[test]
fn from_ffmpeg_error_message_should_be_nonempty() {
let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::ENOMEM);
assert!(!err.to_string().is_empty());
}
#[test]
fn from_ffmpeg_error_eof_should_be_constructible() {
let err = EncodeError::from_ffmpeg_error(ff_sys::error_codes::EOF);
assert!(matches!(err, EncodeError::Ffmpeg { .. }));
assert!(!err.to_string().is_empty());
}
#[test]
fn invalid_dimensions_display_should_contain_dimension_string() {
let err = EncodeError::InvalidDimensions {
width: 0,
height: 720,
};
let msg = err.to_string();
assert!(msg.contains("0x720"), "expected '0x720' in '{msg}'");
}
#[test]
fn invalid_dimensions_display_should_contain_range_hint() {
let err = EncodeError::InvalidDimensions {
width: 99999,
height: 99999,
};
let msg = err.to_string();
assert!(
msg.contains("[2, 32768]"),
"expected '[2, 32768]' in '{msg}'"
);
}
#[test]
fn invalid_bitrate_display_should_contain_bitrate_value() {
let err = EncodeError::InvalidBitrate {
bitrate: 900_000_000,
};
let msg = err.to_string();
assert!(msg.contains("900000000"), "expected '900000000' in '{msg}'");
}
#[test]
fn invalid_bitrate_display_should_contain_maximum_hint() {
let err = EncodeError::InvalidBitrate {
bitrate: 900_000_000,
};
let msg = err.to_string();
assert!(
msg.contains("800,000,000"),
"expected '800,000,000' in '{msg}'"
);
}
#[test]
fn invalid_channel_count_display_should_contain_count() {
let err = EncodeError::InvalidChannelCount { count: 9 };
let msg = err.to_string();
assert!(msg.contains('9'), "expected '9' in '{msg}'");
}
#[test]
fn invalid_channel_count_display_should_contain_maximum_hint() {
let err = EncodeError::InvalidChannelCount { count: 9 };
let msg = err.to_string();
assert!(msg.contains('8'), "expected '8' in '{msg}'");
}
#[test]
fn invalid_sample_rate_display_should_contain_rate() {
let err = EncodeError::InvalidSampleRate { rate: 7999 };
let msg = err.to_string();
assert!(msg.contains("7999"), "expected '7999' in '{msg}'");
}
#[test]
fn invalid_sample_rate_display_should_contain_range_hint() {
let err = EncodeError::InvalidSampleRate { rate: 7999 };
let msg = err.to_string();
assert!(
msg.contains("[8000, 384000]"),
"expected '[8000, 384000]' in '{msg}'"
);
}
#[test]
fn encode_error_media_operation_failed_should_display_correctly() {
let err = EncodeError::MediaOperationFailed {
reason: "input file has no audio stream".to_string(),
};
let msg = err.to_string();
assert!(
msg.contains("media operation failed"),
"expected 'media operation failed' in '{msg}'"
);
assert!(
msg.contains("input file has no audio stream"),
"expected reason in '{msg}'"
);
assert!(
matches!(err, EncodeError::MediaOperationFailed { .. }),
"pattern match with struct syntax must work"
);
}
}