m3u8 0.8.2 → 1.8.1
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 +5 -5
- data/.github/workflows/ci.yml +23 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +31 -0
- data/CHANGELOG.md +107 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +1 -1
- data/README.md +524 -40
- data/Rakefile +1 -0
- data/bin/m3u8 +6 -0
- data/lib/m3u8/attribute_formatter.rb +47 -0
- data/lib/m3u8/bitrate_item.rb +31 -0
- data/lib/m3u8/builder.rb +48 -0
- data/lib/m3u8/byte_range.rb +10 -0
- data/lib/m3u8/cli/inspect_command.rb +97 -0
- data/lib/m3u8/cli/validate_command.rb +24 -0
- data/lib/m3u8/cli.rb +116 -0
- data/lib/m3u8/codecs.rb +89 -0
- data/lib/m3u8/content_steering_item.rb +45 -0
- data/lib/m3u8/date_range_item.rb +135 -64
- data/lib/m3u8/define_item.rb +54 -0
- data/lib/m3u8/discontinuity_item.rb +3 -0
- data/lib/m3u8/encryptable.rb +27 -30
- data/lib/m3u8/error.rb +1 -0
- data/lib/m3u8/gap_item.rb +14 -0
- data/lib/m3u8/key_item.rb +7 -0
- data/lib/m3u8/map_item.rb +16 -5
- data/lib/m3u8/media_item.rb +48 -76
- data/lib/m3u8/part_inf_item.rb +35 -0
- data/lib/m3u8/part_item.rb +67 -0
- data/lib/m3u8/playback_start.rb +19 -12
- data/lib/m3u8/playlist.rb +221 -13
- data/lib/m3u8/playlist_item.rb +128 -124
- data/lib/m3u8/preload_hint_item.rb +54 -0
- data/lib/m3u8/reader.rb +86 -28
- data/lib/m3u8/rendition_report_item.rb +48 -0
- data/lib/m3u8/scte35.rb +130 -0
- data/lib/m3u8/scte35_bit_reader.rb +51 -0
- data/lib/m3u8/scte35_segmentation_descriptor.rb +54 -0
- data/lib/m3u8/scte35_splice_insert.rb +62 -0
- data/lib/m3u8/scte35_splice_null.rb +8 -0
- data/lib/m3u8/scte35_time_signal.rb +19 -0
- data/lib/m3u8/segment_item.rb +37 -3
- data/lib/m3u8/server_control_item.rb +69 -0
- data/lib/m3u8/session_data_item.rb +17 -28
- data/lib/m3u8/session_key_item.rb +8 -1
- data/lib/m3u8/skip_item.rb +48 -0
- data/lib/m3u8/time_item.rb +10 -0
- data/lib/m3u8/version.rb +1 -1
- data/lib/m3u8/writer.rb +24 -1
- data/lib/m3u8.rb +30 -6
- data/m3u8.gemspec +12 -12
- data/spec/fixtures/content_steering.m3u8 +10 -0
- data/spec/fixtures/daterange_playlist.m3u8 +14 -0
- data/spec/fixtures/encrypted_discontinuity.m3u8 +17 -0
- data/spec/fixtures/event_playlist.m3u8 +18 -0
- data/spec/fixtures/gap_playlist.m3u8 +14 -0
- data/spec/fixtures/ll_hls_advanced.m3u8 +18 -0
- data/spec/fixtures/ll_hls_playlist.m3u8 +20 -0
- data/spec/fixtures/master_full.m3u8 +14 -0
- data/spec/fixtures/master_v13.m3u8 +8 -0
- data/spec/lib/m3u8/bitrate_item_spec.rb +26 -0
- data/spec/lib/m3u8/builder_spec.rb +352 -0
- data/spec/lib/m3u8/byte_range_spec.rb +1 -0
- data/spec/lib/m3u8/cli/inspect_command_spec.rb +102 -0
- data/spec/lib/m3u8/cli/validate_command_spec.rb +39 -0
- data/spec/lib/m3u8/cli_spec.rb +104 -0
- data/spec/lib/m3u8/content_steering_item_spec.rb +56 -0
- data/spec/lib/m3u8/date_range_item_spec.rb +159 -31
- data/spec/lib/m3u8/define_item_spec.rb +59 -0
- data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -0
- data/spec/lib/m3u8/gap_item_spec.rb +12 -0
- data/spec/lib/m3u8/key_item_spec.rb +1 -0
- data/spec/lib/m3u8/map_item_spec.rb +1 -0
- data/spec/lib/m3u8/media_item_spec.rb +34 -0
- data/spec/lib/m3u8/part_inf_item_spec.rb +27 -0
- data/spec/lib/m3u8/part_item_spec.rb +67 -0
- data/spec/lib/m3u8/playback_start_spec.rb +4 -5
- data/spec/lib/m3u8/playlist_item_spec.rb +130 -17
- data/spec/lib/m3u8/playlist_spec.rb +545 -13
- data/spec/lib/m3u8/preload_hint_item_spec.rb +57 -0
- data/spec/lib/m3u8/reader_spec.rb +376 -29
- data/spec/lib/m3u8/rendition_report_item_spec.rb +56 -0
- data/spec/lib/m3u8/round_trip_spec.rb +152 -0
- data/spec/lib/m3u8/scte35_bit_reader_spec.rb +106 -0
- data/spec/lib/m3u8/scte35_segmentation_descriptor_spec.rb +143 -0
- data/spec/lib/m3u8/scte35_spec.rb +94 -0
- data/spec/lib/m3u8/scte35_splice_insert_spec.rb +185 -0
- data/spec/lib/m3u8/scte35_splice_null_spec.rb +12 -0
- data/spec/lib/m3u8/scte35_time_signal_spec.rb +50 -0
- data/spec/lib/m3u8/segment_item_spec.rb +47 -0
- data/spec/lib/m3u8/server_control_item_spec.rb +64 -0
- data/spec/lib/m3u8/session_data_item_spec.rb +1 -0
- data/spec/lib/m3u8/session_key_item_spec.rb +1 -0
- data/spec/lib/m3u8/skip_item_spec.rb +48 -0
- data/spec/lib/m3u8/time_item_spec.rb +1 -0
- data/spec/lib/m3u8/writer_spec.rb +69 -30
- data/spec/lib/m3u8_spec.rb +1 -0
- data/spec/spec_helper.rb +4 -87
- metadata +70 -129
- data/.hound.yml +0 -3
- data/.travis.yml +0 -8
- data/Guardfile +0 -6
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require 'spec_helper'
|
|
3
4
|
|
|
4
5
|
describe M3u8::Playlist do
|
|
@@ -43,12 +44,25 @@ describe M3u8::Playlist do
|
|
|
43
44
|
codecs = described_class.codecs(options)
|
|
44
45
|
expect(codecs).to eq('avc1.66.30,mp4a.40.2')
|
|
45
46
|
end
|
|
47
|
+
|
|
48
|
+
it 'generates HEVC codecs string' do
|
|
49
|
+
options = { profile: 'hevc-main', level: 4.0, audio_codec: 'ac-3' }
|
|
50
|
+
codecs = described_class.codecs(options)
|
|
51
|
+
expect(codecs).to eq('hvc1.1.6.L120.B0,ac-3')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'generates AV1 codecs string' do
|
|
55
|
+
options = { profile: 'av1-main', level: 5.0, audio_codec: 'opus' }
|
|
56
|
+
codecs = described_class.codecs(options)
|
|
57
|
+
expect(codecs).to eq('av01.0.12M.08,Opus')
|
|
58
|
+
end
|
|
46
59
|
end
|
|
47
60
|
|
|
48
61
|
describe '.read' do
|
|
49
62
|
it 'returns new playlist from content' do
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
playlist = described_class.read(
|
|
64
|
+
File.read('spec/fixtures/master.m3u8')
|
|
65
|
+
)
|
|
52
66
|
expect(playlist.master?).to be true
|
|
53
67
|
expect(playlist.items.size).to eq(8)
|
|
54
68
|
end
|
|
@@ -168,6 +182,7 @@ describe M3u8::Playlist do
|
|
|
168
182
|
end
|
|
169
183
|
|
|
170
184
|
it 'returns media playlist text' do
|
|
185
|
+
playlist = described_class.new(target: 12)
|
|
171
186
|
options = { duration: 11.344644, segment: '1080-7mbps00000.ts' }
|
|
172
187
|
item = M3u8::SegmentItem.new(options)
|
|
173
188
|
playlist.items << item
|
|
@@ -177,13 +192,13 @@ describe M3u8::Playlist do
|
|
|
177
192
|
playlist.items << item
|
|
178
193
|
|
|
179
194
|
expected = "#EXTM3U\n" \
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
"#EXT-X-MEDIA-SEQUENCE:0\n" \
|
|
196
|
+
"#EXT-X-TARGETDURATION:12\n" \
|
|
197
|
+
"#EXTINF:11.344644,\n" \
|
|
198
|
+
"1080-7mbps00000.ts\n" \
|
|
199
|
+
"#EXTINF:11.261233,\n" \
|
|
200
|
+
"1080-7mbps00001.ts\n" \
|
|
201
|
+
"#EXT-X-ENDLIST\n"
|
|
187
202
|
expect(playlist.to_s).to eq(expected)
|
|
188
203
|
end
|
|
189
204
|
end
|
|
@@ -223,6 +238,524 @@ describe M3u8::Playlist do
|
|
|
223
238
|
end
|
|
224
239
|
end
|
|
225
240
|
|
|
241
|
+
describe '#errors' do
|
|
242
|
+
context 'when playlist is empty' do
|
|
243
|
+
it 'returns no errors' do
|
|
244
|
+
expect(playlist.errors).to be_empty
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
context 'when playlist has only master items' do
|
|
249
|
+
it 'returns no errors' do
|
|
250
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
251
|
+
bandwidth: 540, uri: 'test.url'
|
|
252
|
+
)
|
|
253
|
+
expect(playlist.errors).to be_empty
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
context 'when playlist has only media items' do
|
|
258
|
+
it 'returns no errors' do
|
|
259
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
260
|
+
duration: 10.0, segment: 'test.ts'
|
|
261
|
+
)
|
|
262
|
+
expect(playlist.errors).to be_empty
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
context 'when segment duration exceeds target duration' do
|
|
267
|
+
it 'returns target duration error' do
|
|
268
|
+
playlist = described_class.new(target: 10)
|
|
269
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
270
|
+
duration: 12.1, segment: 'test.ts'
|
|
271
|
+
)
|
|
272
|
+
expect(playlist.errors).to include(
|
|
273
|
+
'Target duration 10 is less than segment duration of 12'
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
context 'when segment duration rounds to target' do
|
|
279
|
+
it 'returns no errors' do
|
|
280
|
+
playlist = described_class.new(target: 11)
|
|
281
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
282
|
+
duration: 10.5, segment: 'test.ts'
|
|
283
|
+
)
|
|
284
|
+
expect(playlist.errors).to be_empty
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
context 'when playlist is master' do
|
|
289
|
+
it 'skips target duration check' do
|
|
290
|
+
playlist = described_class.new(master: true)
|
|
291
|
+
expect(playlist.errors).to be_empty
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
context 'when segment has no URI' do
|
|
296
|
+
it 'returns missing segment error' do
|
|
297
|
+
playlist.items << M3u8::SegmentItem.new(duration: 10.0)
|
|
298
|
+
expect(playlist.errors).to include(
|
|
299
|
+
'Segment item requires a segment URI'
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
context 'when segment has negative duration' do
|
|
305
|
+
it 'returns negative duration error' do
|
|
306
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
307
|
+
duration: -1.0, segment: 'test.ts'
|
|
308
|
+
)
|
|
309
|
+
expect(playlist.errors).to include(
|
|
310
|
+
'Segment item has negative duration'
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
context 'when segment has zero duration' do
|
|
316
|
+
it 'returns no errors' do
|
|
317
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
318
|
+
duration: 0.0, segment: 'test.ts'
|
|
319
|
+
)
|
|
320
|
+
expect(playlist.errors).to be_empty
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
context 'when multiple segments are invalid' do
|
|
325
|
+
it 'accumulates errors' do
|
|
326
|
+
playlist.items << M3u8::SegmentItem.new(duration: 10.0)
|
|
327
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
328
|
+
duration: -1.0, segment: 'test.ts'
|
|
329
|
+
)
|
|
330
|
+
errors = playlist.errors
|
|
331
|
+
expect(errors).to include(
|
|
332
|
+
'Segment item requires a segment URI'
|
|
333
|
+
)
|
|
334
|
+
expect(errors).to include(
|
|
335
|
+
'Segment item has negative duration'
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
context 'when playlist item has no bandwidth' do
|
|
341
|
+
it 'returns missing bandwidth error' do
|
|
342
|
+
playlist.items << M3u8::PlaylistItem.new(uri: 'test.url')
|
|
343
|
+
expect(playlist.errors).to include(
|
|
344
|
+
'Playlist item requires a bandwidth'
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
context 'when playlist item has no URI and is not iframe' do
|
|
350
|
+
it 'returns missing URI error' do
|
|
351
|
+
playlist.items << M3u8::PlaylistItem.new(bandwidth: 540)
|
|
352
|
+
expect(playlist.errors).to include(
|
|
353
|
+
'Playlist item requires a URI'
|
|
354
|
+
)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
context 'when playlist item has zero bandwidth' do
|
|
359
|
+
it 'returns missing bandwidth error' do
|
|
360
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
361
|
+
bandwidth: 0, uri: 'test.url'
|
|
362
|
+
)
|
|
363
|
+
expect(playlist.errors).to include(
|
|
364
|
+
'Playlist item requires a bandwidth'
|
|
365
|
+
)
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
context 'when iframe playlist item has no URI' do
|
|
370
|
+
it 'returns missing URI error' do
|
|
371
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
372
|
+
bandwidth: 540, iframe: true
|
|
373
|
+
)
|
|
374
|
+
expect(playlist.errors).to include(
|
|
375
|
+
'Playlist item requires a URI'
|
|
376
|
+
)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
context 'when media item is missing type' do
|
|
381
|
+
it 'returns missing type error' do
|
|
382
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
383
|
+
bandwidth: 540, uri: 'test.url'
|
|
384
|
+
)
|
|
385
|
+
playlist.items << M3u8::MediaItem.new(
|
|
386
|
+
group_id: 'audio', name: 'English'
|
|
387
|
+
)
|
|
388
|
+
expect(playlist.errors).to include(
|
|
389
|
+
'Media item requires a type'
|
|
390
|
+
)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
context 'when media item is missing group_id' do
|
|
395
|
+
it 'returns missing group_id error' do
|
|
396
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
397
|
+
bandwidth: 540, uri: 'test.url'
|
|
398
|
+
)
|
|
399
|
+
playlist.items << M3u8::MediaItem.new(
|
|
400
|
+
type: 'AUDIO', name: 'English'
|
|
401
|
+
)
|
|
402
|
+
expect(playlist.errors).to include(
|
|
403
|
+
'Media item requires a group ID'
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
context 'when media item is missing name' do
|
|
409
|
+
it 'returns missing name error' do
|
|
410
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
411
|
+
bandwidth: 540, uri: 'test.url'
|
|
412
|
+
)
|
|
413
|
+
playlist.items << M3u8::MediaItem.new(
|
|
414
|
+
type: 'AUDIO', group_id: 'audio'
|
|
415
|
+
)
|
|
416
|
+
expect(playlist.errors).to include(
|
|
417
|
+
'Media item requires a name'
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
context 'when key item has method but no URI' do
|
|
423
|
+
it 'returns missing URI error' do
|
|
424
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
425
|
+
duration: 10.0, segment: 'test.ts'
|
|
426
|
+
)
|
|
427
|
+
playlist.items << M3u8::KeyItem.new(method: 'AES-128')
|
|
428
|
+
expect(playlist.errors).to include(
|
|
429
|
+
'Key item requires a URI when method is not NONE'
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
context 'when key item method is NONE' do
|
|
435
|
+
it 'returns no errors' do
|
|
436
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
437
|
+
duration: 10.0, segment: 'test.ts'
|
|
438
|
+
)
|
|
439
|
+
playlist.items << M3u8::KeyItem.new(method: 'NONE')
|
|
440
|
+
expect(playlist.errors).to be_empty
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
context 'when session key item has method but no URI' do
|
|
445
|
+
it 'returns missing URI error' do
|
|
446
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
447
|
+
bandwidth: 540, uri: 'test.url'
|
|
448
|
+
)
|
|
449
|
+
playlist.items << M3u8::SessionKeyItem.new(
|
|
450
|
+
method: 'AES-128'
|
|
451
|
+
)
|
|
452
|
+
expect(playlist.errors).to include(
|
|
453
|
+
'Session key item requires a URI when method is not NONE'
|
|
454
|
+
)
|
|
455
|
+
end
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
context 'when session data item has no data_id' do
|
|
459
|
+
it 'returns missing data_id error' do
|
|
460
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
461
|
+
bandwidth: 540, uri: 'test.url'
|
|
462
|
+
)
|
|
463
|
+
playlist.items << M3u8::SessionDataItem.new(
|
|
464
|
+
value: 'Test'
|
|
465
|
+
)
|
|
466
|
+
expect(playlist.errors).to include(
|
|
467
|
+
'Session data item requires a data ID'
|
|
468
|
+
)
|
|
469
|
+
end
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
context 'when session data item has both value and uri' do
|
|
473
|
+
it 'returns conflict error' do
|
|
474
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
475
|
+
bandwidth: 540, uri: 'test.url'
|
|
476
|
+
)
|
|
477
|
+
playlist.items << M3u8::SessionDataItem.new(
|
|
478
|
+
data_id: 'com.test', value: 'Test',
|
|
479
|
+
uri: 'http://test'
|
|
480
|
+
)
|
|
481
|
+
expect(playlist.errors).to include(
|
|
482
|
+
'Session data item cannot have both value and URI'
|
|
483
|
+
)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
context 'when session data item has neither value nor uri' do
|
|
488
|
+
it 'returns missing value error' do
|
|
489
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
490
|
+
bandwidth: 540, uri: 'test.url'
|
|
491
|
+
)
|
|
492
|
+
playlist.items << M3u8::SessionDataItem.new(
|
|
493
|
+
data_id: 'com.test'
|
|
494
|
+
)
|
|
495
|
+
expect(playlist.errors).to include(
|
|
496
|
+
'Session data item requires a value or URI'
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
context 'when session data item has only value' do
|
|
502
|
+
it 'returns no session data errors' do
|
|
503
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
504
|
+
bandwidth: 540, uri: 'test.url'
|
|
505
|
+
)
|
|
506
|
+
playlist.items << M3u8::SessionDataItem.new(
|
|
507
|
+
data_id: 'com.test', value: 'Test'
|
|
508
|
+
)
|
|
509
|
+
expect(playlist.errors).to be_empty
|
|
510
|
+
end
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
context 'when part item has no URI' do
|
|
514
|
+
it 'returns missing URI error' do
|
|
515
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
516
|
+
duration: 4.0, segment: 'seg.mp4'
|
|
517
|
+
)
|
|
518
|
+
playlist.items << M3u8::PartItem.new(duration: 0.5)
|
|
519
|
+
expect(playlist.errors).to include(
|
|
520
|
+
'Part item requires a URI'
|
|
521
|
+
)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
context 'when part item has no duration' do
|
|
526
|
+
it 'returns missing duration error' do
|
|
527
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
528
|
+
duration: 4.0, segment: 'seg.mp4'
|
|
529
|
+
)
|
|
530
|
+
playlist.items << M3u8::PartItem.new(uri: 'seg.0.mp4')
|
|
531
|
+
expect(playlist.errors).to include(
|
|
532
|
+
'Part item requires a duration'
|
|
533
|
+
)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
context 'when part item is valid' do
|
|
538
|
+
it 'returns no part errors' do
|
|
539
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
540
|
+
duration: 4.0, segment: 'seg.mp4'
|
|
541
|
+
)
|
|
542
|
+
playlist.items << M3u8::PartItem.new(
|
|
543
|
+
duration: 0.5, uri: 'seg.0.mp4'
|
|
544
|
+
)
|
|
545
|
+
expect(playlist.errors).to be_empty
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
context 'when playlist has mixed items' do
|
|
550
|
+
it 'returns mixed items error' do
|
|
551
|
+
playlist.items << M3u8::PlaylistItem.new(
|
|
552
|
+
bandwidth: 540, uri: 'test.url'
|
|
553
|
+
)
|
|
554
|
+
playlist.items << M3u8::SegmentItem.new(
|
|
555
|
+
duration: 10.0, segment: 'test.ts'
|
|
556
|
+
)
|
|
557
|
+
expect(playlist.errors).to include(
|
|
558
|
+
'Playlist contains both master and media items'
|
|
559
|
+
)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
describe '#segments' do
|
|
565
|
+
it 'returns only segment items' do
|
|
566
|
+
playlist = described_class.read(
|
|
567
|
+
File.read('spec/fixtures/playlist.m3u8')
|
|
568
|
+
)
|
|
569
|
+
expect(playlist.segments).to all be_a(M3u8::SegmentItem)
|
|
570
|
+
expect(playlist.segments.size).to eq(138)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
describe '#playlists' do
|
|
575
|
+
it 'returns only playlist items' do
|
|
576
|
+
playlist = described_class.read(
|
|
577
|
+
File.read('spec/fixtures/master.m3u8')
|
|
578
|
+
)
|
|
579
|
+
expect(playlist.playlists).to all be_a(M3u8::PlaylistItem)
|
|
580
|
+
expect(playlist.playlists.size).to eq(6)
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
describe '#media_items' do
|
|
585
|
+
it 'returns only media items' do
|
|
586
|
+
playlist = described_class.read(
|
|
587
|
+
File.read('spec/fixtures/variant_audio.m3u8')
|
|
588
|
+
)
|
|
589
|
+
expect(playlist.media_items).to all be_a(M3u8::MediaItem)
|
|
590
|
+
expect(playlist.media_items.size).to eq(6)
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
describe '#keys' do
|
|
595
|
+
it 'returns only key items' do
|
|
596
|
+
playlist = described_class.read(
|
|
597
|
+
File.read('spec/fixtures/encrypted.m3u8')
|
|
598
|
+
)
|
|
599
|
+
expect(playlist.keys).to all be_a(M3u8::KeyItem)
|
|
600
|
+
expect(playlist.keys.size).to eq(2)
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
describe '#maps' do
|
|
605
|
+
it 'returns only map items' do
|
|
606
|
+
playlist = described_class.read(
|
|
607
|
+
File.read('spec/fixtures/map_playlist.m3u8')
|
|
608
|
+
)
|
|
609
|
+
expect(playlist.maps).to all be_a(M3u8::MapItem)
|
|
610
|
+
expect(playlist.maps.size).to eq(1)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
describe '#date_ranges' do
|
|
615
|
+
it 'returns only date range items' do
|
|
616
|
+
playlist = described_class.read(
|
|
617
|
+
File.read('spec/fixtures/daterange_playlist.m3u8')
|
|
618
|
+
)
|
|
619
|
+
expect(playlist.date_ranges)
|
|
620
|
+
.to all be_a(M3u8::DateRangeItem)
|
|
621
|
+
expect(playlist.date_ranges.size).to eq(3)
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
describe '#parts' do
|
|
626
|
+
it 'returns only part items' do
|
|
627
|
+
playlist = described_class.read(
|
|
628
|
+
File.read('spec/fixtures/ll_hls_playlist.m3u8')
|
|
629
|
+
)
|
|
630
|
+
expect(playlist.parts).to all be_a(M3u8::PartItem)
|
|
631
|
+
expect(playlist.parts.size).to eq(5)
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
describe '#session_data' do
|
|
636
|
+
it 'returns only session data items' do
|
|
637
|
+
playlist = described_class.read(
|
|
638
|
+
File.read('spec/fixtures/session_data.m3u8')
|
|
639
|
+
)
|
|
640
|
+
expect(playlist.session_data)
|
|
641
|
+
.to all be_a(M3u8::SessionDataItem)
|
|
642
|
+
expect(playlist.session_data.size).to eq(3)
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
describe '#session_keys' do
|
|
647
|
+
it 'returns only session key items' do
|
|
648
|
+
playlist = described_class.read(
|
|
649
|
+
File.read('spec/fixtures/master.m3u8')
|
|
650
|
+
)
|
|
651
|
+
expect(playlist.session_keys)
|
|
652
|
+
.to all be_a(M3u8::SessionKeyItem)
|
|
653
|
+
expect(playlist.session_keys.size).to eq(1)
|
|
654
|
+
end
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
describe '#freeze' do
|
|
658
|
+
it 'freezes the playlist' do
|
|
659
|
+
playlist.freeze
|
|
660
|
+
expect(playlist).to be_frozen
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
it 'freezes the items array' do
|
|
664
|
+
playlist.freeze
|
|
665
|
+
expect(playlist.items).to be_frozen
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
it 'freezes each item' do
|
|
669
|
+
item = M3u8::SegmentItem.new(duration: 10.0, segment: 'test.ts')
|
|
670
|
+
playlist.items << item
|
|
671
|
+
playlist.freeze
|
|
672
|
+
expect(item).to be_frozen
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
it 'freezes nested byterange on items' do
|
|
676
|
+
item = M3u8::SegmentItem.new(
|
|
677
|
+
duration: 10.0, segment: 'test.ts',
|
|
678
|
+
byterange: { length: 4500, start: 600 }
|
|
679
|
+
)
|
|
680
|
+
playlist.items << item
|
|
681
|
+
playlist.freeze
|
|
682
|
+
expect(item.byterange).to be_frozen
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
it 'freezes nested program_date_time on items' do
|
|
686
|
+
time = M3u8::TimeItem.new(time: '2024-06-01T12:00:00Z')
|
|
687
|
+
item = M3u8::SegmentItem.new(
|
|
688
|
+
duration: 10.0, segment: 'test.ts',
|
|
689
|
+
program_date_time: time
|
|
690
|
+
)
|
|
691
|
+
playlist.items << item
|
|
692
|
+
playlist.freeze
|
|
693
|
+
expect(item.program_date_time).to be_frozen
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
it 'freezes nested client_attributes on items' do
|
|
697
|
+
item = M3u8::DateRangeItem.new(
|
|
698
|
+
id: 'ad-1', start_date: '2024-01-01T00:00:00Z',
|
|
699
|
+
client_attributes: { 'X-AD-ID' => '"foo"' }
|
|
700
|
+
)
|
|
701
|
+
playlist.items << item
|
|
702
|
+
playlist.freeze
|
|
703
|
+
expect(item.client_attributes).to be_frozen
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
it 'freezes part_inf' do
|
|
707
|
+
playlist.part_inf = M3u8::PartInfItem.new(part_target: 0.5)
|
|
708
|
+
playlist.freeze
|
|
709
|
+
expect(playlist.part_inf).to be_frozen
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
it 'freezes server_control' do
|
|
713
|
+
playlist.server_control = M3u8::ServerControlItem.new(
|
|
714
|
+
can_skip_until: 24.0
|
|
715
|
+
)
|
|
716
|
+
playlist.freeze
|
|
717
|
+
expect(playlist.server_control).to be_frozen
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
it 'raises FrozenError on attribute set' do
|
|
721
|
+
playlist.freeze
|
|
722
|
+
expect { playlist.version = 7 }
|
|
723
|
+
.to raise_error(FrozenError)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
it 'raises FrozenError on items append' do
|
|
727
|
+
playlist.freeze
|
|
728
|
+
item = M3u8::SegmentItem.new(
|
|
729
|
+
duration: 10.0, segment: 'test.ts'
|
|
730
|
+
)
|
|
731
|
+
expect { playlist.items << item }
|
|
732
|
+
.to raise_error(FrozenError)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
it 'raises FrozenError on item mutation' do
|
|
736
|
+
item = M3u8::SegmentItem.new(
|
|
737
|
+
duration: 10.0, segment: 'test.ts'
|
|
738
|
+
)
|
|
739
|
+
playlist.items << item
|
|
740
|
+
playlist.freeze
|
|
741
|
+
expect { item.duration = 5.0 }
|
|
742
|
+
.to raise_error(FrozenError)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
it 'still supports to_s' do
|
|
746
|
+
item = M3u8::SegmentItem.new(
|
|
747
|
+
duration: 10.0, segment: 'test.ts'
|
|
748
|
+
)
|
|
749
|
+
playlist.items << item
|
|
750
|
+
playlist.freeze
|
|
751
|
+
expect(playlist.to_s).to include('#EXTM3U')
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
it 'returns self' do
|
|
755
|
+
expect(playlist.freeze).to equal(playlist)
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
226
759
|
describe '#write' do
|
|
227
760
|
context 'when playlist is valid' do
|
|
228
761
|
it 'returns playlist text' do
|
|
@@ -233,9 +766,7 @@ describe M3u8::Playlist do
|
|
|
233
766
|
|
|
234
767
|
io = StringIO.new
|
|
235
768
|
playlist.write(io)
|
|
236
|
-
expected = "#EXTM3U\n"
|
|
237
|
-
%(#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34",) +
|
|
238
|
-
"BANDWIDTH=6400\nplaylist_url\n"
|
|
769
|
+
expected = "#EXTM3U\n#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS=\"mp4a.40.34\",BANDWIDTH=6400\nplaylist_url\n"
|
|
239
770
|
expect(io.string).to eq(expected)
|
|
240
771
|
end
|
|
241
772
|
end
|
|
@@ -251,7 +782,8 @@ describe M3u8::Playlist do
|
|
|
251
782
|
item = M3u8::SegmentItem.new(options)
|
|
252
783
|
playlist.items << item
|
|
253
784
|
|
|
254
|
-
message = 'Playlist is invalid
|
|
785
|
+
message = 'Playlist is invalid: Playlist contains both ' \
|
|
786
|
+
'master and media items'
|
|
255
787
|
io = StringIO.new
|
|
256
788
|
expect { playlist.write(io) }
|
|
257
789
|
.to raise_error(M3u8::PlaylistTypeError, message)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::PreloadHintItem do
|
|
6
|
+
describe '.new' do
|
|
7
|
+
it 'assigns attributes from options' do
|
|
8
|
+
options = { type: 'PART', uri: 'next_part.ts',
|
|
9
|
+
byterange_start: 0, byterange_length: 5000 }
|
|
10
|
+
item = described_class.new(options)
|
|
11
|
+
expect(item.type).to eq('PART')
|
|
12
|
+
expect(item.uri).to eq('next_part.ts')
|
|
13
|
+
expect(item.byterange_start).to eq(0)
|
|
14
|
+
expect(item.byterange_length).to eq(5000)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe '.parse' do
|
|
19
|
+
it 'parses tag with all attributes' do
|
|
20
|
+
tag = '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="next_part.ts",' \
|
|
21
|
+
'BYTERANGE-START=0,BYTERANGE-LENGTH=5000'
|
|
22
|
+
item = described_class.parse(tag)
|
|
23
|
+
expect(item.type).to eq('PART')
|
|
24
|
+
expect(item.uri).to eq('next_part.ts')
|
|
25
|
+
expect(item.byterange_start).to eq(0)
|
|
26
|
+
expect(item.byterange_length).to eq(5000)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'parses tag without optional attributes' do
|
|
30
|
+
tag = '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="next_part.ts"'
|
|
31
|
+
item = described_class.parse(tag)
|
|
32
|
+
expect(item.type).to eq('PART')
|
|
33
|
+
expect(item.uri).to eq('next_part.ts')
|
|
34
|
+
expect(item.byterange_start).to be_nil
|
|
35
|
+
expect(item.byterange_length).to be_nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '#to_s' do
|
|
40
|
+
it 'returns tag with all attributes' do
|
|
41
|
+
options = { type: 'PART', uri: 'next_part.ts',
|
|
42
|
+
byterange_start: 0, byterange_length: 5000 }
|
|
43
|
+
item = described_class.new(options)
|
|
44
|
+
expected = '#EXT-X-PRELOAD-HINT:TYPE=PART,' \
|
|
45
|
+
'URI="next_part.ts",' \
|
|
46
|
+
'BYTERANGE-START=0,BYTERANGE-LENGTH=5000'
|
|
47
|
+
expect(item.to_s).to eq(expected)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'returns tag without optional attributes' do
|
|
51
|
+
options = { type: 'PART', uri: 'next_part.ts' }
|
|
52
|
+
item = described_class.new(options)
|
|
53
|
+
expected = '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="next_part.ts"'
|
|
54
|
+
expect(item.to_s).to eq(expected)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|