m3u8 0.8.2 → 1.0.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.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +23 -0
  3. data/.rubocop.yml +31 -0
  4. data/AGENTS.md +24 -0
  5. data/CHANGELOG.md +16 -0
  6. data/CLAUDE.md +1 -0
  7. data/Gemfile +1 -0
  8. data/Guardfile +1 -0
  9. data/README.md +141 -40
  10. data/Rakefile +1 -0
  11. data/lib/m3u8/bitrate_item.rb +24 -0
  12. data/lib/m3u8/byte_range.rb +2 -0
  13. data/lib/m3u8/content_steering_item.rb +46 -0
  14. data/lib/m3u8/date_range_item.rb +18 -5
  15. data/lib/m3u8/define_item.rb +44 -0
  16. data/lib/m3u8/discontinuity_item.rb +1 -0
  17. data/lib/m3u8/encryptable.rb +1 -0
  18. data/lib/m3u8/error.rb +1 -0
  19. data/lib/m3u8/gap_item.rb +12 -0
  20. data/lib/m3u8/key_item.rb +1 -0
  21. data/lib/m3u8/map_item.rb +3 -0
  22. data/lib/m3u8/media_item.rb +43 -3
  23. data/lib/m3u8/part_inf_item.rb +28 -0
  24. data/lib/m3u8/part_item.rb +69 -0
  25. data/lib/m3u8/playback_start.rb +3 -0
  26. data/lib/m3u8/playlist.rb +10 -3
  27. data/lib/m3u8/playlist_item.rb +113 -8
  28. data/lib/m3u8/preload_hint_item.rb +66 -0
  29. data/lib/m3u8/reader.rb +69 -6
  30. data/lib/m3u8/rendition_report_item.rb +58 -0
  31. data/lib/m3u8/segment_item.rb +16 -2
  32. data/lib/m3u8/server_control_item.rb +75 -0
  33. data/lib/m3u8/session_data_item.rb +2 -0
  34. data/lib/m3u8/session_key_item.rb +1 -0
  35. data/lib/m3u8/skip_item.rb +48 -0
  36. data/lib/m3u8/time_item.rb +3 -0
  37. data/lib/m3u8/version.rb +1 -1
  38. data/lib/m3u8/writer.rb +17 -0
  39. data/lib/m3u8.rb +8 -5
  40. data/m3u8.gemspec +3 -2
  41. data/spec/fixtures/content_steering.m3u8 +10 -0
  42. data/spec/fixtures/daterange_playlist.m3u8 +14 -0
  43. data/spec/fixtures/encrypted_discontinuity.m3u8 +17 -0
  44. data/spec/fixtures/event_playlist.m3u8 +18 -0
  45. data/spec/fixtures/gap_playlist.m3u8 +14 -0
  46. data/spec/fixtures/ll_hls_advanced.m3u8 +18 -0
  47. data/spec/fixtures/ll_hls_playlist.m3u8 +20 -0
  48. data/spec/fixtures/master_full.m3u8 +14 -0
  49. data/spec/fixtures/master_v13.m3u8 +8 -0
  50. data/spec/lib/m3u8/bitrate_item_spec.rb +26 -0
  51. data/spec/lib/m3u8/byte_range_spec.rb +1 -0
  52. data/spec/lib/m3u8/content_steering_item_spec.rb +56 -0
  53. data/spec/lib/m3u8/date_range_item_spec.rb +20 -19
  54. data/spec/lib/m3u8/define_item_spec.rb +59 -0
  55. data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -0
  56. data/spec/lib/m3u8/gap_item_spec.rb +12 -0
  57. data/spec/lib/m3u8/key_item_spec.rb +1 -0
  58. data/spec/lib/m3u8/map_item_spec.rb +1 -0
  59. data/spec/lib/m3u8/media_item_spec.rb +34 -0
  60. data/spec/lib/m3u8/part_inf_item_spec.rb +27 -0
  61. data/spec/lib/m3u8/part_item_spec.rb +60 -0
  62. data/spec/lib/m3u8/playback_start_spec.rb +1 -0
  63. data/spec/lib/m3u8/playlist_item_spec.rb +115 -10
  64. data/spec/lib/m3u8/playlist_spec.rb +21 -10
  65. data/spec/lib/m3u8/preload_hint_item_spec.rb +57 -0
  66. data/spec/lib/m3u8/reader_spec.rb +314 -1
  67. data/spec/lib/m3u8/rendition_report_item_spec.rb +56 -0
  68. data/spec/lib/m3u8/round_trip_spec.rb +158 -0
  69. data/spec/lib/m3u8/segment_item_spec.rb +13 -0
  70. data/spec/lib/m3u8/server_control_item_spec.rb +64 -0
  71. data/spec/lib/m3u8/session_data_item_spec.rb +1 -0
  72. data/spec/lib/m3u8/session_key_item_spec.rb +1 -0
  73. data/spec/lib/m3u8/skip_item_spec.rb +48 -0
  74. data/spec/lib/m3u8/time_item_spec.rb +1 -0
  75. data/spec/lib/m3u8/writer_spec.rb +59 -21
  76. data/spec/lib/m3u8_spec.rb +1 -0
  77. data/spec/spec_helper.rb +2 -87
  78. metadata +58 -42
  79. data/.travis.yml +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 79a1150cd18985b297585a60de9670a486eb1206
