m3u8 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac7ba85dc1681fda7ba574fd13afc5a2a644726beb03cd10d80862a62d996a76
4
- data.tar.gz: 8ec9e60e8ca0a7c81f4fdea78aae937bde641d37baa6984aef90da1c97f6c068
3
+ metadata.gz: 7a9a7d6e157afd5fd7028323544bf9d851bd898890f90c66af6f64e672d7e456
4
+ data.tar.gz: ee452e25dfb96ee9634e9a00c287a87ac4640dceb2d965550e31199e0c0b4e26
5
5
  SHA512:
6
- metadata.gz: ede5d1bfd13f64815f5777485bd1a744ce0e13cecf0b51c9d657b7c2890b34f69e70dfba66f707b66007ca176308d99dba14f7a259dfdab2553e2f35e1199bba
7
- data.tar.gz: fb035f0a70f3f675d1c53b5ef46af63e4dd0fc88dad66666b0010eb66210b48eb7ad3b0748a81f55d8b95f0a8af1fb4492a0467bd8f0469fce5ef93b9cdb2b10
6
+ metadata.gz: f6b458aaf9a1a1060d19dca456020b54813f0c1f011615593e088a512a87ed32120cd5d509fb66dd3b12f85fe11a255304eede7852a00197cfd9513c918a33eb
7
+ data.tar.gz: 7c543fd8e6004adaf275adfe95c231b42fe0549805436f8dea3e77f2731f273084c99887781d0b69daf05ec1e53218fb8711be895fc0c14e9f0687f983a611b1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ **1.2.0**
2
+
3
+ * Added `Playlist.build` with block-based Builder DSL for concise playlist construction. Supports both `instance_eval` (clean DSL) and yielded builder (outer scope access) forms. All 19 item types have corresponding DSL methods.
4
+
5
+ ***
6
+
1
7
  **1.1.0**
2
8
 
3
9
  * Added convenience accessor methods to `Playlist` for filtering items by type: `segments`, `playlists`, `media_items`, `keys`, `maps`, `date_ranges`, `parts`, `session_data`.
data/CLAUDE.md CHANGED
@@ -1 +1 @@
1
- AGENTS.md
1
+ ./AGENTS.md
data/README.md CHANGED
@@ -30,6 +30,75 @@ Or install it yourself as:
30
30
 
31
31
  $ gem install m3u8
32
32
 
