m3u8 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/README.md +94 -78
- data/lib/m3u8.rb +6 -0
- data/lib/m3u8/media_item.rb +4 -4
- data/lib/m3u8/playlist.rb +5 -2
- data/lib/m3u8/playlist_item.rb +26 -15
- data/lib/m3u8/reader.rb +98 -69
- data/lib/m3u8/segment_item.rb +15 -2
- data/lib/m3u8/session_data_item.rb +52 -0
- data/lib/m3u8/version.rb +1 -1
- data/lib/m3u8/writer.rb +1 -0
- data/spec/fixtures/iframes.m3u8 +12 -0
- data/spec/fixtures/master_iframes.m3u8 +12 -0
- data/spec/fixtures/session_data.m3u8 +4 -0
- data/spec/m3u8_spec.rb +13 -0
- data/spec/media_item_spec.rb +2 -2
- data/spec/playlist_item_spec.rb +19 -7
- data/spec/playlist_spec.rb +11 -10
- data/spec/reader_spec.rb +53 -4
- data/spec/segment_item_spec.rb +18 -1
- data/spec/session_data_item_spec.rb +49 -0
- data/spec/spec_helper.rb +1 -7
- data/spec/writer_spec.rb +17 -10
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3797c5f6de3b71d0ea5974be7aef7dc416cf0977
|
4
|
+
data.tar.gz: cf67c30e9e26e611a55d5908de78497453181947
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 000f3f17395510b0af051895793cb0a48019e5848b5419dc6c771c987f597cc9da68aab90e0ae7eb031fbde014ab032a78361e3ae638c567797ab2cd1acd6b27
|
7
|
+
data.tar.gz: b1320f06d435c05b174aa19113fe41bc75b11150f6ca097e6dd38bf518ebe3a94b273e2fd8ae59fc305e361610862a315d0b36a72e6b1948cc435c7dc8412aeb
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
### 0.5.0 (2/10/2015) - BREAKING: renamed PlaylistItem.playlist to PlaylistItem.uri, MediaItem.group to MediaItem.group_id, and PlaylistItem.bitrate to PlaylistItem.bandwidth so attributes more closely match the spec. Added support for EXT-X-I-FRAME-STREAM-INF, EXT-X-I-FRAMES-ONLY, EXT-X-BYTERANGE, and EXT-X-SESSION-DATA.
|
2
|
+
|
1
3
|
### 0.4.0 (1/20/2015) - BREAKING: Playlist.audio is now Playlist.audio_codec, audio now represents the newly supported AUDIO attribute, please update your code accordingly. Added support for all EXT-X-MEDIA attributes as well as the following EXT-X-STREAM-INF attributes: AVERAGE-BANDWIDTH, AUDIO, VIDEO, SUBTILES, and CLOSED-CAPTIONS. This means the library now supports alternate audio / camera angles as well as subtitles and closed captions. The EXT-X-PLAYLIST-TYPE attribute is now supported as Playlist.type. SegmentItems now support comments (Thanks @elsurudo). A bug was also fixed in Reader that prevented reuse of the instance.
|
2
4
|
|
3
5
|
### 0.3.2 (1/16/2015) - PROGRAM-ID was removed in protocol version 6, if not provided it will now be omitted. Thanks @elsurudo
|
data/README.md
CHANGED
@@ -24,100 +24,116 @@ Or install it yourself as:
|
|
24
24
|
|
25
25
|
$ gem install m3u8
|
26
26
|
|
27
|
-
## Usage (
|
28
|
-
|
29
|
-
|
30
|
-
require 'm3u8'
|
31
|
-
|
32
|
-
#create a master playlist and add child playlists for adaptive bitrate streaming:
|
33
|
-
playlist = M3u8::Playlist.new
|
34
|
-
#create a new playlist item with options
|
35
|
-
options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
|
36
|
-
profile: 'high', level: 4.1, audio_codec: 'aac-lc', bitrate: 540,
|
37
|
-
playlist: 'test.url' }
|
38
|
-
item = M3u8::PlaylistItem.new options
|
39
|
-
playlist.items.push item
|
40
|
-
|
41
|
-
#alternatively you can set codecs rather than having it generated automatically:
|
42
|
-
options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
|
43
|
-
codecs: 'avc1.66.30,mp4a.40.2', bitrate: 540, playlist: 'test.url' }
|
44
|
-
item = M3u8::PlaylistItem.new options
|
27
|
+
## Usage (creating playlists)
|
45
28
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
#just get the codec string for custom use
|
53
|
-
options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
|
54
|
-
codecs = M3u8::Playlist.codecs options
|
55
|
-
# => "avc1.66.30,mp4a.40.2"
|
56
|
-
|
57
|
-
#specify options for playlist, these are ignored if playlist becomes a master playlist
|
58
|
-
# (child playlist added):
|
59
|
-
options = { version: 1, cache: false, target: 12, sequence: 1 }
|
60
|
-
playlist = M3u8::Playlist.new options
|
61
|
-
|
62
|
-
#You can pass an IO object to the write method
|
63
|
-
require 'tempfile'
|
64
|
-
f = Tempfile.new 'test'
|
65
|
-
playlist.write f
|
66
|
-
|
67
|
-
#You can also access the playlist as a string
|
68
|
-
playlist.to_s
|
69
|
-
|
70
|
-
#There is a M3u8::Writer class if you want more control over the write process
|
71
|
-
|
72
|
-
#values for :audio_codec (Codec name)
|
73
|
-
#aac-lc, he-aac, mp3
|
74
|
-
|
75
|
-
#values for :profile (H.264 Profile)
|
76
|
-
#baseline, main, high
|
77
|
-
|
78
|
-
#values for :level
|
79
|
-
#3.0, 3.1, 4.0, 4.1
|
80
|
-
|
81
|
-
#not all Levels and Profiles can be combined, consult H.264 documentation
|
29
|
+
Create a master playlist and add child playlists for adaptive bitrate streaming:
|
30
|
+
```ruby
|
31
|
+
require 'm3u8'
|
32
|
+
playlist = M3u8::Playlist.new
|
33
|
+
```
|
82
34
|
|
83
|
-
|
35
|
+
Create a new playlist item with options:
|
36
|
+
```ruby
|
37
|
+
options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
|
38
|
+
profile: 'high', level: 4.1, audio_codec: 'aac-lc', bandwidth: 540,
|
39
|
+
playlist: 'test.url' }
|
40
|
+
item = M3u8::PlaylistItem.new options
|
41
|
+
playlist.items.push item
|
42
|
+
```
|
43
|
+
|
44
|
+
Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist:
|
84
45
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
46
|
+
```ruby
|
47
|
+
hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
|
48
|
+
assoc_language: 'spoken', name: 'Francais', autoselect: true,
|
49
|
+
default: false, forced: true, uri: 'frelo/prog_index.m3u8' }
|
50
|
+
item = M3u8::MediaItem.new(hash)
|
51
|
+
playlist.items.push item
|
52
|
+
```
|
53
|
+
|
54
|
+
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):
|
55
|
+
```ruby
|
56
|
+
options = { version: 1, cache: false, target: 12, sequence: 1 }
|
57
|
+
playlist = M3u8::Playlist.new options
|
58
|
+
|
59
|
+
item = M3u8::SegmentItem.new duration: 11, segment: 'test.ts'
|
60
|
+
playlist.items.push item
|
61
|
+
```
|
62
|
+
|
63
|
+
You can pass an IO object to the write method:
|
64
|
+
```ruby
|
65
|
+
require 'tempfile'
|
66
|
+
f = Tempfile.new 'test'
|
67
|
+
playlist.write f
|
68
|
+
```
|
69
|
+
You can also access the playlist as a string:
|
70
|
+
```ruby
|
71
|
+
playlist.to_s
|
72
|
+
```
|
73
|
+
M3u8::Writer is the class that handles generating the playlist output.
|
89
74
|
|
90
|
-
|
91
|
-
|
75
|
+
Alternatively you can set codecs rather than having it generated automatically:
|
76
|
+
```ruby
|
77
|
+
options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
|
78
|
+
codecs: 'avc1.66.30,mp4a.40.2', bandwidth: 540, playlist: 'test.url' }
|
79
|
+
item = M3u8::PlaylistItem.new options
|
80
|
+
```
|
81
|
+
Just get the codec string for custom use:
|
82
|
+
```ruby
|
83
|
+
options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
|
84
|
+
codecs = M3u8::Playlist.codecs options
|
85
|
+
# => "avc1.66.30,mp4a.40.2"
|
86
|
+
```
|
87
|
+
Values for audio_codec (codec name): aac-lc, he-aac, mp3
|
88
|
+
|
89
|
+
Possible values for profile (H.264 Profile): baseline, main, high.
|
90
|
+
|
91
|
+
Possible values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
|
92
92
|
|
93
|
-
|
94
|
-
# => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080",
|
95
|
-
# @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000",
|
96
|
-
# @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
|
93
|
+
Not all Levels and Profiles can be combined, consult H.264 documentation
|
97
94
|
|
98
|
-
|
99
|
-
options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
|
100
|
-
profile: 'high', level: 4.1, audio_codec: 'aac-lc', bitrate: 540,
|
101
|
-
playlist: 'test.url' }
|
102
|
-
item = M3u8::PlaylistItem.new options
|
103
|
-
#add it to the top of the playlist
|
104
|
-
playlist.items.insert 0, item
|
95
|
+
## Parsing Usage
|
105
96
|
|
106
|
-
|
107
|
-
|
97
|
+
```ruby
|
98
|
+
file = File.open 'spec/fixtures/master.m3u8'
|
99
|
+
playlist = M3u8::Playlist.read file
|
100
|
+
playlist.master?
|
101
|
+
# => true
|
102
|
+
```
|
103
|
+
Acess items in playlist:
|
104
|
+
```ruby
|
105
|
+
playlist.items.first
|
106
|
+
# => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080",
|
107
|
+
# @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000",
|
108
|
+
# @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
|
109
|
+
```
|
110
|
+
Create a new playlist item with options:
|
111
|
+
```ruby
|
112
|
+
options = { program_id: 1, width: 1920, height: 1080, width: 1920, height: 1080,
|
113
|
+
profile: 'high', level: 4.1, audio_codec: 'aac-lc', bandwidth: 540,
|
114
|
+
playlist: 'test.url' }
|
115
|
+
item = M3u8::PlaylistItem.new options
|
116
|
+
#add it to the top of the playlist
|
117
|
+
playlist.items.insert 0, item
|
118
|
+
```
|
119
|
+
M3u8::Reader is the class handles parsing if you want more control over the process.
|
120
|
+
|
108
121
|
## Features
|
109
122
|
* Distinction between segment and master playlists is handled automatically (no need to use a different class).
|
110
123
|
* Automatically generates the audio/video codec string based on names and options you are familar with.
|
111
124
|
* Provides validation of input when adding playlists or segments.
|
112
125
|
* Allows all options to be configured on a playlist (caching, version, etc.)
|
126
|
+
* Supports I-Frames (Intra frames) and Byte Ranges in Segments.
|
127
|
+
* Supports subtitles, closed captions, alternate audio and video, and comments.
|
128
|
+
# Supports Session Data in master playlists.
|
113
129
|
* Can write playlist to an IO object (StringIO/File, etc) or access string via to_s.
|
114
130
|
* Can read playlists into a model (Playlist and Items) from an IO object.
|
131
|
+
* Any tag or attribute supported by the object model is supported both parsing and generation of m3u8 playlists.
|
115
132
|
|
116
133
|
## Missing (but planned) Features
|
117
|
-
* Support for
|
118
|
-
*
|
119
|
-
*
|
120
|
-
* Support for all additional attributes in the latest version of the spec.
|
134
|
+
* Support for encrypted segments.
|
135
|
+
* Validation of all attributes and their values to match the rules defined in the spec.
|
136
|
+
* Still missing support for a few tags and attributes.
|
121
137
|
|
122
138
|
## Contributing
|
123
139
|
|
data/lib/m3u8.rb
CHANGED
@@ -4,9 +4,15 @@ require 'm3u8/playlist'
|
|
4
4
|
require 'm3u8/playlist_item'
|
5
5
|
require 'm3u8/segment_item'
|
6
6
|
require 'm3u8/media_item'
|
7
|
+
require 'm3u8/session_data_item'
|
7
8
|
require 'm3u8/reader'
|
8
9
|
require 'm3u8/writer'
|
9
10
|
require 'm3u8/error'
|
10
11
|
|
11
12
|
module M3u8
|
13
|
+
|
14
|
+
def parse_attributes(line)
|
15
|
+
array = line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
|
16
|
+
Hash[array.map { |key, value| [key, value.gsub('"', '')] }]
|
17
|
+
end
|
12
18
|
end
|
data/lib/m3u8/media_item.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module M3u8
|
2
2
|
# MediaItem represents a set of EXT-X-MEDIA attributes
|
3
3
|
class MediaItem
|
4
|
-
attr_accessor :type, :
|
4
|
+
attr_accessor :type, :group_id, :language, :assoc_language, :name,
|
5
5
|
:autoselect, :default, :uri, :forced
|
6
6
|
|
7
7
|
def initialize(params = {})
|
@@ -12,7 +12,7 @@ module M3u8
|
|
12
12
|
|
13
13
|
def to_s
|
14
14
|
attributes = [type_format,
|
15
|
-
|
15
|
+
group_id_format,
|
16
16
|
language_format,
|
17
17
|
assoc_language_format,
|
18
18
|
name_format,
|
@@ -29,8 +29,8 @@ module M3u8
|
|
29
29
|
"TYPE=#{type}"
|
30
30
|
end
|
31
31
|
|
32
|
-
def
|
33
|
-
%(GROUP-ID="#{
|
32
|
+
def group_id_format
|
33
|
+
%(GROUP-ID="#{group_id}")
|
34
34
|
end
|
35
35
|
|
36
36
|
def language_format
|
data/lib/m3u8/playlist.rb
CHANGED
@@ -2,7 +2,8 @@ module M3u8
|
|
2
2
|
# Playlist represents an m3u8 playlist, it can be a master playlist or a set
|
3
3
|
# of media segments
|
4
4
|
class Playlist
|
5
|
-
attr_accessor :items, :version, :cache, :target, :sequence, :type
|
5
|
+
attr_accessor :items, :version, :cache, :target, :sequence, :type,
|
6
|
+
:iframes_only
|
6
7
|
|
7
8
|
def initialize(options = {})
|
8
9
|
assign_options options
|
@@ -55,7 +56,8 @@ module M3u8
|
|
55
56
|
version: 3,
|
56
57
|
sequence: 0,
|
57
58
|
cache: true,
|
58
|
-
target: 10
|
59
|
+
target: 10,
|
60
|
+
iframes_only: false
|
59
61
|
}.merge options
|
60
62
|
|
61
63
|
self.version = options[:version]
|
@@ -63,6 +65,7 @@ module M3u8
|
|
63
65
|
self.cache = options[:cache]
|
64
66
|
self.target = options[:target]
|
65
67
|
self.type = options[:type]
|
68
|
+
self.iframes_only = options[:iframes_only]
|
66
69
|
end
|
67
70
|
|
68
71
|
def playlist_size
|
data/lib/m3u8/playlist_item.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
module M3u8
|
2
|
-
# PlaylistItem represents a set of EXT-X-STREAM-INF
|
2
|
+
# PlaylistItem represents a set of EXT-X-STREAM-INF or
|
3
|
+
# EXT-X-I-FRAME-STREAM-INF attributes
|
3
4
|
class PlaylistItem
|
4
|
-
attr_accessor :program_id, :width, :height, :codecs, :
|
5
|
-
:audio_codec, :level, :profile, :video, :audio,
|
6
|
-
:average_bandwidth, :subtitles, :closed_captions
|
5
|
+
attr_accessor :program_id, :width, :height, :codecs, :bandwidth,
|
6
|
+
:audio_codec, :level, :profile, :video, :audio, :uri,
|
7
|
+
:average_bandwidth, :subtitles, :closed_captions, :iframe
|
7
8
|
MISSING_CODEC_MESSAGE = 'Audio or video codec info should be provided.'
|
8
9
|
|
9
10
|
def initialize(params = {})
|
11
|
+
self.iframe = false
|
10
12
|
params.each do |key, value|
|
11
13
|
instance_variable_set("@#{key}", value)
|
12
14
|
end
|
@@ -36,16 +38,7 @@ module M3u8
|
|
36
38
|
def to_s
|
37
39
|
validate
|
38
40
|
|
39
|
-
|
40
|
-
resolution_format,
|
41
|
-
codecs_format,
|
42
|
-
bandwidth_format,
|
43
|
-
average_bandwidth_format,
|
44
|
-
audio_format,
|
45
|
-
video_format,
|
46
|
-
subtitles_format,
|
47
|
-
closed_captions_format].compact.join(',')
|
48
|
-
"#EXT-X-STREAM-INF:#{attributes}\n#{playlist}"
|
41
|
+
m3u8_format
|
49
42
|
end
|
50
43
|
|
51
44
|
private
|
@@ -54,6 +47,24 @@ module M3u8
|
|
54
47
|
fail MissingCodecError, MISSING_CODEC_MESSAGE if codecs.nil?
|
55
48
|
end
|
56
49
|
|
50
|
+
def m3u8_format
|
51
|
+
return %(#EXT-X-I-FRAME-STREAM-INF:#{attributes},URI="#{uri}") if iframe
|
52
|
+
|
53
|
+
"#EXT-X-STREAM-INF:#{attributes}\n#{uri}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def attributes
|
57
|
+
[program_id_format,
|
58
|
+
resolution_format,
|
59
|
+
codecs_format,
|
60
|
+
bandwidth_format,
|
61
|
+
average_bandwidth_format,
|
62
|
+
audio_format,
|
63
|
+
video_format,
|
64
|
+
subtitles_format,
|
65
|
+
closed_captions_format].compact.join(',')
|
66
|
+
end
|
67
|
+
|
57
68
|
def program_id_format
|
58
69
|
return if program_id.nil?
|
59
70
|
"PROGRAM-ID=#{program_id}"
|
@@ -69,7 +80,7 @@ module M3u8
|
|
69
80
|
end
|
70
81
|
|
71
82
|
def bandwidth_format
|
72
|
-
"BANDWIDTH=#{
|
83
|
+
"BANDWIDTH=#{bandwidth}"
|
73
84
|
end
|
74
85
|
|
75
86
|
def average_bandwidth_format
|
data/lib/m3u8/reader.rb
CHANGED
@@ -7,28 +7,18 @@ module M3u8
|
|
7
7
|
SEQUENCE_START = '#EXT-X-MEDIA-SEQUENCE:'
|
8
8
|
CACHE_START = '#EXT-X-ALLOW-CACHE:'
|
9
9
|
TARGET_START = '#EXT-X-TARGETDURATION:'
|
10
|
+
IFRAME_START = '#EXT-X-I-FRAMES-ONLY'
|
10
11
|
STREAM_START = '#EXT-X-STREAM-INF:'
|
12
|
+
STREAM_IFRAME_START = '#EXT-X-I-FRAME-STREAM-INF:'
|
11
13
|
MEDIA_START = '#EXT-X-MEDIA:'
|
14
|
+
SESSION_DATA_START = '#EXT-X-SESSION-DATA:'
|
12
15
|
SEGMENT_START = '#EXTINF:'
|
13
|
-
#
|
14
|
-
PROGRAM_ID = 'PROGRAM-ID'
|
16
|
+
BYTERANGE_START = '#EXT-X-BYTERANGE:'
|
15
17
|
RESOLUTION = 'RESOLUTION'
|
16
|
-
CODECS = 'CODECS'
|
17
18
|
BANDWIDTH = 'BANDWIDTH'
|
18
19
|
AVERAGE_BANDWIDTH = 'AVERAGE-BANDWIDTH'
|
19
|
-
VIDEO = 'VIDEO'
|
20
|
-
AUDIO = 'AUDIO'
|
21
|
-
SUBTITLES = 'SUBTITLES'
|
22
|
-
CLOSED_CAPTIONS = 'CLOSED-CAPTIONS'
|
23
|
-
# EXT-X-MEDIA:
|
24
|
-
TYPE = 'TYPE'
|
25
|
-
GROUP_ID = 'GROUP-ID'
|
26
|
-
LANGUAGE = 'LANGUAGE'
|
27
|
-
ASSOC_LANGUAGE = 'ASSOC-LANGUAGE'
|
28
|
-
NAME = 'NAME'
|
29
20
|
AUTOSELECT = 'AUTOSELECT'
|
30
21
|
DEFAULT = 'DEFAULT'
|
31
|
-
URI = 'URI'
|
32
22
|
FORCED = 'FORCED'
|
33
23
|
|
34
24
|
def read(input)
|
@@ -44,6 +34,26 @@ module M3u8
|
|
44
34
|
def parse_line(line)
|
45
35
|
return if line.start_with? PLAYLIST_START
|
46
36
|
|
37
|
+
if line.start_with? STREAM_START
|
38
|
+
parse_stream line
|
39
|
+
elsif line.start_with? STREAM_IFRAME_START
|
40
|
+
parse_iframe_stream line
|
41
|
+
elsif line.start_with? SEGMENT_START
|
42
|
+
parse_segment line
|
43
|
+
elsif line.start_with? MEDIA_START
|
44
|
+
parse_media line
|
45
|
+
elsif line.start_with? BYTERANGE_START
|
46
|
+
parse_byterange line
|
47
|
+
elsif line.start_with? SESSION_DATA_START
|
48
|
+
parse_session_data line
|
49
|
+
elsif !item.nil? && open
|
50
|
+
parse_next_line line
|
51
|
+
else
|
52
|
+
parse_header line
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_header(line)
|
47
57
|
if line.start_with? PLAYLIST_TYPE_START
|
48
58
|
parse_playlist_type line
|
49
59
|
elsif line.start_with? VERSION_START
|
@@ -54,14 +64,8 @@ module M3u8
|
|
54
64
|
parse_cache line
|
55
65
|
elsif line.start_with? TARGET_START
|
56
66
|
parse_target line
|
57
|
-
elsif line.start_with?
|
58
|
-
|
59
|
-
elsif line.start_with? SEGMENT_START
|
60
|
-
parse_segment line
|
61
|
-
elsif line.start_with? MEDIA_START
|
62
|
-
parse_media line
|
63
|
-
elsif !item.nil? && open
|
64
|
-
parse_value(line)
|
67
|
+
elsif line.start_with? IFRAME_START
|
68
|
+
playlist.iframes_only = true
|
65
69
|
end
|
66
70
|
end
|
67
71
|
|
@@ -82,10 +86,6 @@ module M3u8
|
|
82
86
|
playlist.cache = parse_yes_no(line)
|
83
87
|
end
|
84
88
|
|
85
|
-
def parse_yes_no(string)
|
86
|
-
string == 'YES' ? true : false
|
87
|
-
end
|
88
|
-
|
89
89
|
def parse_target(line)
|
90
90
|
playlist.target = line.gsub(TARGET_START, '').to_i
|
91
91
|
end
|
@@ -96,88 +96,117 @@ module M3u8
|
|
96
96
|
|
97
97
|
self.item = M3u8::PlaylistItem.new
|
98
98
|
line = line.gsub STREAM_START, ''
|
99
|
-
attributes = line
|
99
|
+
attributes = parse_attributes line
|
100
|
+
parse_stream_attributes attributes
|
101
|
+
end
|
102
|
+
|
103
|
+
def parse_iframe_stream(line)
|
104
|
+
self.master = true
|
105
|
+
self.open = false
|
106
|
+
|
107
|
+
self.item = M3u8::PlaylistItem.new
|
108
|
+
item.iframe = true
|
109
|
+
line = line.gsub STREAM_IFRAME_START, ''
|
110
|
+
attributes = parse_attributes line
|
111
|
+
parse_stream_attributes attributes
|
112
|
+
playlist.items.push item
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse_stream_attributes(attributes)
|
100
116
|
attributes.each do |pair|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
item.program_id = value
|
117
|
+
name = pair[0]
|
118
|
+
value = parse_value pair[1]
|
119
|
+
case name
|
105
120
|
when RESOLUTION
|
106
121
|
parse_resolution value
|
107
|
-
when CODECS
|
108
|
-
item.codecs = value
|
109
122
|
when BANDWIDTH
|
110
|
-
item.
|
123
|
+
item.bandwidth = value.to_i
|
111
124
|
when AVERAGE_BANDWIDTH
|
112
125
|
item.average_bandwidth = value.to_i
|
113
|
-
|
114
|
-
|
115
|
-
when VIDEO
|
116
|
-
item.video = value
|
117
|
-
when SUBTITLES
|
118
|
-
item.subtitles = value
|
119
|
-
when CLOSED_CAPTIONS
|
120
|
-
item.closed_captions = value
|
126
|
+
else
|
127
|
+
set_value name, value
|
121
128
|
end
|
122
129
|
end
|
123
130
|
end
|
124
131
|
|
132
|
+
def parse_resolution(resolution)
|
133
|
+
item.width = resolution.split('x')[0].to_i
|
134
|
+
item.height = resolution.split('x')[1].to_i
|
135
|
+
end
|
136
|
+
|
125
137
|
def parse_segment(line)
|
138
|
+
self.item = M3u8::SegmentItem.new
|
139
|
+
values = line.gsub(SEGMENT_START, '').gsub("\n", ',').split(',')
|
140
|
+
item.duration = values[0].to_f
|
141
|
+
item.comment = values[1] unless values[1].nil?
|
142
|
+
|
126
143
|
self.master = false
|
127
144
|
self.open = true
|
145
|
+
end
|
128
146
|
|
129
|
-
|
147
|
+
def parse_byterange(line)
|
148
|
+
values = line.gsub(BYTERANGE_START, '').gsub("\n", ',').split '@'
|
149
|
+
item.byterange_length = values[0].to_i
|
150
|
+
item.byterange_start = values[1].to_i unless values[1].nil?
|
151
|
+
end
|
130
152
|
|
131
|
-
|
132
|
-
item
|
133
|
-
|
153
|
+
def parse_session_data(line)
|
154
|
+
item = M3u8::SessionDataItem.parse line
|
155
|
+
playlist.items.push item
|
134
156
|
end
|
135
157
|
|
136
158
|
def parse_media(line)
|
137
159
|
self.open = false
|
138
160
|
self.item = M3u8::MediaItem.new
|
139
161
|
line = line.gsub MEDIA_START, ''
|
140
|
-
attributes = line
|
162
|
+
attributes = parse_attributes line
|
163
|
+
parse_media_attributes attributes
|
164
|
+
playlist.items.push item
|
165
|
+
end
|
166
|
+
|
167
|
+
def parse_media_attributes(attributes)
|
141
168
|
attributes.each do |pair|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
item.type = value
|
146
|
-
when GROUP_ID
|
147
|
-
item.group = value
|
148
|
-
when LANGUAGE
|
149
|
-
item.language = value
|
150
|
-
when ASSOC_LANGUAGE
|
151
|
-
item.assoc_language = value
|
152
|
-
when NAME
|
153
|
-
item.name = value
|
169
|
+
name = pair[0]
|
170
|
+
value = parse_value pair[1]
|
171
|
+
case name
|
154
172
|
when AUTOSELECT
|
155
173
|
item.autoselect = parse_yes_no value
|
156
174
|
when DEFAULT
|
157
175
|
item.default = parse_yes_no value
|
158
|
-
when URI
|
159
|
-
item.uri = value
|
160
176
|
when FORCED
|
161
177
|
item.forced = parse_yes_no value
|
178
|
+
else
|
179
|
+
set_value name, value
|
162
180
|
end
|
163
181
|
end
|
164
|
-
playlist.items.push item
|
165
|
-
end
|
166
|
-
|
167
|
-
def parse_resolution(resolution)
|
168
|
-
item.width = resolution.split('x')[0].to_i
|
169
|
-
item.height = resolution.split('x')[1].to_i
|
170
182
|
end
|
171
183
|
|
172
|
-
def
|
184
|
+
def parse_next_line(line)
|
173
185
|
value = line.gsub "\n", ''
|
174
186
|
if master
|
175
|
-
item.
|
187
|
+
item.uri = value
|
176
188
|
else
|
177
189
|
item.segment = value
|
178
190
|
end
|
179
191
|
playlist.items.push item
|
180
192
|
self.open = false
|
181
193
|
end
|
194
|
+
|
195
|
+
def parse_yes_no(string)
|
196
|
+
string == 'YES' ? true : false
|
197
|
+
end
|
198
|
+
|
199
|
+
def parse_attributes(line)
|
200
|
+
line.scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
|
201
|
+
end
|
202
|
+
|
203
|
+
def parse_value(value)
|
204
|
+
value.gsub("\n", '').gsub('"', '')
|
205
|
+
end
|
206
|
+
|
207
|
+
def set_value(name, value)
|
208
|
+
name = name.downcase.gsub('-', '_')
|
209
|
+
item.instance_variable_set("@#{name}", value)
|
210
|
+
end
|
182
211
|
end
|
183
212
|
end
|
data/lib/m3u8/segment_item.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module M3u8
|
2
2
|
class SegmentItem
|
3
|
-
attr_accessor :duration, :segment, :comment
|
3
|
+
attr_accessor :duration, :segment, :comment, :byterange_length,
|
4
|
+
:byterange_start
|
4
5
|
|
5
6
|
def initialize(params = {})
|
6
7
|
params.each do |key, value|
|
@@ -9,7 +10,19 @@ module M3u8
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def to_s
|
12
|
-
"#EXTINF:#{duration},#{comment}\n#{segment}"
|
13
|
+
"#EXTINF:#{duration},#{comment}#{byterange_format}\n#{segment}"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def byterange_format
|
19
|
+
return if byterange_length.nil?
|
20
|
+
"\n#EXT-X-BYTERANGE:#{byterange_length}#{byterange_start_format}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def byterange_start_format
|
24
|
+
return if byterange_start.nil?
|
25
|
+
"@#{byterange_start}"
|
13
26
|
end
|
14
27
|
end
|
15
28
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module M3u8
|
2
|
+
# SessionDataItem represents a set of EXT-X-SESSION-DATA attributes
|
3
|
+
class SessionDataItem
|
4
|
+
extend M3u8
|
5
|
+
attr_accessor :data_id, :value, :uri, :language
|
6
|
+
|
7
|
+
def initialize(params = {})
|
8
|
+
params.each do |key, value|
|
9
|
+
instance_variable_set("@#{key}", value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.parse(text)
|
14
|
+
attributes = parse_attributes text
|
15
|
+
options = { data_id: attributes['DATA-ID'], value: attributes['VALUE'],
|
16
|
+
uri: attributes['URI'], language: attributes['LANGUAGE'] }
|
17
|
+
M3u8::SessionDataItem.new options
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
attributes = [data_id_format,
|
22
|
+
value_format,
|
23
|
+
uri_format,
|
24
|
+
language_format].compact.join(',')
|
25
|
+
"#EXT-X-SESSION-DATA:#{attributes}"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def data_id_format
|
31
|
+
%(DATA-ID="#{data_id}")
|
32
|
+
end
|
33
|
+
|
34
|
+
def value_format
|
35
|
+
return if value.nil?
|
36
|
+
|
37
|
+
%(VALUE="#{value}")
|
38
|
+
end
|
39
|
+
|
40
|
+
def uri_format
|
41
|
+
return if uri.nil?
|
42
|
+
|
43
|
+
%(URI="#{uri}")
|
44
|
+
end
|
45
|
+
|
46
|
+
def language_format
|
47
|
+
return if language.nil?
|
48
|
+
|
49
|
+
%(LANGUAGE="#{language}")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/m3u8/version.rb
CHANGED
data/lib/m3u8/writer.rb
CHANGED
@@ -30,6 +30,7 @@ module M3u8
|
|
30
30
|
def write_header(playlist)
|
31
31
|
io.puts "#EXT-X-PLAYLIST-TYPE:#{playlist.type}" unless playlist.type.nil?
|
32
32
|
io.puts "#EXT-X-VERSION:#{playlist.version}"
|
33
|
+
io.puts '#EXT-X-I-FRAMES-ONLY' if playlist.iframes_only
|
33
34
|
io.puts "#EXT-X-MEDIA-SEQUENCE:#{playlist.sequence}"
|
34
35
|
io.puts "#EXT-X-ALLOW-CACHE:#{cache(playlist)}"
|
35
36
|
io.puts "#EXT-X-TARGETDURATION:#{playlist.target}"
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#EXTM3U
|
2
|
+
#EXT-X-STREAM-INF:BANDWIDTH=1280000
|
3
|
+
low/audio-video.m3u8
|
4
|
+
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8"
|
5
|
+
#EXT-X-STREAM-INF:BANDWIDTH=2560000
|
6
|
+
mid/audio-video.m3u8
|
7
|
+
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8"
|
8
|
+
#EXT-X-STREAM-INF:BANDWIDTH=7680000
|
9
|
+
hi/audio-video.m3u8
|
10
|
+
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8"
|
11
|
+
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5"
|
12
|
+
audio-only.m3u8
|
data/spec/m3u8_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe do
|
4
|
+
let(:test_class) { Class.new { extend M3u8 } }
|
5
|
+
|
6
|
+
it 'should parse attributes to hash' do
|
7
|
+
line = %(TEST-ID="Help",URI="http://test",ID=33)
|
8
|
+
hash = test_class.parse_attributes line
|
9
|
+
expect(hash['TEST-ID']).to eq 'Help'
|
10
|
+
expect(hash['URI']).to eq 'http://test'
|
11
|
+
expect(hash['ID']).to eq '33'
|
12
|
+
end
|
13
|
+
end
|
data/spec/media_item_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe M3u8::MediaItem do
|
4
4
|
it 'should provide m3u8 format representation' do
|
5
|
-
hash = { type: 'AUDIO',
|
5
|
+
hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
|
6
6
|
name: 'Francais', autoselect: true, default: false,
|
7
7
|
uri: 'frelo/prog_index.m3u8' }
|
8
8
|
item = M3u8::MediaItem.new(hash)
|
@@ -12,7 +12,7 @@ describe M3u8::MediaItem do
|
|
12
12
|
'URI="frelo/prog_index.m3u8"'
|
13
13
|
expect(output).to eq expected
|
14
14
|
|
15
|
-
hash = { type: 'AUDIO',
|
15
|
+
hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
|
16
16
|
assoc_language: 'spoken', name: 'Francais', autoselect: true,
|
17
17
|
default: false, forced: true, uri: 'frelo/prog_index.m3u8' }
|
18
18
|
item = M3u8::MediaItem.new(hash)
|
data/spec/playlist_item_spec.rb
CHANGED
@@ -3,34 +3,36 @@ require 'spec_helper'
|
|
3
3
|
describe M3u8::PlaylistItem do
|
4
4
|
it 'should initialize with hash' do
|
5
5
|
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
6
|
-
|
6
|
+
bandwidth: 540, uri: 'test.url' }
|
7
7
|
item = M3u8::PlaylistItem.new(hash)
|
8
8
|
expect(item.program_id).to eq 1
|
9
9
|
expect(item.width).to eq 1920
|
10
10
|
expect(item.height).to eq 1080
|
11
11
|
expect(item.resolution).to eq '1920x1080'
|
12
12
|
expect(item.codecs).to eq 'avc'
|
13
|
-
expect(item.
|
14
|
-
expect(item.
|
13
|
+
expect(item.bandwidth).to eq 540
|
14
|
+
expect(item.uri).to eq 'test.url'
|
15
|
+
expect(item.iframe).to be false
|
15
16
|
end
|
16
17
|
|
17
18
|
it 'should provide m3u8 format representation' do
|
18
19
|
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
19
|
-
|
20
|
+
bandwidth: 540, uri: 'test.url' }
|
20
21
|
item = M3u8::PlaylistItem.new(hash)
|
21
22
|
output = item.to_s
|
22
23
|
expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,' +
|
23
24
|
%(CODECS="avc",BANDWIDTH=540\ntest.url)
|
24
25
|
expect(output).to eq expected
|
25
26
|
|
26
|
-
hash = { program_id: 1, codecs: 'avc',
|
27
|
+
hash = { program_id: 1, codecs: 'avc', bandwidth: 540,
|
28
|
+
uri: 'test.url' }
|
27
29
|
item = M3u8::PlaylistItem.new(hash)
|
28
30
|
output = item.to_s
|
29
31
|
expected = '#EXT-X-STREAM-INF:PROGRAM-ID=1,' +
|
30
32
|
%(CODECS="avc",BANDWIDTH=540\ntest.url)
|
31
33
|
expect(output).to eq expected
|
32
34
|
|
33
|
-
hash = { codecs: 'avc',
|
35
|
+
hash = { codecs: 'avc', bandwidth: 540, uri: 'test.url', audio: 'test',
|
34
36
|
video: 'test2', average_bandwidth: 550, subtitles: 'subs',
|
35
37
|
closed_captions: 'caps' }
|
36
38
|
item = M3u8::PlaylistItem.new(hash)
|
@@ -41,6 +43,16 @@ describe M3u8::PlaylistItem do
|
|
41
43
|
expect(output).to eq expected
|
42
44
|
end
|
43
45
|
|
46
|
+
it 'should provided m3u8 format with I-Frame option' do
|
47
|
+
hash = { codecs: 'avc', bandwidth: 540, uri: 'test.url', iframe: true,
|
48
|
+
video: 'test2', average_bandwidth: 550 }
|
49
|
+
item = M3u8::PlaylistItem.new(hash)
|
50
|
+
output = item.to_s
|
51
|
+
expected = %(#EXT-X-I-FRAME-STREAM-INF:CODECS="avc",BANDWIDTH=540,) +
|
52
|
+
%(AVERAGE-BANDWIDTH=550,VIDEO="test2",URI="test.url")
|
53
|
+
expect(output).to eq expected
|
54
|
+
end
|
55
|
+
|
44
56
|
it 'should generate codecs string' do
|
45
57
|
item = M3u8::PlaylistItem.new
|
46
58
|
expect(item.codecs).to be_nil
|
@@ -119,7 +131,7 @@ describe M3u8::PlaylistItem do
|
|
119
131
|
end
|
120
132
|
|
121
133
|
it 'should raise error if codecs are missing' do
|
122
|
-
params = { program_id: 1,
|
134
|
+
params = { program_id: 1, bandwidth: 540, uri: 'test.url' }
|
123
135
|
item = M3u8::PlaylistItem.new params
|
124
136
|
message = 'Audio or video codec info should be provided.'
|
125
137
|
expect { item.to_s }.to raise_error(M3u8::MissingCodecError, message)
|
data/spec/playlist_spec.rb
CHANGED
@@ -8,7 +8,7 @@ describe M3u8::Playlist do
|
|
8
8
|
end
|
9
9
|
|
10
10
|
it 'should render master playlist' do
|
11
|
-
options = {
|
11
|
+
options = { uri: 'playlist_url', bandwidth: 6400,
|
12
12
|
audio_codec: 'mp3' }
|
13
13
|
item = M3u8::PlaylistItem.new options
|
14
14
|
playlist = M3u8::Playlist.new
|
@@ -19,7 +19,7 @@ describe M3u8::Playlist do
|
|
19
19
|
",BANDWIDTH=6400\nplaylist_url\n"
|
20
20
|
expect(playlist.to_s).to eq output
|
21
21
|
|
22
|
-
options = { program_id: '1',
|
22
|
+
options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
|
23
23
|
audio_codec: 'mp3' }
|
24
24
|
item = M3u8::PlaylistItem.new options
|
25
25
|
playlist = M3u8::Playlist.new
|
@@ -30,7 +30,7 @@ describe M3u8::Playlist do
|
|
30
30
|
",BANDWIDTH=6400\nplaylist_url\n"
|
31
31
|
expect(playlist.to_s).to eq output
|
32
32
|
|
33
|
-
options = { program_id: '2',
|
33
|
+
options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
|
34
34
|
width: 1920, height: 1080, profile: 'high', level: 4.1,
|
35
35
|
audio_codec: 'aac-lc' }
|
36
36
|
item = M3u8::PlaylistItem.new options
|
@@ -45,11 +45,11 @@ describe M3u8::Playlist do
|
|
45
45
|
expect(playlist.to_s).to eq output
|
46
46
|
|
47
47
|
playlist = M3u8::Playlist.new
|
48
|
-
options = { program_id: '1',
|
48
|
+
options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
|
49
49
|
audio_codec: 'mp3' }
|
50
50
|
item = M3u8::PlaylistItem.new options
|
51
51
|
playlist.items.push item
|
52
|
-
options = { program_id: '2',
|
52
|
+
options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
|
53
53
|
width: 1920, height: 1080, profile: 'high', level: 4.1,
|
54
54
|
audio_codec: 'aac-lc' }
|
55
55
|
item = M3u8::PlaylistItem.new options
|
@@ -118,7 +118,7 @@ describe M3u8::Playlist do
|
|
118
118
|
it 'should write playlist to io' do
|
119
119
|
test_io = StringIO.new
|
120
120
|
playlist = M3u8::Playlist.new
|
121
|
-
options = { program_id: '1',
|
121
|
+
options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
|
122
122
|
audio_codec: 'mp3' }
|
123
123
|
item = M3u8::PlaylistItem.new options
|
124
124
|
playlist.items.push item
|
@@ -143,7 +143,7 @@ describe M3u8::Playlist do
|
|
143
143
|
it 'should report if it is a master playlist' do
|
144
144
|
playlist = M3u8::Playlist.new
|
145
145
|
expect(playlist.master?).to be false
|
146
|
-
options = { program_id: '1',
|
146
|
+
options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
|
147
147
|
audio_codec: 'mp3' }
|
148
148
|
item = M3u8::PlaylistItem.new options
|
149
149
|
playlist.items.push item
|
@@ -155,7 +155,7 @@ describe M3u8::Playlist do
|
|
155
155
|
playlist = M3u8::Playlist.new
|
156
156
|
|
157
157
|
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
158
|
-
|
158
|
+
bandwidth: 540, uri: 'test.url' }
|
159
159
|
item = M3u8::PlaylistItem.new(hash)
|
160
160
|
playlist.items.push item
|
161
161
|
|
@@ -174,13 +174,13 @@ describe M3u8::Playlist do
|
|
174
174
|
expect(playlist.valid?).to be true
|
175
175
|
|
176
176
|
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
177
|
-
|
177
|
+
bandwidth: 540, uri: 'test.url' }
|
178
178
|
item = M3u8::PlaylistItem.new(hash)
|
179
179
|
playlist.items.push item
|
180
180
|
expect(playlist.valid?).to be true
|
181
181
|
|
182
182
|
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
183
|
-
|
183
|
+
bandwidth: 540, uri: 'test.url' }
|
184
184
|
item = M3u8::PlaylistItem.new(hash)
|
185
185
|
playlist.items.push item
|
186
186
|
expect(playlist.valid?).to be true
|
@@ -201,6 +201,7 @@ describe M3u8::Playlist do
|
|
201
201
|
expect(playlist.target).to be 12
|
202
202
|
expect(playlist.sequence).to be 1
|
203
203
|
expect(playlist.type).to eq('VOD')
|
204
|
+
expect(playlist.iframes_only).to be false
|
204
205
|
end
|
205
206
|
|
206
207
|
it 'should allow reading of playlists' do
|
data/spec/reader_spec.rb
CHANGED
@@ -9,13 +9,14 @@ describe M3u8::Reader do
|
|
9
9
|
|
10
10
|
item = playlist.items[0]
|
11
11
|
expect(item).to be_a(M3u8::PlaylistItem)
|
12
|
-
expect(item.
|
12
|
+
expect(item.uri).to eq('hls/1080-7mbps/1080-7mbps.m3u8')
|
13
13
|
expect(item.program_id).to eq('1')
|
14
14
|
expect(item.width).to eq(1920)
|
15
15
|
expect(item.height).to eq(1080)
|
16
16
|
expect(item.resolution).to eq('1920x1080')
|
17
17
|
expect(item.codecs).to eq('avc1.640028,mp4a.40.2')
|
18
|
-
expect(item.
|
18
|
+
expect(item.bandwidth).to eq(5_042_000)
|
19
|
+
expect(item.iframe).to be false
|
19
20
|
|
20
21
|
expect(playlist.items.size).to eq 6
|
21
22
|
|
@@ -23,6 +24,21 @@ describe M3u8::Reader do
|
|
23
24
|
expect(item.resolution).to be_nil
|
24
25
|
end
|
25
26
|
|
27
|
+
it 'should parse master playlist with I-Frames' do
|
28
|
+
file = File.open 'spec/fixtures/master_iframes.m3u8'
|
29
|
+
reader = M3u8::Reader.new
|
30
|
+
playlist = reader.read file
|
31
|
+
expect(playlist.master?).to be true
|
32
|
+
|
33
|
+
expect(playlist.items.size).to eq 7
|
34
|
+
|
35
|
+
item = playlist.items[1]
|
36
|
+
expect(item).to be_a(M3u8::PlaylistItem)
|
37
|
+
expect(item.bandwidth).to eq(86_000)
|
38
|
+
expect(item.iframe).to be true
|
39
|
+
expect(item.uri).to eq 'low/iframe.m3u8'
|
40
|
+
end
|
41
|
+
|
26
42
|
it 'should parse segment playlist' do
|
27
43
|
file = File.open 'spec/fixtures/playlist.m3u8'
|
28
44
|
reader = M3u8::Reader.new
|
@@ -42,6 +58,26 @@ describe M3u8::Reader do
|
|
42
58
|
expect(playlist.items.size).to eq 138
|
43
59
|
end
|
44
60
|
|
61
|
+
it 'should parse I-Frame playlist' do
|
62
|
+
file = File.open 'spec/fixtures/iframes.m3u8'
|
63
|
+
reader = M3u8::Reader.new
|
64
|
+
playlist = reader.read file
|
65
|
+
|
66
|
+
expect(playlist.iframes_only).to be true
|
67
|
+
expect(playlist.items.size).to eq 3
|
68
|
+
|
69
|
+
item = playlist.items[0]
|
70
|
+
expect(item).to be_a(M3u8::SegmentItem)
|
71
|
+
expect(item.duration).to eq 4.12
|
72
|
+
expect(item.byterange_length).to eq 9400
|
73
|
+
expect(item.byterange_start).to eq 376
|
74
|
+
expect(item.segment).to eq 'segment1.ts'
|
75
|
+
|
76
|
+
item = playlist.items[1]
|
77
|
+
expect(item.byterange_length).to eq 7144
|
78
|
+
expect(item.byterange_start).to be_nil
|
79
|
+
end
|
80
|
+
|
45
81
|
it 'should parse segment playlist with comments' do
|
46
82
|
file = File.open 'spec/fixtures/playlist_with_comments.m3u8'
|
47
83
|
reader = M3u8::Reader.new
|
@@ -72,7 +108,7 @@ describe M3u8::Reader do
|
|
72
108
|
item = playlist.items[0]
|
73
109
|
expect(item).to be_a M3u8::MediaItem
|
74
110
|
expect(item.type).to eq 'AUDIO'
|
75
|
-
expect(item.
|
111
|
+
expect(item.group_id).to eq 'audio-lo'
|
76
112
|
expect(item.language).to eq 'eng'
|
77
113
|
expect(item.assoc_language).to eq 'spoken'
|
78
114
|
expect(item.name).to eq 'English'
|
@@ -93,7 +129,7 @@ describe M3u8::Reader do
|
|
93
129
|
item = playlist.items[1]
|
94
130
|
expect(item).to be_a M3u8::MediaItem
|
95
131
|
expect(item.type).to eq 'VIDEO'
|
96
|
-
expect(item.
|
132
|
+
expect(item.group_id).to eq '200kbs'
|
97
133
|
expect(item.language).to be_nil
|
98
134
|
expect(item.name).to eq 'Angle2'
|
99
135
|
expect(item.autoselect).to be true
|
@@ -120,4 +156,17 @@ describe M3u8::Reader do
|
|
120
156
|
|
121
157
|
expect(playlist.items.size).to eq 6
|
122
158
|
end
|
159
|
+
|
160
|
+
it 'should parse playlist with session data' do
|
161
|
+
file = File.open 'spec/fixtures/session_data.m3u8'
|
162
|
+
reader = M3u8::Reader.new
|
163
|
+
playlist = reader.read file
|
164
|
+
|
165
|
+
expect(playlist.items.size).to eq 3
|
166
|
+
|
167
|
+
item = playlist.items[0]
|
168
|
+
expect(item).to be_a M3u8::SessionDataItem
|
169
|
+
expect(item.data_id).to eq 'com.example.lyrics'
|
170
|
+
expect(item.uri).to eq 'lyrics.json'
|
171
|
+
end
|
123
172
|
end
|
data/spec/segment_item_spec.rb
CHANGED
@@ -8,9 +8,12 @@ describe M3u8::SegmentItem do
|
|
8
8
|
expect(item.segment).to eq 'test.ts'
|
9
9
|
expect(item.comment).to be_nil
|
10
10
|
|
11
|
-
hash = { duration: 10.991, segment: 'test.ts', comment: 'anything'
|
11
|
+
hash = { duration: 10.991, segment: 'test.ts', comment: 'anything',
|
12
|
+
byterange_length: 4500, byterange_start: 600 }
|
12
13
|
item = M3u8::SegmentItem.new(hash)
|
13
14
|
expect(item.duration).to eq 10.991
|
15
|
+
expect(item.byterange_length).to eq 4500
|
16
|
+
expect(item.byterange_start).to eq 600
|
14
17
|
expect(item.segment).to eq 'test.ts'
|
15
18
|
expect(item.comment).to eq 'anything'
|
16
19
|
end
|
@@ -27,5 +30,19 @@ describe M3u8::SegmentItem do
|
|
27
30
|
output = item.to_s
|
28
31
|
expected = "#EXTINF:10.991,anything\ntest.ts"
|
29
32
|
expect(output).to eq expected
|
33
|
+
|
34
|
+
hash = { duration: 10.991, segment: 'test.ts', comment: 'anything',
|
35
|
+
byterange_length: 4500, byterange_start: 600 }
|
36
|
+
item = M3u8::SegmentItem.new(hash)
|
37
|
+
output = item.to_s
|
38
|
+
expected = "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500@600\ntest.ts"
|
39
|
+
expect(output).to eq expected
|
40
|
+
|
41
|
+
hash = { duration: 10.991, segment: 'test.ts', comment: 'anything',
|
42
|
+
byterange_length: 4500 }
|
43
|
+
item = M3u8::SegmentItem.new(hash)
|
44
|
+
output = item.to_s
|
45
|
+
expected = "#EXTINF:10.991,anything\n#EXT-X-BYTERANGE:4500\ntest.ts"
|
46
|
+
expect(output).to eq expected
|
30
47
|
end
|
31
48
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe M3u8::SessionDataItem do
|
4
|
+
it 'should initialize with hash' do
|
5
|
+
hash = { data_id: 'com.test.movie.title', value: 'Test',
|
6
|
+
uri: 'http://test', language: 'en' }
|
7
|
+
item = M3u8::SessionDataItem.new(hash)
|
8
|
+
expect(item.data_id).to eq 'com.test.movie.title'
|
9
|
+
expect(item.value).to eq 'Test'
|
10
|
+
expect(item.uri).to eq 'http://test'
|
11
|
+
expect(item.language).to eq 'en'
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should provide m3u8 format representation' do
|
15
|
+
hash = { data_id: 'com.test.movie.title', value: 'Test',
|
16
|
+
language: 'en' }
|
17
|
+
item = M3u8::SessionDataItem.new(hash)
|
18
|
+
output = item.to_s
|
19
|
+
expected = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
|
20
|
+
%(VALUE="Test",LANGUAGE="en")
|
21
|
+
expect(output).to eq expected
|
22
|
+
|
23
|
+
hash = { data_id: 'com.test.movie.title', uri: 'http://test',
|
24
|
+
language: 'en' }
|
25
|
+
item = M3u8::SessionDataItem.new(hash)
|
26
|
+
output = item.to_s
|
27
|
+
expected = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
|
28
|
+
%(URI="http://test",LANGUAGE="en")
|
29
|
+
expect(output).to eq expected
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should parse m3u8 format into instance' do
|
33
|
+
format = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
|
34
|
+
%(VALUE="Test",LANGUAGE="en")
|
35
|
+
item = M3u8::SessionDataItem.parse format
|
36
|
+
expect(item.data_id).to eq 'com.test.movie.title'
|
37
|
+
expect(item.value).to eq 'Test'
|
38
|
+
expect(item.uri).to be_nil
|
39
|
+
expect(item.language).to eq 'en'
|
40
|
+
|
41
|
+
format = %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
|
42
|
+
%(URI="http://test",LANGUAGE="en")
|
43
|
+
item = M3u8::SessionDataItem.parse format
|
44
|
+
expect(item.data_id).to eq 'com.test.movie.title'
|
45
|
+
expect(item.value).to be_nil
|
46
|
+
expect(item.uri).to eq 'http://test'
|
47
|
+
expect(item.language).to eq 'en'
|
48
|
+
end
|
49
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/writer_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe M3u8::Writer do
|
4
4
|
it 'should render master playlist' do
|
5
|
-
options = {
|
5
|
+
options = { uri: 'playlist_url', bandwidth: 6400,
|
6
6
|
audio_codec: 'mp3' }
|
7
7
|
item = M3u8::PlaylistItem.new options
|
8
8
|
playlist = M3u8::Playlist.new
|
@@ -17,7 +17,7 @@ describe M3u8::Writer do
|
|
17
17
|
writer.write playlist
|
18
18
|
expect(io.string).to eq output
|
19
19
|
|
20
|
-
options = { program_id: '1',
|
20
|
+
options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
|
21
21
|
audio_codec: 'mp3' }
|
22
22
|
item = M3u8::PlaylistItem.new options
|
23
23
|
playlist = M3u8::Playlist.new
|
@@ -32,7 +32,7 @@ describe M3u8::Writer do
|
|
32
32
|
writer.write playlist
|
33
33
|
expect(io.string).to eq output
|
34
34
|
|
35
|
-
options = { program_id: '2',
|
35
|
+
options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
|
36
36
|
width: 1920, height: 1080, profile: 'high', level: 4.1,
|
37
37
|
audio_codec: 'aac-lc' }
|
38
38
|
item = M3u8::PlaylistItem.new options
|
@@ -50,21 +50,27 @@ describe M3u8::Writer do
|
|
50
50
|
expect(io.string).to eq output
|
51
51
|
|
52
52
|
playlist = M3u8::Playlist.new
|
53
|
-
options = { program_id: '1',
|
53
|
+
options = { program_id: '1', uri: 'playlist_url', bandwidth: 6400,
|
54
54
|
audio_codec: 'mp3' }
|
55
55
|
item = M3u8::PlaylistItem.new options
|
56
56
|
playlist.items.push item
|
57
|
-
options = { program_id: '2',
|
57
|
+
options = { program_id: '2', uri: 'playlist_url', bandwidth: 50_000,
|
58
58
|
width: 1920, height: 1080, profile: 'high', level: 4.1,
|
59
59
|
audio_codec: 'aac-lc' }
|
60
60
|
item = M3u8::PlaylistItem.new options
|
61
61
|
playlist.items.push item
|
62
|
+
options = { data_id: 'com.test.movie.title', value: 'Test',
|
63
|
+
uri: 'http://test', language: 'en' }
|
64
|
+
item = M3u8::SessionDataItem.new(options)
|
65
|
+
playlist.items.push item
|
62
66
|
|
63
67
|
output = "#EXTM3U\n" +
|
64
68
|
%(#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34") +
|
65
69
|
",BANDWIDTH=6400\nplaylist_url\n#EXT-X-STREAM-INF:PROGRAM-ID=2," +
|
66
70
|
%(RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2") +
|
67
|
-
",BANDWIDTH=50000\nplaylist_url\n"
|
71
|
+
",BANDWIDTH=50000\nplaylist_url\n" +
|
72
|
+
%(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
|
73
|
+
%(VALUE="Test",URI="http://test",LANGUAGE="en"\n)
|
68
74
|
|
69
75
|
io = StringIO.open
|
70
76
|
writer = M3u8::Writer.new io
|
@@ -110,8 +116,8 @@ describe M3u8::Writer do
|
|
110
116
|
writer.write playlist
|
111
117
|
expect(io.string).to eq output
|
112
118
|
|
113
|
-
options = { version:
|
114
|
-
type: 'EVENT' }
|
119
|
+
options = { version: 4, cache: false, target: 12, sequence: 1,
|
120
|
+
type: 'EVENT', iframes_only: true }
|
115
121
|
playlist = M3u8::Playlist.new options
|
116
122
|
options = { duration: 11.344644, segment: '1080-7mbps00000.ts' }
|
117
123
|
item = M3u8::SegmentItem.new options
|
@@ -119,7 +125,8 @@ describe M3u8::Writer do
|
|
119
125
|
|
120
126
|
output = "#EXTM3U\n" \
|
121
127
|
"#EXT-X-PLAYLIST-TYPE:EVENT\n" \
|
122
|
-
"#EXT-X-VERSION:
|
128
|
+
"#EXT-X-VERSION:4\n" \
|
129
|
+
"#EXT-X-I-FRAMES-ONLY\n" \
|
123
130
|
"#EXT-X-MEDIA-SEQUENCE:1\n" \
|
124
131
|
"#EXT-X-ALLOW-CACHE:NO\n" \
|
125
132
|
"#EXT-X-TARGETDURATION:12\n" \
|
@@ -136,7 +143,7 @@ describe M3u8::Writer do
|
|
136
143
|
playlist = M3u8::Playlist.new
|
137
144
|
|
138
145
|
hash = { program_id: 1, width: 1920, height: 1080, codecs: 'avc',
|
139
|
-
|
146
|
+
bandwidth: 540, playlist: 'test.url' }
|
140
147
|
item = M3u8::PlaylistItem.new(hash)
|
141
148
|
playlist.items.push item
|
142
149
|
|
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.5.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: 2015-
|
11
|
+
date: 2015-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -76,19 +76,25 @@ files:
|
|
76
76
|
- lib/m3u8/playlist_item.rb
|
77
77
|
- lib/m3u8/reader.rb
|
78
78
|
- lib/m3u8/segment_item.rb
|
79
|
+
- lib/m3u8/session_data_item.rb
|
79
80
|
- lib/m3u8/version.rb
|
80
81
|
- lib/m3u8/writer.rb
|
81
82
|
- m3u8.gemspec
|
83
|
+
- spec/fixtures/iframes.m3u8
|
82
84
|
- spec/fixtures/master.m3u8
|
85
|
+
- spec/fixtures/master_iframes.m3u8
|
83
86
|
- spec/fixtures/playlist.m3u8
|
84
87
|
- spec/fixtures/playlist_with_comments.m3u8
|
88
|
+
- spec/fixtures/session_data.m3u8
|
85
89
|
- spec/fixtures/variant_angles.m3u8
|
86
90
|
- spec/fixtures/variant_audio.m3u8
|
91
|
+
- spec/m3u8_spec.rb
|
87
92
|
- spec/media_item_spec.rb
|
88
93
|
- spec/playlist_item_spec.rb
|
89
94
|
- spec/playlist_spec.rb
|
90
95
|
- spec/reader_spec.rb
|
91
96
|
- spec/segment_item_spec.rb
|
97
|
+
- spec/session_data_item_spec.rb
|
92
98
|
- spec/spec_helper.rb
|
93
99
|
- spec/writer_spec.rb
|
94
100
|
homepage: https://github.com/sethdeckard/m3u8
|
@@ -116,15 +122,20 @@ signing_key:
|
|
116
122
|
specification_version: 4
|
117
123
|
summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
|
118
124
|
test_files:
|
125
|
+
- spec/fixtures/iframes.m3u8
|
119
126
|
- spec/fixtures/master.m3u8
|
127
|
+
- spec/fixtures/master_iframes.m3u8
|
120
128
|
- spec/fixtures/playlist.m3u8
|
121
129
|
- spec/fixtures/playlist_with_comments.m3u8
|
130
|
+
- spec/fixtures/session_data.m3u8
|
122
131
|
- spec/fixtures/variant_angles.m3u8
|
123
132
|
- spec/fixtures/variant_audio.m3u8
|
133
|
+
- spec/m3u8_spec.rb
|
124
134
|
- spec/media_item_spec.rb
|
125
135
|
- spec/playlist_item_spec.rb
|
126
136
|
- spec/playlist_spec.rb
|
127
137
|
- spec/reader_spec.rb
|
128
138
|
- spec/segment_item_spec.rb
|
139
|
+
- spec/session_data_item_spec.rb
|
129
140
|
- spec/spec_helper.rb
|
130
141
|
- spec/writer_spec.rb
|