m3u8 0.7.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +5 -14
- data/.travis.yml +3 -3
- data/CHANGELOG.md +11 -2
- data/README.md +26 -58
- data/lib/m3u8/byte_range.rb +2 -2
- data/lib/m3u8/discontinuity_item.rb +0 -2
- data/lib/m3u8/error.rb +4 -1
- data/lib/m3u8/map_item.rb +2 -2
- data/lib/m3u8/media_item.rb +39 -14
- data/lib/m3u8/playlist.rb +26 -15
- data/lib/m3u8/playlist_item.rb +13 -7
- data/lib/m3u8/reader.rb +22 -3
- data/lib/m3u8/time_item.rb +1 -1
- data/lib/m3u8/version.rb +1 -1
- data/lib/m3u8/writer.rb +20 -9
- data/spec/fixtures/playlist.m3u8 +1 -0
- data/spec/fixtures/session_data.m3u8 +1 -0
- data/spec/lib/m3u8/discontinuity_item_spec.rb +1 -3
- data/spec/lib/m3u8/media_item_spec.rb +80 -33
- data/spec/lib/m3u8/playlist_item_spec.rb +105 -79
- data/spec/lib/m3u8/playlist_spec.rb +211 -190
- data/spec/lib/m3u8/reader_spec.rb +33 -23
- data/spec/lib/m3u8/writer_spec.rb +221 -147
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 266fa2220bfc0c9acdc04144747da7d7a01d9b5f
|
4
|
+
data.tar.gz: ed5b06cd8baccf5eec7a4fd592a4715a367fa791
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9942efab7468eaa317280e6b465251d972700e43ce3b7b0ca281c4fe3084ad3743acbe24074a324fdaa4f9421be0da732a3128e00311b468caf257814337ba1e
|
7
|
+
data.tar.gz: d02d9f3c6853f68557ff89d5a6a7d3d4f3a2c77a5d1ea535fa3e5c89911ec0c724e8646dfaa2da6f951a8672494fb32cfc1b9dd0502da95d29ed0a82e94e3d94
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
-
0.
|
1
|
+
**0.8.0 (4/13/2017)**
|
2
|
+
|
3
|
+
* Added several improvements for writing playlists: Allow playlist to be initalized as master, expose `write_header`/`write_footer` on `Writer` class.
|
4
|
+
* Added support for the `#EXT-X-DISCONTINUITY-SEQUENCE` tag. Added missing attributes to `MediaItem`: `INSTREAM-ID`, `CHARACTERISTICS`, `CHANNELS`, Added missing `HDCP-LEVEL` attribute to `PlaylistItem`.
|
5
|
+
* Fixed issue [#20](https://github.com/sethdeckard/m3u8/issues/20).
|
6
|
+
* Merged pull request #21 from [rmberg](https://github.com/rmberg), adding support for live playlists to `Writer`.
|
7
|
+
|
8
|
+
***
|
9
|
+
|
10
|
+
0.7.1 (2/23/2017) - Added support for the `#EXT-X-DATERANGE` tag. Minor refactoring of existing classes.
|
2
11
|
|
3
12
|
***
|
4
13
|
|
5
|
-
0.7.0 (2/3/2017) - Added support for EXT-X-INDEPENDENT-SEGMENTS and EXT-X-START tags. Minor version bumped due to changes of default values for new playlists.
|
14
|
+
0.7.0 (2/3/2017) - Added support for `#EXT-X-INDEPENDENT-SEGMENTS` and `#EXT-X-START` tags. Minor version bumped due to changes of default values for new playlists.
|
6
15
|
|
7
16
|
***
|
8
17
|
|
data/README.md
CHANGED
@@ -6,7 +6,13 @@
|
|
6
6
|
[![security](https://hakiri.io/github/sethdeckard/m3u8/master.svg)](https://hakiri.io/github/sethdeckard/m3u8/master)
|
7
7
|
# m3u8
|
8
8
|
|
9
|
-
m3u8 provides generation and parsing of m3u8 playlists
|
9
|
+
m3u8 provides easy generation and parsing of m3u8 playlists defined in the [HTTP Live Streaming (HLS)](https://tools.ietf.org/html/draft-pantos-http-live-streaming-20) Internet Draft published by Apple.
|
10
|
+
|
11
|
+
* The library completely implements version 20 of the HLS Internet Draft.
|
12
|
+
* Provides parsing of an m3u8 playlist into an object model from any File, StringIO, or string.
|
13
|
+
* Provides ability to write playlist to a File or StringIO or expose as string via to_s.
|
14
|
+
* Distinction between a master and media playlist is handled automatically (single Playlist class).
|
15
|
+
* Optionally, the library can automatically generate the audio/video codecs string used in the CODEC attribute based on specified H.264, AAC, or MP3 options (such as Profile/Level).
|
10
16
|
|
11
17
|
## Installation
|
12
18
|
|
@@ -86,23 +92,8 @@ options = { width: 1920, height: 1080, codecs: 'avc1.66.30,mp4a.40.2',
|
|
86
92
|
item = M3u8::PlaylistItem.new(options)
|
87
93
|
```
|
88
94
|
|
89
|
-
Just get the codec string for custom use:
|
90
|
-
|
91
|
-
```ruby
|
92
|
-
options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
|
93
|
-
codecs = M3u8::Playlist.codecs(options)
|
94
|
-
# => "avc1.66.30,mp4a.40.2"
|
95
|
-
```
|
96
|
-
|
97
|
-
Values for audio_codec (codec name): aac-lc, he-aac, mp3
|
98
|
-
|
99
|
-
Possible values for profile (H.264 Profile): baseline, main, high.
|
100
|
-
|
101
|
-
Possible values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
|
102
|
-
|
103
|
-
Not all Levels and Profiles can be combined, consult H.264 documentation
|
104
95
|
|
105
|
-
##
|
96
|
+
## Usage (parsing playlists)
|
106
97
|
|
107
98
|
```ruby
|
108
99
|
file = File.open 'spec/fixtures/master.m3u8'
|
@@ -131,49 +122,26 @@ playlist.items.unshift(item)
|
|
131
122
|
```
|
132
123
|
|
133
124
|
M3u8::Reader is the class handles parsing if you want more control over the process.
|
134
|
-
|
135
|
-
##
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
*
|
145
|
-
*
|
146
|
-
*
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
* EXTM3U
|
151
|
-
* EXT-X-VERSION
|
152
|
-
* EXTINF
|
153
|
-
* EXT-X-BYTERANGE
|
154
|
-
* EXT-X-DISCONTINUITY
|
155
|
-
* EXT-X-KEY
|
156
|
-
* EXT-X-MAP
|
157
|
-
* EXT-X-PROGRAM-DATE-TIME
|
158
|
-
* EXT-X-TARGETDURATION
|
159
|
-
* EXT-X-MEDIA-SEQUENCE
|
160
|
-
* EXT-X-ENDLIST
|
161
|
-
* EXT-X-PLAYLIST-TYPE
|
162
|
-
* EXT-X-I-FRAMES-ONLY
|
163
|
-
* EXT-X-MEDIA
|
164
|
-
* EXT-X-STREAM-INF
|
165
|
-
* EXT-X-I-FRAME-STREAM-INF
|
166
|
-
* EXT-X-SESSION-DATA
|
167
|
-
* EXT-X-SESSION-KEY
|
168
|
-
* EXT-X-START
|
169
|
-
|
170
|
-
### TODO:
|
171
|
-
* EXT-X-DATERANGE
|
125
|
+
|
126
|
+
## Usage (misc)
|
127
|
+
Generate the codec string based on audio and video codec options without dealing a playlist instance:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
|
131
|
+
codecs = M3u8::Playlist.codecs(options)
|
132
|
+
# => "avc1.66.30,mp4a.40.2"
|
133
|
+
```
|
134
|
+
|
135
|
+
* Values for audio_codec (codec name): aac-lc, he-aac, mp3
|
136
|
+
* Values for profile (H.264 Profile): baseline, main, high.
|
137
|
+
* Values for level (H.264 Level): 3.0, 3.1, 4.0, 4.1.
|
138
|
+
|
139
|
+
Not all Levels and Profiles can be combined and validation is not currently implemented, consult H.264 documentation for further details.
|
140
|
+
|
172
141
|
|
173
142
|
## Roadmap
|
174
|
-
*
|
175
|
-
*
|
176
|
-
* Support for different versions of spec, defaulting to latest.
|
143
|
+
* Implement validation of all tags, attributes, and values per HLS I-D.
|
144
|
+
* Perhaps support for different versions of HLS I-D, defaulting to latest.
|
177
145
|
|
178
146
|
## Contributing
|
179
147
|
|
data/lib/m3u8/byte_range.rb
CHANGED
@@ -11,11 +11,11 @@ module M3u8
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def self.parse(text)
|
14
|
-
values = text.split
|
14
|
+
values = text.split('@')
|
15
15
|
length_value = values[0].to_i
|
16
16
|
start_value = values[1].to_i unless values[1].nil?
|
17
17
|
options = { length: length_value, start: start_value }
|
18
|
-
ByteRange.new
|
18
|
+
ByteRange.new(options)
|
19
19
|
end
|
20
20
|
|
21
21
|
def to_s
|
data/lib/m3u8/error.rb
CHANGED
data/lib/m3u8/map_item.rb
CHANGED
@@ -12,11 +12,11 @@ module M3u8
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.parse(text)
|
15
|
-
attributes = parse_attributes
|
15
|
+
attributes = parse_attributes(text)
|
16
16
|
range_value = attributes['BYTERANGE']
|
17
17
|
range = ByteRange.parse(range_value) unless range_value.nil?
|
18
18
|
options = { uri: attributes['URI'], byterange: range }
|
19
|
-
MapItem.new
|
19
|
+
MapItem.new(options)
|
20
20
|
end
|
21
21
|
|
22
22
|
def to_s
|
data/lib/m3u8/media_item.rb
CHANGED
@@ -4,7 +4,8 @@ module M3u8
|
|
4
4
|
class MediaItem
|
5
5
|
extend M3u8
|
6
6
|
attr_accessor :type, :group_id, :language, :assoc_language, :name,
|
7
|
-
:autoselect, :default, :uri, :forced
|
7
|
+
:autoselect, :default, :uri, :forced, :instream_id,
|
8
|
+
:characteristics, :channels
|
8
9
|
|
9
10
|
def initialize(params = {})
|
10
11
|
params.each do |key, value|
|
@@ -13,7 +14,7 @@ module M3u8
|
|
13
14
|
end
|
14
15
|
|
15
16
|
def self.parse(text)
|
16
|
-
attributes = parse_attributes
|
17
|
+
attributes = parse_attributes(text)
|
17
18
|
options = { type: attributes['TYPE'], group_id: attributes['GROUP-ID'],
|
18
19
|
language: attributes['LANGUAGE'],
|
19
20
|
assoc_language: attributes['ASSOC-LANGUAGE'],
|
@@ -21,25 +22,34 @@ module M3u8
|
|
21
22
|
autoselect: parse_yes_no(attributes['AUTOSELECT']),
|
22
23
|
default: parse_yes_no(attributes['DEFAULT']),
|
23
24
|
forced: parse_yes_no(attributes['FORCED']),
|
24
|
-
uri: attributes['URI']
|
25
|
-
|
25
|
+
uri: attributes['URI'],
|
26
|
+
instream_id: attributes['INSTREAM-ID'],
|
27
|
+
characteristics: attributes['CHARACTERISTICS'],
|
28
|
+
channels: attributes['CHANNELS'] }
|
29
|
+
MediaItem.new(options)
|
26
30
|
end
|
27
31
|
|
28
32
|
def to_s
|
29
|
-
|
30
|
-
group_id_format,
|
31
|
-
language_format,
|
32
|
-
assoc_language_format,
|
33
|
-
name_format,
|
34
|
-
autoselect_format,
|
35
|
-
default_format,
|
36
|
-
uri_format,
|
37
|
-
forced_format].compact.join(',')
|
38
|
-
"#EXT-X-MEDIA:#{attributes}"
|
33
|
+
"#EXT-X-MEDIA:#{formatted_attributes.join(',')}"
|
39
34
|
end
|
40
35
|
|
41
36
|
private
|
42
37
|
|
38
|
+
def formatted_attributes
|
39
|
+
[type_format,
|
40
|
+
group_id_format,
|
41
|
+
language_format,
|
42
|
+
assoc_language_format,
|
43
|
+
name_format,
|
44
|
+
autoselect_format,
|
45
|
+
default_format,
|
46
|
+
uri_format,
|
47
|
+
forced_format,
|
48
|
+
instream_id_format,
|
49
|
+
characteristics_format,
|
50
|
+
channels_format].compact
|
51
|
+
end
|
52
|
+
|
43
53
|
def type_format
|
44
54
|
"TYPE=#{type}"
|
45
55
|
end
|
@@ -82,6 +92,21 @@ module M3u8
|
|
82
92
|
"FORCED=#{to_yes_no forced}"
|
83
93
|
end
|
84
94
|
|
95
|
+
def instream_id_format
|
96
|
+
return if instream_id.nil?
|
97
|
+
%(INSTREAM-ID="#{instream_id}")
|
98
|
+
end
|
99
|
+
|
100
|
+
def characteristics_format
|
101
|
+
return if characteristics.nil?
|
102
|
+
%(CHARACTERISTICS="#{characteristics}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def channels_format
|
106
|
+
return if channels.nil?
|
107
|
+
%(CHANNELS="#{channels}")
|
108
|
+
end
|
109
|
+
|
85
110
|
def to_yes_no(boolean)
|
86
111
|
boolean == true ? 'YES' : 'NO'
|
87
112
|
end
|
data/lib/m3u8/playlist.rb
CHANGED
@@ -3,22 +3,23 @@ module M3u8
|
|
3
3
|
# Playlist represents an m3u8 playlist, it can be a master playlist or a set
|
4
4
|
# of media segments
|
5
5
|
class Playlist
|
6
|
-
attr_accessor :items, :version, :cache, :target, :sequence,
|
7
|
-
:
|
6
|
+
attr_accessor :items, :version, :cache, :target, :sequence,
|
7
|
+
:discontinuity_sequence, :type, :iframes_only,
|
8
|
+
:independent_segments, :live
|
8
9
|
|
9
10
|
def initialize(options = {})
|
10
|
-
assign_options
|
11
|
-
|
11
|
+
assign_options(options)
|
12
|
+
@items = []
|
12
13
|
end
|
13
14
|
|
14
15
|
def self.codecs(options = {})
|
15
|
-
item = PlaylistItem.new
|
16
|
+
item = PlaylistItem.new(options)
|
16
17
|
item.codecs
|
17
18
|
end
|
18
19
|
|
19
20
|
def self.read(input)
|
20
21
|
reader = Reader.new
|
21
|
-
reader.read
|
22
|
+
reader.read(input)
|
22
23
|
end
|
23
24
|
|
24
25
|
def write(output)
|
@@ -26,8 +27,14 @@ module M3u8
|
|
26
27
|
writer.write(self)
|
27
28
|
end
|
28
29
|
|
30
|
+
def live?
|
31
|
+
return false if master?
|
32
|
+
@live
|
33
|
+
end
|
34
|
+
|
29
35
|
def master?
|
30
|
-
return
|
36
|
+
return @master unless @master.nil?
|
37
|
+
return false if playlist_size.zero? && segment_size.zero?
|
31
38
|
playlist_size > 0
|
32
39
|
end
|
33
40
|
|
@@ -55,13 +62,16 @@ module M3u8
|
|
55
62
|
def assign_options(options)
|
56
63
|
options = defaults.merge(options)
|
57
64
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
+
@version = options[:version]
|
66
|
+
@sequence = options[:sequence]
|
67
|
+
@discontinuity_sequence = options[:discontinuity_sequence]
|
68
|
+
@cache = options[:cache]
|
69
|
+
@target = options[:target]
|
70
|
+
@type = options[:type]
|
71
|
+
@iframes_only = options[:iframes_only]
|
72
|
+
@independent_segments = options[:independent_segments]
|
73
|
+
@master = options[:master]
|
74
|
+
@live = options[:live]
|
65
75
|
end
|
66
76
|
|
67
77
|
def defaults
|
@@ -69,7 +79,8 @@ module M3u8
|
|
69
79
|
sequence: 0,
|
70
80
|
target: 10,
|
71
81
|
iframes_only: false,
|
72
|
-
independent_segments: false
|
82
|
+
independent_segments: false,
|
83
|
+
live: false
|
73
84
|
}
|
74
85
|
end
|
75
86
|
|
data/lib/m3u8/playlist_item.rb
CHANGED
@@ -7,7 +7,7 @@ module M3u8
|
|
7
7
|
attr_accessor :program_id, :width, :height, :codecs, :bandwidth,
|
8
8
|
:audio_codec, :level, :profile, :video, :audio, :uri,
|
9
9
|
:average_bandwidth, :subtitles, :closed_captions, :iframe,
|
10
|
-
:frame_rate, :name
|
10
|
+
:frame_rate, :name, :hdcp_level
|
11
11
|
MISSING_CODEC_MESSAGE = 'Audio or video codec info should be provided.'
|
12
12
|
|
13
13
|
def initialize(params = {})
|
@@ -37,11 +37,11 @@ module M3u8
|
|
37
37
|
def codecs
|
38
38
|
return @codecs unless @codecs.nil?
|
39
39
|
|
40
|
-
|
41
|
-
return
|
42
|
-
return
|
40
|
+
video_code = video_codec(profile, level)
|
41
|
+
return audio_codec_code if video_code.nil?
|
42
|
+
return video_code if audio_codec_code.nil?
|
43
43
|
|
44
|
-
"#{
|
44
|
+
"#{video_code},#{audio_codec_code}"
|
45
45
|
end
|
46
46
|
|
47
47
|
def to_s
|
@@ -65,7 +65,7 @@ module M3u8
|
|
65
65
|
video: attributes['VIDEO'], audio: attributes['AUDIO'],
|
66
66
|
uri: attributes['URI'], subtitles: attributes['SUBTITLES'],
|
67
67
|
closed_captions: attributes['CLOSED-CAPTIONS'],
|
68
|
-
name: attributes['NAME'] }
|
68
|
+
name: attributes['NAME'], hdcp_level: attributes['HDCP-LEVEL'] }
|
69
69
|
end
|
70
70
|
|
71
71
|
def parse_average_bandwidth(value)
|
@@ -105,6 +105,7 @@ module M3u8
|
|
105
105
|
bandwidth_format,
|
106
106
|
average_bandwidth_format,
|
107
107
|
frame_rate_format,
|
108
|
+
hdcp_level_format,
|
108
109
|
audio_format,
|
109
110
|
video_format,
|
110
111
|
subtitles_format,
|
@@ -127,6 +128,11 @@ module M3u8
|
|
127
128
|
"FRAME-RATE=#{format('%.3f', frame_rate)}"
|
128
129
|
end
|
129
130
|
|
131
|
+
def hdcp_level_format
|
132
|
+
return if hdcp_level.nil?
|
133
|
+
"HDCP-LEVEL=#{hdcp_level}"
|
134
|
+
end
|
135
|
+
|
130
136
|
def codecs_format
|
131
137
|
%(CODECS="#{codecs}")
|
132
138
|
end
|
@@ -170,7 +176,7 @@ module M3u8
|
|
170
176
|
%(NAME="#{name}")
|
171
177
|
end
|
172
178
|
|
173
|
-
def
|
179
|
+
def audio_codec_code
|
174
180
|
return if @audio_codec.nil?
|
175
181
|
return 'mp4a.40.2' if @audio_codec.casecmp('aac-lc').zero?
|
176
182
|
return 'mp4a.40.5' if @audio_codec.casecmp('he-aac').zero?
|