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::ByteRange do
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::CLI::InspectCommand do
6
+ let(:stdout) { StringIO.new }
7
+
8
+ def inspect_fixture(name)
9
+ playlist = M3u8::Playlist.read(
10
+ File.read("spec/fixtures/#{name}")
11
+ )
12
+ described_class.new(playlist, stdout).run
13
+ end
14
+
15
+ describe 'media playlist' do
16
+ it 'displays metadata for a VOD playlist' do
17
+ code = inspect_fixture('playlist.m3u8')
18
+ expect(code).to eq(0)
19
+ lines = stdout.string
20
+ expect(lines).to include('Type: Media')
21
+ expect(lines).to include('Version: 4')
22
+ expect(lines).to include('Sequence: 1')
23
+ expect(lines).to include('Target: 12')
24
+ expect(lines).to include('Duration: 1371.99s')
25
+ expect(lines).to include('Playlist: VOD')
26
+ expect(lines).to include('Cache: No')
27
+ expect(lines).to include('Segments: 138')
28
+ expect(lines).to include('Keys: 0')
29
+ expect(lines).to include('Maps: 0')
30
+ end
31
+
32
+ it 'displays metadata for an encrypted playlist' do
33
+ code = inspect_fixture('encrypted.m3u8')
34
+ expect(code).to eq(0)
35
+ lines = stdout.string
36
+ expect(lines).to include('Type: Media')
37
+ expect(lines).to include('Version: 3')
38
+ expect(lines).to include('Sequence: 7794')
39
+ expect(lines).to include('Target: 15')
40
+ expect(lines).to include('Segments: 4')
41
+ expect(lines).to include('Keys: 2')
42
+ expect(lines).not_to include('Playlist:')
43
+ expect(lines).not_to include('Cache:')
44
+ end
45
+
46
+ it 'displays metadata for an LL-HLS playlist' do
47
+ code = inspect_fixture('ll_hls_playlist.m3u8')
48
+ expect(code).to eq(0)
49
+ lines = stdout.string
50
+ expect(lines).to include('Type: Media')
51
+ expect(lines).to include('Version: 9')
52
+ expect(lines).to include('Segments: 2')
53
+ expect(lines).to include('Maps: 1')
54
+ end
55
+ end
56
+
57
+ describe 'master playlist' do
58
+ it 'displays metadata for a master playlist' do
59
+ code = inspect_fixture('master.m3u8')
60
+ expect(code).to eq(0)
61
+ lines = stdout.string
62
+ expect(lines).to include('Type: Master')
63
+ expect(lines).to include(
64
+ 'Independent Segments: Yes'
65
+ )
66
+ expect(lines).to include('Variants: 6')
67
+ expect(lines).to include('1920x1080 5042000 bps')
68
+ expect(lines).to include(
69
+ 'hls/1080-7mbps/1080-7mbps.m3u8'
70
+ )
71
+ expect(lines).to include('6400 bps')
72
+ expect(lines).to include('hls/64k/64k.m3u8')
73
+ expect(lines).to include('Media: 0')
74
+ expect(lines).to include('Session Keys: 1')
75
+ expect(lines).to include('Session Data: 0')
76
+ end
77
+
78
+ it 'displays metadata for a variant audio playlist' do
79
+ code = inspect_fixture('variant_audio.m3u8')
80
+ expect(code).to eq(0)
81
+ lines = stdout.string
82
+ expect(lines).to include('Type: Master')
83
+ expect(lines).to include('Variants: 4')
84
+ expect(lines).to include('Media: 6')
85
+ expect(lines).to include('AUDIO audio-lo English')
86
+ expect(lines).to include(
87
+ "AUDIO audio-hi Fran\xC3\xA7ais"
88
+ )
89
+ expect(lines).to include('Session Keys: 0')
90
+ expect(lines).to include('Session Data: 0')
91
+ expect(lines).not_to include('Independent Segments:')
92
+ end
93
+
94
+ it 'displays session data for a session data playlist' do
95
+ code = inspect_fixture('session_data.m3u8')
96
+ expect(code).to eq(0)
97
+ lines = stdout.string
98
+ expect(lines).to include('Type: Media')
99
+ expect(lines).to include('Segments: 0')
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::CLI::ValidateCommand do
6
+ let(:stdout) { StringIO.new }
7
+
8
+ describe '#run' do
9
+ context 'when playlist is valid' do
10
+ it 'prints Valid and returns 0' do
11
+ playlist = M3u8::Playlist.read(
12
+ File.read('spec/fixtures/master.m3u8')
13
+ )
14
+ code = described_class.new(playlist, stdout).run
15
+ expect(code).to eq(0)
16
+ expect(stdout.string.strip).to eq('Valid')
17
+ end
18
+ end
19
+
20
+ context 'when playlist is invalid' do
21
+ it 'prints Invalid with specific errors and returns 1' do
22
+ playlist = M3u8::Playlist.new
23
+ playlist.items << M3u8::PlaylistItem.new(
24
+ bandwidth: 540, uri: 'test.url'
25
+ )
26
+ playlist.items << M3u8::SegmentItem.new(
27
+ duration: 10.0, segment: 'test.ts'
28
+ )
29
+ code = described_class.new(playlist, stdout).run
30
+ expect(code).to eq(1)
31
+ lines = stdout.string.split("\n")
32
+ expect(lines[0]).to eq('Invalid')
33
+ expect(lines[1]).to eq(
34
+ ' - Playlist contains both master and media items'
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::CLI do
6
+ let(:stdout) { StringIO.new }
7
+ let(:stderr) { StringIO.new }
8
+ let(:stdin) { StringIO.new }
9
+
10
+ def run(argv)
11
+ described_class.run(argv, stdin, stdout, stderr)
12
+ end
13
+
14
+ describe '--version' do
15
+ it 'prints the version and exits 0' do
16
+ expect(run(['--version'])).to eq(0)
17
+ expect(stdout.string.strip).to eq(M3u8::VERSION)
18
+ end
19
+ end
20
+
21
+ describe '--help' do
22
+ it 'prints usage and exits 0' do
23
+ expect(run(['--help'])).to eq(0)
24
+ expect(stdout.string).to include('Usage: m3u8')
25
+ expect(stdout.string).to include('inspect')
26
+ expect(stdout.string).to include('validate')
27
+ end
28
+ end
29
+
30
+ describe 'no command' do
31
+ it 'prints usage to stderr and exits 2' do
32
+ expect(run([])).to eq(2)
33
+ expect(stderr.string).to include('Usage: m3u8')
34
+ end
35
+ end
36
+
37
+ describe 'unknown command' do
38
+ it 'prints error and usage to stderr and exits 2' do
39
+ expect(run(['bogus'])).to eq(2)
40
+ expect(stderr.string).to include('unknown command: bogus')
41
+ end
42
+ end
43
+
44
+ describe 'invalid option' do
45
+ it 'prints error to stderr and exits 2' do
46
+ expect(run(['--bogus'])).to eq(2)
47
+ expect(stderr.string).to include('invalid option')
48
+ end
49
+ end
50
+
51
+ describe 'file not found' do
52
+ it 'prints error to stderr and exits 2' do
53
+ expect(run(['inspect', 'nonexistent.m3u8'])).to eq(2)
54
+ expect(stderr.string).to include('no such file')
55
+ end
56
+ end
57
+
58
+ describe 'file input' do
59
+ it 'reads a playlist from a file for inspect' do
60
+ code = run(['inspect', 'spec/fixtures/master.m3u8'])
61
+ expect(code).to eq(0)
62
+ end
63
+
64
+ it 'reads a playlist from a file for validate' do
65
+ code = run(['validate', 'spec/fixtures/master.m3u8'])
66
+ expect(code).to eq(0)
67
+ expect(stdout.string.strip).to eq('Valid')
68
+ end
69
+ end
70
+
71
+ describe 'parse error' do
72
+ it 'prints error to stderr and exits 2' do
73
+ stdin = StringIO.new('not a playlist')
74
+ code = described_class.run(
75
+ ['inspect'], stdin, stdout, stderr
76
+ )
77
+ expect(code).to eq(2)
78
+ expect(stderr.string).to include('parse error')
79
+ end
80
+ end
81
+
82
+ describe 'stdin input' do
83
+ it 'reads a playlist from stdin' do
84
+ content = File.read('spec/fixtures/master.m3u8')
85
+ stdin = StringIO.new(content)
86
+ code = described_class.run(
87
+ ['inspect'], stdin, stdout, stderr
88
+ )
89
+ expect(code).to eq(0)
90
+ end
91
+ end
92
+
93
+ describe 'command with no input on a tty' do
94
+ it 'prints usage to stderr and exits 2' do
95
+ tty = StringIO.new
96
+ allow(tty).to receive(:tty?).and_return(true)
97
+ code = described_class.run(
98
+ ['inspect'], tty, stdout, stderr
99
+ )
100
+ expect(code).to eq(2)
101
+ expect(stderr.string).to include('Usage: m3u8')
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::ContentSteeringItem do
6
+ describe '.new' do
7
+ it 'assigns attributes from options' do
8
+ item = described_class.new(
9
+ server_uri: 'https://example.com/steering',
10
+ pathway_id: 'CDN-A'
11
+ )
12
+ expect(item.server_uri).to eq('https://example.com/steering')
13
+ expect(item.pathway_id).to eq('CDN-A')
14
+ end
15
+ end
16
+
17
+ describe '.parse' do
18
+ it 'parses tag with all attributes' do
19
+ tag = '#EXT-X-CONTENT-STEERING:SERVER-URI=' \
20
+ '"https://example.com/steering",PATHWAY-ID="CDN-A"'
21
+ item = described_class.parse(tag)
22
+ expect(item.server_uri).to eq('https://example.com/steering')
23
+ expect(item.pathway_id).to eq('CDN-A')
24
+ end
25
+
26
+ it 'parses tag without optional pathway id' do
27
+ tag = '#EXT-X-CONTENT-STEERING:' \
28
+ 'SERVER-URI="https://example.com/steering"'
29
+ item = described_class.parse(tag)
30
+ expect(item.server_uri).to eq('https://example.com/steering')
31
+ expect(item.pathway_id).to be_nil
32
+ end
33
+ end
34
+
35
+ describe '#to_s' do
36
+ it 'returns tag with all attributes' do
37
+ item = described_class.new(
38
+ server_uri: 'https://example.com/steering',
39
+ pathway_id: 'CDN-A'
40
+ )
41
+ expected = '#EXT-X-CONTENT-STEERING:' \
42
+ 'SERVER-URI="https://example.com/steering",' \
43
+ 'PATHWAY-ID="CDN-A"'
44
+ expect(item.to_s).to eq(expected)
45
+ end
46
+
47
+ it 'returns tag without optional pathway id' do
48
+ item = described_class.new(
49
+ server_uri: 'https://example.com/steering'
50
+ )
51
+ expected = '#EXT-X-CONTENT-STEERING:' \
52
+ 'SERVER-URI="https://example.com/steering"'
53
+ expect(item.to_s).to eq(expected)
54
+ end
55
+ end
56
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'spec_helper'
3
4
 
4
5
  describe M3u8::DateRangeItem do
@@ -10,7 +11,17 @@ describe M3u8::DateRangeItem do
10
11
  planned_duration: 59.993,
11
12
  scte35_out: '0xFC002F0000000000FF0',
12
13
  scte35_in: '0xFC002F0000000000FF1',
13
- scte35_cmd: '0xFC002F0000000000FF2', end_on_next: true,
14
+ scte35_cmd: '0xFC002F0000000000FF2',
15
+ cue: 'PRE', end_on_next: true,
16
+ asset_uri: 'http://example.com/ad.m3u8',
17
+ asset_list: 'http://example.com/ads.json',
18
+ resume_offset: 10.5,
19
+ playout_limit: 30.0,
20
+ restrict: 'SKIP,JUMP',
21
+ snap: 'OUT',
22
+ timeline_occupies: 'RANGE',
23
+ timeline_style: 'HIGHLIGHT',
24
+ content_may_vary: 'YES',
14
25
  client_attributes: { 'X-CUSTOM' => 45.3 } }
