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,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# PartItem represents an EXT-X-PART tag for Low-Latency HLS partial
|
|
5
|
+
# segments.
|
|
6
|
+
class PartItem
|
|
7
|
+
extend M3u8
|
|
8
|
+
include M3u8
|
|
9
|
+
include AttributeFormatter
|
|
10
|
+
|
|
11
|
+
# @return [String, nil] partial segment URI
|
|
12
|
+
# @return [Float, nil] partial segment duration
|
|
13
|
+
# @return [Boolean, nil] whether segment is independent
|
|
14
|
+
# @return [ByteRange, nil] byte range
|
|
15
|
+
# @return [Boolean, nil] whether segment is a gap
|
|
16
|
+
attr_accessor :uri, :duration, :independent, :byterange, :gap
|
|
17
|
+
|
|
18
|
+
# @param params [Hash] attribute key-value pairs
|
|
19
|
+
def initialize(params = {})
|
|
20
|
+
initialize_with_byterange(params)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Parse an EXT-X-PART tag.
|
|
24
|
+
# @param text [String] raw tag line
|
|
25
|
+
# @return [PartItem]
|
|
26
|
+
def self.parse(text)
|
|
27
|
+
attributes = parse_attributes(text)
|
|
28
|
+
range_value = attributes['BYTERANGE']
|
|
29
|
+
range = ByteRange.parse(range_value) unless range_value.nil?
|
|
30
|
+
PartItem.new(
|
|
31
|
+
uri: attributes['URI'],
|
|
32
|
+
duration: attributes['DURATION'].to_f,
|
|
33
|
+
independent: parse_yes_no(attributes['INDEPENDENT']),
|
|
34
|
+
byterange: range,
|
|
35
|
+
gap: parse_yes_no(attributes['GAP'])
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Render as an m3u8 EXT-X-PART tag.
|
|
40
|
+
# @return [String]
|
|
41
|
+
def to_s
|
|
42
|
+
"#EXT-X-PART:#{formatted_attributes}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def formatted_attributes
|
|
48
|
+
[unquoted_format('DURATION', decimal_format(duration)),
|
|
49
|
+
quoted_format('URI', uri),
|
|
50
|
+
independent_format,
|
|
51
|
+
quoted_format('BYTERANGE', byterange),
|
|
52
|
+
gap_format].compact.join(',')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def independent_format
|
|
56
|
+
return unless independent
|
|
57
|
+
|
|
58
|
+
'INDEPENDENT=YES'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def gap_format
|
|
62
|
+
return unless gap
|
|
63
|
+
|
|
64
|
+
'GAP=YES'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/m3u8/playback_start.rb
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# PlaybackStart represents a #EXT-X-START tag and attributes
|
|
4
5
|
class PlaybackStart
|
|
5
|
-
|
|
6
|
+
extend M3u8
|
|
7
|
+
include AttributeFormatter
|
|
8
|
+
|
|
9
|
+
# @return [Float, nil] time offset in seconds
|
|
10
|
+
# @return [Boolean, nil] whether start is precise
|
|
6
11
|
attr_accessor :time_offset, :precise
|
|
7
12
|
|
|
13
|
+
# @param options [Hash] attribute key-value pairs
|
|
8
14
|
def initialize(options = {})
|
|
9
15
|
options.each do |key, value|
|
|
10
16
|
instance_variable_set("@#{key}", value)
|
|
11
17
|
end
|
|
12
18
|
end
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
# Parse an EXT-X-START tag.
|
|
21
|
+
# @param text [String] raw tag line
|
|
22
|
+
# @return [PlaybackStart]
|
|
23
|
+
def self.parse(text)
|
|
15
24
|
attributes = parse_attributes(text)
|
|
16
|
-
@time_offset = attributes['TIME-OFFSET'].to_f
|
|
17
25
|
precise = attributes['PRECISE']
|
|
18
|
-
|
|
26
|
+
options = {
|
|
27
|
+
time_offset: attributes['TIME-OFFSET'].to_f,
|
|
28
|
+
precise: precise.nil? ? nil : parse_yes_no(precise)
|
|
29
|
+
}
|
|
30
|
+
PlaybackStart.new(options)
|
|
19
31
|
end
|
|
20
32
|
|
|
33
|
+
# Render as an m3u8 EXT-X-START tag.
|
|
34
|
+
# @return [String]
|
|
21
35
|
def to_s
|
|
22
36
|
attributes = ["TIME-OFFSET=#{time_offset}",
|
|
23
|
-
|
|
37
|
+
boolean_format('PRECISE', precise)].compact.join(',')
|
|
24
38
|
"#EXT-X-START:#{attributes}"
|
|
25
39
|
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def precise_format
|
|
30
|
-
return if precise.nil?
|
|
31
|
-
"PRECISE=#{to_yes_no(precise)}"
|
|
32
|
-
end
|
|
33
40
|
end
|
|
34
41
|
end
|
data/lib/m3u8/playlist.rb
CHANGED
|
@@ -1,64 +1,188 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
|
-
# Playlist represents an m3u8 playlist, it can be a master playlist
|
|
4
|
-
# of media segments
|
|
4
|
+
# Playlist represents an m3u8 playlist, it can be a master playlist
|
|
5
|
+
# or a set of media segments
|
|
5
6
|
class Playlist
|
|
7
|
+
# @return [Array] list of items in the playlist
|
|
8
|
+
# @return [Integer, nil] EXT-X-VERSION value
|
|
9
|
+
# @return [Boolean, nil] EXT-X-ALLOW-CACHE value
|
|
10
|
+
# @return [Integer] EXT-X-TARGETDURATION value
|
|
11
|
+
# @return [Integer] EXT-X-MEDIA-SEQUENCE value
|
|
12
|
+
# @return [Integer, nil] EXT-X-DISCONTINUITY-SEQUENCE value
|
|
13
|
+
# @return [String, nil] EXT-X-PLAYLIST-TYPE (VOD or EVENT)
|
|
14
|
+
# @return [Boolean] whether playlist is I-frames only
|
|
15
|
+
# @return [Boolean] whether segments are independent
|
|
16
|
+
# @return [Boolean] whether playlist is live
|
|
17
|
+
# @return [PartInfItem, nil] EXT-X-PART-INF item
|
|
18
|
+
# @return [ServerControlItem, nil] EXT-X-SERVER-CONTROL item
|
|
6
19
|
attr_accessor :items, :version, :cache, :target, :sequence,
|
|
7
20
|
:discontinuity_sequence, :type, :iframes_only,
|
|
8
|
-
:independent_segments, :live
|
|
21
|
+
:independent_segments, :live, :part_inf,
|
|
22
|
+
:server_control
|
|
9
23
|
|
|
24
|
+
# @param options [Hash] playlist attributes
|
|
10
25
|
def initialize(options = {})
|
|
11
26
|
assign_options(options)
|
|
12
27
|
@items = []
|
|
13
28
|
end
|
|
14
29
|
|
|
30
|
+
# Build a playlist using a DSL block.
|
|
31
|
+
# @param options [Hash] playlist attributes
|
|
32
|
+
# @yield [Builder] block receives builder instance
|
|
33
|
+
# @return [Playlist] frozen playlist
|
|
34
|
+
def self.build(options = {}, &block)
|
|
35
|
+
playlist = new(options)
|
|
36
|
+
builder = Builder.new(playlist)
|
|
37
|
+
if block.arity == 1
|
|
38
|
+
yield builder
|
|
39
|
+
else
|
|
40
|
+
builder.instance_eval(&block)
|
|
41
|
+
end
|
|
42
|
+
playlist.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Generate a codecs string from codec options.
|
|
46
|
+
# @param options [Hash] codec options (:profile, :level, etc.)
|
|
47
|
+
# @return [String, nil] codecs string
|
|
15
48
|
def self.codecs(options = {})
|
|
16
49
|
item = PlaylistItem.new(options)
|
|
17
50
|
item.codecs
|
|
18
51
|
end
|
|
19
52
|
|
|
53
|
+
# Parse an m3u8 playlist from a String or IO.
|
|
54
|
+
# @param input [String, IO] playlist content
|
|
55
|
+
# @return [Playlist] frozen playlist
|
|
20
56
|
def self.read(input)
|
|
21
57
|
reader = Reader.new
|
|
22
58
|
reader.read(input)
|
|
23
59
|
end
|
|
24
60
|
|
|
61
|
+
# Write the playlist to an IO object.
|
|
62
|
+
# @param output [IO] writable IO object
|
|
63
|
+
# @return [void]
|
|
25
64
|
def write(output)
|
|
26
65
|
writer = Writer.new(output)
|
|
27
66
|
writer.write(self)
|
|
28
67
|
end
|
|
29
68
|
|
|
69
|
+
# Whether this is a live (non-VOD) media playlist.
|
|
70
|
+
# @return [Boolean]
|
|
30
71
|
def live?
|
|
31
72
|
return false if master?
|
|
73
|
+
|
|
32
74
|
@live
|
|
33
75
|
end
|
|
34
76
|
|
|
77
|
+
# Whether this is a master (multivariant) playlist.
|
|
78
|
+
# @return [Boolean]
|
|
35
79
|
def master?
|
|
36
80
|
return @master unless @master.nil?
|
|
37
81
|
return false if playlist_size.zero? && segment_size.zero?
|
|
38
|
-
|
|
82
|
+
|
|
83
|
+
playlist_size.positive?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Freeze the playlist and all its items.
|
|
87
|
+
# @return [Playlist]
|
|
88
|
+
def freeze
|
|
89
|
+
items.each { |item| freeze_item(item) }
|
|
90
|
+
items.freeze
|
|
91
|
+
part_inf&.freeze
|
|
92
|
+
server_control&.freeze
|
|
93
|
+
super
|
|
39
94
|
end
|
|
40
95
|
|
|
96
|
+
# Render the playlist as an m3u8 string.
|
|
97
|
+
# @return [String]
|
|
41
98
|
def to_s
|
|
42
99
|
output = StringIO.open
|
|
43
100
|
write(output)
|
|
44
101
|
output.string
|
|
45
102
|
end
|
|
46
103
|
|
|
104
|
+
# Collect validation errors for the playlist.
|
|
105
|
+
# @return [Array<String>] list of error messages
|
|
106
|
+
def errors
|
|
107
|
+
[].tap do |errors|
|
|
108
|
+
validate_mixed_items(errors)
|
|
109
|
+
validate_target_duration(errors)
|
|
110
|
+
validate_segment_items(errors)
|
|
111
|
+
validate_playlist_items(errors)
|
|
112
|
+
validate_media_items(errors)
|
|
113
|
+
validate_key_items(errors)
|
|
114
|
+
validate_session_key_items(errors)
|
|
115
|
+
validate_session_data_items(errors)
|
|
116
|
+
validate_part_items(errors)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Whether the playlist passes all validations.
|
|
121
|
+
# @return [Boolean]
|
|
47
122
|
def valid?
|
|
48
|
-
|
|
49
|
-
|
|
123
|
+
errors.empty?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [Array<SegmentItem>]
|
|
127
|
+
def segments
|
|
128
|
+
items.grep(SegmentItem)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# @return [Array<PlaylistItem>]
|
|
132
|
+
def playlists
|
|
133
|
+
items.grep(PlaylistItem)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @return [Array<MediaItem>]
|
|
137
|
+
def media_items
|
|
138
|
+
items.grep(MediaItem)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Array<KeyItem>]
|
|
142
|
+
def keys
|
|
143
|
+
items.grep(KeyItem)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# @return [Array<MapItem>]
|
|
147
|
+
def maps
|
|
148
|
+
items.grep(MapItem)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# @return [Array<DateRangeItem>]
|
|
152
|
+
def date_ranges
|
|
153
|
+
items.grep(DateRangeItem)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [Array<PartItem>]
|
|
157
|
+
def parts
|
|
158
|
+
items.grep(PartItem)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @return [Array<SessionDataItem>]
|
|
162
|
+
def session_data
|
|
163
|
+
items.grep(SessionDataItem)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @return [Array<SessionKeyItem>]
|
|
167
|
+
def session_keys
|
|
168
|
+
items.grep(SessionKeyItem)
|
|
50
169
|
end
|
|
51
170
|
|
|
171
|
+
# Total duration of all segments.
|
|
172
|
+
# @return [Float]
|
|
52
173
|
def duration
|
|
53
|
-
duration
|
|
54
|
-
items.each do |item|
|
|
55
|
-
duration += item.duration if item.is_a?(M3u8::SegmentItem)
|
|
56
|
-
end
|
|
57
|
-
duration
|
|
174
|
+
segments.sum(&:duration)
|
|
58
175
|
end
|
|
59
176
|
|
|
60
177
|
private
|
|
61
178
|
|
|
179
|
+
def freeze_item(item)
|
|
180
|
+
item.byterange&.freeze if item.respond_to?(:byterange)
|
|
181
|
+
item.program_date_time&.freeze if item.respond_to?(:program_date_time)
|
|
182
|
+
item.client_attributes&.freeze if item.respond_to?(:client_attributes)
|
|
183
|
+
item.freeze
|
|
184
|
+
end
|
|
185
|
+
|
|
62
186
|
def assign_options(options)
|
|
63
187
|
options = defaults.merge(options)
|
|
64
188
|
|
|
@@ -72,6 +196,8 @@ module M3u8
|
|
|
72
196
|
@independent_segments = options[:independent_segments]
|
|
73
197
|
@master = options[:master]
|
|
74
198
|
@live = options[:live]
|
|
199
|
+
@part_inf = options[:part_inf]
|
|
200
|
+
@server_control = options[:server_control]
|
|
75
201
|
end
|
|
76
202
|
|
|
77
203
|
def defaults
|
|
@@ -84,12 +210,94 @@ module M3u8
|
|
|
84
210
|
}
|
|
85
211
|
end
|
|
86
212
|
|
|
213
|
+
def validate_part_items(errors)
|
|
214
|
+
parts.each do |item|
|
|
215
|
+
errors << 'Part item requires a URI' if item.uri.nil?
|
|
216
|
+
errors << 'Part item requires a duration' if item.duration.nil?
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def validate_session_data_items(errors)
|
|
221
|
+
session_data.each do |item|
|
|
222
|
+
errors << 'Session data item requires a data ID' if item.data_id.nil?
|
|
223
|
+
if !item.value.nil? && !item.uri.nil?
|
|
224
|
+
errors << 'Session data item cannot have both value and URI'
|
|
225
|
+
elsif item.value.nil? && item.uri.nil?
|
|
226
|
+
errors << 'Session data item requires a value or URI'
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def validate_key_items(errors)
|
|
232
|
+
keys.each do |item|
|
|
233
|
+
next if item.method == 'NONE'
|
|
234
|
+
|
|
235
|
+
next unless item.uri.nil?
|
|
236
|
+
|
|
237
|
+
errors << 'Key item requires a URI ' \
|
|
238
|
+
'when method is not NONE'
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def validate_session_key_items(errors)
|
|
243
|
+
session_keys.each do |item|
|
|
244
|
+
next if item.method == 'NONE'
|
|
245
|
+
|
|
246
|
+
next unless item.uri.nil?
|
|
247
|
+
|
|
248
|
+
errors << 'Session key item requires a URI ' \
|
|
249
|
+
'when method is not NONE'
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def validate_playlist_items(errors)
|
|
254
|
+
playlists.each do |item|
|
|
255
|
+
unless item.bandwidth&.positive?
|
|
256
|
+
errors << 'Playlist item requires a bandwidth'
|
|
257
|
+
end
|
|
258
|
+
errors << 'Playlist item requires a URI' if item.uri.nil?
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def validate_media_items(errors)
|
|
263
|
+
media_items.each do |item|
|
|
264
|
+
errors << 'Media item requires a type' if item.type.nil?
|
|
265
|
+
errors << 'Media item requires a group ID' if item.group_id.nil?
|
|
266
|
+
errors << 'Media item requires a name' if item.name.nil?
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def validate_segment_items(errors)
|
|
271
|
+
segments.each do |segment|
|
|
272
|
+
errors << 'Segment item requires a segment URI' if segment.segment.nil?
|
|
273
|
+
if segment.duration&.negative?
|
|
274
|
+
errors << 'Segment item has negative duration'
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def validate_target_duration(errors)
|
|
280
|
+
return if master?
|
|
281
|
+
|
|
282
|
+
max = segments.filter_map { |s| s.duration&.round }.max
|
|
283
|
+
return if max.nil? || target >= max
|
|
284
|
+
|
|
285
|
+
errors << "Target duration #{target} is less than " \
|
|
286
|
+
"segment duration of #{max}"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def validate_mixed_items(errors)
|
|
290
|
+
return unless playlist_size.positive? && segment_size.positive?
|
|
291
|
+
|
|
292
|
+
errors << 'Playlist contains both master and media items'
|
|
293
|
+
end
|
|
294
|
+
|
|
87
295
|
def playlist_size
|
|
88
|
-
|
|
296
|
+
playlists.size
|
|
89
297
|
end
|
|
90
298
|
|
|
91
299
|
def segment_size
|
|
92
|
-
|
|
300
|
+
segments.size
|
|
93
301
|
end
|
|
94
302
|
end
|
|
95
303
|
end
|