m3u8 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +27 -4
- data/lib/m3u8.rb +3 -0
- data/lib/m3u8/playlist.rb +62 -42
- data/lib/m3u8/playlist_item.rb +28 -0
- data/lib/m3u8/reader.rb +117 -0
- data/lib/m3u8/segment_item.rb +15 -0
- data/lib/m3u8/version.rb +1 -1
- data/m3u8.gemspec +2 -2
- data/spec/fixtures/master.m3u8 +13 -0
- data/spec/fixtures/playlist.m3u8 +282 -0
- data/spec/playlist_item_spec.rb +33 -0
- data/spec/playlist_spec.rb +52 -3
- data/spec/reader_spec.rb +42 -0
- data/spec/segment_item_spec.rb +18 -0
- data/spec/spec_helper.rb +3 -0
- metadata +17 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 862441ecee4bbc8fe057efdefe16649ba31b6276
|
4
|
+
data.tar.gz: 2f9995c6089dcb771fc6a6781936952b4ad03200
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94a2443f80f7f7f8d780bdd2d0fbc7ef2b8fe22c79e4753a292e1e48dfa740fd02817774408658adf2545fb82aecd93d56be156a1b0affe18d302376f2ff5394
|
7
|
+
data.tar.gz: 278b7d96d867e107d693874a35ee101c10b51800379b261e6d7b7c7728c55fec0b8a37e08af6080a01bd9e303c35edfbaff496d85c0eb1b6f0c1b473368b0bbe
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
[![security](https://hakiri.io/github/sethdeckard/m3u8/master.svg)](https://hakiri.io/github/sethdeckard/m3u8/master)
|
7
7
|
# m3u8
|
8
8
|
|
9
|
-
m3u8 provides generation of m3u8 playlists used the [HTTP Live Streaming](https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1) (HLS) specification created by Apple. This is useful if you wish to generate m3u8 playlists on the fly in your web application (to integrate authentication, do something custom, etc) while of course serving up the actual MPEG transport stream files (.ts) from a CDN. You could also use m3u8 to generate playlist files as part of an encoding pipeline.
|
9
|
+
m3u8 provides generation and parsing of m3u8 playlists used the [HTTP Live Streaming](https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1) (HLS) specification created by Apple. This is useful if you wish to generate m3u8 playlists on the fly in your web application (to integrate authentication, do something custom, etc) while of course serving up the actual MPEG transport stream files (.ts) from a CDN. You could also use m3u8 to generate playlist files as part of an encoding pipeline. You can also parse existing playlists, add content to them and generate a new output.
|
10
10
|
|
11
11
|
## Installation
|
12
12
|
|
@@ -24,7 +24,7 @@ Or install it yourself as:
|
|
24
24
|
|
25
25
|
$ gem install m3u8
|
26
26
|
|
27
|
-
## Usage
|
27
|
+
## Usage (Generation)
|
28
28
|
|
29
29
|
|
30
30
|
require 'm3u8'
|
@@ -38,10 +38,14 @@ Or install it yourself as:
|
|
38
38
|
playlist = M3u8::Playlist.new
|
39
39
|
playlist.add_segment 11.344644, "1080-7mbps00000.ts"
|
40
40
|
|
41
|
+
#you can also add PlaylistItem or SegmentItem instances directly to the array of items in the playlist (new in v.2.0):
|
42
|
+
item = SegmentItem.new(duration: 11, segment: 'test.ts')
|
43
|
+
playlist.items.push item
|
44
|
+
|
41
45
|
#just get the codec string for custom use
|
42
46
|
options = { :profile => 'baseline', :level => 3.0, :audio => 'aac-lc' }
|
43
47
|
codecs = M3u8::Playlist.codecs options
|
44
|
-
|
48
|
+
=> "avc1.66.30,mp4a.40.2"
|
45
49
|
|
46
50
|
#specify options for playlist, these are ignored if playlist becomes a master playlist (child playlist added):
|
47
51
|
options = { :version => 1, :cache => false, :target => 12, :sequence => 1}
|
@@ -65,13 +69,32 @@ Or install it yourself as:
|
|
65
69
|
#3.0, 3.1, 4.0, 4.1
|
66
70
|
|
67
71
|
#not all Levels and Profiles can be combined, consult H.264 documentation
|
72
|
+
|
73
|
+
## Parsing Usage (new in v.2.0)
|
74
|
+
|
75
|
+
file = File.open 'spec/fixtures/master.m3u8'
|
76
|
+
reader = M3u8::Reader.new
|
77
|
+
playlist = reader.read file
|
78
|
+
playlist.master?
|
79
|
+
=> true
|
80
|
+
|
81
|
+
#acess items in playlist:
|
82
|
+
playlist.items
|
83
|
+
|
84
|
+
playlist.items.first
|
85
|
+
=> #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080", @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000", @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
|
68
86
|
|
69
87
|
## Features
|
70
|
-
* Distinction between segment and master playlists
|
88
|
+
* Distinction between segment and master playlists is handled automatically (no need to use a different class).
|
71
89
|
* Automatically generates the audio/video codec string based on names and options you are familar with.
|
72
90
|
* Provides validation of input when adding playlists or segments.
|
73
91
|
* Allows all options to be configured on a playlist (caching, version, etc.)
|
74
92
|
* Can write playlist to an IO object (StringIO/File, etc) or access string via to_s.
|
93
|
+
* Can read playlists into a model (Playlist and Items) from an IO object.
|
94
|
+
|
95
|
+
## Missing (but planned) Features
|
96
|
+
* Support for encryption keys and DRM.
|
97
|
+
* Support for more obscure tags.
|
75
98
|
|
76
99
|
## Contributing
|
77
100
|
|
data/lib/m3u8.rb
CHANGED
data/lib/m3u8/playlist.rb
CHANGED
@@ -1,25 +1,30 @@
|
|
1
1
|
module M3u8
|
2
2
|
class Playlist
|
3
|
-
attr_accessor :
|
3
|
+
attr_accessor :items, :version, :cache, :target, :sequence
|
4
4
|
MISSING_CODEC_MESSAGE = 'An audio or video codec should be provided.'
|
5
5
|
NON_MASTER_ERROR_MESSAGE = 'Playlist is not a master playlist, playlist' \
|
6
6
|
' can not be added.'
|
7
7
|
MASTER_ERROR_MESSAGE = 'Playlist is a master playlist, segment can not ' \
|
8
8
|
'be added.'
|
9
|
+
MIXED_TYPE_ERROR_MESSAGE = 'Playlist contains mixed types of items'
|
9
10
|
|
10
11
|
def initialize(options = {})
|
11
|
-
|
12
|
+
assign_options options
|
13
|
+
self.items = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def assign_options(options)
|
17
|
+
options = {
|
12
18
|
version: 3,
|
13
19
|
sequence: 0,
|
14
20
|
cache: true,
|
15
21
|
target: 10
|
16
22
|
}.merge options
|
17
23
|
|
18
|
-
self.
|
19
|
-
self.
|
20
|
-
self.
|
21
|
-
self.
|
22
|
-
io.puts '#EXTM3U'
|
24
|
+
self.version = options[:version]
|
25
|
+
self.sequence = options[:sequence]
|
26
|
+
self.cache = options[:cache]
|
27
|
+
self.target = options[:target]
|
23
28
|
end
|
24
29
|
|
25
30
|
def self.codecs(options = {})
|
@@ -37,30 +42,24 @@ module M3u8
|
|
37
42
|
}.merge options
|
38
43
|
|
39
44
|
validate_playlist_type true
|
40
|
-
self.master = true
|
41
|
-
self.empty = false
|
42
45
|
|
43
|
-
resolution = resolution options[:width], options[:height]
|
44
46
|
codecs = codecs(audio: options[:audio], profile: options[:profile],
|
45
47
|
level: options[:level])
|
46
48
|
fail MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
|
50
|
+
params = { program_id: program_id, playlist: playlist, bitrate: bitrate,
|
51
|
+
width: options[:width], height: options[:height],
|
52
|
+
codecs: codecs }
|
53
|
+
item = PlaylistItem.new params
|
54
|
+
items.push item
|
50
55
|
end
|
51
56
|
|
52
57
|
def add_segment(duration, segment)
|
53
58
|
validate_playlist_type false
|
54
|
-
self.master = false
|
55
|
-
self.empty = false
|
56
|
-
|
57
|
-
unless header
|
58
|
-
write_header
|
59
|
-
self.header = true
|
60
|
-
end
|
61
59
|
|
62
|
-
|
63
|
-
|
60
|
+
params = { duration: duration, segment: segment }
|
61
|
+
item = SegmentItem.new params
|
62
|
+
items.push item
|
64
63
|
end
|
65
64
|
|
66
65
|
def codecs(options = {})
|
@@ -85,26 +84,44 @@ module M3u8
|
|
85
84
|
end
|
86
85
|
|
87
86
|
def write(output)
|
88
|
-
|
87
|
+
validate
|
88
|
+
|
89
|
+
output.puts '#EXTM3U'
|
90
|
+
write_header(output) unless master?
|
91
|
+
|
92
|
+
items.each do |item|
|
93
|
+
output.puts item.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
return if master?
|
97
|
+
output.puts '#EXT-X-ENDLIST'
|
89
98
|
end
|
90
99
|
|
91
100
|
def master?
|
92
|
-
return false if
|
93
|
-
|
101
|
+
return false if playlist_size == 0 && segment_size == 0
|
102
|
+
playlist_size > 0
|
94
103
|
end
|
95
104
|
|
96
105
|
def to_s
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
106
|
+
output = StringIO.open
|
107
|
+
write output
|
108
|
+
output.string
|
109
|
+
end
|
110
|
+
|
111
|
+
def valid?
|
112
|
+
return false if playlist_size > 0 && segment_size > 0
|
113
|
+
true
|
102
114
|
end
|
103
115
|
|
104
116
|
private
|
105
117
|
|
118
|
+
def validate
|
119
|
+
return if valid?
|
120
|
+
fail PlaylistTypeError, MIXED_TYPE_ERROR_MESSAGE
|
121
|
+
end
|
122
|
+
|
106
123
|
def validate_playlist_type(master)
|
107
|
-
return if
|
124
|
+
return if items.size == 0
|
108
125
|
if master && !master?
|
109
126
|
fail PlaylistTypeError, NON_MASTER_ERROR_MESSAGE
|
110
127
|
elsif !master && master?
|
@@ -112,15 +129,23 @@ module M3u8
|
|
112
129
|
end
|
113
130
|
end
|
114
131
|
|
115
|
-
def
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
132
|
+
def playlist_size
|
133
|
+
items.select { |item| item.is_a?(PlaylistItem) }.size
|
134
|
+
end
|
135
|
+
|
136
|
+
def segment_size
|
137
|
+
items.select { |item| item.is_a?(SegmentItem) }.size
|
138
|
+
end
|
139
|
+
|
140
|
+
def write_header(output)
|
141
|
+
output.puts "#EXT-X-VERSION:#{version}"
|
142
|
+
output.puts "#EXT-X-MEDIA-SEQUENCE:#{sequence}"
|
143
|
+
output.puts "#EXT-X-ALLOW-CACHE:#{cache_string}"
|
144
|
+
output.puts "#EXT-X-TARGETDURATION:#{target}"
|
120
145
|
end
|
121
146
|
|
122
147
|
def cache_string
|
123
|
-
|
148
|
+
cache ? 'YES' : 'NO'
|
124
149
|
end
|
125
150
|
|
126
151
|
def audio_codec(audio)
|
@@ -143,10 +168,5 @@ module M3u8
|
|
143
168
|
return 'avc1.640028' if profile == 'high' &&
|
144
169
|
(level == 4.0 || level == 4.1)
|
145
170
|
end
|
146
|
-
|
147
|
-
def resolution(width, height)
|
148
|
-
return if width.nil?
|
149
|
-
"RESOLUTION=#{width}x#{height},"
|
150
|
-
end
|
151
171
|
end
|
152
172
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module M3u8
|
2
|
+
class PlaylistItem
|
3
|
+
attr_accessor :program_id, :width, :height, :codecs, :bitrate, :playlist
|
4
|
+
|
5
|
+
def initialize(params = {})
|
6
|
+
params.each do |key, value|
|
7
|
+
instance_variable_set("@#{key}", value)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def resolution
|
12
|
+
return if width.nil?
|
13
|
+
"#{width}x#{height}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
"#EXT-X-STREAM-INF:PROGRAM-ID=#{program_id},#{resolution_format}" +
|
18
|
+
%(CODECS="#{codecs}",BANDWIDTH=#{bitrate}\n#{playlist})
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def resolution_format
|
24
|
+
return if resolution.nil?
|
25
|
+
"RESOLUTION=#{resolution},"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/m3u8/reader.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
module M3u8
|
2
|
+
class Reader
|
3
|
+
attr_accessor :playlist, :item, :open, :master
|
4
|
+
PLAYLIST_START = '#EXTM3U'
|
5
|
+
VERSION_START = '#EXT-X-VERSION:'
|
6
|
+
SEQUENCE_START = '#EXT-X-MEDIA-SEQUENCE:'
|
7
|
+
CACHE_START = '#EXT-X-ALLOW-CACHE:'
|
8
|
+
TARGET_START = '#EXT-X-TARGETDURATION:'
|
9
|
+
STREAM_START = '#EXT-X-STREAM-INF:'
|
10
|
+
SEGMENT_START = '#EXTINF:'
|
11
|
+
PROGRAM_ID = 'PROGRAM-ID'
|
12
|
+
RESOLUTION = 'RESOLUTION'
|
13
|
+
CODECS = 'CODECS'
|
14
|
+
BANDWIDTH = 'BANDWIDTH'
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
self.playlist = Playlist.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def read(input)
|
21
|
+
input.each_line do |line|
|
22
|
+
parse_line line
|
23
|
+
end
|
24
|
+
playlist
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def parse_line(line)
|
30
|
+
return if line.start_with? PLAYLIST_START
|
31
|
+
|
32
|
+
if line.start_with? VERSION_START
|
33
|
+
parse_version line
|
34
|
+
elsif line.start_with? SEQUENCE_START
|
35
|
+
parse_sequence line
|
36
|
+
elsif line.start_with? CACHE_START
|
37
|
+
parse_cache line
|
38
|
+
elsif line.start_with? TARGET_START
|
39
|
+
parse_target line
|
40
|
+
elsif line.start_with? STREAM_START
|
41
|
+
parse_stream line
|
42
|
+
elsif line.start_with? SEGMENT_START
|
43
|
+
parse_segment line
|
44
|
+
elsif !item.nil? && open
|
45
|
+
parse_value(line)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_version(line)
|
50
|
+
playlist.version = line.gsub(VERSION_START, '').to_i
|
51
|
+
end
|
52
|
+
|
53
|
+
def parse_sequence(line)
|
54
|
+
playlist.sequence = line.gsub(SEQUENCE_START, '').to_i
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_cache(line)
|
58
|
+
line = line.gsub(CACHE_START, '')
|
59
|
+
playlist.cache = parse_yes_no(line)
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse_yes_no(string)
|
63
|
+
string == 'YES' ? true : false
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_target(line)
|
67
|
+
playlist.target = line.gsub(TARGET_START, '').to_i
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_stream(line)
|
71
|
+
self.master = true
|
72
|
+
self.open = true
|
73
|
+
|
74
|
+
self.item = M3u8::PlaylistItem.new
|
75
|
+
line = line.gsub STREAM_START, ''
|
76
|
+
attributes = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
|
77
|
+
attributes.each do |pair|
|
78
|
+
value = pair[1].gsub("\n", '').gsub('"', '')
|
79
|
+
case pair[0]
|
80
|
+
when PROGRAM_ID
|
81
|
+
item.program_id = value
|
82
|
+
when RESOLUTION
|
83
|
+
parse_resolution value
|
84
|
+
when CODECS
|
85
|
+
item.codecs = value
|
86
|
+
when BANDWIDTH
|
87
|
+
item.bitrate = value.to_i
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_segment(line)
|
93
|
+
self.master = false
|
94
|
+
self.open = true
|
95
|
+
|
96
|
+
self.item = M3u8::SegmentItem.new
|
97
|
+
item.duration = line.gsub(SEGMENT_START, '').gsub("\n", '').gsub(',', '')
|
98
|
+
.to_f
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_resolution(resolution)
|
102
|
+
item.width = resolution.split('x')[0].to_i
|
103
|
+
item.height = resolution.split('x')[1].to_i
|
104
|
+
end
|
105
|
+
|
106
|
+
def parse_value(line)
|
107
|
+
value = line.gsub "\n", ''
|
108
|
+
if master
|
109
|
+
item.playlist = value
|
110
|
+
else
|
111
|
+
item.segment = value
|
112
|
+
end
|
113
|
+
playlist.items.push item
|
114
|
+
self.open = false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module M3u8
|
2
|
+
class SegmentItem
|
3
|
+
attr_accessor :duration, :segment
|
4
|
+
|
5
|
+
def initialize(params = {})
|
6
|
+
params.each do |key, value|
|
7
|
+
instance_variable_set("@#{key}", value)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
"#EXTINF:#{duration},\n#{segment}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/m3u8/version.rb
CHANGED
data/m3u8.gemspec
CHANGED
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
8
8
|
spec.version = M3u8::VERSION
|
9
9
|
spec.authors = ["Seth Deckard"]
|
10
10
|
spec.email = ["seth@deckard.me"]
|
11
|
-
spec.summary = %q{Generate m3u8 playlists for HTTP Live Streaming (HLS).}
|
12
|
-
spec.description = %q{Generate m3u8 playlists for HTTP Live Streaming (HLS).}
|
11
|
+
spec.summary = %q{Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).}
|
12
|
+
spec.description = %q{Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).}
|
13
13
|
spec.homepage = "https://github.com/sethdeckard/m3u8"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#EXTM3U
|
2
|
+
#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=5042000
|
3
|
+
hls/1080-7mbps/1080-7mbps.m3u8
|
4
|
+
#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=4853000
|
5
|
+
hls/1080/1080.m3u8
|
6
|
+
#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=2387000
|
7
|
+
hls/720/720.m3u8
|
8
|
+
#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=896x504,CODECS="avc1.4d001f,mp4a.40.2",BANDWIDTH=1365000
|
9
|
+
hls/504/504.m3u8
|
10
|
+
#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=640x360,CODECS="avc1.66.30,mp4a.40.2",BANDWIDTH=861000
|
11
|
+
hls/360/360.m3u8
|
12
|
+
#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.2",BANDWIDTH=6400
|
13
|
+
hls/64k/64k.m3u8
|
@@ -0,0 +1,282 @@
|
|
1
|
+
#EXTM3U
|
2
|
+
#EXT-X-VERSION:4
|
3
|
+
#EXT-X-MEDIA-SEQUENCE:1
|
4
|
+
#EXT-X-ALLOW-CACHE:NO
|
5
|
+
#EXT-X-TARGETDURATION:12
|
6
|
+
#EXTINF:11.344644,
|
7
|
+
1080-7mbps00000.ts
|
8
|
+
#EXTINF:11.261233,
|
9
|
+
1080-7mbps00001.ts
|
10
|
+
#EXTINF:7.507489,
|
11
|
+
1080-7mbps00002.ts
|
12
|
+
#EXTINF:11.261233,
|
13
|
+
1080-7mbps00003.ts
|
14
|
+
#EXTINF:11.261233,
|
15
|
+
1080-7mbps00004.ts
|
16
|
+
#EXTINF:7.507489,
|
17
|
+
1080-7mbps00005.ts
|
18
|
+
#EXTINF:11.261233,
|
19
|
+
1080-7mbps00006.ts
|
20
|
+
#EXTINF:11.261233,
|
21
|
+
1080-7mbps00007.ts
|
22
|
+
#EXTINF:7.507478,
|
23
|
+
1080-7mbps00008.ts
|
24
|
+
#EXTINF:11.261233,
|
25
|
+
1080-7mbps00009.ts
|
26
|
+
#EXTINF:11.261233,
|
27
|
+
1080-7mbps00010.ts
|
28
|
+
#EXTINF:7.507478,
|
29
|
+
1080-7mbps00011.ts
|
30
|
+
#EXTINF:11.261233,
|
31
|
+
1080-7mbps00012.ts
|
32
|
+
#EXTINF:11.261233,
|
33
|
+
1080-7mbps00013.ts
|
34
|
+
#EXTINF:7.507489,
|
35
|
+
1080-7mbps00014.ts
|
36
|
+
#EXTINF:11.261233,
|
37
|
+
1080-7mbps00015.ts
|
38
|
+
#EXTINF:11.261233,
|
39
|
+
1080-7mbps00016.ts
|
40
|
+
#EXTINF:7.507489,
|
41
|
+
1080-7mbps00017.ts
|
42
|
+
#EXTINF:11.261233,
|
43
|
+
1080-7mbps00018.ts
|
44
|
+
#EXTINF:11.261233,
|
45
|
+
1080-7mbps00019.ts
|
46
|
+
#EXTINF:7.507478,
|
47
|
+
1080-7mbps00020.ts
|
48
|
+
#EXTINF:11.261233,
|
49
|
+
1080-7mbps00021.ts
|
50
|
+
#EXTINF:11.261233,
|
51
|
+
1080-7mbps00022.ts
|
52
|
+
#EXTINF:7.507478,
|
53
|
+
1080-7mbps00023.ts
|
54
|
+
#EXTINF:11.261233,
|
55
|
+
1080-7mbps00024.ts
|
56
|
+
#EXTINF:11.261233,
|
57
|
+
1080-7mbps00025.ts
|
58
|
+
#EXTINF:7.507489,
|
59
|
+
1080-7mbps00026.ts
|
60
|
+
#EXTINF:11.261233,
|
61
|
+
1080-7mbps00027.ts
|
62
|
+
#EXTINF:11.261233,
|
63
|
+
1080-7mbps00028.ts
|
64
|
+
#EXTINF:7.507489,
|
65
|
+
1080-7mbps00029.ts
|
66
|
+
#EXTINF:11.261233,
|
67
|
+
1080-7mbps00030.ts
|
68
|
+
#EXTINF:11.261233,
|
69
|
+
1080-7mbps00031.ts
|
70
|
+
#EXTINF:7.507478,
|
71
|
+
1080-7mbps00032.ts
|
72
|
+
#EXTINF:11.261233,
|
73
|
+
1080-7mbps00033.ts
|
74
|
+
#EXTINF:11.261233,
|
75
|
+
1080-7mbps00034.ts
|
76
|
+
#EXTINF:7.507489,
|
77
|
+
1080-7mbps00035.ts
|
78
|
+
#EXTINF:11.261233,
|
79
|
+
1080-7mbps00036.ts
|
80
|
+
#EXTINF:11.261222,
|
81
|
+
1080-7mbps00037.ts
|
82
|
+
#EXTINF:7.507489,
|
83
|
+
1080-7mbps00038.ts
|
84
|
+
#EXTINF:11.261233,
|
85
|
+
1080-7mbps00039.ts
|
86
|
+
#EXTINF:11.261233,
|
87
|
+
1080-7mbps00040.ts
|
88
|
+
#EXTINF:7.507489,
|
89
|
+
1080-7mbps00041.ts
|
90
|
+
#EXTINF:11.261233,
|
91
|
+
1080-7mbps00042.ts
|
92
|
+
#EXTINF:11.261233,
|
93
|
+
1080-7mbps00043.ts
|
94
|
+
#EXTINF:7.507478,
|
95
|
+
1080-7mbps00044.ts
|
96
|
+
#EXTINF:11.261233,
|
97
|
+
1080-7mbps00045.ts
|
98
|
+
#EXTINF:11.261233,
|
99
|
+
1080-7mbps00046.ts
|
100
|
+
#EXTINF:7.507489,
|
101
|
+
1080-7mbps00047.ts
|
102
|
+
#EXTINF:11.261233,
|
103
|
+
1080-7mbps00048.ts
|
104
|
+
#EXTINF:11.261222,
|
105
|
+
1080-7mbps00049.ts
|
106
|
+
#EXTINF:7.507489,
|
107
|
+
1080-7mbps00050.ts
|
108
|
+
#EXTINF:11.261233,
|
109
|
+
1080-7mbps00051.ts
|
110
|
+
#EXTINF:11.261233,
|
111
|
+
1080-7mbps00052.ts
|
112
|
+
#EXTINF:7.507489,
|
113
|
+
1080-7mbps00053.ts
|
114
|
+
#EXTINF:11.261233,
|
115
|
+
1080-7mbps00054.ts
|
116
|
+
#EXTINF:11.261233,
|
117
|
+
1080-7mbps00055.ts
|
118
|
+
#EXTINF:7.507478,
|
119
|
+
1080-7mbps00056.ts
|
120
|
+
#EXTINF:11.261233,
|
121
|
+
1080-7mbps00057.ts
|
122
|
+
#EXTINF:11.261233,
|
123
|
+
1080-7mbps00058.ts
|
124
|
+
#EXTINF:7.507489,
|
125
|
+
1080-7mbps00059.ts
|
126
|
+
#EXTINF:11.261233,
|
127
|
+
1080-7mbps00060.ts
|
128
|
+
#EXTINF:11.261222,
|
129
|
+
1080-7mbps00061.ts
|
130
|
+
#EXTINF:7.507489,
|
131
|
+
1080-7mbps00062.ts
|
132
|
+
#EXTINF:11.261233,
|
133
|
+
1080-7mbps00063.ts
|
134
|
+
#EXTINF:11.261233,
|
135
|
+
1080-7mbps00064.ts
|
136
|
+
#EXTINF:7.507489,
|
137
|
+
1080-7mbps00065.ts
|
138
|
+
#EXTINF:11.261233,
|
139
|
+
1080-7mbps00066.ts
|
140
|
+
#EXTINF:11.261233,
|
141
|
+
1080-7mbps00067.ts
|
142
|
+
#EXTINF:7.507478,
|
143
|
+
1080-7mbps00068.ts
|
144
|
+
#EXTINF:11.261233,
|
145
|
+
1080-7mbps00069.ts
|
146
|
+
#EXTINF:11.261233,
|
147
|
+
1080-7mbps00070.ts
|
148
|
+
#EXTINF:7.507489,
|
149
|
+
1080-7mbps00071.ts
|
150
|
+
#EXTINF:11.261233,
|
151
|
+
1080-7mbps00072.ts
|
152
|
+
#EXTINF:11.261233,
|
153
|
+
1080-7mbps00073.ts
|
154
|
+
#EXTINF:7.507489,
|
155
|
+
1080-7mbps00074.ts
|
156
|
+
#EXTINF:11.261222,
|
157
|
+
1080-7mbps00075.ts
|
158
|
+
#EXTINF:11.261233,
|
159
|
+
1080-7mbps00076.ts
|
160
|
+
#EXTINF:7.507489,
|
161
|
+
1080-7mbps00077.ts
|
162
|
+
#EXTINF:11.261233,
|
163
|
+
1080-7mbps00078.ts
|
164
|
+
#EXTINF:11.261233,
|
165
|
+
1080-7mbps00079.ts
|
166
|
+
#EXTINF:7.507478,
|
167
|
+
1080-7mbps00080.ts
|
168
|
+
#EXTINF:11.261233,
|
169
|
+
1080-7mbps00081.ts
|
170
|
+
#EXTINF:11.261233,
|
171
|
+
1080-7mbps00082.ts
|
172
|
+
#EXTINF:7.507489,
|
173
|
+
1080-7mbps00083.ts
|
174
|
+
#EXTINF:11.261233,
|
175
|
+
1080-7mbps00084.ts
|
176
|
+
#EXTINF:11.261233,
|
177
|
+
1080-7mbps00085.ts
|
178
|
+
#EXTINF:7.507489,
|
179
|
+
1080-7mbps00086.ts
|
180
|
+
#EXTINF:11.261222,
|
181
|
+
1080-7mbps00087.ts
|
182
|
+
#EXTINF:11.261233,
|
183
|
+
1080-7mbps00088.ts
|
184
|
+
#EXTINF:7.507489,
|
185
|
+
1080-7mbps00089.ts
|
186
|
+
#EXTINF:11.261233,
|
187
|
+
1080-7mbps00090.ts
|
188
|
+
#EXTINF:11.261233,
|
189
|
+
1080-7mbps00091.ts
|
190
|
+
#EXTINF:7.507478,
|
191
|
+
1080-7mbps00092.ts
|
192
|
+
#EXTINF:11.261233,
|
193
|
+
1080-7mbps00093.ts
|
194
|
+
#EXTINF:11.261233,
|
195
|
+
1080-7mbps00094.ts
|
196
|
+
#EXTINF:7.507489,
|
197
|
+
1080-7mbps00095.ts
|
198
|
+
#EXTINF:11.261233,
|
199
|
+
1080-7mbps00096.ts
|
200
|
+
#EXTINF:11.261233,
|
201
|
+
1080-7mbps00097.ts
|
202
|
+
#EXTINF:7.507489,
|
203
|
+
1080-7mbps00098.ts
|
204
|
+
#EXTINF:11.261222,
|
205
|
+
1080-7mbps00099.ts
|
206
|
+
#EXTINF:11.261233,
|
207
|
+
1080-7mbps00100.ts
|
208
|
+
#EXTINF:7.507489,
|
209
|
+
1080-7mbps00101.ts
|
210
|
+
#EXTINF:11.261233,
|
211
|
+
1080-7mbps00102.ts
|
212
|
+
#EXTINF:11.261233,
|
213
|
+
1080-7mbps00103.ts
|
214
|
+
#EXTINF:7.507478,
|
215
|
+
1080-7mbps00104.ts
|
216
|
+
#EXTINF:11.261233,
|
217
|
+
1080-7mbps00105.ts
|
218
|
+
#EXTINF:11.261233,
|
219
|
+
1080-7mbps00106.ts
|
220
|
+
#EXTINF:7.507489,
|
221
|
+
1080-7mbps00107.ts
|
222
|
+
#EXTINF:11.261233,
|
223
|
+
1080-7mbps00108.ts
|
224
|
+
#EXTINF:11.261233,
|
225
|
+
1080-7mbps00109.ts
|
226
|
+
#EXTINF:7.507489,
|
227
|
+
1080-7mbps00110.ts
|
228
|
+
#EXTINF:11.261233,
|
229
|
+
1080-7mbps00111.ts
|
230
|
+
#EXTINF:11.261233,
|
231
|
+
1080-7mbps00112.ts
|
232
|
+
#EXTINF:7.507478,
|
233
|
+
1080-7mbps00113.ts
|
234
|
+
#EXTINF:11.261233,
|
235
|
+
1080-7mbps00114.ts
|
236
|
+
#EXTINF:11.261233,
|
237
|
+
1080-7mbps00115.ts
|
238
|
+
#EXTINF:7.507478,
|
239
|
+
1080-7mbps00116.ts
|
240
|
+
#EXTINF:11.261233,
|
241
|
+
1080-7mbps00117.ts
|
242
|
+
#EXTINF:7.507478,
|
243
|
+
1080-7mbps00118.ts
|
244
|
+
#EXTINF:11.261233,
|
245
|
+
1080-7mbps00119.ts
|
246
|
+
#EXTINF:11.261233,
|
247
|
+
1080-7mbps00120.ts
|
248
|
+
#EXTINF:7.507489,
|
249
|
+
1080-7mbps00121.ts
|
250
|
+
#EXTINF:11.261233,
|
251
|
+
1080-7mbps00122.ts
|
252
|
+
#EXTINF:11.261233,
|
253
|
+
1080-7mbps00123.ts
|
254
|
+
#EXTINF:7.507489,
|
255
|
+
1080-7mbps00124.ts
|
256
|
+
#EXTINF:11.261222,
|
257
|
+
1080-7mbps00125.ts
|
258
|
+
#EXTINF:11.261233,
|
259
|
+
1080-7mbps00126.ts
|
260
|
+
#EXTINF:7.507489,
|
261
|
+
1080-7mbps00127.ts
|
262
|
+
#EXTINF:11.261233,
|
263
|
+
1080-7mbps00128.ts
|
264
|
+
#EXTINF:11.261233,
|
265
|
+
1080-7mbps00129.ts
|
266
|
+
#EXTINF:7.507478,
|
267
|
+
1080-7mbps00130.ts
|
268
|
+
#EXTINF:11.261233,
|
269
|
+
1080-7mbps00131.ts
|
270
|
+
#EXTINF:11.261233,
|
271
|
+
1080-7mbps00132.ts
|
272
|
+
#EXTINF:7.507489,
|
273
|
+
1080-7mbps00133.ts
|
274
|
+
#EXTINF:11.261233,
|
275
|
+
1080-7mbps00134.ts
|
276
|
+
#EXTINF:11.261233,
|
277
|
+
1080-7mbps00135.ts
|
278
|
+
#EXTINF:7.507489,
|
279
|
+
1080-7mbps00136.ts
|
280
|
+
#EXTINF:1.793444,
|
281
|
+
1080-7mbps00137.ts
|
282
|
+
#EXT-X-ENDLIST
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe M3u8::PlaylistItem do
|
4
|
+
it 'should initialize with hash' do
|
5
|
+
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
6
|
+
bitrate: 540, playlist: 'test.url' }
|
7
|
+
item = M3u8::PlaylistItem.new(hash)
|
8
|
+
expect(item.program_id).to eq 1
|
9
|
+
expect(item.width).to eq 1920
|
10
|
+
expect(item.height).to eq 1080
|
11
|
+
expect(item.resolution).to eq '1920x1080'
|
12
|
+
expect(item.codecs).to eq 'avc'
|
13
|
+
expect(item.bitrate).to eq 540
|
14
|
+
expect(item.playlist).to eq 'test.url'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should provide m3u8 format representation' do
|
18
|
+
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
19
|
+
bitrate: 540, playlist: 'test.url' }
|
20
|
+
item = M3u8::PlaylistItem.new(hash)
|
21
|
+
output = item.to_s
|
22
|
+
expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,' +
|
23
|
+
%(CODECS="avc",BANDWIDTH=540\ntest.url)
|
24
|
+
expect(output).to eq expected
|
25
|
+
|
26
|
+
hash = { program_id: 1, codecs: 'avc', bitrate: 540, playlist: 'test.url' }
|
27
|
+
item = M3u8::PlaylistItem.new(hash)
|
28
|
+
output = item.to_s
|
29
|
+
expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,' +
|
30
|
+
%(CODECS="avc",BANDWIDTH=540\ntest.url)
|
31
|
+
expect(output).to eq expected
|
32
|
+
end
|
33
|
+
end
|
data/spec/playlist_spec.rb
CHANGED
@@ -123,7 +123,7 @@ describe M3u8::Playlist do
|
|
123
123
|
"#EXT-X-TARGETDURATION:10\n" \
|
124
124
|
"#EXTINF:11.344644,\n" \
|
125
125
|
"1080-7mbps00000.ts\n" \
|
126
|
-
|
126
|
+
"#EXT-X-ENDLIST\n"
|
127
127
|
|
128
128
|
expect(playlist.to_s).to eq output
|
129
129
|
|
@@ -138,7 +138,7 @@ describe M3u8::Playlist do
|
|
138
138
|
"1080-7mbps00000.ts\n" \
|
139
139
|
"#EXTINF:11.261233,\n" \
|
140
140
|
"1080-7mbps00001.ts\n" \
|
141
|
-
|
141
|
+
"#EXT-X-ENDLIST\n"
|
142
142
|
|
143
143
|
expect(playlist.to_s).to eq output
|
144
144
|
|
@@ -153,7 +153,7 @@ describe M3u8::Playlist do
|
|
153
153
|
"#EXT-X-TARGETDURATION:12\n" \
|
154
154
|
"#EXTINF:11.344644,\n" \
|
155
155
|
"1080-7mbps00000.ts\n" \
|
156
|
-
|
156
|
+
"#EXT-X-ENDLIST\n"
|
157
157
|
|
158
158
|
expect(playlist.to_s).to eq output
|
159
159
|
end
|
@@ -203,6 +203,47 @@ describe M3u8::Playlist do
|
|
203
203
|
.to raise_error(M3u8::PlaylistTypeError, message)
|
204
204
|
end
|
205
205
|
|
206
|
+
it 'should raise error on write if item types are mixed' do
|
207
|
+
playlist = M3u8::Playlist.new
|
208
|
+
|
209
|
+
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
210
|
+
bitrate: 540, playlist: 'test.url' }
|
211
|
+
item = M3u8::PlaylistItem.new(hash)
|
212
|
+
playlist.items.push item
|
213
|
+
|
214
|
+
hash = { duration: 10.991, segment: 'test.ts' }
|
215
|
+
item = M3u8::SegmentItem.new(hash)
|
216
|
+
playlist.items.push item
|
217
|
+
|
218
|
+
message = 'Playlist contains mixed types of items'
|
219
|
+
io = StringIO.new
|
220
|
+
expect { playlist.write io }
|
221
|
+
.to raise_error(M3u8::PlaylistTypeError, message)
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'should return valid status' do
|
225
|
+
playlist = M3u8::Playlist.new
|
226
|
+
expect(playlist.valid?).to be true
|
227
|
+
|
228
|
+
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
229
|
+
bitrate: 540, playlist: 'test.url' }
|
230
|
+
item = M3u8::PlaylistItem.new(hash)
|
231
|
+
playlist.items.push item
|
232
|
+
expect(playlist.valid?).to be true
|
233
|
+
|
234
|
+
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
235
|
+
bitrate: 540, playlist: 'test.url' }
|
236
|
+
item = M3u8::PlaylistItem.new(hash)
|
237
|
+
playlist.items.push item
|
238
|
+
expect(playlist.valid?).to be true
|
239
|
+
|
240
|
+
hash = { duration: 10.991, segment: 'test.ts' }
|
241
|
+
item = M3u8::SegmentItem.new(hash)
|
242
|
+
playlist.items.push item
|
243
|
+
|
244
|
+
expect(playlist.valid?).to be false
|
245
|
+
end
|
246
|
+
|
206
247
|
it 'should raise error if codecs are missing' do
|
207
248
|
playlist = M3u8::Playlist.new
|
208
249
|
message = 'An audio or video codec should be provided.'
|
@@ -210,4 +251,12 @@ describe M3u8::Playlist do
|
|
210
251
|
.to raise_error(M3u8::MissingCodecError, message)
|
211
252
|
end
|
212
253
|
|
254
|
+
it 'should expose options as attributes' do
|
255
|
+
options = { version: 1, cache: false, target: 12, sequence: 1 }
|
256
|
+
playlist = M3u8::Playlist.new options
|
257
|
+
expect(playlist.version).to be 1
|
258
|
+
expect(playlist.cache).to be false
|
259
|
+
expect(playlist.target).to be 12
|
260
|
+
expect(playlist.sequence).to be 1
|
261
|
+
end
|
213
262
|
end
|
data/spec/reader_spec.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe M3u8::Reader do
|
4
|
+
it 'should parse master playlist' do
|
5
|
+
file = File.open 'spec/fixtures/master.m3u8'
|
6
|
+
reader = M3u8::Reader.new
|
7
|
+
playlist = reader.read file
|
8
|
+
expect(playlist.master?).to be true
|
9
|
+
|
10
|
+
item = playlist.items[0]
|
11
|
+
expect(item).to be_a(M3u8::PlaylistItem)
|
12
|
+
expect(item.playlist).to eq('hls/1080-7mbps/1080-7mbps.m3u8')
|
13
|
+
expect(item.program_id).to eq('1')
|
14
|
+
expect(item.width).to eq(1920)
|
15
|
+
expect(item.height).to eq(1080)
|
16
|
+
expect(item.resolution).to eq('1920x1080')
|
17
|
+
expect(item.codecs).to eq('avc1.640028,mp4a.40.2')
|
18
|
+
expect(item.bitrate).to eq(5_042_000)
|
19
|
+
|
20
|
+
expect(playlist.items.size).to eq 6
|
21
|
+
|
22
|
+
item = playlist.items.last
|
23
|
+
expect(item.resolution).to be nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should parse segment playlist' do
|
27
|
+
file = File.open 'spec/fixtures/playlist.m3u8'
|
28
|
+
reader = M3u8::Reader.new
|
29
|
+
playlist = reader.read file
|
30
|
+
expect(playlist.master?).to be false
|
31
|
+
expect(playlist.version).to be 4
|
32
|
+
expect(playlist.sequence).to be 1
|
33
|
+
expect(playlist.cache).to be false
|
34
|
+
expect(playlist.target).to be 12
|
35
|
+
|
36
|
+
item = playlist.items[0]
|
37
|
+
expect(item).to be_a(M3u8::SegmentItem)
|
38
|
+
expect(item.duration).to eq 11.344644
|
39
|
+
|
40
|
+
expect(playlist.items.size).to eq 138
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe M3u8::SegmentItem do
|
4
|
+
it 'should initialize with hash' do
|
5
|
+
hash = { duration: 10.991, segment: 'test.ts' }
|
6
|
+
item = M3u8::SegmentItem.new(hash)
|
7
|
+
expect(item.duration).to eq 10.991
|
8
|
+
expect(item.segment).to eq 'test.ts'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should provide m3u8 format representation' do
|
12
|
+
hash = { duration: 10.991, segment: 'test.ts' }
|
13
|
+
item = M3u8::SegmentItem.new(hash)
|
14
|
+
output = item.to_s
|
15
|
+
expected = "#EXTINF:10.991,\ntest.ts"
|
16
|
+
expect(output).to eq expected
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
CHANGED
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: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seth Deckard
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-11-
|
11
|
+
date: 2014-11-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -52,7 +52,7 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '2.11'
|
55
|
-
description: Generate m3u8 playlists for HTTP Live Streaming (HLS).
|
55
|
+
description: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
|
56
56
|
email:
|
57
57
|
- seth@deckard.me
|
58
58
|
executables: []
|
@@ -69,9 +69,17 @@ files:
|
|
69
69
|
- lib/m3u8.rb
|
70
70
|
- lib/m3u8/error.rb
|
71
71
|
- lib/m3u8/playlist.rb
|
72
|
+
- lib/m3u8/playlist_item.rb
|
73
|
+
- lib/m3u8/reader.rb
|
74
|
+
- lib/m3u8/segment_item.rb
|
72
75
|
- lib/m3u8/version.rb
|
73
76
|
- m3u8.gemspec
|
77
|
+
- spec/fixtures/master.m3u8
|
78
|
+
- spec/fixtures/playlist.m3u8
|
79
|
+
- spec/playlist_item_spec.rb
|
74
80
|
- spec/playlist_spec.rb
|
81
|
+
- spec/reader_spec.rb
|
82
|
+
- spec/segment_item_spec.rb
|
75
83
|
- spec/spec_helper.rb
|
76
84
|
homepage: https://github.com/sethdeckard/m3u8
|
77
85
|
licenses:
|
@@ -96,7 +104,12 @@ rubyforge_project:
|
|
96
104
|
rubygems_version: 2.4.4
|
97
105
|
signing_key:
|
98
106
|
specification_version: 4
|
99
|
-
summary: Generate m3u8 playlists for HTTP Live Streaming (HLS).
|
107
|
+
summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
|
100
108
|
test_files:
|
109
|
+
- spec/fixtures/master.m3u8
|
110
|
+
- spec/fixtures/playlist.m3u8
|
111
|
+
- spec/playlist_item_spec.rb
|
101
112
|
- spec/playlist_spec.rb
|
113
|
+
- spec/reader_spec.rb
|
114
|
+
- spec/segment_item_spec.rb
|
102
115
|
- spec/spec_helper.rb
|