use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{error::Error, fmt};
use alpha2_time::{FrameRate, MediaTime};
use crate::{ColorMetadata, SourceId};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
pub enum Rotation {
#[default]
Deg0,
Deg90,
Deg180,
Deg270,
}
impl Rotation {
#[must_use]
pub const fn quarter_turns(self) -> u32 {
match self {
Self::Deg0 => 0,
Self::Deg90 => 1,
Self::Deg180 => 2,
Self::Deg270 => 3,
}
}
#[must_use]
pub const fn from_quarter_turns(quarter_turns: i32) -> Self {
match quarter_turns.rem_euclid(4) {
0 => Self::Deg0,
1 => Self::Deg90,
2 => Self::Deg180,
_ => Self::Deg270,
}
}
#[must_use]
pub const fn swaps_axes(self) -> bool {
matches!(self, Self::Deg90 | Self::Deg270)
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq)]
pub struct Orientation {
pub rotation: Rotation,
pub mirror: bool,
}
impl Orientation {
pub const UPRIGHT: Self = Self {
rotation: Rotation::Deg0,
mirror: false,
};
#[must_use]
pub const fn rotated(rotation: Rotation) -> Self {
Self {
rotation,
mirror: false,
}
}
#[must_use]
pub const fn from_exif(value: u16) -> Option<Self> {
let (rotation, mirror) = match value {
1 => (Rotation::Deg0, false),
2 => (Rotation::Deg0, true),
3 => (Rotation::Deg180, false),
4 => (Rotation::Deg180, true),
5 => (Rotation::Deg270, true),
6 => (Rotation::Deg90, false),
7 => (Rotation::Deg90, true),
8 => (Rotation::Deg270, false),
_ => return None,
};
Some(Self { rotation, mirror })
}
#[must_use]
pub const fn is_upright(self) -> bool {
matches!(self.rotation, Rotation::Deg0) && !self.mirror
}
#[must_use]
pub const fn displayed_size(self, width: u32, height: u32) -> (u32, u32) {
if self.rotation.swaps_axes() {
(height, width)
} else {
(width, height)
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct SampleAspectRatio {
pub num: u32,
pub den: u32,
}
impl Default for SampleAspectRatio {
fn default() -> Self {
Self { num: 1, den: 1 }
}
}
impl SampleAspectRatio {
#[must_use]
pub fn new(num: u64, den: u64) -> Option<Self> {
if num == 0 || den == 0 {
return None;
}
let divisor = greatest_common_divisor(num, den);
Some(Self {
num: u32::try_from(num / divisor).ok()?,
den: u32::try_from(den / divisor).ok()?,
})
}
#[must_use]
pub fn of_display(visible: (u32, u32), displayed: (u32, u32)) -> Option<Self> {
let [
visible_width,
visible_height,
displayed_width,
displayed_height,
] = [visible.0, visible.1, displayed.0, displayed.1].map(u128::from);
if visible_width == 0
|| visible_height == 0
|| displayed_width == 0
|| displayed_height == 0
{
return None;
}
let slack = |shown: u128, stored: u128| u128::from(shown != stored);
let width_slack = slack(displayed_width, visible_width);
let height_slack = slack(displayed_height, visible_height);
let low = (
(2 * displayed_width - width_slack) * visible_height,
(2 * displayed_height + height_slack) * visible_width,
);
let high = (
(2 * displayed_width + width_slack) * visible_height,
(2 * displayed_height - height_slack) * visible_width,
);
let (num, den) = simplest_fraction_between(low, high)?;
Self::new(u64::try_from(num).ok()?, u64::try_from(den).ok()?)
}
#[must_use]
pub fn square_pixel_size(self, width: u32, height: u32) -> (u32, u32) {
let stretch = |side: u32, by: u32, of: u32| {
let scaled =
(u64::from(side) * u64::from(by) * 2 + u64::from(of)) / (2 * u64::from(of));
u32::try_from(scaled).unwrap_or(u32::MAX)
};
if self.num == 0 || self.den == 0 {
return (width, height);
}
match self.num.cmp(&self.den) {
std::cmp::Ordering::Greater => (stretch(width, self.num, self.den), height),
std::cmp::Ordering::Less => (width, stretch(height, self.den, self.num)),
std::cmp::Ordering::Equal => (width, height),
}
}
}
fn simplest_fraction_between(low: (u128, u128), high: (u128, u128)) -> Option<(u128, u128)> {
let ((low_num, low_den), (high_num, high_den)) = (low, high);
if low_den == 0 || high_den == 0 || low_num * high_den > high_num * low_den {
return None;
}
let whole = low_num / low_den;
if whole * low_den == low_num {
return Some((whole, 1));
}
if (whole + 1) * high_den <= high_num {
return Some((whole + 1, 1));
}
let (num, den) = simplest_fraction_between(
(high_den, high_num - whole * high_den),
(low_den, low_num - whole * low_den),
)?;
Some((whole * num + den, num))
}
fn greatest_common_divisor(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
(a, b) = (b, a % b);
}
a
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum PixelFormat {
Yuv(YuvFormat),
Rgba8,
LinearRgbaF16,
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub struct YuvFormat {
pub chroma_shift_x: u8,
pub chroma_shift_y: u8,
pub bit_depth: u8,
pub interleaved_chroma: bool,
pub msb_aligned: bool,
pub alpha: bool,
}
impl YuvFormat {
pub const I420: Self = Self::planar(1, 1, 8);
pub const NV12: Self = Self {
interleaved_chroma: true,
..Self::I420
};
pub const P010: Self = Self {
bit_depth: 10,
msb_aligned: true,
..Self::NV12
};
#[must_use]
pub const fn planar(chroma_shift_x: u8, chroma_shift_y: u8, bit_depth: u8) -> Self {
Self {
chroma_shift_x,
chroma_shift_y,
bit_depth,
interleaved_chroma: false,
msb_aligned: false,
alpha: false,
}
}
#[must_use]
pub const fn with_alpha(self) -> Self {
Self {
alpha: true,
..self
}
}
#[must_use]
pub const fn is_supported(self) -> bool {
self.bit_depth >= 8
&& self.bit_depth <= 16
&& self.chroma_shift_x <= 2
&& self.chroma_shift_y <= 2
&& (!self.msb_aligned || self.bit_depth > 8)
}
#[must_use]
pub const fn bytes_per_sample(self) -> u32 {
if self.bit_depth > 8 { 2 } else { 1 }
}
#[must_use]
pub const fn plane_count(self) -> usize {
self.color_plane_count() + if self.alpha { 1 } else { 0 }
}
#[must_use]
pub const fn color_plane_count(self) -> usize {
if self.interleaved_chroma { 2 } else { 3 }
}
#[must_use]
pub const fn chroma_size(self, width: u32, height: u32) -> (u32, u32) {
(
width.div_ceil(1 << self.chroma_shift_x),
height.div_ceil(1 << self.chroma_shift_y),
)
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum ChromaLocation {
#[default]
Unspecified,
Left,
Center,
TopLeft,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Plane {
pub bytes: bytes::Bytes,
pub stride: u32,
pub width: u32,
pub height: u32,
}
pub const MAX_FRAME_DIMENSION: u32 = 16_384;
pub const MAX_FRAME_PIXELS: u64 = 67_108_864;
#[must_use]
pub const fn frame_size_supported(width: u32, height: u32) -> bool {
width <= MAX_FRAME_DIMENSION
&& height <= MAX_FRAME_DIMENSION
&& (width as u64) * (height as u64) <= MAX_FRAME_PIXELS
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodedVideoFrame {
pub pts: MediaTime,
pub dts: Option<MediaTime>,
pub duration: Option<MediaTime>,
pub visible_rect: Rect,
pub sample_aspect_ratio: SampleAspectRatio,
pub orientation: Orientation,
pub pixel_format: PixelFormat,
pub chroma_location: ChromaLocation,
pub color: Option<ColorMetadata>,
pub planes: Vec<Plane>,
}
pub const MAX_AUDIO_CHANNELS: usize = 24;
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum Speaker {
FrontLeft,
FrontRight,
FrontCenter,
LowFrequency,
BackLeft,
BackRight,
FrontLeftOfCenter,
FrontRightOfCenter,
BackCenter,
SideLeft,
SideRight,
TopCenter,
TopFrontLeft,
TopFrontCenter,
TopFrontRight,
TopBackLeft,
TopBackCenter,
TopBackRight,
DownmixLeft,
DownmixRight,
WideLeft,
WideRight,
SurroundDirectLeft,
SurroundDirectRight,
LowFrequency2,
TopSideLeft,
TopSideRight,
BottomFrontCenter,
BottomFrontLeft,
BottomFrontRight,
SideSurroundLeft,
SideSurroundRight,
TopSurroundLeft,
TopSurroundRight,
BinauralLeft,
BinauralRight,
Unnamed,
}
impl Speaker {
const TABLE: [(Self, &'static str, u32); 37] = [
(Self::FrontLeft, "FL", 0),
(Self::FrontRight, "FR", 1),
(Self::FrontCenter, "FC", 2),
(Self::LowFrequency, "LFE", 3),
(Self::BackLeft, "BL", 4),
(Self::BackRight, "BR", 5),
(Self::FrontLeftOfCenter, "FLC", 6),
(Self::FrontRightOfCenter, "FRC", 7),
(Self::BackCenter, "BC", 8),
(Self::SideLeft, "SL", 9),
(Self::SideRight, "SR", 10),
(Self::TopCenter, "TC", 11),
(Self::TopFrontLeft, "TFL", 12),
(Self::TopFrontCenter, "TFC", 13),
(Self::TopFrontRight, "TFR", 14),
(Self::TopBackLeft, "TBL", 15),
(Self::TopBackCenter, "TBC", 16),
(Self::TopBackRight, "TBR", 17),
(Self::DownmixLeft, "DL", 29),
(Self::DownmixRight, "DR", 30),
(Self::WideLeft, "WL", 31),
(Self::WideRight, "WR", 32),
(Self::SurroundDirectLeft, "SDL", 33),
(Self::SurroundDirectRight, "SDR", 34),
(Self::LowFrequency2, "LFE2", 35),
(Self::TopSideLeft, "TSL", 36),
(Self::TopSideRight, "TSR", 37),
(Self::BottomFrontCenter, "BFC", 38),
(Self::BottomFrontLeft, "BFL", 39),
(Self::BottomFrontRight, "BFR", 40),
(Self::SideSurroundLeft, "SSL", 41),
(Self::SideSurroundRight, "SSR", 42),
(Self::TopSurroundLeft, "TTL", 43),
(Self::TopSurroundRight, "TTR", 44),
(Self::BinauralLeft, "BIL", 61),
(Self::BinauralRight, "BIR", 62),
(Self::Unnamed, "UNK", 0x300),
];
#[must_use]
pub fn name(self) -> &'static str {
Self::TABLE
.iter()
.find(|(speaker, _, _)| *speaker == self)
.map_or("UNK", |(_, name, _)| name)
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::TABLE
.iter()
.find(|(_, known, _)| *known == name)
.map(|(speaker, _, _)| *speaker)
}
#[must_use]
pub fn ffmpeg_channel(self) -> u32 {
Self::TABLE
.iter()
.find(|(speaker, _, _)| *speaker == self)
.map_or(0x300, |(_, _, channel)| *channel)
}
#[must_use]
pub fn from_ffmpeg_channel(channel: i64) -> Self {
Self::TABLE
.iter()
.find(|(_, _, number)| i64::from(*number) == channel)
.map_or(Self::Unnamed, |(speaker, _, _)| *speaker)
}
}
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
pub struct SpeakerLayout {
speakers: [Speaker; MAX_AUDIO_CHANNELS],
count: u8,
}
impl std::fmt::Debug for SpeakerLayout {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_list()
.entries(self.speakers().iter().map(|speaker| speaker.name()))
.finish()
}
}
impl SpeakerLayout {
pub const MONO: Self = Self::fixed(&[Speaker::FrontCenter]);
pub const STEREO: Self = Self::fixed(&[Speaker::FrontLeft, Speaker::FrontRight]);
const fn fixed(speakers: &[Speaker]) -> Self {
let mut layout = [Speaker::Unnamed; MAX_AUDIO_CHANNELS];
let mut index = 0;
while index < speakers.len() {
layout[index] = speakers[index];
index += 1;
}
#[allow(clippy::cast_possible_truncation)]
Self {
speakers: layout,
count: speakers.len() as u8,
}
}
#[must_use]
pub fn new(speakers: &[Speaker]) -> Option<Self> {
if speakers.is_empty() || speakers.len() > MAX_AUDIO_CHANNELS {
return None;
}
let mut layout = Self {
speakers: [Speaker::Unnamed; MAX_AUDIO_CHANNELS],
count: u8::try_from(speakers.len()).ok()?,
};
layout.speakers[..speakers.len()].copy_from_slice(speakers);
Some(layout)
}
#[must_use]
pub fn default_for(channels: usize) -> Option<Self> {
const SEVEN_ONE: u64 = 0x63f;
let mask: u64 = match channels {
1 => 0x4,
2 => 0x3,
3 => 0xb,
4 => 0x107,
5 => 0x37,
6 => 0x3f,
7 => 0x70f,
10 => 0x2_d60f,
12 => 0x2_d63f,
14 => 0x2_d6ff,
16 => 0x30_0002_d6ff,
24 => 0x1f8_0003_ffff,
8..=MAX_AUDIO_CHANNELS => SEVEN_ONE,
_ => return None,
};
let mut layout = Self::of_mask(mask)?;
layout.count = u8::try_from(channels).ok()?;
Some(layout)
}
fn of_mask(mask: u64) -> Option<Self> {
let speakers = (0..64_i64)
.filter(|bit| mask & (1 << bit) != 0)
.map(Speaker::from_ffmpeg_channel)
.collect::<Vec<_>>();
Self::new(&speakers)
}
#[must_use]
pub fn declared_or_default(speakers: &[Speaker]) -> Option<(Self, bool)> {
if speakers.iter().all(|speaker| *speaker == Speaker::Unnamed) {
return Self::default_for(speakers.len()).map(|layout| (layout, true));
}
Self::new(speakers).map(|layout| (layout, false))
}
#[must_use]
pub fn speakers(&self) -> &[Speaker] {
&self.speakers[..usize::from(self.count)]
}
#[must_use]
pub fn channels(&self) -> usize {
usize::from(self.count)
}
#[must_use]
pub fn describe(&self) -> String {
self.speakers()
.iter()
.map(|speaker| speaker.name())
.collect::<Vec<_>>()
.join("+")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AudioBlock {
pub start_sample: i64,
pub sample_rate: u32,
pub speakers: SpeakerLayout,
pub channels: Vec<Vec<f32>>,
}
impl AudioBlock {
#[must_use]
pub fn frames(&self) -> usize {
self.channels.first().map_or(0, Vec::len)
}
#[must_use]
pub fn end_sample(&self) -> i64 {
self.start_sample
.saturating_add(i64::try_from(self.frames()).unwrap_or(i64::MAX))
}
#[must_use]
pub fn start_time(&self) -> MediaTime {
MediaTime::from_ticks(sample_ticks_floor(self.start_sample, self.sample_rate))
}
}
#[must_use]
pub fn sample_ticks_floor(sample: i64, sample_rate: u32) -> i64 {
let ticks = (i128::from(sample) * i128::from(alpha2_time::TICKS_PER_SECOND))
.div_euclid(i128::from(sample_rate.max(1)));
i64::try_from(ticks).unwrap_or(if ticks < 0 { i64::MIN } else { i64::MAX })
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RasterColorSpace {
Srgb,
LinearSrgb,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Premultiplication {
Premultiplied,
Straight,
Opaque,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HostTexture {
pub content_version: u64,
pub width: u32,
pub height: u32,
pub premultiplication: Premultiplication,
pub color_space: RasterColorSpace,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum TextAlign {
#[default]
Start,
Center,
End,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum EmojiPolicy {
#[default]
Monochrome,
Replace,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextTexture {
pub texture: HostTexture,
pub layout_x: f32,
pub layout_y: f32,
pub layout_width: f32,
pub layout_height: f32,
pub first_baseline: f32,
pub ascent: f32,
pub descent: f32,
pub line_count: u32,
}
impl TextTexture {
#[must_use]
pub fn is_valid(&self) -> bool {
self.texture.width > 0
&& self.texture.height > 0
&& self.line_count > 0
&& [
self.layout_x,
self.layout_y,
self.layout_width,
self.layout_height,
self.first_baseline,
self.ascent,
self.descent,
]
.into_iter()
.all(|value| value.is_finite() && value >= 0.0)
&& self.first_baseline <= self.layout_height
&& f64::from(self.layout_x) + f64::from(self.layout_width)
<= f64::from(self.texture.width)
&& f64::from(self.layout_y) + f64::from(self.layout_height)
<= f64::from(self.texture.height)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct HostTexturePixels {
pub texture: HostTexture,
pub rgba8: Vec<u8>,
}
impl HostTexturePixels {
#[must_use]
pub fn is_valid(&self) -> bool {
usize::try_from(self.texture.width)
.ok()
.and_then(|width| {
usize::try_from(self.texture.height)
.ok()
.and_then(|height| width.checked_mul(height))
})
.and_then(|pixels| pixels.checked_mul(4))
== Some(self.rgba8.len())
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StreamKind {
Video,
Audio,
}
#[derive(Clone, Debug, PartialEq)]
pub struct StreamInfo {
pub index: usize,
pub kind: StreamKind,
pub codec: String,
pub duration: Option<MediaTime>,
pub dimensions: Option<(u32, u32)>,
pub frame_rate: Option<FrameRate>,
pub sample_rate: Option<u32>,
pub channels: Option<u32>,
pub orientation: Orientation,
pub sample_aspect_ratio: SampleAspectRatio,
pub color: Option<ColorMetadata>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum DecodeRequestKind {
Prefetch,
Seek,
Evicted,
DeviceRecovery,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DecodeRequest {
pub source: SourceId,
pub time: MediaTime,
pub kind: DecodeRequestKind,
pub generation: u64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DecodeResult {
pub request: DecodeRequest,
pub event: DecodeEvent,
}
#[derive(Clone, Debug, PartialEq)]
pub enum DecodeEvent {
Video(DecodedVideoFrame),
Audio(AudioBlock),
Retry,
EndOfStream,
}
#[derive(Clone, Debug, Default)]
pub struct CancelToken(Arc<AtomicBool>);
impl CancelToken {
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
pub const GPU_TIMEOUT_MESSAGE: &str = "GPU work did not complete within the wait timeout";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum GpuFault {
DeviceLost,
DeviceMemory,
HostMemory,
Timeout,
}
impl GpuFault {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::DeviceLost => "device-lost",
Self::DeviceMemory => "device-memory",
Self::HostMemory => "host-memory",
Self::Timeout => "timeout",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DecodeError {
Cancelled,
OutOfRange,
Unsupported(String),
Adapter(String),
Gpu {
fault: GpuFault,
message: String,
},
}
impl DecodeError {
#[must_use]
pub const fn is_fatal(&self) -> bool {
matches!(
self,
Self::Unsupported(_) | Self::Adapter(_) | Self::Gpu { .. }
)
}
}
impl fmt::Display for DecodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cancelled => formatter.write_str("decode was cancelled"),
Self::OutOfRange => formatter.write_str("decode request is out of range"),
Self::Unsupported(message) => write!(formatter, "unsupported media: {message}"),
Self::Adapter(message) | Self::Gpu { message, .. } => {
write!(formatter, "decoder adapter failed: {message}")
}
}
}
}
impl Error for DecodeError {}
pub trait Decoder {
fn streams(&self) -> &[StreamInfo];
fn flush(&mut self) -> Result<(), DecodeError>;
fn seek(
&mut self,
request: DecodeRequest,
cancel: &CancelToken,
) -> Result<DecodeResult, DecodeError>;
fn decode_next(&mut self, cancel: &CancelToken) -> Result<DecodeEvent, DecodeError>;
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AudioSpan<'a> {
pub block: &'a AudioBlock,
pub starts_source: bool,
pub ends_source: bool,
}
impl<'a> AudioSpan<'a> {
#[must_use]
pub const fn whole(block: &'a AudioBlock) -> Self {
Self {
block,
starts_source: true,
ends_source: true,
}
}
#[must_use]
pub fn covers(&self, start: i64, end: i64) -> bool {
(self.starts_source || self.block.start_sample <= start)
&& (self.ends_source || self.block.end_sample() >= end)
}
}
pub trait AudioBlockProvider {
fn block(&self, source: &SourceId, start_sample: i64, frames: usize) -> Option<AudioSpan<'_>>;
}
pub trait AudioBlockSink {
fn write_audio(&mut self, block: &AudioBlock, cancel: &CancelToken) -> Result<(), EncodeError>;
fn finish_audio(&mut self, cancel: &CancelToken) -> Result<(), EncodeError>;
fn abort_audio(&mut self) -> Result<(), EncodeError>;
}
#[derive(Clone, Debug, PartialEq)]
pub enum EncodeEvent {
Video(DecodedVideoFrame),
Audio(AudioBlock),
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum EncodeError {
Cancelled,
InvalidTimestamp,
Adapter(String),
Output(String),
OutputInUse(String),
Gpu {
fault: GpuFault,
message: String,
},
}
impl EncodeError {
#[must_use]
pub fn gpu_timeout(what: impl fmt::Display) -> Self {
Self::Gpu {
fault: GpuFault::Timeout,
message: format!("{what}: {GPU_TIMEOUT_MESSAGE}"),
}
}
}
impl fmt::Display for EncodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cancelled => formatter.write_str("encode was cancelled"),
Self::InvalidTimestamp => formatter.write_str("encode timestamp is invalid"),
Self::Adapter(message) | Self::Gpu { message, .. } => {
write!(formatter, "encoder adapter failed: {message}")
}
Self::Output(message) | Self::OutputInUse(message) => {
write!(formatter, "cannot create the output: {message}")
}
}
}
}
impl Error for EncodeError {}
pub trait EncodeSink {
fn write(&mut self, event: EncodeEvent, cancel: &CancelToken) -> Result<(), EncodeError>;
fn finish(&mut self, cancel: &CancelToken) -> Result<(), EncodeError>;
fn abort(&mut self) -> Result<(), EncodeError>;
}
#[cfg(test)]
mod tests {
use super::SampleAspectRatio;
#[test]
fn a_pixel_aspect_ratio_is_kept_in_lowest_terms_and_shows_as_a_browser_shows_it() {
let ratio = |num, den| SampleAspectRatio { num, den };
assert_eq!(SampleAspectRatio::new(8, 6), Some(ratio(4, 3)));
assert_eq!(SampleAspectRatio::new(0, 1), None);
assert_eq!(SampleAspectRatio::new(1, 0), None);
assert_eq!(SampleAspectRatio::new(u64::from(u32::MAX) + 1, 1), None);
let shown = |visible, displayed| SampleAspectRatio::of_display(visible, displayed);
assert_eq!(shown((128, 96), (171, 96)), Some(ratio(4, 3)));
assert_eq!(shown((96, 128), (96, 171)), Some(ratio(3, 4)));
assert_eq!(shown((720, 576), (1024, 576)), Some(ratio(64, 45)));
assert_eq!(shown((720, 480), (655, 480)), Some(ratio(10, 11)));
assert_eq!(shown((720, 480), (720, 524)), Some(ratio(11, 12)));
assert_eq!(shown((1440, 1080), (1920, 1080)), Some(ratio(4, 3)));
assert_eq!(shown((128, 96), (128, 96)), Some(ratio(1, 1)));
for (visible, displayed) in [
((128, 96), (170, 96)),
((1000, 999), (1777, 999)),
((3, 2), (5, 2)),
] {
let found = shown(visible, displayed).expect("a ratio");
assert_eq!(
found.square_pixel_size(visible.0, visible.1),
displayed,
"{visible:?} {displayed:?}"
);
}
assert_eq!(shown((0, 96), (171, 96)), None);
assert_eq!(ratio(4, 3).square_pixel_size(128, 96), (171, 96));
assert_eq!(ratio(64, 45).square_pixel_size(720, 576), (1024, 576));
assert_eq!(ratio(3, 4).square_pixel_size(96, 128), (96, 171));
assert_eq!(ratio(1, 1).square_pixel_size(65, 49), (65, 49));
assert_eq!(ratio(3, 2).square_pixel_size(3, 7), (5, 7));
}
use super::*;
#[test]
fn a_frame_size_is_bounded_on_each_side_and_in_area() {
assert!(frame_size_supported(16_384, 4_096));
assert!(frame_size_supported(8_192, 8_192));
assert!(!frame_size_supported(8_193, 8_192));
assert!(!frame_size_supported(16_385, 1));
assert!(!frame_size_supported(1, 16_385));
assert!(!frame_size_supported(u32::MAX, u32::MAX));
}
#[test]
fn cancellation_is_shared_and_monotonic() {
let token = CancelToken::default();
let other = token.clone();
assert!(!other.is_cancelled());
token.cancel();
assert!(other.is_cancelled());
}
#[test]
fn a_layout_names_each_plane_by_ffmpegs_channel_names() {
let lcr = SpeakerLayout::new(&[
Speaker::FrontCenter,
Speaker::FrontLeft,
Speaker::FrontRight,
])
.expect("layout");
assert_eq!(lcr.channels(), 3);
assert_eq!(lcr.describe(), "FC+FL+FR");
assert_eq!(format!("{lcr:?}"), r#"["FC", "FL", "FR"]"#);
for (speaker, name, channel) in Speaker::TABLE {
assert_eq!(speaker.name(), name);
assert_eq!(Speaker::from_name(name), Some(speaker));
assert_eq!(speaker.ffmpeg_channel(), channel);
assert_eq!(Speaker::from_ffmpeg_channel(i64::from(channel)), speaker);
}
for channel in [-1, 18, 0x200, 0x400, 0x401, 0x7ff, 1_000] {
assert_eq!(Speaker::from_ffmpeg_channel(channel), Speaker::Unnamed);
}
assert_eq!(Speaker::from_name("front left"), None);
assert!(SpeakerLayout::new(&[]).is_none());
assert!(SpeakerLayout::new(&[Speaker::FrontLeft; MAX_AUDIO_CHANNELS + 1]).is_none());
}
#[test]
fn a_source_that_declares_no_speaker_has_ffmpegs_default_for_its_count() {
let names = |channels| {
SpeakerLayout::default_for(channels)
.expect("a default")
.describe()
};
assert_eq!(names(1), "FC");
assert_eq!(names(3), "FL+FR+LFE");
assert_eq!(names(4), "FL+FR+FC+BC");
assert_eq!(names(8), "FL+FR+FC+LFE+BL+BR+SL+SR");
assert_eq!(names(9), "FL+FR+FC+LFE+BL+BR+SL+SR+UNK");
assert_eq!(names(10), "FL+FR+FC+LFE+SL+SR+TFL+TFR+TBL+TBR");
assert_eq!(SpeakerLayout::default_for(24).expect("22.2").channels(), 24);
assert!(SpeakerLayout::default_for(0).is_none());
assert!(SpeakerLayout::default_for(MAX_AUDIO_CHANNELS + 1).is_none());
let unnamed = [Speaker::Unnamed; 2];
assert_eq!(
SpeakerLayout::declared_or_default(&unnamed),
Some((SpeakerLayout::default_for(2).expect("stereo"), true))
);
let partly = [Speaker::FrontCenter, Speaker::Unnamed];
assert_eq!(
SpeakerLayout::declared_or_default(&partly),
Some((SpeakerLayout::new(&partly).expect("layout"), false))
);
}
#[test]
fn a_span_covers_a_request_only_where_it_holds_samples_or_knows_silence() {
let block = AudioBlock {
start_sample: 100,
sample_rate: 48_000,
speakers: SpeakerLayout::default_for(1).expect("mono"),
channels: vec![vec![0.5; 50]],
};
let partial = AudioSpan {
block: &block,
starts_source: false,
ends_source: false,
};
assert!(partial.covers(100, 150));
assert!(partial.covers(120, 130));
assert!(!partial.covers(99, 150), "a sample before the block");
assert!(!partial.covers(100, 151), "a sample after the block");
let starts = AudioSpan {
starts_source: true,
..partial
};
assert!(starts.covers(0, 150));
assert!(!starts.covers(0, 151));
assert!(AudioSpan::whole(&block).covers(-5, 1_000));
}
#[test]
fn text_descriptor_and_generic_pixels_validate_without_compositor_types() {
let texture = HostTexture {
content_version: 2,
width: 4,
height: 3,
premultiplication: Premultiplication::Premultiplied,
color_space: RasterColorSpace::Srgb,
};
let text = TextTexture {
texture: texture.clone(),
layout_x: 0.0,
layout_y: 0.0,
layout_width: 4.0,
layout_height: 3.0,
first_baseline: 2.0,
ascent: 2.0,
descent: 1.0,
line_count: 1,
};
assert!(text.is_valid());
assert!(
HostTexturePixels {
texture,
rgba8: vec![0; 4 * 3 * 4],
}
.is_valid()
);
}
}