m3u8 1.0.0 → 1.1.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/AGENTS.md +7 -4
- data/CHANGELOG.md +6 -0
- data/README.md +121 -3
- data/lib/m3u8/playlist.rb +35 -7
- data/lib/m3u8/version.rb +1 -1
- data/spec/lib/m3u8/playlist_spec.rb +74 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac7ba85dc1681fda7ba574fd13afc5a2a644726beb03cd10d80862a62d996a76
|
|
4
|
+
data.tar.gz: 8ec9e60e8ca0a7c81f4fdea78aae937bde641d37baa6984aef90da1c97f6c068
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ede5d1bfd13f64815f5777485bd1a744ce0e13cecf0b51c9d657b7c2890b34f69e70dfba66f707b66007ca176308d99dba14f7a259dfdab2553e2f35e1199bba
|
|
7
|
+
data.tar.gz: fb035f0a70f3f675d1c53b5ef46af63e4dd0fc88dad66666b0010eb66210b48eb7ad3b0748a81f55d8b95f0a8af1fb4492a0467bd8f0469fce5ef93b9cdb2b10
|
data/AGENTS.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Development Workflow
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
5
|
+
- Git workflow: GitHub flow
|
|
6
|
+
- Small (but logical) commits that can each be deployed independently
|
|
7
|
+
- Each commit must not break CI
|
|
7
8
|
- Prefer incremental changes over large feature branches
|
|
8
9
|
|
|
9
10
|
### Commit Messages
|
|
@@ -12,13 +13,15 @@
|
|
|
12
13
|
|
|
13
14
|
**Body:** Wrap at 72 chars, explain what/why not how, blank line after subject
|
|
14
15
|
|
|
15
|
-
**Leading verbs:** Add, Remove, Fix, Upgrade, Refactor, Reformat,
|
|
16
|
+
**Leading verbs:** Add, Remove, Fix, Upgrade, Refactor, Reformat, Document, Reword
|
|
16
17
|
|
|
17
18
|
## Development Standards
|
|
18
19
|
|
|
20
|
+
- README updated with API changes
|
|
19
21
|
- **Tests must cover all behavior** - check with `coverage/index.html` after running specs
|
|
20
22
|
- RuboCop enforces 80-char line limit and other style
|
|
21
23
|
|
|
22
24
|
## Deployment
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
- Kicking off PR: `ghprcw`
|
|
27
|
+
- Never deploy anything
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
**1.1.0**
|
|
2
|
+
|
|
3
|
+
* Added convenience accessor methods to `Playlist` for filtering items by type: `segments`, `playlists`, `media_items`, `keys`, `maps`, `date_ranges`, `parts`, `session_data`.
|
|
4
|
+
|
|
5
|
+
***
|
|
6
|
+
|
|
1
7
|
**1.0.0**
|
|
2
8
|
|
|
3
9
|
* Full HLS spec compliance with draft-pantos-hls-rfc8216bis-19 (Protocol Version 13).
|
data/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
[](https://github.com/sethdeckard/m3u8/actions/workflows/ci.yml)
|
|
3
3
|
# m3u8
|
|
4
4
|
|
|
5
|
-
m3u8 provides easy generation and parsing of m3u8 playlists defined in
|
|
5
|
+
m3u8 provides easy generation and parsing of m3u8 playlists defined in [RFC 8216 HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216) and its proposed successor [draft-pantos-hls-rfc8216bis](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis).
|
|
6
6
|
|
|
7
|
-
* Full coverage of [draft-pantos-hls-rfc8216bis-19](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-19) (Protocol Version 13), including Low-Latency HLS and Content Steering.
|
|
7
|
+
* Full coverage of [RFC 8216](https://www.rfc-editor.org/rfc/rfc8216) and [draft-pantos-hls-rfc8216bis-19](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-19) (Protocol Version 13), including Low-Latency HLS and Content Steering.
|
|
8
8
|
* Provides parsing of an m3u8 playlist into an object model from any File, StringIO, or string.
|
|
9
9
|
* Provides ability to write playlist to a File or StringIO or expose as string via to_s.
|
|
10
10
|
* Distinction between a master and media playlist is handled automatically (single Playlist class).
|
|
@@ -75,6 +75,25 @@ item = M3u8::DefineItem.new(name: 'base', value: 'https://example.com')
|
|
|
75
75
|
playlist.items << item
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Add a session-level encryption key (master playlists):
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
item = M3u8::SessionKeyItem.new(
|
|
82
|
+
method: 'AES-128', uri: 'https://example.com/key.bin'
|
|
83
|
+
)
|
|
84
|
+
playlist.items << item
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Add session-level data (master playlists):
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
item = M3u8::SessionDataItem.new(
|
|
91
|
+
data_id: 'com.example.title', value: 'My Video',
|
|
92
|
+
language: 'en'
|
|
93
|
+
)
|
|
94
|
+
playlist.items << item
|
|
95
|
+
```
|
|
96
|
+
|
|
78
97
|
Create a standard playlist and add MPEG-TS segments via SegmentItem:
|
|
79
98
|
|
|
80
99
|
```ruby
|
|
@@ -85,6 +104,65 @@ item = M3u8::SegmentItem.new(duration: 11, segment: 'test.ts')
|
|
|
85
104
|
playlist.items << item
|
|
86
105
|
```
|
|
87
106
|
|
|
107
|
+
### Media segment tags
|
|
108
|
+
|
|
109
|
+
Add an encryption key for subsequent segments:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
item = M3u8::KeyItem.new(
|
|
113
|
+
method: 'AES-128',
|
|
114
|
+
uri: 'https://example.com/key.bin',
|
|
115
|
+
iv: '0x1234567890abcdef1234567890abcdef'
|
|
116
|
+
)
|
|
117
|
+
playlist.items << item
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Specify an initialization segment (e.g. fMP4 header):
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
item = M3u8::MapItem.new(
|
|
124
|
+
uri: 'init.mp4', byterange: { length: 812, start: 0 }
|
|
125
|
+
)
|
|
126
|
+
playlist.items << item
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Insert a timed metadata date range:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
item = M3u8::DateRangeItem.new(
|
|
133
|
+
id: 'ad-break-1', start_date: '2024-06-01T12:00:00Z',
|
|
134
|
+
planned_duration: 30.0,
|
|
135
|
+
client_attributes: { 'X-AD-ID' => '"foo"' }
|
|
136
|
+
)
|
|
137
|
+
playlist.items << item
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Signal an encoding discontinuity:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
playlist.items << M3u8::DiscontinuityItem.new
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Attach a program date/time to the next segment:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
item = M3u8::TimeItem.new(time: Time.iso8601('2024-06-01T12:00:00Z'))
|
|
150
|
+
playlist.items << item
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Mark a gap in segment availability:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
playlist.items << M3u8::GapItem.new
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Add a bitrate hint for upcoming segments:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
item = M3u8::BitrateItem.new(bitrate: 1500)
|
|
163
|
+
playlist.items << item
|
|
164
|
+
```
|
|
165
|
+
|
|
88
166
|
### Low-Latency HLS
|
|
89
167
|
|
|
90
168
|
Create an LL-HLS playlist with server control, partial segments, and preload hints:
|
|
@@ -153,11 +231,50 @@ playlist.master?
|
|
|
153
231
|
# => true
|
|
154
232
|
```
|
|
155
233
|
|
|
156
|
-
|
|
234
|
+
Query playlist properties:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
playlist.master?
|
|
238
|
+
# => true (contains variant streams)
|
|
239
|
+
playlist.live?
|
|
240
|
+
# => false (master playlists are never live)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For media playlists, `duration` returns total segment duration:
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
media = M3u8::Playlist.read(
|
|
247
|
+
File.open('spec/fixtures/event_playlist.m3u8')
|
|
248
|
+
)
|
|
249
|
+
media.live?
|
|
250
|
+
# => false
|
|
251
|
+
media.duration
|
|
252
|
+
# => 17.0 (sum of all segment durations)
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Access items and their attributes:
|
|
157
256
|
|
|
158
257
|
```ruby
|
|
159
258
|
playlist.items.first
|
|
160
259
|
# => #<M3u8::PlaylistItem ...>
|
|
260
|
+
|
|
261
|
+
media.segments.first.duration
|
|
262
|
+
# => 6.0
|
|
263
|
+
media.segments.first.segment
|
|
264
|
+
# => "segment0.mp4"
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Convenience methods filter items by type:
|
|
268
|
+
|
|
269
|
+
```ruby
|
|
270
|
+
playlist.playlists # => [PlaylistItem, ...]
|
|
271
|
+
playlist.segments # => [SegmentItem, ...]
|
|
272
|
+
playlist.media_items # => [MediaItem, ...]
|
|
273
|
+
playlist.keys # => [KeyItem, ...]
|
|
274
|
+
playlist.maps # => [MapItem, ...]
|
|
275
|
+
playlist.date_ranges # => [DateRangeItem, ...]
|
|
276
|
+
playlist.parts # => [PartItem, ...]
|
|
277
|
+
playlist.session_data # => [SessionDataItem, ...]
|
|
161
278
|
```
|
|
162
279
|
|
|
163
280
|
Parse an LL-HLS playlist:
|
|
@@ -219,6 +336,7 @@ codecs = M3u8::Playlist.codecs(options)
|
|
|
219
336
|
* `EXT-X-PLAYLIST-TYPE`
|
|
220
337
|
* `EXT-X-I-FRAMES-ONLY`
|
|
221
338
|
* `EXT-X-ALLOW-CACHE`
|
|
339
|
+
* `EXT-X-ENDLIST`
|
|
222
340
|
|
|
223
341
|
### Media segment tags
|
|
224
342
|
* `EXTINF`
|
data/lib/m3u8/playlist.rb
CHANGED
|
@@ -54,12 +54,40 @@ module M3u8
|
|
|
54
54
|
true
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
def segments
|
|
58
|
+
items.select { |item| item.is_a?(SegmentItem) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def playlists
|
|
62
|
+
items.select { |item| item.is_a?(PlaylistItem) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def media_items
|
|
66
|
+
items.select { |item| item.is_a?(MediaItem) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def keys
|
|
70
|
+
items.select { |item| item.is_a?(KeyItem) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def maps
|
|
74
|
+
items.select { |item| item.is_a?(MapItem) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def date_ranges
|
|
78
|
+
items.select { |item| item.is_a?(DateRangeItem) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parts
|
|
82
|
+
items.select { |item| item.is_a?(PartItem) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def session_data
|
|
86
|
+
items.select { |item| item.is_a?(SessionDataItem) }
|
|
87
|
+
end
|
|
88
|
+
|
|
57
89
|
def duration
|
|
58
|
-
duration
|
|
59
|
-
items.each do |item|
|
|
60
|
-
duration += item.duration if item.is_a?(M3u8::SegmentItem)
|
|
61
|
-
end
|
|
62
|
-
duration
|
|
90
|
+
segments.sum(&:duration)
|
|
63
91
|
end
|
|
64
92
|
|
|
65
93
|
private
|
|
@@ -92,11 +120,11 @@ module M3u8
|
|
|
92
120
|
end
|
|
93
121
|
|
|
94
122
|
def playlist_size
|
|
95
|
-
|
|
123
|
+
playlists.size
|
|
96
124
|
end
|
|
97
125
|
|
|
98
126
|
def segment_size
|
|
99
|
-
|
|
127
|
+
segments.size
|
|
100
128
|
end
|
|
101
129
|
end
|
|
102
130
|
end
|
data/lib/m3u8/version.rb
CHANGED
|
@@ -236,6 +236,80 @@ describe M3u8::Playlist do
|
|
|
236
236
|
end
|
|
237
237
|
end
|
|
238
238
|
|
|
239
|
+
describe '#segments' do
|
|
240
|
+
it 'returns only segment items' do
|
|
241
|
+
file = File.open('spec/fixtures/playlist.m3u8')
|
|
242
|
+
playlist = described_class.read(file)
|
|
243
|
+
expect(playlist.segments).to all be_a(M3u8::SegmentItem)
|
|
244
|
+
expect(playlist.segments.size).to eq(138)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
describe '#playlists' do
|
|
249
|
+
it 'returns only playlist items' do
|
|
250
|
+
file = File.open('spec/fixtures/master.m3u8')
|
|
251
|
+
playlist = described_class.read(file)
|
|
252
|
+
expect(playlist.playlists).to all be_a(M3u8::PlaylistItem)
|
|
253
|
+
expect(playlist.playlists.size).to eq(6)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
describe '#media_items' do
|
|
258
|
+
it 'returns only media items' do
|
|
259
|
+
file = File.open('spec/fixtures/variant_audio.m3u8')
|
|
260
|
+
playlist = described_class.read(file)
|
|
261
|
+
expect(playlist.media_items).to all be_a(M3u8::MediaItem)
|
|
262
|
+
expect(playlist.media_items.size).to eq(6)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
describe '#keys' do
|
|
267
|
+
it 'returns only key items' do
|
|
268
|
+
file = File.open('spec/fixtures/encrypted.m3u8')
|
|
269
|
+
playlist = described_class.read(file)
|
|
270
|
+
expect(playlist.keys).to all be_a(M3u8::KeyItem)
|
|
271
|
+
expect(playlist.keys.size).to eq(2)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
describe '#maps' do
|
|
276
|
+
it 'returns only map items' do
|
|
277
|
+
file = File.open('spec/fixtures/map_playlist.m3u8')
|
|
278
|
+
playlist = described_class.read(file)
|
|
279
|
+
expect(playlist.maps).to all be_a(M3u8::MapItem)
|
|
280
|
+
expect(playlist.maps.size).to eq(1)
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
describe '#date_ranges' do
|
|
285
|
+
it 'returns only date range items' do
|
|
286
|
+
file = File.open('spec/fixtures/daterange_playlist.m3u8')
|
|
287
|
+
playlist = described_class.read(file)
|
|
288
|
+
expect(playlist.date_ranges)
|
|
289
|
+
.to all be_a(M3u8::DateRangeItem)
|
|
290
|
+
expect(playlist.date_ranges.size).to eq(3)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
describe '#parts' do
|
|
295
|
+
it 'returns only part items' do
|
|
296
|
+
file = File.open('spec/fixtures/ll_hls_playlist.m3u8')
|
|
297
|
+
playlist = described_class.read(file)
|
|
298
|
+
expect(playlist.parts).to all be_a(M3u8::PartItem)
|
|
299
|
+
expect(playlist.parts.size).to eq(5)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
describe '#session_data' do
|
|
304
|
+
it 'returns only session data items' do
|
|
305
|
+
file = File.open('spec/fixtures/session_data.m3u8')
|
|
306
|
+
playlist = described_class.read(file)
|
|
307
|
+
expect(playlist.session_data)
|
|
308
|
+
.to all be_a(M3u8::SessionDataItem)
|
|
309
|
+
expect(playlist.session_data.size).to eq(3)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
239
313
|
describe '#write' do
|
|
240
314
|
context 'when playlist is valid' do
|
|
241
315
|
it 'returns playlist text' do
|