15
26
  item = described_class.new(options)
16
27
 
@@ -23,23 +34,42 @@ describe M3u8::DateRangeItem do
23
34
  expect(item.scte35_out).to eq('0xFC002F0000000000FF0')
24
35
  expect(item.scte35_in).to eq('0xFC002F0000000000FF1')
25
36
  expect(item.scte35_cmd).to eq('0xFC002F0000000000FF2')
37
+ expect(item.cue).to eq('PRE')
26
38
  expect(item.end_on_next).to be true
39
+ expect(item.asset_uri).to eq('http://example.com/ad.m3u8')
40
+ expect(item.asset_list).to eq('http://example.com/ads.json')
41
+ expect(item.resume_offset).to eq(10.5)
42
+ expect(item.playout_limit).to eq(30.0)
43
+ expect(item.restrict).to eq('SKIP,JUMP')
44
+ expect(item.snap).to eq('OUT')
45
+ expect(item.timeline_occupies).to eq('RANGE')
46
+ expect(item.timeline_style).to eq('HIGHLIGHT')
47
+ expect(item.content_may_vary).to eq('YES')
27
48
  expect(item.client_attributes.empty?).to be false
28
49
  expect(item.client_attributes['X-CUSTOM']).to eq(45.3)
29
50
  end
30
51
  end
