m3u8 0.7.1 → 0.8.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/.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
|
[](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?
|