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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df37c1a4cc89e0bd918c6137c5bc86682d61638b407697240e76f40acb8983e7
4
- data.tar.gz: e19fd10c1b3f732057285247f08bb235573fa6434085295b53a76be22f1a53aa
3
+ metadata.gz: 1fb7826061b077f6e56712067f673393591d628e4e35a895af1910ff7fae859d
4
+ data.tar.gz: 8e0298d7fb9806e4fa2d3bc66b522a6f7bf7d0bbad1a57fcefeafeac7255a1f4
5
5
  SHA512:
6
- metadata.gz: c4ff8a727525925718fcb0ff7fed0b8bb758556d8e262c97ebacaca4fe96550b4494817ce5d8f4a0525d961a274a1905b16af6b5a85b8ac57235254f54392f8a
7
- data.tar.gz: 19ae07aa8cd202846254983b823973c4b7d0a9d4bde50e5cc8be7e1e4d91182205bec84731f628298f2659085434716120135a074656e1c9c2aa2c1988415b4f
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:
@@ -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
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # Represents a splice_null SCTE-35 command (type 0x00).
5
+ # Contains no data fields.
6
+ class Scte35SpliceNull # rubocop:disable Lint/EmptyClass
7
+ end
8
+ 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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
4
4
  module M3u8
5
- VERSION = '1.5.0'
5
+ VERSION = '1.6.0'
6
6
  end
@@ -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.5.0
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