Skip to main content

Crate broadcast_hls

Crate broadcast_hls 

Source
Expand description

HLS playlist generation — RFC 8216.

Produces #EXTM3U-formatted media and master playlists from structured data, suitable for VOD and live CMAF workflows.

§Trick-play (I-frame-only) signalling

HLS supports two complementary tags for trick-play (timeline scrubbing / thumbnail extraction) renditions; both are strictly opt-in so existing playlists are byte-for-byte unchanged:

  • #EXT-X-I-FRAME-STREAM-INF (RFC 8216 §4.3.4.2) — a master-playlist tag declaring an I-frame-only rendition. Unlike #EXT-X-STREAM-INF the URI is an attribute on the tag line itself, not a following line. Add one IFrameVariant per rendition to MasterPlaylist::iframe_variants; to_m3u8 renders each as #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=<n>[,CODECS="<c>"][,RESOLUTION=<w>x<h>],URI="<uri>".

  • #EXT-X-I-FRAMES-ONLY (RFC 8216 §4.3.3.6) — a media-playlist tag declaring that every segment carries a single I-frame. Set MediaPlaylist::iframes_only to true; to_m3u8 emits the tag in the header block (after the version line). RFC 8216 §4.3.3.6 requires protocol version ≥ 4 when this tag is present; the renderer computes this as one input to the general #EXT-X-VERSION derivation (see “Protocol version derivation” below), not as a special case.

§Discontinuity support

The playlist model supports RFC 8216 discontinuity signalling:

  • #EXT-X-DISCONTINUITY (RFC 8216 §4.3.4.3) — a marker emitted immediately before the #EXTINF of a discontinuous MediaSegment (one whose MediaSegment::discontinuous flag is true). It signals a break in the media timeline between the preceding segment and the one that follows it (change in encoding, timestamps, tracks, or format).

  • #EXT-X-DISCONTINUITY-SEQUENCE (RFC 8216 §4.3.3.3) — a header tag equal to the count of discontinuities that have already rolled off the front of a live/sliding-window playlist. Emitted as #EXT-X-DISCONTINUITY-SEQUENCE:<n> when n > 0; absent (defaulting to 0) otherwise.

A caller assembling segments (e.g. the transmux crate’s Segmenter, via its mark_discontinuity method) marks the next cut as discontinuous; a segmenter also typically auto-detects init-segment changes and marks those cuts automatically (see mark_init_discontinuities below).

§Low-Latency HLS (RFC 8216bis)

Low-Latency HLS (LL-HLS — the HLS 2nd edition draft, RFC 8216bis) drives end-to-end latency below one segment duration by publishing each segment’s partial segments (“parts”, RFC 8216bis §4.4.4.9) as they are produced, before the parent segment is complete. This model adds four opt-in playlist directives, all rendered only when MediaPlaylist::low_latency is set (so a plain playlist is byte-for-byte unchanged):

  • #EXT-X-SERVER-CONTROL (RFC 8216bis §4.4.3.8) — the header carries CAN-BLOCK-RELOAD=YES (the server supports blocking playlist reload) and PART-HOLD-BACK=<sec> (how far from the live edge a client may play parts). Per the spec, PART-HOLD-BACK MUST be at least three times the part-target duration.
  • #EXT-X-PART-INF:PART-TARGET=<sec> (RFC 8216bis §4.4.3.7) — the header declaring the part-target duration.
  • #EXT-X-PART:DURATION=<sec>,URI="<uri>"[,INDEPENDENT=YES] (RFC 8216bis §4.4.4.9) — one line per part, emitted before the parent segment’s #EXTINF. INDEPENDENT=YES marks a part that begins with an independently decodable frame (a sync sample).
  • #EXT-X-PRELOAD-HINT:TYPE=PART,URI="<next-part-uri>" (RFC 8216bis §4.4.5.3) — hints the URI of the next, not-yet-available part so a client can request it ahead of time.

A live origin’s trailing segment is often still open — being filled in by new parts as they are produced, not yet closed with a duration and URI. MediaPlaylist::open_segment carries that in-progress OpenSegment’s known parts; to_m3u8 renders them as trailing #EXT-X-PART lines with no #EXTINF/URI (RFC 8216bis §4.4.4.9), same opt-in gating as the closed segments’ parts above.

