Skip to main content

moq_net/model/
bandwidth.rs

1//! Bandwidth estimation, split into a [Producer] and [Consumer] handle.
2//!
3//! A [Producer] is used to set the current estimated bitrate, notifying consumers.
4//! A [Consumer] can read the current estimate and wait for changes.
5//!
6//! What a sender does with an estimate is policy and lives with the sender, not
7//! here: see `moq_video::encode::rate` for the encoder's.
8
9use std::task::Poll;
10
11use crate::{Error, Result};
12
13#[derive(Default)]
14struct State {
15	bitrate: Option<u64>,
16	abort: Option<Error>,
17}
18
19/// Produces bandwidth estimates, notifying consumers when the value changes.
20#[derive(Clone)]
21pub struct Producer {
22	state: kio::Producer<State>,
23}
24
25impl Producer {
26	/// Create a fresh producer with no current estimate.
27	pub fn new() -> Self {
28		Self {
29			state: kio::Producer::default(),
30		}
31	}
32
33	/// Set the current bandwidth estimate in bits per second.
34	pub fn set(&self, bitrate: Option<u64>) -> Result<()> {
35		let mut state = self.modify()?;
36		if state.bitrate != bitrate {
37			state.bitrate = bitrate;
38		}
39		Ok(())
40	}
41
42	/// Create a new consumer for the bandwidth estimate.
43	pub fn consume(&self) -> Consumer {
44		Consumer {
45			state: self.state.consume(),
46			last: None,
47		}
48	}
49
50	/// Close the producer with an error, notifying all consumers.
51	pub fn abort(&self, err: Error) -> Result<()> {
52		let mut state = self.modify()?;
53		state.abort = Some(err);
54		state.close();
55		Ok(())
56	}
57
58	/// Block until the channel is closed.
59	pub async fn closed(&self) {
60		self.state.closed().await
61	}
62
63	/// Block until there are no active consumers.
64	pub async fn unused(&self) -> Result<()> {
65		kio::wait(|waiter| self.poll_unused(waiter)).await
66	}
67
68	/// Poll until there are no active consumers. Errors if the channel closes first.
69	pub fn poll_unused(&self, waiter: &kio::Waiter) -> Poll<Result<()>> {
70		self.state.poll_unused(waiter).map(|used| match used {
71			Some(()) => Ok(()),
72			None => Err(self.close_error()),
73		})
74	}
75
76	/// Block until there is at least one active consumer.
77	pub async fn used(&self) -> Result<()> {
78		kio::wait(|waiter| self.poll_used(waiter)).await
79	}
80
81	/// Poll until at least one active consumer exists. Errors if the channel closes first.
82	pub fn poll_used(&self, waiter: &kio::Waiter) -> Poll<Result<()>> {
83		self.state.poll_used(waiter).map(|used| match used {
84			Some(()) => Ok(()),
85			None => Err(self.close_error()),
86		})
87	}
88
89	fn modify(&self) -> Result<kio::Mut<'_, State>> {
90		self.state
91			.write()
92			.map_err(|r| r.abort.clone().unwrap_or(Error::Dropped))
93	}
94
95	/// The close error, once the channel is closed.
96	fn close_error(&self) -> Error {
97		self.state.read().abort.clone().unwrap_or(Error::Dropped)
98	}
99}
100
101impl Default for Producer {
102	fn default() -> Self {
103		Self::new()
104	}
105}
106
107/// Consumes bandwidth estimates, allowing reads and async change notifications.
108#[derive(Clone)]
109pub struct Consumer {
110	state: kio::Consumer<State>,
111	last: Option<u64>,
112}
113
114impl Consumer {
115	/// Get the current bandwidth estimate synchronously.
116	pub fn peek(&self) -> Option<u64> {
117		self.state.read().bitrate
118	}
119
120	/// Poll for a bandwidth change without blocking.
121	///
122	/// `Ok(None)` means the estimate is unavailable *for now*: the backend
123	/// stopped reporting one, or the handle spans reconnects and is between
124	/// sessions. `Err` means the producer is gone and no further change will ever
125	/// arrive. They're distinct because a caller holds its current rate for the
126	/// first and stops watching for the second.
127	///
128	/// A backend with no bandwidth estimation at all yields no [Consumer] in the
129	/// first place, so that case never reaches here.
130	pub fn poll_changed(&mut self, waiter: &kio::Waiter) -> Poll<Result<Option<u64>>> {
131		let last = self.last;
132
133		match self.state.poll(waiter, |state| {
134			if state.bitrate != last {
135				Poll::Ready(state.bitrate)
136			} else {
137				Poll::Pending
138			}
139		}) {
140			Poll::Ready(Ok(bitrate)) => {
141				self.last = bitrate;
142				Poll::Ready(Ok(bitrate))
143			}
144			// Closed, and the value hasn't moved since the last read: report it as
145			// terminal. Collapsing this into `Ok(None)` would be indistinguishable
146			// from a live-but-unavailable estimate, and since a closed channel is
147			// always immediately ready, a `select!` over it would spin forever.
148			Poll::Ready(Err(state)) => Poll::Ready(Err(state.abort.clone().unwrap_or(Error::Dropped))),
149			Poll::Pending => Poll::Pending,
150		}
151	}
152
153	/// Block until the bandwidth estimate changes, returning the new value, or
154	/// `None` when the estimate has become unavailable.
155	///
156	/// # Errors
157	///
158	/// Returns an error once the producer is closed or dropped, so a caller can
159	/// stop watching. See [`poll_changed`](Self::poll_changed).
160	pub async fn changed(&mut self) -> Result<Option<u64>> {
161		kio::wait(|waiter| self.poll_changed(waiter)).await
162	}
163}
164
165#[cfg(test)]
166mod tests {
167	use super::*;
168
169	/// An unavailable estimate and a dead producer must not look alike: a caller
170	/// holds its rate for the former and stops watching for the latter.
171	/// Reporting closure as `Ok(None)` would spin any `select!` over `changed()`,
172	/// because a closed channel is always immediately ready.
173	#[tokio::test]
174	async fn closed_is_distinct_from_unavailable() {
175		let producer = Producer::new();
176		let mut consumer = producer.consume();
177
178		producer.set(Some(1_000_000)).unwrap();
179		assert_eq!(consumer.changed().await.unwrap(), Some(1_000_000));
180
181		// Live, but the estimate went away (e.g. disconnected): still watchable.
182		producer.set(None).unwrap();
183		assert_eq!(consumer.changed().await.unwrap(), None);
184
185		// Gone for good.
186		producer.abort(Error::Cancel).unwrap();
187		assert!(consumer.changed().await.is_err());
188		// And it stays terminal rather than flapping back to a value.
189		assert!(consumer.changed().await.is_err());
190	}
191}