Skip to content

Commit 623a36a

Browse files
kixelatedclaude
andauthored
moq-net: RAII broadcast announcements via OriginPublish; remove broadcast abort/close (#1640)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 711e761 commit 623a36a

18 files changed

Lines changed: 355 additions & 225 deletions

File tree

rs/hang/examples/video.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// cargo run --example video
2+
use anyhow::Context;
23
use bytes::Bytes;
34

45
#[tokio::main]
@@ -94,7 +95,9 @@ async fn run_broadcast(origin: moq_net::OriginProducer) -> anyhow::Result<()> {
9495

9596
// NOTE: The path is empty because we're using the URL to scope the broadcast.
9697
// OPTIONAL: We publish after inserting the tracks just to avoid a nearly impossible race condition.
97-
origin.publish_broadcast("", broadcast.consume());
98+
let _publish = origin
99+
.publish_broadcast("", broadcast.consume())
100+
.context("failed to publish broadcast")?;
98101

99102
// Wrap in a Producer for keyframe-based group management.
100103
let mut producer = moq_mux::container::Producer::new(track, moq_mux::catalog::hang::Container::Legacy);

rs/libmoq/src/api.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ pub extern "C" fn moq_origin_create() -> i32 {
214214
///
215215
/// The broadcast will be announced to any origin consumers, such as over the network.
216216
///
217-
/// Returns a zero on success, or a negative code on failure.
217+
/// Returns a positive publish handle on success, or a negative code on failure. The broadcast
218+
/// stays announced until the handle is passed to [moq_origin_unpublish]; closing the broadcast
219+
/// itself does not unannounce it.
218220
///
219221
/// # Safety
220222
/// - The caller must ensure that path is a valid pointer to path_len bytes of data.
@@ -231,6 +233,18 @@ pub unsafe extern "C" fn moq_origin_publish(origin: u32, path: *const c_char, pa
231233
})
232234
}
233235

236+
/// Unannounce a broadcast previously published with [moq_origin_publish].
237+
///
238+
/// Takes the publish handle returned by [moq_origin_publish]. Returns zero on success, or a
239+
/// negative code on failure.
240+
#[unsafe(no_mangle)]
241+
pub extern "C" fn moq_origin_unpublish(publish: u32) -> i32 {
242+
ffi::enter(move || {
243+
let publish = ffi::parse_id(publish)?;
244+
State::lock().origin.unpublish(publish)
245+
})
246+
}
247+
234248
/// Learn about all broadcasts published to an origin.
235249
///
236250
/// `on_announce` is invoked with a positive announced ID for each broadcast,

rs/libmoq/src/origin.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ pub struct Origin {
2525
/// Active origin producers for publishing and consuming broadcasts.
2626
active: NonZeroSlab<moq_net::OriginProducer>,
2727

28+
/// Announcement guards from `publish`. Removing an entry (via `unpublish`) drops the
29+
/// guard, which unannounces the broadcast.
30+
published: NonZeroSlab<moq_net::OriginPublish>,
31+
2832
/// Broadcast announcement information (path, active status).
2933
announced: NonZeroSlab<(String, bool)>,
3034

@@ -185,14 +189,25 @@ impl Origin {
185189
Ok(())
186190
}
187191

192+
/// Announce `broadcast` under `path`, returning a publish handle. The announcement stays
193+
/// live until [`Self::unpublish`] is called with that handle (independent of the broadcast's
194+
/// own lifetime). Errors with [`Error::Moq`] if the path is outside the origin's scope.
188195
pub fn publish<P: moq_net::AsPath>(
189196
&mut self,
190197
origin: Id,
191198
path: P,
192199
broadcast: moq_net::BroadcastConsumer,
193-
) -> Result<(), Error> {
194-
let origin = self.active.get_mut(origin).ok_or(Error::OriginNotFound)?;
195-
origin.publish_broadcast(path, broadcast);
200+
) -> Result<Id, Error> {
201+
let origin = self.active.get(origin).ok_or(Error::OriginNotFound)?;
202+
let publish = origin.publish_broadcast(path, broadcast)?;
203+
self.published.insert(publish)
204+
}
205+
206+
/// Drop a publish handle from [`Self::publish`], unannouncing the broadcast.
207+
pub fn unpublish(&mut self, publish: Id) -> Result<(), Error> {
208+
// Dropping the removed guard is what unannounces the broadcast.
209+
let publish = self.published.remove(publish).ok_or(Error::BroadcastNotFound)?;
210+
drop(publish);
196211
Ok(())
197212
}
198213

rs/moq-boy/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ async fn run(config: &Config) -> Result<()> {
219219
let viewer_prefix = config.prefix_viewer.as_deref().unwrap_or(&default_viewer_prefix);
220220

221221
let broadcast_path = format!("{game_prefix}/{name}");
222-
publish_origin.publish_broadcast(&broadcast_path, broadcast.consume());
222+
let _publish = publish_origin
223+
.publish_broadcast(&broadcast_path, broadcast.consume())
224+
.context("failed to publish broadcast")?;
223225

224226
// Consume origin: viewer broadcasts under the viewer prefix.
225227
// JS publishes viewer feedback at "{viewer_prefix}/{name}/{viewerId}"

rs/moq-cli/src/client.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use crate::Publish;
22

3+
use anyhow::Context;
34
use hang::moq_net;
45
use url::Url;
56

67
pub async fn run_client(client: moq_native::Client, url: Url, name: String, publish: Publish) -> anyhow::Result<()> {
78
// Create an origin producer to publish to the broadcast.
89
let origin = moq_net::Origin::random().produce();
9-
origin.publish_broadcast(&name, publish.consume());
10+
let _publish = origin
11+
.publish_broadcast(&name, publish.consume())
12+
.context("failed to publish broadcast")?;
1013

1114
tracing::info!(%url, %name, "connecting");
1215

rs/moq-cli/src/server.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use anyhow::Context;
12
use hang::moq_net;
23

34
pub async fn run_server(
@@ -40,7 +41,9 @@ async fn run_serve_session(
4041
) -> anyhow::Result<()> {
4142
// Create an origin producer to publish to the broadcast.
4243
let origin = moq_net::Origin::random().produce();
43-
origin.publish_broadcast(&name, consumer);
44+
let _publish = origin
45+
.publish_broadcast(&name, consumer)
46+
.context("failed to publish broadcast")?;
4447

4548
// Blindly accept the session (WebTransport or QUIC), regardless of the URL.
4649
let session = session.with_publisher(origin.clone()).ok().await?;

rs/moq-ffi/src/origin.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,16 @@ impl MoqOriginProducer {
114114
pub fn announce(&self, path: String, broadcast: &MoqBroadcastProducer) -> Result<(), MoqError> {
115115
let _guard = crate::ffi::RUNTIME.enter();
116116
let consumer = broadcast.consume_inner()?;
117-
if !self.inner.publish_broadcast(path.as_str(), consumer) {
118-
return Err(MoqError::Unauthorized);
119-
}
117+
// Surfaces Error::Unauthorized (out of scope) via the MoqError::Protocol conversion.
118+
let publish = self.inner.publish_broadcast(path.as_str(), consumer.clone())?;
119+
120+
// Auto-unannounce when the broadcast closes (all producers dropped). The origin no longer
121+
// watches closure itself, so the spawn lives here at the runtime-bound FFI boundary.
122+
tokio::spawn(async move {
123+
consumer.closed().await;
124+
drop(publish);
125+
});
126+
120127
Ok(())
121128
}
122129
}

rs/moq-gst/src/sink/imp.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,11 +393,10 @@ async fn run_session(
393393

394394
let catalog = moq_mux::catalog::hang::Producer::new(&mut broadcast)?;
395395

396-
anyhow::ensure!(
397-
origin.publish_broadcast(&settings.broadcast, broadcast_consumer),
398-
"failed to publish broadcast {}",
399-
settings.broadcast
400-
);
396+
// Held for the lifetime of this task; dropping it (on return) unannounces the broadcast.
397+
let _publish = origin
398+
.publish_broadcast(&settings.broadcast, broadcast_consumer)
399+
.context("failed to publish broadcast")?;
401400

402401
let client = client.with_publisher(origin.clone());
403402
let session = client.connect(settings.url.clone()).await?;

rs/moq-native/examples/chat.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// cargo run --example chat
22

3+
use anyhow::Context;
4+
35
#[tokio::main]
46
async fn main() -> anyhow::Result<()> {
57
// Optional: Use moq_native to configure a logger.
@@ -46,7 +48,9 @@ async fn run_broadcast(origin: moq_net::OriginProducer) -> anyhow::Result<()> {
4648
// NOTE: The path is empty because we're using the URL to scope the broadcast.
4749
// If you put "alice" here, it would be published as "anon/chat-example/alice".
4850
// OPTIONAL: We publish after inserting the track just to avoid a nearly impossible race condition.
49-
origin.publish_broadcast("", broadcast.consume());
51+
let _publish = origin
52+
.publish_broadcast("", broadcast.consume())
53+
.context("failed to publish broadcast")?;
5054

5155
// Create a group.
5256
// Each group is independent and the newest group(s) will be prioritized.

rs/moq-native/examples/clock.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ async fn main() -> anyhow::Result<()> {
7070
let track = broadcast.create_track(track, None)?;
7171
let clock = Publisher::new(track);
7272

73-
origin.publish_broadcast(&config.broadcast, broadcast.consume());
73+
let _publish = origin
74+
.publish_broadcast(&config.broadcast, broadcast.consume())
75+
.context("failed to publish broadcast")?;
7476

7577
let reconnect = client.with_publisher(origin.clone()).reconnect(config.url);
7678

0 commit comments

Comments
 (0)