31
52
 
32
- describe '#parse' do
53
+ describe '.parse' do
33
54
  it 'should parse m3u8 tag into instance' do
34
- item = described_class.new
35
- line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class"' \
36
- 'START-DATE="2014-03-05T11:15:00Z",' \
37
- 'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
38
- 'PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0,' \
39
- 'SCTE35-IN=0xFC002F0000000000FF1,' \
40
- 'SCTE35-CMD=0xFC002F0000000000FF2,' \
41
- 'END-ON-NEXT=YES'
42
- item.parse(line)
55
+ line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class",' \
56
+ 'START-DATE="2014-03-05T11:15:00Z",' \
57
+ 'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
58
+ 'PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0,' \
59
+ 'SCTE35-IN=0xFC002F0000000000FF1,' \
60
+ 'SCTE35-CMD=0xFC002F0000000000FF2,' \
61
+ 'X-ASSET-URI="http://example.com/ad.m3u8",' \
62
+ 'X-ASSET-LIST="http://example.com/ads.json",' \
63
+ 'X-RESUME-OFFSET=10.5,' \
64
+ 'X-PLAYOUT-LIMIT=30.0,' \
65
+ 'X-RESTRICT="SKIP,JUMP",' \
66
+ 'X-SNAP="OUT",' \
67
+ 'X-TIMELINE-OCCUPIES="RANGE",' \
68
+ 'X-TIMELINE-STYLE="HIGHLIGHT",' \
69
+ 'X-CONTENT-MAY-VARY="YES",' \
70
+ 'CUE="PRE",' \
71
+ 'END-ON-NEXT=YES'
72
+ item = described_class.parse(line)
43
73
 
