m3u8 1.6.0 → 1.7.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: 1fb7826061b077f6e56712067f673393591d628e4e35a895af1910ff7fae859d
4
- data.tar.gz: 8e0298d7fb9806e4fa2d3bc66b522a6f7bf7d0bbad1a57fcefeafeac7255a1f4
3
+ metadata.gz: c38180f01d16f00854720acaf6fc8359f2362417e19b8e45248a1573597716ca
4
+ data.tar.gz: b5192ed27a57a1f3a06d32f09d5d1fdbcd3c1e788d6bf515c68f739531ba8f23
5
5
  SHA512:
6
- metadata.gz: 20c066c9f250d0d7948c3211f958bf553d8385d07f8715274b2738d3c497cf8cf450790489f80ccca88a1e09a67fc43411f055efcc16bf6f9fa07efed43ed407
7
- data.tar.gz: 515ba07430d27872130625dc1d5eac52ebd68a76c20306ff4443b9829f74b3d3a7ceee8d63f118a7e832c6f48a7742e6157eb5bff05e26c198f262566f443c78
6
+ metadata.gz: be03dc5ca38934401073972dfab62b7ec35e4733eda199a91320b1b668e4046f04eae8518a720795d945bf7439da70ebe2d15d9bb53db3cd65cffedd3f70c0d9
7
+ data.tar.gz: a4faf2090ab7d3cd6c5788723a842d795903748e42a8ced6726af626ae81dc4373262074c2eeff70baae56c3b73c02d5a4a93d6caf84646d530092202c54dda1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ **1.7.0**
2
+
3
+ * Added HLS Interstitials first-class `DateRangeItem` accessors:
4
+ `asset_uri`, `asset_list`, `resume_offset`, `playout_limit`,
5
+ `restrict`, `snap`, `timeline_occupies`, `timeline_style`, and
6
+ `content_may_vary`.
7
+ * Promoted supported interstitial `X-` attributes out of
8
+ `client_attributes` into typed fields during parsing and formatting.
9
+ * Refactored `DateRangeItem` attribute formatting helpers to reduce
10
+ duplication while preserving output behavior.
11
+
12
+ ***
13
+
1
14
  **1.6.0**
2
15
 
3
16
  * Added SCTE-35 parsing with `M3u8::Scte35` for `splice_info_section`
data/README.md CHANGED
@@ -253,12 +253,43 @@ Insert a timed metadata date range:
253
253
  ```ruby
254
254
  item = M3u8::DateRangeItem.new(
255
255
  id: 'ad-break-1', start_date: '2024-06-01T12:00:00Z',
256
- planned_duration: 30.0,
256
+ planned_duration: 30.0, cue: 'PRE',
257
257
  client_attributes: { 'X-AD-ID' => '"foo"' }
258
258
  )
259
259
  playlist.items << item
260
260
  ```
261
261
 
