hls-runtime 0.12.0

Sans-IO Low-Latency HLS (RFC 8216bis) client + server engines in one crate (blocking reload, part prefetch, rolling-window origin), with an optional tokio+reqwest IO adapter.
Documentation
//! Error type returned by [`crate::client::HlsClient`].

use alloc::string::String;
use thiserror::Error;

/// Crate-wide result alias.
pub type Result<T> = core::result::Result<T, Error>;

/// Error variants [`crate::client::HlsClient`] can return.
#[derive(Debug, Error, Clone, PartialEq)]
#[non_exhaustive]
pub enum Error {
    /// The playlist bytes fed to [`crate::client::HlsClient::on_playlist`]
    /// are not valid UTF-8 (RFC 8216 §4.1 playlists are UTF-8 text).
    #[error("playlist is not valid UTF-8: {0}")]
    PlaylistNotUtf8(#[from] core::str::Utf8Error),

    /// `broadcast_hls::MediaPlaylist::parse` rejected the playlist text (a
    /// known tag with a missing/unparsable required attribute).
    #[error("playlist parse: {0}")]
    PlaylistParse(#[from] broadcast_hls::Error),

    /// [`crate::client::HlsClient::on_resource`] was fed bytes for a
    /// [`crate::client::ResourceId`] the client never requested (a
    /// caller/driver bug, or a stale/duplicate delivery after the client
    /// already moved past it).
    #[error("resource delivered for an id the client did not request: {id:?}")]
    UnrequestedResource {
        /// The unexpected id.
        id: super::action::ResourceId,
    },

    /// Defensive-only: a Part/Segment reached the demux step with no init
    /// segment cached. In normal operation this cannot happen —
    /// [`crate::client::HlsClient::on_resource`] buffers any Part/Segment
    /// that arrives before the init segment and replays it once the init is
    /// delivered, so callers may complete fetches in any order.
    #[error("resource {id:?} delivered before the init segment was available")]
    InitNotYetAvailable {
        /// The id that could not be demuxed yet.
        id: super::action::ResourceId,
    },

    /// Demuxing the concatenation of the init segment + a fetched Part/Segment
    /// resource (via `transmux::Fmp4Demux`) failed.
    #[error("demux of resource {id:?} failed: {source}")]
    Demux {
        /// The id whose bytes failed to demux.
        id: super::action::ResourceId,
        /// The underlying transmux error.
        #[source]
        source: transmux::Error,
    },

    /// A segment of an fMP4 playlist is a progressive movie (`moov`, no
    /// `moof`): HLS fMP4 segments carry movie fragments (RFC 8216 §3.3), so no
    /// sample can be taken from it.
    #[error("resource {id:?} is a non-fragmented MP4: HLS fMP4 segments carry movie fragments")]
    NotFragmented {
        /// The id whose bytes are a progressive MP4.
        id: super::action::ResourceId,
    },

    /// A fetched Part/Segment answered with bytes that are not media for its
    /// container: for MPEG-TS (ISO/IEC 13818-1 §2.4.3.2) no sync-byte lattice, for ISO BMFF (ISO/IEC 14496-12 §8.8.4) no complete
    /// `moof` followed by its `mdat`. The resource is marked unrequested, so a
    /// retry ([`crate::client::HlsClient::retry_resource`] or the next
    /// playlist) fetches it again. Structurally valid media that yields no
    /// samples is not this error.
    #[error("resource {id:?} is not media for its container")]
    NotMedia {
        /// The id whose bytes are not media.
        id: super::action::ResourceId,
    },

    /// A fetched Media Initialization Section holds no `moov` box (it is not an
    /// ISO BMFF init at all, or it is truncated). The init is marked failed, so
    /// [`crate::client::HlsClient::retry_init`] fetches it again.
    #[error("media initialization section holds no complete moov box")]
    InitUndecodable,

    /// An `EXT-X-BYTERANGE` (or `EXT-X-PRELOAD-HINT` byte range,
    /// RFC 8216bis §4.4.4.9/§4.4.5.3) `offset + length` overflowed `u64`.
    /// Both values come straight from the remote origin's playlist text —
    /// `broadcast_hls::MediaPlaylist::parse` places no upper bound on
    /// either (confirmed: it is a bare `str::parse::<u64>()`) — so a
    /// malicious or corrupt origin can advertise e.g.
    /// `BYTERANGE:18446744073709551615@0`, or a chain of omitted-offset
    /// ranges whose per-URL running cursor
    /// ([`crate::client::HlsClient`]'s `byte_range_cursor`) accumulates past
    /// `u64::MAX`. Rejected outright rather than saturating: a saturated
    /// range would silently become a *different, wrong* byte range, and the
    /// client would demux whatever bytes the origin happened to return for
    /// it — a worse outcome than simply failing this one fetch.
    #[error("byte range for {url:?} overflows u64: offset {offset} + length {length}")]
    ByteRangeOverflow {
        /// The resolved resource URL the range applies to.
        url: String,
        /// The range's starting offset (explicit, or the carried-forward
        /// cursor for this URL).
        offset: u64,
        /// The range's length, as advertised by the playlist.
        length: u64,
    },

    /// [`crate::client::HlsClient::on_resource`] was fed a second delivery
    /// of a [`crate::client::ResourceId`] it has already accepted (for
    /// example a driver retry racing a slow success). Rejected so a part's
    /// samples are never emitted twice and the outstanding-fetch count never
    /// drifts (the `Output` exactly-once contract).
    #[error("resource {id:?} was already delivered")]
    DuplicateResource {
        /// The id delivered twice.
        id: super::action::ResourceId,
    },

    /// `#EXT-X-MEDIA-SEQUENCE` plus the playlist's segment count overflows
    /// `u64` (RFC 8216 §4.3.3.2 puts no upper bound on the tag's value, so a
    /// hostile or corrupt origin can advertise one). Rejected outright
    /// rather than wrapping the derived Media Sequence Numbers.
    #[error("media sequence {media_sequence} + {segments} segments overflows u64")]
    MediaSequenceOverflow {
        /// The playlist's `EXT-X-MEDIA-SEQUENCE`.
        media_sequence: u64,
        /// The number of closed segments the playlist lists.
        segments: usize,
    },

    /// [`crate::client::HlsClient::select_variant`] named a variant index that
    /// does not exist, or was called before any master playlist had been seen.
    #[error("no such variant: {index}")]
    NoSuchVariant {
        /// The requested index.
        index: usize,
    },

    /// [`crate::client::HlsClient::on_key`] was fed bytes that are not exactly
    /// one AES-128 key (RFC 8216 §5.1: a key file is 16 octets).
    #[error("key {id:?} is {len} bytes, an AES-128 key is exactly 16")]
    BadKeyLength {
        /// The key whose response was the wrong size.
        id: super::action::KeyId,
        /// The number of bytes received.
        len: usize,
    },

    /// [`crate::client::HlsClient::on_key`] was fed a [`super::action::KeyId`]
    /// the client never assigned, or one whose key it already holds.
    #[error("key delivered for an id the client did not request: {id:?}")]
    UnrequestedKey {
        /// The unexpected id.
        id: super::action::KeyId,
    },

    /// A segment could not be AES-128 decrypted (ciphertext not a positive
    /// multiple of 16 bytes, or invalid PKCS7 padding — e.g. the wrong key).
    #[error("segment {id:?} could not be decrypted with key {key_id:?}")]
    DecryptFailed {
        /// The segment that failed to decrypt.
        id: super::action::ResourceId,
        /// The key it was decrypted with.
        key_id: super::action::KeyId,
    },

    /// The playlist uses an encryption this client does not decrypt. The
    /// segment is never passed through as plaintext.
    #[error("unsupported encryption ({method}): {what}")]
    UnsupportedEncryption {
        /// The `METHOD` of the key that applies.
        method: broadcast_hls::EncryptionMethod,
        /// What is unsupported.
        what: &'static str,
    },

    /// Encrypted segments waiting for their key exceeded the buffering cap;
    /// the oldest was dropped and will be requested again by a later reload.
    #[error("encrypted segments waiting for a key exceed the buffer cap; dropped {dropped:?}")]
    PendingKeyedOverflow {
        /// The ciphertext that was dropped.
        dropped: super::action::ResourceId,
    },

    /// A delta playlist (`EXT-X-SKIP`) could not be merged into a full view,
    /// so none of its segments was processed; a full playlist has been
    /// requested instead.
    #[error(
        "delta playlist at media sequence {media_sequence} skipping {skipped_segments} segments cannot be merged"
    )]
    DeltaNotMergeable {
        /// The delta's `EXT-X-MEDIA-SEQUENCE`.
        media_sequence: u64,
        /// Its `EXT-X-SKIP` `SKIPPED-SEGMENTS`.
        skipped_segments: u64,
    },
}