44
74
  expect(item.id).to eq('splice-6FFFFFF0')
45
75
  expect(item.class_name).to eq('test_class')
@@ -50,15 +80,24 @@ describe M3u8::DateRangeItem do
50
80
  expect(item.scte35_out).to eq('0xFC002F0000000000FF0')
51
81
  expect(item.scte35_in).to eq('0xFC002F0000000000FF1')
52
82
  expect(item.scte35_cmd).to eq('0xFC002F0000000000FF2')
83
+ expect(item.asset_uri).to eq('http://example.com/ad.m3u8')
84
+ expect(item.asset_list).to eq('http://example.com/ads.json')
85
+ expect(item.resume_offset).to eq(10.5)
86
+ expect(item.playout_limit).to eq(30.0)
87
+ expect(item.restrict).to eq('SKIP,JUMP')
88
+ expect(item.snap).to eq('OUT')
89
+ expect(item.timeline_occupies).to eq('RANGE')
90
+ expect(item.timeline_style).to eq('HIGHLIGHT')
91
+ expect(item.content_may_vary).to eq('YES')
92
+ expect(item.cue).to eq('PRE')
53
93
  expect(item.end_on_next).to be true
54
94
  expect(item.client_attributes.empty?).to be true
55
95
  end
56
96
 
57
97
  it 'should ignore optional attributes' do
