broadcast-hls 0.1.0

HLS (M3U8) playlist syntax (RFC 8216 / RFC 8216bis) — Media/Master Playlist parse + serialize, no_std.
Documentation

broadcast-hls

Crates.io docs.rs

Extracted from transmux (issue #878). transmux/src/hls.rs — the M3U8 playlist syntax — moved here so a crate that only needs to parse/render HLS playlists (e.g. media-doctor, an HLS-pull client, a fuzz target) no longer has to pull in an entire any-to-any container muxing hub to parse a text file. transmux's own HLS/LL-HLS segmenters (ts_hls, ll_hls — they produce container bytes, not playlist syntax) stayed put.

Install

[dependencies]
broadcast-hls = "0.1"

HLS (M3U8) playlist syntax — RFC 8216 / RFC 8216bis (Low-Latency HLS): typed Media/Master Playlist parse + serialize.

Implements:

  • MediaPlaylist / MasterPlaylist#EXTM3U Media and Multivariant (Master) Playlists; to_m3u8() renders, parse() is its symmetric inverse.
  • MediaSegment / Variant / IFrameVariant — per-segment and per-variant entries, including RFC 8216 §4.3.4.2 I-frame-only trick-play signalling.
  • Low-Latency HLS (RFC 8216bis)LowLatencyConfig, PartSpec, OpenSegment, MapTag, ByteRange, PreloadHintType, RenditionReport, SkipInfo — the partial-segment / blocking-reload / playlist-delta-update directives, all strictly opt-in so a plain playlist is byte-for-byte unchanged.
  • #EXT-X-DISCONTINUITY[-SEQUENCE] signalling, including mark_init_discontinuities (auto-detect an init-segment change across a segment run).
  • CENC/CBCS DRM signalling (ISO/IEC 23001-7, issue #564) — cenc_ext_x_key renders the #EXT-X-KEY tag for a cbcs-protected CMAF track (cenc/AES-CTR has no valid HLS METHOD, so it returns None).
  • All 32 RFC 8216bis §4.4 tags (issue #872) — including #EXT-X-INDEPENDENT-SEGMENTS, #EXT-X-START (StartPoint), #EXT-X-DEFINE (Define), #EXT-X-PLAYLIST-TYPE (PlaylistType: VOD/EVENT), #EXT-X-GAP, #EXT-X-BITRATE, #EXT-X-SESSION-DATA (SessionData), #EXT-X-SESSION-KEY (SessionKey, EncryptionMethod) and #EXT-X-CONTENT-STEERING (ContentSteering) parse and serialize; see tests/hls_tag_completeness.rs for the drift-guard enumerating all 32 by name.

#![no_std] + alloc; depends only on broadcast-common. Builds for thumbv7em-none-eabi.

Round-trip fidelity

Per this workspace's text-format rule (docs/CRATE-ACCEPTANCE.md §1), a playlist is not required to be byte-identical after a parse → serialize round trip, only for a second parse of the rendered output to equal the first parse. Known ways rendered output can differ from arbitrary input text:

  • Unmodeled tags are preserved, not dropped, in a MasterPlaylist. #EXT-X-MEDIA (alternate audio/video/subtitle renditions) has no corresponding typed field but is preserved verbatim into MasterPlaylist::extra_tags, the Multivariant-Playlist counterpart of MediaPlaylist::extra_tags. A fixture containing #EXT-X-MEDIA therefore round-trips under this crate's invariant (the parsed struct is stable across a second parse); the rendered text differs in tag ordering (see below) but no tag is silently lost.

  • Tag ordering is canonical on re-emit, not input-preserving. to_m3u8() always emits tags in a fixed order relative to the variant list: extra_tags first, then INDEPENDENT-SEGMENTS / DEFINE / START, then SESSION-KEY / SESSION-DATA / CONTENT-STEERING, then the variant and I-frame-variant entries — regardless of where those tags appeared in the source text. The ordering divergence is a formatting difference, not data loss: every parsed tag (typed or verbatim) survives the round trip; it is just re-sequenced on output.

  • Unknown attributes on modeled tags are retained and re-emitted (issue #884). Every tag struct that carries an attribute list ( ContentSteering, Variant, IFrameVariant, MapTag, PartSpec, RenditionReport, SkipInfo, SessionData, SessionKey, StartPoint, Define, LowLatencyConfig) now holds an extra_attrs: Vec<(String, String)> for attribute names this crate does not model. These survive parse → serialize and feed the §8 row 12 REQ- check. They are always emitted after each tag's known attributes, so a tag with unknown attrs will have them appended in sorted-by-name order.

  • Tag ordering is canonical, not preserved. to_m3u8() always emits tags in a fixed order (e.g. #EXT-X-INDEPENDENT-SEGMENTS / #EXT-X-DEFINE / #EXT-X-START right after #EXT-X-VERSION; #EXT-X-SESSION-KEY / #EXT-X-SESSION-DATA / #EXT-X-CONTENT-STEERING before the variant list), regardless of where those tags appeared in the source text.

  • Line-continuation backslashes are never round-tripped. Some RFC 8216bis §9 examples use a trailing \ to wrap a long attribute list across lines for readability in the spec text itself (not literal m3u8 syntax — see docs/examples.md); this crate's fixtures reflow those onto single lines before committing them, and to_m3u8() never emits a continuation of its own.

  • #EXT-X-BITRATE's "does not apply to a byte-ranged segment" rule is not enforced. The tag is carried forward and dedup-rendered exactly like #EXT-X-MAP; a segment carrying its own #EXT-X-BYTERANGE still renders (and round-trips) a carried-forward bitrate value, though the spec says the tag does not semantically apply to it.

  • Cross-tag/cross-file MUST-constraints are not enforced — e.g. #EXT-X-DEFINE's IMPORT/QUERYPARAM resolution against a parent Multivariant Playlist or request URI, #EXT-X-SESSION-KEY's "METHOD MUST NOT be NONE", or any "MUST NOT appear more than once" rule. Only each tag's own attribute grammar is validated; broader semantic checks are left to a higher-level tool (e.g. media-doctor).

  • #EXT-X-VERSION is always emitted, even if the input omitted it. A playlist with no version tag parses as version 1 (the spec's implicit baseline) and renders back as an explicit #EXT-X-VERSION:1. The parsed document is unchanged across a second parse; the text gains a line. This crate renders the version field as given — it does not compute the §8 minimum for you, so declaring a correct version is the caller's job.

  • Whitespace and comments are not preserved. Blank lines, trailing whitespace (real playlists do carry it — Apple's BipBop #EXTINF lines end in a tab), and non-#EXT # comment lines are dropped at parse time and never re-emitted.

Durations are not in this list: #EXTINF and every seconds-valued attribute round-trip bit-exactly, including sub-millisecond values such as Apple's 9.9766 and RFC 8216bis §9.11's 2.00004. (They were lossy until issue #872 — see fixtures/hls/MANIFEST.md, Tier 2 "Finding".) Because that rendering is faithful to the exact f64 you supply, a duration that came from an unrounded division (e.g. 11.0 / 30.0) renders with all the digits needed to reproduce it — 0.36666666666666664. That is correct and round-trips, but if you want compact playlists, round your own durations before building the playlist; this crate will not round them for you, because it cannot tell a deliberate 2.00004 from an artefact.

Quick start

use broadcast_hls::{MediaPlaylist, MediaSegment};

let playlist = MediaPlaylist {
    version: 3,
    target_duration: 10,
    segments: vec![MediaSegment {
        uri: "seg0.m4s".into(),
        duration: 9.009,
        ..Default::default()
    }],
    endlist: true,
    ..Default::default()
};
let m3u8 = playlist.to_m3u8();
assert_eq!(MediaPlaylist::parse(&m3u8).unwrap(), playlist);

Examples

cargo run -p broadcast-hls --example build_playlist
cargo run -p broadcast-hls --example parse_playlist

Features

Feature Default Description
std yes Link the standard library. Without it the crate is #![no_std] + alloc.
serde no serde::Serialize derives on public types.

Minimum Supported Rust Version

1.86

License

MIT OR Apache-2.0