§Protocol version derivation (RFC 8216bis §8, issue #871)

#EXT-X-VERSION is never chosen ahead of time — it is computed as the max() of the minimums the playlist’s actual content triggers, per the feature → minimum-version table transcribed at docs/version-compatibility.md (twice-verified against draft-pantos-hls-rfc8216bis-22 §8). MediaPlaylist::computed_version and MasterPlaylist::computed_version expose this directly; to_m3u8 uses it internally. A Playlist that triggers nothing (fully compatible with version 1) carries no #EXT-X-VERSION tag at all, per §8’s opening rule.

MediaPlaylist::version/MasterPlaylist::version stay a settable floor rather than becoming computed-only: 0 (the field’s Default) means “no explicit floor” (render exactly the computed value, or nothing); a nonzero value is raised — never lowered — to the computed minimum, so a caller can still deliberately over-declare (e.g. the backward-compatible EXT-X-MEDIA/AUDIO/VIDEO/SUBTITLES MAY-rule in §8) but can never silently under-declare an invalid playlist.

This replaces a real bug (issue #871): an LL-HLS origin previously baked in a hardcoded EXT-X-VERSION:9 unconditionally, even though none of the low-latency tags it emits (EXT-X-PART/EXT-X-PART-INF/ EXT-X-PRELOAD-HINT/EXT-X-SERVER-CONTROL) carry any version requirement at all — only EXT-X-SKIP does. RFC 8216 §7: “A client MUST NOT attempt playback if it does not support the protocol version specified by the EXT-X-VERSION tag” — so over-declaring silently locks out every client that supports the playlist’s true (lower) requirement.

§CENC/CBCS DRM signalling (ISO/IEC 23001-7, issue #564)

cenc_ext_x_key renders the #EXT-X-KEY tag line for a cbcs (AES-128 pattern CBC)-protected CMAF track — the CMAF-HLS case Apple’s HLS authoring guidance carries as METHOD=SAMPLE-AES. Push the returned line into MediaPlaylist::extra_tags (before the segments it protects). cenc (AES-128 full-block CTR) has no valid HLS METHOD — CTR is not one of HLS’s two encryption methods (SAMPLE-AES/ AES-128, both CBC) — so cenc-protected CMAF is signalling-only on the DASH side (the transmux crate’s dash module); cenc_ext_x_key returns None rather than emit an invalid tag.

§Parsing (RFC 8216bis, issue #717 slice 1)

MediaPlaylist::parse and MasterPlaylist::parse are the symmetric inverse of to_m3u8(): they parse an m3u8 string back into the same structs the renderer consumes, so an LL-HLS client (issue #717) can reuse the origin’s wire model rather than growing a second one. Recognized tags are the ones listed above plus the client-relevant LL-HLS tags — #EXT-X-BYTERANGE, #EXT-X-MAP, #EXT-X-SKIP, #EXT-X-RENDITION-REPORT and the BYTERANGE/GAP/CAN-SKIP-UNTIL/preload-hint-byte-range attributes. Unrecognized tags are preserved verbatim into MediaPlaylist::extra_tags (never an error — forward-compat); a malformed known tag (missing required attribute, unparsable value) returns crate::Error::HlsParse.

Known, documented gaps (data the current struct shape cannot yet carry, called out per the project’s round-trip-fidelity discipline rather than silently dropped):

  • #EXT-X-MEDIA (Multivariant Playlist alternate audio/subtitle renditions) is not modeled with typed fields — but MasterPlaylist now has its own extra_tags (mirroring MediaPlaylist::extra_tags): MasterPlaylist::parse preserves an unrecognized #EXT-... tag like this one verbatim, and to_m3u8 re-renders it, so it round-trips (just without structured field access) rather than being silently dropped.
  • #EXT-X-MAP is carried on MediaSegment::map with carry-forward parse semantics (a map applies to every following segment until the next EXT-X-MAP, per spec) and dedup-render semantics (re-emitted only when it changes from the previous segment). A hand-built MediaPlaylist whose segments’ map fields are not a valid carry-forward sequence (e.g. reverting to None after a Some) cannot round-trip, since the wire format has no way to say “stop applying the map” short of #EXT-X-DISCONTINUITY + a new #EXT-X-MAP.
  • A per-segment tag outside the recognized set above (e.g. #EXT-X-PROGRAM-DATE-TIME, a segment-scoped #EXT-X-KEY) is captured into the flat, playlist-level MediaPlaylist::extra_tags — the data is preserved, not dropped, but re-rendering loses its original interleaved position (extra tags always render as one block before all segments, matching to_m3u8()’s existing placement).
  • MediaSegment::bitrate (#EXT-X-BITRATE, RFC 8216bis §4.4.4.8) uses the same carry-forward + dedup-render rule as map above; the spec’s producer-side constraint that the tag “does not apply” to a segment carrying its own #EXT-X-BYTERANGE is not enforced here (the value is still carried and rendered on such a segment if present).

§Issue #872: the remaining 9 of RFC 8216bis §4.4’s 32 tags

#EXT-X-INDEPENDENT-SEGMENTS (§4.4.2.1), #EXT-X-START (§4.4.2.2, StartPoint), #EXT-X-DEFINE (§4.4.2.3, Define) are valid in either playlist type, so MediaPlaylist and MasterPlaylist each carry their own copies of these fields. #EXT-X-PLAYLIST-TYPE (§4.4.3.5, PlaylistType), #EXT-X-GAP (§4.4.4.7, MediaSegment::gap) and #EXT-X-BITRATE (§4.4.4.8, MediaSegment::bitrate) are MediaPlaylist-only. #EXT-X-SESSION-DATA (§4.4.6.4, SessionData), #EXT-X-SESSION-KEY (§4.4.6.5, SessionKey) and #EXT-X-CONTENT-STEERING (§4.4.6.6, ContentSteering) are MasterPlaylist-only. Together with the tags documented above, all 32 §4.4 tags now parse; see tests/hls_tag_completeness.rs for the drift-guard enumerating all 32 by name.

This crate does not enforce every spec MUST-constraint that requires cross-tag or cross-file context it cannot see at single-playlist parse time (e.g. EXT-X-DEFINE’s IMPORT/QUERYPARAM resolution against a parent Multivariant Playlist or a request URI, or any “MUST NOT appear more than once” rule) — it parses the tag’s own attribute grammar and leaves such semantic validation to a higher-level tool (e.g. media-doctor).

Depends only on broadcast-common. #![no_std] (+ alloc) when the std feature is disabled.

§Runnable examples

Run with cargo run -p broadcast-hls --example <name>.

§build_playlist

//! Build a VOD Media Playlist from typed fields and render it to `#EXTM3U`
//! text — RFC 8216 §4.3.
//!
//! Run with `cargo run -p broadcast-hls --example build_playlist`.

use broadcast_hls::{MediaPlaylist, MediaSegment};

fn main() {
    let playlist = MediaPlaylist {
        version: 3,
        target_duration: 10,
        media_sequence: 0,
        discontinuity_sequence: 0,
        segments: vec![
            MediaSegment {
                uri: "seg0.m4s".into(),
                duration: 9.009,
                discontinuous: false,
                parts: vec![],
                ..Default::default()
            },
            MediaSegment {
                uri: "seg1.m4s".into(),
                duration: 9.009,
                discontinuous: false,
                parts: vec![],
                ..Default::default()
            },
            MediaSegment {
                uri: "seg2.m4s".into(),
                duration: 3.003,
                discontinuous: false,
                parts: vec![],
                ..Default::default()
            },
        ],
        endlist: true,
        ..Default::default()
    };

    print!("{}", playlist.to_m3u8());
}

§parse_playlist

//! Parse an `#EXTM3U` Media Playlist back into structured fields — the
//! symmetric inverse of [`broadcast_hls::MediaPlaylist::to_m3u8`] (RFC 8216
//! §4.3 / RFC 8216bis).
//!
//! Run with `cargo run -p broadcast-hls --example parse_playlist`.

use broadcast_hls::MediaPlaylist;

const PLAYLIST: &str = "#EXTM3U\n\
#EXT-X-VERSION:3\n\
#EXT-X-TARGETDURATION:10\n\
#EXT-X-MEDIA-SEQUENCE:0\n\
#EXTINF:9.009,\n\
seg0.m4s\n\
#EXTINF:9.009,\n\
seg1.m4s\n\
#EXTINF:3.003,\n\
seg2.m4s\n\
#EXT-X-ENDLIST\n";

fn main() {
    let playlist = MediaPlaylist::parse(PLAYLIST).expect("valid playlist");

    println!("version: {}", playlist.version);
    println!("target_duration: {}", playlist.target_duration);
    println!("segments: {}", playlist.segments.len());
    for seg in &playlist.segments {
        println!("  {} ({:.3}s)", seg.uri, seg.duration);
    }
    assert!(playlist.endlist);

    // Round-trip: re-rendering a parsed playlist reproduces the same text.
    assert_eq!(playlist.to_m3u8(), PLAYLIST);
}

Structs§

ByteRange
A byte sub-range into a resource.
ContentSteering
#EXT-X-CONTENT-STEERING (RFC 8216bis §4.4.6.6) — a pointer to a Content Steering Manifest. MasterPlaylist-only; at most one per Playlist.
IFrameVariant
An I-frame-only rendition entry for a master playlist — RFC 8216 §4.3.4.2 (#EXT-X-I-FRAME-STREAM-INF).
LowLatencyConfig
Low-Latency HLS playlist configuration — RFC 8216bis.
MapTag
The Media Initialization Section reference of #EXT-X-MAP (RFC 8216bis §4.4.4.5) — see MediaSegment::map for carry-forward/dedup semantics.
MasterPlaylist
A master playlist (#EXTM3U / #EXT-X-STREAM-INF / …).
MediaPlaylist
A media playlist (#EXTM3U / #EXTINF / …).
MediaSegment
A single media segment in a media playlist.
OpenSegment
An in-progress (open) LL-HLS segment: its parts are known and being served, but the segment is not yet complete, so it carries no #EXTINF/URI (RFC 8216bis §4.4.4.9 — an open segment is represented by its trailing #EXT-X-PART lines only, until it closes).
PartSpec
A single partial segment (“part”) of a MediaSegment — RFC 8216bis §4.4.4.9 (#EXT-X-PART).
RenditionReport
#EXT-X-RENDITION-REPORT (RFC 8216bis §4.4.5.4) — a pointer to the current state of an associated Rendition’s own Media Playlist, so an LL-HLS client following one Rendition can discover how far another has progressed without polling it.
SessionData
#EXT-X-SESSION-DATA (RFC 8216bis §4.4.6.4) — arbitrary session data carried in a MasterPlaylist (Multivariant Playlist only). A Playlist MAY carry multiple entries, including repeats of the same DATA-ID distinguished by LANGUAGE.
SessionKey
#EXT-X-SESSION-KEY (RFC 8216bis §4.4.6.5) — preloadable decryption key info for a MasterPlaylist (Multivariant Playlist only), carrying the same attributes as #EXT-X-KEY (§4.4.4.4) except that the spec requires METHOD not be NONE (enforced at parse time).
SkipInfo
#EXT-X-SKIP (RFC 8216bis §4.4.5.2) — present on a Playlist Delta Update response in place of the segments/tags before the Skip Boundary.
StartPoint
#EXT-X-START (RFC 8216bis §4.4.2.2) — a preferred playback start point. Valid in either a MediaPlaylist or a MasterPlaylist.
Variant
A variant stream entry in a master playlist.

Enums§

CencScheme
A CENC protection scheme (schm.scheme_type) — ISO/IEC 23001-7 §4.
Define
A single #EXT-X-DEFINE variable declaration (RFC 8216bis §4.4.2.3). Unlike every other §4.4.2 tag, EXT-X-DEFINE MAY appear more than once per Playlist, so it is carried as a Vec on both MediaPlaylist and MasterPlaylist rather than a single field.
EncryptionMethod
METHOD attribute shared by #EXT-X-KEY/#EXT-X-SESSION-KEY (RFC 8216bis §4.4.4.4 / §4.4.6.5).
Error
Error variants that crate::MediaPlaylist::parse/crate::MasterPlaylist::parse can return.
PlaylistType
#EXT-X-PLAYLIST-TYPE (RFC 8216bis §4.4.3.5) mutability declaration — MediaPlaylist-only.
PreloadHintType
TYPE attribute of #EXT-X-PRELOAD-HINT (RFC 8216bis §4.4.5.3).
SessionDataContent
The mutually-exclusive VALUE/URI content of #EXT-X-SESSION-DATA (RFC 8216bis §4.4.6.4: “Each … tag MUST contain either a VALUE or URI attribute, but not both”).
SessionDataFormat
FORMAT attribute of #EXT-X-SESSION-DATA (RFC 8216bis §4.4.6.4).

Constants§

CENC_KEYFORMAT
KEYFORMAT for the generic CENC identification (mirrors DASH’s ContentProtection@schemeIdUri for the “common” scheme — ISO/IEC 23001-7 / urn:mpeg:dash:mp4protection:2011).
CENC_KEYFORMATVERSIONS
KEYFORMATVERSIONS for CENC_KEYFORMAT (there is only version "1").

Functions§

cenc_ext_x_key
Build the #EXT-X-KEY tag line for a cbcs-protected CMAF track (RFC 8216 §4.3.2.4 METHOD=SAMPLE-AES, KEYFORMAT/KEYFORMATVERSIONS per CENC_KEYFORMAT/CENC_KEYFORMATVERSIONS, plus the KEYID attribute Apple’s HLS CMAF/fMP4 authoring guidance uses to identify the CENC key ID).
mark_init_discontinuities
Auto-detect init-segment changes across a sequence of segments and mark the first segment that follows an init change as discontinuous (RFC 8216 §4.3.4.3).

Type Aliases§

Result
Crate-wide result alias.