4
- data.tar.gz: 723fc2ccb9856cadb5758678861e3e57b853d2c6
2
+ SHA256:
3
+ metadata.gz: 2be533f088c62f6247e71b571a4481c92e56f8d575511ecae16be472dd90006a
4
+ data.tar.gz: 3d6de6bef331beefea33a156f6129dbcb30c89b195d807440acd740b53027a1b
5
5
  SHA512:
6
- metadata.gz: 1ebb81bf922cee507b51beb8f608d7779a4ff35d64b935760e285675ce8715ae4e036748f622bdb91b762daf291e267fa1374bf6252631743582374be55fc611
7
- data.tar.gz: 4983b2b1cb6826eb5293bf5df6011e203ed9067cac4a4e218a3e2537c9948f0dda8318028156cf3e5a9b392f1aa2a2418d61651223c2a14a59d58b86902d6e19
6
+ metadata.gz: edd32a67f619c6fd6b234163cbd16837cd92100f9d84f3729dd1ab09d6fdb1467f8cdab9e89cf64db344d4bb4359620c7aaad3d8532cb33195734a8c22833874
7
+ data.tar.gz: c24310d02ff3f844640618cf73fba927fd06bdf2f24e0c20d65166b459092f605d59f1ad408853342c3196e3eceed9535ef08a71c7c75bed8024d5f6383d2f8e
@@ -0,0 +1,23 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [master]
5
+ pull_request:
6
+ branches: [master]
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - name: Set up Ruby ${{ matrix.ruby-version }}
16
+ uses: ruby/setup-ruby@v1
17
+ with:
18
+ ruby-version: ${{ matrix.ruby-version }}
19
+ bundler-cache: true
20
+ - name: Run tests
21
+ run: bundle exec rspec
22
+ - name: Run linter
23
+ run: bundle exec rubocop
data/.rubocop.yml CHANGED
@@ -1,9 +1,40 @@
1
1
  AllCops:
2
+ TargetRubyVersion: 3.0
3
+ NewCops: enable
4
+ SuggestExtensions: false
2
5
  Exclude:
3
6
  - 'spec/spec_helper.rb'
4
7
  - 'm3u8.gemspec'
8
+ - 'vendor/**/*'
5
9
  Metrics/BlockLength:
6
10
  Exclude:
7
11
  - 'spec/**/*'
