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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +32 -1
- data/lib/m3u8/date_range_item.rb +93 -23
- data/lib/m3u8/version.rb +1 -1
- data/spec/lib/m3u8/date_range_item_spec.rb +69 -6
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c38180f01d16f00854720acaf6fc8359f2362417e19b8e45248a1573597716ca
|
|
4
|
+
data.tar.gz: b5192ed27a57a1f3a06d32f09d5d1fdbcd3c1e788d6bf515c68f739531ba8f23
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/m3u8/date_range_item.rb
CHANGED
|
@@ -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
|
-
|
|
75
|
+
cue_format,
|
|
76
|
+
end_on_next_format].flatten.compact.join(',')
|
|
63
77
|
end
|
|
64
78
|
|
|
65
79
|
def class_name_format
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
%(CLASS="#{class_name}")
|
|
80
|
+
quoted_format('CLASS', class_name)
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
def end_date_format
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
%(END-DATE="#{end_date}")
|
|
84
|
+
quoted_format('END-DATE', end_date)
|
|
75
85
|
end
|
|
76
86
|
|
|
77
87
|
def duration_format
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"DURATION=#{duration}"
|
|
88
|
+
unquoted_format('DURATION', duration)
|
|
81
89
|
end
|
|
82
90
|
|
|
83
91
|
def planned_duration_format
|
|
84
|
-
|
|
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
|
|
111
|
-
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
+
unquoted_format('SCTE35-IN', scte35_in)
|
|
182
|
+
end
|
|
124
183
|
|
|
125
|
-
|
|
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
|
|
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
|
@@ -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',
|
|
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',
|
|
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' =>
|
|
148
|
+
'X-CUSTOM-TEXT' =>
|
|
149
|
+
'test_value' } }
|
|
99
150
|
item = described_class.new(options)
|
|
100
151
|
|
|
101
|
-
expected = '#EXT-X-DATERANGE:ID="test_id",
|
|
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",
|
|
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.
|
|
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-
|
|
11
|
+
date: 2026-03-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|