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,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# BitrateItem represents an EXT-X-BITRATE tag that indicates the
|
|
5
|
+
# approximate bitrate of the following media segments in kbps.
|
|
6
|
+
class BitrateItem
|
|
7
|
+
# @return [Integer, nil] approximate bitrate in kbps
|
|
8
|
+
attr_accessor :bitrate
|
|
9
|
+
|
|
10
|
+
# @param params [Hash] attribute key-value pairs
|
|
11
|
+
def initialize(params = {})
|
|
12
|
+
params.each do |key, value|
|
|
13
|
+
instance_variable_set("@#{key}", value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Parse an EXT-X-BITRATE tag.
|
|
18
|
+
# @param text [String] raw tag line
|
|
19
|
+
# @return [BitrateItem]
|
|
20
|
+
def self.parse(text)
|
|
21
|
+
value = text.gsub('#EXT-X-BITRATE:', '').strip
|
|
22
|
+
BitrateItem.new(bitrate: value.to_i)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Render as an m3u8 EXT-X-BITRATE tag.
|
|
26
|
+
# @return [String]
|
|
27
|
+
def to_s
|
|
28
|
+
"#EXT-X-BITRATE:#{bitrate}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/m3u8/builder.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Builder provides a block-based DSL for constructing playlists
|
|
5
|
+
class Builder
|
|
6
|
+
ITEMS = {
|
|
7
|
+
segment: 'SegmentItem',
|
|
8
|
+
playlist: 'PlaylistItem',
|
|
9
|
+
media: 'MediaItem',
|
|
10
|
+
session_data: 'SessionDataItem',
|
|
11
|
+
session_key: 'SessionKeyItem',
|
|
12
|
+
content_steering: 'ContentSteeringItem',
|
|
13
|
+
key: 'KeyItem',
|
|
14
|
+
map: 'MapItem',
|
|
15
|
+
date_range: 'DateRangeItem',
|
|
16
|
+
time: 'TimeItem',
|
|
17
|
+
bitrate: 'BitrateItem',
|
|
18
|
+
part: 'PartItem',
|
|
19
|
+
preload_hint: 'PreloadHintItem',
|
|
20
|
+
rendition_report: 'RenditionReportItem',
|
|
21
|
+
skip: 'SkipItem',
|
|
22
|
+
define: 'DefineItem',
|
|
23
|
+
playback_start: 'PlaybackStart'
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
ZERO_ARG_ITEMS = {
|
|
27
|
+
discontinuity: 'DiscontinuityItem',
|
|
28
|
+
gap: 'GapItem'
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
# @param playlist [Playlist] playlist to build into
|
|
32
|
+
def initialize(playlist)
|
|
33
|
+
@playlist = playlist
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
ITEMS.each do |method_name, class_name|
|
|
37
|
+
define_method(method_name) do |params = {}|
|
|
38
|
+
@playlist.items << M3u8.const_get(class_name).new(params)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
ZERO_ARG_ITEMS.each do |method_name, class_name|
|
|
43
|
+
define_method(method_name) do
|
|
44
|
+
@playlist.items << M3u8.const_get(class_name).new
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/m3u8/byte_range.rb
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# ByteRange represents sub range of a resource
|
|
4
5
|
class ByteRange
|
|
6
|
+
# @return [Integer, nil] number of bytes
|
|
7
|
+
# @return [Integer, nil] start offset in bytes
|
|
5
8
|
attr_accessor :length, :start
|
|
6
9
|
|
|
10
|
+
# @param params [Hash] :length and optional :start
|
|
7
11
|
def initialize(params = {})
|
|
8
12
|
params.each do |key, value|
|
|
9
13
|
instance_variable_set("@#{key}", value)
|
|
10
14
|
end
|
|
11
15
|
end
|
|
12
16
|
|
|
17
|
+
# Parse a byte range string (e.g. "4500@600").
|
|
18
|
+
# @param text [String] byte range string
|
|
19
|
+
# @return [ByteRange]
|
|
13
20
|
def self.parse(text)
|
|
14
21
|
values = text.split('@')
|
|
15
22
|
length_value = values[0].to_i
|
|
@@ -18,6 +25,8 @@ module M3u8
|
|
|
18
25
|
ByteRange.new(options)
|
|
19
26
|
end
|
|
20
27
|
|
|
28
|
+
# Render as a byte range string (e.g. "4500@600").
|
|
29
|
+
# @return [String]
|
|
21
30
|
def to_s
|
|
22
31
|
"#{length}#{start_format}"
|
|
23
32
|
end
|
|
@@ -26,6 +35,7 @@ module M3u8
|
|
|
26
35
|
|
|
27
36
|
def start_format
|
|
28
37
|
return if start.nil?
|
|
38
|
+
|
|
29
39
|
"@#{start}"
|
|
30
40
|
end
|
|
31
41
|
end
|
|
@@ -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,24 @@
|
|
|
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'
|
|
18
|
+
@playlist.errors.each { |e| @stdout.puts " - #{e}" }
|
|
19
|
+
1
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
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/codecs.rb
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Codec lookup tables for HLS playlist items
|
|
5
|
+
module Codecs
|
|
6
|
+
AUDIO_CODECS = {
|
|
7
|
+
'aac-lc' => 'mp4a.40.2',
|
|
8
|
+
'he-aac' => 'mp4a.40.5',
|
|
9
|
+
'mp3' => 'mp4a.40.34',
|
|
10
|
+
'ac-3' => 'ac-3',
|
|
11
|
+
'ec-3' => 'ec-3',
|
|
12
|
+
'e-ac-3' => 'ec-3',
|
|
13
|
+
'flac' => 'fLaC',
|
|
14
|
+
'opus' => 'Opus'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
BASELINE_CODECS = {
|
|
18
|
+
3.0 => 'avc1.66.30',
|
|
19
|
+
3.1 => 'avc1.42001f'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
MAIN_CODECS = {
|
|
23
|
+
3.0 => 'avc1.77.30',
|
|
24
|
+
3.1 => 'avc1.4d001f',
|
|
25
|
+
4.0 => 'avc1.4d0028',
|
|
26
|
+
4.1 => 'avc1.4d0029'
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
HIGH_LEVELS = [3.0, 3.1, 3.2, 4.0, 4.1, 4.2,
|
|
30
|
+
5.0, 5.1, 5.2].freeze
|
|
31
|
+
|
|
32
|
+
HEVC_CODECS = {
|
|
33
|
+
['hevc-main', 3.1] => 'hvc1.1.6.L93.B0',
|
|
34
|
+
['hevc-main', 4.0] => 'hvc1.1.6.L120.B0',
|
|
35
|
+
['hevc-main', 5.0] => 'hvc1.1.6.L150.B0',
|
|
36
|
+
['hevc-main', 5.1] => 'hvc1.1.6.L153.B0',
|
|
37
|
+
['hevc-main-10', 3.1] => 'hvc1.2.4.L93.B0',
|
|
38
|
+
['hevc-main-10', 4.0] => 'hvc1.2.4.L120.B0',
|
|
39
|
+
['hevc-main-10', 5.0] => 'hvc1.2.4.L150.B0',
|
|
40
|
+
['hevc-main-10', 5.1] => 'hvc1.2.4.L153.B0'
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
AV1_CODECS = {
|
|
44
|
+
['av1-main', 3.1] => 'av01.0.04M.08',
|
|
45
|
+
['av1-main', 4.0] => 'av01.0.08M.08',
|
|
46
|
+
['av1-main', 5.0] => 'av01.0.12M.08',
|
|
47
|
+
['av1-main', 5.1] => 'av01.0.13M.08',
|
|
48
|
+
['av1-high', 3.1] => 'av01.1.04H.10',
|
|
49
|
+
['av1-high', 4.0] => 'av01.1.08H.10',
|
|
50
|
+
['av1-high', 5.0] => 'av01.1.12H.10',
|
|
51
|
+
['av1-high', 5.1] => 'av01.1.13H.10'
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Look up the codec string for an audio codec name.
|
|
55
|
+
# @param codec [String, nil] audio codec name
|
|
56
|
+
# @return [String, nil] codec string
|
|
57
|
+
def self.audio_codec(codec)
|
|
58
|
+
return if codec.nil?
|
|
59
|
+
|
|
60
|
+
AUDIO_CODECS[codec.downcase]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Look up the codec string for a video profile and level.
|
|
64
|
+
# @param profile [String, nil] video profile name
|
|
65
|
+
# @param level [Float, Integer, nil] video level
|
|
66
|
+
# @return [String, nil] codec string
|
|
67
|
+
def self.video_codec(profile, level)
|
|
68
|
+
return if profile.nil? || level.nil?
|
|
69
|
+
|
|
70
|
+
level = level.to_f
|
|
71
|
+
name = profile.downcase
|
|
72
|
+
return BASELINE_CODECS[level] if name == 'baseline'
|
|
73
|
+
return MAIN_CODECS[level] if name == 'main'
|
|
74
|
+
return high_codec_string(level) if name == 'high'
|
|
75
|
+
return HEVC_CODECS[[profile, level]] if name.start_with?('hevc-')
|
|
76
|
+
|
|
77
|
+
AV1_CODECS[[profile, level]] if name.start_with?('av1-')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.high_codec_string(level)
|
|
81
|
+
return nil unless HIGH_LEVELS.include?(level)
|
|
82
|
+
|
|
83
|
+
hex = level.to_s.sub('.', '').to_i.to_s(16)
|
|
84
|
+
"avc1.6400#{hex}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private_class_method :high_codec_string
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# ContentSteeringItem represents an EXT-X-CONTENT-STEERING tag which
|
|
5
|
+
# indicates a Content Steering Manifest for dynamic pathway selection.
|
|
6
|
+
class ContentSteeringItem
|
|
7
|
+
extend M3u8
|
|
8
|
+
include AttributeFormatter
|
|
9
|
+
|
|
10
|
+
# @return [String, nil] steering manifest server URI
|
|
11
|
+
# @return [String, nil] default pathway ID
|
|
12
|
+
attr_accessor :server_uri, :pathway_id
|
|
13
|
+
|
|
14
|
+
# @param params [Hash] attribute key-value pairs
|
|
15
|
+
def initialize(params = {})
|
|
16
|
+
params.each do |key, value|
|
|
17
|
+
instance_variable_set("@#{key}", value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Parse an EXT-X-CONTENT-STEERING tag.
|
|
22
|
+
# @param text [String] raw tag line
|
|
23
|
+
# @return [ContentSteeringItem]
|
|
24
|
+
def self.parse(text)
|
|
25
|
+
attributes = parse_attributes(text)
|
|
26
|
+
ContentSteeringItem.new(
|
|
27
|
+
server_uri: attributes['SERVER-URI'],
|
|
28
|
+
pathway_id: attributes['PATHWAY-ID']
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Render as an m3u8 EXT-X-CONTENT-STEERING tag.
|
|
33
|
+
# @return [String]
|
|
34
|
+
def to_s
|
|
35
|
+
"#EXT-X-CONTENT-STEERING:#{formatted_attributes}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def formatted_attributes
|
|
41
|
+
[quoted_format('SERVER-URI', server_uri),
|
|
42
|
+
quoted_format('PATHWAY-ID', pathway_id)].compact.join(',')
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|