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-INFthe URI is an attribute on the tag line itself, not a following line. Add oneIFrameVariantper rendition toMasterPlaylist::iframe_variants;to_m3u8renders 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. SetMediaPlaylist::iframes_onlytotrue;to_m3u8emits 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-VERSIONderivation (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#EXTINFof a discontinuousMediaSegment(one whoseMediaSegment::discontinuousflag istrue). 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>whenn > 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 carriesCAN-BLOCK-RELOAD=YES(the server supports blocking playlist reload) andPART-HOLD-BACK=<sec>(how far from the live edge a client may play parts). Per the spec,PART-HOLD-BACKMUST 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=YESmarks 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 — butMasterPlaylistnow has its ownextra_tags(mirroringMediaPlaylist::extra_tags):MasterPlaylist::parsepreserves an unrecognized#EXT-...tag like this one verbatim, andto_m3u8re-renders it, so it round-trips (just without structured field access) rather than being silently dropped.#EXT-X-MAPis carried onMediaSegment::mapwith carry-forward parse semantics (a map applies to every following segment until the nextEXT-X-MAP, per spec) and dedup-render semantics (re-emitted only when it changes from the previous segment). A hand-builtMediaPlaylistwhose segments’mapfields are not a valid carry-forward sequence (e.g. reverting toNoneafter aSome) 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-levelMediaPlaylist::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, matchingto_m3u8()’s existing placement). MediaSegment::bitrate(#EXT-X-BITRATE, RFC 8216bis §4.4.4.8) uses the same carry-forward + dedup-render rule asmapabove; the spec’s producer-side constraint that the tag “does not apply” to a segment carrying its own#EXT-X-BYTERANGEis 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§
- Byte
Range - A byte sub-range into a resource.
- Content
Steering #EXT-X-CONTENT-STEERING(RFC 8216bis §4.4.6.6) — a pointer to a Content Steering Manifest.MasterPlaylist-only; at most one per Playlist.- IFrame
Variant - An I-frame-only rendition entry for a master playlist — RFC 8216 §4.3.4.2
(
#EXT-X-I-FRAME-STREAM-INF). - LowLatency
Config - Low-Latency HLS playlist configuration — RFC 8216bis.
- MapTag
- The Media Initialization Section reference of
#EXT-X-MAP(RFC 8216bis §4.4.4.5) — seeMediaSegment::mapfor carry-forward/dedup semantics. - Master
Playlist - A master playlist (
#EXTM3U/#EXT-X-STREAM-INF/ …). - Media
Playlist - A media playlist (
#EXTM3U/#EXTINF/ …). - Media
Segment - A single media segment in a media playlist.
- Open
Segment - 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-PARTlines only, until it closes). - Part
Spec - A single partial segment (“part”) of a
MediaSegment— RFC 8216bis §4.4.4.9 (#EXT-X-PART). - Rendition
Report #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.- Session
Data #EXT-X-SESSION-DATA(RFC 8216bis §4.4.6.4) — arbitrary session data carried in aMasterPlaylist(Multivariant Playlist only). A Playlist MAY carry multiple entries, including repeats of the sameDATA-IDdistinguished byLANGUAGE.- Session
Key #EXT-X-SESSION-KEY(RFC 8216bis §4.4.6.5) — preloadable decryption key info for aMasterPlaylist(Multivariant Playlist only), carrying the same attributes as#EXT-X-KEY(§4.4.4.4) except that the spec requiresMETHODnot beNONE(enforced at parse time).- Skip
Info #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.- Start
Point #EXT-X-START(RFC 8216bis §4.4.2.2) — a preferred playback start point. Valid in either aMediaPlaylistor aMasterPlaylist.- Variant
- A variant stream entry in a master playlist.
Enums§
- Cenc
Scheme - A CENC protection scheme (
schm.scheme_type) — ISO/IEC 23001-7 §4. - Define
- A single
#EXT-X-DEFINEvariable declaration (RFC 8216bis §4.4.2.3). Unlike every other §4.4.2 tag,EXT-X-DEFINEMAY appear more than once per Playlist, so it is carried as aVecon bothMediaPlaylistandMasterPlaylistrather than a single field. - Encryption
Method METHODattribute 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::parsecan return. - Playlist
Type #EXT-X-PLAYLIST-TYPE(RFC 8216bis §4.4.3.5) mutability declaration —MediaPlaylist-only.- Preload
Hint Type TYPEattribute of#EXT-X-PRELOAD-HINT(RFC 8216bis §4.4.5.3).- Session
Data Content - The mutually-exclusive
VALUE/URIcontent of#EXT-X-SESSION-DATA(RFC 8216bis §4.4.6.4: “Each … tag MUST contain either a VALUE or URI attribute, but not both”). - Session
Data Format FORMATattribute of#EXT-X-SESSION-DATA(RFC 8216bis §4.4.6.4).
Constants§
- CENC_
KEYFORMAT KEYFORMATfor the generic CENC identification (mirrors DASH’sContentProtection@schemeIdUrifor the “common” scheme — ISO/IEC 23001-7 /urn:mpeg:dash:mp4protection:2011).- CENC_
KEYFORMATVERSIONS KEYFORMATVERSIONSforCENC_KEYFORMAT(there is only version"1").
Functions§
- cenc_
ext_ x_ key - Build the
#EXT-X-KEYtag line for acbcs-protected CMAF track (RFC 8216 §4.3.2.4METHOD=SAMPLE-AES,KEYFORMAT/KEYFORMATVERSIONSperCENC_KEYFORMAT/CENC_KEYFORMATVERSIONS, plus theKEYIDattribute 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.