creaturesurvive/hlsplaylist
A fast, lossless, pure-Swift parser and writer for HTTP Live Streaming (.m3u8) playlists.
Why
AVFoundation plays HLS but gives you no way to read or write a playlist. Apps end up needing to do that for a lot of reasons:
- rewriting segment URLs for a local proxy,
AVAssetResourceLoader, or signed CDN tokens - choosing a variant or audio/subtitle rendition yourself (for example for Chromecast, downloads,
or custom players)
- building offline or DVR playlists, stitching ads, or trimming live windows
- reading
EXT-X-PROGRAM-DATE-TIMEorEXT-X-DATERANGE(SCTE-35) for live TV guides and ad markers - validating or debugging a media server's output
The existing Swift options are unmaintained (last updated 2017–2019), partial (media playlists only, no LL-HLS), or Objective-C.
Features
- Complete model: multivariant playlists (variants, I-frame variants, renditions, session
data and keys, content steering) and media playlists (segments, byte ranges, key rotation, init sections, discontinuities, program date-time, date ranges, gaps, bitrate, partial segments, server control, skip, preload hints, rendition reports).
- Lossless round-trips: unknown tags and unmodeled attributes are preserved in place, and
quoted and unquoted values stay distinct. parse → render → parse is identity. This is verified against Apple's reference streams.
- Effective state: each segment carries the keys and
EXT-X-MAPin effect for it, and the
writer emits them only where they change.
- Variable substitution:
EXT-X-DEFINEwithNAME/VALUE,IMPORTandQUERYPARAM. - Lenient by default, strict on request: problems are collected as
ParseWarnings, and
ParseOptions(strict: true) throws instead. Handles BOMs, CRLF, whitespace, unterminated quotes and colon-less time-zone offsets.
- Utilities: variant selection, rendition lookup, codec classification, segment timing and
lookup by time, program-date-time extrapolation, live offset (HOLD-BACK/PART-HOLD-BACK), IV derivation, URI resolution and rewriting.
- Fast: 20,000 segments with program-date-times parse in about 0.25 s (release build).
Dates use an allocation-free ISO 8601 fast path.
Installation
Add HLSPlaylist to your Package.swift:
dependencies: [
.package(url: "https://github.com/CreatureSurvive/HLSPlaylist.git", from: "1.0.0"),
],
targets: [
.target(name: "MyApp", dependencies: ["HLSPlaylist"]),
]Or in Xcode, choose File › Add Package Dependencies… and enter https://github.com/CreatureSurvive/HLSPlaylist.
Requirements
| Platform | Minimum | | --- | --- | | iOS | 15.0 | | macOS | 12.0 | | tvOS | 15.0 | | watchOS | 8.0 | | visionOS | 1.0 |
Swift 6.1 (Xcode 16.4) or later, in Swift 6 language mode. No third-party dependencies.
Usage
Parsing
let playlist = try Playlist(text) // String
let playlist = try Playlist(data: data) // Data
// Warnings, variables and options
let result = try PlaylistParser(options: ParseOptions(
strict: false,
importedVariables: masterResult.variables, // for EXT-X-DEFINE:IMPORT
playlistURL: url // for EXT-X-DEFINE:QUERYPARAM
)).parse(data)
result.warnings.forEach { print($0) }Selecting streams
let master = try Playlist(data: data).multivariant!
let variant = master.preferredVariant(
maxBandwidth: 6_000_000,
maxResolution: Resolution(width: 1920, height: 1080),
isCodecSupported: { CodecFamily(codec: $0) != .av1 }
)
let audioTracks = master.audioRenditions(for: variant!) // [Rendition]
let subtitles = master.subtitleRenditions(for: variant!)Media playlists
let media = try Playlist(data: data).media!
media.duration // total seconds
media.isLive // no ENDLIST and not VOD
media.segmentIndex(containing: 125.0)
media.programDateTimes // wall-clock time for every segment
media.recommendedLiveOffset // HOLD-BACK, PART-HOLD-BACK or 3 × target duration
for segment in media.segments where segment.isEncrypted {
print(segment.uri, segment.keys.map(\.method), segment.map?.uri ?? "-")
}Rewriting URIs (proxies, tokens, offline playback)
let proxied = media.mapURIs { uri, role in
switch role {
case .key: return uri // leave keys alone
default: return "http://127.0.0.1:8080/proxy?u=\(uri.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)"
}
}
let text = proxied.render()
let absolute = master.resolvingURIs(against: masterURL)Writing
var playlist = MediaPlaylist(targetDuration: 6, playlistType: .vod, hasEndList: true, version: 3)
playlist.segments = files.map { Segment(uri: $0.name, duration: $0.duration) }
try playlist.render().write(to: outputURL, atomically: true, encoding: .utf8)Design notes
- Typed properties are the source of truth. Anything this library doesn't model lands in
otherAttributes (per tag) or unknownTags (per position), so vendor extensions such as #EXT-X-CUE-OUT or #EXT-X-PLEX-* survive editing.
Segment.keysmodels the effective key set, so multipleKEYFORMATs (FairPlay plus Widevine)
can coexist, and METHOD=NONE clears them.
- Partial segments are attached to the segment they complete. Parts at the live edge are in
MediaPlaylist.trailingParts.
Testing
swift test runs the unit suite: spec fixtures, round-trips, key rotation, LL-HLS, variable substitution, date edge cases, and a 20k-segment performance check. HLS_LIVE_TESTS=1 swift test also fetches, parses and round-trips every playlist in Apple's public example streams.
Changelog
See CHANGELOG.md. Releases follow Semantic Versioning.
Contributing
Issues and pull requests are welcome. Please run swift test before opening a pull request, and add tests for new behavior.
License
Available under the MIT license. See LICENSE for details.
Package Metadata
Repository: creaturesurvive/hlsplaylist
Default branch: main
README: README.md