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/scte35.rb
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Parses SCTE-35 splice_info_section binary payloads from hex strings
|
|
5
|
+
class Scte35
|
|
6
|
+
class ParseError < StandardError; end
|
|
7
|
+
|
|
8
|
+
attr_reader :table_id, :pts_adjustment, :tier,
|
|
9
|
+
:splice_command_type, :splice_command, :descriptors
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
options.each do |key, value|
|
|
13
|
+
instance_variable_set("@#{key}", value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse(hex_string)
|
|
18
|
+
raw = hex_string.sub(/\A0x/i, '')
|
|
19
|
+
data = [raw].pack('H*')
|
|
20
|
+
reader = Scte35BitReader.new(data)
|
|
21
|
+
|
|
22
|
+
header = parse_header(reader)
|
|
23
|
+
command = parse_command(reader, header)
|
|
24
|
+
descriptors = parse_descriptors(reader, header)
|
|
25
|
+
|
|
26
|
+
args = header.merge(splice_command: command,
|
|
27
|
+
descriptors: descriptors, raw: hex_string)
|
|
28
|
+
new(**args)
|
|
29
|
+
rescue NoMethodError, ArgumentError => e
|
|
30
|
+
raise ParseError, "invalid SCTE-35 data: #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_s
|
|
34
|
+
@raw
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.parse_header(reader)
|
|
38
|
+
table_id = reader.read_bits(8)
|
|
39
|
+
reader.skip_bits(4) # section_syntax_indicator, private, reserved
|
|
40
|
+
section_length = reader.read_bits(12)
|
|
41
|
+
reader.read_bits(8) # protocol_version
|
|
42
|
+
reader.read_flag # encrypted_packet
|
|
43
|
+
reader.read_bits(6) # encryption_algorithm
|
|
44
|
+
pts_adjustment = reader.read_bits(33)
|
|
45
|
+
reader.read_bits(8) # cw_index
|
|
46
|
+
tier = reader.read_bits(12)
|
|
47
|
+
splice_command_length = reader.read_bits(12)
|
|
48
|
+
splice_command_type = reader.read_bits(8)
|
|
49
|
+
|
|
50
|
+
{ table_id: table_id, pts_adjustment: pts_adjustment,
|
|
51
|
+
tier: tier, splice_command_type: splice_command_type,
|
|
52
|
+
splice_command_length: splice_command_length,
|
|
53
|
+
section_length: section_length }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.parse_command(reader, header)
|
|
57
|
+
cmd_length = command_data_length(reader, header)
|
|
58
|
+
|
|
59
|
+
case header[:splice_command_type]
|
|
60
|
+
when 0x00 then Scte35SpliceNull.new
|
|
61
|
+
when 0x05 then Scte35SpliceInsert.parse_from(reader, cmd_length)
|
|
62
|
+
when 0x06 then Scte35TimeSignal.parse_from(reader, cmd_length)
|
|
63
|
+
else reader.read_bytes(cmd_length)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.parse_splice_time(reader)
|
|
68
|
+
unless reader.read_flag # time_specified
|
|
69
|
+
reader.skip_bits(7) # reserved
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
reader.skip_bits(6) # reserved
|
|
74
|
+
reader.read_bits(33)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.parse_descriptors(reader, header)
|
|
78
|
+
# For unknown commands with 0xFFF, command consumed all
|
|
79
|
+
# remaining bytes (per spec), so no descriptors to parse
|
|
80
|
+
return [] if unknown_command_with_unspecified_length?(header)
|
|
81
|
+
|
|
82
|
+
desc_loop_length = reader.read_bits(16)
|
|
83
|
+
parse_descriptor_loop(reader, desc_loop_length)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.unknown_command_with_unspecified_length?(header)
|
|
87
|
+
header[:splice_command_length] == 0xFFF &&
|
|
88
|
+
![0x00, 0x05, 0x06].include?(header[:splice_command_type])
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def self.parse_descriptor_loop(reader, remaining)
|
|
92
|
+
descriptors = []
|
|
93
|
+
while remaining.positive?
|
|
94
|
+
tag = reader.read_bits(8)
|
|
95
|
+
length = reader.read_bits(8)
|
|
96
|
+
remaining -= 2 + length
|
|
97
|
+
descriptors << parse_single_descriptor(reader, tag, length)
|
|
98
|
+
end
|
|
99
|
+
descriptors
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.parse_single_descriptor(reader, tag, length)
|
|
103
|
+
identifier = reader.read_bits(32)
|
|
104
|
+
if tag == Scte35SegmentationDescriptor::DESCRIPTOR_TAG &&
|
|
105
|
+
identifier == Scte35SegmentationDescriptor::CUEI_IDENTIFIER
|
|
106
|
+
Scte35SegmentationDescriptor.parse_from(reader, length)
|
|
107
|
+
else
|
|
108
|
+
reader.read_bytes(length - 4)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.command_data_length(reader, header)
|
|
113
|
+
length = header[:splice_command_length]
|
|
114
|
+
return length unless length == 0xFFF
|
|
115
|
+
|
|
116
|
+
if unknown_command_with_unspecified_length?(header)
|
|
117
|
+
# Unknown command: consume everything up to CRC
|
|
118
|
+
reader.bytes_remaining - 4
|
|
119
|
+
else
|
|
120
|
+
# Known command types parse their own fields; length unused
|
|
121
|
+
0
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private_class_method :parse_header, :parse_command,
|
|
126
|
+
:parse_descriptors, :command_data_length,
|
|
127
|
+
:parse_descriptor_loop, :parse_single_descriptor,
|
|
128
|
+
:unknown_command_with_unspecified_length?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Reads sub-byte bit fields from binary data for SCTE-35 parsing
|
|
5
|
+
class Scte35BitReader
|
|
6
|
+
def initialize(data)
|
|
7
|
+
@data = data.b
|
|
8
|
+
@byte_pos = 0
|
|
9
|
+
@bit_pos = 0
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def read_bits(count)
|
|
13
|
+
value = 0
|
|
14
|
+
count.times do
|
|
15
|
+
value = (value << 1) | current_bit
|
|
16
|
+
advance_bit
|
|
17
|
+
end
|
|
18
|
+
value
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def read_flag
|
|
22
|
+
read_bits(1) == 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read_bytes(count)
|
|
26
|
+
@data[@byte_pos, count].tap { @byte_pos += count }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def skip_bits(count)
|
|
30
|
+
count.times { advance_bit }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def bytes_remaining
|
|
34
|
+
@data.length - @byte_pos - (@bit_pos.positive? ? 1 : 0)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def current_bit
|
|
40
|
+
(@data.getbyte(@byte_pos) >> (7 - @bit_pos)) & 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def advance_bit
|
|
44
|
+
@bit_pos += 1
|
|
45
|
+
return unless @bit_pos >= 8
|
|
46
|
+
|
|
47
|
+
@bit_pos = 0
|
|
48
|
+
@byte_pos += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Represents a segmentation_descriptor (tag 0x02) in SCTE-35
|
|
5
|
+
class Scte35SegmentationDescriptor
|
|
6
|
+
CUEI_IDENTIFIER = 0x43554549
|
|
7
|
+
DESCRIPTOR_TAG = 0x02
|
|
8
|
+
|
|
9
|
+
attr_reader :segmentation_event_id, :segmentation_event_cancel_indicator,
|
|
10
|
+
:segmentation_type_id, :segmentation_duration,
|
|
11
|
+
:segmentation_upid_type, :segmentation_upid,
|
|
12
|
+
:segment_num, :segments_expected
|
|
13
|
+
|
|
14
|
+
def initialize(options = {})
|
|
15
|
+
options.each do |key, value|
|
|
16
|
+
instance_variable_set("@#{key}", value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.parse_from(reader, _length)
|
|
21
|
+
attrs = { segmentation_event_id: reader.read_bits(32) }
|
|
22
|
+
attrs[:segmentation_event_cancel_indicator] = reader.read_flag
|
|
23
|
+
reader.skip_bits(7) # reserved
|
|
24
|
+
return new(**attrs) if attrs[:segmentation_event_cancel_indicator]
|
|
25
|
+
|
|
26
|
+
parse_segmentation_detail(reader, attrs)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.parse_segmentation_detail(reader, attrs)
|
|
30
|
+
reader.read_flag # program_segmentation_flag
|
|
31
|
+
duration_flag = reader.read_flag
|
|
32
|
+
reader.skip_bits(6) # delivery_not_restricted + reserved/flags
|
|
33
|
+
|
|
34
|
+
attrs[:segmentation_duration] = reader.read_bits(40) if duration_flag
|
|
35
|
+
parse_upid(reader, attrs)
|
|
36
|
+
attrs[:segmentation_type_id] = reader.read_bits(8)
|
|
37
|
+
attrs[:segment_num] = reader.read_bits(8)
|
|
38
|
+
attrs[:segments_expected] = reader.read_bits(8)
|
|
39
|
+
new(**attrs)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.parse_upid(reader, attrs)
|
|
43
|
+
attrs[:segmentation_upid_type] = reader.read_bits(8)
|
|
44
|
+
upid_length = reader.read_bits(8)
|
|
45
|
+
return if upid_length.zero?
|
|
46
|
+
|
|
47
|
+
attrs[:segmentation_upid] =
|
|
48
|
+
reader.read_bytes(upid_length)
|
|
49
|
+
.force_encoding('UTF-8')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_class_method :parse_segmentation_detail, :parse_upid
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Represents a splice_insert SCTE-35 command (type 0x05)
|
|
5
|
+
class Scte35SpliceInsert
|
|
6
|
+
attr_reader :splice_event_id, :splice_event_cancel_indicator,
|
|
7
|
+
:out_of_network_indicator, :pts_time,
|
|
8
|
+
:break_duration, :break_auto_return,
|
|
9
|
+
:unique_program_id, :avail_num, :avails_expected
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
options.each do |key, value|
|
|
13
|
+
instance_variable_set("@#{key}", value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse_from(reader, _length)
|
|
18
|
+
attrs = { splice_event_id: reader.read_bits(32) }
|
|
19
|
+
attrs[:splice_event_cancel_indicator] = reader.read_flag
|
|
20
|
+
reader.skip_bits(7) # reserved
|
|
21
|
+
return new(**attrs) if attrs[:splice_event_cancel_indicator]
|
|
22
|
+
|
|
23
|
+
parse_splice_detail(reader, attrs)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.parse_splice_detail(reader, attrs)
|
|
27
|
+
attrs[:out_of_network_indicator] = reader.read_flag
|
|
28
|
+
program_splice = reader.read_flag
|
|
29
|
+
duration_flag = reader.read_flag
|
|
30
|
+
immediate = reader.read_flag
|
|
31
|
+
reader.skip_bits(4) # reserved
|
|
32
|
+
|
|
33
|
+
if program_splice
|
|
34
|
+
attrs[:pts_time] = Scte35.parse_splice_time(reader) unless immediate
|
|
35
|
+
else
|
|
36
|
+
parse_components(reader, immediate)
|
|
37
|
+
end
|
|
38
|
+
parse_break_duration(reader, attrs) if duration_flag
|
|
39
|
+
attrs[:unique_program_id] = reader.read_bits(16)
|
|
40
|
+
attrs[:avail_num] = reader.read_bits(8)
|
|
41
|
+
attrs[:avails_expected] = reader.read_bits(8)
|
|
42
|
+
new(**attrs)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.parse_components(reader, immediate)
|
|
46
|
+
component_count = reader.read_bits(8)
|
|
47
|
+
component_count.times do
|
|
48
|
+
reader.read_bits(8) # component_tag
|
|
49
|
+
Scte35.parse_splice_time(reader) unless immediate
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.parse_break_duration(reader, attrs)
|
|
54
|
+
attrs[:break_auto_return] = reader.read_flag
|
|
55
|
+
reader.skip_bits(6) # reserved
|
|
56
|
+
attrs[:break_duration] = reader.read_bits(33)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private_class_method :parse_splice_detail, :parse_break_duration,
|
|
60
|
+
:parse_components
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Represents a time_signal SCTE-35 command (type 0x06)
|
|
5
|
+
class Scte35TimeSignal
|
|
6
|
+
attr_reader :pts_time
|
|
7
|
+
|
|
8
|
+
def initialize(options = {})
|
|
9
|
+
options.each do |key, value|
|
|
10
|
+
instance_variable_set("@#{key}", value)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.parse_from(reader, _length)
|
|
15
|
+
pts_time = Scte35.parse_splice_time(reader)
|
|
16
|
+
new(pts_time: pts_time)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/m3u8/segment_item.rb
CHANGED
|
@@ -1,24 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# SegmentItem represents EXTINF attributes with the URI that follows,
|
|
4
5
|
# optionally allowing an EXT-X-BYTERANGE tag to be set.
|
|
5
6
|
class SegmentItem
|
|
6
7
|
include M3u8
|
|
8
|
+
include AttributeFormatter
|
|
9
|
+
|
|
10
|
+
# @return [Float, nil] segment duration in seconds
|
|
11
|
+
# @return [String, nil] segment URI
|
|
12
|
+
# @return [String, nil] human-readable comment after duration
|
|
13
|
+
# @return [TimeItem, Time, nil] program date-time
|
|
14
|
+
# @return [ByteRange, nil] byte range
|
|
7
15
|
attr_accessor :duration, :segment, :comment, :program_date_time, :byterange
|
|
8
16
|
|
|
17
|
+
# @param params [Hash] attribute key-value pairs
|
|
9
18
|
def initialize(params = {})
|
|
10
|
-
|
|
19
|
+
initialize_with_byterange(params)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Parse an EXTINF tag line.
|
|
23
|
+
# @param text [String] raw tag line
|
|
24
|
+
# @return [SegmentItem]
|
|
25
|
+
def self.parse(text)
|
|
26
|
+
values = text.gsub('#EXTINF:', '')
|
|
27
|
+
.tr("\n", ',').split(',')
|
|
28
|
+
options = { duration: values[0].to_f }
|
|
29
|
+
options[:comment] = values[1] unless values[1].nil?
|
|
30
|
+
SegmentItem.new(options)
|
|
11
31
|
end
|
|
12
32
|
|
|
33
|
+
# Render as an m3u8 EXTINF tag with segment URI.
|
|
34
|
+
# @return [String]
|
|
13
35
|
def to_s
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
"#EXTINF:#{decimal_format(duration)},#{comment}#{byterange_format}" \
|
|
37
|
+
"\n#{date_format}#{segment}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def date_format
|
|
41
|
+
return if program_date_time.nil?
|
|
42
|
+
|
|
43
|
+
pdt = if program_date_time.is_a?(TimeItem)
|
|
44
|
+
program_date_time
|
|
45
|
+
else
|
|
46
|
+
TimeItem.new(time: program_date_time)
|
|
47
|
+
end
|
|
48
|
+
"#{pdt}\n"
|
|
16
49
|
end
|
|
17
50
|
|
|
18
51
|
private
|
|
19
52
|
|
|
20
53
|
def byterange_format
|
|
21
54
|
return if byterange.nil?
|
|
55
|
+
|
|
22
56
|
"\n#EXT-X-BYTERANGE:#{byterange}"
|
|
23
57
|
end
|
|
24
58
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# ServerControlItem represents an EXT-X-SERVER-CONTROL tag which
|
|
5
|
+
# provides directives for Low-Latency HLS delivery.
|
|
6
|
+
class ServerControlItem
|
|
7
|
+
extend M3u8
|
|
8
|
+
include AttributeFormatter
|
|
9
|
+
|
|
10
|
+
# @return [Float, nil] skip threshold in seconds
|
|
11
|
+
# @return [Boolean, nil] whether dateranges can be skipped
|
|
12
|
+
# @return [Float, nil] hold-back duration in seconds
|
|
13
|
+
# @return [Float, nil] part hold-back duration in seconds
|
|
14
|
+
# @return [Boolean, nil] whether blocking reload is supported
|
|
15
|
+
attr_accessor :can_skip_until, :can_skip_dateranges, :hold_back,
|
|
16
|
+
:part_hold_back, :can_block_reload
|
|
17
|
+
|
|
18
|
+
# @param params [Hash] attribute key-value pairs
|
|
19
|
+
def initialize(params = {})
|
|
20
|
+
params.each do |key, value|
|
|
21
|
+
instance_variable_set("@#{key}", value)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Parse an EXT-X-SERVER-CONTROL tag.
|
|
26
|
+
# @param text [String] raw tag line
|
|
27
|
+
# @return [ServerControlItem]
|
|
28
|
+
def self.parse(text)
|
|
29
|
+
attributes = parse_attributes(text)
|
|
30
|
+
ServerControlItem.new(
|
|
31
|
+
can_skip_until: parse_float(attributes['CAN-SKIP-UNTIL']),
|
|
32
|
+
can_skip_dateranges:
|
|
33
|
+
parse_yes_no(attributes['CAN-SKIP-DATERANGES']),
|
|
34
|
+
hold_back: parse_float(attributes['HOLD-BACK']),
|
|
35
|
+
part_hold_back: parse_float(attributes['PART-HOLD-BACK']),
|
|
36
|
+
can_block_reload:
|
|
37
|
+
parse_yes_no(attributes['CAN-BLOCK-RELOAD'])
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Render as an m3u8 EXT-X-SERVER-CONTROL tag.
|
|
42
|
+
# @return [String]
|
|
43
|
+
def to_s
|
|
44
|
+
"#EXT-X-SERVER-CONTROL:#{formatted_attributes}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def formatted_attributes
|
|
50
|
+
[unquoted_format('CAN-SKIP-UNTIL', can_skip_until),
|
|
51
|
+
can_skip_dateranges_format,
|
|
52
|
+
unquoted_format('HOLD-BACK', hold_back),
|
|
53
|
+
unquoted_format('PART-HOLD-BACK', part_hold_back),
|
|
54
|
+
can_block_reload_format].compact.join(',')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def can_skip_dateranges_format
|
|
58
|
+
return unless can_skip_dateranges
|
|
59
|
+
|
|
60
|
+
'CAN-SKIP-DATERANGES=YES'
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def can_block_reload_format
|
|
64
|
+
return unless can_block_reload
|
|
65
|
+
|
|
66
|
+
'CAN-BLOCK-RELOAD=YES'
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# SessionDataItem represents a set of EXT-X-SESSION-DATA attributes
|
|
4
5
|
class SessionDataItem
|
|
5
6
|
extend M3u8
|
|
7
|
+
include AttributeFormatter
|
|
8
|
+
|
|
9
|
+
# @return [String, nil] DATA-ID value
|
|
10
|
+
# @return [String, nil] VALUE attribute
|
|
11
|
+
# @return [String, nil] URI attribute
|
|
12
|
+
# @return [String, nil] LANGUAGE attribute
|
|
6
13
|
attr_accessor :data_id, :value, :uri, :language
|
|
7
14
|
|
|
15
|
+
# @param params [Hash] attribute key-value pairs
|
|
8
16
|
def initialize(params = {})
|
|
9
17
|
params.each do |key, value|
|
|
10
18
|
instance_variable_set("@#{key}", value)
|
|
11
19
|
end
|
|
12
20
|
end
|
|
13
21
|
|
|
22
|
+
# Parse an EXT-X-SESSION-DATA tag.
|
|
23
|
+
# @param text [String] raw tag line
|
|
24
|
+
# @return [SessionDataItem]
|
|
14
25
|
def self.parse(text)
|
|
15
26
|
attributes = parse_attributes text
|
|
16
27
|
options = { data_id: attributes['DATA-ID'], value: attributes['VALUE'],
|
|
@@ -18,36 +29,14 @@ module M3u8
|
|
|
18
29
|
M3u8::SessionDataItem.new options
|
|
19
30
|
end
|
|
20
31
|
|
|
32
|
+
# Render as an m3u8 EXT-X-SESSION-DATA tag.
|
|
33
|
+
# @return [String]
|
|
21
34
|
def to_s
|
|
22
|
-
attributes = [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
attributes = [quoted_format('DATA-ID', data_id),
|
|
36
|
+
quoted_format('VALUE', value),
|
|
37
|
+
quoted_format('URI', uri),
|
|
38
|
+
quoted_format('LANGUAGE', language)].compact.join(',')
|
|
26
39
|
"#EXT-X-SESSION-DATA:#{attributes}"
|
|
27
40
|
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def data_id_format
|
|
32
|
-
%(DATA-ID="#{data_id}")
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def value_format
|
|
36
|
-
return if value.nil?
|
|
37
|
-
|
|
38
|
-
%(VALUE="#{value}")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def uri_format
|
|
42
|
-
return if uri.nil?
|
|
43
|
-
|
|
44
|
-
%(URI="#{uri}")
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def language_format
|
|
48
|
-
return if language.nil?
|
|
49
|
-
|
|
50
|
-
%(LANGUAGE="#{language}")
|
|
51
|
-
end
|
|
52
41
|
end
|
|
53
42
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
|
-
#
|
|
4
|
+
# SessionKeyItem represents EXT-X-SESSION-KEY attributes
|
|
4
5
|
class SessionKeyItem
|
|
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-SESSION-KEY tag.
|
|
18
|
+
# @param text [String] raw tag line
|
|
19
|
+
# @return [SessionKeyItem]
|
|
15
20
|
def self.parse(text)
|
|
16
21
|
attributes = parse_attributes(text)
|
|
17
22
|
SessionKeyItem.new(attributes)
|
|
18
23
|
end
|
|
19
24
|
|
|
25
|
+
# Render as an m3u8 EXT-X-SESSION-KEY tag.
|
|
26
|
+
# @return [String]
|
|
20
27
|
def to_s
|
|
21
28
|
"#EXT-X-SESSION-KEY:#{attributes_to_s}"
|
|
22
29
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# SkipItem represents an EXT-X-SKIP tag used in Playlist Delta
|
|
5
|
+
# Updates for Low-Latency HLS.
|
|
6
|
+
class SkipItem
|
|
7
|
+
extend M3u8
|
|
8
|
+
include AttributeFormatter
|
|
9
|
+
|
|
10
|
+
# @return [Integer, nil] number of skipped segments
|
|
11
|
+
# @return [String, nil] recently removed dateranges
|
|
12
|
+
attr_accessor :skipped_segments, :recently_removed_dateranges
|
|
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-SKIP tag.
|
|
22
|
+
# @param text [String] raw tag line
|
|
23
|
+
# @return [SkipItem]
|
|
24
|
+
def self.parse(text)
|
|
25
|
+
attributes = parse_attributes(text)
|
|
26
|
+
SkipItem.new(
|
|
27
|
+
skipped_segments:
|
|
28
|
+
attributes['SKIPPED-SEGMENTS'].to_i,
|
|
29
|
+
recently_removed_dateranges:
|
|
30
|
+
attributes['RECENTLY-REMOVED-DATERANGES']
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Render as an m3u8 EXT-X-SKIP tag.
|
|
35
|
+
# @return [String]
|
|
36
|
+
def to_s
|
|
37
|
+
"#EXT-X-SKIP:#{formatted_attributes}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def formatted_attributes
|
|
43
|
+
[unquoted_format('SKIPPED-SEGMENTS', skipped_segments),
|
|
44
|
+
quoted_format('RECENTLY-REMOVED-DATERANGES',
|
|
45
|
+
recently_removed_dateranges)].compact.join(',')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/m3u8/time_item.rb
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module M3u8
|
|
3
4
|
# TimeItem represents EXT-X-PROGRAM-DATE-TIME
|
|
4
5
|
class TimeItem
|
|
5
6
|
extend M3u8
|
|
7
|
+
|
|
8
|
+
# @return [Time, String, nil] program date-time value
|
|
6
9
|
attr_accessor :time
|
|
7
10
|
|
|
11
|
+
# @param params [Hash] :time value
|
|
8
12
|
def initialize(params = {})
|
|
9
13
|
params.each do |key, value|
|
|
10
14
|
instance_variable_set("@#{key}", value)
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
|
|
18
|
+
# Parse an EXT-X-PROGRAM-DATE-TIME tag.
|
|
19
|
+
# @param text [String] raw tag line
|
|
20
|
+
# @return [TimeItem]
|
|
14
21
|
def self.parse(text)
|
|
15
22
|
time = text.gsub('#EXT-X-PROGRAM-DATE-TIME:', '')
|
|
16
23
|
options = { time: Time.parse(time) }
|
|
17
24
|
TimeItem.new(options)
|
|
18
25
|
end
|
|
19
26
|
|
|
27
|
+
# Render as an m3u8 EXT-X-PROGRAM-DATE-TIME tag.
|
|
28
|
+
# @return [String]
|
|
20
29
|
def to_s
|
|
21
30
|
%(#EXT-X-PROGRAM-DATE-TIME:#{time_format})
|
|
22
31
|
end
|
|
@@ -25,6 +34,7 @@ module M3u8
|
|
|
25
34
|
|
|
26
35
|
def time_format
|
|
27
36
|
return time if time.is_a?(String)
|
|
37
|
+
|
|
28
38
|
time.iso8601
|
|
29
39
|
end
|
|
30
40
|
end
|
data/lib/m3u8/version.rb
CHANGED