cat / goq
clone

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

refactor: single-source the MoQ catalog document in sigil-protocol

Dami committed c4ae230
GoqCatalogDocument (the immutable catalog.json envelope both peers must
agree on byte-for-byte) was defined three times - host producer, Portal
subscriber, and the probe's subscriber module - with independently
phrased validation and no test tying the copies together. A serde or
validation change to one copy compiled cleanly and failed only at
runtime when the peer rejected the catalog.

The document now lives in sigil-protocol (which already owned its only
dependency, MoqCatalogExtensionV1) with a round-trip golden test; all
three call sites consume the shared type. sigil-protocol gains the
workspace hang dependency the envelope requires.
commit
c4ae230c5481b60e496d4cfcb39c65f71e538bb0
parent
5f090adffd773ab596726969758c545128078cf4
author
Dami <dami.etoile@gmail.com>
7 files changed, +63 -101
MCargo.lock1+
Mcrates/sigil-host/src/bin/moq_catalog.rs39+++-----------------------
Mcrates/sigil-host/src/moq_catalog.rs34+++--------------------
Mcrates/sigil-protocol/Cargo.toml1+
Mcrates/sigil-protocol/src/lib.rs6++--
Mcrates/sigil-protocol/src/moq_catalog.rs44++++++++++++++++++++++++++++++
Msrc-tauri/src/media/moq_catalog.rs39+++-----------------------
M Cargo.lock +1 -0
M crates/sigil-host/src/bin/moq_catalog.rs +5 -34
@@ -2,36 +2,7 @@ use std::time::Duration;
22 33 use anyhow::{Context, Result};44 use moq_net::{BroadcastConsumer, Error as MoqError, Track, TrackConsumer};5-use serde::{Deserialize, Serialize};6-use sigil_protocol::{MAX_MOQ_CATALOG_BYTES, MoqCatalogExtensionV1};7-8-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]9-#[serde(rename_all = "camelCase")]10-struct GoqCatalogDocument {11- #[serde(flatten)]12- media: hang::Catalog,13- goq: MoqCatalogExtensionV1,14-}15-16-impl GoqCatalogDocument {17- #[cfg(test)]18- fn video_h264() -> Self {19- Self {20- media: hang::Catalog::default(),21- goq: MoqCatalogExtensionV1::video_h264(),22- }23- }24-25- fn validate(&self) -> Result<()> {26- anyhow::ensure!(27- self.media == hang::Catalog::default(),28- "Goq catalog must not advertise a standard Hang rendition for enveloped media"29- );30- self.goq31- .validate()32- .context("validating Goq catalog extension")33- }34-}5+use sigil_protocol::{GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES};356 367 #[derive(Clone, Copy, Debug, PartialEq, Eq)]378 pub(crate) enum MoqCatalogMode {
@@ -118,7 +89,9 @@ pub(crate) async fn subscribe_goq_video_track(
11889 .context("reading Goq catalog frame")?;11990 let document: GoqCatalogDocument =12091 serde_json::from_slice(&snapshot).context("decoding Goq catalog snapshot")?;121- document.validate()?;92+ document93+ .validate()94+ .context("validating Goq catalog document")?;12295 let track = broadcast12396 .subscribe_track(&Track::new(document.goq.video.track.name))12497 .context("subscribing to catalog-selected Goq video track")?;
@@ -232,9 +205,7 @@ mod tests {
232205 subscribe_goq_video_track(&invalid_broadcast.consume(), Duration::from_millis(100))233206 .await;234207 assert!(result.err().is_some_and(|error| {235- error236- .to_string()237- .contains("validating Goq catalog extension")208+ format!("{error:#}").contains("validating Goq catalog document")238209 }));239210 240211 let mut stalled_broadcast = Broadcast::new().produce();
M crates/sigil-host/src/moq_catalog.rs +4 -30
@@ -1,34 +1,6 @@
11 use anyhow::{Context, Result, ensure};22 use moq_net::BroadcastProducer;3-use serde::{Deserialize, Serialize};4-use sigil_protocol::{MAX_MOQ_CATALOG_BYTES, MoqCatalogExtensionV1};5-6-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]7-#[serde(rename_all = "camelCase")]8-struct GoqCatalogDocument {9- #[serde(flatten)]10- media: hang::Catalog,11- goq: MoqCatalogExtensionV1,12-}13-14-impl GoqCatalogDocument {15- fn video_h264() -> Self {16- Self {17- media: hang::Catalog::default(),18- goq: MoqCatalogExtensionV1::video_h264(),19- }20- }21-22- fn validate(&self) -> Result<()> {23- ensure!(24- self.media == hang::Catalog::default(),25- "Goq catalog must not advertise a standard Hang rendition for enveloped media"26- );27- self.goq28- .validate()29- .context("validating Goq catalog extension")30- }31-}3+use sigil_protocol::{GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES};324 335 pub(crate) struct GoqCatalogProducer {346 producer: moq_json::Producer<GoqCatalogDocument>,
@@ -48,7 +20,9 @@ pub(crate) fn publish_goq_catalog(broadcast: &mut BroadcastProducer) -> Result<G
4820 .context("creating catalog.json track")?;4921 let mut producer = moq_json::Producer::new(track, moq_json::Config::default());5022 let document = GoqCatalogDocument::video_h264();51- document.validate()?;23+ document24+ .validate()25+ .context("validating Goq catalog document")?;5226 let snapshot = serde_json::to_vec(&document).context("serializing Goq catalog snapshot")?;5327 ensure!(5428 snapshot.len() <= MAX_MOQ_CATALOG_BYTES,
M crates/sigil-protocol/Cargo.toml +1 -0
M crates/sigil-protocol/src/lib.rs +3 -3
M crates/sigil-protocol/src/moq_catalog.rs +44 -0
@@ -89,6 +89,37 @@ impl MoqCatalogExtensionV1 {
8989 }9090 }9191 92+/// The immutable catalog.json document both peers must agree on byte-for-byte:93+/// a default (empty) Hang catalog envelope carrying the Goq extension. This is94+/// the single definition — the host producer, Portal subscriber, and probe all95+/// consume it from here so the wire shape cannot drift between peers.96+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]97+#[serde(rename_all = "camelCase")]98+pub struct GoqCatalogDocument {99+ #[serde(flatten)]100+ pub media: hang::Catalog,101+ pub goq: MoqCatalogExtensionV1,102+}103+104+impl GoqCatalogDocument {105+ pub fn video_h264() -> Self {106+ Self {107+ media: hang::Catalog::default(),108+ goq: MoqCatalogExtensionV1::video_h264(),109+ }110+ }111+112+ pub fn validate(&self) -> Result<()> {113+ if self.media != hang::Catalog::default() {114+ return Err(ProtocolError::InvalidMessage {115+ message_type: "Goq MoQ catalog document",116+ reason: "catalog must not advertise a standard Hang rendition for enveloped media",117+ });118+ }119+ self.goq.validate()120+ }121+}122+92123 #[cfg(test)]93124 mod tests {94125 use super::*;
@@ -103,6 +134,19 @@ mod tests {
103134 );104135 }105136 137+ #[test]138+ fn goq_catalog_document_has_a_stable_golden_envelope() {139+ let document = GoqCatalogDocument::video_h264();140+ document.validate().unwrap();141+ let json = serde_json::to_value(&document).unwrap();142+ assert_eq!(143+ json["goq"],144+ serde_json::to_value(MoqCatalogExtensionV1::video_h264()).unwrap()145+ );146+ let round_trip: GoqCatalogDocument = serde_json::from_value(json).unwrap();147+ assert_eq!(round_trip, document);148+ }149+106150 #[test]107151 fn goq_catalog_validation_rejects_every_mismatched_contract_field() {108152 let mut cases = Vec::new();
M src-tauri/src/media/moq_catalog.rs +5 -34
@@ -1,38 +1,7 @@
11 use std::time::Duration;22 33 use moq_net::{BroadcastConsumer, Error as MoqError, Track, TrackConsumer};4-use serde::{Deserialize, Serialize};5-use sigil_protocol::{MAX_MOQ_CATALOG_BYTES, MoqCatalogExtensionV1};6-7-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]8-#[serde(rename_all = "camelCase")]9-struct GoqCatalogDocument {10- #[serde(flatten)]11- media: hang::Catalog,12- goq: MoqCatalogExtensionV1,13-}14-15-impl GoqCatalogDocument {16- #[cfg(test)]17- fn video_h264() -> Self {18- Self {19- media: hang::Catalog::default(),20- goq: MoqCatalogExtensionV1::video_h264(),21- }22- }23-24- fn validate(&self) -> Result<(), String> {25- if self.media != hang::Catalog::default() {26- return Err(27- "Goq catalog falsely advertises a standard Hang rendition for enveloped media"28- .to_string(),29- );30- }31- self.goq32- .validate()33- .map_err(|error| format!("Invalid Goq catalog extension: {error}"))34- }35-}4+use sigil_protocol::{GoqCatalogDocument, MAX_MOQ_CATALOG_BYTES};365 376 #[derive(Clone, Copy, Debug, PartialEq, Eq)]387 pub(crate) enum MoqCatalogMode {
@@ -126,7 +95,9 @@ pub(crate) async fn subscribe_goq_video_track(
12695 .map_err(|error| format!("Failed to read Goq catalog frame: {error}"))?;12796 let document: GoqCatalogDocument = serde_json::from_slice(&snapshot)12897 .map_err(|error| format!("Failed to decode Goq catalog snapshot: {error}"))?;129- document.validate()?;98+ document99+ .validate()100+ .map_err(|error| format!("Invalid Goq catalog: {error}"))?;130101 let track = broadcast131102 .subscribe_track(&Track::new(document.goq.video.track.name))132103 .map_err(|error| {
@@ -244,7 +215,7 @@ mod tests {
244215 assert!(245216 result246217 .err()247- .is_some_and(|error| error.contains("Invalid Goq catalog extension"))218+ .is_some_and(|error| error.contains("Invalid Goq catalog"))248219 );249220 250221 let mut stalled_broadcast = Broadcast::new().produce();