m3u8 1.5.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 +14 -0
- data/README.md +56 -0
- data/lib/m3u8/date_range_item.rb +12 -0
- 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/date_range_item_spec.rb +47 -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,17 @@
|
|
|
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
|
+
|
|
1
15
|
**1.5.0**
|
|
2
16
|
|
|
3
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.
|
data/README.md
CHANGED
|
@@ -365,6 +365,62 @@ playlist.freeze
|
|
|
365
365
|
|
|
366
366
|
Frozen playlists still support `to_s` and `write` for output.
|
|
367
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
|
+
|
|
368
424
|
## Validation
|
|
369
425
|
|
|
370
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/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
|
@@ -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
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Scte35BitReader do
|
|
6
|
+
describe '#read_bits' do
|
|
7
|
+
it 'should read 8-bit values' do
|
|
8
|
+
reader = described_class.new("\xAB")
|
|
9
|
+
expect(reader.read_bits(8)).to eq(0xAB)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'should read 1-bit values' do
|
|
13
|
+
reader = described_class.new("\xA0")
|
|
14
|
+
expect(reader.read_bits(1)).to eq(1)
|
|
15
|
+
expect(reader.read_bits(1)).to eq(0)
|
|
16
|
+
expect(reader.read_bits(1)).to eq(1)
|
|
17
|
+
expect(reader.read_bits(1)).to eq(0)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it 'should read 12-bit values' do
|
|
21
|
+
reader = described_class.new("\xAB\xCD")
|
|
22
|
+
expect(reader.read_bits(12)).to eq(0xABC)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'should read 16-bit values' do
|
|
26
|
+
reader = described_class.new("\xAB\xCD")
|
|
27
|
+
expect(reader.read_bits(16)).to eq(0xABCD)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'should read 32-bit values' do
|
|
31
|
+
reader = described_class.new("\x12\x34\x56\x78")
|
|
32
|
+
expect(reader.read_bits(32)).to eq(0x12345678)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'should read 33-bit values' do
|
|
36
|
+
# 1 followed by 32 zeros = 2^32
|
|
37
|
+
reader = described_class.new("\x80\x00\x00\x00\x00")
|
|
38
|
+
expect(reader.read_bits(33)).to eq(2**32)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'should read 33-bit values after partial byte reads' do
|
|
42
|
+
# Simulates: time_specified(1), reserved(6), pts_time(33)
|
|
43
|
+
# 1_111111_0 00000000 00000000 00000000 00000001
|
|
44
|
+
reader = described_class.new("\xFE\x00\x00\x00\x01")
|
|
45
|
+
expect(reader.read_bits(1)).to eq(1)
|
|
46
|
+
expect(reader.read_bits(6)).to eq(0x3F)
|
|
47
|
+
expect(reader.read_bits(33)).to eq(1)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it 'should read values across byte boundaries' do
|
|
51
|
+
reader = described_class.new("\xF0\x0F")
|
|
52
|
+
expect(reader.read_bits(4)).to eq(0xF)
|
|
53
|
+
expect(reader.read_bits(8)).to eq(0x00)
|
|
54
|
+
expect(reader.read_bits(4)).to eq(0xF)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
describe '#read_flag' do
|
|
59
|
+
it 'should return true for 1' do
|
|
60
|
+
reader = described_class.new("\x80")
|
|
61
|
+
expect(reader.read_flag).to be true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'should return false for 0' do
|
|
65
|
+
reader = described_class.new("\x00")
|
|
66
|
+
expect(reader.read_flag).to be false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe '#read_bytes' do
|
|
71
|
+
it 'should read raw bytes' do
|
|
72
|
+
reader = described_class.new("\xDE\xAD\xBE\xEF")
|
|
73
|
+
expect(reader.read_bytes(2)).to eq("\xDE\xAD".b)
|
|
74
|
+
expect(reader.read_bytes(2)).to eq("\xBE\xEF".b)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#skip_bits' do
|
|
79
|
+
it 'should advance the position' do
|
|
80
|
+
reader = described_class.new("\xFF\x42")
|
|
81
|
+
reader.skip_bits(8)
|
|
82
|
+
expect(reader.read_bits(8)).to eq(0x42)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
it 'should skip sub-byte amounts' do
|
|
86
|
+
reader = described_class.new("\xF5")
|
|
87
|
+
reader.skip_bits(4)
|
|
88
|
+
expect(reader.read_bits(4)).to eq(0x5)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#bytes_remaining' do
|
|
93
|
+
it 'should return remaining bytes' do
|
|
94
|
+
reader = described_class.new("\x01\x02\x03\x04")
|
|
95
|
+
expect(reader.bytes_remaining).to eq(4)
|
|
96
|
+
reader.read_bits(8)
|
|
97
|
+
expect(reader.bytes_remaining).to eq(3)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'should account for partial byte reads' do
|
|
101
|
+
reader = described_class.new("\x01\x02")
|
|
102
|
+
reader.read_bits(4)
|
|
103
|
+
expect(reader.bytes_remaining).to eq(1)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Scte35SegmentationDescriptor do
|
|
6
|
+
# Builds a full SCTE-35 hex string with a time_signal command and descriptors
|
|
7
|
+
def build_hex_with_descriptors(descriptor_bytes)
|
|
8
|
+
# time_signal with pts=90000: FE 00 01 5F 90 (5 bytes, type 0x06)
|
|
9
|
+
cmd_bytes = 'FE00015F90'
|
|
10
|
+
cmd_len = 5
|
|
11
|
+
desc_loop_length = descriptor_bytes.length
|
|
12
|
+
desc_hex = descriptor_bytes.map { |b| format('%02X', b) }.join
|
|
13
|
+
desc_loop_hex = format('%04X', desc_loop_length)
|
|
14
|
+
|
|
15
|
+
# section_length = 11(header) + cmd_len + 2(desc_loop_len) + desc_loop + 4(CRC)
|
|
16
|
+
section_length = 11 + cmd_len + 2 + desc_loop_length + 4
|
|
17
|
+
header = splice_info_header(section_length, cmd_len)
|
|
18
|
+
|
|
19
|
+
"0x#{header}06#{cmd_bytes}#{desc_loop_hex}#{desc_hex}DEADBEEF"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def splice_info_header(section_length, cmd_length)
|
|
23
|
+
section_header = format('%04X', 0x3000 | section_length)
|
|
24
|
+
tier_cmd = format('%06X', (0xFFF << 12) | cmd_length)
|
|
25
|
+
"FC#{section_header}00000000000000#{tier_cmd}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Builds segmentation_descriptor bytes (tag=0x02, identifier=CUEI)
|
|
29
|
+
def segmentation_descriptor(fields)
|
|
30
|
+
# descriptor_tag(8): 0x02
|
|
31
|
+
# descriptor_length(8): computed
|
|
32
|
+
# identifier(32): CUEI = 0x43554549
|
|
33
|
+
# segmentation_event_id(32)
|
|
34
|
+
# segmentation_event_cancel_indicator(1) + reserved(7)
|
|
35
|
+
# if not cancelled:
|
|
36
|
+
# program_segmentation_flag(1)=1 + segmentation_duration_flag(1)
|
|
37
|
+
# + delivery_not_restricted_flag(1)=1 + reserved(5)=11111
|
|
38
|
+
# if duration_flag: segmentation_duration(40)
|
|
39
|
+
# segmentation_upid_type(8) + segmentation_upid_length(8)
|
|
40
|
+
# + upid data
|
|
41
|
+
# segmentation_type_id(8) + segment_num(8) + segments_expected(8)
|
|
42
|
+
bytes = [0x43, 0x55, 0x45, 0x49] # CUEI identifier
|
|
43
|
+
bytes += to_bytes(fields[:event_id], 4)
|
|
44
|
+
|
|
45
|
+
if fields[:cancel]
|
|
46
|
+
bytes += [0xFF] # cancel=1, reserved=1111111
|
|
47
|
+
else
|
|
48
|
+
bytes << 0x7F # cancel=0, reserved=1111111
|
|
49
|
+
duration_flag = fields[:duration] ? 1 : 0
|
|
50
|
+
# program_seg=1, duration_flag, delivery_not_restricted=1, reserved=11111
|
|
51
|
+
flags = (1 << 7) | (duration_flag << 6) | (1 << 5) | 0x1F
|
|
52
|
+
bytes << flags
|
|
53
|
+
|
|
54
|
+
if fields[:duration]
|
|
55
|
+
bytes += to_bytes(fields[:duration], 5) # 40-bit duration
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
upid = fields[:upid] || ''
|
|
59
|
+
bytes << (fields[:upid_type] || 0)
|
|
60
|
+
bytes << upid.length
|
|
61
|
+
bytes += upid.bytes if upid.length.positive?
|
|
62
|
+
|
|
63
|
+
bytes << fields[:type_id]
|
|
64
|
+
bytes << (fields[:segment_num] || 0)
|
|
65
|
+
bytes << (fields[:segments_expected] || 0)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Prepend tag and length
|
|
69
|
+
[0x02, bytes.length] + bytes
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def to_bytes(value, count)
|
|
73
|
+
count.times.map { |i| (value >> (8 * (count - 1 - i))) & 0xFF }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe 'parsing via Scte35.parse' do
|
|
77
|
+
it 'should parse a segmentation descriptor with ad-start type' do
|
|
78
|
+
desc = segmentation_descriptor(
|
|
79
|
+
event_id: 0x00000001, type_id: 0x30,
|
|
80
|
+
segment_num: 0, segments_expected: 0
|
|
81
|
+
)
|
|
82
|
+
hex = build_hex_with_descriptors(desc)
|
|
83
|
+
result = M3u8::Scte35.parse(hex)
|
|
84
|
+
|
|
85
|
+
expect(result.descriptors.length).to eq(1)
|
|
86
|
+
seg = result.descriptors.first
|
|
87
|
+
expect(seg).to be_a(described_class)
|
|
88
|
+
expect(seg.segmentation_event_id).to eq(1)
|
|
89
|
+
expect(seg.segmentation_type_id).to eq(0x30)
|
|
90
|
+
expect(seg.segment_num).to eq(0)
|
|
91
|
+
expect(seg.segments_expected).to eq(0)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'should parse a segmentation descriptor with duration' do
|
|
95
|
+
desc = segmentation_descriptor(
|
|
96
|
+
event_id: 0x00000002, type_id: 0x34,
|
|
97
|
+
duration: 2_700_000, segment_num: 1, segments_expected: 2
|
|
98
|
+
)
|
|
99
|
+
hex = build_hex_with_descriptors(desc)
|
|
100
|
+
result = M3u8::Scte35.parse(hex)
|
|
101
|
+
seg = result.descriptors.first
|
|
102
|
+
|
|
103
|
+
expect(seg.segmentation_duration).to eq(2_700_000)
|
|
104
|
+
expect(seg.segmentation_type_id).to eq(0x34)
|
|
105
|
+
expect(seg.segment_num).to eq(1)
|
|
106
|
+
expect(seg.segments_expected).to eq(2)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it 'should parse a segmentation descriptor with UPID' do
|
|
110
|
+
desc = segmentation_descriptor(
|
|
111
|
+
event_id: 0x00000003, type_id: 0x30,
|
|
112
|
+
upid_type: 0x09, upid: 'SIGNAL123'
|
|
113
|
+
)
|
|
114
|
+
hex = build_hex_with_descriptors(desc)
|
|
115
|
+
result = M3u8::Scte35.parse(hex)
|
|
116
|
+
seg = result.descriptors.first
|
|
117
|
+
|
|
118
|
+
expect(seg.segmentation_upid_type).to eq(0x09)
|
|
119
|
+
expect(seg.segmentation_upid).to eq('SIGNAL123')
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it 'should parse a cancelled segmentation descriptor' do
|
|
123
|
+
desc = segmentation_descriptor(event_id: 0x00000004, cancel: true)
|
|
124
|
+
hex = build_hex_with_descriptors(desc)
|
|
125
|
+
result = M3u8::Scte35.parse(hex)
|
|
126
|
+
seg = result.descriptors.first
|
|
127
|
+
|
|
128
|
+
expect(seg.segmentation_event_id).to eq(4)
|
|
129
|
+
expect(seg.segmentation_event_cancel_indicator).to be true
|
|
130
|
+
expect(seg.segmentation_type_id).to be_nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'should store raw bytes for unknown descriptor tags' do
|
|
134
|
+
# Unknown tag 0xFF with 4 bytes of identifier + 2 bytes data
|
|
135
|
+
unknown_desc = [0xFF, 0x06, 0x43, 0x55, 0x45, 0x49, 0xAA, 0xBB]
|
|
136
|
+
hex = build_hex_with_descriptors(unknown_desc)
|
|
137
|
+
result = M3u8::Scte35.parse(hex)
|
|
138
|
+
|
|
139
|
+
expect(result.descriptors.length).to eq(1)
|
|
140
|
+
expect(result.descriptors.first).to be_a(String)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Scte35 do
|
|
6
|
+
describe '.parse' do
|
|
7
|
+
it 'should parse a splice_null command' do
|
|
8
|
+
# splice_info_section with splice_null (type 0x00):
|
|
9
|
+
# table_id=0xFC, section_length=17, protocol=0, pts_adjustment=0
|
|
10
|
+
# cw_index=0, tier=0xFFF, command_length=0, command_type=0x00
|
|
11
|
+
# descriptor_loop_length=0, crc=0xDEADBEEF
|
|
12
|
+
hex = '0xFC301100000000000000FFF000000000DEADBEEF'
|
|
13
|
+
result = described_class.parse(hex)
|
|
14
|
+
|
|
15
|
+
expect(result.table_id).to eq(0xFC)
|
|
16
|
+
expect(result.pts_adjustment).to eq(0)
|
|
17
|
+
expect(result.tier).to eq(0xFFF)
|
|
18
|
+
expect(result.splice_command_type).to eq(0x00)
|
|
19
|
+
expect(result.splice_command).to be_a(M3u8::Scte35SpliceNull)
|
|
20
|
+
expect(result.descriptors).to eq([])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'should round-trip hex string via to_s' do
|
|
24
|
+
hex = '0xFC301100000000000000FFF000000000DEADBEEF'
|
|
25
|
+
result = described_class.parse(hex)
|
|
26
|
+
expect(result.to_s).to eq(hex)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'should store raw bytes for unknown command types' do
|
|
30
|
+
# command_type=0xFF with 2 bytes of command data (0xAABB)
|
|
31
|
+
# section_length=19 to accommodate extra 2 bytes
|
|
32
|
+
hex = '0xFC301300000000000000FFF002FFAABB0000DEADBEEF'
|
|
33
|
+
result = described_class.parse(hex)
|
|
34
|
+
|
|
35
|
+
expect(result.splice_command_type).to eq(0xFF)
|
|
36
|
+
expect(result.splice_command).to eq("\xAA\xBB".b)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'should parse nonzero pts_adjustment' do
|
|
40
|
+
# pts_adjustment=300 (0x12C): encrypted=0, algo=0, pts=300
|
|
41
|
+
# 0_000000_0_00000000_00000000_00000001_00101100
|
|
42
|
+
# bytes: 00 00 00 01 2C
|
|
43
|
+
hex = '0xFC301100000000012C00FFF000000000DEADBEEF'
|
|
44
|
+
result = described_class.parse(hex)
|
|
45
|
+
|
|
46
|
+
expect(result.pts_adjustment).to eq(300)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'should handle hex strings without 0x prefix' do
|
|
50
|
+
hex = 'FC301100000000000000FFF000000000DEADBEEF'
|
|
51
|
+
result = described_class.parse(hex)
|
|
52
|
+
|
|
53
|
+
expect(result.table_id).to eq(0xFC)
|
|
54
|
+
expect(result.splice_command_type).to eq(0x00)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'should parse descriptors with 0xFFF command length' do
|
|
58
|
+
# time_signal (type 0x06) with splice_command_length=0xFFF
|
|
59
|
+
# and a descriptor present
|
|
60
|
+
# time_signal: time_specified=1, pts=90000 (5 bytes)
|
|
61
|
+
# descriptor: tag=0xFF, length=6, identifier=CUEI, data=0xAABB
|
|
62
|
+
hex = '0xFC301E00000000000000FFFFFF06FE00015F900008FF0643554549AABBDEADBEEF'
|
|
63
|
+
result = described_class.parse(hex)
|
|
64
|
+
|
|
65
|
+
expect(result.splice_command_type).to eq(0x06)
|
|
66
|
+
expect(result.splice_command.pts_time).to eq(90_000)
|
|
67
|
+
expect(result.descriptors.length).to eq(1)
|
|
68
|
+
expect(result.descriptors.first).to be_a(String)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'should consume remaining bytes for unknown type with 0xFFF' do
|
|
72
|
+
# unknown command type 0xFF with 0xFFF length
|
|
73
|
+
# command data is everything up to CRC (no descriptors per spec)
|
|
74
|
+
# section_length = 11(header) + 3(command) + 4(CRC) = 18
|
|
75
|
+
hex = '0xFC301200000000000000FFFFFFFFAABBCCDEADBEEF'
|
|
76
|
+
result = described_class.parse(hex)
|
|
77
|
+
|
|
78
|
+
expect(result.splice_command_type).to eq(0xFF)
|
|
79
|
+
expect(result.splice_command).to eq("\xAA\xBB\xCC".b)
|
|
80
|
+
expect(result.descriptors).to eq([])
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'should raise ParseError for truncated data' do
|
|
84
|
+
hex = '0xFC30'
|
|
85
|
+
expect { described_class.parse(hex) }
|
|
86
|
+
.to raise_error(M3u8::Scte35::ParseError, /invalid SCTE-35 data/)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'should raise ParseError for empty hex string' do
|
|
90
|
+
expect { described_class.parse('0x') }
|
|
91
|
+
.to raise_error(M3u8::Scte35::ParseError, /invalid SCTE-35 data/)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Scte35SpliceInsert do
|
|
6
|
+
def build_splice_insert_hex(command_bytes, command_length: nil)
|
|
7
|
+
cmd_len = command_length || command_bytes.length
|
|
8
|
+
# section_length = 11(header) + cmd_len + 2(desc_loop) + 4(CRC)
|
|
9
|
+
section_length = 11 + cmd_len + 6
|
|
10
|
+
header = splice_info_header(section_length, cmd_len)
|
|
11
|
+
command_hex = command_bytes.map { |b| format('%02X', b) }.join
|
|
12
|
+
desc_and_crc = '0000DEADBEEF'
|
|
13
|
+
"0x#{header}05#{command_hex}#{desc_and_crc}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def splice_info_header(section_length, cmd_length)
|
|
17
|
+
table_id = 'FC'
|
|
18
|
+
section_header = format('%04X', 0x3000 | section_length)
|
|
19
|
+
proto_and_pts = '000000000000'
|
|
20
|
+
cw_index = '00'
|
|
21
|
+
tier_cmd = format('%06X', (0xFFF << 12) | cmd_length)
|
|
22
|
+
"#{table_id}#{section_header}#{proto_and_pts}#{cw_index}#{tier_cmd}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '.parse_from' do
|
|
26
|
+
it 'should parse an ad-start splice_insert' do
|
|
27
|
+
# splice_event_id(32): 0x00000001
|
|
28
|
+
# splice_event_cancel_indicator(1): 0
|
|
29
|
+
# reserved(7): 1111111
|
|
30
|
+
# out_of_network_indicator(1): 1
|
|
31
|
+
# program_splice_flag(1): 1
|
|
32
|
+
# duration_flag(1): 0
|
|
33
|
+
# splice_immediate_flag(1): 0
|
|
34
|
+
# reserved(4): 1111
|
|
35
|
+
# splice_time: time_specified(1)=1, reserved(6)=111111, pts(33)=90000
|
|
36
|
+
# unique_program_id(16): 0x0001
|
|
37
|
+
# avail_num(8): 0
|
|
38
|
+
# avails_expected(8): 0
|
|
39
|
+
#
|
|
40
|
+
# pts_time 90000 = 0x15F90:
|
|
41
|
+
# time_specified(1)=1, reserved(6)=111111, pts(33)=0x00015F90
|
|
42
|
+
# 1_111111_0_00000000_00000000_00010101_11111001_0000...
|
|
43
|
+
# Wait, 33 bits of 90000 = 0x15F90:
|
|
44
|
+
# binary: 0_00000000_00000001_01011111_10010000
|
|
45
|
+
# with prefix: 1_111111_0_00000000_00000001_01011111_10010000
|
|
46
|
+
# bytes: FE 00 00 01 5F 90 ... but that's 6 bytes (48 bits) for 40 bits
|
|
47
|
+
#
|
|
48
|
+
# Actually: 1+6+33 = 40 bits = 5 bytes:
|
|
49
|
+
# 1_111111_0 00000000 00000001 01011111 10010000
|
|
50
|
+
# = FE 00 01 5F 90
|
|
51
|
+
command_bytes = [
|
|
52
|
+
0x00, 0x00, 0x00, 0x01, # splice_event_id = 1
|
|
53
|
+
0x7F, # cancel=0, reserved=1111111
|
|
54
|
+
0xCF, # out=1, program=1, duration=0, immediate=0, reserved=1111
|
|
55
|
+
0xFE, 0x00, 0x01, 0x5F, 0x90, # splice_time: specified=1, pts=90000
|
|
56
|
+
0x00, 0x01, # unique_program_id = 1
|
|
57
|
+
0x00, # avail_num = 0
|
|
58
|
+
0x00 # avails_expected = 0
|
|
59
|
+
]
|
|
60
|
+
hex = build_splice_insert_hex(command_bytes)
|
|
61
|
+
result = M3u8::Scte35.parse(hex)
|
|
62
|
+
cmd = result.splice_command
|
|
63
|
+
|
|
64
|
+
expect(cmd).to be_a(described_class)
|
|
65
|
+
expect(cmd.splice_event_id).to eq(1)
|
|
66
|
+
expect(cmd.out_of_network_indicator).to be true
|
|
67
|
+
expect(cmd.pts_time).to eq(90_000)
|
|
68
|
+
expect(cmd.unique_program_id).to eq(1)
|
|
69
|
+
expect(cmd.avail_num).to eq(0)
|
|
70
|
+
expect(cmd.avails_expected).to eq(0)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'should parse a cancelled splice event' do
|
|
74
|
+
command_bytes = [
|
|
75
|
+
0x00, 0x00, 0x00, 0x02, # splice_event_id = 2
|
|
76
|
+
0xFF # cancel=1, reserved=1111111
|
|
77
|
+
]
|
|
78
|
+
hex = build_splice_insert_hex(command_bytes)
|
|
79
|
+
result = M3u8::Scte35.parse(hex)
|
|
80
|
+
cmd = result.splice_command
|
|
81
|
+
|
|
82
|
+
expect(cmd.splice_event_id).to eq(2)
|
|
83
|
+
expect(cmd.splice_event_cancel_indicator).to be true
|
|
84
|
+
expect(cmd.out_of_network_indicator).to be_nil
|
|
85
|
+
expect(cmd.pts_time).to be_nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'should parse splice_insert with break duration' do
|
|
89
|
+
# out=1, program=1, duration=1, immediate=0, reserved=1111
|
|
90
|
+
# splice_time: specified=1, pts=90000
|
|
91
|
+
# break_duration: auto_return(1)=1, reserved(6)=111111, duration(33)=2700000
|
|
92
|
+
# 2700000 = 0x293370
|
|
93
|
+
# 1_111111_0_00000000_00000000_00101001_00110011_01110000
|
|
94
|
+
# Wait, 33 bits: 0_00000000_00101001_00110011_01110000
|
|
95
|
+
# = 00 29 33 70 in the last 4 bytes, first byte has auto_return+reserved+MSB
|
|
96
|
+
# 1_111111_0 00000000 00101001 00110011 01110000
|
|
97
|
+
# = FE 00 29 33 70
|
|
98
|
+
command_bytes = [
|
|
99
|
+
0x00, 0x00, 0x00, 0x03, # splice_event_id = 3
|
|
100
|
+
0x7F, # cancel=0, reserved=1111111
|
|
101
|
+
0xEF, # out=1, program=1, duration=1, immediate=0, reserved=1111
|
|
102
|
+
0xFE, 0x00, 0x01, 0x5F, 0x90, # splice_time: specified=1, pts=90000
|
|
103
|
+
0xFE, 0x00, 0x29, 0x32, 0xE0, # break_duration: auto=1, dur=2700000
|
|
104
|
+
0x00, 0x01, # unique_program_id = 1
|
|
105
|
+
0x00, # avail_num = 0
|
|
106
|
+
0x02 # avails_expected = 2
|
|
107
|
+
]
|
|
108
|
+
hex = build_splice_insert_hex(command_bytes)
|
|
109
|
+
result = M3u8::Scte35.parse(hex)
|
|
110
|
+
cmd = result.splice_command
|
|
111
|
+
|
|
112
|
+
expect(cmd.break_duration).to eq(2_700_000)
|
|
113
|
+
expect(cmd.break_auto_return).to be true
|
|
114
|
+
expect(cmd.avails_expected).to eq(2)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'should parse an immediate splice (no PTS)' do
|
|
118
|
+
# out=1, program=1, duration=0, immediate=1, reserved=1111
|
|
119
|
+
command_bytes = [
|
|
120
|
+
0x00, 0x00, 0x00, 0x04, # splice_event_id = 4
|
|
121
|
+
0x7F, # cancel=0, reserved=1111111
|
|
122
|
+
0xDF, # out=1, program=1, duration=0, immediate=1, reserved=1111
|
|
123
|
+
0x00, 0x01, # unique_program_id = 1
|
|
124
|
+
0x00, # avail_num = 0
|
|
125
|
+
0x00 # avails_expected = 0
|
|
126
|
+
]
|
|
127
|
+
hex = build_splice_insert_hex(command_bytes)
|
|
128
|
+
result = M3u8::Scte35.parse(hex)
|
|
129
|
+
cmd = result.splice_command
|
|
130
|
+
|
|
131
|
+
expect(cmd.pts_time).to be_nil
|
|
132
|
+
expect(cmd.out_of_network_indicator).to be true
|
|
133
|
+
expect(cmd.splice_event_id).to eq(4)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
it 'should parse component mode splice_insert' do
|
|
137
|
+
# out=1, program=0, duration=0, immediate=0, reserved=1111
|
|
138
|
+
# 1_0_0_0_1111 = 0x8F
|
|
139
|
+
# component_count=1, component_tag=0x22,
|
|
140
|
+
# splice_time: specified=1, pts=90000
|
|
141
|
+
command_bytes = [
|
|
142
|
+
0x00, 0x00, 0x00, 0x01, # splice_event_id = 1
|
|
143
|
+
0x7F, # cancel=0, reserved=1111111
|
|
144
|
+
0x8F, # out=1, program=0, duration=0, immediate=0, reserved=1111
|
|
145
|
+
0x01, # component_count = 1
|
|
146
|
+
0x22, # component_tag = 0x22
|
|
147
|
+
0xFE, 0x00, 0x01, 0x5F, 0x90, # splice_time: specified=1, pts=90000
|
|
148
|
+
0x00, 0x01, # unique_program_id = 1
|
|
149
|
+
0x00, # avail_num = 0
|
|
150
|
+
0x00 # avails_expected = 0
|
|
151
|
+
]
|
|
152
|
+
hex = build_splice_insert_hex(command_bytes)
|
|
153
|
+
result = M3u8::Scte35.parse(hex)
|
|
154
|
+
cmd = result.splice_command
|
|
155
|
+
|
|
156
|
+
expect(cmd).to be_a(described_class)
|
|
157
|
+
expect(cmd.splice_event_id).to eq(1)
|
|
158
|
+
expect(cmd.out_of_network_indicator).to be true
|
|
159
|
+
expect(cmd.pts_time).to be_nil
|
|
160
|
+
expect(cmd.unique_program_id).to eq(1)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it 'should parse component mode with immediate flag' do
|
|
164
|
+
# out=1, program=0, duration=0, immediate=1, reserved=1111
|
|
165
|
+
# 1_0_0_1_1111 = 0x9F
|
|
166
|
+
command_bytes = [
|
|
167
|
+
0x00, 0x00, 0x00, 0x05, # splice_event_id = 5
|
|
168
|
+
0x7F, # cancel=0, reserved=1111111
|
|
169
|
+
0x9F, # out=1, program=0, duration=0, immediate=1, reserved=1111
|
|
170
|
+
0x02, # component_count = 2
|
|
171
|
+
0x10, # component_tag = 0x10
|
|
172
|
+
0x20, # component_tag = 0x20
|
|
173
|
+
0x00, 0x01, # unique_program_id = 1
|
|
174
|
+
0x00, # avail_num = 0
|
|
175
|
+
0x00 # avails_expected = 0
|
|
176
|
+
]
|
|
177
|
+
hex = build_splice_insert_hex(command_bytes)
|
|
178
|
+
result = M3u8::Scte35.parse(hex)
|
|
179
|
+
cmd = result.splice_command
|
|
180
|
+
|
|
181
|
+
expect(cmd.splice_event_id).to eq(5)
|
|
182
|
+
expect(cmd.unique_program_id).to eq(1)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Scte35SpliceNull do
|
|
6
|
+
describe '#new' do
|
|
7
|
+
it 'should create an empty splice null command' do
|
|
8
|
+
command = described_class.new
|
|
9
|
+
expect(command).to be_a(described_class)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Scte35TimeSignal do
|
|
6
|
+
def build_time_signal_hex(command_bytes, command_length: nil)
|
|
7
|
+
cmd_len = command_length || command_bytes.length
|
|
8
|
+
section_length = 11 + cmd_len + 6
|
|
9
|
+
header = splice_info_header(section_length, cmd_len)
|
|
10
|
+
command_hex = command_bytes.map { |b| format('%02X', b) }.join
|
|
11
|
+
"0x#{header}06#{command_hex}0000DEADBEEF"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def splice_info_header(section_length, cmd_length)
|
|
15
|
+
table_id = 'FC'
|
|
16
|
+
section_header = format('%04X', 0x3000 | section_length)
|
|
17
|
+
proto_and_pts = '000000000000'
|
|
18
|
+
cw_index = '00'
|
|
19
|
+
tier_cmd = format('%06X', (0xFFF << 12) | cmd_length)
|
|
20
|
+
"#{table_id}#{section_header}#{proto_and_pts}#{cw_index}#{tier_cmd}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.parse_from' do
|
|
24
|
+
it 'should parse time_signal with PTS time' do
|
|
25
|
+
# splice_time: time_specified(1)=1, reserved(6)=111111, pts(33)=90000
|
|
26
|
+
# 90000 = 0x15F90
|
|
27
|
+
# 1_111111_0_00000000_00000001_01011111_10010000
|
|
28
|
+
# = FE 00 01 5F 90
|
|
29
|
+
command_bytes = [0xFE, 0x00, 0x01, 0x5F, 0x90]
|
|
30
|
+
hex = build_time_signal_hex(command_bytes)
|
|
31
|
+
result = M3u8::Scte35.parse(hex)
|
|
32
|
+
cmd = result.splice_command
|
|
33
|
+
|
|
34
|
+
expect(cmd).to be_a(described_class)
|
|
35
|
+
expect(cmd.pts_time).to eq(90_000)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'should parse time_signal without time_specified' do
|
|
39
|
+
# time_specified(1)=0, reserved(7)=1111111
|
|
40
|
+
# = 0x7F (1 byte)
|
|
41
|
+
command_bytes = [0x7F]
|
|
42
|
+
hex = build_time_signal_hex(command_bytes)
|
|
43
|
+
result = M3u8::Scte35.parse(hex)
|
|
44
|
+
cmd = result.splice_command
|
|
45
|
+
|
|
46
|
+
expect(cmd).to be_a(described_class)
|
|
47
|
+
expect(cmd.pts_time).to be_nil
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: m3u8
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seth Deckard
|
|
@@ -167,6 +167,12 @@ files:
|
|
|
167
167
|
- lib/m3u8/preload_hint_item.rb
|
|
168
168
|
- lib/m3u8/reader.rb
|
|
169
169
|
- lib/m3u8/rendition_report_item.rb
|
|
170
|
+
- lib/m3u8/scte35.rb
|
|
171
|
+
- lib/m3u8/scte35_bit_reader.rb
|
|
172
|
+
- lib/m3u8/scte35_segmentation_descriptor.rb
|
|
173
|
+
- lib/m3u8/scte35_splice_insert.rb
|
|
174
|
+
- lib/m3u8/scte35_splice_null.rb
|
|
175
|
+
- lib/m3u8/scte35_time_signal.rb
|
|
170
176
|
- lib/m3u8/segment_item.rb
|
|
171
177
|
- lib/m3u8/server_control_item.rb
|
|
172
178
|
- lib/m3u8/session_data_item.rb
|
|
@@ -220,6 +226,12 @@ files:
|
|
|
220
226
|
- spec/lib/m3u8/reader_spec.rb
|
|
221
227
|
- spec/lib/m3u8/rendition_report_item_spec.rb
|
|
222
228
|
- spec/lib/m3u8/round_trip_spec.rb
|
|
229
|
+
- spec/lib/m3u8/scte35_bit_reader_spec.rb
|
|
230
|
+
- spec/lib/m3u8/scte35_segmentation_descriptor_spec.rb
|
|
231
|
+
- spec/lib/m3u8/scte35_spec.rb
|
|
232
|
+
- spec/lib/m3u8/scte35_splice_insert_spec.rb
|
|
233
|
+
- spec/lib/m3u8/scte35_splice_null_spec.rb
|
|
234
|
+
- spec/lib/m3u8/scte35_time_signal_spec.rb
|
|
223
235
|
- spec/lib/m3u8/segment_item_spec.rb
|
|
224
236
|
- spec/lib/m3u8/server_control_item_spec.rb
|
|
225
237
|
- spec/lib/m3u8/session_data_item_spec.rb
|