use std::ops::{Deref, DerefMut};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
pub trait CatalogExt: Serialize + DeserializeOwned + Default + Clone + Send + 'static {}
impl CatalogExt for () {}
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
#[serde(transparent)]
pub struct Extra(serde_json::Map<String, serde_json::Value>);
impl CatalogExt for Extra {}
impl Extra {
pub fn get(&self, name: &str) -> Option<&serde_json::Value> {
self.0.get(name)
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
self.0.iter()
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn set(&mut self, name: impl Into<String>, value: serde_json::Value) -> crate::Result<()> {
let name = name.into();
if matches!(name.as_str(), "video" | "audio") {
return Err(crate::Error::ReservedSection(name));
}
self.0.insert(name, value);
Ok(())
}
pub fn remove(&mut self, name: &str) -> Option<serde_json::Value> {
self.0.remove(name)
}
}
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq)]
#[serde(bound(serialize = "E: Serialize", deserialize = "E: DeserializeOwned"))]
pub struct Catalog<E: CatalogExt = Extra> {
#[serde(default)]
pub video: hang::catalog::Video,
#[serde(default)]
pub audio: hang::catalog::Audio,
#[serde(flatten)]
pub ext: E,
}
impl<E: CatalogExt> Catalog<E> {
pub(crate) fn media(&self) -> hang::Catalog {
hang::Catalog {
video: self.video.clone(),
audio: self.audio.clone(),
}
}
}
impl Catalog<Extra> {
pub fn section(&self, name: &str) -> Option<&serde_json::Value> {
self.ext.get(name)
}
pub fn sections(&self) -> impl Iterator<Item = (&String, &serde_json::Value)> {
self.ext.iter()
}
}
impl<E: CatalogExt> Deref for Catalog<E> {
type Target = E;
fn deref(&self) -> &E {
&self.ext
}
}
impl<E: CatalogExt> DerefMut for Catalog<E> {
fn deref_mut(&mut self) -> &mut E {
&mut self.ext
}
}
#[cfg(test)]
mod test {
use std::task::Poll;
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
struct Scte35Ext {
#[serde(skip_serializing_if = "Option::is_none")]
scte35: Option<Scte35>,
}
#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Debug)]
struct Scte35 {
splice_id: u32,
}
impl CatalogExt for Scte35Ext {}
#[test]
fn extension_roundtrip() {
let mut broadcast = moq_net::Broadcast::new().produce();
let mut producer =
crate::catalog::Producer::with_catalog(&mut broadcast, Catalog::<Scte35Ext>::default()).unwrap();
let mut consumer = producer.consume().unwrap();
producer.lock().audio.renditions.insert(
"audio0".to_string(),
hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2),
);
producer.lock().scte35 = Some(Scte35 { splice_id: 42 });
let waiter = kio::Waiter::noop();
let mut latest = None;
while let Poll::Ready(Ok(Some(catalog))) = consumer.poll_next(&waiter) {
latest = Some(catalog);
}
let catalog = latest.expect("catalog published");
assert!(catalog.audio.renditions.contains_key("audio0"));
assert_eq!(catalog.scte35, Some(Scte35 { splice_id: 42 }));
}
#[test]
fn untyped_extra_roundtrip() {
let mut broadcast = moq_net::Broadcast::new().produce();
let mut producer = crate::catalog::Producer::new(&mut broadcast).unwrap();
let mut consumer = producer.consume().unwrap();
producer.lock().audio.renditions.insert(
"audio0".to_string(),
hang::catalog::AudioConfig::new(hang::catalog::AudioCodec::Opus, 48_000, 2),
);
producer
.set_section("transcript", serde_json::json!({ "track": "transcript.json" }))
.unwrap();
assert!(matches!(
producer.set_section("video", serde_json::json!({})),
Err(crate::Error::ReservedSection(_))
));
let waiter = kio::Waiter::noop();
let mut latest = None;
while let Poll::Ready(Ok(Some(catalog))) = consumer.poll_next(&waiter) {
latest = Some(catalog);
}
let catalog = latest.expect("catalog published");
assert!(catalog.audio.renditions.contains_key("audio0"));
assert_eq!(
catalog.section("transcript"),
Some(&serde_json::json!({ "track": "transcript.json" }))
);
assert_eq!(catalog.sections().count(), 1);
}
#[test]
fn untyped_extra_serializes_flat_and_omits_when_empty() {
let empty = Catalog::<Extra>::default();
assert_eq!(
serde_json::to_value(&empty).unwrap(),
serde_json::to_value(Catalog::<()>::default()).unwrap()
);
let mut catalog = Catalog::<Extra>::default();
catalog.ext.set("scte35", serde_json::json!({ "spliceId": 7 })).unwrap();
let json = serde_json::to_value(&catalog).unwrap();
assert_eq!(json["scte35"], serde_json::json!({ "spliceId": 7 }));
}
}