SkillAgentSearch skills...

M3u8

Parse and generate m3u8 playlists for Apple HTTP Live Streaming (HLS) in Ruby.

Install / Use

/learn @sethdeckard/M3u8

README

Gem Version CI

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)
View on GitHub
GitHub Stars112
CategoryDevelopment
Updated4d ago
Forks62

Languages

Ruby

Security Score

100/100

Audited on Mar 19, 2026

No findings