58
- item = described_class.new
59
98
  line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",' \
60
- 'START-DATE="2014-03-05T11:15:00Z"'
61
- item.parse(line)
99
+ 'START-DATE="2014-03-05T11:15:00Z"'
100
+ item = described_class.parse(line)
62
101
 
63
102
  expect(item.id).to eq('splice-6FFFFFF0')
64
103
  expect(item.class_name).to be_nil
@@ -74,11 +113,10 @@ describe M3u8::DateRangeItem do
74
113
  end
75
114
 
76
115
  it 'should parse client-defined attributes' do
77
- item = described_class.new
78
116
  line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",' \
79
- 'START-DATE="2014-03-05T11:15:00Z",' \
80
- 'X-CUSTOM-VALUE="test_value",'
81
- item.parse(line)
117
+ 'START-DATE="2014-03-05T11:15:00Z",' \
118
+ 'X-CUSTOM-VALUE="test_value",'
119
+ item = described_class.parse(line)
82
120
 
83
121
  expect(item.client_attributes['X-CUSTOM-VALUE']).to eq('test_value')
84
122
  end
@@ -92,21 +130,64 @@ describe M3u8::DateRangeItem do
92
130
  planned_duration: 59.993,
93
131
  scte35_out: '0xFC002F0000000000FF0',
94
132
  scte35_in: '0xFC002F0000000000FF1',
95
- scte35_cmd: '0xFC002F0000000000FF2', end_on_next: true,
133
+ scte35_cmd: '0xFC002F0000000000FF2',
134
+ asset_uri: 'http://example.com/ad.m3u8',
135
+ asset_list: 'http://example.com/ads.json',
136
+ resume_offset: 10.5,
137
+ playout_limit: 30.0,
138
+ restrict: 'SKIP,JUMP',
139
+ snap: 'OUT',
140
+ timeline_occupies: 'RANGE',
141
+ timeline_style: 'HIGHLIGHT',
142
+ content_may_vary: 'YES',
143
+ cue: 'POST,ONCE', end_on_next: true,
96
144
  client_attributes: { 'X-CUSTOM' => 45.3,
97
- 'X-CUSTOM-TEXT' => 'test_value' } }
145
+ 'X-CUSTOM-TEXT' =>
146
+ 'test_value' } }
98
147
  item = described_class.new(options)
99
148
 
