moqtap_client/dispatch.rs
1//! Unified multi-draft entry-point types.
2//!
3//! This module is the facade downstream consumers use to hold a MoQT
4//! connection without caring which draft was negotiated.
5//! It mirrors [`moqtap_codec::dispatch`]: one enum variant per enabled draft,
6//! gated on its feature flag.
7//!
8//! Four types live here:
9//!
10//! - `AnyConnection` — wraps a draft-specific `Connection`.
11//! - `AnyClientEvent` — wraps a draft-specific `ClientEvent`.
12//! - `AnyConnectionObserver` — a trait that receives `AnyClientEvent`s.
13//! Attached to an `AnyConnection` via `AnyConnection::set_observer`,
14//! which installs a per-draft adapter on the inner connection.
15//! - `AnyRequest` — what a request made through `AnyConnection` leaves the
16//! caller holding. Drafts 07-16 put every request on the one control
17//! stream and hand back only a request ID; from draft-17 on each request
18//! owns a bidirectional stream, and the handle owns that stream.
19//!
20//! `AnyConnection` carries only the handful of protocol methods whose
21//! arguments can be reconciled across every draft (`subscribe`, `fetch`,
22//! `track_status`, `subscribe_namespace`). The rest differ too much in
23//! signature — match on the variant to reach them.
24
25use std::sync::Arc;
26
27use moqtap_codec::kvp::KeyValuePair;
28use moqtap_codec::version::DraftVersion;
29
30/// Generates the `AnyConnection` and `AnyClientEvent` enums plus the per-draft
31/// observer adapter, with one variant per enabled draft feature.
32macro_rules! dispatch_all {
33 (
34 $(
35 #[cfg(feature = $feat:literal)]
36 $variant:ident => $module:ident,
37 )+
38 ) => {
39 /// A MoQT client connection of any enabled draft version.
40 ///
41 /// Wraps the draft-specific `Connection` type. Methods common to all
42 /// drafts are forwarded; for draft-specific protocol calls, match on
43 /// the variant.
44 pub enum AnyConnection {
45 $(
46 #[cfg(feature = $feat)]
47 #[doc = concat!("A draft-", $feat, " connection.")]
48 $variant(crate::$module::connection::Connection),
49 )+
50 }
51
52 impl AnyConnection {
53 /// Returns the draft version this connection is using.
54 #[allow(unreachable_code)]
55 pub fn draft(&self) -> DraftVersion {
56 match self {
57 $(
58 #[cfg(feature = $feat)]
59 Self::$variant(_) => DraftVersion::$variant,
60 )+
61 #[allow(unreachable_patterns)]
62 _ => unreachable!("AnyConnection has no enabled variants"),
63 }
64 }
65
66 /// Attach an observer. The observer is adapted into the
67 /// draft-specific observer trait and installed on the inner
68 /// connection; events are forwarded as [`AnyClientEvent`].
69 ///
70 /// Replaces any previously attached observer.
71 #[allow(unused_variables)]
72 pub fn set_observer(&mut self, observer: Arc<dyn AnyConnectionObserver>) {
73 match self {
74 $(
75 #[cfg(feature = $feat)]
76 Self::$variant(c) => {
77 c.set_observer(Box::new($variant::Adapter(observer)));
78 }
79 )+
80 #[allow(unreachable_patterns)]
81 _ => {}
82 }
83 }
84
85 /// Remove any attached observer.
86 pub fn clear_observer(&mut self) {
87 match self {
88 $(
89 #[cfg(feature = $feat)]
90 Self::$variant(c) => c.clear_observer(),
91 )+
92 #[allow(unreachable_patterns)]
93 _ => {}
94 }
95 }
96
97 /// Close the connection with the given application error code
98 /// and reason.
99 #[allow(unused_variables)]
100 pub fn close(&self, code: u32, reason: &[u8]) {
101 match self {
102 $(
103 #[cfg(feature = $feat)]
104 Self::$variant(c) => c.close(code, reason),
105 )+
106 #[allow(unreachable_patterns)]
107 _ => {}
108 }
109 }
110 }
111
112 /// An event from a MoQT connection of any enabled draft version.
113 ///
114 /// Event shapes differ across drafts (e.g. draft-17's
115 /// `SubgroupObjectReceived` carries header types, while earlier
116 /// drafts carry decoded objects). Match on the variant to inspect
117 /// the draft-specific event.
118 #[non_exhaustive]
119 #[derive(Debug, Clone)]
120 pub enum AnyClientEvent {
121 $(
122 #[cfg(feature = $feat)]
123 #[doc = concat!("A draft-", $feat, " event.")]
124 $variant(crate::$module::event::ClientEvent),
125 )+
126 }
127
128 impl AnyClientEvent {
129 /// Returns the draft version this event belongs to.
130 #[allow(unreachable_code)]
131 pub fn draft(&self) -> DraftVersion {
132 match self {
133 $(
134 #[cfg(feature = $feat)]
135 Self::$variant(_) => DraftVersion::$variant,
136 )+
137 #[allow(unreachable_patterns)]
138 _ => unreachable!("AnyClientEvent has no enabled variants"),
139 }
140 }
141 }
142
143 // Per-draft adapter modules. Each holds an `Adapter` struct that
144 // implements the draft's `ConnectionObserver` trait by forwarding to
145 // an `AnyConnectionObserver`.
146 $(
147 #[cfg(feature = $feat)]
148 #[allow(non_snake_case)]
149 mod $variant {
150 use super::{AnyClientEvent, AnyConnectionObserver};
151 use std::sync::Arc;
152
153 pub(super) struct Adapter(pub(super) Arc<dyn AnyConnectionObserver>);
154
155 impl crate::$module::observer::ConnectionObserver for Adapter {
156 fn on_event(&self, event: &crate::$module::event::ClientEvent) {
157 self.0.on_event(&AnyClientEvent::$variant(event.clone()));
158 }
159
160 fn on_event_owned(&self, event: crate::$module::event::ClientEvent) {
161 self.0.on_event(&AnyClientEvent::$variant(event));
162 }
163 }
164 }
165 )+
166 };
167}
168
169dispatch_all! {
170 #[cfg(feature = "draft07")]
171 Draft07 => draft07,
172 #[cfg(feature = "draft08")]
173 Draft08 => draft08,
174 #[cfg(feature = "draft09")]
175 Draft09 => draft09,
176 #[cfg(feature = "draft10")]
177 Draft10 => draft10,
178 #[cfg(feature = "draft11")]
179 Draft11 => draft11,
180 #[cfg(feature = "draft12")]
181 Draft12 => draft12,
182 #[cfg(feature = "draft13")]
183 Draft13 => draft13,
184 #[cfg(feature = "draft14")]
185 Draft14 => draft14,
186 #[cfg(feature = "draft15")]
187 Draft15 => draft15,
188 #[cfg(feature = "draft16")]
189 Draft16 => draft16,
190 #[cfg(feature = "draft17")]
191 Draft17 => draft17,
192 #[cfg(feature = "draft18")]
193 Draft18 => draft18,
194 #[cfg(feature = "draft19")]
195 Draft19 => draft19,
196 #[cfg(feature = "draft20")]
197 Draft20 => draft20,
198}
199
200/// Draft-agnostic transport choice for [`AnyConnection::connect`].
201#[derive(Debug, Clone)]
202pub enum AnyTransportType {
203 /// Raw QUIC via quinn. The `addr` passed to `connect` should be `host:port`.
204 Quic,
205 /// WebTransport via wtransport. The `url` is the WebTransport endpoint.
206 WebTransport {
207 /// The WebTransport endpoint URL (e.g., `https://host:port/path`).
208 url: String,
209 },
210}
211
212/// Draft-agnostic client configuration. The exact per-draft `ClientConfig`
213/// is constructed internally by [`AnyConnection::connect`] based on `draft`.
214///
215/// Fields that aren't meaningful for the selected draft are ignored:
216/// `additional_versions` is not carried by drafts 15–17 (single-version
217/// setup) and drafts 07–13 always offer their own draft first.
218#[derive(Debug, Clone)]
219pub struct AnyClientConfig {
220 /// Primary draft version for the connection.
221 pub draft: DraftVersion,
222 /// Additional draft versions to offer in CLIENT_SETUP.
223 pub additional_versions: Vec<DraftVersion>,
224 /// Transport type (QUIC or WebTransport).
225 pub transport: AnyTransportType,
226 /// Whether to skip TLS certificate verification (for testing).
227 pub skip_cert_verification: bool,
228 /// Custom CA certificates to trust (DER-encoded).
229 pub ca_certs: Vec<Vec<u8>>,
230 /// Setup parameters to include in CLIENT_SETUP (e.g., auth tokens).
231 pub setup_parameters: Vec<KeyValuePair>,
232}
233
234/// Error returned by [`AnyConnection::connect`] and
235/// [`AnyConnection::recv_and_dispatch`]. Draft-specific errors are flattened
236/// to strings so callers don't have to branch on draft to inspect errors.
237#[derive(Debug, thiserror::Error)]
238#[error("{0}")]
239pub struct AnyConnectionError(pub String);
240
241/// Where a FETCH's range ends, said once in a way no draft can read two ways.
242///
243/// The drafts do not agree about what a number in an "end object" field means,
244/// and the disagreement is silent: draft-19 Section 10.13 defines `End
245/// Location` as "the last Object, plus 1; or 0 to indicate the entire Group",
246/// while draft-20 Sections 5.1.2 and 10.13 make the `LOCATION_FILTER` range
247/// "inclusive" at both ends and delete both conventions without a note in the
248/// change log. The same `end_object = 10` therefore asks for objects 0 through
249/// 9 on one draft and 0 through 10 on the other, and nothing on the wire says
250/// which was meant.
251///
252/// So this enum, not a number. [`FetchEnd::Object`] is the last Object the
253/// fetch covers and the range **holds** it; [`FetchEnd::EntireGroup`] is
254/// draft-19's `0` spelled out. [`AnyConnection::fetch`] converts to whichever
255/// the negotiated draft writes.
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum FetchEnd {
258 /// Through this Object ID, **inclusive** — it is the last Object the fetch
259 /// covers, and the range holds it.
260 ///
261 /// `Object(0)` is a range ending at Object 0, one object long if it also
262 /// starts there. It is not "the whole group"; that is
263 /// [`FetchEnd::EntireGroup`].
264 Object(u64),
265 /// Through the last Object of the end Group, however many it turns out to
266 /// hold.
267 ///
268 /// Drafts 14-19 write this as `End Location.Object = 0`. Draft-20 writes it
269 /// as a three-field `LOCATION_FILTER`, which Section 5.1.2 defines as
270 /// covering all Objects in the end Group.
271 EntireGroup,
272}
273
274/// The range one [`AnyConnection::fetch`] asks for.
275///
276/// Four numbers on drafts 14 through 19, a `LOCATION_FILTER` parameter on
277/// draft-20, and one meaning here. The end Group is **absolute** on both sides
278/// of that split — draft-20's `EndGroupDelta` is derived from it, not asked for
279/// — and the end Object is [`FetchEnd`], which is the whole point of the type.
280///
281/// # Porting a draft-19 call
282///
283/// A caller that wrote `end_object` as "the last Object plus one" writes
284/// [`FetchEnd::Object`] with the last Object, and one that wrote `0` for a whole
285/// group writes [`FetchEnd::EntireGroup`]. Both send exactly the bytes they sent
286/// before on drafts 14 through 19, and the same request on draft-20.
287#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub struct FetchRange {
289 /// The Group the range starts in.
290 pub start_group: u64,
291 /// The Object within `start_group` the range starts at, inclusive.
292 pub start_object: u64,
293 /// The Group the range ends in, absolute. Must not be below
294 /// `start_group`: draft-20 encodes it as an unsigned delta from the start
295 /// and has no way to say "backwards".
296 pub end_group: u64,
297 /// Where the range stops inside `end_group`.
298 pub end: FetchEnd,
299}
300
301impl FetchRange {
302 /// A range ending at `end_object` in `end_group`, **inclusive** — that
303 /// Object is fetched.
304 pub fn through_object(
305 start_group: u64,
306 start_object: u64,
307 end_group: u64,
308 end_object: u64,
309 ) -> Self {
310 Self { start_group, start_object, end_group, end: FetchEnd::Object(end_object) }
311 }
312
313 /// A range covering every Object of `end_group`, however many there are.
314 pub fn through_end_of_group(start_group: u64, start_object: u64, end_group: u64) -> Self {
315 Self { start_group, start_object, end_group, end: FetchEnd::EntireGroup }
316 }
317
318 /// A range holding exactly one Object.
319 ///
320 /// The shape an off-by-one is loudest in: a draft-19 encoder writing
321 /// `end_object = object` rather than `object + 1` asks for nothing at all,
322 /// and one that ports that arithmetic to draft-20 unchanged asks for two.
323 pub fn one_object(group: u64, object: u64) -> Self {
324 Self::through_object(group, object, group, object)
325 }
326
327 /// The `End Location.Object` drafts 14 through 19 carry inline in FETCH.
328 ///
329 /// Their field is "The end Location, plus 1. A Location.Object value of 0
330 /// means the entire group is requested." — draft-19 Section 10.12.1, and
331 /// drafts 14 through 18 in the same words. So [`FetchEnd::Object`] gains
332 /// one here and
333 /// [`FetchEnd::EntireGroup`] is `0`. **This is the only place that `+ 1`
334 /// lives**, which is what keeps it from reaching draft-20.
335 ///
336 /// # Errors
337 ///
338 /// `FetchEnd::Object(u64::MAX)` has no encoding in that field — the plus
339 /// one leaves the number space — and is the one range these drafts cannot
340 /// express that draft-20 can. Refused rather than wrapped to `0`, which
341 /// would silently ask for the whole group.
342 pub fn inline_end_object(&self) -> Result<u64, AnyConnectionError> {
343 match self.end {
344 FetchEnd::EntireGroup => Ok(0),
345 FetchEnd::Object(last) => last.checked_add(1).ok_or_else(|| {
346 AnyConnectionError(format!(
347 "fetch: a range ending at Object {last} cannot be expressed on drafts 14 \
348 through 19, whose End Location.Object is the last Object plus 1"
349 ))
350 }),
351 }
352 }
353
354 /// The draft-20 `LOCATION_FILTER` that carries this range.
355 ///
356 /// Draft-20 Section 10.13 deleted `Start Location` and `End Location` from
357 /// FETCH and moved the range into the parameter, whose ranges Section 5.1.2
358 /// calls inclusive. So [`FetchEnd::Object`] is written **as it stands** —
359 /// nothing here adds one — and [`FetchEnd::EntireGroup`] becomes the
360 /// three-field filter, which Section 5.1.2 defines as covering all Objects
361 /// in the end Group. The end Group travels as `EndGroupDelta`, "delta
362 /// encoded from StartGroup", so it is `end_group - start_group`.
363 ///
364 /// # Errors
365 ///
366 /// [`AnyConnectionError`] when `end_group` is below `start_group`: the
367 /// delta is unsigned and there is no such filter. Drafts 14 through 19 have
368 /// two absolute fields and would put such a range on the wire, where the
369 /// publisher answers it with `INVALID_RANGE`; the difference is where the
370 /// refusal happens, not whether the fetch is legal.
371 #[cfg(feature = "draft20")]
372 pub fn location_filter(
373 &self,
374 ) -> Result<crate::draft20::fill::LocationFilter, AnyConnectionError> {
375 use crate::draft20::fill::LocationFilter;
376
377 let delta = self.end_group.checked_sub(self.start_group).ok_or_else(|| {
378 AnyConnectionError(format!(
379 "fetch: a range from Group {} to Group {} runs backwards, and draft-20 \
380 Section 5.1.2 encodes the end Group as an unsigned delta from the start",
381 self.start_group, self.end_group
382 ))
383 })?;
384 let filter = match self.end {
385 FetchEnd::EntireGroup => {
386 LocationFilter::range(self.start_group, self.start_object, delta)
387 }
388 FetchEnd::Object(last) => {
389 LocationFilter::range_to(self.start_group, self.start_object, delta, last)
390 }
391 };
392 filter.map_err(|e| AnyConnectionError(e.to_string()))
393 }
394}
395
396/// A request made through [`AnyConnection`], in whichever form the negotiated
397/// draft carries it.
398///
399/// Drafts 07-15 put every request on the single bidirectional control stream,
400/// so all a requester keeps is the request ID it was allocated. Draft-16 moved
401/// one request off it: Section 3.3 there names "two uses of bidirectional
402/// streams, the control stream, which begins with CLIENT_SETUP, and
403/// SUBSCRIBE_NAMESPACE", so a namespace subscription on that draft owns a
404/// stream and everything else does not. From draft-17 on every request does,
405/// and the stream *is* the correlation — responses on those drafts carry no
406/// request ID at all. The variants reflect that split rather than hiding it.
407///
408/// Hold on to this value for as long as the request is live. Dropping a
409/// per-request variant resets its stream, which the peer reads as a
410/// cancellation; dropping [`AnyRequest::ControlPlane`] does nothing, because
411/// there is no stream to reset.
412///
413/// The per-request variants are large — a `RequestStream` holds both halves of
414/// a QUIC stream — and `ControlPlane` is two small fields. That disparity is
415/// only visible when a single draft from 17 on is the only one enabled; with
416/// more than one, the largest variants are the same size as each other.
417/// Boxing them to close it would put an allocation on every request of every
418/// draft to flatter a build no released configuration uses.
419#[allow(clippy::large_enum_variant)]
420#[must_use = "dropping a request stream cancels the request"]
421pub enum AnyRequest {
422 /// Drafts 07-16: the request was written on the control stream and is
423 /// identified only by its request ID.
424 ControlPlane {
425 /// The request ID the endpoint allocated.
426 request_id: moqtap_codec::varint::VarInt,
427 /// The draft that allocated it.
428 draft: DraftVersion,
429 },
430 /// Draft-16: a namespace subscription, the one request on that draft that
431 /// owns a bidirectional stream. Every other draft-16 request comes back as
432 /// [`AnyRequest::ControlPlane`].
433 #[cfg(feature = "draft16")]
434 Draft16(crate::draft16::connection::NamespaceStream),
435 /// Draft-17: the request owns a bidirectional stream.
436 #[cfg(feature = "draft17")]
437 Draft17(crate::draft17::connection::RequestStream),
438 /// Draft-18: the request owns a bidirectional stream.
439 #[cfg(feature = "draft18")]
440 Draft18(crate::draft18::connection::RequestStream),
441 /// Draft-19: the request owns a bidirectional stream.
442 #[cfg(feature = "draft19")]
443 Draft19(crate::draft19::connection::RequestStream),
444 /// Draft-20: the request owns a bidirectional stream.
445 #[cfg(feature = "draft20")]
446 Draft20(crate::draft20::connection::RequestStream),
447}
448
449impl AnyRequest {
450 /// The request ID this request was allocated.
451 pub fn request_id(&self) -> moqtap_codec::varint::VarInt {
452 match self {
453 Self::ControlPlane { request_id, .. } => *request_id,
454 #[cfg(feature = "draft16")]
455 Self::Draft16(r) => r.request_id(),
456 #[cfg(feature = "draft17")]
457 Self::Draft17(r) => r.request_id(),
458 #[cfg(feature = "draft18")]
459 Self::Draft18(r) => r.request_id(),
460 #[cfg(feature = "draft19")]
461 Self::Draft19(r) => r.request_id(),
462 #[cfg(feature = "draft20")]
463 Self::Draft20(r) => r.request_id(),
464 }
465 }
466
467 /// The draft that carries this request.
468 pub fn draft(&self) -> DraftVersion {
469 match self {
470 Self::ControlPlane { draft, .. } => *draft,
471 #[cfg(feature = "draft16")]
472 Self::Draft16(r) => r.draft(),
473 #[cfg(feature = "draft17")]
474 Self::Draft17(r) => r.draft(),
475 #[cfg(feature = "draft18")]
476 Self::Draft18(r) => r.draft(),
477 #[cfg(feature = "draft19")]
478 Self::Draft19(r) => r.draft(),
479 #[cfg(feature = "draft20")]
480 Self::Draft20(r) => r.draft(),
481 }
482 }
483
484 /// The transport stream ID this request owns, or `None` on a draft that
485 /// carries requests on the shared control stream.
486 ///
487 /// This is the observable difference between the two variants: a caller
488 /// that needs to correlate a response by stream — which is the only
489 /// correlation drafts 17-19 offer — gets `Some` exactly when the draft
490 /// provides one.
491 pub fn stream_id(&self) -> Option<u64> {
492 match self {
493 Self::ControlPlane { .. } => None,
494 #[cfg(feature = "draft16")]
495 Self::Draft16(r) => Some(r.stream_id()),
496 #[cfg(feature = "draft17")]
497 Self::Draft17(r) => Some(r.stream_id()),
498 #[cfg(feature = "draft18")]
499 Self::Draft18(r) => Some(r.stream_id()),
500 #[cfg(feature = "draft19")]
501 Self::Draft19(r) => Some(r.stream_id()),
502 #[cfg(feature = "draft20")]
503 Self::Draft20(r) => Some(r.stream_id()),
504 }
505 }
506
507 /// Cancel the request by resetting its stream with `code`.
508 ///
509 /// Only a request that owns a stream can do this, which is a draft-16
510 /// namespace subscription and every request from draft-17 on. Cancelling a
511 /// request that lives on the control stream means sending a message
512 /// (UNSUBSCRIBE, FETCH_CANCEL, and so on), which needs the connection and
513 /// is therefore not reachable from the request handle alone. There this
514 /// refuses rather than silently doing nothing.
515 #[allow(unused_variables)]
516 pub fn cancel(&mut self, code: u64) -> Result<(), AnyConnectionError> {
517 match self {
518 Self::ControlPlane { draft, .. } => Err(AnyConnectionError(format!(
519 "cancel: draft {draft:?} carries this request on the control stream, so the \
520 request handle has no stream to reset; send the draft's own cancellation \
521 message instead"
522 ))),
523 #[cfg(feature = "draft16")]
524 Self::Draft16(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
525 #[cfg(feature = "draft17")]
526 Self::Draft17(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
527 #[cfg(feature = "draft18")]
528 Self::Draft18(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
529 #[cfg(feature = "draft19")]
530 Self::Draft19(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
531 #[cfg(feature = "draft20")]
532 Self::Draft20(r) => r.cancel(code).map_err(|e| AnyConnectionError(e.to_string())),
533 }
534 }
535}
536
537impl AnyConnection {
538 /// Connect to a MoQT server using the requested draft. Builds the
539 /// draft-specific `ClientConfig` from the provided [`AnyClientConfig`]
540 /// and dispatches to the appropriate `Connection::connect`.
541 pub async fn connect(addr: &str, config: AnyClientConfig) -> Result<Self, AnyConnectionError> {
542 // Every arm of the match below is `#[cfg(feature = "draftNN")]`. A build
543 // with no draft feature enabled keeps only the catch-all, which never
544 // dials anything, so `addr` is genuinely unread in exactly that build.
545 #[cfg(not(any(
546 feature = "draft07",
547 feature = "draft08",
548 feature = "draft09",
549 feature = "draft10",
550 feature = "draft11",
551 feature = "draft12",
552 feature = "draft13",
553 feature = "draft14",
554 feature = "draft15",
555 feature = "draft16",
556 feature = "draft17",
557 feature = "draft18",
558 feature = "draft19",
559 feature = "draft20"
560 )))]
561 let _ = addr;
562 match config.draft {
563 #[cfg(feature = "draft07")]
564 DraftVersion::Draft07 => {
565 use crate::draft07::connection::{ClientConfig, Connection, TransportType};
566 let transport = match config.transport {
567 AnyTransportType::Quic => TransportType::Quic,
568 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
569 };
570 let inner = ClientConfig {
571 additional_versions: config.additional_versions,
572 transport,
573 skip_cert_verification: config.skip_cert_verification,
574 ca_certs: config.ca_certs,
575 setup_parameters: config.setup_parameters,
576 };
577 let c = Connection::connect(addr, inner)
578 .await
579 .map_err(|e| AnyConnectionError(e.to_string()))?;
580 Ok(AnyConnection::Draft07(c))
581 }
582 #[cfg(feature = "draft08")]
583 DraftVersion::Draft08 => {
584 use crate::draft08::connection::{ClientConfig, Connection, TransportType};
585 let transport = match config.transport {
586 AnyTransportType::Quic => TransportType::Quic,
587 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
588 };
589 let inner = ClientConfig {
590 additional_versions: config.additional_versions,
591 transport,
592 skip_cert_verification: config.skip_cert_verification,
593 ca_certs: config.ca_certs,
594 setup_parameters: config.setup_parameters,
595 };
596 let c = Connection::connect(addr, inner)
597 .await
598 .map_err(|e| AnyConnectionError(e.to_string()))?;
599 Ok(AnyConnection::Draft08(c))
600 }
601 #[cfg(feature = "draft09")]
602 DraftVersion::Draft09 => {
603 use crate::draft09::connection::{ClientConfig, Connection, TransportType};
604 let transport = match config.transport {
605 AnyTransportType::Quic => TransportType::Quic,
606 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
607 };
608 let inner = ClientConfig {
609 additional_versions: config.additional_versions,
610 transport,
611 skip_cert_verification: config.skip_cert_verification,
612 ca_certs: config.ca_certs,
613 setup_parameters: config.setup_parameters,
614 };
615 let c = Connection::connect(addr, inner)
616 .await
617 .map_err(|e| AnyConnectionError(e.to_string()))?;
618 Ok(AnyConnection::Draft09(c))
619 }
620 #[cfg(feature = "draft10")]
621 DraftVersion::Draft10 => {
622 use crate::draft10::connection::{ClientConfig, Connection, TransportType};
623 let transport = match config.transport {
624 AnyTransportType::Quic => TransportType::Quic,
625 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
626 };
627 let inner = ClientConfig {
628 additional_versions: config.additional_versions,
629 transport,
630 skip_cert_verification: config.skip_cert_verification,
631 ca_certs: config.ca_certs,
632 setup_parameters: config.setup_parameters,
633 };
634 let c = Connection::connect(addr, inner)
635 .await
636 .map_err(|e| AnyConnectionError(e.to_string()))?;
637 Ok(AnyConnection::Draft10(c))
638 }
639 #[cfg(feature = "draft11")]
640 DraftVersion::Draft11 => {
641 use crate::draft11::connection::{ClientConfig, Connection, TransportType};
642 let transport = match config.transport {
643 AnyTransportType::Quic => TransportType::Quic,
644 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
645 };
646 let inner = ClientConfig {
647 additional_versions: config.additional_versions,
648 transport,
649 skip_cert_verification: config.skip_cert_verification,
650 ca_certs: config.ca_certs,
651 setup_parameters: config.setup_parameters,
652 };
653 let c = Connection::connect(addr, inner)
654 .await
655 .map_err(|e| AnyConnectionError(e.to_string()))?;
656 Ok(AnyConnection::Draft11(c))
657 }
658 #[cfg(feature = "draft12")]
659 DraftVersion::Draft12 => {
660 use crate::draft12::connection::{ClientConfig, Connection, TransportType};
661 let transport = match config.transport {
662 AnyTransportType::Quic => TransportType::Quic,
663 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
664 };
665 let inner = ClientConfig {
666 additional_versions: config.additional_versions,
667 transport,
668 skip_cert_verification: config.skip_cert_verification,
669 ca_certs: config.ca_certs,
670 setup_parameters: config.setup_parameters,
671 };
672 let c = Connection::connect(addr, inner)
673 .await
674 .map_err(|e| AnyConnectionError(e.to_string()))?;
675 Ok(AnyConnection::Draft12(c))
676 }
677 #[cfg(feature = "draft13")]
678 DraftVersion::Draft13 => {
679 use crate::draft13::connection::{ClientConfig, Connection, TransportType};
680 let transport = match config.transport {
681 AnyTransportType::Quic => TransportType::Quic,
682 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
683 };
684 let inner = ClientConfig {
685 additional_versions: config.additional_versions,
686 transport,
687 skip_cert_verification: config.skip_cert_verification,
688 ca_certs: config.ca_certs,
689 setup_parameters: config.setup_parameters,
690 };
691 let c = Connection::connect(addr, inner)
692 .await
693 .map_err(|e| AnyConnectionError(e.to_string()))?;
694 Ok(AnyConnection::Draft13(c))
695 }
696 #[cfg(feature = "draft14")]
697 DraftVersion::Draft14 => {
698 use crate::draft14::connection::{ClientConfig, Connection, TransportType};
699 let transport = match config.transport {
700 AnyTransportType::Quic => TransportType::Quic,
701 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
702 };
703 let inner = ClientConfig {
704 draft: config.draft,
705 additional_versions: config.additional_versions,
706 transport,
707 skip_cert_verification: config.skip_cert_verification,
708 ca_certs: config.ca_certs,
709 setup_parameters: config.setup_parameters,
710 };
711 let c = Connection::connect(addr, inner)
712 .await
713 .map_err(|e| AnyConnectionError(e.to_string()))?;
714 Ok(AnyConnection::Draft14(c))
715 }
716 #[cfg(feature = "draft15")]
717 DraftVersion::Draft15 => {
718 use crate::draft15::connection::{ClientConfig, Connection, TransportType};
719 let transport = match config.transport {
720 AnyTransportType::Quic => TransportType::Quic,
721 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
722 };
723 let inner = ClientConfig {
724 draft: config.draft,
725 transport,
726 skip_cert_verification: config.skip_cert_verification,
727 ca_certs: config.ca_certs,
728 setup_parameters: config.setup_parameters,
729 };
730 let c = Connection::connect(addr, inner)
731 .await
732 .map_err(|e| AnyConnectionError(e.to_string()))?;
733 Ok(AnyConnection::Draft15(c))
734 }
735 #[cfg(feature = "draft16")]
736 DraftVersion::Draft16 => {
737 use crate::draft16::connection::{ClientConfig, Connection, TransportType};
738 let transport = match config.transport {
739 AnyTransportType::Quic => TransportType::Quic,
740 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
741 };
742 let inner = ClientConfig {
743 draft: config.draft,
744 transport,
745 skip_cert_verification: config.skip_cert_verification,
746 ca_certs: config.ca_certs,
747 setup_parameters: config.setup_parameters,
748 };
749 let c = Connection::connect(addr, inner)
750 .await
751 .map_err(|e| AnyConnectionError(e.to_string()))?;
752 Ok(AnyConnection::Draft16(c))
753 }
754 #[cfg(feature = "draft17")]
755 DraftVersion::Draft17 => {
756 use crate::draft17::connection::{ClientConfig, Connection, TransportType};
757 let transport = match config.transport {
758 AnyTransportType::Quic => TransportType::Quic,
759 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
760 };
761 let inner = ClientConfig {
762 draft: config.draft,
763 transport,
764 skip_cert_verification: config.skip_cert_verification,
765 ca_certs: config.ca_certs,
766 setup_parameters: config.setup_parameters,
767 };
768 let c = Connection::connect(addr, inner)
769 .await
770 .map_err(|e| AnyConnectionError(e.to_string()))?;
771 Ok(AnyConnection::Draft17(c))
772 }
773 #[cfg(feature = "draft18")]
774 DraftVersion::Draft18 => {
775 use crate::draft18::connection::{ClientConfig, Connection, TransportType};
776 let transport = match config.transport {
777 AnyTransportType::Quic => TransportType::Quic,
778 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
779 };
780 let inner = ClientConfig {
781 draft: config.draft,
782 transport,
783 skip_cert_verification: config.skip_cert_verification,
784 ca_certs: config.ca_certs,
785 setup_parameters: config.setup_parameters,
786 };
787 let c = Connection::connect(addr, inner)
788 .await
789 .map_err(|e| AnyConnectionError(e.to_string()))?;
790 Ok(AnyConnection::Draft18(c))
791 }
792 #[cfg(feature = "draft19")]
793 DraftVersion::Draft19 => {
794 use crate::draft19::connection::{ClientConfig, Connection, TransportType};
795 let transport = match config.transport {
796 AnyTransportType::Quic => TransportType::Quic,
797 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
798 };
799 let inner = ClientConfig {
800 draft: config.draft,
801 transport,
802 skip_cert_verification: config.skip_cert_verification,
803 ca_certs: config.ca_certs,
804 setup_parameters: config.setup_parameters,
805 };
806 let c = Connection::connect(addr, inner)
807 .await
808 .map_err(|e| AnyConnectionError(e.to_string()))?;
809 Ok(AnyConnection::Draft19(c))
810 }
811 #[cfg(feature = "draft20")]
812 DraftVersion::Draft20 => {
813 use crate::draft20::connection::{ClientConfig, Connection, TransportType};
814 let transport = match config.transport {
815 AnyTransportType::Quic => TransportType::Quic,
816 AnyTransportType::WebTransport { url } => TransportType::WebTransport { url },
817 };
818 let inner = ClientConfig {
819 draft: config.draft,
820 transport,
821 skip_cert_verification: config.skip_cert_verification,
822 ca_certs: config.ca_certs,
823 setup_parameters: config.setup_parameters,
824 };
825 let c = Connection::connect(addr, inner)
826 .await
827 .map_err(|e| AnyConnectionError(e.to_string()))?;
828 Ok(AnyConnection::Draft20(c))
829 }
830 #[allow(unreachable_patterns)]
831 other => Err(AnyConnectionError(format!("draft {other:?} not enabled in this build",))),
832 }
833 }
834
835 /// Read and dispatch one control message on the active draft. Draft-specific
836 /// control-message return values are discarded because event delivery goes
837 /// through the attached observer; callers only care about success/failure.
838 pub async fn recv_and_dispatch(&mut self) -> Result<(), AnyConnectionError> {
839 match self {
840 #[cfg(feature = "draft07")]
841 Self::Draft07(c) => c
842 .recv_and_dispatch()
843 .await
844 .map(|_| ())
845 .map_err(|e| AnyConnectionError(e.to_string())),
846 #[cfg(feature = "draft08")]
847 Self::Draft08(c) => c
848 .recv_and_dispatch()
849 .await
850 .map(|_| ())
851 .map_err(|e| AnyConnectionError(e.to_string())),
852 #[cfg(feature = "draft09")]
853 Self::Draft09(c) => c
854 .recv_and_dispatch()
855 .await
856 .map(|_| ())
857 .map_err(|e| AnyConnectionError(e.to_string())),
858 #[cfg(feature = "draft10")]
859 Self::Draft10(c) => c
860 .recv_and_dispatch()
861 .await
862 .map(|_| ())
863 .map_err(|e| AnyConnectionError(e.to_string())),
864 #[cfg(feature = "draft11")]
865 Self::Draft11(c) => c
866 .recv_and_dispatch()
867 .await
868 .map(|_| ())
869 .map_err(|e| AnyConnectionError(e.to_string())),
870 #[cfg(feature = "draft12")]
871 Self::Draft12(c) => c
872 .recv_and_dispatch()
873 .await
874 .map(|_| ())
875 .map_err(|e| AnyConnectionError(e.to_string())),
876 #[cfg(feature = "draft13")]
877 Self::Draft13(c) => c
878 .recv_and_dispatch()
879 .await
880 .map(|_| ())
881 .map_err(|e| AnyConnectionError(e.to_string())),
882 #[cfg(feature = "draft14")]
883 Self::Draft14(c) => c
884 .recv_and_dispatch()
885 .await
886 .map(|_| ())
887 .map_err(|e| AnyConnectionError(e.to_string())),
888 #[cfg(feature = "draft15")]
889 Self::Draft15(c) => c
890 .recv_and_dispatch()
891 .await
892 .map(|_| ())
893 .map_err(|e| AnyConnectionError(e.to_string())),
894 #[cfg(feature = "draft16")]
895 Self::Draft16(c) => c
896 .recv_and_dispatch()
897 .await
898 .map(|_| ())
899 .map_err(|e| AnyConnectionError(e.to_string())),
900 #[cfg(feature = "draft17")]
901 Self::Draft17(c) => c
902 .recv_and_dispatch()
903 .await
904 .map(|_| ())
905 .map_err(|e| AnyConnectionError(e.to_string())),
906 #[cfg(feature = "draft18")]
907 Self::Draft18(c) => c
908 .recv_and_dispatch()
909 .await
910 .map(|_| ())
911 .map_err(|e| AnyConnectionError(e.to_string())),
912 #[cfg(feature = "draft19")]
913 Self::Draft19(c) => c
914 .recv_and_dispatch()
915 .await
916 .map(|_| ())
917 .map_err(|e| AnyConnectionError(e.to_string())),
918 #[cfg(feature = "draft20")]
919 Self::Draft20(c) => c
920 .recv_and_dispatch()
921 .await
922 .map(|_| ())
923 .map_err(|e| AnyConnectionError(e.to_string())),
924 #[allow(unreachable_patterns)]
925 _ => Err(AnyConnectionError("AnyConnection has no enabled variants".into())),
926 }
927 }
928
929 // ── Unified control-message helpers ──────────────────────────────────
930 //
931 // Draft-agnostic shorthands. Each dispatches to the active variant and
932 // defaults fields not expressible in the unified shape; drafts that
933 // lack the operation return an `AnyConnectionError`. Match on the
934 // variant directly when full per-draft control is needed.
935
936 /// Send an UNSUBSCRIBE for the given request ID. Drafts 07 through 16.
937 ///
938 /// # Drafts 17 through 20 have no such message
939 ///
940 /// Draft-17 deleted UNSUBSCRIBE, and drafts 18, 19 and 20 keep it deleted:
941 /// a subscriber ends a subscription by **resetting its request stream**,
942 /// which is [`AnyRequest::cancel`], or waits for PUBLISH_DONE. So the error
943 /// those four return is not a gap to be filled later — there is nothing to
944 /// wire — and a caller reaching for it on one of them wants `cancel` on the
945 /// handle `subscribe` returned.
946 #[allow(unused_variables)]
947 pub async fn unsubscribe(
948 &mut self,
949 request_id: moqtap_codec::varint::VarInt,
950 ) -> Result<(), AnyConnectionError> {
951 match self {
952 #[cfg(feature = "draft07")]
953 Self::Draft07(c) => {
954 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
955 }
956 #[cfg(feature = "draft08")]
957 Self::Draft08(c) => {
958 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
959 }
960 #[cfg(feature = "draft09")]
961 Self::Draft09(c) => {
962 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
963 }
964 #[cfg(feature = "draft10")]
965 Self::Draft10(c) => {
966 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
967 }
968 #[cfg(feature = "draft11")]
969 Self::Draft11(c) => {
970 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
971 }
972 #[cfg(feature = "draft12")]
973 Self::Draft12(c) => {
974 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
975 }
976 #[cfg(feature = "draft13")]
977 Self::Draft13(c) => {
978 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
979 }
980 #[cfg(feature = "draft14")]
981 Self::Draft14(c) => {
982 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
983 }
984 #[cfg(feature = "draft15")]
985 Self::Draft15(c) => {
986 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
987 }
988 #[cfg(feature = "draft16")]
989 Self::Draft16(c) => {
990 c.unsubscribe(request_id).await.map_err(|e| AnyConnectionError(e.to_string()))
991 }
992 #[allow(unreachable_patterns)]
993 other => Err(AnyConnectionError(format!(
994 "unsubscribe: not yet wired up for draft {:?} via AnyConnection",
995 other.draft()
996 ))),
997 }
998 }
999
1000 /// Send a SUBSCRIBE with the given filter, priority, and group order.
1001 /// Supported on drafts 12 through 20. Drafts 15 onward carry
1002 /// priority/order/filter as parameters rather than fields; this helper
1003 /// passes an empty parameter list, so on those drafts all three take the
1004 /// protocol default and the three arguments here are ignored.
1005 ///
1006 /// There is no range to get wrong: a subscription with no filter is one
1007 /// that starts where the draft says it starts, and nothing is converted.
1008 /// A draft-20 caller that wants a Location filter, a fill, or anything else
1009 /// from Section 10.2 reaches
1010 /// [`draft20::connection::Connection::subscribe`](crate::draft20::connection::Connection::subscribe)
1011 /// through the variant with the parameters it wants.
1012 ///
1013 /// The returned [`AnyRequest`] must be held while the request is live: on
1014 /// drafts 17-20 it owns the bidirectional stream the request went out on
1015 /// and dropping it cancels the subscription. See [`AnyRequest`] for how
1016 /// the two kinds of handle differ.
1017 #[allow(unused_variables)]
1018 pub async fn subscribe(
1019 &mut self,
1020 namespace: moqtap_codec::types::TrackNamespace,
1021 track_name: Vec<u8>,
1022 subscriber_priority: u8,
1023 group_order: moqtap_codec::types::GroupOrder,
1024 filter_type: moqtap_codec::types::FilterType,
1025 ) -> Result<AnyRequest, AnyConnectionError> {
1026 // Only the draft-12 arm below converts `filter_type` by hand: draft-12
1027 // draws that field as a variable-length integer, where the drafts on
1028 // either side of it take the typed value straight through. So the
1029 // import is dead outside that build.
1030 #[cfg(feature = "draft12")]
1031 use moqtap_codec::varint::VarInt;
1032 let draft = self.draft();
1033 match self {
1034 #[cfg(feature = "draft12")]
1035 Self::Draft12(c) => {
1036 let ft = VarInt::from_u64(filter_type as u64)
1037 .map_err(|e| AnyConnectionError(e.to_string()))?;
1038 c.subscribe(namespace, track_name, subscriber_priority, group_order, ft, Vec::new())
1039 .await
1040 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1041 .map_err(|e| AnyConnectionError(e.to_string()))
1042 }
1043 #[cfg(feature = "draft13")]
1044 Self::Draft13(c) => c
1045 .subscribe(
1046 namespace,
1047 track_name,
1048 subscriber_priority,
1049 group_order,
1050 filter_type,
1051 Vec::new(),
1052 )
1053 .await
1054 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1055 .map_err(|e| AnyConnectionError(e.to_string())),
1056 #[cfg(feature = "draft14")]
1057 Self::Draft14(c) => c
1058 .subscribe(
1059 namespace,
1060 track_name,
1061 subscriber_priority,
1062 group_order,
1063 filter_type,
1064 Vec::new(),
1065 )
1066 .await
1067 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1068 .map_err(|e| AnyConnectionError(e.to_string())),
1069 #[cfg(feature = "draft15")]
1070 Self::Draft15(c) => c
1071 .subscribe(namespace, track_name, Vec::new())
1072 .await
1073 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1074 .map_err(|e| AnyConnectionError(e.to_string())),
1075 #[cfg(feature = "draft16")]
1076 Self::Draft16(c) => c
1077 .subscribe(namespace, track_name, Vec::new())
1078 .await
1079 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1080 .map_err(|e| AnyConnectionError(e.to_string())),
1081 #[cfg(feature = "draft17")]
1082 Self::Draft17(c) => c
1083 .subscribe(namespace, track_name, Vec::new())
1084 .await
1085 .map(AnyRequest::Draft17)
1086 .map_err(|e| AnyConnectionError(e.to_string())),
1087 #[cfg(feature = "draft18")]
1088 Self::Draft18(c) => c
1089 .subscribe(namespace, track_name, Vec::new())
1090 .await
1091 .map(AnyRequest::Draft18)
1092 .map_err(|e| AnyConnectionError(e.to_string())),
1093 #[cfg(feature = "draft19")]
1094 Self::Draft19(c) => c
1095 .subscribe(namespace, track_name, Vec::new())
1096 .await
1097 .map(AnyRequest::Draft19)
1098 .map_err(|e| AnyConnectionError(e.to_string())),
1099 #[cfg(feature = "draft20")]
1100 Self::Draft20(c) => c
1101 .subscribe(namespace, track_name, Vec::new())
1102 .await
1103 .map(AnyRequest::Draft20)
1104 .map_err(|e| AnyConnectionError(e.to_string())),
1105 #[allow(unreachable_patterns)]
1106 other => Err(AnyConnectionError(format!(
1107 "subscribe: not yet wired up for draft {:?} via AnyConnection",
1108 other.draft()
1109 ))),
1110 }
1111 }
1112
1113 /// Send a standalone FETCH for `range`. Wired on drafts 14 through 20;
1114 /// every earlier draft returns the error at the end of the match.
1115 ///
1116 /// # What the range means here
1117 ///
1118 /// **[`FetchEnd::Object`] is the last Object the fetch covers, and the
1119 /// range holds it. [`FetchEnd::EntireGroup`] covers the whole end Group.
1120 /// `end_group` is absolute.** That is the whole contract, and it is stated
1121 /// in [`FetchEnd`] as well because it is the one thing about this call a
1122 /// caller can get wrong without being told.
1123 ///
1124 /// The drafts do not agree, which is why the argument is a
1125 /// [`FetchRange`] rather than four numbers. Drafts 14 through 19 carry
1126 /// `Start Location` and `End Location` inline in FETCH, and all six word
1127 /// the end alike — draft-19 Section 10.12.1: "The end Location, plus 1. A
1128 /// Location.Object value of 0 means the entire group is requested." The
1129 /// section number moves between drafts; the sentence does not.
1130 /// Draft-20 Section 10.13 deleted both fields and
1131 /// moved the range into the `LOCATION_FILTER` parameter, whose ranges
1132 /// Section 5.1.2 calls **inclusive** — the `+ 1` and the `0`-means-whole-
1133 /// group convention are both gone, and neither deletion is in the draft's
1134 /// own change log. A single `end_object: u64` at this boundary would have
1135 /// meant one of those two things and looked like the other.
1136 ///
1137 /// The conversion is [`FetchRange::inline_end_object`] for the first group
1138 /// and [`FetchRange::location_filter`] for draft-20. **The `+ 1` exists in
1139 /// exactly one place**, the first of those, so it cannot reach draft-20 by
1140 /// being ported.
1141 ///
1142 /// # What this does not carry
1143 ///
1144 /// An empty parameter list, on every draft. Draft-14's subscriber priority
1145 /// and group order are fields of its FETCH and of no later draft's, so they
1146 /// are sent as they always were. Reach a draft's own `Connection::fetch`
1147 /// through the variant for anything past a plain range.
1148 ///
1149 /// # Errors
1150 ///
1151 /// A range the negotiated draft cannot express, before anything is written:
1152 /// `FetchEnd::Object(u64::MAX)` on drafts 14 through 19, and an `end_group`
1153 /// below `start_group` on draft-20. See the two conversions for why each is
1154 /// inexpressible rather than merely unusual.
1155 ///
1156 /// The returned [`AnyRequest`] must be held while the request is live: on
1157 /// drafts 17-20 it owns the bidirectional stream the request went out on
1158 /// and dropping it cancels the fetch.
1159 #[allow(unused_variables)]
1160 pub async fn fetch(
1161 &mut self,
1162 namespace: moqtap_codec::types::TrackNamespace,
1163 track_name: Vec<u8>,
1164 range: FetchRange,
1165 ) -> Result<AnyRequest, AnyConnectionError> {
1166 // The three fields drafts 14 through 19 carry unchanged, built once for
1167 // whichever arm runs. `from_u64_moqt` rather than `from_u64` because
1168 // MoQT's varint reaches the full 64-bit range (Section 1.4.1) and the
1169 // newtype *is* the value: a `VarInt` a caller could have handed the old
1170 // four-argument form is the same `VarInt` this makes, so every draft
1171 // that was wired before writes the bytes it wrote before. The fourth
1172 // field is the one that differs, and it is built inside each arm from
1173 // `inline_end_object`.
1174 let start_group = moqtap_codec::varint::VarInt::from_u64_moqt(range.start_group);
1175 let start_object = moqtap_codec::varint::VarInt::from_u64_moqt(range.start_object);
1176 let end_group = moqtap_codec::varint::VarInt::from_u64_moqt(range.end_group);
1177 // The fourth field, which is the one that differs: draft-19 Section
1178 // 10.13 makes `End Location.Object` the last Object plus 1, or 0 for
1179 // the whole Group. Built here so the six arms that need it stay
1180 // identical, and lazily so that draft-20 — which can express a range
1181 // those six cannot — is not refused on their behalf.
1182 let inline_end_object = || -> Result<moqtap_codec::varint::VarInt, AnyConnectionError> {
1183 Ok(moqtap_codec::varint::VarInt::from_u64_moqt(range.inline_end_object()?))
1184 };
1185 let draft = self.draft();
1186 match self {
1187 #[cfg(feature = "draft14")]
1188 Self::Draft14(c) => c
1189 .fetch(
1190 namespace,
1191 track_name,
1192 // The priority and the order are fields of draft-14's FETCH
1193 // and of no later draft's, so this entry point does not
1194 // carry them and sends what it always sent. The end of the
1195 // range it does carry, and used to drop here.
1196 128,
1197 moqtap_codec::types::GroupOrder::Ascending,
1198 start_group,
1199 start_object,
1200 end_group,
1201 inline_end_object()?,
1202 Vec::new(),
1203 )
1204 .await
1205 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1206 .map_err(|e| AnyConnectionError(e.to_string())),
1207 #[cfg(feature = "draft15")]
1208 Self::Draft15(c) => c
1209 .fetch(
1210 namespace,
1211 track_name,
1212 start_group,
1213 start_object,
1214 end_group,
1215 inline_end_object()?,
1216 Vec::new(),
1217 )
1218 .await
1219 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1220 .map_err(|e| AnyConnectionError(e.to_string())),
1221 #[cfg(feature = "draft16")]
1222 Self::Draft16(c) => c
1223 .fetch(
1224 namespace,
1225 track_name,
1226 start_group,
1227 start_object,
1228 end_group,
1229 inline_end_object()?,
1230 Vec::new(),
1231 )
1232 .await
1233 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1234 .map_err(|e| AnyConnectionError(e.to_string())),
1235 #[cfg(feature = "draft17")]
1236 Self::Draft17(c) => c
1237 .fetch(
1238 namespace,
1239 track_name,
1240 start_group,
1241 start_object,
1242 end_group,
1243 inline_end_object()?,
1244 Vec::new(),
1245 )
1246 .await
1247 .map(AnyRequest::Draft17)
1248 .map_err(|e| AnyConnectionError(e.to_string())),
1249 #[cfg(feature = "draft18")]
1250 Self::Draft18(c) => c
1251 .fetch(
1252 namespace,
1253 track_name,
1254 start_group,
1255 start_object,
1256 end_group,
1257 inline_end_object()?,
1258 Vec::new(),
1259 )
1260 .await
1261 .map(AnyRequest::Draft18)
1262 .map_err(|e| AnyConnectionError(e.to_string())),
1263 #[cfg(feature = "draft19")]
1264 Self::Draft19(c) => c
1265 .fetch(
1266 namespace,
1267 track_name,
1268 start_group,
1269 start_object,
1270 end_group,
1271 inline_end_object()?,
1272 Vec::new(),
1273 )
1274 .await
1275 .map(AnyRequest::Draft19)
1276 .map_err(|e| AnyConnectionError(e.to_string())),
1277 #[cfg(feature = "draft20")]
1278 Self::Draft20(c) => {
1279 // Draft-20 Section 10.13 has no location fields to fill in:
1280 // `fetch_range` puts the whole range in the `LOCATION_FILTER`
1281 // parameter, at the position ascending Parameter Type order
1282 // requires. Nothing here adds one to the end — Section 5.1.2
1283 // makes the filter's range inclusive, and the `+ 1` the six
1284 // arms above apply lives in `inline_end_object` alone.
1285 let filter = range.location_filter()?;
1286 c.fetch_range(namespace, track_name, &filter, Vec::new())
1287 .await
1288 .map(AnyRequest::Draft20)
1289 .map_err(|e| AnyConnectionError(e.to_string()))
1290 }
1291 #[allow(unreachable_patterns)]
1292 other => Err(AnyConnectionError(format!(
1293 "fetch: not yet wired up for draft {:?} via AnyConnection",
1294 other.draft()
1295 ))),
1296 }
1297 }
1298
1299 /// Send a TRACK_STATUS query for the given track. Supported on drafts 14
1300 /// through 20. From draft-15 on, passes an empty parameter list.
1301 ///
1302 /// The returned [`AnyRequest`] must be held until the answer arrives: on
1303 /// drafts 17-20 it owns the bidirectional stream the query went out on and
1304 /// dropping it cancels the query.
1305 #[allow(unused_variables)]
1306 pub async fn track_status(
1307 &mut self,
1308 namespace: moqtap_codec::types::TrackNamespace,
1309 track_name: Vec<u8>,
1310 ) -> Result<AnyRequest, AnyConnectionError> {
1311 let draft = self.draft();
1312 match self {
1313 #[cfg(feature = "draft14")]
1314 Self::Draft14(c) => c
1315 .track_status(
1316 namespace,
1317 track_name,
1318 // Draft-14 words TRACK_STATUS like a SUBSCRIBE and no
1319 // later draft does, so these four are sent as they always
1320 // were rather than widening an entry point shared with six
1321 // drafts that have no such fields.
1322 128,
1323 moqtap_codec::types::GroupOrder::Ascending,
1324 moqtap_codec::types::Forward::Forward,
1325 moqtap_codec::types::FilterType::LargestObject,
1326 Vec::new(),
1327 )
1328 .await
1329 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1330 .map_err(|e| AnyConnectionError(e.to_string())),
1331 #[cfg(feature = "draft15")]
1332 Self::Draft15(c) => c
1333 .track_status(namespace, track_name, Vec::new())
1334 .await
1335 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1336 .map_err(|e| AnyConnectionError(e.to_string())),
1337 #[cfg(feature = "draft16")]
1338 Self::Draft16(c) => c
1339 .track_status(namespace, track_name, Vec::new())
1340 .await
1341 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1342 .map_err(|e| AnyConnectionError(e.to_string())),
1343 #[cfg(feature = "draft17")]
1344 Self::Draft17(c) => c
1345 .track_status(namespace, track_name, Vec::new())
1346 .await
1347 .map(AnyRequest::Draft17)
1348 .map_err(|e| AnyConnectionError(e.to_string())),
1349 #[cfg(feature = "draft18")]
1350 Self::Draft18(c) => c
1351 .track_status(namespace, track_name, Vec::new())
1352 .await
1353 .map(AnyRequest::Draft18)
1354 .map_err(|e| AnyConnectionError(e.to_string())),
1355 #[cfg(feature = "draft19")]
1356 Self::Draft19(c) => c
1357 .track_status(namespace, track_name, Vec::new())
1358 .await
1359 .map(AnyRequest::Draft19)
1360 .map_err(|e| AnyConnectionError(e.to_string())),
1361 #[cfg(feature = "draft20")]
1362 Self::Draft20(c) => c
1363 .track_status(namespace, track_name, Vec::new())
1364 .await
1365 .map(AnyRequest::Draft20)
1366 .map_err(|e| AnyConnectionError(e.to_string())),
1367 #[allow(unreachable_patterns)]
1368 other => Err(AnyConnectionError(format!(
1369 "track_status: not yet wired up for draft {:?} via AnyConnection",
1370 other.draft()
1371 ))),
1372 }
1373 }
1374
1375 /// Send a SUBSCRIBE_NAMESPACE (or SUBSCRIBE_ANNOUNCES on drafts 11–12).
1376 /// Supported on drafts 11 through 20. Drafts 16 and 17 pass default
1377 /// subscribe options; every draft from 12 on passes an empty parameter
1378 /// list.
1379 ///
1380 /// From draft-18 this is the renumbered SUBSCRIBE_NAMESPACE (0x50), which
1381 /// asks for NAMESPACE and NAMESPACE_DONE only. SUBSCRIBE_TRACKS, the other
1382 /// half of the draft-18 split, has no entry point here — reach a draft's
1383 /// own `Connection::subscribe_tracks` through the variant.
1384 ///
1385 /// The returned [`AnyRequest`] must be held while the request is live: on
1386 /// drafts 17-20 it owns the bidirectional stream the request went out on
1387 /// and dropping it cancels the namespace subscription.
1388 #[allow(unused_variables)]
1389 pub async fn subscribe_namespace(
1390 &mut self,
1391 namespace_prefix: moqtap_codec::types::TrackNamespace,
1392 ) -> Result<AnyRequest, AnyConnectionError> {
1393 // Only the draft-16 and draft-17 arms below build a subscribe-options
1394 // varint; the other drafts' wrappers take no such argument, so the
1395 // import is dead outside those two builds.
1396 #[cfg(any(feature = "draft16", feature = "draft17"))]
1397 use moqtap_codec::varint::VarInt;
1398 let draft = self.draft();
1399 match self {
1400 #[cfg(feature = "draft11")]
1401 Self::Draft11(c) => c
1402 .subscribe_announces(namespace_prefix)
1403 .await
1404 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1405 .map_err(|e| AnyConnectionError(e.to_string())),
1406 #[cfg(feature = "draft12")]
1407 Self::Draft12(c) => c
1408 .subscribe_announces(namespace_prefix, Vec::new())
1409 .await
1410 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1411 .map_err(|e| AnyConnectionError(e.to_string())),
1412 #[cfg(feature = "draft13")]
1413 Self::Draft13(c) => c
1414 .subscribe_namespace(namespace_prefix, Vec::new())
1415 .await
1416 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1417 .map_err(|e| AnyConnectionError(e.to_string())),
1418 #[cfg(feature = "draft14")]
1419 Self::Draft14(c) => c
1420 .subscribe_namespace(namespace_prefix, Vec::new())
1421 .await
1422 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1423 .map_err(|e| AnyConnectionError(e.to_string())),
1424 #[cfg(feature = "draft15")]
1425 Self::Draft15(c) => c
1426 .subscribe_namespace(namespace_prefix, Vec::new())
1427 .await
1428 .map(|request_id| AnyRequest::ControlPlane { request_id, draft })
1429 .map_err(|e| AnyConnectionError(e.to_string())),
1430 #[cfg(feature = "draft16")]
1431 Self::Draft16(c) => {
1432 let opts = VarInt::from_u64(0).expect("0 fits in VarInt");
1433 c.subscribe_namespace(namespace_prefix, opts, Vec::new())
1434 .await
1435 .map(AnyRequest::Draft16)
1436 .map_err(|e| AnyConnectionError(e.to_string()))
1437 }
1438 #[cfg(feature = "draft17")]
1439 Self::Draft17(c) => {
1440 let opts = VarInt::from_u64(0).expect("0 fits in VarInt");
1441 c.subscribe_namespace(namespace_prefix, opts, Vec::new())
1442 .await
1443 .map(AnyRequest::Draft17)
1444 .map_err(|e| AnyConnectionError(e.to_string()))
1445 }
1446 #[cfg(feature = "draft18")]
1447 Self::Draft18(c) => c
1448 .subscribe_namespace(namespace_prefix, Vec::new())
1449 .await
1450 .map(AnyRequest::Draft18)
1451 .map_err(|e| AnyConnectionError(e.to_string())),
1452 #[cfg(feature = "draft19")]
1453 Self::Draft19(c) => c
1454 .subscribe_namespace(namespace_prefix, Vec::new())
1455 .await
1456 .map(AnyRequest::Draft19)
1457 .map_err(|e| AnyConnectionError(e.to_string())),
1458 #[cfg(feature = "draft20")]
1459 Self::Draft20(c) => c
1460 .subscribe_namespace(namespace_prefix, Vec::new())
1461 .await
1462 .map(AnyRequest::Draft20)
1463 .map_err(|e| AnyConnectionError(e.to_string())),
1464 #[allow(unreachable_patterns)]
1465 other => Err(AnyConnectionError(format!(
1466 "subscribe_namespace: not yet wired up for draft {:?} via AnyConnection",
1467 other.draft()
1468 ))),
1469 }
1470 }
1471
1472 /// Send a SUBSCRIBE_UPDATE for an active subscription. Draft-14 only.
1473 ///
1474 /// # Why no later draft is wired, and why that is not this call's to fix
1475 ///
1476 /// Draft-15 renamed the message REQUEST_UPDATE and rebuilt it, and from
1477 /// draft-17 it travels **on the request's own bidirectional stream** rather
1478 /// than on a control stream — so an update needs the [`AnyRequest`] the
1479 /// original request returned, which this signature does not take and cannot
1480 /// be given without becoming a different call. Draft-20 goes further and
1481 /// has no `start_location` / `end_group` pair at all: Section 10.9 carries
1482 /// the new range as a `LOCATION_FILTER` parameter, on the same inclusive
1483 /// terms [`FetchRange`] describes. Reach a draft's own
1484 /// `Connection::send_on_request_stream` through the variant.
1485 #[allow(unused_variables)]
1486 pub async fn subscribe_update(
1487 &mut self,
1488 subscription_request_id: moqtap_codec::varint::VarInt,
1489 start_location: moqtap_codec::types::Location,
1490 end_group: moqtap_codec::varint::VarInt,
1491 subscriber_priority: u8,
1492 forward: moqtap_codec::types::Forward,
1493 ) -> Result<(), AnyConnectionError> {
1494 match self {
1495 #[cfg(feature = "draft14")]
1496 Self::Draft14(c) => c
1497 .subscribe_update(
1498 subscription_request_id,
1499 start_location,
1500 end_group,
1501 subscriber_priority,
1502 forward,
1503 Vec::new(),
1504 )
1505 .await
1506 .map(|_| ())
1507 .map_err(|e| AnyConnectionError(e.to_string())),
1508 #[allow(unreachable_patterns)]
1509 other => Err(AnyConnectionError(format!(
1510 "subscribe_update: not yet wired up for draft {:?} via AnyConnection",
1511 other.draft()
1512 ))),
1513 }
1514 }
1515}
1516
1517/// Trait for receiving events from an [`AnyConnection`].
1518///
1519/// Implementations must be `Send + Sync` because the adapter installed on
1520/// the inner draft-specific connection may emit events from async tasks.
1521/// `on_event` takes `&self` — implementations that need mutation should use
1522/// interior mutability (e.g. `Mutex`, `mpsc::Sender`).
1523///
1524/// The per-draft adapter clones the draft-specific event into the matching
1525/// [`AnyClientEvent`] variant before invoking `on_event`.
1526pub trait AnyConnectionObserver: Send + Sync {
1527 /// Called when a connection event occurs on any draft.
1528 fn on_event(&self, event: &AnyClientEvent);
1529}
1530
1531/// A no-op observer that discards all events.
1532pub struct NoOpObserver;
1533
1534impl AnyConnectionObserver for NoOpObserver {
1535 fn on_event(&self, _event: &AnyClientEvent) {}
1536}