m3u8 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +77 -0
- data/lib/m3u8/date_range_item.rb +12 -0
- data/lib/m3u8/playlist.rb +16 -1
- data/lib/m3u8/reader.rb +1 -1
- data/lib/m3u8/scte35.rb +129 -0
- data/lib/m3u8/scte35_bit_reader.rb +51 -0
- data/lib/m3u8/scte35_segmentation_descriptor.rb +52 -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/version.rb +1 -1
- data/spec/lib/m3u8/builder_spec.rb +14 -0
- data/spec/lib/m3u8/date_range_item_spec.rb +47 -0
- data/spec/lib/m3u8/playlist_spec.rb +102 -0
- data/spec/lib/m3u8/reader_spec.rb +14 -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
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fb7826061b077f6e56712067f673393591d628e4e35a895af1910ff7fae859d
|
|
4
|
+
data.tar.gz: 8e0298d7fb9806e4fa2d3bc66b522a6f7bf7d0bbad1a57fcefeafeac7255a1f4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20c066c9f250d0d7948c3211f958bf553d8385d07f8715274b2738d3c497cf8cf450790489f80ccca88a1e09a67fc43411f055efcc16bf6f9fa07efed43ed407
|
|
7
|
+
data.tar.gz: 515ba07430d27872130625dc1d5eac52ebd68a76c20306ff4443b9829f74b3d3a7ceee8d63f118a7e832c6f48a7742e6157eb5bff05e26c198f262566f443c78
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
**1.6.0**
|
|
2
|
+
|
|
3
|
+
* Added SCTE-35 parsing with `M3u8::Scte35` for `splice_info_section`
|
|
4
|
+
payloads, including `splice_null` (0x00), `splice_insert` (0x05),
|
|
5
|
+
`time_signal` (0x06), and descriptor loop parsing.
|
|
6
|
+
* Added `DateRangeItem` SCTE-35 convenience methods:
|
|
7
|
+
`scte35_cmd_info`, `scte35_out_info`, and `scte35_in_info`.
|
|
8
|
+
* Added parsing support for SCTE-35 segmentation descriptors
|
|
9
|
+
(`segmentation_descriptor`, tag 0x02 with `CUEI` identifier).
|
|
10
|
+
* Added `M3u8::Scte35::ParseError` for malformed SCTE-35 payloads.
|
|
11
|
+
* Documented SCTE-35 usage and parsed fields in README.
|
|
12
|
+
|
|
13
|
+
***
|
|
14
|
+
|
|
15
|
+
**1.5.0**
|
|
16
|
+
|
|
17
|
+
* Added `Playlist#freeze` for deep-freezing playlists, items, nested objects, and playlist-level objects. `Playlist.build` and `Playlist.read` now return frozen playlists. `Playlist.new` remains mutable until `freeze` is called explicitly.
|
|
18
|
+
|
|
19
|
+
***
|
|
20
|
+
|
|
1
21
|
**1.4.0**
|
|
2
22
|
|
|
3
23
|
* Added `Playlist#errors` method returning an array of validation error messages. `Playlist#valid?` now delegates to `errors.empty?`. Validates mixed item types, target duration, segment items, playlist items, media items, encryption keys, session keys, session data, and LL-HLS part items.
|
data/README.md
CHANGED
|
@@ -344,6 +344,83 @@ options = { width: 1920, height: 1080, codecs: 'avc1.66.30,mp4a.40.2',
|
|
|
344
344
|
item = M3u8::PlaylistItem.new(options)
|
|
345
345
|
```
|
|
346
346
|
|
|
347
|
+
## Frozen playlists
|
|
348
|
+
|
|
349
|
+
Playlists returned by `Playlist.build` and `Playlist.read` are frozen (deeply immutable). Items, nested objects, and the items array are all frozen, preventing accidental mutation after construction:
|
|
350
|
+
|
|
351
|
+
```ruby
|
|
352
|
+
playlist = M3u8::Playlist.read(File.open('master.m3u8'))
|
|
353
|
+
playlist.frozen? # => true
|
|
354
|
+
playlist.items.frozen? # => true
|
|
355
|
+
playlist.items.first.frozen? # => true
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Playlists created with `Playlist.new` remain mutable. Call `freeze` explicitly when ready:
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
playlist = M3u8::Playlist.new
|
|
362
|
+
playlist.items << M3u8::SegmentItem.new(duration: 10.0, segment: 'test.ts')
|
|
363
|
+
playlist.freeze
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Frozen playlists still support `to_s` and `write` for output.
|
|
367
|
+
|
|
368
|
+
## SCTE-35 parsing
|
|
369
|
+
|
|
370
|
+
`DateRangeItem` stores SCTE-35 values (`scte35_cmd`, `scte35_out`, `scte35_in`) as raw hex strings. Convenience methods parse them into structured objects:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
playlist = M3u8::Playlist.read(file)
|
|
374
|
+
date_range = playlist.date_ranges.first
|
|
375
|
+
|
|
376
|
+
info = date_range.scte35_out_info
|
|
377
|
+
info.table_id # => 252 (0xFC)
|
|
378
|
+
info.pts_adjustment # => 0
|
|
379
|
+
info.tier # => 4095
|
|
380
|
+
info.splice_command_type # => 5
|
|
381
|
+
|
|
382
|
+
cmd = info.splice_command # => Scte35SpliceInsert
|
|
383
|
+
cmd.splice_event_id # => 1
|
|
384
|
+
cmd.out_of_network_indicator # => true
|
|
385
|
+
cmd.pts_time # => 90000
|
|
386
|
+
cmd.break_duration # => 2700000
|
|
387
|
+
cmd.break_auto_return # => true
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Parse any SCTE-35 hex string directly:
|
|
391
|
+
|
|
392
|
+
```ruby
|
|
393
|
+
info = M3u8::Scte35.parse('0xFC301100...')
|
|
394
|
+
info.to_s # => original hex string
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Command types
|
|
398
|
+
|
|
399
|
+
| Type | Class | Key attributes |
|
|
400
|
+
|------|-------|----------------|
|
|
401
|
+
| 0x00 | `Scte35SpliceNull` | *(none)* |
|
|
402
|
+
| 0x05 | `Scte35SpliceInsert` | `splice_event_id`, `out_of_network_indicator`, `pts_time`, `break_duration`, `break_auto_return`, `unique_program_id`, `avail_num`, `avails_expected` |
|
|
403
|
+
| 0x06 | `Scte35TimeSignal` | `pts_time` |
|
|
404
|
+
|
|
405
|
+
Unknown command types store raw bytes in `splice_command`.
|
|
406
|
+
|
|
407
|
+
### Descriptors
|
|
408
|
+
|
|
409
|
+
Segmentation descriptors (tag 0x02, identifier `CUEI`) are parsed as `Scte35SegmentationDescriptor`:
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
desc = info.descriptors.first
|
|
413
|
+
desc.segmentation_event_id # => 1
|
|
414
|
+
desc.segmentation_type_id # => 0x30
|
|
415
|
+
desc.segmentation_duration # => 2700000
|
|
416
|
+
desc.segmentation_upid_type # => 9
|
|
417
|
+
desc.segmentation_upid # => "SIGNAL123"
|
|
418
|
+
desc.segment_num # => 0
|
|
419
|
+
desc.segments_expected # => 0
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Unknown descriptor tags store raw bytes.
|
|
423
|
+
|
|
347
424
|
## Validation
|
|
348
425
|
|
|
349
426
|
Check whether a playlist is valid and inspect specific errors:
|
data/lib/m3u8/date_range_item.rb
CHANGED
|
@@ -34,6 +34,18 @@ module M3u8
|
|
|
34
34
|
"#EXT-X-DATERANGE:#{formatted_attributes}"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def scte35_cmd_info
|
|
38
|
+
Scte35.parse(scte35_cmd) unless scte35_cmd.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def scte35_out_info
|
|
42
|
+
Scte35.parse(scte35_out) unless scte35_out.nil?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def scte35_in_info
|
|
46
|
+
Scte35.parse(scte35_in) unless scte35_in.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
37
49
|
private
|
|
38
50
|
|
|
39
51
|
def formatted_attributes
|
data/lib/m3u8/playlist.rb
CHANGED
|
@@ -22,7 +22,7 @@ module M3u8
|
|
|
22
22
|
else
|
|
23
23
|
builder.instance_eval(&block)
|
|
24
24
|
end
|
|
25
|
-
playlist
|
|
25
|
+
playlist.freeze
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.codecs(options = {})
|
|
@@ -53,6 +53,14 @@ module M3u8
|
|
|
53
53
|
playlist_size.positive?
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def freeze
|
|
57
|
+
items.each { |item| freeze_item(item) }
|
|
58
|
+
items.freeze
|
|
59
|
+
part_inf&.freeze
|
|
60
|
+
server_control&.freeze
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
|
|
56
64
|
def to_s
|
|
57
65
|
output = StringIO.open
|
|
58
66
|
write(output)
|
|
@@ -119,6 +127,13 @@ module M3u8
|
|
|
119
127
|
|
|
120
128
|
private
|
|
121
129
|
|
|
130
|
+
def freeze_item(item)
|
|
131
|
+
item.byterange&.freeze if item.respond_to?(:byterange)
|
|
132
|
+
item.program_date_time&.freeze if item.respond_to?(:program_date_time)
|
|
133
|
+
item.client_attributes&.freeze if item.respond_to?(:client_attributes)
|
|
134
|
+
item.freeze
|
|
135
|
+
end
|
|
136
|
+
|
|
122
137
|
def assign_options(options)
|
|
123
138
|
options = defaults.merge(options)
|
|
124
139
|
|
data/lib/m3u8/reader.rb
CHANGED
data/lib/m3u8/scte35.rb
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
return header[:splice_command_length] unless header[:splice_command_length] == 0xFFF
|
|
114
|
+
|
|
115
|
+
if unknown_command_with_unspecified_length?(header)
|
|
116
|
+
# Unknown command: consume everything up to CRC
|
|
117
|
+
reader.bytes_remaining - 4
|
|
118
|
+
else
|
|
119
|
+
# Known command types parse their own fields; length unused
|
|
120
|
+
0
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private_class_method :parse_header, :parse_command,
|
|
125
|
+
:parse_descriptors, :command_data_length,
|
|
126
|
+
:parse_descriptor_loop, :parse_single_descriptor,
|
|
127
|
+
:unknown_command_with_unspecified_length?
|
|
128
|
+
end
|
|
129
|
+
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,52 @@
|
|
|
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] = reader.read_bytes(upid_length).force_encoding('UTF-8')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private_class_method :parse_segmentation_detail, :parse_upid
|
|
51
|
+
end
|
|
52
|
+
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/version.rb
CHANGED
|
@@ -191,6 +191,20 @@ describe M3u8::Builder do
|
|
|
191
191
|
expect(playlist.target).to eq(12)
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
+
it 'returns a frozen playlist' do
|
|
195
|
+
playlist = M3u8::Playlist.build do
|
|
196
|
+
segment duration: 10.0, segment: 'test.ts'
|
|
197
|
+
end
|
|
198
|
+
expect(playlist).to be_frozen
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'returns a frozen playlist with yielded form' do
|
|
202
|
+
playlist = M3u8::Playlist.build do |b|
|
|
203
|
+
b.segment duration: 10.0, segment: 'test.ts'
|
|
204
|
+
end
|
|
205
|
+
expect(playlist).to be_frozen
|
|
206
|
+
end
|
|
207
|
+
|
|
194
208
|
it 'supports yielded builder form' do
|
|
195
209
|
files = %w[seg1.ts seg2.ts]
|
|
196
210
|
playlist = M3u8::Playlist.build(version: 4) do |b|
|
|
@@ -122,4 +122,51 @@ describe M3u8::DateRangeItem do
|
|
|
122
122
|
expect(item.to_s).to eq(expected)
|
|
123
123
|
end
|
|
124
124
|
end
|
|
125
|
+
|
|
126
|
+
describe '#scte35_out_info' do
|
|
127
|
+
it 'should return parsed Scte35 when scte35_out is set' do
|
|
128
|
+
hex = '0xFC301100000000000000FFF000000000DEADBEEF'
|
|
129
|
+
item = described_class.new(scte35_out: hex)
|
|
130
|
+
result = item.scte35_out_info
|
|
131
|
+
|
|
132
|
+
expect(result).to be_a(M3u8::Scte35)
|
|
133
|
+
expect(result.table_id).to eq(0xFC)
|
|
134
|
+
expect(result.to_s).to eq(hex)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'should return nil when scte35_out is nil' do
|
|
138
|
+
item = described_class.new
|
|
139
|
+
expect(item.scte35_out_info).to be_nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
describe '#scte35_in_info' do
|
|
144
|
+
it 'should return parsed Scte35 when scte35_in is set' do
|
|
145
|
+
hex = '0xFC301100000000000000FFF000000000DEADBEEF'
|
|
146
|
+
item = described_class.new(scte35_in: hex)
|
|
147
|
+
result = item.scte35_in_info
|
|
148
|
+
|
|
149
|
+
expect(result).to be_a(M3u8::Scte35)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
it 'should return nil when scte35_in is nil' do
|
|
153
|
+
item = described_class.new
|
|
154
|
+
expect(item.scte35_in_info).to be_nil
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#scte35_cmd_info' do
|
|
159
|
+
it 'should return parsed Scte35 when scte35_cmd is set' do
|
|
160
|
+
hex = '0xFC301100000000000000FFF000000000DEADBEEF'
|
|
161
|
+
item = described_class.new(scte35_cmd: hex)
|
|
162
|
+
result = item.scte35_cmd_info
|
|
163
|
+
|
|
164
|
+
expect(result).to be_a(M3u8::Scte35)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
it 'should return nil when scte35_cmd is nil' do
|
|
168
|
+
item = described_class.new
|
|
169
|
+
expect(item.scte35_cmd_info).to be_nil
|
|
170
|
+
end
|
|
171
|
+
end
|
|
125
172
|
end
|