cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

release/portal-packaging
goq / crates / sigil-host / src / moq_catalog.rs
moq_catalog.rs 6.6K
1use anyhow::{Context, Result, ensure};2use moq_net::BroadcastProducer;3use sigil_protocol::{4 GoqCatalogDocument, GoqCatalogDocumentV2, MAX_MOQ_CATALOG_BYTES,5 SignedMediaGenerationCertificate,6};78pub(crate) enum GoqCatalogProducer {9 V1(moq_json::Producer<GoqCatalogDocument>),10 V2(moq_json::Producer<GoqCatalogDocumentV2>),11}1213impl GoqCatalogProducer {14 pub(crate) fn finish(self) -> Result<()> {15 match self {16 Self::V1(mut producer) => producer.finish(),17 Self::V2(mut producer) => producer.finish(),18 }19 .context("finishing Goq catalog track")20 }21}2223pub(crate) fn publish_goq_catalog(broadcast: &mut BroadcastProducer) -> Result<GoqCatalogProducer> {24 let track = broadcast25 .create_track(hang::Catalog::default_track())26 .context("creating catalog.json track")?;27 let mut producer = moq_json::Producer::new(track, moq_json::Config::default());28 let document = GoqCatalogDocument::video_h264();29 document30 .validate()31 .context("validating Goq catalog document")?;32 let snapshot = serde_json::to_vec(&document).context("serializing Goq catalog snapshot")?;33 ensure!(34 snapshot.len() <= MAX_MOQ_CATALOG_BYTES,35 "Goq catalog snapshot exceeds {MAX_MOQ_CATALOG_BYTES} bytes"36 );37 producer38 .update(&document)39 .context("publishing immutable Goq catalog snapshot")?;40 Ok(GoqCatalogProducer::V1(producer))41}4243pub(crate) fn publish_goq_catalog_v2(44 broadcast: &mut BroadcastProducer,45 generation_id: u64,46 certificate: &SignedMediaGenerationCertificate,47 audio_enabled: bool,48) -> Result<GoqCatalogProducer> {49 let track = broadcast50 .create_track(hang::Catalog::default_track())51 .context("creating authenticated catalog.json track")?;52 let mut producer = moq_json::Producer::new(track, moq_json::Config::default());53 let document = if audio_enabled {54 GoqCatalogDocumentV2::video_h264_opus(generation_id, certificate)55 } else {56 GoqCatalogDocumentV2::video_h264(generation_id, certificate)57 }58 .context("constructing authenticated Goq catalog")?;59 document60 .validate()61 .context("validating authenticated Goq catalog document")?;62 let snapshot =63 serde_json::to_vec(&document).context("serializing authenticated Goq catalog snapshot")?;64 ensure!(65 snapshot.len() <= MAX_MOQ_CATALOG_BYTES,66 "authenticated Goq catalog snapshot exceeds {MAX_MOQ_CATALOG_BYTES} bytes"67 );68 producer69 .update(&document)70 .context("publishing immutable authenticated Goq catalog snapshot")?;71 Ok(GoqCatalogProducer::V2(producer))72}7374#[cfg(test)]75mod tests {76 use super::*;77 use moq_net::{Broadcast, Track};78 use std::time::Duration;7980 fn certificate() -> SignedMediaGenerationCertificate {81 let host = iroh::SecretKey::from_bytes(&[7; 32]);82 let generation = sigil_protocol::MediaGenerationSigningKey::from_bytes(&[9; 32]);83 generation84 .certify(85 *host.public().as_bytes(),86 &host.to_bytes(),87 42,88 1_700_000_000,89 1_700_000_600,90 )91 .unwrap()92 }9394 #[tokio::test]95 async fn catalog_snapshot_is_late_subscribable_and_hang_compatible() {96 let mut broadcast = Broadcast::new().produce();97 let _video = broadcast98 .create_track(Track {99 name: sigil_protocol::MOQ_VIDEO_H264_TRACK.to_owned(),100 priority: sigil_protocol::MOQ_VIDEO_TRACK_PRIORITY,101 })102 .unwrap();103 let catalog = publish_goq_catalog(&mut broadcast).unwrap();104105 let base_track = broadcast106 .consume()107 .subscribe_track(&hang::Catalog::default_track())108 .unwrap();109 let mut base_consumer = moq_json::Consumer::<hang::Catalog>::new(base_track);110 let base = tokio::time::timeout(Duration::from_millis(100), base_consumer.next())111 .await112 .unwrap()113 .unwrap()114 .unwrap();115 assert_eq!(base, hang::Catalog::default());116117 let document_track = broadcast118 .consume()119 .subscribe_track(&hang::Catalog::default_track())120 .unwrap();121 let mut document_consumer = moq_json::Consumer::<GoqCatalogDocument>::new(document_track);122 let document = tokio::time::timeout(Duration::from_millis(100), document_consumer.next())123 .await124 .unwrap()125 .unwrap()126 .unwrap();127 document.validate().unwrap();128 assert_eq!(document, GoqCatalogDocument::video_h264());129 catalog.finish().unwrap();130 }131132 #[tokio::test]133 async fn authenticated_catalog_snapshot_carries_the_certified_generation() {134 let mut broadcast = Broadcast::new().produce();135 let _video = broadcast136 .create_track(Track {137 name: sigil_protocol::MOQ_VIDEO_H264_TRACK.to_owned(),138 priority: sigil_protocol::MOQ_VIDEO_TRACK_PRIORITY,139 })140 .unwrap();141 let certificate = certificate();142 let catalog = publish_goq_catalog_v2(&mut broadcast, 42, &certificate, false).unwrap();143 let document_track = broadcast144 .consume()145 .subscribe_track(&hang::Catalog::default_track())146 .unwrap();147 let mut consumer = moq_json::Consumer::<GoqCatalogDocumentV2>::new(document_track);148 let document = tokio::time::timeout(Duration::from_millis(100), consumer.next())149 .await150 .unwrap()151 .unwrap()152 .unwrap();153 assert_eq!(document.validate().unwrap(), certificate);154 catalog.finish().unwrap();155 }156157 #[tokio::test]158 async fn authenticated_catalog_can_advertise_video_and_audio_tracks() {159 let mut broadcast = Broadcast::new().produce();160 let _video = broadcast161 .create_track(Track::new(sigil_protocol::MOQ_VIDEO_H264_TRACK))162 .unwrap();163 let _audio = broadcast164 .create_track(Track::new(sigil_protocol::MOQ_AUDIO_OPUS_TRACK))165 .unwrap();166 let certificate = certificate();167 let catalog = publish_goq_catalog_v2(&mut broadcast, 42, &certificate, true).unwrap();168 let track = broadcast169 .consume()170 .subscribe_track(&hang::Catalog::default_track())171 .unwrap();172 let mut consumer = moq_json::Consumer::<GoqCatalogDocumentV2>::new(track);173 let document = consumer.next().await.unwrap().unwrap();174 assert!(document.goq.audio.is_some());175 catalog.finish().unwrap();176 }177}