m3u8 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/CLAUDE.md +1 -1
- data/README.md +52 -0
- data/bin/m3u8 +6 -0
- data/lib/m3u8/cli/inspect_command.rb +97 -0
- data/lib/m3u8/cli/validate_command.rb +23 -0
- data/lib/m3u8/cli.rb +116 -0
- data/lib/m3u8/playlist.rb +12 -8
- data/lib/m3u8/version.rb +1 -1
- data/lib/m3u8.rb +0 -2
- data/spec/lib/m3u8/cli/inspect_command_spec.rb +102 -0
- data/spec/lib/m3u8/cli/validate_command_spec.rb +35 -0
- data/spec/lib/m3u8/cli_spec.rb +104 -0
- data/spec/lib/m3u8/playlist_spec.rb +38 -18
- data/spec/lib/m3u8/reader_spec.rb +66 -46
- data/spec/lib/m3u8/round_trip_spec.rb +3 -9
- metadata +14 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bbf61a694bdc2daf585ad9f405be821f09ba53289ef075e4342f267e674eb21b
|
|
4
|
+
data.tar.gz: d599367f1344f9b1ad91c20590bc0b4c57a6d0369c71e3a34fd69325ae0c7f44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9574b7ac1a95fe6b2fabe0bd28576b2b21713a1bed68e2a18d4ea9f644920a91a08e4696df69a19d7b8392eda87fc8fa14b090338ffebe31524cd5f3f4235539
|
|
7
|
+
data.tar.gz: 2f34f676d931929f6671b4e3b3ba3efc0e49e73fc2dc54f6c3a64f499e8d2c1a20b974fcad3febcb0f17252d6aef1f9fb612b1c1ec9ad1911e41aa28314f8ea1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
**1.3.0**
|
|
2
|
+
|
|
3
|
+
* Added CLI tool (`bin/m3u8`) with `inspect` and `validate` subcommands for inspecting playlist metadata and checking validity from the command line. Supports file arguments and stdin piping.
|
|
4
|
+
* Added `session_keys` convenience accessor to `Playlist`.
|
|
5
|
+
|
|
6
|
+
***
|
|
7
|
+
|
|
1
8
|
**1.2.0**
|
|
2
9
|
|
|
3
10
|
* Added `Playlist.build` with block-based Builder DSL for concise playlist construction. Supports both `instance_eval` (clean DSL) and yielded builder (outer scope access) forms. All 19 item types have corresponding DSL methods.
|
data/CLAUDE.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
AGENTS.md
|
data/README.md
CHANGED
|
@@ -30,6 +30,58 @@ Or install it yourself as:
|
|
|
30
30
|
|
|
31
31
|
$ gem install m3u8
|
|
32
32
|
|
|
33
|
+
## CLI
|
|
34
|
+
|
|
35
|
+
The gem includes a command-line tool for inspecting and validating playlists.
|
|
36
|
+
|
|
37
|
+
### Inspect
|
|
38
|
+
|
|
39
|
+
Display playlist metadata and item summary:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
$ m3u8 inspect master.m3u8
|
|
43
|
+
Type: Master
|
|
44
|
+
Independent Segments: Yes
|
|
45
|
+
|
|
46
|
+
Variants: 6
|
|
47
|
+
1920x1080 5042000 bps hls/1080/1080.m3u8
|
|
48
|
+
640x360 861000 bps hls/360/360.m3u8
|
|
49
|
+
Media: 2
|
|
50
|
+
Session Keys: 1
|
|
51
|
+
Session Data: 0
|
|
52
|
+
|
|
53
|
+
$ m3u8 inspect media.m3u8
|
|
54
|
+
Type: Media
|
|
55
|
+
Version: 4
|
|
56
|
+
Sequence: 1
|
|
57
|
+
Target: 12
|
|
58
|
+
Duration: 1371.99s
|
|
59
|
+
Playlist: VOD
|
|
60
|
+
Cache: No
|
|
61
|
+
|
|
62
|
+
Segments: 138
|
|
63
|
+
Keys: 0
|
|
64
|
+
Maps: 0
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Reads from stdin when no file is given:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
$ cat playlist.m3u8 | m3u8 inspect
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Validate
|
|
74
|
+
|
|
75
|
+
Check playlist validity (exit 0 for valid, 1 for invalid):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
$ m3u8 validate playlist.m3u8
|
|
79
|
+
Valid
|
|
80
|
+
|
|
81
|
+
$ m3u8 validate bad.m3u8
|
|
82
|
+
Invalid: mixed playlist and segment items
|
|
83
|
+
```
|
|
84
|
+
|
|
33
85
|
## Usage (Builder DSL)
|
|
34
86
|
|
|
35
87
|
`Playlist.build` provides a block-based DSL for concise playlist construction. It supports two forms:
|
data/bin/m3u8
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
class CLI
|
|
5
|
+
# InspectCommand displays metadata about a playlist
|
|
6
|
+
class InspectCommand
|
|
7
|
+
MEDIA_WIDTH = 12
|
|
8
|
+
MASTER_WIDTH = 23
|
|
9
|
+
|
|
10
|
+
def initialize(playlist, stdout)
|
|
11
|
+
@playlist = playlist
|
|
12
|
+
@stdout = stdout
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
if @playlist.master?
|
|
17
|
+
print_master
|
|
18
|
+
else
|
|
19
|
+
print_media
|
|
20
|
+
end
|
|
21
|
+
0
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def print_media
|
|
27
|
+
field 'Type', 'Media', MEDIA_WIDTH
|
|
28
|
+
field 'Version', @playlist.version, MEDIA_WIDTH
|
|
29
|
+
field 'Sequence', @playlist.sequence, MEDIA_WIDTH
|
|
30
|
+
field 'Target', @playlist.target, MEDIA_WIDTH
|
|
31
|
+
field 'Duration', duration_value, MEDIA_WIDTH
|
|
32
|
+
field 'Playlist', @playlist.type, MEDIA_WIDTH
|
|
33
|
+
field 'Cache', cache_value, MEDIA_WIDTH
|
|
34
|
+
@stdout.puts
|
|
35
|
+
field 'Segments', @playlist.segments.size, MEDIA_WIDTH
|
|
36
|
+
field 'Keys', @playlist.keys.size, MEDIA_WIDTH
|
|
37
|
+
field 'Maps', @playlist.maps.size, MEDIA_WIDTH
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def print_master
|
|
41
|
+
field 'Type', 'Master', MASTER_WIDTH
|
|
42
|
+
field 'Independent Segments',
|
|
43
|
+
independent_segments_value, MASTER_WIDTH
|
|
44
|
+
@stdout.puts
|
|
45
|
+
print_variants
|
|
46
|
+
print_media_items
|
|
47
|
+
field 'Session Keys',
|
|
48
|
+
@playlist.session_keys.size, MASTER_WIDTH
|
|
49
|
+
field 'Session Data',
|
|
50
|
+
@playlist.session_data.size, MASTER_WIDTH
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def print_variants
|
|
54
|
+
variants = @playlist.playlists
|
|
55
|
+
field 'Variants', variants.size, MASTER_WIDTH
|
|
56
|
+
variants.each { |v| @stdout.puts variant_line(v) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def print_media_items
|
|
60
|
+
items = @playlist.media_items
|
|
61
|
+
field 'Media', items.size, MASTER_WIDTH
|
|
62
|
+
items.each do |m|
|
|
63
|
+
@stdout.puts " #{m.type} #{m.group_id} #{m.name}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def variant_line(variant)
|
|
68
|
+
res = variant.resolution || ''
|
|
69
|
+
format(' %-11<res>s%<bw>s bps %<uri>s',
|
|
70
|
+
res: res, bw: variant.bandwidth, uri: variant.uri)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def independent_segments_value
|
|
74
|
+
return unless @playlist.independent_segments
|
|
75
|
+
|
|
76
|
+
'Yes'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def duration_value
|
|
80
|
+
format('%<s>gs', s: @playlist.duration)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cache_value
|
|
84
|
+
return unless @playlist.cache == false
|
|
85
|
+
|
|
86
|
+
'No'
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def field(label, value, width)
|
|
90
|
+
return if value.nil?
|
|
91
|
+
|
|
92
|
+
@stdout.puts format("%-#{width}<label>s%<value>s",
|
|
93
|
+
label: "#{label}:", value: value)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
class CLI
|
|
5
|
+
# ValidateCommand checks playlist validity
|
|
6
|
+
class ValidateCommand
|
|
7
|
+
def initialize(playlist, stdout)
|
|
8
|
+
@playlist = playlist
|
|
9
|
+
@stdout = stdout
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
if @playlist.valid?
|
|
14
|
+
@stdout.puts 'Valid'
|
|
15
|
+
0
|
|
16
|
+
else
|
|
17
|
+
@stdout.puts 'Invalid: mixed playlist and segment items'
|
|
18
|
+
1
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/m3u8/cli.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require_relative 'cli/inspect_command'
|
|
5
|
+
require_relative 'cli/validate_command'
|
|
6
|
+
|
|
7
|
+
module M3u8
|
|
8
|
+
# CLI provides a command-line interface for inspecting and validating
|
|
9
|
+
# m3u8 playlists
|
|
10
|
+
class CLI
|
|
11
|
+
COMMANDS = %w[inspect validate].freeze
|
|
12
|
+
|
|
13
|
+
def self.run(argv, stdin, stdout, stderr)
|
|
14
|
+
new(argv, stdin, stdout, stderr).run
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(argv, stdin, stdout, stderr)
|
|
18
|
+
@argv = argv.dup
|
|
19
|
+
@stdin = stdin
|
|
20
|
+
@stdout = stdout
|
|
21
|
+
@stderr = stderr
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run
|
|
25
|
+
parse_global_options
|
|
26
|
+
dispatch
|
|
27
|
+
rescue OptionParser::InvalidOption => e
|
|
28
|
+
@stderr.puts e.message
|
|
29
|
+
2
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse_global_options
|
|
35
|
+
@parser = OptionParser.new do |opts|
|
|
36
|
+
opts.banner = 'Usage: m3u8 <command> [options] [file]'
|
|
37
|
+
opts.separator ''
|
|
38
|
+
opts.separator 'Commands:'
|
|
39
|
+
opts.separator ' inspect Show playlist metadata'
|
|
40
|
+
opts.separator ' validate Check playlist validity'
|
|
41
|
+
opts.separator ''
|
|
42
|
+
opts.on('-v', '--version', 'Show version') do
|
|
43
|
+
@stdout.puts M3u8::VERSION
|
|
44
|
+
throw :exit, 0
|
|
45
|
+
end
|
|
46
|
+
opts.on('-h', '--help', 'Show help') do
|
|
47
|
+
@stdout.puts opts
|
|
48
|
+
throw :exit, 0
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@exit_code = catch(:exit) do
|
|
53
|
+
@parser.order!(@argv)
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dispatch
|
|
59
|
+
return @exit_code if @exit_code
|
|
60
|
+
|
|
61
|
+
command = @argv.shift
|
|
62
|
+
return usage_error if command.nil?
|
|
63
|
+
return usage_error("unknown command: #{command}") \
|
|
64
|
+
unless COMMANDS.include?(command)
|
|
65
|
+
|
|
66
|
+
input = resolve_input
|
|
67
|
+
return 2 unless input
|
|
68
|
+
|
|
69
|
+
playlist = parse_playlist(input)
|
|
70
|
+
return 2 unless playlist
|
|
71
|
+
|
|
72
|
+
execute_command(command, playlist)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def execute_command(command, playlist)
|
|
76
|
+
case command
|
|
77
|
+
when 'inspect'
|
|
78
|
+
InspectCommand.new(playlist, @stdout).run
|
|
79
|
+
when 'validate'
|
|
80
|
+
ValidateCommand.new(playlist, @stdout).run
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def resolve_input
|
|
85
|
+
file = @argv.shift
|
|
86
|
+
if file
|
|
87
|
+
read_file(file)
|
|
88
|
+
elsif !@stdin.tty?
|
|
89
|
+
@stdin.read
|
|
90
|
+
else
|
|
91
|
+
usage_error
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def read_file(path)
|
|
97
|
+
File.read(path)
|
|
98
|
+
rescue Errno::ENOENT
|
|
99
|
+
@stderr.puts "no such file: #{path}"
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def parse_playlist(input)
|
|
104
|
+
Playlist.read(input)
|
|
105
|
+
rescue StandardError => e
|
|
106
|
+
@stderr.puts "parse error: #{e.message}"
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def usage_error(message = nil)
|
|
111
|
+
@stderr.puts message if message
|
|
112
|
+
@stderr.puts @parser.to_s
|
|
113
|
+
2
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/m3u8/playlist.rb
CHANGED
|
@@ -66,35 +66,39 @@ module M3u8
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
def segments
|
|
69
|
-
items.
|
|
69
|
+
items.grep(SegmentItem)
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def playlists
|
|
73
|
-
items.
|
|
73
|
+
items.grep(PlaylistItem)
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def media_items
|
|
77
|
-
items.
|
|
77
|
+
items.grep(MediaItem)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def keys
|
|
81
|
-
items.
|
|
81
|
+
items.grep(KeyItem)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def maps
|
|
85
|
-
items.
|
|
85
|
+
items.grep(MapItem)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def date_ranges
|
|
89
|
-
items.
|
|
89
|
+
items.grep(DateRangeItem)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def parts
|
|
93
|
-
items.
|
|
93
|
+
items.grep(PartItem)
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def session_data
|
|
97
|
-
items.
|
|
97
|
+
items.grep(SessionDataItem)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def session_keys
|
|
101
|
+
items.grep(SessionKeyItem)
|
|
98
102
|
end
|
|
99
103
|
|
|
100
104
|
def duration
|
data/lib/m3u8/version.rb
CHANGED
data/lib/m3u8.rb
CHANGED
|
@@ -14,10 +14,8 @@ module M3u8
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def parse_attributes(line)
|
|
17
|
-
# rubocop:disable Style/HashTransformValues
|
|
18
17
|
line.delete("\n").scan(/([A-Za-z0-9-]+)\s*=\s*("[^"]*"|[^,]*)/)
|
|
19
18
|
.to_h { |key, value| [key, value.delete('"')] }
|
|
20
|
-
# rubocop:enable Style/HashTransformValues
|
|
21
19
|
end
|
|
22
20
|
|
|
23
21
|
def parse_float(value)
|
|
@@ -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,35 @@
|
|
|
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 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
|
+
expect(stdout.string.strip).to include('Invalid')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
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
|
|
@@ -60,8 +60,9 @@ describe M3u8::Playlist do
|
|
|
60
60
|
|
|
61
61
|
describe '.read' do
|
|
62
62
|
it 'returns new playlist from content' do
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
playlist = described_class.read(
|
|
64
|
+
File.read('spec/fixtures/master.m3u8')
|
|
65
|
+
)
|
|
65
66
|
expect(playlist.master?).to be true
|
|
66
67
|
expect(playlist.items.size).to eq(8)
|
|
67
68
|
end
|
|
@@ -238,8 +239,9 @@ describe M3u8::Playlist do
|
|
|
238
239
|
|
|
239
240
|
describe '#segments' do
|
|
240
241
|
it 'returns only segment items' do
|
|
241
|
-
|
|
242
|
-
|
|
242
|
+
playlist = described_class.read(
|
|
243
|
+
File.read('spec/fixtures/playlist.m3u8')
|
|
244
|
+
)
|
|
243
245
|
expect(playlist.segments).to all be_a(M3u8::SegmentItem)
|
|
244
246
|
expect(playlist.segments.size).to eq(138)
|
|
245
247
|
end
|
|
@@ -247,8 +249,9 @@ describe M3u8::Playlist do
|
|
|
247
249
|
|
|
248
250
|
describe '#playlists' do
|
|
249
251
|
it 'returns only playlist items' do
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
playlist = described_class.read(
|
|
253
|
+
File.read('spec/fixtures/master.m3u8')
|
|
254
|
+
)
|
|
252
255
|
expect(playlist.playlists).to all be_a(M3u8::PlaylistItem)
|
|
253
256
|
expect(playlist.playlists.size).to eq(6)
|
|
254
257
|
end
|
|
@@ -256,8 +259,9 @@ describe M3u8::Playlist do
|
|
|
256
259
|
|
|
257
260
|
describe '#media_items' do
|
|
258
261
|
it 'returns only media items' do
|
|
259
|
-
|
|
260
|
-
|
|
262
|
+
playlist = described_class.read(
|
|
263
|
+
File.read('spec/fixtures/variant_audio.m3u8')
|
|
264
|
+
)
|
|
261
265
|
expect(playlist.media_items).to all be_a(M3u8::MediaItem)
|
|
262
266
|
expect(playlist.media_items.size).to eq(6)
|
|
263
267
|
end
|
|
@@ -265,8 +269,9 @@ describe M3u8::Playlist do
|
|
|
265
269
|
|
|
266
270
|
describe '#keys' do
|
|
267
271
|
it 'returns only key items' do
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
playlist = described_class.read(
|
|
273
|
+
File.read('spec/fixtures/encrypted.m3u8')
|
|
274
|
+
)
|
|
270
275
|
expect(playlist.keys).to all be_a(M3u8::KeyItem)
|
|
271
276
|
expect(playlist.keys.size).to eq(2)
|
|
272
277
|
end
|
|
@@ -274,8 +279,9 @@ describe M3u8::Playlist do
|
|
|
274
279
|
|
|
275
280
|
describe '#maps' do
|
|
276
281
|
it 'returns only map items' do
|
|
277
|
-
|
|
278
|
-
|
|
282
|
+
playlist = described_class.read(
|
|
283
|
+
File.read('spec/fixtures/map_playlist.m3u8')
|
|
284
|
+
)
|
|
279
285
|
expect(playlist.maps).to all be_a(M3u8::MapItem)
|
|
280
286
|
expect(playlist.maps.size).to eq(1)
|
|
281
287
|
end
|
|
@@ -283,8 +289,9 @@ describe M3u8::Playlist do
|
|
|
283
289
|
|
|
284
290
|
describe '#date_ranges' do
|
|
285
291
|
it 'returns only date range items' do
|
|
286
|
-
|
|
287
|
-
|
|
292
|
+
playlist = described_class.read(
|
|
293
|
+
File.read('spec/fixtures/daterange_playlist.m3u8')
|
|
294
|
+
)
|
|
288
295
|
expect(playlist.date_ranges)
|
|
289
296
|
.to all be_a(M3u8::DateRangeItem)
|
|
290
297
|
expect(playlist.date_ranges.size).to eq(3)
|
|
@@ -293,8 +300,9 @@ describe M3u8::Playlist do
|
|
|
293
300
|
|
|
294
301
|
describe '#parts' do
|
|
295
302
|
it 'returns only part items' do
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
playlist = described_class.read(
|
|
304
|
+
File.read('spec/fixtures/ll_hls_playlist.m3u8')
|
|
305
|
+
)
|
|
298
306
|
expect(playlist.parts).to all be_a(M3u8::PartItem)
|
|
299
307
|
expect(playlist.parts.size).to eq(5)
|
|
300
308
|
end
|
|
@@ -302,14 +310,26 @@ describe M3u8::Playlist do
|
|
|
302
310
|
|
|
303
311
|
describe '#session_data' do
|
|
304
312
|
it 'returns only session data items' do
|
|
305
|
-
|
|
306
|
-
|
|
313
|
+
playlist = described_class.read(
|
|
314
|
+
File.read('spec/fixtures/session_data.m3u8')
|
|
315
|
+
)
|
|
307
316
|
expect(playlist.session_data)
|
|
308
317
|
.to all be_a(M3u8::SessionDataItem)
|
|
309
318
|
expect(playlist.session_data.size).to eq(3)
|
|
310
319
|
end
|
|
311
320
|
end
|
|
312
321
|
|
|
322
|
+
describe '#session_keys' do
|
|
323
|
+
it 'returns only session key items' do
|
|
324
|
+
playlist = described_class.read(
|
|
325
|
+
File.read('spec/fixtures/master.m3u8')
|
|
326
|
+
)
|
|
327
|
+
expect(playlist.session_keys)
|
|
328
|
+
.to all be_a(M3u8::SessionKeyItem)
|
|
329
|
+
expect(playlist.session_keys.size).to eq(1)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
313
333
|
describe '#write' do
|
|
314
334
|
context 'when playlist is valid' do
|
|
315
335
|
it 'returns playlist text' do
|
|
@@ -7,9 +7,10 @@ describe M3u8::Reader do
|
|
|
7
7
|
|
|
8
8
|
describe '#read' do
|
|
9
9
|
it 'parses master playlist' do
|
|
10
|
-
file = File.open('spec/fixtures/master.m3u8')
|
|
11
10
|
reader = M3u8::Reader.new
|
|
12
|
-
playlist = reader.read(
|
|
11
|
+
playlist = reader.read(
|
|
12
|
+
File.read('spec/fixtures/master.m3u8')
|
|
13
|
+
)
|
|
13
14
|
expect(playlist.master?).to be true
|
|
14
15
|
expect(playlist.discontinuity_sequence).to be_nil
|
|
15
16
|
expect(playlist.independent_segments).to be true
|
|
@@ -54,9 +55,10 @@ describe M3u8::Reader do
|
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
it 'parses master playlist with I-Frames' do
|
|
57
|
-
file = File.open('spec/fixtures/master_iframes.m3u8')
|
|
58
58
|
reader = M3u8::Reader.new
|
|
59
|
-
playlist = reader.read(
|
|
59
|
+
playlist = reader.read(
|
|
60
|
+
File.read('spec/fixtures/master_iframes.m3u8')
|
|
61
|
+
)
|
|
60
62
|
expect(playlist.master?).to be true
|
|
61
63
|
|
|
62
64
|
expect(playlist.items.size).to eq(7)
|
|
@@ -69,9 +71,10 @@ describe M3u8::Reader do
|
|
|
69
71
|
end
|
|
70
72
|
|
|
71
73
|
it 'parses media playlist' do
|
|
72
|
-
file = File.open('spec/fixtures/playlist.m3u8')
|
|
73
74
|
reader = M3u8::Reader.new
|
|
74
|
-
playlist = reader.read(
|
|
75
|
+
playlist = reader.read(
|
|
76
|
+
File.read('spec/fixtures/playlist.m3u8')
|
|
77
|
+
)
|
|
75
78
|
expect(playlist.master?).to be false
|
|
76
79
|
expect(playlist.version).to eq(4)
|
|
77
80
|
expect(playlist.sequence).to eq(1)
|
|
@@ -94,9 +97,10 @@ describe M3u8::Reader do
|
|
|
94
97
|
end
|
|
95
98
|
|
|
96
99
|
it 'parses I-Frame playlist' do
|
|
97
|
-
file = File.open('spec/fixtures/iframes.m3u8')
|
|
98
100
|
reader = M3u8::Reader.new
|
|
99
|
-
playlist = reader.read(
|
|
101
|
+
playlist = reader.read(
|
|
102
|
+
File.read('spec/fixtures/iframes.m3u8')
|
|
103
|
+
)
|
|
100
104
|
|
|
101
105
|
expect(playlist.iframes_only).to be true
|
|
102
106
|
expect(playlist.items.size).to eq(3)
|
|
@@ -114,9 +118,10 @@ describe M3u8::Reader do
|
|
|
114
118
|
end
|
|
115
119
|
|
|
116
120
|
it 'parses segment playlist with comments' do
|
|
117
|
-
file = File.open('spec/fixtures/playlist_with_comments.m3u8')
|
|
118
121
|
reader = M3u8::Reader.new
|
|
119
|
-
playlist = reader.read(
|
|
122
|
+
playlist = reader.read(
|
|
123
|
+
File.read('spec/fixtures/playlist_with_comments.m3u8')
|
|
124
|
+
)
|
|
120
125
|
expect(playlist.master?).to be false
|
|
121
126
|
expect(playlist.version).to eq(4)
|
|
122
127
|
expect(playlist.sequence).to eq(1)
|
|
@@ -136,9 +141,10 @@ describe M3u8::Reader do
|
|
|
136
141
|
end
|
|
137
142
|
|
|
138
143
|
it 'parses variant playlist with audio options and groups' do
|
|
139
|
-
file = File.open('spec/fixtures/variant_audio.m3u8')
|
|
140
144
|
reader = M3u8::Reader.new
|
|
141
|
-
playlist = reader.read(
|
|
145
|
+
playlist = reader.read(
|
|
146
|
+
File.read('spec/fixtures/variant_audio.m3u8')
|
|
147
|
+
)
|
|
142
148
|
|
|
143
149
|
expect(playlist.master?).to be true
|
|
144
150
|
expect(playlist.items.size).to eq(10)
|
|
@@ -157,9 +163,10 @@ describe M3u8::Reader do
|
|
|
157
163
|
end
|
|
158
164
|
|
|
159
165
|
it 'parses variant playlist with camera angles' do
|
|
160
|
-
file = File.open('spec/fixtures/variant_angles.m3u8')
|
|
161
166
|
reader = M3u8::Reader.new
|
|
162
|
-
playlist = reader.read(
|
|
167
|
+
playlist = reader.read(
|
|
168
|
+
File.read('spec/fixtures/variant_angles.m3u8')
|
|
169
|
+
)
|
|
163
170
|
|
|
164
171
|
expect(playlist.master?).to be true
|
|
165
172
|
expect(playlist.items.size).to eq(11)
|
|
@@ -183,22 +190,22 @@ describe M3u8::Reader do
|
|
|
183
190
|
end
|
|
184
191
|
|
|
185
192
|
it 'processes multiple reads as separate playlists' do
|
|
186
|
-
file = File.open('spec/fixtures/master.m3u8')
|
|
187
193
|
reader = M3u8::Reader.new
|
|
188
|
-
|
|
194
|
+
content = File.read('spec/fixtures/master.m3u8')
|
|
195
|
+
playlist = reader.read(content)
|
|
189
196
|
|
|
190
197
|
expect(playlist.items.size).to eq(8)
|
|
191
198
|
|
|
192
|
-
|
|
193
|
-
playlist = reader.read(file)
|
|
199
|
+
playlist = reader.read(content)
|
|
194
200
|
|
|
195
201
|
expect(playlist.items.size).to eq(8)
|
|
196
202
|
end
|
|
197
203
|
|
|
198
204
|
it 'parses playlist with session data' do
|
|
199
|
-
file = File.open('spec/fixtures/session_data.m3u8')
|
|
200
205
|
reader = M3u8::Reader.new
|
|
201
|
-
playlist = reader.read(
|
|
206
|
+
playlist = reader.read(
|
|
207
|
+
File.read('spec/fixtures/session_data.m3u8')
|
|
208
|
+
)
|
|
202
209
|
|
|
203
210
|
expect(playlist.items.size).to eq(3)
|
|
204
211
|
|
|
@@ -209,9 +216,10 @@ describe M3u8::Reader do
|
|
|
209
216
|
end
|
|
210
217
|
|
|
211
218
|
it 'parses encrypted playlist' do
|
|
212
|
-
file = File.open('spec/fixtures/encrypted.m3u8')
|
|
213
219
|
reader = M3u8::Reader.new
|
|
214
|
-
playlist = reader.read(
|
|
220
|
+
playlist = reader.read(
|
|
221
|
+
File.read('spec/fixtures/encrypted.m3u8')
|
|
222
|
+
)
|
|
215
223
|
|
|
216
224
|
expect(playlist.items.size).to eq(6)
|
|
217
225
|
|
|
@@ -222,9 +230,10 @@ describe M3u8::Reader do
|
|
|
222
230
|
end
|
|
223
231
|
|
|
224
232
|
it 'parses map (media intialization section) playlists' do
|
|
225
|
-
file = File.open('spec/fixtures/map_playlist.m3u8')
|
|
226
233
|
reader = M3u8::Reader.new
|
|
227
|
-
playlist = reader.read(
|
|
234
|
+
playlist = reader.read(
|
|
235
|
+
File.read('spec/fixtures/map_playlist.m3u8')
|
|
236
|
+
)
|
|
228
237
|
|
|
229
238
|
expect(playlist.items.size).to eq(1)
|
|
230
239
|
|
|
@@ -236,9 +245,10 @@ describe M3u8::Reader do
|
|
|
236
245
|
end
|
|
237
246
|
|
|
238
247
|
it 'reads segment with timestamp' do
|
|
239
|
-
file = File.open('spec/fixtures/timestamp_playlist.m3u8')
|
|
240
248
|
reader = M3u8::Reader.new
|
|
241
|
-
playlist = reader.read(
|
|
249
|
+
playlist = reader.read(
|
|
250
|
+
File.read('spec/fixtures/timestamp_playlist.m3u8')
|
|
251
|
+
)
|
|
242
252
|
expect(playlist.items.count).to eq(6)
|
|
243
253
|
|
|
244
254
|
item_date_time = playlist.items.first.program_date_time
|
|
@@ -247,9 +257,10 @@ describe M3u8::Reader do
|
|
|
247
257
|
end
|
|
248
258
|
|
|
249
259
|
it 'parses playlist with daterange' do
|
|
250
|
-
file = File.open('spec/fixtures/date_range_scte35.m3u8')
|
|
251
260
|
reader = M3u8::Reader.new
|
|
252
|
-
playlist = reader.read(
|
|
261
|
+
playlist = reader.read(
|
|
262
|
+
File.read('spec/fixtures/date_range_scte35.m3u8')
|
|
263
|
+
)
|
|
253
264
|
expect(playlist.items.count).to eq(5)
|
|
254
265
|
|
|
255
266
|
item = playlist.items[0]
|
|
@@ -260,9 +271,10 @@ describe M3u8::Reader do
|
|
|
260
271
|
end
|
|
261
272
|
|
|
262
273
|
it 'parses master playlist with v13 attributes' do
|
|
263
|
-
file = File.open('spec/fixtures/master_v13.m3u8')
|
|
264
274
|
reader = M3u8::Reader.new
|
|
265
|
-
playlist = reader.read(
|
|
275
|
+
playlist = reader.read(
|
|
276
|
+
File.read('spec/fixtures/master_v13.m3u8')
|
|
277
|
+
)
|
|
266
278
|
expect(playlist.master?).to be true
|
|
267
279
|
expect(playlist.version).to eq(13)
|
|
268
280
|
|
|
@@ -288,9 +300,10 @@ describe M3u8::Reader do
|
|
|
288
300
|
end
|
|
289
301
|
|
|
290
302
|
it 'parses playlist with content steering and defines' do
|
|
291
|
-
file = File.open('spec/fixtures/content_steering.m3u8')
|
|
292
303
|
reader = M3u8::Reader.new
|
|
293
|
-
playlist = reader.read(
|
|
304
|
+
playlist = reader.read(
|
|
305
|
+
File.read('spec/fixtures/content_steering.m3u8')
|
|
306
|
+
)
|
|
294
307
|
expect(playlist.master?).to be true
|
|
295
308
|
expect(playlist.items.size).to eq(5)
|
|
296
309
|
|
|
@@ -310,9 +323,10 @@ describe M3u8::Reader do
|
|
|
310
323
|
end
|
|
311
324
|
|
|
312
325
|
it 'parses LL-HLS playlist' do
|
|
313
|
-
file = File.open('spec/fixtures/ll_hls_playlist.m3u8')
|
|
314
326
|
reader = M3u8::Reader.new
|
|
315
|
-
playlist = reader.read(
|
|
327
|
+
playlist = reader.read(
|
|
328
|
+
File.read('spec/fixtures/ll_hls_playlist.m3u8')
|
|
329
|
+
)
|
|
316
330
|
expect(playlist.master?).to be false
|
|
317
331
|
expect(playlist.live?).to be true
|
|
318
332
|
expect(playlist.version).to eq(9)
|
|
@@ -361,9 +375,10 @@ describe M3u8::Reader do
|
|
|
361
375
|
end
|
|
362
376
|
|
|
363
377
|
it 'parses playlist with gap and bitrate tags' do
|
|
364
|
-
file = File.open('spec/fixtures/gap_playlist.m3u8')
|
|
365
378
|
reader = M3u8::Reader.new
|
|
366
|
-
playlist = reader.read(
|
|
379
|
+
playlist = reader.read(
|
|
380
|
+
File.read('spec/fixtures/gap_playlist.m3u8')
|
|
381
|
+
)
|
|
367
382
|
expect(playlist.master?).to be false
|
|
368
383
|
expect(playlist.items.size).to eq(6)
|
|
369
384
|
|
|
@@ -383,8 +398,9 @@ describe M3u8::Reader do
|
|
|
383
398
|
end
|
|
384
399
|
|
|
385
400
|
it 'parses event playlist with byterange and map change' do
|
|
386
|
-
|
|
387
|
-
|
|
401
|
+
playlist = reader.read(
|
|
402
|
+
File.read('spec/fixtures/event_playlist.m3u8')
|
|
403
|
+
)
|
|
388
404
|
expect(playlist.master?).to be false
|
|
389
405
|
expect(playlist.live?).to be false
|
|
390
406
|
expect(playlist.type).to eq('EVENT')
|
|
@@ -417,8 +433,9 @@ describe M3u8::Reader do
|
|
|
417
433
|
end
|
|
418
434
|
|
|
419
435
|
it 'parses daterange playlist' do
|
|
420
|
-
|
|
421
|
-
|
|
436
|
+
playlist = reader.read(
|
|
437
|
+
File.read('spec/fixtures/daterange_playlist.m3u8')
|
|
438
|
+
)
|
|
422
439
|
expect(playlist.master?).to be false
|
|
423
440
|
expect(playlist.items.size).to eq(6)
|
|
424
441
|
|
|
@@ -445,8 +462,9 @@ describe M3u8::Reader do
|
|
|
445
462
|
end
|
|
446
463
|
|
|
447
464
|
it 'parses full master playlist' do
|
|
448
|
-
|
|
449
|
-
|
|
465
|
+
playlist = reader.read(
|
|
466
|
+
File.read('spec/fixtures/master_full.m3u8')
|
|
467
|
+
)
|
|
450
468
|
expect(playlist.master?).to be true
|
|
451
469
|
expect(playlist.version).to eq(13)
|
|
452
470
|
expect(playlist.independent_segments).to be true
|
|
@@ -489,8 +507,9 @@ describe M3u8::Reader do
|
|
|
489
507
|
end
|
|
490
508
|
|
|
491
509
|
it 'parses encrypted playlist with discontinuities' do
|
|
492
|
-
|
|
493
|
-
|
|
510
|
+
playlist = reader.read(
|
|
511
|
+
File.read('spec/fixtures/encrypted_discontinuity.m3u8')
|
|
512
|
+
)
|
|
494
513
|
expect(playlist.master?).to be false
|
|
495
514
|
expect(playlist.live?).to be false
|
|
496
515
|
expect(playlist.items.size).to eq(8)
|
|
@@ -515,8 +534,9 @@ describe M3u8::Reader do
|
|
|
515
534
|
end
|
|
516
535
|
|
|
517
536
|
it 'parses advanced LL-HLS playlist' do
|
|
518
|
-
|
|
519
|
-
|
|
537
|
+
playlist = reader.read(
|
|
538
|
+
File.read('spec/fixtures/ll_hls_advanced.m3u8')
|
|
539
|
+
)
|
|
520
540
|
expect(playlist.master?).to be false
|
|
521
541
|
expect(playlist.live?).to be true
|
|
522
542
|
expect(playlist.version).to eq(9)
|
|
@@ -76,9 +76,7 @@ describe 'Round-trip serialization' do
|
|
|
76
76
|
second = parse(first.to_s)
|
|
77
77
|
|
|
78
78
|
expect(second.items.size).to eq(first.items.size)
|
|
79
|
-
defines = second.items.
|
|
80
|
-
i.is_a?(M3u8::DefineItem)
|
|
81
|
-
end
|
|
79
|
+
defines = second.items.grep(M3u8::DefineItem)
|
|
82
80
|
expect(defines.size).to eq(2)
|
|
83
81
|
|
|
84
82
|
steering = second.items.find do |i|
|
|
@@ -106,9 +104,7 @@ describe 'Round-trip serialization' do
|
|
|
106
104
|
second = parse(first.to_s)
|
|
107
105
|
|
|
108
106
|
expect(second.items.size).to eq(first.items.size)
|
|
109
|
-
media = second.items.
|
|
110
|
-
i.is_a?(M3u8::MediaItem)
|
|
111
|
-
end
|
|
107
|
+
media = second.items.grep(M3u8::MediaItem)
|
|
112
108
|
expect(media.size).to eq(6)
|
|
113
109
|
expect(media.first.group_id).to eq('audio-lo')
|
|
114
110
|
end
|
|
@@ -119,9 +115,7 @@ describe 'Round-trip serialization' do
|
|
|
119
115
|
second = parse(first.to_s)
|
|
120
116
|
|
|
121
117
|
expect(second.items.size).to eq(first.items.size)
|
|
122
|
-
media = second.items.
|
|
123
|
-
i.is_a?(M3u8::MediaItem)
|
|
124
|
-
end
|
|
118
|
+
media = second.items.grep(M3u8::MediaItem)
|
|
125
119
|
expect(media.size).to eq(9)
|
|
126
120
|
types = media.map(&:type).uniq.sort
|
|
127
121
|
expect(types).to eq(%w[AUDIO CLOSED-CAPTIONS
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: m3u8
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seth Deckard
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-02-28 00:00:00.000000000 Z
|
|
@@ -125,7 +125,8 @@ dependencies:
|
|
|
125
125
|
description: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
|
|
126
126
|
email:
|
|
127
127
|
- seth@deckard.me
|
|
128
|
-
executables:
|
|
128
|
+
executables:
|
|
129
|
+
- m3u8
|
|
129
130
|
extensions: []
|
|
130
131
|
extra_rdoc_files: []
|
|
131
132
|
files:
|
|
@@ -142,10 +143,14 @@ files:
|
|
|
142
143
|
- LICENSE.txt
|
|
143
144
|
- README.md
|
|
144
145
|
- Rakefile
|
|
146
|
+
- bin/m3u8
|
|
145
147
|
- lib/m3u8.rb
|
|
146
148
|
- lib/m3u8/bitrate_item.rb
|
|
147
149
|
- lib/m3u8/builder.rb
|
|
148
150
|
- lib/m3u8/byte_range.rb
|
|
151
|
+
- lib/m3u8/cli.rb
|
|
152
|
+
- lib/m3u8/cli/inspect_command.rb
|
|
153
|
+
- lib/m3u8/cli/validate_command.rb
|
|
149
154
|
- lib/m3u8/content_steering_item.rb
|
|
150
155
|
- lib/m3u8/date_range_item.rb
|
|
151
156
|
- lib/m3u8/define_item.rb
|
|
@@ -197,6 +202,9 @@ files:
|
|
|
197
202
|
- spec/lib/m3u8/bitrate_item_spec.rb
|
|
198
203
|
- spec/lib/m3u8/builder_spec.rb
|
|
199
204
|
- spec/lib/m3u8/byte_range_spec.rb
|
|
205
|
+
- spec/lib/m3u8/cli/inspect_command_spec.rb
|
|
206
|
+
- spec/lib/m3u8/cli/validate_command_spec.rb
|
|
207
|
+
- spec/lib/m3u8/cli_spec.rb
|
|
200
208
|
- spec/lib/m3u8/content_steering_item_spec.rb
|
|
201
209
|
- spec/lib/m3u8/date_range_item_spec.rb
|
|
202
210
|
- spec/lib/m3u8/define_item_spec.rb
|
|
@@ -227,7 +235,7 @@ homepage: https://github.com/sethdeckard/m3u8
|
|
|
227
235
|
licenses:
|
|
228
236
|
- MIT
|
|
229
237
|
metadata: {}
|
|
230
|
-
post_install_message:
|
|
238
|
+
post_install_message:
|
|
231
239
|
rdoc_options: []
|
|
232
240
|
require_paths:
|
|
233
241
|
- lib
|
|
@@ -242,8 +250,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
242
250
|
- !ruby/object:Gem::Version
|
|
243
251
|
version: '0'
|
|
244
252
|
requirements: []
|
|
245
|
-
rubygems_version: 3.
|
|
246
|
-
signing_key:
|
|
253
|
+
rubygems_version: 3.5.22
|
|
254
|
+
signing_key:
|
|
247
255
|
specification_version: 4
|
|
248
256
|
summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
|
|
249
257
|
test_files: []
|