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
data/lib/m3u8/date_range_item.rb
CHANGED
|
@@ -1,113 +1,184 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# DateRangeItem represents a #EXT-X-DATERANGE tag
|
|
4
5
|
class DateRangeItem
|
|
5
|
-
|
|
6
|
+
extend M3u8
|
|
7
|
+
include AttributeFormatter
|
|
8
|
+
|
|
9
|
+
# @return [String, nil] unique date range identifier
|
|
10
|
+
# @return [String, nil] CLASS attribute
|
|
11
|
+
# @return [String, nil] start date (ISO 8601)
|
|
12
|
+
# @return [String, nil] end date (ISO 8601)
|
|
13
|
+
# @return [Float, nil] duration in seconds
|
|
14
|
+
# @return [Float, nil] planned duration in seconds
|
|
15
|
+
# @return [String, nil] SCTE-35 command hex string
|
|
16
|
+
# @return [String, nil] SCTE-35 out hex string
|
|
17
|
+
# @return [String, nil] SCTE-35 in hex string
|
|
18
|
+
# @return [String, nil] CUE attribute
|
|
19
|
+
# @return [Boolean, nil] END-ON-NEXT flag
|
|
20
|
+
# @return [Hash, nil] client-defined X- attributes
|
|
21
|
+
# @return [String, nil] interstitial asset URI
|
|
22
|
+
# @return [String, nil] interstitial asset list URI
|
|
23
|
+
# @return [Float, nil] interstitial resume offset
|
|
24
|
+
# @return [Float, nil] interstitial playout limit
|
|
25
|
+
# @return [String, nil] interstitial restrict value
|
|
26
|
+
# @return [String, nil] interstitial snap value
|
|
27
|
+
# @return [String, nil] interstitial timeline occupies
|
|
28
|
+
# @return [String, nil] interstitial timeline style
|
|
29
|
+
# @return [String, nil] content may vary flag
|
|
6
30
|
attr_accessor :id, :class_name, :start_date, :end_date, :duration,
|
|
7
31
|
:planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
|
|
8
|
-
:end_on_next, :client_attributes
|
|
9
|
-
|
|
32
|
+
:cue, :end_on_next, :client_attributes,
|
|
33
|
+
:asset_uri, :asset_list, :resume_offset,
|
|
34
|
+
:playout_limit, :restrict, :snap,
|
|
35
|
+
:timeline_occupies, :timeline_style,
|
|
36
|
+
:content_may_vary
|
|
37
|
+
|
|
38
|
+
INTERSTITIAL_KEYS = %w[
|
|
39
|
+
X-ASSET-URI X-ASSET-LIST X-RESUME-OFFSET X-PLAYOUT-LIMIT
|
|
40
|
+
X-RESTRICT X-SNAP X-TIMELINE-OCCUPIES X-TIMELINE-STYLE
|
|
41
|
+
X-CONTENT-MAY-VARY
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
# @param options [Hash] attribute key-value pairs
|
|
10
45
|
def initialize(options = {})
|
|
11
46
|
options.each do |key, value|
|
|
12
47
|
instance_variable_set("@#{key}", value)
|
|
13
48
|
end
|
|
14
49
|
end
|
|
15
50
|
|
|
16
|
-
|
|
51
|
+
# Parse an EXT-X-DATERANGE tag.
|
|
52
|
+
# @param text [String] raw tag line
|
|
53
|
+
# @return [DateRangeItem]
|
|
54
|
+
def self.parse(text)
|
|
17
55
|
attributes = parse_attributes(text)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@planned_duration = parse_float(attributes['PLANNED-DURATION'])
|
|
24
|
-
@scte35_cmd = attributes['SCTE35-CMD']
|
|
25
|
-
@scte35_out = attributes['SCTE35-OUT']
|
|
26
|
-
@scte35_in = attributes['SCTE35-IN']
|
|
27
|
-
@end_on_next = attributes.key?('END-ON-NEXT') ? true : false
|
|
28
|
-
@client_attributes = parse_client_attributes(attributes)
|
|
56
|
+
options = parse_base_attributes(attributes)
|
|
57
|
+
.merge(parse_interstitials(attributes))
|
|
58
|
+
.merge(client_attributes:
|
|
59
|
+
parse_client_attributes(attributes))
|
|
60
|
+
DateRangeItem.new(options)
|
|
29
61
|
end
|
|
30
62
|
|
|
63
|
+
def self.parse_base_attributes(attributes)
|
|
64
|
+
{ id: attributes['ID'],
|
|
65
|
+
class_name: attributes['CLASS'],
|
|
66
|
+
start_date: attributes['START-DATE'],
|
|
67
|
+
end_date: attributes['END-DATE'],
|
|
68
|
+
duration: parse_float(attributes['DURATION']),
|
|
69
|
+
planned_duration:
|
|
70
|
+
parse_float(attributes['PLANNED-DURATION']),
|
|
71
|
+
scte35_cmd: attributes['SCTE35-CMD'],
|
|
72
|
+
scte35_out: attributes['SCTE35-OUT'],
|
|
73
|
+
scte35_in: attributes['SCTE35-IN'],
|
|
74
|
+
cue: attributes['CUE'],
|
|
75
|
+
end_on_next: attributes.key?('END-ON-NEXT') }
|
|
76
|
+
end
|
|
77
|
+
private_class_method :parse_base_attributes
|
|
78
|
+
|
|
79
|
+
def self.parse_interstitials(attributes)
|
|
80
|
+
{ asset_uri: attributes['X-ASSET-URI'],
|
|
81
|
+
asset_list: attributes['X-ASSET-LIST'],
|
|
82
|
+
resume_offset:
|
|
83
|
+
parse_float(attributes['X-RESUME-OFFSET']),
|
|
84
|
+
playout_limit:
|
|
85
|
+
parse_float(attributes['X-PLAYOUT-LIMIT']),
|
|
86
|
+
restrict: attributes['X-RESTRICT'],
|
|
87
|
+
snap: attributes['X-SNAP'],
|
|
88
|
+
timeline_occupies:
|
|
89
|
+
attributes['X-TIMELINE-OCCUPIES'],
|
|
90
|
+
timeline_style: attributes['X-TIMELINE-STYLE'],
|
|
91
|
+
content_may_vary:
|
|
92
|
+
attributes['X-CONTENT-MAY-VARY'] }
|
|
93
|
+
end
|
|
94
|
+
private_class_method :parse_interstitials
|
|
95
|
+
|
|
96
|
+
# Render as an m3u8 EXT-X-DATERANGE tag.
|
|
97
|
+
# @return [String]
|
|
31
98
|
def to_s
|
|
32
99
|
"#EXT-X-DATERANGE:#{formatted_attributes}"
|
|
33
100
|
end
|
|
34
101
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
class_name_format,
|
|
40
|
-
%(START-DATE="#{start_date}"),
|
|
41
|
-
end_date_format,
|
|
42
|
-
duration_format,
|
|
43
|
-
planned_duration_format,
|
|
44
|
-
client_attributes_format,
|
|
45
|
-
scte35_cmd_format,
|
|
46
|
-
scte35_out_format,
|
|
47
|
-
scte35_in_format,
|
|
48
|
-
end_on_next_format].compact.join(',')
|
|
102
|
+
# Parse SCTE-35 command data.
|
|
103
|
+
# @return [Scte35, nil]
|
|
104
|
+
def scte35_cmd_info
|
|
105
|
+
Scte35.parse(scte35_cmd) unless scte35_cmd.nil?
|
|
49
106
|
end
|
|
50
107
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
108
|
+
# Parse SCTE-35 out data.
|
|
109
|
+
# @return [Scte35, nil]
|
|
110
|
+
def scte35_out_info
|
|
111
|
+
Scte35.parse(scte35_out) unless scte35_out.nil?
|
|
54
112
|
end
|
|
55
113
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
114
|
+
# Parse SCTE-35 in data.
|
|
115
|
+
# @return [Scte35, nil]
|
|
116
|
+
def scte35_in_info
|
|
117
|
+
Scte35.parse(scte35_in) unless scte35_in.nil?
|
|
59
118
|
end
|
|
60
119
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
|
|
120
|
+
def self.parse_client_attributes(attributes)
|
|
121
|
+
attributes.select do |key|
|
|
122
|
+
key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
|
|
123
|
+
end
|
|
64
124
|
end
|
|
125
|
+
private_class_method :parse_client_attributes
|
|
126
|
+
|
|
127
|
+
private
|
|
65
128
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
129
|
+
def formatted_attributes
|
|
130
|
+
[%(ID="#{id}"),
|
|
131
|
+
quoted_format('CLASS', class_name),
|
|
132
|
+
%(START-DATE="#{start_date}"),
|
|
133
|
+
quoted_format('END-DATE', end_date),
|
|
134
|
+
unquoted_format('DURATION', decimal_format(duration)),
|
|
135
|
+
unquoted_format('PLANNED-DURATION', decimal_format(planned_duration)),
|
|
136
|
+
client_attributes_format,
|
|
137
|
+
interstitial_formats,
|
|
138
|
+
unquoted_format('SCTE35-CMD', scte35_cmd),
|
|
139
|
+
unquoted_format('SCTE35-OUT', scte35_out),
|
|
140
|
+
unquoted_format('SCTE35-IN', scte35_in),
|
|
141
|
+
quoted_format('CUE', cue),
|
|
142
|
+
end_on_next_format].flatten.compact.join(',')
|
|
69
143
|
end
|
|
70
144
|
|
|
71
145
|
def client_attributes_format
|
|
72
|
-
return if client_attributes.nil?
|
|
146
|
+
return if client_attributes.nil? || client_attributes.empty?
|
|
147
|
+
|
|
73
148
|
client_attributes.map do |attribute|
|
|
74
149
|
value = attribute.last
|
|
75
|
-
|
|
76
|
-
"#{attribute.first}=#{
|
|
150
|
+
fmt = decimal?(value) ? decimal_format(value) : %("#{value}")
|
|
151
|
+
"#{attribute.first}=#{fmt}"
|
|
77
152
|
end
|
|
78
153
|
end
|
|
79
154
|
|
|
80
155
|
def decimal?(value)
|
|
81
|
-
|
|
156
|
+
val = value.to_s
|
|
157
|
+
return true if val =~ /\A\d+\Z/
|
|
158
|
+
|
|
82
159
|
begin
|
|
83
|
-
|
|
84
|
-
rescue
|
|
160
|
+
true if Float(val)
|
|
161
|
+
rescue StandardError
|
|
85
162
|
false
|
|
86
163
|
end
|
|
87
164
|
end
|
|
88
165
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def scte35_in_format
|
|
100
|
-
return if scte35_in.nil?
|
|
101
|
-
"SCTE35-IN=#{scte35_in}"
|
|
166
|
+
def interstitial_formats
|
|
167
|
+
[quoted_format('X-ASSET-URI', asset_uri),
|
|
168
|
+
quoted_format('X-ASSET-LIST', asset_list),
|
|
169
|
+
unquoted_format('X-RESUME-OFFSET', decimal_format(resume_offset)),
|
|
170
|
+
unquoted_format('X-PLAYOUT-LIMIT', decimal_format(playout_limit)),
|
|
171
|
+
quoted_format('X-RESTRICT', restrict),
|
|
172
|
+
quoted_format('X-SNAP', snap),
|
|
173
|
+
quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies),
|
|
174
|
+
quoted_format('X-TIMELINE-STYLE', timeline_style),
|
|
175
|
+
quoted_format('X-CONTENT-MAY-VARY', content_may_vary)]
|
|
102
176
|
end
|
|
103
177
|
|
|
104
178
|
def end_on_next_format
|
|
105
179
|
return unless end_on_next
|
|
106
|
-
'END-ON-NEXT=YES'
|
|
107
|
-
end
|
|
108
180
|
|
|
109
|
-
|
|
110
|
-
attributes.select { |key| key.start_with?('X-') }
|
|
181
|
+
'END-ON-NEXT=YES'
|
|
111
182
|
end
|
|
112
183
|
end
|
|
113
184
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# DefineItem represents an EXT-X-DEFINE tag which provides variable
|
|
5
|
+
# definitions for variable substitution. Supports three mutually
|
|
6
|
+
# exclusive modes: NAME/VALUE, IMPORT, or QUERYPARAM.
|
|
7
|
+
class DefineItem
|
|
8
|
+
extend M3u8
|
|
9
|
+
|
|
10
|
+
# @return [String, nil] variable name
|
|
11
|
+
# @return [String, nil] variable value
|
|
12
|
+
# @return [String, nil] imported variable name
|
|
13
|
+
# @return [String, nil] query parameter name
|
|
14
|
+
attr_accessor :name, :value, :import, :queryparam
|
|
15
|
+
|
|
16
|
+
# @param params [Hash] attribute key-value pairs
|
|
17
|
+
def initialize(params = {})
|
|
18
|
+
params.each do |key, val|
|
|
19
|
+
instance_variable_set("@#{key}", val)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Parse an EXT-X-DEFINE tag.
|
|
24
|
+
# @param text [String] raw tag line
|
|
25
|
+
# @return [DefineItem]
|
|
26
|
+
def self.parse(text)
|
|
27
|
+
attributes = parse_attributes(text)
|
|
28
|
+
DefineItem.new(
|
|
29
|
+
name: attributes['NAME'],
|
|
30
|
+
value: attributes['VALUE'],
|
|
31
|
+
import: attributes['IMPORT'],
|
|
32
|
+
queryparam: attributes['QUERYPARAM']
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Render as an m3u8 EXT-X-DEFINE tag.
|
|
37
|
+
# @return [String]
|
|
38
|
+
def to_s
|
|
39
|
+
"#EXT-X-DEFINE:#{formatted_attributes}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def formatted_attributes
|
|
45
|
+
if import
|
|
46
|
+
%(IMPORT="#{import}")
|
|
47
|
+
elsif queryparam
|
|
48
|
+
%(QUERYPARAM="#{queryparam}")
|
|
49
|
+
else
|
|
50
|
+
%(NAME="#{name}",VALUE="#{value}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
|
|
4
5
|
# discontinuity between the SegmentItems that proceed and follow it.
|
|
5
6
|
class DiscontinuityItem
|
|
7
|
+
# Render as an m3u8 EXT-X-DISCONTINUITY tag.
|
|
8
|
+
# @return [String]
|
|
6
9
|
def to_s
|
|
7
10
|
"#EXT-X-DISCONTINUITY\n"
|
|
8
11
|
end
|
data/lib/m3u8/encryptable.rb
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
|
-
# Encapsulates logic common to encryption key tags
|
|
4
|
+
# Encapsulates logic common to encryption key tags.
|
|
5
|
+
# Adds :method, :uri, :iv, :key_format, and
|
|
6
|
+
# :key_format_versions accessors when included.
|
|
4
7
|
module Encryptable
|
|
8
|
+
include AttributeFormatter
|
|
9
|
+
|
|
10
|
+
# @!attribute [rw] method
|
|
11
|
+
# @return [String, nil] encryption method
|
|
12
|
+
# @!attribute [rw] uri
|
|
13
|
+
# @return [String, nil] key URI
|
|
14
|
+
# @!attribute [rw] iv
|
|
15
|
+
# @return [String, nil] initialization vector
|
|
16
|
+
# @!attribute [rw] key_format
|
|
17
|
+
# @return [String, nil] KEYFORMAT value
|
|
18
|
+
# @!attribute [rw] key_format_versions
|
|
19
|
+
# @return [String, nil] KEYFORMATVERSIONS value
|
|
5
20
|
def self.included(base)
|
|
6
21
|
base.send :attr_accessor, :method
|
|
7
22
|
base.send :attr_accessor, :uri
|
|
@@ -10,42 +25,24 @@ module M3u8
|
|
|
10
25
|
base.send :attr_accessor, :key_format_versions
|
|
11
26
|
end
|
|
12
27
|
|
|
28
|
+
# Render encryption attributes as a comma-separated string.
|
|
29
|
+
# @return [String]
|
|
13
30
|
def attributes_to_s
|
|
14
|
-
[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
[unquoted_format('METHOD', method),
|
|
32
|
+
quoted_format('URI', uri),
|
|
33
|
+
unquoted_format('IV', iv),
|
|
34
|
+
quoted_format('KEYFORMAT', key_format),
|
|
35
|
+
quoted_format('KEYFORMATVERSIONS',
|
|
36
|
+
key_format_versions)].compact.join(',')
|
|
19
37
|
end
|
|
20
38
|
|
|
39
|
+
# Map HLS attribute names to Ruby symbol keys.
|
|
40
|
+
# @param attributes [Hash] raw attribute hash
|
|
41
|
+
# @return [Hash] symbolized options
|
|
21
42
|
def convert_key_names(attributes)
|
|
22
43
|
{ method: attributes['METHOD'], uri: attributes['URI'],
|
|
23
44
|
iv: attributes['IV'], key_format: attributes['KEYFORMAT'],
|
|
24
45
|
key_format_versions: attributes['KEYFORMATVERSIONS'] }
|
|
25
46
|
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def method_format
|
|
30
|
-
"METHOD=#{method}"
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def uri_format
|
|
34
|
-
%(URI="#{uri}") unless uri.nil?
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def iv_format
|
|
38
|
-
"IV=#{iv}" unless iv.nil?
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def key_format_format
|
|
42
|
-
%(KEYFORMAT="#{key_format}") unless key_format.nil?
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def key_format_versions_format
|
|
46
|
-
return if key_format_versions.nil?
|
|
47
|
-
|
|
48
|
-
%(KEYFORMATVERSIONS="#{key_format_versions}")
|
|
49
|
-
end
|
|
50
47
|
end
|
|
51
48
|
end
|
data/lib/m3u8/error.rb
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# GapItem represents an EXT-X-GAP tag to indicate that the segment URI
|
|
5
|
+
# to which it applies does not contain media data and should not be
|
|
6
|
+
# loaded by clients.
|
|
7
|
+
class GapItem
|
|
8
|
+
# Render as an m3u8 EXT-X-GAP tag.
|
|
9
|
+
# @return [String]
|
|
10
|
+
def to_s
|
|
11
|
+
'#EXT-X-GAP'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/m3u8/key_item.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# KeyItem represents a set of EXT-X-KEY attributes
|
|
4
5
|
class KeyItem
|
|
5
6
|
include Encryptable
|
|
6
7
|
extend M3u8
|
|
7
8
|
|
|
9
|
+
# @param params [Hash] attribute key-value pairs
|
|
8
10
|
def initialize(params = {})
|
|
9
11
|
options = convert_key_names(params)
|
|
10
12
|
options.merge(params).each do |key, value|
|
|
@@ -12,11 +14,16 @@ module M3u8
|
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# Parse an EXT-X-KEY tag.
|
|
18
|
+
# @param text [String] raw tag line
|
|
19
|
+
# @return [KeyItem]
|
|
15
20
|
def self.parse(text)
|
|
16
21
|
attributes = parse_attributes(text)
|
|
17
22
|
KeyItem.new(attributes)
|
|
18
23
|
end
|
|
19
24
|
|
|
25
|
+
# Render as an m3u8 EXT-X-KEY tag.
|
|
26
|
+
# @return [String]
|
|
20
27
|
def to_s
|
|
21
28
|
"#EXT-X-KEY:#{attributes_to_s}"
|
|
22
29
|
end
|
data/lib/m3u8/map_item.rb
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# MapItem represents a EXT-X-MAP tag which specifies how to obtain the Media
|
|
4
5
|
# Initialization Section
|
|
5
6
|
class MapItem
|
|
6
7
|
extend M3u8
|
|
7
8
|
include M3u8
|
|
9
|
+
include AttributeFormatter
|
|
10
|
+
|
|
11
|
+
# @return [String, nil] URI of the initialization section
|
|
12
|
+
# @return [ByteRange, nil] byte range within the resource
|
|
8
13
|
attr_accessor :uri, :byterange
|
|
9
14
|
|
|
15
|
+
# @param params [Hash] attribute key-value pairs
|
|
10
16
|
def initialize(params = {})
|
|
11
|
-
|
|
17
|
+
initialize_with_byterange(params)
|
|
12
18
|
end
|
|
13
19
|
|
|
20
|
+
# Parse an EXT-X-MAP tag.
|
|
21
|
+
# @param text [String] raw tag line
|
|
22
|
+
# @return [MapItem]
|
|
14
23
|
def self.parse(text)
|
|
15
24
|
attributes = parse_attributes(text)
|
|
16
25
|
range_value = attributes['BYTERANGE']
|
|
@@ -19,15 +28,17 @@ module M3u8
|
|
|
19
28
|
MapItem.new(options)
|
|
20
29
|
end
|
|
21
30
|
|
|
31
|
+
# Render as an m3u8 EXT-X-MAP tag.
|
|
32
|
+
# @return [String]
|
|
22
33
|
def to_s
|
|
23
|
-
|
|
34
|
+
"#EXT-X-MAP:#{formatted_attributes.compact.join(',')}"
|
|
24
35
|
end
|
|
25
36
|
|
|
26
37
|
private
|
|
27
38
|
|
|
28
|
-
def
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
def formatted_attributes
|
|
40
|
+
[%(URI="#{uri}"),
|
|
41
|
+
quoted_format('BYTERANGE', byterange)]
|
|
31
42
|
end
|
|
32
43
|
end
|
|
33
44
|
end
|
data/lib/m3u8/media_item.rb
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# MediaItem represents a set of EXT-X-MEDIA attributes
|
|
4
5
|
class MediaItem
|
|
5
6
|
extend M3u8
|
|
7
|
+
include AttributeFormatter
|
|
8
|
+
|
|
9
|
+
# @return [String, nil] media type (AUDIO, VIDEO, etc.)
|
|
10
|
+
# @return [String, nil] group ID
|
|
11
|
+
# @return [String, nil] language tag (RFC 5646)
|
|
12
|
+
# @return [String, nil] associated language tag
|
|
13
|
+
# @return [String, nil] rendition name
|
|
14
|
+
# @return [Boolean, nil] AUTOSELECT flag
|
|
15
|
+
# @return [Boolean, nil] DEFAULT flag
|
|
16
|
+
# @return [String, nil] rendition URI
|
|
17
|
+
# @return [Boolean, nil] FORCED flag
|
|
18
|
+
# @return [String, nil] instream ID for CC
|
|
19
|
+
# @return [String, nil] media characteristics
|
|
20
|
+
# @return [String, nil] audio channels
|
|
21
|
+
# @return [String, nil] stable rendition ID
|
|
22
|
+
# @return [Integer, nil] audio bit depth
|
|
23
|
+
# @return [Integer, nil] audio sample rate
|
|
6
24
|
attr_accessor :type, :group_id, :language, :assoc_language, :name,
|
|
7
25
|
:autoselect, :default, :uri, :forced, :instream_id,
|
|
8
|
-
:characteristics, :channels
|
|
26
|
+
:characteristics, :channels, :stable_rendition_id,
|
|
27
|
+
:bit_depth, :sample_rate
|
|
9
28
|
|
|
29
|
+
# @param params [Hash] attribute key-value pairs
|
|
10
30
|
def initialize(params = {})
|
|
11
31
|
params.each do |key, value|
|
|
12
32
|
instance_variable_set("@#{key}", value)
|
|
13
33
|
end
|
|
14
34
|
end
|
|
15
35
|
|
|
36
|
+
# Parse an EXT-X-MEDIA tag.
|
|
37
|
+
# @param text [String] raw tag line
|
|
38
|
+
# @return [MediaItem]
|
|
16
39
|
def self.parse(text)
|
|
17
40
|
attributes = parse_attributes(text)
|
|
18
|
-
options = { type: attributes['TYPE'],
|
|
41
|
+
options = { type: attributes['TYPE'],
|
|
42
|
+
group_id: attributes['GROUP-ID'],
|
|
19
43
|
language: attributes['LANGUAGE'],
|
|
20
44
|
assoc_language: attributes['ASSOC-LANGUAGE'],
|
|
21
45
|
name: attributes['NAME'],
|
|
@@ -25,10 +49,16 @@ module M3u8
|
|
|
25
49
|
uri: attributes['URI'],
|
|
26
50
|
instream_id: attributes['INSTREAM-ID'],
|
|
27
51
|
characteristics: attributes['CHARACTERISTICS'],
|
|
28
|
-
channels: attributes['CHANNELS']
|
|
52
|
+
channels: attributes['CHANNELS'],
|
|
53
|
+
stable_rendition_id:
|
|
54
|
+
attributes['STABLE-RENDITION-ID'],
|
|
55
|
+
bit_depth: parse_int(attributes['BIT-DEPTH']),
|
|
56
|
+
sample_rate: parse_int(attributes['SAMPLE-RATE']) }
|
|
29
57
|
MediaItem.new(options)
|
|
30
58
|
end
|
|
31
59
|
|
|
60
|
+
# Render as an m3u8 EXT-X-MEDIA tag.
|
|
61
|
+
# @return [String]
|
|
32
62
|
def to_s
|
|
33
63
|
"#EXT-X-MEDIA:#{formatted_attributes.join(',')}"
|
|
34
64
|
end
|
|
@@ -36,79 +66,21 @@ module M3u8
|
|
|
36
66
|
private
|
|
37
67
|
|
|
38
68
|
def formatted_attributes
|
|
39
|
-
[
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"TYPE=#{type}"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def group_id_format
|
|
58
|
-
%(GROUP-ID="#{group_id}")
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def language_format
|
|
62
|
-
return if language.nil?
|
|
63
|
-
%(LANGUAGE="#{language}")
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def assoc_language_format
|
|
67
|
-
return if assoc_language.nil?
|
|
68
|
-
%(ASSOC-LANGUAGE="#{assoc_language}")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def name_format
|
|
72
|
-
%(NAME="#{name}")
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def autoselect_format
|
|
76
|
-
return if autoselect.nil?
|
|
77
|
-
"AUTOSELECT=#{to_yes_no autoselect}"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def default_format
|
|
81
|
-
return if default.nil?
|
|
82
|
-
"DEFAULT=#{to_yes_no default}"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def uri_format
|
|
86
|
-
return if uri.nil?
|
|
87
|
-
%(URI="#{uri}")
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def forced_format
|
|
91
|
-
return if forced.nil?
|
|
92
|
-
"FORCED=#{to_yes_no forced}"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def instream_id_format
|
|
96
|
-
return if instream_id.nil?
|
|
97
|
-
%(INSTREAM-ID="#{instream_id}")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def characteristics_format
|
|
101
|
-
return if characteristics.nil?
|
|
102
|
-
%(CHARACTERISTICS="#{characteristics}")
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def channels_format
|
|
106
|
-
return if channels.nil?
|
|
107
|
-
%(CHANNELS="#{channels}")
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def to_yes_no(boolean)
|
|
111
|
-
boolean == true ? 'YES' : 'NO'
|
|
69
|
+
["TYPE=#{type}",
|
|
70
|
+
%(GROUP-ID="#{group_id}"),
|
|
71
|
+
quoted_format('LANGUAGE', language),
|
|
72
|
+
quoted_format('ASSOC-LANGUAGE', assoc_language),
|
|
73
|
+
%(NAME="#{name}"),
|
|
74
|
+
boolean_format('AUTOSELECT', autoselect),
|
|
75
|
+
boolean_format('DEFAULT', default),
|
|
76
|
+
quoted_format('URI', uri),
|
|
77
|
+
boolean_format('FORCED', forced),
|
|
78
|
+
quoted_format('INSTREAM-ID', instream_id),
|
|
79
|
+
quoted_format('CHARACTERISTICS', characteristics),
|
|
80
|
+
quoted_format('CHANNELS', channels),
|
|
81
|
+
quoted_format('STABLE-RENDITION-ID', stable_rendition_id),
|
|
82
|
+
unquoted_format('BIT-DEPTH', bit_depth),
|
|
83
|
+
unquoted_format('SAMPLE-RATE', sample_rate)].compact
|
|
112
84
|
end
|
|
113
85
|
end
|
|
114
86
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# PartInfItem represents an EXT-X-PART-INF tag which provides
|
|
5
|
+
# information about partial segments in the playlist.
|
|
6
|
+
class PartInfItem
|
|
7
|
+
extend M3u8
|
|
8
|
+
|
|
9
|
+
# @return [Float, nil] partial segment target duration
|
|
10
|
+
attr_accessor :part_target
|
|
11
|
+
|
|
12
|
+
# @param params [Hash] attribute key-value pairs
|
|
13
|
+
def initialize(params = {})
|
|
14
|
+
params.each do |key, value|
|
|
15
|
+
instance_variable_set("@#{key}", value)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Parse an EXT-X-PART-INF tag.
|
|
20
|
+
# @param text [String] raw tag line
|
|
21
|
+
# @return [PartInfItem]
|
|
22
|
+
def self.parse(text)
|
|
23
|
+
attributes = parse_attributes(text)
|
|
24
|
+
PartInfItem.new(
|
|
25
|
+
part_target: attributes['PART-TARGET'].to_f
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Render as an m3u8 EXT-X-PART-INF tag.
|
|
30
|
+
# @return [String]
|
|
31
|
+
def to_s
|
|
32
|
+
"#EXT-X-PART-INF:PART-TARGET=#{part_target}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|