100
- expected = '#EXT-X-DATERANGE:ID="test_id",CLASS="test_class",' \
101
- 'START-DATE="2014-03-05T11:15:00Z",' \
102
- 'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
103
- 'PLANNED-DURATION=59.993,' \
104
- 'X-CUSTOM=45.3,' \
105
- 'X-CUSTOM-TEXT="test_value",' \
106
- 'SCTE35-CMD=0xFC002F0000000000FF2,' \
107
- 'SCTE35-OUT=0xFC002F0000000000FF0,' \
108
- 'SCTE35-IN=0xFC002F0000000000FF1,' \
109
- 'END-ON-NEXT=YES'
149
+ expected = '#EXT-X-DATERANGE:ID="test_id",' \
150
+ 'CLASS="test_class",' \
151
+ 'START-DATE="2014-03-05T11:15:00Z",' \
152
+ 'END-DATE="2014-03-05T11:16:00Z",' \
153
+ 'DURATION=60.1,' \
154
+ 'PLANNED-DURATION=59.993,' \
155
+ 'X-CUSTOM=45.3,' \
156
+ 'X-CUSTOM-TEXT="test_value",' \
157
+ 'X-ASSET-URI="http://example.com/ad.m3u8",' \
158
+ 'X-ASSET-LIST="http://example.com/ads.json",' \
159
+ 'X-RESUME-OFFSET=10.5,' \
160
+ 'X-PLAYOUT-LIMIT=30.0,' \
161
+ 'X-RESTRICT="SKIP,JUMP",' \
162
+ 'X-SNAP="OUT",' \
163
+ 'X-TIMELINE-OCCUPIES="RANGE",' \
164
+ 'X-TIMELINE-STYLE="HIGHLIGHT",' \
165
+ 'X-CONTENT-MAY-VARY="YES",' \
166
+ 'SCTE35-CMD=0xFC002F0000000000FF2,' \
167
+ 'SCTE35-OUT=0xFC002F0000000000FF0,' \
168
+ 'SCTE35-IN=0xFC002F0000000000FF1,' \
169
+ 'CUE="POST,ONCE",' \
170
+ 'END-ON-NEXT=YES'
171
+
172
+ expect(item.to_s).to eq(expected)
173
+ end
174
+
175
+ it 'should render small float values as floating-point number instead of scientific notation' do
176
+ options = { id: 'test_id', start_date: '2014-03-05T11:15:00Z',
177
+ duration: 0.00001,
178
+ planned_duration: 0.00002,
179
+ resume_offset: 0.00003,
180
+ playout_limit: 0.00004,
181
+ client_attributes: { 'X-CUSTOM' => 0.00005 } }
182
+ item = described_class.new(options)
183
+
184
+ expected = '#EXT-X-DATERANGE:ID="test_id",' \
185
+ 'START-DATE="2014-03-05T11:15:00Z",' \
186
+ 'DURATION=0.00001,' \
187
+ 'PLANNED-DURATION=0.00002,' \
188
+ 'X-CUSTOM=0.00005,' \
189
+ 'X-RESUME-OFFSET=0.00003,' \
190
+ 'X-PLAYOUT-LIMIT=0.00004'
110
191
 
111
192
  expect(item.to_s).to eq(expected)
112
193
  end
@@ -116,9 +197,56 @@ describe M3u8::DateRangeItem do
116
197
  item = described_class.new(options)
117
198
 
118
199
  expected = '#EXT-X-DATERANGE:ID="test_id",' \
119
- 'START-DATE="2014-03-05T11:15:00Z"'
200
+ 'START-DATE="2014-03-05T11:15:00Z"'
120
201
 
121
202
  expect(item.to_s).to eq(expected)
122
203
  end
123
204
  end