12
+ Metrics/MethodLength:
13
+ Max: 20
14
+ Exclude:
15
+ - 'spec/**/*'
16
+ Metrics/AbcSize:
17
+ Max: 35
18
+ Exclude:
19
+ - 'spec/**/*'
20
+ Metrics/ClassLength:
21
+ Enabled: false
22
+ Metrics/CyclomaticComplexity:
23
+ Enabled: false
24
+ Metrics/PerceivedComplexity:
25
+ Enabled: false
26
+ Layout/LineLength:
27
+ Max: 120
28
+ Exclude:
29
+ - 'spec/**/*'
8
30
  Style/StringLiterals:
9
31
  EnforcedStyle: single_quotes
32
+ Style/StringConcatenation:
33
+ Exclude:
34
+ - 'spec/**/*'
35
+ Lint/FloatComparison:
36
+ Enabled: false
37
+ Lint/DuplicateMethods:
38
+ Enabled: false
39
+ Naming/PredicateMethod:
40
+ Enabled: false
data/AGENTS.md ADDED
@@ -0,0 +1,24 @@
1
+ # AGENTS.md
2
+
3
+ ## Development Workflow
4
+
5
+ - Small commits that can each be deployed independently
6
+ - Each commit must not break production (CI/CD safe)
7
+ - Prefer incremental changes over large feature branches
8
+
9
+ ### Commit Messages
10
+
11
+ **Subject:** Max 50 chars, capitalized, no period, imperative mood ("Add" not "Added")
12
+
13
+ **Body:** Wrap at 72 chars, explain what/why not how, blank line after subject
14
+
15
+ **Leading verbs:** Add, Remove, Fix, Upgrade, Refactor, Reformat, Start, Stop, Document, Reword
16
+
17
+ ## Development Standards
18
+
19
+ - **Tests must cover all behavior** - check with `coverage/index.html` after running specs
20
+ - RuboCop enforces 80-char line limit and other style
21
+
22
+ ## Deployment
23
+
24
+ Never deploy anything.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ **1.0.0**
2
+
3
+ * Full HLS spec compliance with draft-pantos-hls-rfc8216bis-19 (Protocol Version 13).
4
+ * Added Low-Latency HLS support: `EXT-X-PART`, `EXT-X-PART-INF`, `EXT-X-SERVER-CONTROL`, `EXT-X-SKIP`, `EXT-X-PRELOAD-HINT`, `EXT-X-RENDITION-REPORT`.
5
+ * Added HEVC/H.265 and AV1 video codec string generation.
6
+ * Added AC-3, E-AC-3, FLAC, and Opus audio codec string generation.
7
+ * Added new attributes to `PlaylistItem`: `STABLE-VARIANT-ID`, `VIDEO-RANGE`, `ALLOWED-CPC`, `PATHWAY-ID`, `REQ-VIDEO-LAYOUT`, `SUPPLEMENTAL-CODECS`, `SCORE`.
8
+ * Added new attributes to `MediaItem`: `STABLE-RENDITION-ID`, `BIT-DEPTH`, `SAMPLE-RATE`.
9
+ * Added `EXT-X-CONTENT-STEERING` tag support.
10
+ * Added `EXT-X-DEFINE` tag support.
11
+ * Added `EXT-X-GAP` and `EXT-X-BITRATE` tag support.
12
+ * Upgraded CI from Travis CI to GitHub Actions.
13
+ * Requires Ruby 3.0+.
14
+
15
+ ***
16
+
1
17
  **0.8.1**
2
18
  Merged pull request #23 from [ryanische](https:/github.com/ryanische) which fixes issue of CODEC attribute validation not matching the HLS I-D.
3
19
 
data/CLAUDE.md ADDED
@@ -0,0 +1 @@
1
+ AGENTS.md
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  source 'https://rubygems.org'
3
4
 
4
5
  # Specify your gem's dependencies in m3u8.gemspec
data/Guardfile CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  guard :rspec, cmd: 'bundle exec rspec' do
3
4
  watch(%r{^spec/.+_spec\.rb$})
4
5
  watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
