M3u8
Parse and generate m3u8 playlists for Apple HTTP Live Streaming (HLS) in Ruby.
Install / Use
/learn @sethdeckard/M3u8README
m3u8
m3u8 provides easy generation and parsing of m3u8 playlists defined in RFC 8216 HTTP Live Streaming and its proposed successor draft-pantos-hls-rfc8216bis.
- Full coverage of RFC 8216 and draft-pantos-hls-rfc8216bis-19 (Protocol Version 13), including Low-Latency HLS and Content Steering.
- Provides parsing of an m3u8 playlist into an object model from any File, StringIO, or string.
- Provides ability to write playlist to a File or StringIO or expose as string via to_s.
- Distinction between a master and media playlist is handled automatically (single Playlist class).
- Automatic generation of codec strings for H.264, HEVC, AV1, AAC, AC-3, E-AC-3, FLAC, Opus, and MP3.
Requirements
Ruby 3.1+
Installation
Add this line to your application's Gemfile:
gem 'm3u8'
And then execute:
$ bundle
Or install it yourself as:
$ gem install m3u8
CLI
The gem includes a command-line tool for inspecting and validating playlists.
Inspect
Display playlist metadata and item summary:
$ m3u8 inspect master.m3u8
Type: Master
Independent Segments: Yes
Variants: 6
1920x1080 5042000 bps hls/1080/1080.m3u8
640x360 861000 bps hls/360/360.m3u8
Media: 2
Session Keys: 1
Session Data: 0
$ m3u8 inspect media.m3u8
Type: Media
Version: 4
Sequence: 1
Target: 12
Duration: 1371.99s
Playlist: VOD
Cache: No
Segments: 138
Keys: 0
Maps: 0
Reads from stdin when no file is given:
$ cat playlist.m3u8 | m3u8 inspect
Validate
Check playlist validity (exit 0 for valid, 1 for invalid):
$ m3u8 validate playlist.m3u8
Valid
$ m3u8 validate bad.m3u8
Invalid
- Playlist contains both master and media items
Usage (Builder DSL)
Playlist.build provides a block-based DSL for concise playlist construction. It supports two forms:
# instance_eval form (clean DSL)
playlist = M3u8::Playlist.build(version: 4, target: 12) do
segment duration: 11.34, segment: '1080-7mbps00000.ts'
segment duration: 11.26, segment: '1080-7mbps00001.ts'
end
# yielded builder form (access outer scope)
playlist = M3u8::Playlist.build(version: 4) do |b|
files.each { |f| b.segment duration: 10.0, segment: f }
end
Build a master playlist:
playlist = M3u8::Playlist.build(independent_segments: true) do
media type: 'AUDIO', group_id: 'audio', name: 'English',
default: true, uri: 'eng/index.m3u8'
playlist bandwidth: 5_042_000, width: 1920, height: 1080,
profile: 'high', level: 4.1, audio_codec: 'aac-lc',
uri: 'hls/1080.m3u8'
playlist bandwidth: 2_387_000, width: 1280, height: 720,
profile: 'main', level: 3.1, audio_codec: 'aac-lc',
uri: 'hls/720.m3u8'
end
Build a media playlist:
playlist = M3u8::Playlist.build(version: 4, target: 12,
sequence: 1, type: 'VOD') do
key method: 'AES-128', uri: 'https://example.com/key.bin'
map uri: 'init.mp4'
segment duration: 11.34, segment: '00000.ts'
discontinuity
segment duration: 11.26, segment: '00001.ts'
end
Build an LL-HLS playlist:
sc = M3u8::ServerControlItem.new(
can_skip_until: 24.0, part_hold_back: 1.0,
can_block_reload: true
)
pi = M3u8::PartInfItem.new(part_target: 0.5)
playlist = M3u8::Playlist.build(
version: 9, target: 4, sequence: 100,
server_control: sc, part_inf: pi, live: true
) do
map uri: 'init.mp4'
segment duration: 4.0, segment: 'seg100.mp4'
part duration: 0.5, uri: 'seg101.0.mp4', independent: true
preload_hint type: 'PART', uri: 'seg101.1.mp4'
rendition_report uri: '../alt/index.m3u8',
last_msn: 101, last_part: 0
end
All DSL methods correspond to item classes: segment, playlist, media, session_data, session_key, content_steering, key, map, date_range, discontinuity, gap, time, bitrate, part, preload_hint, rendition_report, skip, define, playback_start.
Usage (creating playlists)
Create a master playlist and add child playlists for adaptive bitrate streaming:
require 'm3u8'
playlist = M3u8::Playlist.new
Create a new playlist item with options:
options = { width: 1920, height: 1080, profile: 'high', level: 4.1,
audio_codec: 'aac-lc', bandwidth: 540, uri: 'test.url' }
item = M3u8::PlaylistItem.new(options)
playlist.items << item
Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist:
hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
assoc_language: 'spoken', name: 'Francais', autoselect: true,
default: false, forced: true, uri: 'frelo/prog_index.m3u8' }
item = M3u8::MediaItem.new(hash)
playlist.items << item
Add Content Steering for dynamic CDN pathway selection:
item = M3u8::ContentSteeringItem.new(
server_uri: 'https://example.com/steering',
pathway_id: 'CDN-A'
)
playlist.items << item
Add variable definitions:
item = M3u8::DefineItem.new(name: 'base', value: 'https://example.com')
playlist.items << item
Add a session-level encryption key (master playlists):
item = M3u8::SessionKeyItem.new(
method: 'AES-128', uri: 'https://example.com/key.bin'
)
playlist.items << item
Add session-level data (master playlists):
item = M3u8::SessionDataItem.new(
data_id: 'com.example.title', value: 'My Video',
language: 'en'
)
playlist.items << item
Create a standard playlist and add MPEG-TS segments via SegmentItem:
options = { version: 1, cache: false, target: 12, sequence: 1 }
playlist = M3u8::Playlist.new(options)
item = M3u8::SegmentItem.new(duration: 11, segment: 'test.ts')
playlist.items << item
Media segment tags
Add an encryption key for subsequent segments:
item = M3u8::KeyItem.new(
method: 'AES-128',
uri: 'https://example.com/key.bin',
iv: '0x1234567890abcdef1234567890abcdef'
)
playlist.items << item
Specify an initialization segment (e.g. fMP4 header):
item = M3u8::MapItem.new(
uri: 'init.mp4', byterange: { length: 812, start: 0 }
)
playlist.items << item
Insert a timed metadata date range:
item = M3u8::DateRangeItem.new(
id: 'ad-break-1', start_date: '2024-06-01T12:00:00Z',
planned_duration: 30.0, cue: 'PRE',
client_attributes: { 'X-AD-ID' => '"foo"' }
)
playlist.items << item
HLS Interstitials
DateRangeItem supports HLS Interstitials attributes as first-class accessors for ad insertion, pre/post-rolls, and timeline integration:
item = M3u8::DateRangeItem.new(
id: 'ad-break-1',
class_name: 'com.apple.hls.interstitial',
start_date: '2024-06-01T12:00:00Z',
asset_uri: 'http://example.com/ad.m3u8',
resume_offset: 0.0,
playout_limit: 30.0,
restrict: 'SKIP,JUMP',
snap: 'OUT',
content_may_vary: 'YES'
)
playlist.items << item
| HLS Attribute | Accessor | Type |
|----------------------|---------------------|--------|
| X-ASSET-URI | asset_uri | String |
| X-ASSET-LIST | asset_list | String |
| X-RESUME-OFFSET | resume_offset | Float |
| X-PLAYOUT-LIMIT | playout_limit | Float |
| X-RESTRICT | restrict | String |
| X-SNAP | snap | String |
| X-TIMELINE-OCCUPIES | timeline_occupies | String |
| X-TIMELINE-STYLE | timeline_style | String |
| X-CONTENT-MAY-VARY | content_may_vary | String |
Signal an encoding discontinuity:
playlist.items << M3u8::DiscontinuityItem.new
Attach a program date/time to the next segment:
item = M3u8::TimeItem.new(time: Time.iso8601('2024-06-01T12:00:00Z'))
playlist.items << item
Mark a gap in segment availability:
playlist.items << M3u8::GapItem.new
Add a bitrate hint for upcoming segments:
item = M3u8::BitrateItem.new(bitrate: 1500)
playlist.items << item
Low-Latency HLS
Create an LL-HLS playlist with server control, partial segments, and preload hints:
server_control = M3u8::ServerControlItem.new(
can_skip_until: 24.0, part_hold_back: 1.0,
can_block_reload: true
)
part_inf = M3u8::PartInfItem.new(part_target: 0.5)
playlist = M3u8::Playlist.new(
version: 9, target: 4, sequence: 100,
server_control: server_control, part_inf: part_inf,
live: true
)
item = M3u8::SegmentItem.new(duration: 4.0, segment: 'seg100.mp4')
playlist.items << item
part = M3u8::PartItem.new(
duration: 0.5, uri: 'seg101.0.mp4', independent: true
)
playlist.items << part
hint = M3u8::PreloadHintItem.new(type: 'PART', uri: 'seg101.1.mp4')
playlist.items << hint
report = M3u8::RenditionReportItem.new(
uri: '../alt/index.m3u8', last_msn: 101, last_part: 0
)
playlist.items << report
Writing playlists
You can pass an IO object to the write method:
require 'tempfile'
file = Tempfile.new('test')
playlist.write(file)
You can also access the playlist as a string:
playlist.to_s
M3u8::Writer is the class that handles generating the playlist output.
Alternatively you can set codecs rather than having it generated automatically:
options = { width: 1920, height: 1080, codecs: 'avc1.66.30,mp4a.40.2',
bandwidth: 540, uri: 'test.url' }
item = M3u8::PlaylistItem.new(options)
