Skip to main content

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}