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 feature of the MPD that this client deliberately does not implement,
147    /// with nothing playable left ([`crate::error::Unsupported`]).
148    Unsupported(crate::error::Unsupported),
149    /// A URL derived from the MPD was refused.
150    UnsupportedAddressing {
151        /// Which rule (`cross_origin`, `unusable_url`).
152        reason: &'static str,
153    },
154    /// A segment failed the configured number of times in a row.
155    SegmentFetchFailed {
156        /// The segment.
157        id: ResourceId,
158        /// Status of the last failure, when it was an HTTP status.
159        status: Option<u16>,
160    },
161    /// An init or index failed the configured number of times in a row.
162    InitFetchFailed {
163        /// The resource.
164        id: ResourceId,
165        /// Status of the last failure, when it was an HTTP status.
166        status: Option<u16>,
167    },
168    /// A progressive MP4 was served as a media segment of a fragmented
169    /// presentation.
170    NonFragmentedMp4 {
171        /// The segment.
172        id: ResourceId,
173    },
174}
175
176impl FatalKind {
177    /// Stable snake_case label.
178    #[must_use]
179    pub fn name(&self) -> &'static str {
180        match self {
181            Self::ClockUnavailable => "clock_unavailable",
182            Self::MpdInvalid { .. } => "mpd_invalid",
183            Self::Unsupported(u) => u.name(),
184            Self::UnsupportedAddressing { .. } => "unsupported_addressing",
185            Self::SegmentFetchFailed { .. } => "segment_fetch_failed",
186            Self::InitFetchFailed { .. } => "init_fetch_failed",
187            Self::NonFragmentedMp4 { .. } => "non_fragmented_mp4",
188        }
189    }
190}
191impl_spec_display!(FatalKind);
192
193/// Errors returned by the client's `on_*` and `select_*` calls.
194#[derive(Debug, Clone, PartialEq, Eq, Error)]
195#[non_exhaustive]
196pub enum Error {
197    /// A resource was delivered for an id the client did not request, or one
198    /// it no longer waits for.
199    #[error("resource {id:?} was not requested")]
200    UnrequestedResource {
201        /// The id.
202        id: ResourceId,
203    },
204    /// A second delivery of a resource already accepted.
205    #[error("resource {id:?} was delivered twice")]
206    DuplicateResource {
207        /// The id.
208        id: ResourceId,
209    },
210    /// The MPD was not requested, or already accepted.
211    #[error("no MPD is awaited")]
212    MpdNotAwaited,
213    /// The MPD bytes were rejected; request it again with
214    /// [`super::DashClient::retry_mpd`].
215    #[error("MPD rejected: {0}")]
216    Mpd(MpdError),
217    /// A `UTCTiming` answer for a request the client does not wait for
218    /// (answered twice, abandoned after its timeout, or never made).
219    #[error("UTC request {id:?} was not requested")]
220    UnrequestedUtc {
221        /// The id.
222        id: UtcId,
223    },
224    /// A body was rejected and counted as a failure; `class` says what to do.
225    #[error("resource {id:?} rejected ({reason}): {class:?}")]
226    Rejected {
227        /// The resource.
228        id: ResourceId,
229        /// Why.
230        reason: Rejection,
231        /// What the caller does next.
232        class: FailureClass,
233    },
234    /// The presentation has ended or failed; everything is ignored.
235    #[error("the client has stopped")]
236    Stopped,
237    /// [`super::DashClient::select_representation`] with an index outside the
238    /// current variant list.
239    #[error("no such variant")]
240    NoSuchVariant,
241    /// [`super::DashClient::select_audio`] with an index outside the current
242    /// audio list.
243    #[error("no such audio set")]
244    NoSuchAudio,
245    /// A switch is only possible while the Period being played is the one
246    /// being fetched.
247    #[error("the Representation cannot be switched now")]
248    NotSwitchable,
249}
250
251/// Result alias.
252pub type Result<T> = core::result::Result<T, Error>;