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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2be533f088c62f6247e71b571a4481c92e56f8d575511ecae16be472dd90006a
4
- data.tar.gz: 3d6de6bef331beefea33a156f6129dbcb30c89b195d807440acd740b53027a1b
3
+ metadata.gz: ac7ba85dc1681fda7ba574fd13afc5a2a644726beb03cd10d80862a62d996a76
4
+ data.tar.gz: 8ec9e60e8ca0a7c81f4fdea78aae937bde641d37baa6984aef90da1c97f6c068
5
5
  SHA512:
6
- metadata.gz: edd32a67f619c6fd6b234163cbd16837cd92100f9d84f3729dd1ab09d6fdb1467f8cdab9e89cf64db344d4bb4359620c7aaad3d8532cb33195734a8c22833874
7
- data.tar.gz: c24310d02ff3f844640618cf73fba927fd06bdf2f24e0c20d65166b459092f605d59f1ad408853342c3196e3eceed9535ef08a71c7c75bed8024d5f6383d2f8e
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
- - Small commits that can each be deployed independently
6
- - Each commit must not break production (CI/CD safe)
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, Start, Stop, Document, Reword
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
- Never deploy anything.
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
  [![CI](https://github.com/sethdeckard/m3u8/actions/workflows/ci.yml/badge.svg)](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 the [HTTP Live Streaming (HLS)](https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis) Internet Draft published by Apple.
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
- Access items in playlist:
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 = 0.0
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
- items.count { |item| item.is_a?(PlaylistItem) }
123
+ playlists.size
96
124
  end
97
125
 
98
126
  def segment_size
99
- items.count { |item| item.is_a?(SegmentItem) }
127
+ segments.size
100
128
  end
101
129
  end
102
130
  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.0.0'
5
+ VERSION = '1.1.0'
6
6
  end
@@ -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
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.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Deckard