262
+ #### HLS Interstitials
263
+
264
+ `DateRangeItem` supports [HLS Interstitials](https://developer.apple.com/documentation/http-live-streaming/providing-an-hls-interstitial) attributes as first-class accessors for ad insertion, pre/post-rolls, and timeline integration:
265
+
266
+ ```ruby
267
+ item = M3u8::DateRangeItem.new(
268
+ id: 'ad-break-1',
269
+ class_name: 'com.apple.hls.interstitial',
270
+ start_date: '2024-06-01T12:00:00Z',
271
+ asset_uri: 'http://example.com/ad.m3u8',
272
+ resume_offset: 0.0,
273
+ playout_limit: 30.0,
274
+ restrict: 'SKIP,JUMP',
275
+ snap: 'OUT',
276
+ content_may_vary: 'YES'
277
+ )
278
+ playlist.items << item
279
+ ```
280
+
281
+ | HLS Attribute | Accessor | Type |
282
+ |----------------------|---------------------|--------|
283
+ | X-ASSET-URI | `asset_uri` | String |
284
+ | X-ASSET-LIST | `asset_list` | String |
285
+ | X-RESUME-OFFSET | `resume_offset` | Float |
286
+ | X-PLAYOUT-LIMIT | `playout_limit` | Float |
287
+ | X-RESTRICT | `restrict` | String |
288
+ | X-SNAP | `snap` | String |
289
+ | X-TIMELINE-OCCUPIES | `timeline_occupies` | String |
290
+ | X-TIMELINE-STYLE | `timeline_style` | String |
291
+ | X-CONTENT-MAY-VARY | `content_may_vary` | String |
292
+
262
293
  Signal an encoding discontinuity:
263
294
 
264
295
  ```ruby
@@ -7,7 +7,17 @@ module M3u8
7
7
 
8
8
  attr_accessor :id, :class_name, :start_date, :end_date, :duration,
9
9
  :planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
10
- :end_on_next, :client_attributes
10
+ :cue, :end_on_next, :client_attributes,
11
+ :asset_uri, :asset_list, :resume_offset,
12
+ :playout_limit, :restrict, :snap,
13
+ :timeline_occupies, :timeline_style,
14
+ :content_may_vary
15
+
16
+ INTERSTITIAL_KEYS = %w[
17
+ X-ASSET-URI X-ASSET-LIST X-RESUME-OFFSET X-PLAYOUT-LIMIT
18
+ X-RESTRICT X-SNAP X-TIMELINE-OCCUPIES X-TIMELINE-STYLE
19
+ X-CONTENT-MAY-VARY
20
+ ].freeze
11
21
 
12
22
  def initialize(options = {})
13
23
  options.each do |key, value|
@@ -26,7 +36,9 @@ module M3u8
26
36
  @scte35_cmd = attributes['SCTE35-CMD']
27
37
  @scte35_out = attributes['SCTE35-OUT']
28
38
  @scte35_in = attributes['SCTE35-IN']
39
+ @cue = attributes['CUE']
29
40
  @end_on_next = attributes.key?('END-ON-NEXT')
41
+ parse_interstitials(attributes)
30
42
  @client_attributes = parse_client_attributes(attributes)
31
43
  end
32
44
 
@@ -56,34 +68,28 @@ module M3u8
56
68
  duration_format,
57
69
  planned_duration_format,
58
70
  client_attributes_format,
71
+ interstitial_formats,
59
72
  scte35_cmd_format,
60
73
  scte35_out_format,
61
74
  scte35_in_format,
62
- end_on_next_format].compact.join(',')
75
+ cue_format,
76
+ end_on_next_format].flatten.compact.join(',')
63
77
  end
64
78
 
65
79
  def class_name_format
66
- return if class_name.nil?
67
-
68
- %(CLASS="#{class_name}")
80
+ quoted_format('CLASS', class_name)
69
81
  end
70
82
 
71
83
  def end_date_format
72
- return if end_date.nil?
73
-
74
- %(END-DATE="#{end_date}")
84
+ quoted_format('END-DATE', end_date)
75
85
  end
76
86
 
77
87
  def duration_format
78
- return if duration.nil?
79
-
80
- "DURATION=#{duration}"
88
+ unquoted_format('DURATION', duration)
81
89
  end
82
90
 
83
91
  def planned_duration_format
84
- return if planned_duration.nil?
85
-
86
- "PLANNED-DURATION=#{planned_duration}"
92
+ unquoted_format('PLANNED-DURATION', planned_duration)
87
93
  end
88
94
 
89
95
  def client_attributes_format
@@ -107,22 +113,76 @@ module M3u8
107
113
  end
108
114
  end
109
115
 
110
- def scte35_cmd_format
111
- return if scte35_cmd.nil?
116
+ def parse_interstitials(attributes)
117
+ @asset_uri = attributes['X-ASSET-URI']
118
+ @asset_list = attributes['X-ASSET-LIST']
119
+ @resume_offset = parse_float(attributes['X-RESUME-OFFSET'])
120
+ @playout_limit = parse_float(attributes['X-PLAYOUT-LIMIT'])
121
+ @restrict = attributes['X-RESTRICT']
122
+ @snap = attributes['X-SNAP']
123
+ @timeline_occupies = attributes['X-TIMELINE-OCCUPIES']
124
+ @timeline_style = attributes['X-TIMELINE-STYLE']
125
+ @content_may_vary = attributes['X-CONTENT-MAY-VARY']
126
+ end
112
127
 
113
- "SCTE35-CMD=#{scte35_cmd}"
128
+ def interstitial_formats
129
+ [asset_uri_format, asset_list_format,
130
+ resume_offset_format, playout_limit_format,
131
+ restrict_format, snap_format,
132
+ timeline_occupies_format, timeline_style_format,
133
+ content_may_vary_format]
114
134
  end
115
135
 
116
- def scte35_out_format
117
- return if scte35_out.nil?
136
+ def asset_uri_format
137
+ quoted_format('X-ASSET-URI', asset_uri)
138
+ end
139
+
140
+ def asset_list_format
141
+ quoted_format('X-ASSET-LIST', asset_list)
142
+ end
143
+
144
+ def resume_offset_format
145
+ unquoted_format('X-RESUME-OFFSET', resume_offset)
146
+ end
147
+
148
+ def playout_limit_format
149
+ unquoted_format('X-PLAYOUT-LIMIT', playout_limit)
150
+ end
151
+
152
+ def restrict_format
153
+ quoted_format('X-RESTRICT', restrict)
154
+ end
155
+
156
+ def snap_format
157
+ quoted_format('X-SNAP', snap)
158
+ end
159
+
160
+ def timeline_occupies_format
161
+ quoted_format('X-TIMELINE-OCCUPIES', timeline_occupies)
162
+ end
163
+
164
+ def timeline_style_format
165
+ quoted_format('X-TIMELINE-STYLE', timeline_style)
166
+ end
118
167
 
119
- "SCTE35-OUT=#{scte35_out}"
168
+ def content_may_vary_format
169
+ quoted_format('X-CONTENT-MAY-VARY', content_may_vary)
170
+ end
171
+
172
+ def scte35_cmd_format
173
+ unquoted_format('SCTE35-CMD', scte35_cmd)
174
+ end
175
+
176
+ def scte35_out_format
177
+ unquoted_format('SCTE35-OUT', scte35_out)
120
178
  end
121
179
 
122
180
  def scte35_in_format
123
- return if scte35_in.nil?
181
+ unquoted_format('SCTE35-IN', scte35_in)
182
+ end
124
183
 
125
- "SCTE35-IN=#{scte35_in}"
184
+ def cue_format
185
+ quoted_format('CUE', cue)
126
186
  end
127
187
 
128
188
  def end_on_next_format
@@ -131,8 +191,18 @@ module M3u8
131
191
  'END-ON-NEXT=YES'
132
192
  end
133
193
 
194
+ def quoted_format(key, value)
195
+ %(#{key}="#{value}") unless value.nil?
196
+ end
197
+
198
+ def unquoted_format(key, value)
199
+ "#{key}=#{value}" unless value.nil?
200
+ end
201
+
134
202
  def parse_client_attributes(attributes)
135
- attributes.select { |key| key.start_with?('X-') }
203
+ attributes.select do |key|
204
+ key.start_with?('X-') && !INTERSTITIAL_KEYS.include?(key)
205
+ end
136
206
  end
137
207
  end
138
208
  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.6.0'
5
+ VERSION = '1.7.0'
6
6
  end
@@ -11,7 +11,17 @@ describe M3u8::DateRangeItem do
11
11
  planned_duration: 59.993,
12
12
  scte35_out: '0xFC002F0000000000FF0',
13
13
  scte35_in: '0xFC002F0000000000FF1',
14
- scte35_cmd: '0xFC002F0000000000FF2', end_on_next: true,
14
+ scte35_cmd: '0xFC002F0000000000FF2',
15
+ cue: 'PRE', end_on_next: true,
16
+ asset_uri: 'http://example.com/ad.m3u8',
17
+ asset_list: 'http://example.com/ads.json',
18
+ resume_offset: 10.5,
19
+ playout_limit: 30.0,
20
+ restrict: 'SKIP,JUMP',
21
+ snap: 'OUT',
22
+ timeline_occupies: 'RANGE',
23
+ timeline_style: 'HIGHLIGHT',
24
+ content_may_vary: 'YES',
15
25
  client_attributes: { 'X-CUSTOM' => 45.3 } }
16
26
  item = described_class.new(options)
17
27
 
@@ -24,7 +34,17 @@ describe M3u8::DateRangeItem do
24
34
  expect(item.scte35_out).to eq('0xFC002F0000000000FF0')
25
35
  expect(item.scte35_in).to eq('0xFC002F0000000000FF1')
26
36
  expect(item.scte35_cmd).to eq('0xFC002F0000000000FF2')
37
+ expect(item.cue).to eq('PRE')
27
38
  expect(item.end_on_next).to be true
39
+ expect(item.asset_uri).to eq('http://example.com/ad.m3u8')
40
+ expect(item.asset_list).to eq('http://example.com/ads.json')
41
+ expect(item.resume_offset).to eq(10.5)
42
+ expect(item.playout_limit).to eq(30.0)
43
+ expect(item.restrict).to eq('SKIP,JUMP')
44
+ expect(item.snap).to eq('OUT')
45
+ expect(item.timeline_occupies).to eq('RANGE')
46
+ expect(item.timeline_style).to eq('HIGHLIGHT')
47
+ expect(item.content_may_vary).to eq('YES')
28
48
  expect(item.client_attributes.empty?).to be false
29
49
  expect(item.client_attributes['X-CUSTOM']).to eq(45.3)
30
50
  end
@@ -33,12 +53,22 @@ describe M3u8::DateRangeItem do
33
53
  describe '#parse' do
34
54
  it 'should parse m3u8 tag into instance' do
35
55
  item = described_class.new
36
- line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class"' \
56
+ line = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",CLASS="test_class",' \
37
57
  'START-DATE="2014-03-05T11:15:00Z",' \
38
58
  'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
39
59
  'PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF0,' \
40
60
  'SCTE35-IN=0xFC002F0000000000FF1,' \
41
61
  'SCTE35-CMD=0xFC002F0000000000FF2,' \
62
+ 'X-ASSET-URI="http://example.com/ad.m3u8",' \
63
+ 'X-ASSET-LIST="http://example.com/ads.json",' \
64
+ 'X-RESUME-OFFSET=10.5,' \
65
+ 'X-PLAYOUT-LIMIT=30.0,' \
66
+ 'X-RESTRICT="SKIP,JUMP",' \
67
+ 'X-SNAP="OUT",' \
68
+ 'X-TIMELINE-OCCUPIES="RANGE",' \
69
+ 'X-TIMELINE-STYLE="HIGHLIGHT",' \
70
+ 'X-CONTENT-MAY-VARY="YES",' \
71
+ 'CUE="PRE",' \
42
72
  'END-ON-NEXT=YES'
43
73
  item.parse(line)
44
74
 
@@ -51,6 +81,16 @@ describe M3u8::DateRangeItem do
51
81
  expect(item.scte35_out).to eq('0xFC002F0000000000FF0')
52
82
  expect(item.scte35_in).to eq('0xFC002F0000000000FF1')
53
83
  expect(item.scte35_cmd).to eq('0xFC002F0000000000FF2')
84
+ expect(item.asset_uri).to eq('http://example.com/ad.m3u8')
85
+ expect(item.asset_list).to eq('http://example.com/ads.json')
86
+ expect(item.resume_offset).to eq(10.5)
87
+ expect(item.playout_limit).to eq(30.0)
88
+ expect(item.restrict).to eq('SKIP,JUMP')
89
+ expect(item.snap).to eq('OUT')
90
+ expect(item.timeline_occupies).to eq('RANGE')
91
+ expect(item.timeline_style).to eq('HIGHLIGHT')
92
+ expect(item.content_may_vary).to eq('YES')
93
+ expect(item.cue).to eq('PRE')
54
94
  expect(item.end_on_next).to be true
55
95
  expect(item.client_attributes.empty?).to be true
56
96
  end
@@ -93,20 +133,43 @@ describe M3u8::DateRangeItem do
93
133
  planned_duration: 59.993,
94
134
  scte35_out: '0xFC002F0000000000FF0',
95
135
  scte35_in: '0xFC002F0000000000FF1',
96
- scte35_cmd: '0xFC002F0000000000FF2', end_on_next: true,
136
+ scte35_cmd: '0xFC002F0000000000FF2',
137
+ asset_uri: 'http://example.com/ad.m3u8',
138
+ asset_list: 'http://example.com/ads.json',
139
+ resume_offset: 10.5,
140
+ playout_limit: 30.0,
141
+ restrict: 'SKIP,JUMP',
142
+ snap: 'OUT',
143
+ timeline_occupies: 'RANGE',
144
+ timeline_style: 'HIGHLIGHT',
145
+ content_may_vary: 'YES',
146
+ cue: 'POST,ONCE', end_on_next: true,
97
147
  client_attributes: { 'X-CUSTOM' => 45.3,
98
- 'X-CUSTOM-TEXT' => 'test_value' } }
148
+ 'X-CUSTOM-TEXT' =>
149
+ 'test_value' } }
99
150
  item = described_class.new(options)
100
151
 
101
- expected = '#EXT-X-DATERANGE:ID="test_id",CLASS="test_class",' \
152
+ expected = '#EXT-X-DATERANGE:ID="test_id",' \
153
+ 'CLASS="test_class",' \
102
154
  'START-DATE="2014-03-05T11:15:00Z",' \
103
- 'END-DATE="2014-03-05T11:16:00Z",DURATION=60.1,' \
155
+ 'END-DATE="2014-03-05T11:16:00Z",' \
156
+ 'DURATION=60.1,' \
104
157
  'PLANNED-DURATION=59.993,' \
105
158
  'X-CUSTOM=45.3,' \
106
159
  'X-CUSTOM-TEXT="test_value",' \
160
+ 'X-ASSET-URI="http://example.com/ad.m3u8",' \
161
+ 'X-ASSET-LIST="http://example.com/ads.json",' \
162
+ 'X-RESUME-OFFSET=10.5,' \
163
+ 'X-PLAYOUT-LIMIT=30.0,' \
164
+ 'X-RESTRICT="SKIP,JUMP",' \
165
+ 'X-SNAP="OUT",' \
166
+ 'X-TIMELINE-OCCUPIES="RANGE",' \
167
+ 'X-TIMELINE-STYLE="HIGHLIGHT",' \
168
+ 'X-CONTENT-MAY-VARY="YES",' \
107
169
  'SCTE35-CMD=0xFC002F0000000000FF2,' \
108
170
  'SCTE35-OUT=0xFC002F0000000000FF0,' \
109
171
  'SCTE35-IN=0xFC002F0000000000FF1,' \
172
+ 'CUE="POST,ONCE",' \
110
173
  'END-ON-NEXT=YES'
111
174
 
112
175
  expect(item.to_s).to eq(expected)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: m3u8
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Deckard
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-01 00:00:00.000000000 Z
11
+ date: 2026-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler