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.
Files changed (103) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +23 -0
  3. data/.gitignore +1 -1
  4. data/.rubocop.yml +31 -0
  5. data/CHANGELOG.md +107 -0
  6. data/Gemfile +7 -0
  7. data/LICENSE.txt +1 -1
  8. data/README.md +524 -40
  9. data/Rakefile +1 -0
  10. data/bin/m3u8 +6 -0
  11. data/lib/m3u8/attribute_formatter.rb +47 -0
  12. data/lib/m3u8/bitrate_item.rb +31 -0
  13. data/lib/m3u8/builder.rb +48 -0
  14. data/lib/m3u8/byte_range.rb +10 -0
  15. data/lib/m3u8/cli/inspect_command.rb +97 -0
  16. data/lib/m3u8/cli/validate_command.rb +24 -0
  17. data/lib/m3u8/cli.rb +116 -0
  18. data/lib/m3u8/codecs.rb +89 -0
  19. data/lib/m3u8/content_steering_item.rb +45 -0
  20. data/lib/m3u8/date_range_item.rb +135 -64
  21. data/lib/m3u8/define_item.rb +54 -0
  22. data/lib/m3u8/discontinuity_item.rb +3 -0
  23. data/lib/m3u8/encryptable.rb +27 -30
  24. data/lib/m3u8/error.rb +1 -0
  25. data/lib/m3u8/gap_item.rb +14 -0
  26. data/lib/m3u8/key_item.rb +7 -0
  27. data/lib/m3u8/map_item.rb +16 -5
  28. data/lib/m3u8/media_item.rb +48 -76
  29. data/lib/m3u8/part_inf_item.rb +35 -0
  30. data/lib/m3u8/part_item.rb +67 -0
  31. data/lib/m3u8/playback_start.rb +19 -12
  32. data/lib/m3u8/playlist.rb +221 -13
  33. data/lib/m3u8/playlist_item.rb +128 -124
  34. data/lib/m3u8/preload_hint_item.rb +54 -0
  35. data/lib/m3u8/reader.rb +86 -28
  36. data/lib/m3u8/rendition_report_item.rb +48 -0
  37. data/lib/m3u8/scte35.rb +130 -0
  38. data/lib/m3u8/scte35_bit_reader.rb +51 -0
  39. data/lib/m3u8/scte35_segmentation_descriptor.rb +54 -0
  40. data/lib/m3u8/scte35_splice_insert.rb +62 -0
  41. data/lib/m3u8/scte35_splice_null.rb +8 -0
  42. data/lib/m3u8/scte35_time_signal.rb +19 -0
  43. data/lib/m3u8/segment_item.rb +37 -3
  44. data/lib/m3u8/server_control_item.rb +69 -0
  45. data/lib/m3u8/session_data_item.rb +17 -28
  46. data/lib/m3u8/session_key_item.rb +8 -1
  47. data/lib/m3u8/skip_item.rb +48 -0
  48. data/lib/m3u8/time_item.rb +10 -0
  49. data/lib/m3u8/version.rb +1 -1
  50. data/lib/m3u8/writer.rb +24 -1
  51. data/lib/m3u8.rb +30 -6
  52. data/m3u8.gemspec +12 -12
  53. data/spec/fixtures/content_steering.m3u8 +10 -0
  54. data/spec/fixtures/daterange_playlist.m3u8 +14 -0
  55. data/spec/fixtures/encrypted_discontinuity.m3u8 +17 -0
  56. data/spec/fixtures/event_playlist.m3u8 +18 -0
  57. data/spec/fixtures/gap_playlist.m3u8 +14 -0
  58. data/spec/fixtures/ll_hls_advanced.m3u8 +18 -0
  59. data/spec/fixtures/ll_hls_playlist.m3u8 +20 -0
  60. data/spec/fixtures/master_full.m3u8 +14 -0
  61. data/spec/fixtures/master_v13.m3u8 +8 -0
  62. data/spec/lib/m3u8/bitrate_item_spec.rb +26 -0
  63. data/spec/lib/m3u8/builder_spec.rb +352 -0
  64. data/spec/lib/m3u8/byte_range_spec.rb +1 -0
  65. data/spec/lib/m3u8/cli/inspect_command_spec.rb +102 -0
  66. data/spec/lib/m3u8/cli/validate_command_spec.rb +39 -0
  67. data/spec/lib/m3u8/cli_spec.rb +104 -0
  68. data/spec/lib/m3u8/content_steering_item_spec.rb +56 -0
  69. data/spec/lib/m3u8/date_range_item_spec.rb +159 -31
  70. data/spec/lib/m3u8/define_item_spec.rb +59 -0
  71. data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -0
  72. data/spec/lib/m3u8/gap_item_spec.rb +12 -0
  73. data/spec/lib/m3u8/key_item_spec.rb +1 -0
  74. data/spec/lib/m3u8/map_item_spec.rb +1 -0
  75. data/spec/lib/m3u8/media_item_spec.rb +34 -0
  76. data/spec/lib/m3u8/part_inf_item_spec.rb +27 -0
  77. data/spec/lib/m3u8/part_item_spec.rb +67 -0
  78. data/spec/lib/m3u8/playback_start_spec.rb +4 -5
  79. data/spec/lib/m3u8/playlist_item_spec.rb +130 -17
  80. data/spec/lib/m3u8/playlist_spec.rb +545 -13
  81. data/spec/lib/m3u8/preload_hint_item_spec.rb +57 -0
  82. data/spec/lib/m3u8/reader_spec.rb +376 -29
  83. data/spec/lib/m3u8/rendition_report_item_spec.rb +56 -0
  84. data/spec/lib/m3u8/round_trip_spec.rb +152 -0
  85. data/spec/lib/m3u8/scte35_bit_reader_spec.rb +106 -0
  86. data/spec/lib/m3u8/scte35_segmentation_descriptor_spec.rb +143 -0
  87. data/spec/lib/m3u8/scte35_spec.rb +94 -0
  88. data/spec/lib/m3u8/scte35_splice_insert_spec.rb +185 -0
  89. data/spec/lib/m3u8/scte35_splice_null_spec.rb +12 -0
  90. data/spec/lib/m3u8/scte35_time_signal_spec.rb +50 -0
  91. data/spec/lib/m3u8/segment_item_spec.rb +47 -0
  92. data/spec/lib/m3u8/server_control_item_spec.rb +64 -0
  93. data/spec/lib/m3u8/session_data_item_spec.rb +1 -0
  94. data/spec/lib/m3u8/session_key_item_spec.rb +1 -0
  95. data/spec/lib/m3u8/skip_item_spec.rb +48 -0
  96. data/spec/lib/m3u8/time_item_spec.rb +1 -0
  97. data/spec/lib/m3u8/writer_spec.rb +69 -30
  98. data/spec/lib/m3u8_spec.rb +1 -0
  99. data/spec/spec_helper.rb +4 -87
  100. metadata +70 -129
  101. data/.hound.yml +0 -3
  102. data/.travis.yml +0 -8
  103. 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::Reader do
@@ -6,9 +7,10 @@ describe M3u8::Reader do
6
7
 
7
8
  describe '#read' do
8
9
  it 'parses master playlist' do
9
- file = File.open('spec/fixtures/master.m3u8')
10
10
  reader = M3u8::Reader.new
11
- playlist = reader.read(file)
11
+ playlist = reader.read(
12
+ File.read('spec/fixtures/master.m3u8')
13
+ )
12
14
  expect(playlist.master?).to be true
13
15
  expect(playlist.discontinuity_sequence).to be_nil
14
16
  expect(playlist.independent_segments).to be true
@@ -53,9 +55,10 @@ describe M3u8::Reader do
53
55
  end
54
56
 
55
57
  it 'parses master playlist with I-Frames' do
56
- file = File.open('spec/fixtures/master_iframes.m3u8')
57
58
  reader = M3u8::Reader.new
58
- playlist = reader.read(file)
59
+ playlist = reader.read(
60
+ File.read('spec/fixtures/master_iframes.m3u8')
61
+ )
59
62
  expect(playlist.master?).to be true
60
63
 
61
64
  expect(playlist.items.size).to eq(7)
@@ -68,9 +71,10 @@ describe M3u8::Reader do
68
71
  end
69
72
 
70
73
  it 'parses media playlist' do
71
- file = File.open('spec/fixtures/playlist.m3u8')
72
74
  reader = M3u8::Reader.new
73
- playlist = reader.read(file)
75
+ playlist = reader.read(
76
+ File.read('spec/fixtures/playlist.m3u8')
77
+ )
74
78
  expect(playlist.master?).to be false
75
79
  expect(playlist.version).to eq(4)
76
80
  expect(playlist.sequence).to eq(1)
@@ -78,6 +82,7 @@ describe M3u8::Reader do
78
82
  expect(playlist.cache).to be false
79
83
  expect(playlist.target).to eq(12)
80
84
  expect(playlist.type).to eq('VOD')
85
+ expect(playlist.live?).to be false
81
86
 
82
87
  item = playlist.items[0]
83
88
  expect(item).to be_a(M3u8::SegmentItem)
@@ -92,9 +97,10 @@ describe M3u8::Reader do
92
97
  end
93
98
 
94
99
  it 'parses I-Frame playlist' do
95
- file = File.open('spec/fixtures/iframes.m3u8')
96
100
  reader = M3u8::Reader.new
97
- playlist = reader.read(file)
101
+ playlist = reader.read(
102
+ File.read('spec/fixtures/iframes.m3u8')
103
+ )
98
104
 
99
105
  expect(playlist.iframes_only).to be true
100
106
  expect(playlist.items.size).to eq(3)
@@ -112,9 +118,10 @@ describe M3u8::Reader do
112
118
  end
113
119
 
114
120
  it 'parses segment playlist with comments' do
115
- file = File.open('spec/fixtures/playlist_with_comments.m3u8')
116
121
  reader = M3u8::Reader.new
117
- playlist = reader.read(file)
122
+ playlist = reader.read(
123
+ File.read('spec/fixtures/playlist_with_comments.m3u8')
124
+ )
118
125
  expect(playlist.master?).to be false
119
126
  expect(playlist.version).to eq(4)
120
127
  expect(playlist.sequence).to eq(1)
@@ -134,9 +141,10 @@ describe M3u8::Reader do
134
141
  end
135
142
 
136
143
  it 'parses variant playlist with audio options and groups' do
137
- file = File.open('spec/fixtures/variant_audio.m3u8')
138
144
  reader = M3u8::Reader.new
139
- playlist = reader.read(file)
145
+ playlist = reader.read(
146
+ File.read('spec/fixtures/variant_audio.m3u8')
147
+ )
140
148
 
141
149
  expect(playlist.master?).to be true
142
150
  expect(playlist.items.size).to eq(10)
@@ -155,9 +163,10 @@ describe M3u8::Reader do
155
163
  end
156
164
 
157
165
  it 'parses variant playlist with camera angles' do
158
- file = File.open('spec/fixtures/variant_angles.m3u8')
159
166
  reader = M3u8::Reader.new
160
- playlist = reader.read(file)
167
+ playlist = reader.read(
168
+ File.read('spec/fixtures/variant_angles.m3u8')
169
+ )
161
170
 
162
171
  expect(playlist.master?).to be true
163
172
  expect(playlist.items.size).to eq(11)
@@ -181,22 +190,22 @@ describe M3u8::Reader do
181
190
  end
182
191
 
183
192
  it 'processes multiple reads as separate playlists' do
184
- file = File.open('spec/fixtures/master.m3u8')
185
193
  reader = M3u8::Reader.new
186
- playlist = reader.read(file)
194
+ content = File.read('spec/fixtures/master.m3u8')
195
+ playlist = reader.read(content)
187
196
 
188
197
  expect(playlist.items.size).to eq(8)
189
198
 
190
- file = File.open('spec/fixtures/master.m3u8')
191
- playlist = reader.read(file)
199
+ playlist = reader.read(content)
192
200
 
193
201
  expect(playlist.items.size).to eq(8)
194
202
  end
195
203
 
196
204
  it 'parses playlist with session data' do
197
- file = File.open('spec/fixtures/session_data.m3u8')
198
205
  reader = M3u8::Reader.new
199
- playlist = reader.read(file)
206
+ playlist = reader.read(
207
+ File.read('spec/fixtures/session_data.m3u8')
208
+ )
200
209
 
201
210
  expect(playlist.items.size).to eq(3)
202
211
 
@@ -207,9 +216,10 @@ describe M3u8::Reader do
207
216
  end
208
217
 
209
218
  it 'parses encrypted playlist' do
210
- file = File.open('spec/fixtures/encrypted.m3u8')
211
219
  reader = M3u8::Reader.new
212
- playlist = reader.read(file)
220
+ playlist = reader.read(
221
+ File.read('spec/fixtures/encrypted.m3u8')
222
+ )
213
223
 
214
224
  expect(playlist.items.size).to eq(6)
215
225
 
@@ -220,9 +230,10 @@ describe M3u8::Reader do
220
230
  end
221
231
 
222
232
  it 'parses map (media intialization section) playlists' do
223
- file = File.open('spec/fixtures/map_playlist.m3u8')
224
233
  reader = M3u8::Reader.new
225
- playlist = reader.read(file)
234
+ playlist = reader.read(
235
+ File.read('spec/fixtures/map_playlist.m3u8')
236
+ )
226
237
 
227
238
  expect(playlist.items.size).to eq(1)
228
239
 
@@ -234,9 +245,10 @@ describe M3u8::Reader do
234
245
  end
235
246
 
236
247
  it 'reads segment with timestamp' do
237
- file = File.open('spec/fixtures/timestamp_playlist.m3u8')
238
248
  reader = M3u8::Reader.new
239
- playlist = reader.read(file)
249
+ playlist = reader.read(
250
+ File.read('spec/fixtures/timestamp_playlist.m3u8')
251
+ )
240
252
  expect(playlist.items.count).to eq(6)
241
253
 
242
254
  item_date_time = playlist.items.first.program_date_time
@@ -245,9 +257,10 @@ describe M3u8::Reader do
245
257
  end
246
258
 
247
259
  it 'parses playlist with daterange' do
248
- file = File.open('spec/fixtures/date_range_scte35.m3u8')
249
260
  reader = M3u8::Reader.new
250
- playlist = reader.read(file)
261
+ playlist = reader.read(
262
+ File.read('spec/fixtures/date_range_scte35.m3u8')
263
+ )
251
264
  expect(playlist.items.count).to eq(5)
252
265
 
253
266
  item = playlist.items[0]
@@ -257,10 +270,344 @@ describe M3u8::Reader do
257
270
  expect(item).to be_a(M3u8::DateRangeItem)
258
271
  end
259
272
 
273
+ it 'parses master playlist with v13 attributes' do
274
+ reader = M3u8::Reader.new
275
+ playlist = reader.read(
276
+ File.read('spec/fixtures/master_v13.m3u8')
277
+ )
278
+ expect(playlist.master?).to be true
279
+ expect(playlist.version).to eq(13)
280
+
281
+ item = playlist.items[0]
282
+ expect(item).to be_a(M3u8::MediaItem)
283
+ expect(item.stable_rendition_id).to eq('audio-en')
284
+ expect(item.bit_depth).to eq(16)
285
+ expect(item.sample_rate).to eq(44_100)
286
+
287
+ item = playlist.items[1]
288
+ expect(item).to be_a(M3u8::PlaylistItem)
289
+ expect(item.stable_variant_id).to eq('hd-1080')
290
+ expect(item.video_range).to eq('SDR')
291
+ expect(item.pathway_id).to eq('CDN-A')
292
+ expect(item.score).to eq(12.5)
293
+ expect(item.supplemental_codecs).to eq('dvh1.05.06/db4g')
294
+ expect(item.allowed_cpc).to eq('com.example.drm:SMART-TV/PC')
295
+ expect(item.req_video_layout).to eq('CH-MONO')
296
+
297
+ item = playlist.items[2]
298
+ expect(item).to be_a(M3u8::PlaylistItem)
299
+ expect(item.stable_variant_id).to eq('hd-720')
300
+ end
301
+
302
+ it 'parses playlist with content steering and defines' do
303
+ reader = M3u8::Reader.new
304
+ playlist = reader.read(
305
+ File.read('spec/fixtures/content_steering.m3u8')
306
+ )
307
+ expect(playlist.master?).to be true
308
+ expect(playlist.items.size).to eq(5)
309
+
310
+ item = playlist.items[0]
311
+ expect(item).to be_a(M3u8::DefineItem)
312
+ expect(item.name).to eq('base_url')
313
+ expect(item.value).to eq('https://example.com')
314
+
315
+ item = playlist.items[1]
316
+ expect(item).to be_a(M3u8::DefineItem)
317
+ expect(item.import).to eq('token')
318
+
319
+ item = playlist.items[2]
320
+ expect(item).to be_a(M3u8::ContentSteeringItem)
321
+ expect(item.server_uri).to eq('https://example.com/steering')
322
+ expect(item.pathway_id).to eq('CDN-A')
323
+ end
324
+
325
+ it 'parses LL-HLS playlist' do
326
+ reader = M3u8::Reader.new
327
+ playlist = reader.read(
328
+ File.read('spec/fixtures/ll_hls_playlist.m3u8')
329
+ )
330
+ expect(playlist.master?).to be false
331
+ expect(playlist.live?).to be true
332
+ expect(playlist.version).to eq(9)
333
+
334
+ expect(playlist.server_control).to be_a(M3u8::ServerControlItem)
335
+ expect(playlist.server_control.can_skip_until).to eq(24.0)
336
+ expect(playlist.server_control.can_block_reload).to be true
337
+ expect(playlist.server_control.part_hold_back).to eq(1.0)
338
+
339
+ expect(playlist.part_inf).to be_a(M3u8::PartInfItem)
340
+ expect(playlist.part_inf.part_target).to eq(0.5)
341
+
342
+ item = playlist.items[0]
343
+ expect(item).to be_a(M3u8::SkipItem)
344
+ expect(item.skipped_segments).to eq(5)
345
+
346
+ item = playlist.items[1]
347
+ expect(item).to be_a(M3u8::MapItem)
348
+
349
+ item = playlist.items[3]
350
+ expect(item).to be_a(M3u8::PartItem)
351
+ expect(item.duration).to eq(0.5)
352
+ expect(item.uri).to eq('segment101.0.mp4')
353
+ expect(item.independent).to be true
354
+
355
+ item = playlist.items[4]
356
+ expect(item).to be_a(M3u8::PartItem)
357
+ expect(item.independent).to be false
358
+
359
+ item = playlist.items[8]
360
+ expect(item).to be_a(M3u8::PartItem)
361
+ expect(item.uri).to eq('segment102.0.mp4')
362
+
363
+ item = playlist.items[9]
364
+ expect(item).to be_a(M3u8::PreloadHintItem)
365
+ expect(item.type).to eq('PART')
366
+ expect(item.uri).to eq('segment102.1.mp4')
367
+
368
+ item = playlist.items[10]
369
+ expect(item).to be_a(M3u8::RenditionReportItem)
370
+ expect(item.uri).to eq('../720p/stream.m3u8')
371
+ expect(item.last_msn).to eq(101)
372
+ expect(item.last_part).to eq(3)
373
+
374
+ expect(playlist.items.size).to eq(12)
375
+ end
376
+
377
+ it 'parses playlist with gap and bitrate tags' do
378
+ reader = M3u8::Reader.new
379
+ playlist = reader.read(
380
+ File.read('spec/fixtures/gap_playlist.m3u8')
381
+ )
382
+ expect(playlist.master?).to be false
383
+ expect(playlist.items.size).to eq(6)
384
+
385
+ item = playlist.items[0]
386
+ expect(item).to be_a(M3u8::BitrateItem)
387
+ expect(item.bitrate).to eq(128)
388
+
389
+ item = playlist.items[2]
390
+ expect(item).to be_a(M3u8::GapItem)
391
+
392
+ item = playlist.items[3]
393
+ expect(item).to be_a(M3u8::SegmentItem)
394
+
395
+ item = playlist.items[4]
396
+ expect(item).to be_a(M3u8::BitrateItem)
397
+ expect(item.bitrate).to eq(256)
398
+ end
399
+
400
+ it 'parses event playlist with byterange and map change' do
401
+ playlist = reader.read(
402
+ File.read('spec/fixtures/event_playlist.m3u8')
403
+ )
404
+ expect(playlist.master?).to be false
405
+ expect(playlist.live?).to be false
406
+ expect(playlist.type).to eq('EVENT')
407
+ expect(playlist.version).to eq(7)
408
+ expect(playlist.independent_segments).to be true
409
+ expect(playlist.items.size).to eq(6)
410
+
411
+ item = playlist.items[0]
412
+ expect(item).to be_a(M3u8::MapItem)
413
+ expect(item.uri).to eq('init.mp4')
414
+ expect(item.byterange).to be_nil
415
+
416
+ item = playlist.items[1]
417
+ expect(item).to be_a(M3u8::SegmentItem)
418
+ expect(item.duration).to eq(6.0)
419
+ expect(item.byterange.length).to eq(75_232)
420
+ expect(item.byterange.start).to eq(0)
421
+
422
+ item = playlist.items[2]
423
+ expect(item).to be_a(M3u8::SegmentItem)
424
+ expect(item.byterange.length).to eq(82_112)
425
+ expect(item.byterange.start).to eq(75_232)
426
+
427
+ item = playlist.items[3]
428
+ expect(item).to be_a(M3u8::DiscontinuityItem)
429
+
430
+ item = playlist.items[4]
431
+ expect(item).to be_a(M3u8::MapItem)
432
+ expect(item.uri).to eq('init2.mp4')
433
+ end
434
+
435
+ it 'parses daterange playlist' do
436
+ playlist = reader.read(
437
+ File.read('spec/fixtures/daterange_playlist.m3u8')
438
+ )
439
+ expect(playlist.master?).to be false
440
+ expect(playlist.items.size).to eq(6)
441
+
442
+ item = playlist.items[0]
443
+ expect(item).to be_a(M3u8::DateRangeItem)
444
+ expect(item.id).to eq('ad-1')
445
+ expect(item.class_name).to eq('com.example.ad')
446
+ expect(item.start_date).to eq('2024-01-01T00:00:00Z')
447
+ expect(item.end_date).to eq('2024-01-01T00:00:30Z')
448
+ expect(item.duration).to eq(30.0)
449
+ expect(item.client_attributes['X-AD-ID']).to eq('ad-123')
450
+ expect(item.client_attributes['X-AD-URL'])
451
+ .to eq('https://example.com/ad')
452
+
453
+ item = playlist.items[2]
454
+ expect(item).to be_a(M3u8::DateRangeItem)
455
+ expect(item.id).to eq('ad-2')
456
+ expect(item.planned_duration).to eq(15.0)
457
+
458
+ item = playlist.items[4]
459
+ expect(item).to be_a(M3u8::DateRangeItem)
460
+ expect(item.id).to eq('ch-1')
461
+ expect(item.end_on_next).to be true
462
+ end
463
+
464
+ it 'parses full master playlist' do
465
+ playlist = reader.read(
466
+ File.read('spec/fixtures/master_full.m3u8')
467
+ )
468
+ expect(playlist.master?).to be true
469
+ expect(playlist.version).to eq(13)
470
+ expect(playlist.independent_segments).to be true
471
+ expect(playlist.items.size).to eq(9)
472
+
473
+ item = playlist.items[0]
474
+ expect(item).to be_a(M3u8::DefineItem)
475
+ expect(item.queryparam).to eq('token')
476
+
477
+ item = playlist.items[1]
478
+ expect(item).to be_a(M3u8::PlaybackStart)
479
+ expect(item.time_offset).to eq(10.5)
480
+ expect(item.precise).to be true
481
+
482
+ item = playlist.items[2]
483
+ expect(item).to be_a(M3u8::SessionKeyItem)
484
+ expect(item.method).to eq('SAMPLE-AES')
485
+ expect(item.key_format)
486
+ .to eq('com.apple.streamingkeydelivery')
487
+
488
+ item = playlist.items[3]
489
+ expect(item).to be_a(M3u8::SessionDataItem)
490
+ expect(item.data_id).to eq('com.example.title')
491
+ expect(item.language).to eq('en')
492
+
493
+ item = playlist.items[4]
494
+ expect(item).to be_a(M3u8::ContentSteeringItem)
495
+ expect(item.server_uri)
496
+ .to eq('https://example.com/steering')
497
+
498
+ item = playlist.items[5]
499
+ expect(item).to be_a(M3u8::MediaItem)
500
+ expect(item.type).to eq('AUDIO')
501
+ expect(item.language).to eq('en')
502
+
503
+ item = playlist.items[7]
504
+ expect(item).to be_a(M3u8::PlaylistItem)
505
+ expect(item.bandwidth).to eq(5_000_000)
506
+ expect(item.average_bandwidth).to eq(4_500_000)
507
+ end
508
+
509
+ it 'parses encrypted playlist with discontinuities' do
510
+ playlist = reader.read(
511
+ File.read('spec/fixtures/encrypted_discontinuity.m3u8')
512
+ )
513
+ expect(playlist.master?).to be false
514
+ expect(playlist.live?).to be false
515
+ expect(playlist.items.size).to eq(8)
516
+
517
+ item = playlist.items[0]
518
+ expect(item).to be_a(M3u8::KeyItem)
519
+ expect(item.method).to eq('AES-128')
520
+ expect(item.uri).to eq('https://example.com/key1.bin')
521
+ expect(item.iv)
522
+ .to eq('0x00000000000000000000000000000001')
523
+
524
+ item = playlist.items[3]
525
+ expect(item).to be_a(M3u8::DiscontinuityItem)
526
+
527
+ item = playlist.items[4]
528
+ expect(item).to be_a(M3u8::KeyItem)
529
+ expect(item.uri).to eq('https://example.com/key2.bin')
530
+
531
+ item = playlist.items[6]
532
+ expect(item).to be_a(M3u8::KeyItem)
533
+ expect(item.method).to eq('NONE')
534
+ end
535
+
536
+ it 'parses advanced LL-HLS playlist' do
537
+ playlist = reader.read(
538
+ File.read('spec/fixtures/ll_hls_advanced.m3u8')
539
+ )
540
+ expect(playlist.master?).to be false
541
+ expect(playlist.live?).to be true
542
+ expect(playlist.version).to eq(9)
543
+
544
+ sc = playlist.server_control
545
+ expect(sc.can_skip_until).to eq(24.0)
546
+ expect(sc.can_skip_dateranges).to be true
547
+ expect(sc.hold_back).to eq(12.0)
548
+ expect(sc.part_hold_back).to eq(1.0)
549
+
550
+ skip = playlist.items[0]
551
+ expect(skip).to be_a(M3u8::SkipItem)
552
+ expect(skip.skipped_segments).to eq(10)
553
+ expect(skip.recently_removed_dateranges).to eq('dr-1')
554
+
555
+ item = playlist.items[4]
556
+ expect(item).to be_a(M3u8::PartItem)
557
+ expect(item.independent).to be true
558
+
559
+ item = playlist.items[5]
560
+ expect(item).to be_a(M3u8::PartItem)
561
+ expect(item.byterange.length).to eq(1024)
562
+ expect(item.byterange.start).to eq(0)
563
+ expect(item.gap).to be true
564
+
565
+ hint = playlist.items[8]
566
+ expect(hint).to be_a(M3u8::PreloadHintItem)
567
+ expect(hint.type).to eq('MAP')
568
+ expect(hint.uri).to eq('init2.mp4')
569
+
570
+ report = playlist.items[9]
571
+ expect(report).to be_a(M3u8::RenditionReportItem)
572
+ expect(report.last_msn).to eq(101)
573
+ end
574
+
575
+ it 'parses playlist with \\r line endings' do
576
+ text = "#EXTM3U\r" \
577
+ "#EXT-X-VERSION:4\r" \
578
+ "#EXT-X-TARGETDURATION:10\r" \
579
+ "#EXT-X-MEDIA-SEQUENCE:0\r" \
580
+ "#EXTINF:10.0,\r" \
581
+ "segment0.ts\r" \
582
+ "#EXT-X-ENDLIST\r"
583
+ playlist = reader.read(text)
584
+ expect(playlist.master?).to be false
585
+ expect(playlist.version).to eq(4)
586
+ expect(playlist.items.size).to eq(1)
587
+
588
+ item = playlist.items[0]
589
+ expect(item).to be_a(M3u8::SegmentItem)
590
+ expect(item.segment).to eq('segment0.ts')
591
+ end
592
+
593
+ it 'returns a frozen master playlist' do
594
+ playlist = reader.read(
595
+ File.read('spec/fixtures/master.m3u8')
596
+ )
597
+ expect(playlist).to be_frozen
598
+ end
599
+
600
+ it 'returns a frozen media playlist' do
601
+ playlist = reader.read(
602
+ File.read('spec/fixtures/playlist.m3u8')
603
+ )
604
+ expect(playlist).to be_frozen
605
+ end
606
+
260
607
  context 'when playlist source is invalid' do
261
608
  it 'raises error with message' do
262
609
  message = 'Playlist must start with a #EXTM3U tag, line read ' \
263
- 'contained the value: /path/to/file'
610
+ 'contained the value: /path/to/file'
264
611
  expect { reader.read('/path/to/file') }
265
612
  .to raise_error(M3u8::InvalidPlaylistError, message)
266
613
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::RenditionReportItem do
6
+ describe '.new' do
7
+ it 'assigns attributes from options' do
8
+ options = { uri: '../720p/stream.m3u8', last_msn: 100,
9
+ last_part: 3 }
10
+ item = described_class.new(options)
11
+ expect(item.uri).to eq('../720p/stream.m3u8')
12
+ expect(item.last_msn).to eq(100)
13
+ expect(item.last_part).to eq(3)
14
+ end
15
+ end
16
+
17
+ describe '.parse' do
18
+ it 'parses tag with all attributes' do
19
+ tag = '#EXT-X-RENDITION-REPORT:URI="../720p/stream.m3u8",' \
20
+ 'LAST-MSN=100,LAST-PART=3'
21
+ item = described_class.parse(tag)
22
+ expect(item.uri).to eq('../720p/stream.m3u8')
23
+ expect(item.last_msn).to eq(100)
24
+ expect(item.last_part).to eq(3)
25
+ end
26
+
27
+ it 'parses tag without optional attributes' do
28
+ tag = '#EXT-X-RENDITION-REPORT:URI="../720p/stream.m3u8",' \
29
+ 'LAST-MSN=100'
30
+ item = described_class.parse(tag)
31
+ expect(item.uri).to eq('../720p/stream.m3u8')
32
+ expect(item.last_msn).to eq(100)
33
+ expect(item.last_part).to be_nil
34
+ end
35
+ end
36
+
37
+ describe '#to_s' do
38
+ it 'returns tag with all attributes' do
39
+ options = { uri: '../720p/stream.m3u8', last_msn: 100,
40
+ last_part: 3 }
41
+ item = described_class.new(options)
42
+ expected = '#EXT-X-RENDITION-REPORT:' \
43
+ 'URI="../720p/stream.m3u8",' \
44
+ 'LAST-MSN=100,LAST-PART=3'
45
+ expect(item.to_s).to eq(expected)
46
+ end
47
+
48
+ it 'returns tag without optional attributes' do
49
+ options = { uri: '../720p/stream.m3u8', last_msn: 100 }
50
+ item = described_class.new(options)
51
+ expected = '#EXT-X-RENDITION-REPORT:' \
52
+ 'URI="../720p/stream.m3u8",LAST-MSN=100'
53
+ expect(item.to_s).to eq(expected)
54
+ end
55
+ end
56
+ end