data/README.md CHANGED
@@ -1,18 +1,18 @@
1
1
  [![Gem Version](https://badge.fury.io/rb/m3u8.svg)](http://badge.fury.io/rb/m3u8)
2
- [![Build Status](https://travis-ci.org/sethdeckard/m3u8.svg?branch=master)](https://travis-ci.org/sethdeckard/m3u8)
3
- [![Coverage Status](https://coveralls.io/repos/github/sethdeckard/m3u8/badge.svg?branch=master)](https://coveralls.io/github/sethdeckard/m3u8?branch=master)
4
- [![Code Climate](https://codeclimate.com/github/sethdeckard/m3u8/badges/gpa.svg)](https://codeclimate.com/github/sethdeckard/m3u8)
5
- [![Dependency Status](https://gemnasium.com/sethdeckard/m3u8.svg)](https://gemnasium.com/sethdeckard/m3u8)
6
- [![security](https://hakiri.io/github/sethdeckard/m3u8/master.svg)](https://hakiri.io/github/sethdeckard/m3u8/master)
2
+ [![CI](https://github.com/sethdeckard/m3u8/actions/workflows/ci.yml/badge.svg)](https://github.com/sethdeckard/m3u8/actions/workflows/ci.yml)
7
3
  # m3u8
8
4
 
9
- m3u8 provides easy generation and parsing of m3u8 playlists defined in the [HTTP Live Streaming (HLS)](https://tools.ietf.org/html/draft-pantos-http-live-streaming-20) Internet Draft published by Apple.
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.
10
6
 
11
- * The library completely implements version 20 of the HLS Internet Draft.
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.
12
8
  * Provides parsing of an m3u8 playlist into an object model from any File, StringIO, or string.
13
9
  * Provides ability to write playlist to a File or StringIO or expose as string via to_s.
14
10
  * Distinction between a master and media playlist is handled automatically (single Playlist class).
15
- * Optionally, the library can automatically generate the audio/video codecs string used in the CODEC attribute based on specified H.264, AAC, or MP3 options (such as Profile/Level).
11
+ * Automatic generation of codec strings for H.264, HEVC, AV1, AAC, AC-3, E-AC-3, FLAC, Opus, and MP3.
12
+
13
+ ## Requirements
14
+
15
+ Ruby 3.0+
16
16
 
17
17
  ## Installation
18
18
 
@@ -31,7 +31,7 @@ Or install it yourself as:
31
31
  $ gem install m3u8
32
32
 
33
33
  ## Usage (creating playlists)
34
-
34
+
35
35
  Create a master playlist and add child playlists for adaptive bitrate streaming:
36
36
 
37
37
  ```ruby
@@ -46,8 +46,8 @@ options = { width: 1920, height: 1080, profile: 'high', level: 4.1,
46
46
  audio_codec: 'aac-lc', bandwidth: 540, uri: 'test.url' }
47
47
  item = M3u8::PlaylistItem.new(options)
48
48
  playlist.items << item
49
- ```
50
-
49
+ ```
50
+
51
51
  Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist:
52
52
 
53
53
  ```ruby
@@ -57,8 +57,25 @@ hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
57
57
  item = M3u8::MediaItem.new(hash)
58
58
  playlist.items << item
59
59
  ```
60
-
61
- Create a standard playlist and add MPEG-TS segments via SegmentItem. You can also specify options for this type of playlist, however these options are ignored if playlist becomes a master playlist (anything but segments added):
60
+
61
+ Add Content Steering for dynamic CDN pathway selection:
62
+
63
+ ```ruby
64
+ item = M3u8::ContentSteeringItem.new(
65
+ server_uri: 'https://example.com/steering',
66
+ pathway_id: 'CDN-A'
67
+ )
68
+ playlist.items << item
69
+ ```
70
+
71
+ Add variable definitions:
72
+
73
+ ```ruby
74
+ item = M3u8::DefineItem.new(name: 'base', value: 'https://example.com')
75
+ playlist.items << item
76
+ ```
77
+
78
+ Create a standard playlist and add MPEG-TS segments via SegmentItem:
62
79
 
63
80
  ```ruby
64
81
  options = { version: 1, cache: false, target: 12, sequence: 1 }
@@ -67,7 +84,42 @@ playlist = M3u8::Playlist.new(options)
67
84
  item = M3u8::SegmentItem.new(duration: 11, segment: 'test.ts')
68
85
  playlist.items << item
69
86
  ```
70
-
87
+
88
+ ### Low-Latency HLS
89
+
90
+ Create an LL-HLS playlist with server control, partial segments, and preload hints:
91
+
92
+ ```ruby
93
+ server_control = M3u8::ServerControlItem.new(
94
+ can_skip_until: 24.0, part_hold_back: 1.0,
95
+ can_block_reload: true
96
+ )
97
+ part_inf = M3u8::PartInfItem.new(part_target: 0.5)
98
+ playlist = M3u8::Playlist.new(
99
+ version: 9, target: 4, sequence: 100,
100
+ server_control: server_control, part_inf: part_inf,
101
+ live: true
102
+ )
103
+
104
+ item = M3u8::SegmentItem.new(duration: 4.0, segment: 'seg100.mp4')
105
+ playlist.items << item
106
+
107
+ part = M3u8::PartItem.new(
108
+ duration: 0.5, uri: 'seg101.0.mp4', independent: true
109
+ )
110
+ playlist.items << part
111
+
112
+ hint = M3u8::PreloadHintItem.new(type: 'PART', uri: 'seg101.1.mp4')
113
+ playlist.items << hint
114
+
115
+ report = M3u8::RenditionReportItem.new(
116
+ uri: '../alt/index.m3u8', last_msn: 101, last_part: 0
117
+ )
118
+ playlist.items << report
119
+ ```
120
+
121
+ ### Writing playlists
122
+
71
123
  You can pass an IO object to the write method:
72
124
 
73
125
  ```ruby
@@ -80,7 +132,7 @@ You can also access the playlist as a string:
80
132
 
81
133
  ```ruby
82
134
  playlist.to_s
83
- ```
135
+ ```
84
136
 
85
137
  M3u8::Writer is the class that handles generating the playlist output.
86
138
 
@@ -92,7 +144,6 @@ options = { width: 1920, height: 1080, codecs: 'avc1.66.30,mp4a.40.2',
92
144
  item = M3u8::PlaylistItem.new(options)
93
145
  ```
94
146
 
95
-
96
147
  ## Usage (parsing playlists)
97
148
 
98
149
  ```ruby
@@ -106,42 +157,93 @@ Access items in playlist:
106
157
 
107
158
  ```ruby
108
159
  playlist.items.first
109
- # => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080",
110
- # @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000",
111
- # @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
160
+ # => #<M3u8::PlaylistItem ...>
112
161
  ```
113
162
 
114
- Create a new playlist item with options:
163
+ Parse an LL-HLS playlist:
115
164
 
116
165
  ```ruby
117
- options = { width: 1920, height: 1080, profile: 'high', level: 4.1,
118
- audio_codec: 'aac-lc', bandwidth: 540, uri: 'test.url' }
119
- item = M3u8::PlaylistItem.new(options)
120
- #add it to the top of the playlist
121
- playlist.items.unshift(item)
166
+ file = File.open 'spec/fixtures/ll_hls_playlist.m3u8'
167
+ playlist = M3u8::Playlist.read(file)
168
+ playlist.server_control.can_block_reload
169
+ # => true
170
+ playlist.part_inf.part_target
171
+ # => 0.5
122
172
  ```
123
173
 
124
- M3u8::Reader is the class handles parsing if you want more control over the process.
174
+ M3u8::Reader is the class that handles parsing if you want more control over the process.
125
175
 
126
- ## Usage (misc)
127
- Generate the codec string based on audio and video codec options without dealing a playlist instance:
176
+ ## Codec string generation
177
+
178
+ Generate the codec string based on audio and video codec options without dealing with a playlist instance:
128
179
 
129
180
  ```ruby
130
181
  options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
131
182
  codecs = M3u8::Playlist.codecs(options)
132
183
  # => "avc1.66.30,mp4a.40.2"
133
- ```
134
-
135
- * Values for audio_codec (codec name): aac-lc, he-aac, mp3
136
- * Values for profile (H.264 Profile): baseline, main, high.
137
- * Values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
138
-
139
- Not all Levels and Profiles can be combined and validation is not currently implemented, consult H.264 documentation for further details.
140
-
184
+ ```
141
185
 
142
- ## Roadmap
143
- * Implement validation of all tags, attributes, and values per HLS I-D.
144
- * Perhaps support for different versions of HLS I-D, defaulting to latest.
186
+ ### Video codecs
187
+
188
+ | Profile | Description |
189
+ |---------|-------------|
190
+ | `baseline`, `main`, `high` | H.264/AVC |
191
+ | `hevc-main`, `hevc-main-10` | HEVC/H.265 |
192
+ | `av1-main`, `av1-high` | AV1 |
193
+
194
+ ### Audio codecs
195
+
196
+ | Value | Codec |
197
+ |-------|-------|
198
+ | `aac-lc` | AAC-LC |
199
+ | `he-aac` | HE-AAC |
200
+ | `mp3` | MP3 |
201
+ | `ac-3` | AC-3 (Dolby Digital) |
202
+ | `ec-3`, `e-ac-3` | E-AC-3 (Dolby Digital Plus) |
203
+ | `flac` | FLAC |
204
+ | `opus` | Opus |
205
+
206
+ ## Supported tags
207
+
208
+ ### Master playlist tags
209
+ * `EXT-X-STREAM-INF` / `EXT-X-I-FRAME-STREAM-INF` — including `STABLE-VARIANT-ID`, `VIDEO-RANGE`, `ALLOWED-CPC`, `PATHWAY-ID`, `REQ-VIDEO-LAYOUT`, `SUPPLEMENTAL-CODECS`, `SCORE`
210
+ * `EXT-X-MEDIA` — including `STABLE-RENDITION-ID`, `BIT-DEPTH`, `SAMPLE-RATE`
211
+ * `EXT-X-SESSION-DATA`
212
+ * `EXT-X-SESSION-KEY`
213
+ * `EXT-X-CONTENT-STEERING`
214
+
215
+ ### Media playlist tags
216
+ * `EXT-X-TARGETDURATION`
217
+ * `EXT-X-MEDIA-SEQUENCE`
218
+ * `EXT-X-DISCONTINUITY-SEQUENCE`
219
+ * `EXT-X-PLAYLIST-TYPE`
220
+ * `EXT-X-I-FRAMES-ONLY`
221
+ * `EXT-X-ALLOW-CACHE`
222
+
223
+ ### Media segment tags
224
+ * `EXTINF`
225
+ * `EXT-X-BYTERANGE`
226
+ * `EXT-X-DISCONTINUITY`
227
+ * `EXT-X-KEY`
228
+ * `EXT-X-MAP`
229
+ * `EXT-X-PROGRAM-DATE-TIME`
230
+ * `EXT-X-DATERANGE`
231
+ * `EXT-X-GAP`
232
+ * `EXT-X-BITRATE`
233
+
234
+ ### Universal tags
235
+ * `EXT-X-INDEPENDENT-SEGMENTS`
236
+ * `EXT-X-START`
237
+ * `EXT-X-DEFINE`
238
+ * `EXT-X-VERSION`
239
+
240
+ ### Low-Latency HLS tags
241
+ * `EXT-X-SERVER-CONTROL`
242
+ * `EXT-X-PART-INF`
243
+ * `EXT-X-PART`
244
+ * `EXT-X-SKIP`
245
+ * `EXT-X-PRELOAD-HINT`
246
+ * `EXT-X-RENDITION-REPORT`
145
247
 
146
248
  ## Contributing
147
249
 
@@ -152,6 +254,5 @@ Not all Levels and Profiles can be combined and validation is not currently impl
152
254
  5. Push to the branch (`git push origin my-new-feature`)
153
255
  6. Create a new Pull Request
154
256
 
155
-
156
257
  ## License
157
258
  MIT License - See [LICENSE.txt](https://github.com/sethdeckard/m3u8/blob/master/LICENSE.txt) for details.
data/Rakefile CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'bundler/gem_tasks'
3
4
  require 'rspec/core/rake_task'
4
5
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # BitrateItem represents an EXT-X-BITRATE tag that indicates the
5
+ # approximate bitrate of the following media segments in kbps.
6
+ class BitrateItem
7
+ attr_accessor :bitrate
8
+
9
+ def initialize(params = {})
10
+ params.each do |key, value|
11
+ instance_variable_set("@#{key}", value)
12
+ end
13
+ end
14
+
15
+ def self.parse(text)
16
+ value = text.gsub('#EXT-X-BITRATE:', '').strip
17
+ BitrateItem.new(bitrate: value.to_i)
18
+ end
19
+
20
+ def to_s
21
+ "#EXT-X-BITRATE:#{bitrate}"
22
+ end
23
+ end
24
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # ByteRange represents sub range of a resource
4
5
  class ByteRange
@@ -26,6 +27,7 @@ module M3u8
26
27
 
27
28
  def start_format
28
29
  return if start.nil?
30
+
29
31
  "@#{start}"
30
32
  end
31
33
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # ContentSteeringItem represents an EXT-X-CONTENT-STEERING tag which
5
+ # indicates a Content Steering Manifest for dynamic pathway selection.
6
+ class ContentSteeringItem
7
+ extend M3u8
8
+
9
+ attr_accessor :server_uri, :pathway_id
10
+
11
+ def initialize(params = {})
12
+ params.each do |key, value|
13
+ instance_variable_set("@#{key}", value)
14
+ end
15
+ end
16
+
17
+ def self.parse(text)
18
+ attributes = parse_attributes(text)
19
+ ContentSteeringItem.new(
20
+ server_uri: attributes['SERVER-URI'],
21
+ pathway_id: attributes['PATHWAY-ID']
22
+ )
23
+ end
24
+
25
+ def to_s
26
+ "#EXT-X-CONTENT-STEERING:#{formatted_attributes}"
27
+ end
28
+
29
+ private
30
+
31
+ def formatted_attributes
32
+ [server_uri_format,
33
+ pathway_id_format].compact.join(',')
34
+ end
35
+
36
+ def server_uri_format
37
+ %(SERVER-URI="#{server_uri}")
38
+ end
39
+
40
+ def pathway_id_format
41
+ return if pathway_id.nil?
42
+
43
+ %(PATHWAY-ID="#{pathway_id}")
44
+ end
45
+ end
46
+ end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # DateRangeItem represents a #EXT-X-DATERANGE tag
4
5
  class DateRangeItem
5
6
  include M3u8
7
+
6
8
  attr_accessor :id, :class_name, :start_date, :end_date, :duration,
7
9
  :planned_duration, :scte35_cmd, :scte35_out, :scte35_in,
8
10
  :end_on_next, :client_attributes
@@ -24,7 +26,7 @@ module M3u8
24
26
  @scte35_cmd = attributes['SCTE35-CMD']
25
27
  @scte35_out = attributes['SCTE35-OUT']
26
28
  @scte35_in = attributes['SCTE35-IN']
27
- @end_on_next = attributes.key?('END-ON-NEXT') ? true : false
29
+ @end_on_next = attributes.key?('END-ON-NEXT')
28
30
  @client_attributes = parse_client_attributes(attributes)
29
31
  end
30
32
 
@@ -50,26 +52,31 @@ module M3u8
50
52
 
51
53
  def class_name_format
52
54
  return if class_name.nil?
55
+
53
56
  %(CLASS="#{class_name}")
54
57
  end
55
58
 
56
59
  def end_date_format
57
60
  return if end_date.nil?
61
+
58
62
  %(END-DATE="#{end_date}")
59
63
  end
60
64
 
61
65
  def duration_format
62
66
  return if duration.nil?
67
+
63
68
  "DURATION=#{duration}"
64
69
  end
65
70
 
66
71
  def planned_duration_format
67
72
  return if planned_duration.nil?
73
+
68
74
  "PLANNED-DURATION=#{planned_duration}"
69
75
  end
70
76
 
71
77
  def client_attributes_format
72
- return if client_attributes.nil?
78
+ return if client_attributes.nil? || client_attributes.empty?
79
+
73
80
  client_attributes.map do |attribute|
74
81
  value = attribute.last
75
82
  value_format = decimal?(value) ? value : %("#{value}")
@@ -78,31 +85,37 @@ module M3u8
78
85
  end
79
86
 
80
87
  def decimal?(value)
81
- return true if value =~ /\A\d+\Z/
88
+ val = value.to_s
89
+ return true if val =~ /\A\d+\Z/
90
+
82
91
  begin
83
- return true if Float(value)
84
- rescue
92
+ true if Float(val)
93
+ rescue StandardError
85
94
  false
86
95
  end
87
96
  end
88
97
 
89
98
  def scte35_cmd_format
90
99
  return if scte35_cmd.nil?
100
+
91
101
  "SCTE35-CMD=#{scte35_cmd}"
92
102
  end
93
103
 
94
104
  def scte35_out_format
95
105
  return if scte35_out.nil?
106
+
96
107
  "SCTE35-OUT=#{scte35_out}"
97
108
  end
98
109
 
99
110
  def scte35_in_format
100
111
  return if scte35_in.nil?
112
+
101
113
  "SCTE35-IN=#{scte35_in}"
102
114
  end
103
115
 
104
116
  def end_on_next_format
105
117
  return unless end_on_next
118
+
106
119
  'END-ON-NEXT=YES'
107
120
  end
108
121
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # DefineItem represents an EXT-X-DEFINE tag which provides variable
5
+ # definitions for variable substitution. Supports three mutually
6
+ # exclusive modes: NAME/VALUE, IMPORT, or QUERYPARAM.
7
+ class DefineItem
8
+ extend M3u8
9
+
10
+ attr_accessor :name, :value, :import, :queryparam
11
+
12
+ def initialize(params = {})
13
+ params.each do |key, val|
14
+ instance_variable_set("@#{key}", val)
15
+ end
16
+ end
17
+
18
+ def self.parse(text)
19
+ attributes = parse_attributes(text)
20
+ DefineItem.new(
21
+ name: attributes['NAME'],
22
+ value: attributes['VALUE'],
23
+ import: attributes['IMPORT'],
24
+ queryparam: attributes['QUERYPARAM']
25
+ )
26
+ end
27
+
28
+ def to_s
29
+ "#EXT-X-DEFINE:#{formatted_attributes}"
30
+ end
31
+
32
+ private
33
+
34
+ def formatted_attributes
35
+ if import
36
+ %(IMPORT="#{import}")
37
+ elsif queryparam
38
+ %(QUERYPARAM="#{queryparam}")
39
+ else
40
+ %(NAME="#{name}",VALUE="#{value}")
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # DiscontinuityItem represents a EXT-X-DISCONTINUITY tag to indicate a
4
5
  # discontinuity between the SegmentItems that proceed and follow it.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # Encapsulates logic common to encryption key tags
4
5
  module Encryptable
data/lib/m3u8/error.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  class InvalidPlaylistError < StandardError
4
5
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module M3u8
4
+ # GapItem represents an EXT-X-GAP tag to indicate that the segment URI
5
+ # to which it applies does not contain media data and should not be
6
+ # loaded by clients.
7
+ class GapItem
8
+ def to_s
9
+ '#EXT-X-GAP'
10
+ end
11
+ end
12
+ end
data/lib/m3u8/key_item.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module M3u8
3
4
  # KeyItem represents a set of EXT-X-KEY attributes
4
5
  class KeyItem