Contents

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-TIME or EXT-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-MAP in effect for it, and the

writer emits them only where they change.

  • Variable substitution: EXT-X-DEFINE with NAME/VALUE, IMPORT and QUERYPARAM.
  • 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.keys models the effective key set, so multiple KEYFORMATs (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