205
+
206
+ describe '#scte35_out_info' do
207
+ it 'should return parsed Scte35 when scte35_out is set' do
208
+ hex = '0xFC301100000000000000FFF000000000DEADBEEF'
209
+ item = described_class.new(scte35_out: hex)
210
+ result = item.scte35_out_info
211
+
212
+ expect(result).to be_a(M3u8::Scte35)
213
+ expect(result.table_id).to eq(0xFC)
214
+ expect(result.to_s).to eq(hex)
215
+ end
216
+
217
+ it 'should return nil when scte35_out is nil' do
218
+ item = described_class.new
219
+ expect(item.scte35_out_info).to be_nil
220
+ end
221
+ end
222
+
223
+ describe '#scte35_in_info' do
224
+ it 'should return parsed Scte35 when scte35_in is set' do
225
+ hex = '0xFC301100000000000000FFF000000000DEADBEEF'
226
+ item = described_class.new(scte35_in: hex)
227
+ result = item.scte35_in_info
228
+
229
+ expect(result).to be_a(M3u8::Scte35)
230
+ end
231
+
232
+ it 'should return nil when scte35_in is nil' do
233
+ item = described_class.new
234
+ expect(item.scte35_in_info).to be_nil
235
+ end
236
+ end
237
+
238
+ describe '#scte35_cmd_info' do
239
+ it 'should return parsed Scte35 when scte35_cmd is set' do
240
+ hex = '0xFC301100000000000000FFF000000000DEADBEEF'
241
+ item = described_class.new(scte35_cmd: hex)
242
+ result = item.scte35_cmd_info
243
+
244
+ expect(result).to be_a(M3u8::Scte35)
245
+ end
246
+
247
+ it 'should return nil when scte35_cmd is nil' do
248
+ item = described_class.new
249
+ expect(item.scte35_cmd_info).to be_nil
250
+ end
251
+ end
124
252
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::DefineItem do
6
+ describe '.new' do
7
+ it 'assigns attributes from options' do
8
+ item = described_class.new(name: 'base_url',
9
+ value: 'http://example.com')
10
+ expect(item.name).to eq('base_url')
11
+ expect(item.value).to eq('http://example.com')
12
+ end
13
+ end
14
+
15
+ describe '.parse' do
16
+ it 'parses NAME/VALUE define' do
17
+ tag = '#EXT-X-DEFINE:NAME="base_url",VALUE="http://example.com"'
18
+ item = described_class.parse(tag)
19
+ expect(item.name).to eq('base_url')
20
+ expect(item.value).to eq('http://example.com')
21
+ expect(item.import).to be_nil
22
+ expect(item.queryparam).to be_nil
23
+ end
24
+
25
+ it 'parses IMPORT define' do
26
+ tag = '#EXT-X-DEFINE:IMPORT="base_url"'
27
+ item = described_class.parse(tag)
28
+ expect(item.import).to eq('base_url')
29
+ expect(item.name).to be_nil
30
+ end
31
+
32
+ it 'parses QUERYPARAM define' do
33
+ tag = '#EXT-X-DEFINE:QUERYPARAM="token"'
34
+ item = described_class.parse(tag)
35
+ expect(item.queryparam).to eq('token')
36
+ expect(item.name).to be_nil
37
+ end
38
+ end
39
+
40
+ describe '#to_s' do
41
+ it 'returns NAME/VALUE format' do
42
+ item = described_class.new(name: 'base_url',
43
+ value: 'http://example.com')
44
+ expected = '#EXT-X-DEFINE:NAME="base_url",' \
45
+ 'VALUE="http://example.com"'
46
+ expect(item.to_s).to eq(expected)
47
+ end
48
+
49
+ it 'returns IMPORT format' do
50
+ item = described_class.new(import: 'base_url')
51
+ expect(item.to_s).to eq('#EXT-X-DEFINE:IMPORT="base_url"')
52
+ end
53
+
54
+ it 'returns QUERYPARAM format' do
55
+ item = described_class.new(queryparam: 'token')
56
+ expect(item.to_s).to eq('#EXT-X-DEFINE:QUERYPARAM="token"')
57
+ end
58
+ end
59
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'spec_helper'
3
4
 
4
5
  describe M3u8::DiscontinuityItem do
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe M3u8::GapItem do
6
+ describe '#to_s' do
7
+ it 'returns m3u8 format representation' do
8
+ item = described_class.new
9
+ expect(item.to_s).to eq('#EXT-X-GAP')
10
+ end
11
+ end
12
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'spec_helper'
3
4
 
4
5
  describe M3u8::KeyItem do
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'spec_helper'
3
4
 
4
5
  describe M3u8::MapItem do