Skip to main content

dash_runtime/client/
error.rs

1//! Failure vocabulary: what went wrong with a fetch ([`Failure`]), how the
2//! core classifies it ([`FailureClass`]) and the errors the `on_*` calls
3//! return ([`Error`]).
4
5use broadcast_common::impl_spec_display;
6use thiserror::Error;
7
8use super::action::{ResourceId, UtcId};
9use crate::error::MpdError;
10
11/// How a fetch failed, as the caller observed it.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[non_exhaustive]
14pub enum Failure {
15    /// The server answered with this HTTP status (an error status, or a
16    /// success status the caller could not use).
17    Status(u16),
18    /// Connection refused, reset, or closed before the response completed.
19    Transport,
20    /// No (complete) response within the caller's deadline.
21    Timeout,
22    /// The body exceeded the caller's size cap.
23    TooLarge,
24}
25
26/// The cause the core attributes to a counted failure.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum FailureKind {
30    /// 404 or 410: the resource is not there.
31    Missing,
32    /// 425 (RFC 8470 Too Early).
33    TooEarly,
34    /// Any other error status.
35    Server,
36    /// Transport error or timeout.
37    Transport,
38    /// A body arrived that is not usable (not media, wrong length, rejected
39    /// by the demuxer, oversize).
40    Invalid,
41}
42
43impl FailureKind {
44    /// Stable snake_case label.
45    #[must_use]
46    pub fn name(&self) -> &'static str {
47        match self {
48            Self::Missing => "missing",
49            Self::TooEarly => "too_early",
50            Self::Server => "server",
51            Self::Transport => "transport",
52            Self::Invalid => "invalid",
53        }
54    }
55}
56impl_spec_display!(FailureKind);
57
58/// What the caller does after a failure.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum FailureClass {
62    /// Back off (the adapter owns the arithmetic, `Retry-After` included) and
63    /// call [`super::DashClient::retry`]. `attempt` is the 1-based count of
64    /// consecutive failures of this resource.
65    Retry {
66        /// Consecutive failures including this one.
67        attempt: u32,
68        /// Why.
69        kind: FailureKind,
70    },
71    /// The failure count reached the configured cap: the presentation has
72    /// ended with an [`super::Output::Fatal`].
73    GiveUp,
74    /// A live segment requested before it exists (a `425`, or a `404`/`410`
75    /// inside the early-grace window): not counted. The client asks for it
76    /// again from [`super::DashClient::poll`] at `retry_at_us`; the caller
77    /// does nothing.
78    NotYetAvailable {
79        /// Monotonic microseconds at which the segment is requested again.
80        retry_at_us: u64,
81    },
82    /// A live segment that left the time-shift window before it was fetched:
83    /// not counted and not retried. The client continues at the oldest
84    /// segment still available and marks the gap with a `Discontinuity`.
85    Expired,
86    /// A live segment given up on after the configured number of consecutive
87    /// failures: [`super::Output::SegmentLost`] and a `Discontinuity` follow.
88    SegmentSkipped,
89    /// The resource is the missing tail of a Period (DESIGN section 5.5 and
90    /// the multi-period decision in the crate README): nothing to retry,
91    /// nothing counted, playback continues.
92    EndTail,
93}
94
95/// Why a delivered body was rejected.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97#[non_exhaustive]
98pub enum Rejection {
99    /// Not a media fragment (`moof` then `mdat`): an error page, a truncated
100    /// transfer or garbage.
101    NotMedia,
102    /// An init without a complete `moov`.
103    InitUndecodable,
104    /// The demuxer refused the init and segment.
105    DemuxFailed,
106    /// The `sidx` bytes are not a usable index.
107    BadIndex,
108    /// A ranged response whose length differs from the requested range.
109    WrongLength,
110    /// Larger than `DashConfig::max_segment_bytes`.
111    TooLarge,
112    /// A progressive MP4 served as a media segment.
113    NonFragmentedMp4,
114}
115
116impl Rejection {
117    /// Stable snake_case label.
118    #[must_use]
119    pub fn name(&self) -> &'static str {
120        match self {
121            Self::NotMedia => "not_media",
122            Self::InitUndecodable => "init_undecodable",
123            Self::DemuxFailed => "demux_failed",
124            Self::BadIndex => "bad_index",
125            Self::WrongLength => "wrong_length",
126            Self::TooLarge => "too_large",
127            Self::NonFragmentedMp4 => "non_fragmented_mp4",
128        }
129    }
130}
131impl_spec_display!(Rejection);
132
133/// Why the presentation cannot continue.
134#[derive(Debug, Clone, PartialEq, Eq)]
135#[non_exhaustive]
136pub enum FatalKind {
137    /// A dynamic MPD and no UTC source at all: no `UTCTiming` could be read,
138    /// the host never supplied a wall clock and the MPD response carried no
139    /// usable `Date`. Segment availability cannot be decided (DESIGN 3.4).
140    ClockUnavailable,
141    /// The MPD has nothing a client can play.
142    MpdInvalid {
143        /// What is missing.
144        reason: &'static str,
145    },
146    /// A URL derived from the MPD was refused.
147    UnsupportedAddressing {
148        /// Which rule (`cross_origin`, `unusable_url`).
149        reason: &'static str,
150    },
151    /// A segment failed the configured number of times in a row.
152    SegmentFetchFailed {
153        /// The segment.
154        id: ResourceId,
155        /// Status of the last failure, when it was an HTTP status.
156        status: Option<u16>,
157    },
158    /// An init or index failed the configured number of times in a row.
159    InitFetchFailed {
160        /// The resource.
161        id: ResourceId,
162        /// Status of the last failure, when it was an HTTP status.
163        status: Option<u16>,
164    },
165    /// A progressive MP4 was served as a media segment of a fragmented
166    /// presentation.
167    NonFragmentedMp4 {
168        /// The segment.
169        id: ResourceId,
170    },
171}
172
173impl FatalKind {
174    /// Stable snake_case label.
175    #[must_use]
176    pub fn name(&self) -> &'static str {
177        match self {
178            Self::ClockUnavailable => "clock_unavailable",
179            Self::MpdInvalid { .. } => "mpd_invalid",
180            Self::UnsupportedAddressing { .. } => "unsupported_addressing",
181            Self::SegmentFetchFailed { .. } => "segment_fetch_failed",
182            Self::InitFetchFailed { .. } => "init_fetch_failed",
183            Self::NonFragmentedMp4 { .. } => "non_fragmented_mp4",
184        }
185    }
186}
187impl_spec_display!(FatalKind);
188
189/// Errors returned by the client's `on_*` and `select_*` calls.
190#[derive(Debug, Clone, PartialEq, Eq, Error)]
191#[non_exhaustive]
192pub enum Error {
193    /// A resource was delivered for an id the client did not request, or one
194    /// it no longer waits for.
195    #[error("resource {id:?} was not requested")]
196    UnrequestedResource {
197        /// The id.
198        id: ResourceId,
199    },
200    /// A second delivery of a resource already accepted.
201    #[error("resource {id:?} was delivered twice")]
202    DuplicateResource {
203        /// The id.
204        id: ResourceId,
205    },
206    /// The MPD was not requested, or already accepted.
207    #[error("no MPD is awaited")]
208    MpdNotAwaited,
209    /// The MPD bytes were rejected; request it again with
210    /// [`super::DashClient::retry_mpd`].
211    #[error("MPD rejected: {0}")]
212    Mpd(MpdError),
213    /// A `UTCTiming` answer for a request the client does not wait for
214    /// (answered twice, abandoned after its timeout, or never made).
215    #[error("UTC request {id:?} was not requested")]
216    UnrequestedUtc {
217        /// The id.
218        id: UtcId,
219    },
220    /// A body was rejected and counted as a failure; `class` says what to do.
221    #[error("resource {id:?} rejected ({reason}): {class:?}")]
222    Rejected {
223        /// The resource.
224        id: ResourceId,
225        /// Why.
226        reason: Rejection,
227        /// What the caller does next.
228        class: FailureClass,
229    },
230    /// The presentation has ended or failed; everything is ignored.
231    #[error("the client has stopped")]
232    Stopped,
233    /// [`super::DashClient::select_representation`] with an index outside the
234    /// current variant list.
235    #[error("no such variant")]
236    NoSuchVariant,
237    /// [`super::DashClient::select_audio`] with an index outside the current
238    /// audio list.
239    #[error("no such audio set")]
240    NoSuchAudio,
241    /// A switch is only possible while the Period being played is the one
242    /// being fetched.
243    #[error("the Representation cannot be switched now")]
244    NotSwitchable,
245}
246
247/// Result alias.
248pub type Result<T> = core::result::Result<T, Error>;