# HLS

HTTP Live Streaming (HLS) library implementing RFC 8216 specifications.
## Installation
Add `kim_hls` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:kim_hls, "~> 2.5"}
]
end
```
## From v2.x.x to v3.x.x
This release is a major architectural update focused on a fully functional packager and stricter RFC 8216 compliance. Highlights:
- `HLS.Packager` is now pure functional: operations return state + actions, and callers execute I/O.
- GenServer-based packager APIs are removed (no `start_link/1` or `GenServer.call/cast` usage).
- Segment upload flow is explicit (`put_segment` + `confirm_upload`), enabling caller-controlled concurrency.
- Discontinuities reset program date-time to a new shared reference.
- Master playlist output is reordered to place `#EXT-X-MEDIA` before `#EXT-X-STREAM-INF`.
- `#EXT-X-MEDIA` validation tightened (required attributes, CLOSED-CAPTIONS URI rules).
- Master and media playlist tests now assert RFC tag ordering and required attributes.
- Media target duration remains fixed to configured value (no per-playlist drift).
## Architecture
### Core Component
- **HLS.Packager** - Pure functional module for HLS playlist generation and segment handling. Returns actions for the caller to execute, providing explicit control over I/O operations.
### Supporting Modules
- **HLS.Playlist** - Playlist marshaling/unmarshaling with Master and Media playlist modules
- **HLS.Playlist.Tag** - Individual HLS tag implementations (EXT-X-* tags)
- **HLS.Segment** - Represents HLS segments with duration, URI, and optional init sections
- **HLS.VariantStream** & **HLS.AlternativeRendition** - Stream representation structures
- **HLS.Storage** - Protocol for get/put/delete helpers with file and Req-based implementations.
## Usage
### Basic Packager Usage
```elixir
# Initialize packager state
{:ok, state} = HLS.Packager.new(
manifest_uri: URI.new!("stream.m3u8"),
max_segments: 10 # Optional: enables sliding window
)
# Add a variant stream
{state, []} = HLS.Packager.add_track(state, "video_480p",
stream: %HLS.VariantStream{
bandwidth: 800_000,
resolution: {854, 480},
codecs: ["avc1.64001e"]
},
segment_extension: ".ts",
target_segment_duration: 6.0
)
# Add segment (returns upload action)
{state, [action]} = HLS.Packager.put_segment(state, "video_480p", duration: 6.0, pts: 0)
# Caller uploads the segment via storage helper
storage = HLS.Storage.File.new(base_dir: "./output")
:ok = HLS.Storage.put(storage, action.uri, segment_data)
# Confirm upload (may return playlist write actions)
{state, actions} = HLS.Packager.confirm_upload(state, action.id)
# Execute write actions
Enum.each(actions, fn
%HLS.Packager.Action.WritePlaylist{uri: uri, content: content} ->
HLS.Storage.put(storage, uri, content)
end)
# Sync and flush to create VOD playlist
{state, actions} = HLS.Packager.flush(state)
Enum.each(actions, &execute_action(&1, storage))
```
### Storage Helpers
```elixir
# File-backed storage for local output
storage = HLS.Storage.File.new(base_dir: "./output")
# Req-backed storage for HTTP(S)
req = Req.new(base_url: "https://cdn.example.com")
storage = HLS.Storage.Req.new(req)
```
### Configuration Options
Key options for `HLS.Packager.new/1`:
- `manifest_uri` (required) - URI of the master playlist
- `max_segments` - Maximum segments to retain (enables sliding window mode with automatic cleanup)
For resuming from existing playlists, use `HLS.Packager.resume/1` with loaded playlist data.
The caller must load the master and media playlists; resume trims tracks to the
last common sync point and schedules a discontinuity at the next segment.
If a referenced media playlist is missing or empty, the track is marked incomplete and
`put_segment/3` returns `{:error, %HLS.Packager.Error{code: :resume_track_not_ready}, state}`
until the caller reconciles it via `add_track/3` and `put_init_section/2`.
## Packager Edge Cases (Production)
`HLS.Packager` enforces RFC compliance and returns errors or stall warnings
instead of emitting non-compliant playlists:
- Segment duration overruns: returns `{:error, %HLS.Packager.Error{code: :segment_duration_over_target}, state}`
when a segment exceeds the configured target duration.
- Timing drift between segments: returns `{:error, %HLS.Packager.Error{code: :timing_drift}, state}`
when timestamps drift beyond `timing_tolerance_ms`. Callers should ignore the segment and
call `skip_sync_point/2` before continuing.
- Grouped recovery: `skip_sync_point/2` marks the sync point as skipped for all tracks and
schedules a discontinuity. The next `put_segment/3` call for each track at that index
returns `{:warning, %HLS.Packager.Error{code: :sync_point_skipped}, state}` so callers
can drop that segment across the group.
- Discontinuity synchronization: `discontinue/1` returns
`{:error, %HLS.Packager.Error{code: :discontinuity_point_missed}, state}` if any track
already passed the sync point.
- Sync readiness: `sync/2` returns `{:warning, [%HLS.Packager.Error{code: :mandatory_track_missing_segment_at_sync}], state}`
when mandatory tracks are missing segments (stall without advancing playlists).
- Track timing alignment: `sync/2` returns `{:error, %HLS.Packager.Error{code: :track_timing_mismatch_at_sync}, state}`
if timestamps are misaligned at the sync point.
- Upload confirmation: `confirm_upload/2` returns `{:warning, %HLS.Packager.Error{code: :upload_id_not_found}, state}`
when the upload id is unknown, without changing state.
- Sliding window cleanup: when `max_segments` is set, it trims old segments, bumps
`EXT-X-MEDIA-SEQUENCE` and `EXT-X-DISCONTINUITY-SEQUENCE`, and deletes orphaned
init sections to keep playlists consistent.
## Copyright and License
Copyright 2024, [KIM Keep In Mind GmbH](https://www.keepinmind.info/)
Licensed under the [Apache License, Version 2.0](LICENSE)