Skip to main content

moq_net/model/
subscription.rs

1use std::{task::Poll, time::Duration};
2
3/// Subscriber-side preferences for receiving a track.
4///
5/// Each subscriber holds its own [`Subscription`]; the publisher observes an
6/// aggregate across all live subscribers via [`crate::track::Producer::subscription`].
7/// A subscriber can change its preferences after the fact with
8/// [`crate::track::Subscriber::update`].
9#[derive(Clone, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub struct Subscription {
12	/// Delivery priority. Higher values preempt lower ones when bandwidth is constrained.
13	pub priority: u8,
14	/// Whether groups are prioritized in sequence order. Groups may always arrive
15	/// out-of-order (or not at all) over the network. Defaults to `false`; the
16	/// aggregate is ordered only when every live subscriber asks for it.
17	pub ordered: bool,
18	/// The maximum age of a non-latest group before it is skipped. `Duration::ZERO`
19	/// skips immediately (e.g. group 8 arriving means group 7 is skipped); a larger
20	/// value tolerates that much reordering before giving up on the older group.
21	///
22	/// This is the `Subscriber Max Latency` on the wire, enforced by the publisher's
23	/// cache. Receivers that buffer (e.g. a jitter buffer) enforce the same budget
24	/// locally, and a group is skipped once either measure exceeds it.
25	pub latency_max: Duration,
26	/// First group the publisher should deliver, or `None` to start at the latest group.
27	///
28	/// A request, aggregated across every live subscriber (the earliest explicit start
29	/// wins), so it says what the publisher sends, not what any one subscriber sees.
30	/// [`crate::track::Subscriber::start_at`] is the local read cursor; setting one does
31	/// not imply the other. See [Local cursor vs wire
32	/// preference](crate::track::Subscriber#local-cursor-vs-wire-preference).
33	pub group_start: Option<u64>,
34	/// Last group the publisher should deliver (inclusive), or `None` for no end.
35	///
36	/// A request, aggregated across every live subscriber (any unbounded subscriber makes
37	/// the aggregate unbounded). [`crate::track::Subscriber::end_at`] is the local read
38	/// cursor; setting one does not imply the other. See [Local cursor vs wire
39	/// preference](crate::track::Subscriber#local-cursor-vs-wire-preference).
40	pub group_end: Option<u64>,
41}
42
43impl Default for Subscription {
44	fn default() -> Self {
45		Self {
46			priority: 0,
47			ordered: false,
48			latency_max: Duration::ZERO,
49			group_start: None,
50			group_end: None,
51		}
52	}
53}
54
55impl Subscription {
56	/// Set the delivery priority, returning `self` for chaining.
57	pub fn with_priority(mut self, priority: u8) -> Self {
58		self.priority = priority;
59		self
60	}
61
62	/// Set whether groups are prioritized in sequence order, returning `self` for
63	/// chaining. Groups may always arrive out-of-order (or not at all) over the network.
64	pub fn with_ordered(mut self, ordered: bool) -> Self {
65		self.ordered = ordered;
66		self
67	}
68
69	/// Set the maximum age of a non-latest group before it is skipped, returning
70	/// `self` for chaining.
71	pub fn with_latency_max(mut self, latency_max: Duration) -> Self {
72		self.latency_max = latency_max;
73		self
74	}
75
76	/// Set the first group to deliver, returning `self` for chaining.
77	pub fn with_group_start(mut self, group_start: impl Into<Option<u64>>) -> Self {
78		self.group_start = group_start.into();
79		self
80	}
81
82	/// Set the last group to deliver (inclusive), returning `self` for chaining.
83	pub fn with_group_end(mut self, group_end: impl Into<Option<u64>>) -> Self {
84		self.group_end = group_end.into();
85		self
86	}
87
88	// Fold this subscription into the running aggregate: Ready with the merged
89	// result when it demands more than `combined`, Pending when it's a subset
90	// (so callers can skip a redundant broadcast of the same aggregate).
91	pub(super) fn poll_combined(&self, combined: &Option<Subscription>) -> Poll<Subscription> {
92		let Some(combined) = combined else {
93			return Poll::Ready(self.clone());
94		};
95
96		let merged = Subscription {
97			priority: self.priority.max(combined.priority),
98			// Sequence-first prioritization is enabled only when every subscriber wants it.
99			ordered: self.ordered && combined.ordered,
100			latency_max: self.latency_max.max(combined.latency_max),
101			group_start: min_some(self.group_start, combined.group_start),
102			group_end: max_unbounded(self.group_end, combined.group_end),
103		};
104
105		if &merged != combined {
106			return Poll::Ready(merged);
107		}
108
109		Poll::Pending
110	}
111}
112
113/// The lower of two optional bounds, treating `None` as unbounded.
114pub(super) fn min_some(a: Option<u64>, b: Option<u64>) -> Option<u64> {
115	match (a, b) {
116		(Some(a), Some(b)) => Some(a.min(b)),
117		(Some(a), None) | (None, Some(a)) => Some(a),
118		(None, None) => None,
119	}
120}
121
122fn max_unbounded(a: Option<u64>, b: Option<u64>) -> Option<u64> {
123	match (a, b) {
124		(Some(a), Some(b)) => Some(a.max(b)),
125		(None, _) | (_, None) => None,
126	}
127}
128
129#[cfg(test)]
130mod tests {
131	use super::*;
132
133	fn combine(subscriptions: &[Subscription]) -> Option<Subscription> {
134		let mut combined = None;
135		for sub in subscriptions {
136			if let Poll::Ready(merged) = sub.poll_combined(&combined) {
137				combined = Some(merged);
138			}
139		}
140		combined
141	}
142
143	#[test]
144	fn combined_ordered_stays_ordered_for_multiple_ordered_viewers() {
145		let subscription = Subscription::default().with_ordered(true);
146
147		let combined = combine(&[subscription.clone(), subscription.clone(), subscription]).unwrap();
148
149		assert!(combined.ordered);
150	}
151
152	#[test]
153	fn combined_any_unordered_viewer_disables_ordered() {
154		let unordered = Subscription::default().with_ordered(false);
155		let ordered = Subscription::default().with_ordered(true);
156
157		let combined = combine(&[unordered, ordered]).unwrap();
158
159		assert!(!combined.ordered);
160	}
161
162	#[test]
163	fn combined_group_start_uses_earliest_explicit_start() {
164		let live = Subscription::default().with_group_start(None);
165		let catchup = Subscription::default().with_group_start(10);
166		let older_catchup = Subscription::default().with_group_start(5);
167
168		let combined = combine(&[live, catchup, older_catchup]).unwrap();
169
170		assert_eq!(combined.group_start, Some(5));
171	}
172
173	#[test]
174	fn combined_group_end_keeps_live_subscription_unbounded() {
175		let live = Subscription::default().with_group_end(None);
176		let bounded = Subscription::default().with_group_end(10);
177
178		let combined = combine(&[live, bounded]).unwrap();
179
180		assert_eq!(combined.group_end, None);
181	}
182
183	#[test]
184	fn combined_group_end_uses_latest_bounded_end() {
185		let early = Subscription::default().with_group_end(10);
186		let late = Subscription::default().with_group_end(20);
187
188		let combined = combine(&[early, late]).unwrap();
189
190		assert_eq!(combined.group_end, Some(20));
191	}
192}