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.
- checksums.yaml +5 -5
- data/.github/workflows/ci.yml +23 -0
- data/.rubocop.yml +31 -0
- data/AGENTS.md +24 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +1 -0
- data/Gemfile +1 -0
- data/Guardfile +1 -0
- data/README.md +141 -40
- data/Rakefile +1 -0
- data/lib/m3u8/bitrate_item.rb +24 -0
- data/lib/m3u8/byte_range.rb +2 -0
- data/lib/m3u8/content_steering_item.rb +46 -0
- data/lib/m3u8/date_range_item.rb +18 -5
- data/lib/m3u8/define_item.rb +44 -0
- data/lib/m3u8/discontinuity_item.rb +1 -0
- data/lib/m3u8/encryptable.rb +1 -0
- data/lib/m3u8/error.rb +1 -0
- data/lib/m3u8/gap_item.rb +12 -0
- data/lib/m3u8/key_item.rb +1 -0
- data/lib/m3u8/map_item.rb +3 -0
- data/lib/m3u8/media_item.rb +43 -3
- data/lib/m3u8/part_inf_item.rb +28 -0
- data/lib/m3u8/part_item.rb +69 -0
- data/lib/m3u8/playback_start.rb +3 -0
- data/lib/m3u8/playlist.rb +10 -3
- data/lib/m3u8/playlist_item.rb +113 -8
- data/lib/m3u8/preload_hint_item.rb +66 -0
- data/lib/m3u8/reader.rb +69 -6
- data/lib/m3u8/rendition_report_item.rb +58 -0
- data/lib/m3u8/segment_item.rb +16 -2
- data/lib/m3u8/server_control_item.rb +75 -0
- data/lib/m3u8/session_data_item.rb +2 -0
- data/lib/m3u8/session_key_item.rb +1 -0
- data/lib/m3u8/skip_item.rb +48 -0
- data/lib/m3u8/time_item.rb +3 -0
- data/lib/m3u8/version.rb +1 -1
- data/lib/m3u8/writer.rb +17 -0
- data/lib/m3u8.rb +8 -5
- data/m3u8.gemspec +3 -2
- data/spec/fixtures/content_steering.m3u8 +10 -0
- data/spec/fixtures/daterange_playlist.m3u8 +14 -0
- data/spec/fixtures/encrypted_discontinuity.m3u8 +17 -0
- data/spec/fixtures/event_playlist.m3u8 +18 -0
- data/spec/fixtures/gap_playlist.m3u8 +14 -0
- data/spec/fixtures/ll_hls_advanced.m3u8 +18 -0
- data/spec/fixtures/ll_hls_playlist.m3u8 +20 -0
- data/spec/fixtures/master_full.m3u8 +14 -0
- data/spec/fixtures/master_v13.m3u8 +8 -0
- data/spec/lib/m3u8/bitrate_item_spec.rb +26 -0
- data/spec/lib/m3u8/byte_range_spec.rb +1 -0
- data/spec/lib/m3u8/content_steering_item_spec.rb +56 -0
- data/spec/lib/m3u8/date_range_item_spec.rb +20 -19
- data/spec/lib/m3u8/define_item_spec.rb +59 -0
- data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -0
- data/spec/lib/m3u8/gap_item_spec.rb +12 -0
- data/spec/lib/m3u8/key_item_spec.rb +1 -0
- data/spec/lib/m3u8/map_item_spec.rb +1 -0
- data/spec/lib/m3u8/media_item_spec.rb +34 -0
- data/spec/lib/m3u8/part_inf_item_spec.rb +27 -0
- data/spec/lib/m3u8/part_item_spec.rb +60 -0
- data/spec/lib/m3u8/playback_start_spec.rb +1 -0
- data/spec/lib/m3u8/playlist_item_spec.rb +115 -10
- data/spec/lib/m3u8/playlist_spec.rb +21 -10
- data/spec/lib/m3u8/preload_hint_item_spec.rb +57 -0
- data/spec/lib/m3u8/reader_spec.rb +314 -1
- data/spec/lib/m3u8/rendition_report_item_spec.rb +56 -0
- data/spec/lib/m3u8/round_trip_spec.rb +158 -0
- data/spec/lib/m3u8/segment_item_spec.rb +13 -0
- data/spec/lib/m3u8/server_control_item_spec.rb +64 -0
- data/spec/lib/m3u8/session_data_item_spec.rb +1 -0
- data/spec/lib/m3u8/session_key_item_spec.rb +1 -0
- data/spec/lib/m3u8/skip_item_spec.rb +48 -0
- data/spec/lib/m3u8/time_item_spec.rb +1 -0
- data/spec/lib/m3u8/writer_spec.rb +59 -21
- data/spec/lib/m3u8_spec.rb +1 -0
- data/spec/spec_helper.rb +2 -87
- metadata +58 -42
- data/.travis.yml +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2be533f088c62f6247e71b571a4481c92e56f8d575511ecae16be472dd90006a
|
|
4
|
+
data.tar.gz: 3d6de6bef331beefea33a156f6129dbcb30c89b195d807440acd740b53027a1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/Guardfile
CHANGED
data/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
[](http://badge.fury.io/rb/m3u8)
|
|
2
|
-
[](https://coveralls.io/github/sethdeckard/m3u8?branch=master)
|
|
4
|
-
[](https://codeclimate.com/github/sethdeckard/m3u8)
|
|
5
|
-
[](https://gemnasium.com/sethdeckard/m3u8)
|
|
6
|
-
[](https://hakiri.io/github/sethdeckard/m3u8/master)
|
|
2
|
+
[](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://
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
163
|
+
Parse an LL-HLS playlist:
|
|
115
164
|
|
|
116
165
|
```ruby
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
#
|
|
121
|
-
playlist.
|
|
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
|
-
##
|
|
127
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
@@ -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
|
data/lib/m3u8/byte_range.rb
CHANGED
|
@@ -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
|
data/lib/m3u8/date_range_item.rb
CHANGED
|
@@ -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')
|
|
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
|
-
|
|
88
|
+
val = value.to_s
|
|
89
|
+
return true if val =~ /\A\d+\Z/
|
|
90
|
+
|
|
82
91
|
begin
|
|
83
|
-
|
|
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
|
data/lib/m3u8/encryptable.rb
CHANGED
data/lib/m3u8/error.rb
CHANGED
|
@@ -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
|