m3u8 0.4.0 → 0.5.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 +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
|