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.
Ruby 3.0+
Add this line to your application's Gemfile:
gem 'm3u8'And then execute:
$ bundle
Or install it yourself as:
$ gem install m3u8
The gem includes a command-line tool for inspecting and validating playlists.
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
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
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 }
endBuild 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'
endBuild 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'
endBuild 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
endAll 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.
Create a master playlist and add child playlists for adaptive bitrate streaming:
require 'm3u8'
playlist = M3u8::Playlist.newCreate 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 << itemAdd 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 << itemAdd Content Steering for dynamic CDN pathway selection:
item = M3u8::ContentSteeringItem.new(
server_uri: 'https://example.com/steering',
pathway_id: 'CDN-A'
)
playlist.items << itemAdd variable definitions:
item = M3u8::DefineItem.new(name: 'base', value: 'https://example.com')
playlist.items << itemAdd a session-level encryption key (master playlists):
item = M3u8::SessionKeyItem.new(
method: 'AES-128', uri: 'https://example.com/key.bin'
)
playlist.items << itemAdd session-level data (master playlists):
item = M3u8::SessionDataItem.new(
data_id: 'com.example.title', value: 'My Video',
language: 'en'
)
playlist.items << itemCreate 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 << itemAdd an encryption key for subsequent segments:
item = M3u8::KeyItem.new(
method: 'AES-128',
uri: 'https://example.com/key.bin',
iv: '0x1234567890abcdef1234567890abcdef'
)
playlist.items << itemSpecify an initialization segment (e.g. fMP4 header):
item = M3u8::MapItem.new(
uri: 'init.mp4', byterange: { length: 812, start: 0 }
)
playlist.items << itemInsert 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 << itemDateRangeItem 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.newAttach a program date/time to the next segment:
item = M3u8::TimeItem.new(time: Time.iso8601('2024-06-01T12:00:00Z'))
playlist.items << itemMark a gap in segment availability:
playlist.items << M3u8::GapItem.newAdd a bitrate hint for upcoming segments:
item = M3u8::BitrateItem.new(bitrate: 1500)
playlist.items << itemCreate 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 << reportYou 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_sM3u8::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)Playlists returned by Playlist.build and Playlist.read are frozen (deeply immutable). Items, nested objects, and the items array are all frozen, preventing accidental mutation after construction:
playlist = M3u8::Playlist.read(File.open('master.m3u8'))
playlist.frozen? # => true
playlist.items.frozen? # => true
playlist.items.first.frozen? # => truePlaylists created with Playlist.new remain mutable. Call freeze explicitly when ready:
playlist = M3u8::Playlist.new
playlist.items << M3u8::SegmentItem.new(duration: 10.0, segment: 'test.ts')
playlist.freezeFrozen playlists still support to_s and write for output.
DateRangeItem stores SCTE-35 values (scte35_cmd, scte35_out, scte35_in) as raw hex strings. Convenience methods parse them into structured objects:
playlist = M3u8::Playlist.read(file)
date_range = playlist.date_ranges.first
info = date_range.scte35_out_info
info.table_id # => 252 (0xFC)
info.pts_adjustment # => 0
info.tier # => 4095
info.splice_command_type # => 5
cmd = info.splice_command # => Scte35SpliceInsert
cmd.splice_event_id # => 1
cmd.out_of_network_indicator # => true
cmd.pts_time # => 90000
cmd.break_duration # => 2700000
cmd.break_auto_return # => trueParse any SCTE-35 hex string directly:
info = M3u8::Scte35.parse('0xFC301100...')
info.to_s # => original hex string| Type | Class | Key attributes |
|---|---|---|
| 0x00 | Scte35SpliceNull |
(none) |
| 0x05 | Scte35SpliceInsert |
splice_event_id, out_of_network_indicator, pts_time, break_duration, break_auto_return, unique_program_id, avail_num, avails_expected |
| 0x06 | Scte35TimeSignal |
pts_time |
Unknown command types store raw bytes in splice_command.
Segmentation descriptors (tag 0x02, identifier CUEI) are parsed as Scte35SegmentationDescriptor:
desc = info.descriptors.first
desc.segmentation_event_id # => 1
desc.segmentation_type_id # => 0x30
desc.segmentation_duration # => 2700000
desc.segmentation_upid_type # => 9
desc.segmentation_upid # => "SIGNAL123"
desc.segment_num # => 0
desc.segments_expected # => 0Unknown descriptor tags store raw bytes.
Check whether a playlist is valid and inspect specific errors:
playlist.valid?
# => true
playlist.errors
# => []When a playlist has issues, errors returns descriptive messages:
playlist.valid?
# => false
playlist.errors
# => ["Playlist contains both master and media items"]The following validations are performed:
- Mixed item types (both master and media items in one playlist)
- Target duration less than any segment's rounded duration
- Segment items missing a URI or having a negative duration
- Playlist items missing a URI or valid bandwidth
- Media items missing type, group ID, or name
- Key and session key items missing a URI when method is not NONE
- Session data items missing data ID, or having both/neither value and URI
- Part items missing a URI or duration
valid? delegates to errors.empty? and both are recomputed on each call.
file = File.open 'spec/fixtures/master.m3u8'
playlist = M3u8::Playlist.read(file)
playlist.master?
# => trueQuery playlist properties:
playlist.master?
# => true (contains variant streams)
playlist.live?
# => false (master playlists are never live)For media playlists, duration returns total segment duration:
media = M3u8::Playlist.read(
File.open('spec/fixtures/event_playlist.m3u8')
)
media.live?
# => false
media.duration
# => 17.0 (sum of all segment durations)Access items and their attributes:
playlist.items.first
# => #<M3u8::PlaylistItem ...>
media.segments.first.duration
# => 6.0
media.segments.first.segment
# => "segment0.mp4"Convenience methods filter items by type:
playlist.playlists # => [PlaylistItem, ...]
playlist.segments # => [SegmentItem, ...]
playlist.media_items # => [MediaItem, ...]
playlist.keys # => [KeyItem, ...]
playlist.maps # => [MapItem, ...]
playlist.date_ranges # => [DateRangeItem, ...]
playlist.parts # => [PartItem, ...]
playlist.session_data # => [SessionDataItem, ...]Parse an LL-HLS playlist:
file = File.open 'spec/fixtures/ll_hls_playlist.m3u8'
playlist = M3u8::Playlist.read(file)
playlist.server_control.can_block_reload
# => true
playlist.part_inf.part_target
# => 0.5M3u8::Reader is the class that handles parsing if you want more control over the process.
Generate the codec string based on audio and video codec options without dealing with a playlist instance:
options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
codecs = M3u8::Playlist.codecs(options)
# => "avc1.66.30,mp4a.40.2"| Profile | Description |
|---|---|
baseline, main, high |
H.264/AVC |
hevc-main, hevc-main-10 |
HEVC/H.265 |
av1-main, av1-high |
AV1 |
| Value | Codec |
|---|---|
aac-lc |
AAC-LC |
he-aac |
HE-AAC |
mp3 |
MP3 |
ac-3 |
AC-3 (Dolby Digital) |
ec-3, e-ac-3 |
E-AC-3 (Dolby Digital Plus) |
flac |
FLAC |
opus |
Opus |
EXT-X-STREAM-INF/EXT-X-I-FRAME-STREAM-INF— includingSTABLE-VARIANT-ID,VIDEO-RANGE,ALLOWED-CPC,PATHWAY-ID,REQ-VIDEO-LAYOUT,SUPPLEMENTAL-CODECS,SCOREEXT-X-MEDIA— includingSTABLE-RENDITION-ID,BIT-DEPTH,SAMPLE-RATEEXT-X-SESSION-DATAEXT-X-SESSION-KEYEXT-X-CONTENT-STEERING
EXT-X-TARGETDURATIONEXT-X-MEDIA-SEQUENCEEXT-X-DISCONTINUITY-SEQUENCEEXT-X-PLAYLIST-TYPEEXT-X-I-FRAMES-ONLYEXT-X-ALLOW-CACHEEXT-X-ENDLIST
EXTINFEXT-X-BYTERANGEEXT-X-DISCONTINUITYEXT-X-KEYEXT-X-MAPEXT-X-PROGRAM-DATE-TIMEEXT-X-DATERANGEEXT-X-GAPEXT-X-BITRATE
EXT-X-INDEPENDENT-SEGMENTSEXT-X-STARTEXT-X-DEFINEEXT-X-VERSION
EXT-X-SERVER-CONTROLEXT-X-PART-INFEXT-X-PARTEXT-X-SKIPEXT-X-PRELOAD-HINTEXT-X-RENDITION-REPORT
- Fork it ( https://github.com/sethdeckard/m3u8/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Run the specs, make sure they pass and that new features are covered. Code coverage should be 100%.
- Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
MIT License - See LICENSE.txt for details.