moq_srt/server.rs
1//! SRT server: accept connections, and hand each pending request to the caller
2//! as a [`Request`] to authorize.
3//!
4//! [`Server::accept`] yields a [`Request`] for each incoming SRT connection,
5//! before the handshake is finalized, classified by its stream-id `m=` mode into
6//! one of two directions. The caller inspects [`Request::resource`] /
7//! [`Request::stream_id`], makes an authorization decision, and either:
8//!
9//! - **[`Request::Publish`]**: [`Publish::accept`] (ingest the connection's
10//! MPEG-TS into an origin at a path) or [`Publish::reject`]. This is the
11//! contribution path (OBS, ffmpeg).
12//! - **[`Request::Subscribe`]**: [`Subscribe::accept`] (re-mux a broadcast from
13//! an origin back to MPEG-TS and stream it down to the caller) or
14//! [`Subscribe::reject`]. This is the egress path: a player (VLC, ffmpeg) pulls
15//! `srt://host:port?streamid=#!::r=<broadcast>,m=request`.
16//!
17//! This mirrors `moq-native`'s `Server` / `Request`, so the gateway stays
18//! unopinionated about auth: the embedder (e.g. a relay verifying the stream id
19//! as a JWT) owns that policy. For the unauthenticated convenience that accepts
20//! everything and routes by prefix, use [`crate::run`].
21
22use std::net::SocketAddr;
23use std::time::{Duration, Instant};
24
25use futures::{SinkExt, StreamExt};
26use moq_net::origin;
27use srt_tokio::access::{
28 AccessControlList, ConnectionMode, RejectReason, ServerRejectReason, StandardAccessControlEntry,
29};
30use srt_tokio::options::{PacketCount, SocketOptions, StreamId};
31use srt_tokio::{ConnectionRequest, SrtIncoming, SrtListener, SrtSocket};
32
33use crate::Result;
34
35/// Default SRT receive latency: the negotiated buffer that trades delay for loss
36/// recovery. Override per-server with [`Server::bind`]'s `latency` argument.
37pub(crate) const DEFAULT_LATENCY: Duration = Duration::from_millis(500);
38
39/// SRT payload size for egress: 7 MPEG-TS packets (7 x 188), the de-facto
40/// standard for TS-over-SRT and a clean fit under the typical SRT MTU.
41const SRT_PAYLOAD: usize = 7 * 188;
42
43/// Coalesce TS bytes that share one SRT pacing instant.
44#[derive(Default)]
45struct SrtChunker {
46 buffer: bytes::BytesMut,
47 send_at: Option<Instant>,
48}
49
50impl SrtChunker {
51 /// Add one muxer frame, flushing a partial chunk before its pacing instant changes.
52 fn push(&mut self, send_at: Instant, payload: &[u8]) -> Vec<(Instant, bytes::Bytes)> {
53 if payload.is_empty() {
54 return Vec::new();
55 }
56
57 let mut chunks = Vec::new();
58 if self.send_at.is_some_and(|buffered_at| buffered_at != send_at) {
59 chunks.extend(self.flush());
60 }
61
62 self.send_at = Some(send_at);
63 self.buffer.extend_from_slice(payload);
64 while self.buffer.len() >= SRT_PAYLOAD {
65 chunks.push((send_at, self.buffer.split_to(SRT_PAYLOAD).freeze()));
66 }
67
68 if self.buffer.is_empty() {
69 self.send_at = None;
70 }
71 chunks
72 }
73
74 /// Flush the final partial payload, if any.
75 fn flush(&mut self) -> Option<(Instant, bytes::Bytes)> {
76 let send_at = self.send_at.take()?;
77 debug_assert!(!self.buffer.is_empty());
78 Some((send_at, self.buffer.split().freeze()))
79 }
80}
81
82/// Match libsrt's standard send-buffer window.
83const SRT_BUFFER_PACKETS: PacketCount = PacketCount(8192);
84
85/// srt-tokio defaults its sender to only 32 packets, so one large keyframe can
86/// evict an unsent packet and wedge its send queue behind the missing sequence
87/// number.
88pub(crate) fn configure_buffers(options: &mut SocketOptions) {
89 options.sender.buffer_size = SRT_BUFFER_PACKETS * options.session.max_segment_size;
90}
91
92/// An SRT server that yields each incoming connection's pending request as a
93/// [`Request`].
94///
95/// Build it with [`bind`](Self::bind), then loop on [`accept`](Self::accept).
96/// Each [`Request`] is produced before the SRT handshake is finalized, so the
97/// caller can authorize (and pick the broadcast path) before any media flows.
98pub struct Server {
99 /// Held to keep the listener (and its UDP socket) alive for the server's lifetime.
100 _listener: SrtListener,
101 incoming: SrtIncoming,
102 /// The negotiated SRT receive latency, reused as the egress skip threshold on
103 /// each [`Subscribe`] (see [`crate::ts::Subscriber::new`]).
104 latency: Duration,
105}
106
107impl Server {
108 /// Bind an SRT listener on `addr` (SRT has no well-known port; 9000 is common).
109 ///
110 /// `latency` is the SRT receive latency, negotiated at handshake time; pass
111 /// `None` for a sensible default (500ms). It doubles as the egress skip
112 /// threshold for [`Subscribe`] requests.
113 pub async fn bind(addr: SocketAddr, latency: impl Into<Option<Duration>>) -> Result<Self> {
114 let latency = latency.into().unwrap_or(DEFAULT_LATENCY);
115 let (listener, incoming) = SrtListener::builder()
116 .latency(latency)
117 .set(configure_buffers)
118 .bind(addr)
119 .await?;
120 Ok(Self {
121 _listener: listener,
122 incoming,
123 latency,
124 })
125 }
126
127 /// Wait for the next connection that wants to publish or subscribe.
128 ///
129 /// Connections whose stream id can't be routed (no usable resource name) are
130 /// rejected internally and skipped, so every [`Request`] returned is
131 /// actionable. Returns `None` only if the listener stops accepting (it
132 /// currently never does).
133 pub async fn accept(&mut self) -> Option<Request> {
134 while let Some(request) = self.incoming.incoming().next().await {
135 let peer = request.remote();
136 let Some((resource, mode)) = parse_stream_id(request.stream_id()) else {
137 tracing::warn!(%peer, stream_id = ?request.stream_id(), "rejecting SRT: no usable stream id");
138 reject_log(request, ServerRejectReason::BadRequest, peer).await;
139 continue;
140 };
141
142 let stream_id = request.stream_id().map(|id| id.as_str().to_string());
143 let pending = Pending {
144 request,
145 resource,
146 stream_id,
147 peer,
148 latency: self.latency,
149 };
150
151 // `m=request` reads a broadcast out; everything else publishes one in.
152 return Some(match mode {
153 ConnectionMode::Request => Request::Subscribe(Subscribe(pending)),
154 _ => Request::Publish(Publish(pending)),
155 });
156 }
157
158 None
159 }
160}
161
162/// Common state behind a pending [`Request`]: the SRT connection plus the
163/// routing info parsed from its stream id.
164struct Pending {
165 request: ConnectionRequest,
166 /// The resource name to route on: the stream id's `r=` value, or the raw
167 /// stream id when it carries no access-control list.
168 resource: String,
169 /// The raw stream id string, if any. Exposed so an embedder can parse its own
170 /// fields out of it (e.g. a token in `u=` or a custom key).
171 stream_id: Option<String>,
172 peer: SocketAddr,
173 /// The SRT receive latency, reused as the egress skip threshold on a subscribe.
174 latency: Duration,
175}
176
177/// What an accepted SRT connection wants: to contribute media ([`Publish`]) or to
178/// view it ([`Subscribe`]).
179///
180/// Yielded by [`Server::accept`], classified by the stream id's `m=` mode.
181/// Inspect [`resource`](Self::resource) / [`stream_id`](Self::stream_id), then
182/// match to authorize the right direction. Dropping it without accepting or
183/// rejecting drops the connection.
184#[non_exhaustive]
185pub enum Request {
186 /// A client pushing media in (OBS, ffmpeg). Ingest it with [`Publish::accept`].
187 Publish(Publish),
188 /// A client pulling media out (VLC, ffmpeg). Serve it with [`Subscribe::accept`].
189 Subscribe(Subscribe),
190}
191
192impl Request {
193 /// The resource name to route on: the stream id's `r=` value, or the raw
194 /// stream id when it carries no access-control list.
195 pub fn resource(&self) -> &str {
196 match self {
197 Request::Publish(r) => r.resource(),
198 Request::Subscribe(r) => r.resource(),
199 }
200 }
201
202 /// The raw SRT stream id, if the client supplied one.
203 pub fn stream_id(&self) -> Option<&str> {
204 match self {
205 Request::Publish(r) => r.stream_id(),
206 Request::Subscribe(r) => r.stream_id(),
207 }
208 }
209
210 /// The remote peer address.
211 pub fn peer(&self) -> SocketAddr {
212 match self {
213 Request::Publish(r) => r.peer(),
214 Request::Subscribe(r) => r.peer(),
215 }
216 }
217}
218
219/// A pending SRT publish (contribution), waiting on the caller to authorize it.
220///
221/// Inspect [`resource`](Self::resource) / [`stream_id`](Self::stream_id), then
222/// either [`accept`](Self::accept) the publish into an origin at a chosen
223/// broadcast path or [`reject`](Self::reject) it. Dropping it without either
224/// drops the connection.
225pub struct Publish(Pending);
226
227impl Publish {
228 /// The resource name to route on (the stream id's `r=` value, or the raw
229 /// stream id).
230 pub fn resource(&self) -> &str {
231 &self.0.resource
232 }
233
234 /// The raw SRT stream id, if the client supplied one.
235 ///
236 /// Conventionally just a resource path, but an embedder can treat it (or a
237 /// field within it) as a token to authenticate the publish.
238 pub fn stream_id(&self) -> Option<&str> {
239 self.0.stream_id.as_deref()
240 }
241
242 /// The remote peer address.
243 pub fn peer(&self) -> SocketAddr {
244 self.0.peer
245 }
246
247 /// Accept the publish: announce a broadcast at `path` in `origin` and pump the
248 /// connection's MPEG-TS into it until the client disconnects.
249 ///
250 /// `origin` is whatever the caller wants the media published into (e.g. a
251 /// relay's shared origin, optionally scoped per the authenticated token). This
252 /// future resolves when the connection ends, so callers usually run it on its
253 /// own task.
254 pub async fn accept(self, origin: &origin::Producer, path: impl moq_net::AsPath) -> Result<()> {
255 let path = path.as_path();
256 let socket = self.0.request.accept(None).await?;
257 tracing::info!(peer = %self.0.peer, %path, "SRT publish accepted");
258 serve_publish(origin, path.as_str(), socket).await
259 }
260
261 /// Reject the publish, sending the client a `Forbidden` rejection.
262 pub async fn reject(self) -> Result<()> {
263 Ok(self
264 .0
265 .request
266 .reject(RejectReason::Server(ServerRejectReason::Forbidden))
267 .await?)
268 }
269}
270
271/// A pending SRT subscribe (egress), waiting on the caller to authorize it.
272///
273/// The viewing counterpart of [`Publish`]: inspect [`resource`](Self::resource) /
274/// [`stream_id`](Self::stream_id), then [`accept`](Self::accept) to serve a
275/// broadcast from an origin down to the caller, or [`reject`](Self::reject) it.
276/// Dropping it without either drops the connection.
277pub struct Subscribe(Pending);
278
279impl Subscribe {
280 /// The resource name to route on (the stream id's `r=` value, or the raw
281 /// stream id).
282 pub fn resource(&self) -> &str {
283 &self.0.resource
284 }
285
286 /// The raw SRT stream id, if the client supplied one.
287 ///
288 /// As with a publish, an embedder can treat this as a token to authorize the
289 /// viewer.
290 pub fn stream_id(&self) -> Option<&str> {
291 self.0.stream_id.as_deref()
292 }
293
294 /// The remote peer address.
295 pub fn peer(&self) -> SocketAddr {
296 self.0.peer
297 }
298
299 /// Accept the subscribe: resolve the broadcast at `path` in `origin`, re-mux
300 /// it to MPEG-TS, and stream it down to the caller until either side ends.
301 ///
302 /// Waits for the broadcast to be announced (so a caller may connect before the
303 /// publisher), cancelling cleanly if the caller disconnects first. This future
304 /// resolves when playback ends, so callers usually run it on its own task.
305 pub async fn accept(self, origin: &origin::Consumer, path: impl moq_net::AsPath) -> Result<()> {
306 let path = path.as_path();
307 let socket = self.0.request.accept(None).await?;
308 tracing::info!(peer = %self.0.peer, %path, "SRT subscribe accepted");
309 serve_subscribe(origin, path.as_str(), socket, self.0.latency).await
310 }
311
312 /// Reject the subscribe, sending the client a `Forbidden` rejection.
313 pub async fn reject(self) -> Result<()> {
314 Ok(self
315 .0
316 .request
317 .reject(RejectReason::Server(ServerRejectReason::Forbidden))
318 .await?)
319 }
320}
321
322/// Reject a connection request, logging (but not propagating) a send failure.
323/// Used for connections the server drops itself, before they reach the caller.
324async fn reject_log(request: ConnectionRequest, reason: ServerRejectReason, peer: SocketAddr) {
325 if let Err(err) = request.reject(RejectReason::Server(reason)).await {
326 tracing::debug!(%peer, %err, "failed to send SRT rejection");
327 }
328}
329
330/// Pump one accepted SRT socket's MPEG-TS payload into the origin (`m=publish`).
331pub(crate) async fn serve_publish(origin: &origin::Producer, path: &str, mut socket: SrtSocket) -> Result<()> {
332 use futures::TryStreamExt;
333
334 let mut publisher = crate::ts::Publisher::new(origin, path)?;
335
336 // Run the read/feed loop so an error surfaces here instead of unwinding past
337 // the publisher, which would drop it (and its tracks) with a bare Error::Dropped.
338 let result: Result<()> = async {
339 while let Some((_instant, bytes)) = socket.try_next().await? {
340 publisher.feed(bytes)?;
341 }
342 Ok(())
343 }
344 .await;
345
346 match &result {
347 // Clean end (the caller closed): flush the final groups.
348 Ok(()) => publisher.finish()?,
349 // The socket or demux failed: abort with the real cause so subscribers see it.
350 Err(err) => publisher.abort(moq_net::Error::Transport(err.to_string())),
351 }
352 result
353}
354
355/// Mux the requested broadcast back to MPEG-TS and stream it to the SRT caller
356/// (`m=request`).
357///
358/// Waits for the broadcast to be announced (so a caller may connect before the
359/// publisher), then packs the muxer's output into [`SRT_PAYLOAD`]-sized SRT
360/// messages. Returns once the broadcast ends or the caller disconnects.
361pub(crate) async fn serve_subscribe(
362 origin: &origin::Consumer,
363 path: &str,
364 mut socket: SrtSocket,
365 latency: Duration,
366) -> Result<()> {
367 // Resolve the broadcast, but watch the socket while we wait: `announced_broadcast`
368 // parks forever for a stream that is never published, and nothing else polls the
369 // socket during that wait, so without this a caller who requests a non-existent
370 // stream (or hangs up before it starts) would leak this task and its socket.
371 let subscriber = tokio::select! {
372 biased;
373 _ = wait_closed(&mut socket) => {
374 tracing::debug!(%path, "SRT subscribe closed before its broadcast was available");
375 return Ok(());
376 }
377 subscriber = crate::ts::Subscriber::new(origin, path, latency) => subscriber?,
378 };
379
380 let Some(mut subscriber) = subscriber else {
381 tracing::warn!(%path, "SRT subscribe for an unroutable broadcast");
382 return Ok(());
383 };
384
385 // MPEG-TS is a continuous byte stream, so coalesce bytes that share a pacing
386 // instant and slice them on a fixed boundary. Flush a partial payload before the
387 // instant changes: one SRT message has only one TSBPD timestamp, so mixing frames
388 // here would re-stamp the earlier bytes with the frame that completed the chunk.
389 //
390 // Pace each payload on the media clock: the Instant handed to `send` is the
391 // payload's origin time feeding the receiver's TSBPD, which reconstructs the
392 // inter-frame spacing from it. We don't know the live playhead when a subscriber
393 // attaches, so the pacer anchors it for us -- the newest frame is "now" and
394 // earlier frames map to proportionally earlier instants, re-anchoring whenever
395 // the media outruns wall-clock (a tune-in burst, a catch-up, or producer
396 // drift). The default zero lead is deliberate: the receiver owns the jitter
397 // buffer (the SRT latency parameter), so the sender adds no lookahead of its
398 // own.
399 let mut pacer = moq_mux::Pacer::default();
400 // The first payload's send instant, the floor every later one is clamped up to
401 // (see `clamp_to_floor`).
402 let mut floor = None;
403 let mut chunker = SrtChunker::default();
404 while let Some(frame) = subscriber.next().await? {
405 // Preserve the media-clock pacing for future frames, but never transmit a
406 // timestamp below the first packet's (see `floor` above).
407 let send_at = clamp_to_floor(pacer.pace(frame.timestamp, Instant::now()), &mut floor);
408
409 for chunk in chunker.push(send_at, &frame.payload) {
410 socket.send(chunk).await?;
411 }
412 }
413
414 if let Some(chunk) = chunker.flush() {
415 socket.send(chunk).await?;
416 }
417 socket.close().await?;
418
419 Ok(())
420}
421
422/// Clamp a paced send instant up to the connection's first one, seeding that floor
423/// on the first call.
424///
425/// The receiver anchors its TSBPD clock on the first packet, and an SRT packet
426/// timestamp is `u32` microseconds relative to the socket epoch, so a later payload
427/// stamped *before* the first underflows on the receiver -- it wraps ~4295s into the
428/// future, and in-order TSBPD delivery stalls behind it after ~one packet. `pace`
429/// paces a reordered B-frame *before* the current anchor, and at tune-in the anchor
430/// sits at the first packet, so without this clamp that reorder underflows.
431fn clamp_to_floor(send_at: Instant, floor: &mut Option<Instant>) -> Instant {
432 send_at.max(*floor.get_or_insert(send_at))
433}
434
435/// Resolve once the SRT caller hangs up (a clean close or an error), draining and
436/// ignoring any unexpected inbound packets. A subscribe caller normally sends
437/// nothing, so this is purely a disconnect signal to race against the announce wait.
438async fn wait_closed(socket: &mut SrtSocket) {
439 use futures::TryStreamExt;
440 while let Ok(Some(_)) = socket.try_next().await {}
441}
442
443/// Parse an SRT stream id into its resource name and connection mode.
444///
445/// Prefers the standard `#!::r=<resource>,m=<mode>` form, then falls back to the
446/// raw stream-id string (always treated as publish). Returns `None` when there's
447/// nothing usable to route on.
448fn parse_stream_id(stream_id: Option<&StreamId>) -> Option<(String, ConnectionMode)> {
449 let raw = stream_id?.as_str().trim();
450
451 // Standard SRT access-control form: `#!::r=<resource>,m=<mode>,...`. Absent
452 // `m=` defaults to publish, matching a bare stream id and OBS-style ingest.
453 let mut resource = None;
454 let mut mode = ConnectionMode::Publish;
455 if let Ok(acl) = raw.parse::<AccessControlList>() {
456 for entry in acl.0 {
457 match StandardAccessControlEntry::try_from(entry) {
458 Ok(StandardAccessControlEntry::ResourceName(name)) if !name.is_empty() => resource = Some(name),
459 Ok(StandardAccessControlEntry::Mode(m)) => mode = m,
460 _ => {}
461 }
462 }
463 }
464
465 // Fall back to the raw stream id (e.g. OBS-style `app/key`), but never to an
466 // unparsed `#!::` control string.
467 let name = match resource {
468 Some(name) => name,
469 None if raw.is_empty() || raw.starts_with("#!::") => return None,
470 None => raw.to_string(),
471 };
472
473 Some((name, mode))
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use bytes::Bytes;
480 use std::net::SocketAddr;
481 use std::time::Duration;
482
483 #[test]
484 fn send_buffer_uses_standard_srt_window() {
485 let mut options = SocketOptions::default();
486 configure_buffers(&mut options);
487
488 assert_eq!(
489 options.sender.buffer_size,
490 SRT_BUFFER_PACKETS * options.session.max_segment_size
491 );
492 }
493
494 /// Regression for #2978: a frame that completes a chunk must not re-stamp bytes
495 /// already buffered from an earlier pacing instant.
496 #[test]
497 fn chunker_flushes_before_the_pacing_instant_changes() {
498 let first = Instant::now();
499 let second = first + Duration::from_millis(25);
500 let mut chunker = SrtChunker::default();
501
502 assert!(chunker.push(first, &[1; 188]).is_empty());
503 let flushed = chunker.push(second, &[2; SRT_PAYLOAD - 188]);
504 assert_eq!(flushed.len(), 1);
505 assert_eq!(flushed[0].0, first);
506 assert_eq!(flushed[0].1.as_ref(), &[1; 188]);
507
508 let completed = chunker.push(second, &[3; 188]);
509 assert_eq!(completed.len(), 1);
510 assert_eq!(completed[0].0, second);
511 assert_eq!(&completed[0].1[..SRT_PAYLOAD - 188], &[2; SRT_PAYLOAD - 188]);
512 assert_eq!(&completed[0].1[SRT_PAYLOAD - 188..], &[3; 188]);
513 assert!(chunker.flush().is_none());
514 }
515
516 /// Regression: srt-tokio's 32-packet default sender buffer evicts unsent packets
517 /// once a burst overflows it, wedging the connection within the first few messages
518 /// (see [`configure_buffers`]).
519 #[tokio::test]
520 async fn accepted_socket_sends_a_burst_larger_than_srt_tokio_default() {
521 let probe = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
522 let addr: SocketAddr = probe.local_addr().unwrap();
523 drop(probe);
524
525 // TSBPD holds every payload for the negotiated latency before releasing it, so
526 // ask for a short one: this asserts buffering, not delay.
527 let mut server = Server::bind(addr, Duration::from_millis(50)).await.unwrap();
528 let caller = tokio::spawn(async move {
529 SrtSocket::builder()
530 .call(addr, Some("#!::r=buffer-test,m=request"))
531 .await
532 .unwrap()
533 });
534
535 let request = server.accept().await.expect("an SRT request");
536 let Request::Subscribe(subscribe) = request else {
537 panic!("m=request must create a subscribe request");
538 };
539 let mut sender = subscribe.0.request.accept(None).await.unwrap();
540 let mut receiver = caller.await.unwrap();
541
542 // Several times srt-tokio's 32-packet default, which stalls before the tenth
543 // message. Keep it well under the ~1000 packets the sender paces out per second
544 // while its send buffer ages: past that, SRT drops the tail of the burst as too
545 // late and (silently, in srt-tokio) never retransmits it, which is a property of
546 // the burst size rather than of the buffer under test.
547 const MESSAGES: usize = 128;
548 for sequence in 0..MESSAGES {
549 sender
550 .send((Instant::now(), Bytes::copy_from_slice(&sequence.to_be_bytes())))
551 .await
552 .unwrap();
553 }
554
555 for sequence in 0..MESSAGES {
556 let (_, payload) = tokio::time::timeout(Duration::from_secs(3), receiver.next())
557 .await
558 .expect("SRT sender stalled after its small default buffer overflowed")
559 .expect("SRT sender closed before the burst finished")
560 .unwrap();
561 assert_eq!(payload.as_ref(), sequence.to_be_bytes());
562 }
563 }
564
565 /// Regression: a reordered frame paced before the first packet must be clamped up
566 /// to it, not transmitted with an earlier SRT timestamp. Reproduces the tune-in
567 /// sequence a looping TS source triggers intermittently: a fast first delivery
568 /// leaves the anchor at the live edge, then a newer frame re-anchors and an older
569 /// (reordered) frame paces behind it -- below the first packet, which would
570 /// underflow the receiver's u32 timestamp and stall in-order delivery after ~one
571 /// packet.
572 #[test]
573 fn reordered_frame_is_clamped_to_the_first_packet() {
574 use moq_net::Timestamp;
575 let ms = |m: u64| Timestamp::from_micros(m * 1_000).unwrap();
576
577 // Drive pace + clamp exactly like `serve_subscribe`, with controlled `now`s so
578 // the second frame re-anchors (its media outruns wall-clock) and the third is a
579 // reorder whose media trails the new anchor.
580 let start = Instant::now();
581 let mut pacer = moq_mux::Pacer::default();
582 let mut floor = None;
583
584 // i0: first frame, delivered ~instantly -> stamped at the live edge.
585 let first = clamp_to_floor(pacer.pace(ms(1_400), start), &mut floor);
586 // i1: 83ms newer in media, produced ~1ms later -> re-anchors to `now`.
587 let _ = clamp_to_floor(pacer.pace(ms(1_483), start + Duration::from_millis(1)), &mut floor);
588 // i2: a reordered B-frame 41ms behind the new anchor.
589 let unclamped = pacer.pace(ms(1_442), start + Duration::from_millis(2));
590 let clamped = clamp_to_floor(unclamped, &mut floor);
591
592 assert!(
593 unclamped < first,
594 "the reorder paces before the first packet without the clamp (the bug)"
595 );
596 assert_eq!(clamped, first, "the clamp holds it at the first packet's instant");
597 }
598
599 fn sid(s: &str) -> StreamId {
600 StreamId::try_from(s.as_bytes().to_vec()).unwrap()
601 }
602
603 fn parse(s: &str) -> Option<(String, ConnectionMode)> {
604 parse_stream_id(Some(&sid(s)))
605 }
606
607 #[test]
608 fn standard_resource_form() {
609 let (resource, mode) = parse("#!::r=live/cam0,m=publish").unwrap();
610 assert_eq!(resource, "live/cam0");
611 assert_eq!(mode, ConnectionMode::Publish);
612 }
613
614 #[test]
615 fn request_mode_is_egress() {
616 let (resource, mode) = parse("#!::r=live/cam0,m=request").unwrap();
617 assert_eq!(resource, "live/cam0");
618 assert_eq!(mode, ConnectionMode::Request);
619 }
620
621 #[test]
622 fn absent_mode_defaults_to_publish() {
623 // Both a bare stream id and an `r=`-only ACL ingest by default.
624 assert_eq!(parse("app/key").unwrap().1, ConnectionMode::Publish);
625 assert_eq!(parse("#!::r=cam0").unwrap().1, ConnectionMode::Publish);
626 }
627
628 #[test]
629 fn raw_stream_id() {
630 let (resource, mode) = parse("app/key").unwrap();
631 assert_eq!(resource, "app/key");
632 assert_eq!(mode, ConnectionMode::Publish);
633 }
634
635 #[test]
636 fn missing_or_empty_is_rejected() {
637 assert!(parse_stream_id(None).is_none());
638 assert!(parse("").is_none());
639 assert!(parse("#!::").is_none());
640 }
641}