m3u8 1.1.0 → 1.2.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 +6 -0
- data/CLAUDE.md +1 -1
- data/README.md +69 -0
- data/lib/m3u8/builder.rb +47 -0
- data/lib/m3u8/playlist.rb +11 -0
- data/lib/m3u8/version.rb +1 -1
- data/spec/lib/m3u8/builder_spec.rb +338 -0
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a9a7d6e157afd5fd7028323544bf9d851bd898890f90c66af6f64e672d7e456
|
|
4
|
+
data.tar.gz: ee452e25dfb96ee9634e9a00c287a87ac4640dceb2d965550e31199e0c0b4e26
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6b458aaf9a1a1060d19dca456020b54813f0c1f011615593e088a512a87ed32120cd5d509fb66dd3b12f85fe11a255304eede7852a00197cfd9513c918a33eb
|
|
7
|
+
data.tar.gz: 7c543fd8e6004adaf275adfe95c231b42fe0549805436f8dea3e77f2731f273084c99887781d0b69daf05ec1e53218fb8711be895fc0c14e9f0687f983a611b1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
**1.2.0**
|
|
2
|
+
|
|
3
|
+
* Added `Playlist.build` with block-based Builder DSL for concise playlist construction. Supports both `instance_eval` (clean DSL) and yielded builder (outer scope access) forms. All 19 item types have corresponding DSL methods.
|
|
4
|
+
|
|
5
|
+
***
|
|
6
|
+
|
|
1
7
|
**1.1.0**
|
|
2
8
|
|
|
3
9
|
* Added convenience accessor methods to `Playlist` for filtering items by type: `segments`, `playlists`, `media_items`, `keys`, `maps`, `date_ranges`, `parts`, `session_data`.
|
data/CLAUDE.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
AGENTS.md
|
|
1
|
+
./AGENTS.md
|
data/README.md
CHANGED
|
@@ -30,6 +30,75 @@ Or install it yourself as:
|
|
|
30
30
|
|
|
31
31
|
$ gem install m3u8
|
|
32
32
|
|
|
33
|
+
## Usage (Builder DSL)
|
|
34
|
+
|
|
35
|
+
`Playlist.build` provides a block-based DSL for concise playlist construction. It supports two forms:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# instance_eval form (clean DSL)
|
|
39
|
+
playlist = M3u8::Playlist.build(version: 4, target: 12) do
|
|
40
|
+
segment duration: 11.34, segment: '1080-7mbps00000.ts'
|
|
41
|
+
segment duration: 11.26, segment: '1080-7mbps00001.ts'
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# yielded builder form (access outer scope)
|
|
45
|
+
playlist = M3u8::Playlist.build(version: 4) do |b|
|
|
46
|
+
files.each { |f| b.segment duration: 10.0, segment: f }
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Build a master playlist:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
playlist = M3u8::Playlist.build(independent_segments: true) do
|
|
54
|
+
media type: 'AUDIO', group_id: 'audio', name: 'English',
|
|
55
|
+
default: true, uri: 'eng/index.m3u8'
|
|
56
|
+
playlist bandwidth: 5_042_000, width: 1920, height: 1080,
|
|
57
|
+
profile: 'high', level: 4.1, audio_codec: 'aac-lc',
|
|
58
|
+
uri: 'hls/1080.m3u8'
|
|
59
|
+
playlist bandwidth: 2_387_000, width: 1280, height: 720,
|
|
60
|
+
profile: 'main', level: 3.1, audio_codec: 'aac-lc',
|
|
61
|
+
uri: 'hls/720.m3u8'
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Build a media playlist:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
playlist = M3u8::Playlist.build(version: 4, target: 12,
|
|
69
|
+
sequence: 1, type: 'VOD') do
|
|
70
|
+
key method: 'AES-128', uri: 'https://example.com/key.bin'
|
|
71
|
+
map uri: 'init.mp4'
|
|
72
|
+
segment duration: 11.34, segment: '00000.ts'
|
|
73
|
+
discontinuity
|
|
74
|
+
segment duration: 11.26, segment: '00001.ts'
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Build an LL-HLS playlist:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
sc = M3u8::ServerControlItem.new(
|
|
82
|
+
can_skip_until: 24.0, part_hold_back: 1.0,
|
|
83
|
+
can_block_reload: true
|
|
84
|
+
)
|
|
85
|
+
pi = M3u8::PartInfItem.new(part_target: 0.5)
|
|
86
|
+
|
|
87
|
+
playlist = M3u8::Playlist.build(
|
|
88
|
+
version: 9, target: 4, sequence: 100,
|
|
89
|
+
server_control: sc, part_inf: pi, live: true
|
|
90
|
+
) do
|
|
91
|
+
map uri: 'init.mp4'
|
|
92
|
+
segment duration: 4.0, segment: 'seg100.mp4'
|
|
93
|
+
part duration: 0.5, uri: 'seg101.0.mp4', independent: true
|
|
94
|
+
preload_hint type: 'PART', uri: 'seg101.1.mp4'
|
|
95
|
+
rendition_report uri: '../alt/index.m3u8',
|
|
96
|
+
last_msn: 101, last_part: 0
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
All DSL methods correspond to item classes: `segment`, `playlist`, `media`, `session_data`, `session_key`, `content_steering`, `key`, `map`, `date_range`, `discontinuity`, `gap`, `time`, `bitrate`, `part`, `preload_hint`, `rendition_report`, `skip`, `define`, `playback_start`.
|
|
101
|
+
|
|
33
102
|
## Usage (creating playlists)
|
|
34
103
|
|
|
35
104
|
Create a master playlist and add child playlists for adaptive bitrate streaming:
|
data/lib/m3u8/builder.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M3u8
|
|
4
|
+
# Builder provides a block-based DSL for constructing playlists
|
|
5
|
+
class Builder
|
|
6
|
+
ITEMS = {
|
|
7
|
+
segment: 'SegmentItem',
|
|
8
|
+
playlist: 'PlaylistItem',
|
|
9
|
+
media: 'MediaItem',
|
|
10
|
+
session_data: 'SessionDataItem',
|
|
11
|
+
session_key: 'SessionKeyItem',
|
|
12
|
+
content_steering: 'ContentSteeringItem',
|
|
13
|
+
key: 'KeyItem',
|
|
14
|
+
map: 'MapItem',
|
|
15
|
+
date_range: 'DateRangeItem',
|
|
16
|
+
time: 'TimeItem',
|
|
17
|
+
bitrate: 'BitrateItem',
|
|
18
|
+
part: 'PartItem',
|
|
19
|
+
preload_hint: 'PreloadHintItem',
|
|
20
|
+
rendition_report: 'RenditionReportItem',
|
|
21
|
+
skip: 'SkipItem',
|
|
22
|
+
define: 'DefineItem',
|
|
23
|
+
playback_start: 'PlaybackStart'
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
ZERO_ARG_ITEMS = {
|
|
27
|
+
discontinuity: 'DiscontinuityItem',
|
|
28
|
+
gap: 'GapItem'
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def initialize(playlist)
|
|
32
|
+
@playlist = playlist
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
ITEMS.each do |method_name, class_name|
|
|
36
|
+
define_method(method_name) do |params = {}|
|
|
37
|
+
@playlist.items << M3u8.const_get(class_name).new(params)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
ZERO_ARG_ITEMS.each do |method_name, class_name|
|
|
42
|
+
define_method(method_name) do
|
|
43
|
+
@playlist.items << M3u8.const_get(class_name).new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/m3u8/playlist.rb
CHANGED
|
@@ -14,6 +14,17 @@ module M3u8
|
|
|
14
14
|
@items = []
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
def self.build(options = {}, &block)
|
|
18
|
+
playlist = new(options)
|
|
19
|
+
builder = Builder.new(playlist)
|
|
20
|
+
if block.arity == 1
|
|
21
|
+
yield builder
|
|
22
|
+
else
|
|
23
|
+
builder.instance_eval(&block)
|
|
24
|
+
end
|
|
25
|
+
playlist
|
|
26
|
+
end
|
|
27
|
+
|
|
17
28
|
def self.codecs(options = {})
|
|
18
29
|
item = PlaylistItem.new(options)
|
|
19
30
|
item.codecs
|
data/lib/m3u8/version.rb
CHANGED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe M3u8::Builder do
|
|
6
|
+
shared_examples 'a builder method' do |method, klass, params, checks|
|
|
7
|
+
it "adds a #{klass} to the playlist" do
|
|
8
|
+
pl = M3u8::Playlist.build { send(method, **params) }
|
|
9
|
+
|
|
10
|
+
item = pl.items.first
|
|
11
|
+
expect(item).to be_a(klass)
|
|
12
|
+
checks.each do |attr, value|
|
|
13
|
+
expect(item.public_send(attr)).to eq(value)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
shared_examples 'a zero-arg builder method' do |method, klass|
|
|
19
|
+
it "adds a #{klass} to the playlist" do
|
|
20
|
+
pl = M3u8::Playlist.build { send(method) }
|
|
21
|
+
expect(pl.items.first).to be_a(klass)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe '#segment' do
|
|
26
|
+
include_examples 'a builder method',
|
|
27
|
+
:segment, M3u8::SegmentItem,
|
|
28
|
+
{ duration: 11.34,
|
|
29
|
+
segment: '1080-7mbps00000.ts' },
|
|
30
|
+
{ duration: 11.34,
|
|
31
|
+
segment: '1080-7mbps00000.ts' }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe '#playlist' do
|
|
35
|
+
include_examples 'a builder method',
|
|
36
|
+
:playlist, M3u8::PlaylistItem,
|
|
37
|
+
{ bandwidth: 540, uri: 'test.url' },
|
|
38
|
+
{ bandwidth: 540, uri: 'test.url' }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#media' do
|
|
42
|
+
include_examples 'a builder method',
|
|
43
|
+
:media, M3u8::MediaItem,
|
|
44
|
+
{ type: 'AUDIO', group_id: 'audio-lo',
|
|
45
|
+
name: 'English', default: true,
|
|
46
|
+
uri: 'eng/prog_index.m3u8' },
|
|
47
|
+
{ type: 'AUDIO', group_id: 'audio-lo',
|
|
48
|
+
name: 'English' }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe '#session_data' do
|
|
52
|
+
include_examples 'a builder method',
|
|
53
|
+
:session_data, M3u8::SessionDataItem,
|
|
54
|
+
{ data_id: 'com.example.title',
|
|
55
|
+
value: 'My Video', language: 'en' },
|
|
56
|
+
{ data_id: 'com.example.title',
|
|
57
|
+
value: 'My Video' }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe '#session_key' do
|
|
61
|
+
include_examples 'a builder method',
|
|
62
|
+
:session_key, M3u8::SessionKeyItem,
|
|
63
|
+
{ method: 'AES-128',
|
|
64
|
+
uri: 'https://example.com/key.bin' },
|
|
65
|
+
{ method: 'AES-128' }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe '#content_steering' do
|
|
69
|
+
include_examples 'a builder method',
|
|
70
|
+
:content_steering,
|
|
71
|
+
M3u8::ContentSteeringItem,
|
|
72
|
+
{ server_uri: 'https://example.com/s',
|
|
73
|
+
pathway_id: 'CDN-A' },
|
|
74
|
+
{ server_uri: 'https://example.com/s',
|
|
75
|
+
pathway_id: 'CDN-A' }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe '#key' do
|
|
79
|
+
include_examples 'a builder method',
|
|
80
|
+
:key, M3u8::KeyItem,
|
|
81
|
+
{ method: 'AES-128',
|
|
82
|
+
uri: 'https://example.com/key.bin' },
|
|
83
|
+
{ method: 'AES-128' }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
describe '#map' do
|
|
87
|
+
it 'adds a MapItem to the playlist' do
|
|
88
|
+
pl = M3u8::Playlist.build do
|
|
89
|
+
map uri: 'init.mp4',
|
|
90
|
+
byterange: { length: 812, start: 0 }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
item = pl.items.first
|
|
94
|
+
expect(item).to be_a(M3u8::MapItem)
|
|
95
|
+
expect(item.uri).to eq('init.mp4')
|
|
96
|
+
expect(item.byterange.length).to eq(812)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#date_range' do
|
|
101
|
+
include_examples 'a builder method',
|
|
102
|
+
:date_range, M3u8::DateRangeItem,
|
|
103
|
+
{ id: 'ad-break-1',
|
|
104
|
+
start_date: '2024-06-01T12:00:00Z',
|
|
105
|
+
planned_duration: 30.0 },
|
|
106
|
+
{ id: 'ad-break-1',
|
|
107
|
+
planned_duration: 30.0 }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
describe '#discontinuity' do
|
|
111
|
+
include_examples 'a zero-arg builder method',
|
|
112
|
+
:discontinuity, M3u8::DiscontinuityItem
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
describe '#gap' do
|
|
116
|
+
include_examples 'a zero-arg builder method',
|
|
117
|
+
:gap, M3u8::GapItem
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe '#time' do
|
|
121
|
+
include_examples 'a builder method',
|
|
122
|
+
:time, M3u8::TimeItem,
|
|
123
|
+
{ time: '2024-06-01T12:00:00Z' },
|
|
124
|
+
{ time: '2024-06-01T12:00:00Z' }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
describe '#bitrate' do
|
|
128
|
+
include_examples 'a builder method',
|
|
129
|
+
:bitrate, M3u8::BitrateItem,
|
|
130
|
+
{ bitrate: 1500 },
|
|
131
|
+
{ bitrate: 1500 }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
describe '#part' do
|
|
135
|
+
include_examples 'a builder method',
|
|
136
|
+
:part, M3u8::PartItem,
|
|
137
|
+
{ duration: 0.5, uri: 'seg101.0.mp4',
|
|
138
|
+
independent: true },
|
|
139
|
+
{ duration: 0.5, uri: 'seg101.0.mp4' }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe '#preload_hint' do
|
|
143
|
+
include_examples 'a builder method',
|
|
144
|
+
:preload_hint, M3u8::PreloadHintItem,
|
|
145
|
+
{ type: 'PART', uri: 'seg101.1.mp4' },
|
|
146
|
+
{ type: 'PART', uri: 'seg101.1.mp4' }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
describe '#rendition_report' do
|
|
150
|
+
include_examples 'a builder method',
|
|
151
|
+
:rendition_report,
|
|
152
|
+
M3u8::RenditionReportItem,
|
|
153
|
+
{ uri: '../alt/index.m3u8',
|
|
154
|
+
last_msn: 101, last_part: 0 },
|
|
155
|
+
{ uri: '../alt/index.m3u8',
|
|
156
|
+
last_msn: 101 }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe '#skip' do
|
|
160
|
+
include_examples 'a builder method',
|
|
161
|
+
:skip, M3u8::SkipItem,
|
|
162
|
+
{ skipped_segments: 10 },
|
|
163
|
+
{ skipped_segments: 10 }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
describe '#define' do
|
|
167
|
+
include_examples 'a builder method',
|
|
168
|
+
:define, M3u8::DefineItem,
|
|
169
|
+
{ name: 'base',
|
|
170
|
+
value: 'https://example.com' },
|
|
171
|
+
{ name: 'base',
|
|
172
|
+
value: 'https://example.com' }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
describe '#playback_start' do
|
|
176
|
+
include_examples 'a builder method',
|
|
177
|
+
:playback_start, M3u8::PlaybackStart,
|
|
178
|
+
{ time_offset: 10.0, precise: true },
|
|
179
|
+
{ time_offset: 10.0 }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe 'Playlist.build' do
|
|
183
|
+
it 'returns a Playlist with options' do
|
|
184
|
+
playlist = M3u8::Playlist.build(version: 4,
|
|
185
|
+
target: 12) do
|
|
186
|
+
segment duration: 11.34, segment: 'test.ts'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
expect(playlist).to be_a(M3u8::Playlist)
|
|
190
|
+
expect(playlist.version).to eq(4)
|
|
191
|
+
expect(playlist.target).to eq(12)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'supports yielded builder form' do
|
|
195
|
+
files = %w[seg1.ts seg2.ts]
|
|
196
|
+
playlist = M3u8::Playlist.build(version: 4) do |b|
|
|
197
|
+
files.each do |f|
|
|
198
|
+
b.segment duration: 10.0, segment: f
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
expect(playlist.items.size).to eq(2)
|
|
203
|
+
expect(playlist.items[0].segment).to eq('seg1.ts')
|
|
204
|
+
expect(playlist.items[1].segment).to eq('seg2.ts')
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe 'integration' do
|
|
209
|
+
it 'produces identical output to imperative API ' \
|
|
210
|
+
'for a master playlist' do
|
|
211
|
+
imperative = M3u8::Playlist.new(
|
|
212
|
+
independent_segments: true
|
|
213
|
+
)
|
|
214
|
+
imperative.items << M3u8::PlaylistItem.new(
|
|
215
|
+
program_id: '1', bandwidth: 6400,
|
|
216
|
+
audio_codec: 'mp3', uri: 'lo/index.m3u8'
|
|
217
|
+
)
|
|
218
|
+
imperative.items << M3u8::PlaylistItem.new(
|
|
219
|
+
program_id: '2', bandwidth: 50_000,
|
|
220
|
+
width: 1920, height: 1080,
|
|
221
|
+
profile: 'high', level: 4.1,
|
|
222
|
+
audio_codec: 'aac-lc', uri: 'hi/index.m3u8'
|
|
223
|
+
)
|
|
224
|
+
imperative.items << M3u8::SessionDataItem.new(
|
|
225
|
+
data_id: 'com.test.title', value: 'Test',
|
|
226
|
+
language: 'en'
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
built = M3u8::Playlist.build(
|
|
230
|
+
independent_segments: true
|
|
231
|
+
) do
|
|
232
|
+
playlist program_id: '1', bandwidth: 6400,
|
|
233
|
+
audio_codec: 'mp3', uri: 'lo/index.m3u8'
|
|
234
|
+
playlist program_id: '2', bandwidth: 50_000,
|
|
235
|
+
width: 1920, height: 1080,
|
|
236
|
+
profile: 'high', level: 4.1,
|
|
237
|
+
audio_codec: 'aac-lc',
|
|
238
|
+
uri: 'hi/index.m3u8'
|
|
239
|
+
session_data data_id: 'com.test.title',
|
|
240
|
+
value: 'Test', language: 'en'
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
expect(built.to_s).to eq(imperative.to_s)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
it 'produces identical output to imperative API ' \
|
|
247
|
+
'for a media playlist' do
|
|
248
|
+
imperative = M3u8::Playlist.new(
|
|
249
|
+
version: 7, cache: false, target: 12,
|
|
250
|
+
sequence: 1, type: 'VOD'
|
|
251
|
+
)
|
|
252
|
+
imperative.items << M3u8::KeyItem.new(
|
|
253
|
+
method: 'AES-128', uri: 'http://test.key',
|
|
254
|
+
iv: 'D512BBF', key_format: 'identity',
|
|
255
|
+
key_format_versions: '1/3'
|
|
256
|
+
)
|
|
257
|
+
imperative.items << M3u8::SegmentItem.new(
|
|
258
|
+
duration: 11.344644, segment: '00000.ts'
|
|
259
|
+
)
|
|
260
|
+
imperative.items << M3u8::DiscontinuityItem.new
|
|
261
|
+
imperative.items << M3u8::TimeItem.new(
|
|
262
|
+
time: '2024-06-01T12:00:00Z'
|
|
263
|
+
)
|
|
264
|
+
imperative.items << M3u8::SegmentItem.new(
|
|
265
|
+
duration: 11.261233, segment: '00001.ts'
|
|
266
|
+
)
|
|
267
|
+
imperative.items << M3u8::MapItem.new(
|
|
268
|
+
uri: 'init.mp4',
|
|
269
|
+
byterange: { length: 812, start: 0 }
|
|
270
|
+
)
|
|
271
|
+
imperative.items << M3u8::SegmentItem.new(
|
|
272
|
+
duration: 7.5, segment: '00002.ts'
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
built = M3u8::Playlist.build(
|
|
276
|
+
version: 7, cache: false, target: 12,
|
|
277
|
+
sequence: 1, type: 'VOD'
|
|
278
|
+
) do
|
|
279
|
+
key method: 'AES-128', uri: 'http://test.key',
|
|
280
|
+
iv: 'D512BBF', key_format: 'identity',
|
|
281
|
+
key_format_versions: '1/3'
|
|
282
|
+
segment duration: 11.344644, segment: '00000.ts'
|
|
283
|
+
discontinuity
|
|
284
|
+
time time: '2024-06-01T12:00:00Z'
|
|
285
|
+
segment duration: 11.261233, segment: '00001.ts'
|
|
286
|
+
map uri: 'init.mp4',
|
|
287
|
+
byterange: { length: 812, start: 0 }
|
|
288
|
+
segment duration: 7.5, segment: '00002.ts'
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
expect(built.to_s).to eq(imperative.to_s)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
it 'produces identical output to imperative API ' \
|
|
295
|
+
'for an LL-HLS playlist' do
|
|
296
|
+
sc = M3u8::ServerControlItem.new(
|
|
297
|
+
can_skip_until: 24.0, part_hold_back: 1.0,
|
|
298
|
+
can_block_reload: true
|
|
299
|
+
)
|
|
300
|
+
pi = M3u8::PartInfItem.new(part_target: 0.5)
|
|
301
|
+
opts = {
|
|
302
|
+
version: 9, target: 4, sequence: 100,
|
|
303
|
+
server_control: sc, part_inf: pi, live: true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
imperative = M3u8::Playlist.new(opts)
|
|
307
|
+
imperative.items << M3u8::MapItem.new(
|
|
308
|
+
uri: 'init.mp4'
|
|
309
|
+
)
|
|
310
|
+
imperative.items << M3u8::SegmentItem.new(
|
|
311
|
+
duration: 4.0, segment: 'seg100.mp4'
|
|
312
|
+
)
|
|
313
|
+
imperative.items << M3u8::PartItem.new(
|
|
314
|
+
duration: 0.5, uri: 'seg101.0.mp4',
|
|
315
|
+
independent: true
|
|
316
|
+
)
|
|
317
|
+
imperative.items << M3u8::PreloadHintItem.new(
|
|
318
|
+
type: 'PART', uri: 'seg101.1.mp4'
|
|
319
|
+
)
|
|
320
|
+
imperative.items << M3u8::RenditionReportItem.new(
|
|
321
|
+
uri: '../alt/index.m3u8',
|
|
322
|
+
last_msn: 101, last_part: 0
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
built = M3u8::Playlist.build(opts) do
|
|
326
|
+
map uri: 'init.mp4'
|
|
327
|
+
segment duration: 4.0, segment: 'seg100.mp4'
|
|
328
|
+
part duration: 0.5, uri: 'seg101.0.mp4',
|
|
329
|
+
independent: true
|
|
330
|
+
preload_hint type: 'PART', uri: 'seg101.1.mp4'
|
|
331
|
+
rendition_report uri: '../alt/index.m3u8',
|
|
332
|
+
last_msn: 101, last_part: 0
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
expect(built.to_s).to eq(imperative.to_s)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
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.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Seth Deckard
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -144,6 +144,7 @@ files:
|
|
|
144
144
|
- Rakefile
|
|
145
145
|
- lib/m3u8.rb
|
|
146
146
|
- lib/m3u8/bitrate_item.rb
|
|
147
|
+
- lib/m3u8/builder.rb
|
|
147
148
|
- lib/m3u8/byte_range.rb
|
|
148
149
|
- lib/m3u8/content_steering_item.rb
|
|
149
150
|
- lib/m3u8/date_range_item.rb
|
|
@@ -194,6 +195,7 @@ files:
|
|
|
194
195
|
- spec/fixtures/variant_angles.m3u8
|
|
195
196
|
- spec/fixtures/variant_audio.m3u8
|
|
196
197
|
- spec/lib/m3u8/bitrate_item_spec.rb
|
|
198
|
+
- spec/lib/m3u8/builder_spec.rb
|
|
197
199
|
- spec/lib/m3u8/byte_range_spec.rb
|
|
198
200
|
- spec/lib/m3u8/content_steering_item_spec.rb
|
|
199
201
|
- spec/lib/m3u8/date_range_item_spec.rb
|
|
@@ -225,7 +227,7 @@ homepage: https://github.com/sethdeckard/m3u8
|
|
|
225
227
|
licenses:
|
|
226
228
|
- MIT
|
|
227
229
|
metadata: {}
|
|
228
|
-
post_install_message:
|
|
230
|
+
post_install_message:
|
|
229
231
|
rdoc_options: []
|
|
230
232
|
require_paths:
|
|
231
233
|
- lib
|
|
@@ -240,8 +242,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
240
242
|
- !ruby/object:Gem::Version
|
|
241
243
|
version: '0'
|
|
242
244
|
requirements: []
|
|
243
|
-
rubygems_version: 3.
|
|
244
|
-
signing_key:
|
|
245
|
+
rubygems_version: 3.0.3.1
|
|
246
|
+
signing_key:
|
|
245
247
|
specification_version: 4
|
|
246
248
|
summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
|
|
247
249
|
test_files: []
|