use std::time::Duration;
use serde::de::Deserializer;
use serde::ser::Error as _;
use serde::{Deserialize, Serialize, Serializer};
use serde_with::{DeserializeAs, SerializeAs};
pub(crate) struct MillisCeil;
impl SerializeAs<Option<Duration>> for MillisCeil {
fn serialize_as<S: Serializer>(source: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error> {
let Some(duration) = source else {
return serializer.serialize_none();
};
if duration.is_zero() {
return Err(S::Error::custom(
"a duration of 0 has no meaning here; omit the field for a track flushed immediately",
));
}
let millis = duration.as_nanos().div_ceil(1_000_000);
u64::try_from(millis)
.map_err(|_| S::Error::custom("a duration too long to express in milliseconds"))?
.serialize(serializer)
}
}
impl<'de> DeserializeAs<'de, Option<Duration>> for MillisCeil {
fn deserialize_as<D: Deserializer<'de>>(deserializer: D) -> Result<Option<Duration>, D::Error> {
Ok(Option::<u64>::deserialize(deserializer)?
.filter(|millis| *millis != 0)
.map(Duration::from_millis))
}
}
#[cfg(test)]
mod test {
use crate::catalog::Catalog;
fn audio(jitter: Option<std::time::Duration>) -> Catalog {
let mut catalog = Catalog::default();
let mut config = crate::catalog::AudioConfig::new(crate::catalog::AudioCodec::Opus, 48_000, 2);
config.jitter = jitter;
catalog.audio.renditions.insert("audio".to_string(), config);
catalog
}
#[test]
fn text_rounds_up_and_normalizes_zero() {
let mut catalog = Catalog::<()>::default();
let mut text = crate::catalog::TextConfig::new(crate::catalog::TextFormat::Utf8);
text.jitter = Some(std::time::Duration::from_micros(500));
catalog.text.renditions.insert("caption".to_string(), text);
let json = catalog.to_json().unwrap();
assert!(json.contains(r#""jitter":1"#), "{json}");
let json = json.replace(r#""jitter":1"#, r#""jitter":0"#);
let parsed = Catalog::<()>::from_str(&json).unwrap();
assert_eq!(parsed.text.renditions["caption"].jitter, None);
}
#[test]
fn rounds_up() {
let json = audio(Some(std::time::Duration::from_micros(23_220))).to_json().unwrap();
assert!(json.contains(r#""jitter":24"#), "{json}");
let json = audio(Some(std::time::Duration::from_micros(500))).to_json().unwrap();
assert!(json.contains(r#""jitter":1"#), "{json}");
let json = audio(Some(std::time::Duration::from_millis(20))).to_json().unwrap();
assert!(json.contains(r#""jitter":20"#), "{json}");
}
#[test]
fn zero_is_absent() {
let err = audio(Some(std::time::Duration::ZERO))
.to_json()
.unwrap_err()
.to_string();
assert!(err.contains("omit the field"), "{err}");
let json =
r#"{"audio":{"renditions":{"audio":{"codec":"opus","sampleRate":48000,"numberOfChannels":2,"jitter":0}}}}"#;
let catalog = Catalog::<()>::from_str(json).unwrap();
assert_eq!(catalog.audio.renditions["audio"].jitter, None);
assert!(!catalog.to_json().unwrap().contains("jitter"));
}
#[test]
fn too_long_is_refused() {
let err = audio(Some(std::time::Duration::MAX)).to_json().unwrap_err().to_string();
assert!(err.contains("too long"), "{err}");
}
}