mod av1;
mod codec;
mod h264;
mod h265;
mod vp9;
pub use av1::*;
pub use codec::*;
pub use h264::*;
pub use h265::*;
pub use vp9::*;
use std::collections::{BTreeMap, btree_map};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, DurationMilliSeconds, hex::Hex};
use crate::catalog::Container;
#[serde_with::serde_as]
#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Video {
pub renditions: BTreeMap<String, VideoConfig>,
#[serde(default)]
pub display: Option<Display>,
#[serde(default)]
pub rotation: Option<f64>,
#[serde(default)]
pub flip: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Default)]
#[non_exhaustive]
pub struct VideoProperties {
pub display: Option<Display>,
pub rotation: Option<f64>,
pub flip: Option<bool>,
}
impl VideoProperties {
fn normalized(mut self) -> crate::Result<Self> {
self.rotation = self.rotation.map(normalize_video_rotation).transpose()?;
Ok(self)
}
}
const FULL_TURN_DEGREES: f64 = 360.0;
const QUARTER_TURN_DEGREES: f64 = 90.0;
const QUARTER_TURNS_PER_FULL_TURN: u16 = 4;
fn normalize_video_rotation(rotation: f64) -> crate::Result<f64> {
if !rotation.is_finite() {
return Err(crate::Error::InvalidVideoRotation);
}
let normalized = rotation.rem_euclid(FULL_TURN_DEGREES);
let quarter_turns = (normalized / QUARTER_TURN_DEGREES).round() as u16 % QUARTER_TURNS_PER_FULL_TURN;
Ok(quarter_turns as f64 * QUARTER_TURN_DEGREES)
}
impl Video {
pub fn insert(&mut self, name: &str, config: VideoConfig) -> crate::Result<()> {
let btree_map::Entry::Vacant(entry) = self.renditions.entry(name.to_string()) else {
return Err(crate::Error::Duplicate(name.to_string()));
};
entry.insert(config);
Ok(())
}
pub fn remove(&mut self, name: &str) -> Option<VideoConfig> {
self.renditions.remove(name)
}
pub fn set_properties(&mut self, properties: VideoProperties) -> crate::Result<()> {
let properties = properties.normalized()?;
self.display = properties.display;
self.rotation = properties.rotation;
self.flip = properties.flip;
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Display {
pub width: u32,
pub height: u32,
}
#[serde_with::serde_as]
#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct VideoConfig {
#[serde(default)]
pub broadcast: Option<moq_net::PathRelativeOwned>,
#[serde_as(as = "DisplayFromStr")]
pub codec: VideoCodec,
#[serde(default)]
#[serde_as(as = "Option<Hex>")]
pub description: Option<Bytes>,
pub coded_width: Option<u32>,
pub coded_height: Option<u32>,
#[serde(alias = "displayRatioWidth")]
pub display_aspect_width: Option<u32>,
#[serde(alias = "displayRatioHeight")]
pub display_aspect_height: Option<u32>,
#[serde(default)]
pub bitrate: Option<u64>,
#[serde(default)]
pub framerate: Option<f64>,
#[serde(default)]
pub optimize_for_latency: Option<bool>,
#[serde(default)]
pub container: Container,
#[serde_as(as = "Option<DurationMilliSeconds<u64>>")]
#[serde(default)]
pub jitter: Option<std::time::Duration>,
#[serde(default)]
pub timeline: Option<crate::catalog::Timeline>,
}
impl VideoConfig {
pub fn new(codec: impl Into<VideoCodec>) -> Self {
Self {
broadcast: None,
codec: codec.into(),
description: None,
coded_width: None,
coded_height: None,
display_aspect_width: None,
display_aspect_height: None,
bitrate: None,
framerate: None,
optimize_for_latency: None,
container: Container::default(),
jitter: None,
timeline: None,
}
}
}
#[cfg(test)]
mod test {
use crate::catalog::{Container, H264};
use super::*;
#[test]
fn display_aspect_uses_canonical_json_names() {
let mut config = VideoConfig::new(H264 {
profile: 0x64,
constraints: 0,
level: 0x1f,
inline: false,
});
config.display_aspect_width = Some(4);
config.display_aspect_height = Some(3);
config.container = Container::Legacy;
let encoded = serde_json::to_value(config).expect("failed to encode");
assert_eq!(encoded["displayAspectWidth"], 4);
assert_eq!(encoded["displayAspectHeight"], 3);
assert!(encoded.get("displayRatioWidth").is_none());
assert!(encoded.get("displayRatioHeight").is_none());
}
#[test]
fn decodes_legacy_display_ratio_keys() {
let json = serde_json::json!({
"codec": "avc1.640028",
"displayRatioWidth": 16,
"displayRatioHeight": 9,
});
let config: VideoConfig = serde_json::from_value(json).expect("failed to decode legacy keys");
assert_eq!(config.display_aspect_width, Some(16));
assert_eq!(config.display_aspect_height, Some(9));
}
#[test]
fn normalizes_video_rotation_to_quarter_turns() {
for (rotation, expected) in [
(0.0, 0.0),
(44.9, 0.0),
(45.0, 90.0),
(134.9, 90.0),
(135.0, 180.0),
(225.0, 270.0),
(315.0, 0.0),
(360.0, 0.0),
(-45.0, 0.0),
] {
assert_eq!(normalize_video_rotation(rotation).unwrap(), expected);
}
}
#[test]
fn rejects_non_finite_video_rotation() {
for rotation in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(matches!(
normalize_video_rotation(rotation),
Err(crate::Error::InvalidVideoRotation)
));
}
}
#[test]
fn invalid_rotation_does_not_replace_video_properties() {
let mut video = Video {
display: Some(Display {
width: 640,
height: 480,
}),
rotation: Some(90.0),
flip: Some(true),
..Default::default()
};
let expected = video.clone();
assert!(matches!(
video.set_properties(VideoProperties {
display: None,
rotation: Some(f64::NAN),
flip: None,
}),
Err(crate::Error::InvalidVideoRotation)
));
assert_eq!(video, expected);
}
#[test]
fn video_properties_replace_shared_fields_without_touching_renditions() {
let mut video = Video::default();
video
.insert(
"video",
VideoConfig::new(H264 {
profile: 0x64,
constraints: 0,
level: 0x1f,
inline: false,
}),
)
.unwrap();
video.display = Some(Display {
width: 640,
height: 480,
});
video.rotation = Some(90.0);
video.flip = Some(true);
let renditions = video.renditions.clone();
video
.set_properties(VideoProperties {
display: Some(Display {
width: 1920,
height: 1080,
}),
rotation: Some(315.0),
flip: None,
})
.unwrap();
assert_eq!(video.renditions, renditions);
assert_eq!(
video.display,
Some(Display {
width: 1920,
height: 1080
})
);
assert_eq!(video.rotation, Some(0.0));
assert_eq!(video.flip, None);
}
}