33
+ ## Usage (Builder DSL)
34
+
35
+ `Playlist.build` provides a block-based DSL for concise playlist construction. It supports two forms:
36
+
37
+ ```ruby
38
+ # instance_eval form (clean DSL)
39
+ playlist = M3u8::Playlist.build(version: 4, target: 12) do
40
+ segment duration: 11.34, segment: '1080-7mbps00000.ts'
41
+ segment duration: 11.26, segment: '1080-7mbps00001.ts'
42
+ end
43
+
44
+ # yielded builder form (access outer scope)
45
+ playlist = M3u8::Playlist.build(version: 4) do |b|
46
+ files.each { |f| b.segment duration: 10.0, segment: f }
47
+ end
48
+ ```
49
+
50
+ Build a master playlist:
51
+
52
+ ```ruby
53
+ playlist = M3u8::Playlist.build(independent_segments: true) do
54
+ media type: 'AUDIO', group_id: 'audio', name: 'English',
55
+ default: true, uri: 'eng/index.m3u8'
56
+ playlist bandwidth: 5_042_000, width: 1920, height: 1080,
57
+ profile: 'high', level: 4.1, audio_codec: 'aac-lc',
58
+ uri: 'hls/1080.m3u8'
59
+ playlist bandwidth: 2_387_000, width: 1280, height: 720,
60
+ profile: 'main', level: 3.1, audio_codec: 'aac-lc',
61
+ uri: 'hls/720.m3u8'
62
+ end
63
+ ```
64
+
65
+ Build a media playlist:
66
+
67
+ ```ruby
68
+ playlist = M3u8::Playlist.build(version: 4, target: 12,
69
+ sequence: 1, type: 'VOD') do
70
+ key method: 'AES-128', uri: 'https://example.com/key.bin'
71
+ map uri: 'init.mp4'
72
+ segment duration: 11.34, segment: '00000.ts'
73
+ discontinuity
74
+ segment duration: 11.26, segment: '00001.ts'
75
+ end
76
+ ```
77
+
78
+ Build an LL-HLS playlist:
79
+
80
+ ```ruby
81
+ sc = M3u8::ServerControlItem.new(
82
+ can_skip_until: 24.0, part_hold_back: 1.0,
83
+ can_block_reload: true
84
+ )
85
+ pi = M3u8::PartInfItem.new(part_target: 0.5)
86
+
87
+ playlist = M3u8::Playlist.build(
88
+ version: 9, target: 4, sequence: 100,
89
+ server_control: sc, part_inf: pi, live: true
90
+ ) do
91
+ map uri: 'init.mp4'
92
+ segment duration: 4.0, segment: 'seg100.mp4'
93
+ part duration: 0.5, uri: 'seg101.0.mp4', independent: true
94
+ preload_hint type: 'PART', uri: 'seg101.1.mp4'
95
+ rendition_report uri: '../alt/index.m3u8',
96
+ last_msn: 101, last_part: 0
97
+ end
98
+ ```
99
+
100
+ 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`.
101
+
33
102
  ## Usage (creating playlists)
34
103
 
35
104
  Create a master playlist and add child playlists for adaptive bitrate streaming:
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Builder provides a block-based DSL for constructing playlists
5
+ class Builder
6
+ ITEMS = {
7
+ segment: 'SegmentItem',
8
+ playlist: 'PlaylistItem',
9
+ media: 'MediaItem',
10
+ session_data: 'SessionDataItem',
11
+ session_key: 'SessionKeyItem',
12
+ content_steering: 'ContentSteeringItem',
13
+ key: 'KeyItem',
14
+ map: 'MapItem',
15
+ date_range: 'DateRangeItem',
16
+ time: 'TimeItem',
17
+ bitrate: 'BitrateItem',
18
+ part: 'PartItem',
19
+ preload_hint: 'PreloadHintItem',
20
+ rendition_report: 'RenditionReportItem',
21
+ skip: 'SkipItem',
22
+ define: 'DefineItem',
23
+ playback_start: 'PlaybackStart'
24
+ }.freeze
25
+
26
+ ZERO_ARG_ITEMS = {
27
+ discontinuity: 'DiscontinuityItem',
28
+ gap: 'GapItem'
29
+ }.freeze
30
+
31
+ def initialize(playlist)
32
+ @playlist = playlist
33
+ end
34
+
35
+ ITEMS.each do |method_name, class_name|
36
+ define_method(method_name) do |params = {}|
37
+ @playlist.items << M3u8.const_get(class_name).new(params)
38
+ end
39
+ end
40
+
41
+ ZERO_ARG_ITEMS.each do |method_name, class_name|
42
+ define_method(method_name) do
43
+ @playlist.items << M3u8.const_get(class_name).new
44
+ end
45
+ end
46
+ end
47
+ end
data/lib/m3u8/playlist.rb CHANGED
@@ -14,6 +14,17 @@ module M3u8
14
14
  @items = []
15
15
  end
16
16
 
17
+ def self.build(options = {}, &block)
18
+ playlist = new(options)
19
+ builder = Builder.new(playlist)
20
+ if block.arity == 1
21
+ yield builder
22
+ else
23
+ builder.instance_eval(&block)
24
+ end
25
+ playlist
26
+ end
27
+
17
28
  def self.codecs(options = {})
18
29
  item = PlaylistItem.new(options)
19
30
  item.codecs
data/lib/m3u8/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
4
4
  module M3u8
5
- VERSION = '1.1.0'
5
+ VERSION = '1.2.0'
6
6
  end
@@ -0,0 +1,338 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::Builder do
6
+ shared_examples 'a builder method' do |method, klass, params, checks|
7
+ it "adds a #{klass} to the playlist" do
8
+ pl = M3u8::Playlist.build { send(method, **params) }
9
+
10
+ item = pl.items.first
11
+ expect(item).to be_a(klass)
12
+ checks.each do |attr, value|
13
+ expect(item.public_send(attr)).to eq(value)
14
+ end
15
+ end
16
+ end
17
+
18
+ shared_examples 'a zero-arg builder method' do |method, klass|
19
+ it "adds a #{klass} to the playlist" do
20
+ pl = M3u8::Playlist.build { send(method) }
21
+ expect(pl.items.first).to be_a(klass)
22
+ end
23
+ end
24
+
25
+ describe '#segment' do
26
+ include_examples 'a builder method',
27
+ :segment, M3u8::SegmentItem,
28
+ { duration: 11.34,
29
+ segment: '1080-7mbps00000.ts' },
30
+ { duration: 11.34,
31
+ segment: '1080-7mbps00000.ts' }
32
+ end
33
+
34
+ describe '#playlist' do
35
+ include_examples 'a builder method',
36
+ :playlist, M3u8::PlaylistItem,
37
+ { bandwidth: 540, uri: 'test.url' },
38
+ { bandwidth: 540, uri: 'test.url' }
39
+ end
40
+
41
+ describe '#media' do
42
+ include_examples 'a builder method',
43
+ :media, M3u8::MediaItem,
44
+ { type: 'AUDIO', group_id: 'audio-lo',
45
+ name: 'English', default: true,
46
+ uri: 'eng/prog_index.m3u8' },
47
+ { type: 'AUDIO', group_id: 'audio-lo',
48
+ name: 'English' }
49
+ end
50
+
51
+ describe '#session_data' do
52
+ include_examples 'a builder method',
53
+ :session_data, M3u8::SessionDataItem,
54
+ { data_id: 'com.example.title',
55
+ value: 'My Video', language: 'en' },
56
+ { data_id: 'com.example.title',
57
+ value: 'My Video' }
58
+ end
59
+
60
+ describe '#session_key' do
61
+ include_examples 'a builder method',
62
+ :session_key, M3u8::SessionKeyItem,
63
+ { method: 'AES-128',
64
+ uri: 'https://example.com/key.bin' },
65
+ { method: 'AES-128' }
66
+ end
67
+
68
+ describe '#content_steering' do
69
+ include_examples 'a builder method',
70
+ :content_steering,
71
+ M3u8::ContentSteeringItem,
72
+ { server_uri: 'https://example.com/s',
73
+ pathway_id: 'CDN-A' },
74
+ { server_uri: 'https://example.com/s',
75
+ pathway_id: 'CDN-A' }
76
+ end
77
+
78
+ describe '#key' do
79
+ include_examples 'a builder method',
80
+ :key, M3u8::KeyItem,
81
+ { method: 'AES-128',
82
+ uri: 'https://example.com/key.bin' },
83
+ { method: 'AES-128' }
84
+ end
85
+
86
+ describe '#map' do
87
+ it 'adds a MapItem to the playlist' do
88
+ pl = M3u8::Playlist.build do
89
+ map uri: 'init.mp4',
90
+ byterange: { length: 812, start: 0 }
91
+ end
92
+
93
+ item = pl.items.first
94
+ expect(item).to be_a(M3u8::MapItem)
95
+ expect(item.uri).to eq('init.mp4')
96
+ expect(item.byterange.length).to eq(812)
97
+ end
98
+ end
99
+
100
+ describe '#date_range' do
101
+ include_examples 'a builder method',
102
+ :date_range, M3u8::DateRangeItem,
103
+ { id: 'ad-break-1',
104
+ start_date: '2024-06-01T12:00:00Z',
105
+ planned_duration: 30.0 },
106
+ { id: 'ad-break-1',
107
+ planned_duration: 30.0 }
108
+ end
109
+
110
+ describe '#discontinuity' do
111
+ include_examples 'a zero-arg builder method',
112
+ :discontinuity, M3u8::DiscontinuityItem
113
+ end
114
+
115
+ describe '#gap' do
116
+ include_examples 'a zero-arg builder method',
117
+ :gap, M3u8::GapItem
118
+ end
119
+
120
+ describe '#time' do
121
+ include_examples 'a builder method',
122
+ :time, M3u8::TimeItem,
123
+ { time: '2024-06-01T12:00:00Z' },
124
+ { time: '2024-06-01T12:00:00Z' }
125
+ end
126
+
127
+ describe '#bitrate' do
128
+ include_examples 'a builder method',
129
+ :bitrate, M3u8::BitrateItem,
130
+ { bitrate: 1500 },
131
+ { bitrate: 1500 }
132
+ end
133
+
134
+ describe '#part' do
135
+ include_examples 'a builder method',
136
+ :part, M3u8::PartItem,
137
+ { duration: 0.5, uri: 'seg101.0.mp4',
138
+ independent: true },
139
+ { duration: 0.5, uri: 'seg101.0.mp4' }
140
+ end
141
+
142
+ describe '#preload_hint' do
143
+ include_examples 'a builder method',
144
+ :preload_hint, M3u8::PreloadHintItem,
145
+ { type: 'PART', uri: 'seg101.1.mp4' },
146
+ { type: 'PART', uri: 'seg101.1.mp4' }
147
+ end
148
+
149
+ describe '#rendition_report' do
150
+ include_examples 'a builder method',
151
+ :rendition_report,
152
+ M3u8::RenditionReportItem,
153
+ { uri: '../alt/index.m3u8',
154
+ last_msn: 101, last_part: 0 },
155
+ { uri: '../alt/index.m3u8',
156
+ last_msn: 101 }
157
+ end
158
+
159
+ describe '#skip' do
160
+ include_examples 'a builder method',
161
+ :skip, M3u8::SkipItem,
162
+ { skipped_segments: 10 },
163
+ { skipped_segments: 10 }
164
+ end
165
+
166
+ describe '#define' do
167
+ include_examples 'a builder method',
168
+ :define, M3u8::DefineItem,
169
+ { name: 'base',
170
+ value: 'https://example.com' },
171
+ { name: 'base',
172
+ value: 'https://example.com' }
173
+ end
174
+
175
+ describe '#playback_start' do
176
+ include_examples 'a builder method',
177
+ :playback_start, M3u8::PlaybackStart,
178
+ { time_offset: 10.0, precise: true },
179
+ { time_offset: 10.0 }
180
+ end
181
+
182
+ describe 'Playlist.build' do
183
+ it 'returns a Playlist with options' do
184
+ playlist = M3u8::Playlist.build(version: 4,
185
+ target: 12) do
186
+ segment duration: 11.34, segment: 'test.ts'
187
+ end
188
+
189
+ expect(playlist).to be_a(M3u8::Playlist)
190
+ expect(playlist.version).to eq(4)
191
+ expect(playlist.target).to eq(12)
192
+ end
193
+
194
+ it 'supports yielded builder form' do
195
+ files = %w[seg1.ts seg2.ts]
196
+ playlist = M3u8::Playlist.build(version: 4) do |b|
197
+ files.each do |f|
198
+ b.segment duration: 10.0, segment: f
199
+ end
200
+ end
201
+
202
+ expect(playlist.items.size).to eq(2)
203
+ expect(playlist.items[0].segment).to eq('seg1.ts')
204
+ expect(playlist.items[1].segment).to eq('seg2.ts')
205
+ end
206
+ end
207
+
208
+ describe 'integration' do
209
+ it 'produces identical output to imperative API ' \
210
+ 'for a master playlist' do
211
+ imperative = M3u8::Playlist.new(
212
+ independent_segments: true
213
+ )
214
+ imperative.items << M3u8::PlaylistItem.new(
215
+ program_id: '1', bandwidth: 6400,
216
+ audio_codec: 'mp3', uri: 'lo/index.m3u8'
217
+ )
218
+ imperative.items << M3u8::PlaylistItem.new(
219
+ program_id: '2', bandwidth: 50_000,
220
+ width: 1920, height: 1080,
221
+ profile: 'high', level: 4.1,
222
+ audio_codec: 'aac-lc', uri: 'hi/index.m3u8'
223
+ )
224
+ imperative.items << M3u8::SessionDataItem.new(
225
+ data_id: 'com.test.title', value: 'Test',
226
+ language: 'en'
227
+ )
228
+
229
+ built = M3u8::Playlist.build(
230
+ independent_segments: true
231
+ ) do
232
+ playlist program_id: '1', bandwidth: 6400,
233
+ audio_codec: 'mp3', uri: 'lo/index.m3u8'
234
+ playlist program_id: '2', bandwidth: 50_000,
235
+ width: 1920, height: 1080,
236
+ profile: 'high', level: 4.1,
237
+ audio_codec: 'aac-lc',
238
+ uri: 'hi/index.m3u8'
239
+ session_data data_id: 'com.test.title',
240
+ value: 'Test', language: 'en'
241
+ end
242
+
243
+ expect(built.to_s).to eq(imperative.to_s)
244
+ end
245
+
246
+ it 'produces identical output to imperative API ' \
247
+ 'for a media playlist' do
248
+ imperative = M3u8::Playlist.new(
249
+ version: 7, cache: false, target: 12,
250
+ sequence: 1, type: 'VOD'
251
+ )
252
+ imperative.items << M3u8::KeyItem.new(
253
+ method: 'AES-128', uri: 'http://test.key',
254
+ iv: 'D512BBF', key_format: 'identity',
255
+ key_format_versions: '1/3'
256
+ )
257
+ imperative.items << M3u8::SegmentItem.new(
258
+ duration: 11.344644, segment: '00000.ts'
259
+ )
260
+ imperative.items << M3u8::DiscontinuityItem.new
261
+ imperative.items << M3u8::TimeItem.new(
262
+ time: '2024-06-01T12:00:00Z'
263
+ )
264
+ imperative.items << M3u8::SegmentItem.new(
265
+ duration: 11.261233, segment: '00001.ts'
266
+ )
267
+ imperative.items << M3u8::MapItem.new(
268
+ uri: 'init.mp4',
269
+ byterange: { length: 812, start: 0 }
270
+ )
271
+ imperative.items << M3u8::SegmentItem.new(
272
+ duration: 7.5, segment: '00002.ts'
273
+ )
274
+
275
+ built = M3u8::Playlist.build(
276
+ version: 7, cache: false, target: 12,
277
+ sequence: 1, type: 'VOD'
278
+ ) do
279
+ key method: 'AES-128', uri: 'http://test.key',
280
+ iv: 'D512BBF', key_format: 'identity',
281
+ key_format_versions: '1/3'
282
+ segment duration: 11.344644, segment: '00000.ts'
283
+ discontinuity
284
+ time time: '2024-06-01T12:00:00Z'
285
+ segment duration: 11.261233, segment: '00001.ts'
286
+ map uri: 'init.mp4',
287
+ byterange: { length: 812, start: 0 }
288
+ segment duration: 7.5, segment: '00002.ts'
289
+ end
290
+
291
+ expect(built.to_s).to eq(imperative.to_s)
292
+ end
293
+
294
+ it 'produces identical output to imperative API ' \
295
+ 'for an LL-HLS playlist' do
296
+ sc = M3u8::ServerControlItem.new(
297
+ can_skip_until: 24.0, part_hold_back: 1.0,
298
+ can_block_reload: true
299
+ )
300
+ pi = M3u8::PartInfItem.new(part_target: 0.5)
301
+ opts = {
302
+ version: 9, target: 4, sequence: 100,
303
+ server_control: sc, part_inf: pi, live: true
304
+ }
305
+
306
+ imperative = M3u8::Playlist.new(opts)
307
+ imperative.items << M3u8::MapItem.new(
308
+ uri: 'init.mp4'
309
+ )
310
+ imperative.items << M3u8::SegmentItem.new(
311
+ duration: 4.0, segment: 'seg100.mp4'
312
+ )
313
+ imperative.items << M3u8::PartItem.new(
314
+ duration: 0.5, uri: 'seg101.0.mp4',
315
+ independent: true
316
+ )
317
+ imperative.items << M3u8::PreloadHintItem.new(
318
+ type: 'PART', uri: 'seg101.1.mp4'
319
+ )
320
+ imperative.items << M3u8::RenditionReportItem.new(
321
+ uri: '../alt/index.m3u8',
322
+ last_msn: 101, last_part: 0
323
+ )
324
+
325
+ built = M3u8::Playlist.build(opts) do
326
+ map uri: 'init.mp4'
327
+ segment duration: 4.0, segment: 'seg100.mp4'
328
+ part duration: 0.5, uri: 'seg101.0.mp4',
329
+ independent: true
330
+ preload_hint type: 'PART', uri: 'seg101.1.mp4'
331
+ rendition_report uri: '../alt/index.m3u8',
332
+ last_msn: 101, last_part: 0
333
+ end
334
+
335
+ expect(built.to_s).to eq(imperative.to_s)
336
+ end
337
+ end
338
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: m3u8
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Deckard
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-07 00:00:00.000000000 Z
11
+ date: 2026-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -144,6 +144,7 @@ files:
144
144
  - Rakefile
145
145
  - lib/m3u8.rb
146
146
  - lib/m3u8/bitrate_item.rb
147
+ - lib/m3u8/builder.rb
147
148
  - lib/m3u8/byte_range.rb
148
149
  - lib/m3u8/content_steering_item.rb
149
150
  - lib/m3u8/date_range_item.rb
@@ -194,6 +195,7 @@ files:
194
195
  - spec/fixtures/variant_angles.m3u8
195
196
  - spec/fixtures/variant_audio.m3u8
196
197
  - spec/lib/m3u8/bitrate_item_spec.rb
198
+ - spec/lib/m3u8/builder_spec.rb
197
199
  - spec/lib/m3u8/byte_range_spec.rb
198
200
  - spec/lib/m3u8/content_steering_item_spec.rb
199
201
  - spec/lib/m3u8/date_range_item_spec.rb
@@ -225,7 +227,7 @@ homepage: https://github.com/sethdeckard/m3u8
225
227
  licenses:
226
228
  - MIT
227
229
  metadata: {}
228
- post_install_message:
230
+ post_install_message:
229
231
  rdoc_options: []
230
232
  require_paths:
231
233
  - lib
@@ -240,8 +242,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
240
242
  - !ruby/object:Gem::Version
241
243
  version: '0'
242
244
  requirements: []
243
- rubygems_version: 3.2.32
244
- signing_key:
245
+ rubygems_version: 3.0.3.1
246
+ signing_key:
245
247
  specification_version: 4
246
248
  summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
247
249
  test_files: []