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
|
@@ -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',
|
|
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 '
|
|
53
|
+
describe '.parse' do
|
|
33
54
|
it 'should parse m3u8 tag into instance' do
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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',
|
|
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' =>
|
|
145
|
+
'X-CUSTOM-TEXT' =>
|
|
146
|
+
'test_value' } }
|
|
98
147
|
item = described_class.new(options)
|
|
99
148
|
|
|
100
|
-
expected = '#EXT-X-DATERANGE:ID="test_id",
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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
|