m3u8 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8bead9657f94696922af4e09d7f19800b80572ff
4
- data.tar.gz: b654a6d47b3cfad36b08f58cb95ffd12e46c3b66
3
+ metadata.gz: 113427414ddf5ec31124dbcc67b905d8f4d29ab9
4
+ data.tar.gz: 0aa8693ced40c395ade39b026ffce25f99fe49d1
5
5
  SHA512:
6
- metadata.gz: fc8d26287b07199eaba044e66330f519a6c96b44b52e607ea48cafb286d4a005e22d1f43cdfb2d353c9ccc79e4ea92fd188ed7e4cdee570580110b7b23ff5f6f
7
- data.tar.gz: 9b82bbb066aa4ccbb6e3f9827ec7ca56fbb845f56ac57826fbe29d37bdd06631bf1800b43cd42e28b582630c5fd50a6c91818af1c87b223e76cac54ed71b0062
6
+ metadata.gz: 12f12fe680621b9bd4bdb8eca4f5042de4f063772fff5ac331db34f12458131e47f1f8420a40af7072f85ed835e623ed4a6324cfb1a912f2e6d251ea11a3a3dc
7
+ data.tar.gz: 038002c0468e4c332ad06d6b38ff748aa6bdead131e0b5eb356da563c85648a5609c87fe233a1343768a532aebe99e12a3b7853b4843d487997fe89193eb9640
@@ -1,5 +1,6 @@
1
1
  AllCops:
2
2
  Exclude:
3
3
  - 'spec/spec_helper.rb'
4
+ - 'm3u8.gemspec'
4
5
  Style/StringLiterals:
5
- EnforcedStyle: single_quotes
6
+ EnforcedStyle: single_quotes
@@ -0,0 +1 @@
1
+ 2.2.3
@@ -1,7 +1,8 @@
1
1
  language: ruby
2
+ sudo: false
2
3
  rvm:
4
+ - 2.2.3
5
+ - 2.1.7
3
6
  - 2.0.0
4
- - 1.9.3
5
- - jruby-19mode
6
7
 
7
- script: rspec spec
8
+ script: rspec spec
@@ -1,3 +1,5 @@
1
+ #### 0.6.2 (2/16/2016) - Added support for EXT-X-SESSION-KEY tags. Merged pulled request #6 from [jviney](https://github.com/jviney), fixing bug with codec string value.
2
+
1
3
  #### 0.6.1 (8/15/2015) - Added support for EXT-X-PROGRAM-DATE-TIME tags.
2
4
 
3
5
  #### 0.6.0 (6/20/2015) - Added support for EXT-X-MAP tags, introduced ByteRange class to handled shared byterange parsing/serialization functionality. Breaking changes: SegmentItem now exposes a single byterange instance accessor rather than byterange_length and byterange_start, made all parse methods static for consistency (ex: MediaItem.parse).
data/Gemfile CHANGED
@@ -4,4 +4,6 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem 'coveralls', require: false
7
+ gem 'guard'
8
+ gem 'guard-rspec', require: false
7
9
  gem 'simplecov', require: false
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { 'spec' }
5
+ end
data/README.md CHANGED
@@ -27,17 +27,19 @@ Or install it yourself as:
27
27
  ## Usage (creating playlists)
28
28
 
29
29
  Create a master playlist and add child playlists for adaptive bitrate streaming:
30
+
30
31
  ```ruby
31
32
  require 'm3u8'
32
33
  playlist = M3u8::Playlist.new
33
34
  ```
34
35
 
35
36
  Create a new playlist item with options:
37
+
36
38
  ```ruby
37
39
  options = { width: 1920, height: 1080, profile: 'high', level: 4.1,
38
40
  audio_codec: 'aac-lc', bandwidth: 540, uri: 'test.url' }
39
- item = M3u8::PlaylistItem.new options
40
- playlist.items.push item
41
+ item = M3u8::PlaylistItem.new(options)
42
+ playlist.items << item
41
43
  ```
42
44
 
43
45
  Add alternate audio, camera angles, closed captions and subtitles by creating MediaItem instances and adding them to the Playlist:
@@ -47,42 +49,51 @@ hash = { type: 'AUDIO', group_id: 'audio-lo', language: 'fre',
47
49
  assoc_language: 'spoken', name: 'Francais', autoselect: true,
48
50
  default: false, forced: true, uri: 'frelo/prog_index.m3u8' }
49
51
  item = M3u8::MediaItem.new(hash)
50
- playlist.items.push item
52
+ playlist.items << item
51
53
  ```
52
54
 
53
55
  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):
56
+
54
57
  ```ruby
55
58
  options = { version: 1, cache: false, target: 12, sequence: 1 }
56
- playlist = M3u8::Playlist.new options
59
+ playlist = M3u8::Playlist.new(options)
57
60
 
58
- item = M3u8::SegmentItem.new duration: 11, segment: 'test.ts'
59
- playlist.items.push item
61
+ item = M3u8::SegmentItem.new(duration: 11, segment: 'test.ts')
62
+ playlist.items << item
60
63
  ```
61
64
 
62
65
  You can pass an IO object to the write method:
66
+
63
67
  ```ruby
64
68
  require 'tempfile'
65
- f = Tempfile.new 'test'
66
- playlist.write f
69
+ file = Tempfile.new('test')
70
+ playlist.write(file)
67
71
  ```
72
+
68
73
  You can also access the playlist as a string:
74
+
69
75
  ```ruby
70
76
  playlist.to_s
71
77
  ```
78
+
72
79
  M3u8::Writer is the class that handles generating the playlist output.
73
80
 
74
81
  Alternatively you can set codecs rather than having it generated automatically:
82
+
75
83
  ```ruby
76
84
  options = { width: 1920, height: 1080, codecs: 'avc1.66.30,mp4a.40.2',
77
85
  bandwidth: 540, uri: 'test.url' }
78
- item = M3u8::PlaylistItem.new options
86
+ item = M3u8::PlaylistItem.new(options)
79
87
  ```
88
+
80
89
  Just get the codec string for custom use:
90
+
81
91
  ```ruby
82
92
  options = { profile: 'baseline', level: 3.0, audio_codec: 'aac-lc' }
83
- codecs = M3u8::Playlist.codecs options
93
+ codecs = M3u8::Playlist.codecs(options)
84
94
  # => "avc1.66.30,mp4a.40.2"
85
- ```
95
+ ```
96
+
86
97
  Values for audio_codec (codec name): aac-lc, he-aac, mp3
87
98
 
88
99
  Possible values for profile (H.264 Profile): baseline, main, high.
@@ -95,50 +106,81 @@ Not all Levels and Profiles can be combined, consult H.264 documentation
95
106
 
96
107
  ```ruby
97
108
  file = File.open 'spec/fixtures/master.m3u8'
98
- playlist = M3u8::Playlist.read file
109
+ playlist = M3u8::Playlist.read(file)
99
110
  playlist.master?
100
111
  # => true
101
112
  ```
102
- Acess items in playlist:
113
+
114
+ Access items in playlist:
115
+
103
116
  ```ruby
104
117
  playlist.items.first
105
118
  # => #<M3u8::PlaylistItem:0x007fa569bc7698 @program_id="1", @resolution="1920x1080",
106
119
  # @codecs="avc1.640028,mp4a.40.2", @bandwidth="5042000",
107
120
  # @playlist="hls/1080-7mbps/1080-7mbps.m3u8">
108
121
  ```
122
+
109
123
  Create a new playlist item with options:
124
+
110
125
  ```ruby
111
126
  options = { width: 1920, height: 1080, profile: 'high', level: 4.1,
112
127
  audio_codec: 'aac-lc', bandwidth: 540, uri: 'test.url' }
113
- item = M3u8::PlaylistItem.new options
128
+ item = M3u8::PlaylistItem.new(options)
114
129
  #add it to the top of the playlist
115
- playlist.items.insert 0, item
130
+ playlist.items.unshift(item)
116
131
  ```
132
+
117
133
  M3u8::Reader is the class handles parsing if you want more control over the process.
118
134
 
119
135
  ## Features
120
136
  * Distinction between segment and master playlists is handled automatically (no need to use a different class).
121
- * Automatically generates the audio/video codec string based on names and options you are familar with.
137
+ * Automatically generates the audio/video codec string based on names and options you are familiar with.
122
138
  * Provides validation of input when adding playlists or segments.
123
139
  * Allows all options to be configured on a playlist (caching, version, etc.)
124
- * Supports I-Frames (Intra frames) and Byte Ranges in Segments.
125
- * Supports subtitles, closed captions, alternate audio and video, and comments.
126
- # Supports Session Data in master playlists.
127
- * Supports keys for encrypted media segments (EXT-X-KEY).
128
- * Supports EXT-X-DISCONTINUITY in media segments.
129
140
  * Can write playlist to an IO object (StringIO/File, etc) or access string via to_s.
130
141
  * Can read playlists into a model (Playlist and Items) from an IO object.
131
142
  * Any tag or attribute supported by the object model is supported both parsing and generation of m3u8 playlists.
132
-
133
- ## Missing (but planned) Features
143
+ * Supports I-frames (Intra frames) and byte ranges in Segments.
144
+ * Supports subtitles, closed captions, alternate audio and video, and comments.
145
+ * Supports session data in master playlists.
146
+ * Supports keys for encrypted media segments (EXT-X-KEY, EXT-SESSION-KEY).
147
+
148
+ ## HLS Spec Status (version 17)
149
+ ### Implemented:
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
+
169
+ ### TODO:
170
+ * EXT-X-DISCONTINUITY-SEQUENCE
171
+ * EXT-X-INDEPENDENT-SEGMENTS
172
+ * EXT-X-START
173
+
174
+ ## Roadmap
175
+ * Add the last remaining tags for latest version of the spec.
134
176
  * Validation of all attributes and their values to match the rules defined in the spec.
135
- * Still missing support for a few tags and attributes.
177
+ * Support for different versions of spec, defaulting to latest.
136
178
 
137
179
  ## Contributing
138
180
 
139
181
  1. Fork it ( https://github.com/sethdeckard/m3u8/fork )
140
182
  2. Create your feature branch (`git checkout -b my-new-feature`)
141
- 3. Run the specs, make sure they pass and that new features are covered
183
+ 3. Run the specs, make sure they pass and that new features are covered. Code coverage should be 100%.
142
184
  4. Commit your changes (`git commit -am 'Add some feature'`)
143
185
  5. Push to the branch (`git push origin my-new-feature`)
144
186
  6. Create a new Pull Request
@@ -1,24 +1,11 @@
1
1
  require 'stringio'
2
- require 'm3u8/version'
3
- require 'm3u8/playlist'
4
- require 'm3u8/playlist_item'
5
- require 'm3u8/segment_item'
6
- require 'm3u8/discontinuity_item'
7
- require 'm3u8/key_item'
8
- require 'm3u8/map_item'
9
- require 'm3u8/media_item'
10
- require 'm3u8/time_item'
11
- require 'm3u8/session_data_item'
12
- require 'm3u8/byte_range'
13
- require 'm3u8/reader'
14
- require 'm3u8/writer'
15
- require 'm3u8/error'
2
+ Dir[File.dirname(__FILE__) + '/m3u8/*.rb'].each { |file| require file }
16
3
 
17
4
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
18
5
  module M3u8
19
6
  def parse_attributes(line)
20
- array = line.gsub("\n", '').scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
21
- Hash[array.map { |key, value| [key, value.gsub('"', '')] }]
7
+ array = line.delete("\n").scan(/([A-z-]+)\s*=\s*("[^"]*"|[^,]*)/)
8
+ Hash[array.map { |key, value| [key, value.delete('"')] }]
22
9
  end
23
10
 
24
11
  def parse_yes_no(value)
@@ -0,0 +1,50 @@
1
+ module M3u8
2
+ # Encapsulates logic common to encryption key tags
3
+ module Encryptable
4
+ def self.included(base)
5
+ base.send :attr_accessor, :method
6
+ base.send :attr_accessor, :uri
7
+ base.send :attr_accessor, :iv
8
+ base.send :attr_accessor, :key_format
9
+ base.send :attr_accessor, :key_format_versions
10
+ end
11
+
12
+ def attributes_to_s
13
+ [method_format,
14
+ uri_format,
15
+ iv_format,
16
+ key_format_format,
17
+ key_format_versions_format].compact.join(',')
18
+ end
19
+
20
+ def convert_key_names(attributes)
21
+ { method: attributes['METHOD'], uri: attributes['URI'],
22
+ iv: attributes['IV'], key_format: attributes['KEYFORMAT'],
23
+ key_format_versions: attributes['KEYFORMATVERSIONS'] }
24
+ end
25
+
26
+ private
27
+
28
+ def method_format
29
+ "METHOD=#{method}"
30
+ end
31
+
32
+ def uri_format
33
+ %(URI="#{uri}") unless uri.nil?
34
+ end
35
+
36
+ def iv_format
37
+ "IV=#{iv}" unless iv.nil?
38
+ end
39
+
40
+ def key_format_format
41
+ %(KEYFORMAT="#{key_format}") unless key_format.nil?
42
+ end
43
+
44
+ def key_format_versions_format
45
+ return if key_format_versions.nil?
46
+
47
+ %(KEYFORMATVERSIONS="#{key_format_versions}")
48
+ end
49
+ end
50
+ end
@@ -1,54 +1,23 @@
1
1
  module M3u8
2
2
  # KeyItem represents a set of EXT-X-KEY attributes
3
3
  class KeyItem
4
+ include Encryptable
4
5
  extend M3u8
5
- attr_accessor :method, :uri, :iv, :key_format, :key_format_versions
6
6
 
7
7
  def initialize(params = {})
8
- params.each do |key, value|
8
+ options = convert_key_names(params)
9
+ options.merge(params).each do |key, value|
9
10
  instance_variable_set("@#{key}", value)
10
11
  end
11
12
  end
12
13
 
13
14
  def self.parse(text)
14
- attributes = parse_attributes text
15
- options = { method: attributes['METHOD'], uri: attributes['URI'],
16
- iv: attributes['IV'], key_format: attributes['KEYFORMAT'],
17
- key_format_versions: attributes['KEYFORMATVERSIONS'] }
18
- KeyItem.new options
15
+ attributes = parse_attributes(text)
16
+ KeyItem.new(attributes)
19
17
  end
20
18
 
21
19
  def to_s
22
- attributes = [method_format,
23
- uri_format,
24
- iv_format,
25
- key_format_format,
26
- key_format_versions_format].compact.join(',')
27
- "#EXT-X-KEY:#{attributes}"
28
- end
29
-
30
- private
31
-
32
- def method_format
33
- "METHOD=#{method}"
34
- end
35
-
36
- def uri_format
37
- %(URI="#{uri}") unless uri.nil?
38
- end
39
-
40
- def iv_format
41
- "IV=#{iv}" unless iv.nil?
42
- end
43
-
44
- def key_format_format
45
- %(KEYFORMAT="#{key_format}") unless key_format.nil?
46
- end
47
-
48
- def key_format_versions_format
49
- return if key_format_versions.nil?
50
-
51
- %(KEYFORMATVERSIONS="#{key_format_versions}")
20
+ "#EXT-X-KEY:#{attributes_to_s}"
52
21
  end
53
22
  end
54
23
  end
@@ -148,15 +148,27 @@ module M3u8
148
148
  def video_codec(profile, level)
149
149
  return if profile.nil? || level.nil?
150
150
 
151
- profile = profile.downcase
152
- return 'avc1.66.30' if profile == 'baseline' && level == 3.0
153
- return 'avc1.42001f' if profile == 'baseline' && level == 3.1
154
- return 'avc1.77.30' if profile == 'main' && level == 3.0
155
- return 'avc1.4d001f' if profile == 'main' && level == 3.1
156
- return 'avc1.4d0028' if profile == 'main' && level == 4.0
157
- return 'avc1.64001f' if profile == 'high' && level == 3.1
158
- return 'avc1.640028' if profile == 'high' &&
159
- (level == 4.0 || level == 4.1)
151
+ profile.downcase!
152
+ return baseline_codec_string(level) if profile == 'baseline'
153
+ return main_codec_string(level) if profile == 'main'
154
+ return high_codec_string(level) if profile == 'high'
155
+ end
156
+
157
+ def baseline_codec_string(level)
158
+ return 'avc1.66.30' if level == 3.0
159
+ return 'avc1.42001f' if level == 3.1
160
+ end
161
+
162
+ def main_codec_string(level)
163
+ return 'avc1.77.30' if level == 3.0
164
+ return 'avc1.4d001f' if level == 3.1
165
+ return 'avc1.4d0028' if level == 4.0
166
+ end
167
+
168
+ def high_codec_string(level)
169
+ return 'avc1.64001f' if level == 3.1
170
+ return 'avc1.640028' if level == 4.0
171
+ return 'avc1.640029' if level == 4.1
160
172
  end
161
173
  end
162
174
  end
@@ -22,45 +22,46 @@ module M3u8
22
22
  private
23
23
 
24
24
  def basic_tags
25
- { '#EXT-X-VERSION' => ->(line) { parse_version line } }
25
+ { '#EXT-X-VERSION' => ->(line) { parse_version(line) } }
26
26
  end
27
27
 
28
28
  def media_segment_tags
29
- { '#EXTINF' => ->(line) { parse_segment line },
30
- '#EXT-X-DISCONTINUITY' => ->(line) { parse_discontinuity line },
31
- '#EXT-X-BYTERANGE' => ->(line) { parse_byterange line },
32
- '#EXT-X-KEY' => ->(line) { parse_key line },
33
- '#EXT-X-MAP' => ->(line) { parse_map line },
34
- '#EXT-X-PROGRAM-DATE-TIME' => ->(line) { parse_time line }
29
+ { '#EXTINF' => ->(line) { parse_segment(line) },
30
+ '#EXT-X-DISCONTINUITY' => ->(line) { parse_discontinuity(line) },
31
+ '#EXT-X-BYTERANGE' => ->(line) { parse_byterange(line) },
32
+ '#EXT-X-KEY' => ->(line) { parse_key(line) },
33
+ '#EXT-X-MAP' => ->(line) { parse_map(line) },
34
+ '#EXT-X-PROGRAM-DATE-TIME' => ->(line) { parse_time(line) }
35
35
  }
36
36
  end
37
37
 
38
38
  def media_playlist_tags
39
- { '#EXT-X-MEDIA-SEQUENCE' => ->(line) { parse_sequence line },
40
- '#EXT-X-ALLOW-CACHE' => ->(line) { parse_cache line },
41
- '#EXT-X-TARGETDURATION' => ->(line) { parse_target line },
39
+ { '#EXT-X-MEDIA-SEQUENCE' => ->(line) { parse_sequence(line) },
40
+ '#EXT-X-ALLOW-CACHE' => ->(line) { parse_cache(line) },
41
+ '#EXT-X-TARGETDURATION' => ->(line) { parse_target(line) },
42
42
  '#EXT-X-I-FRAMES-ONLY' => proc { playlist.iframes_only = true },
43
- '#EXT-X-PLAYLIST-TYPE' => ->(line) { parse_playlist_type line }
43
+ '#EXT-X-PLAYLIST-TYPE' => ->(line) { parse_playlist_type(line) }
44
44
  }
45
45
  end
46
46
 
47
47
  def master_playlist_tags
48
- { '#EXT-X-MEDIA' => ->(line) { parse_media line },
49
- '#EXT-X-SESSION-DATA' => ->(line) { parse_session_data line },
50
- '#EXT-X-STREAM-INF' => ->(line) { parse_stream line },
51
- '#EXT-X-I-FRAME-STREAM-INF' => ->(line) { parse_iframe_stream line }
48
+ { '#EXT-X-MEDIA' => ->(line) { parse_media(line) },
49
+ '#EXT-X-SESSION-DATA' => ->(line) { parse_session_data(line) },
50
+ '#EXT-X-SESSION-KEY' => ->(line) { parse_session_key(line) },
51
+ '#EXT-X-STREAM-INF' => ->(line) { parse_stream(line) },
52
+ '#EXT-X-I-FRAME-STREAM-INF' => ->(line) { parse_iframe_stream(line) }
52
53
  }
53
54
  end
54
55
 
55
56
  def parse_line(line)
56
57
  return if match_tag(line)
57
- parse_next_line line if !item.nil? && open
58
+ parse_next_line(line) if !item.nil? && open
58
59
  end
59
60
 
60
61
  def match_tag(line)
61
62
  tag = @tags.select { |key| line.start_with? key }
62
63
  return unless tag.values.first
63
- tag.values.first.call line
64
+ tag.values.first.call(line)
64
65
  true
65
66
  end
66
67
 
@@ -89,16 +90,16 @@ module M3u8
89
90
  self.master = true
90
91
  self.open = true
91
92
 
92
- self.item = M3u8::PlaylistItem.parse line
93
+ self.item = M3u8::PlaylistItem.parse(line)
93
94
  end
94
95
 
95
96
  def parse_iframe_stream(line)
96
97
  self.master = true
97
98
  self.open = false
98
99
 
99
- self.item = M3u8::PlaylistItem.parse line
100
+ self.item = M3u8::PlaylistItem.parse(line)
100
101
  item.iframe = true
101
- playlist.items.push item
102
+ playlist.items << item
102
103
  end
103
104
 
104
105
  def parse_discontinuity(*)
@@ -106,22 +107,22 @@ module M3u8
106
107
  self.open = false
107
108
 
108
109
  self.item = M3u8::DiscontinuityItem.new
109
- playlist.items.push item
110
+ playlist.items << item
110
111
  end
111
112
 
112
113
  def parse_key(line)
113
- item = M3u8::KeyItem.parse line
114
- playlist.items.push item
114
+ item = M3u8::KeyItem.parse(line)
115
+ playlist.items << item
115
116
  end
116
117
 
117
118
  def parse_map(line)
118
- item = M3u8::MapItem.parse line
119
- playlist.items.push item
119
+ item = M3u8::MapItem.parse(line)
120
+ playlist.items << item
120
121
  end
121
122
 
122
123
  def parse_segment(line)
123
124
  self.item = M3u8::SegmentItem.new
124
- values = line.gsub('#EXTINF:', '').gsub("\n", ',').split(',')
125
+ values = line.gsub('#EXTINF:', '').tr("\n", ',').split(',')
125
126
  item.duration = values[0].to_f
126
127
  item.comment = values[1] unless values[1].nil?
127
128
 
@@ -130,34 +131,39 @@ module M3u8
130
131
  end
131
132
 
132
133
  def parse_byterange(line)
133
- values = line.gsub('#EXT-X-BYTERANGE:', '').gsub("\n", ',')
134
+ values = line.gsub('#EXT-X-BYTERANGE:', '').delete("\n")
134
135
  item.byterange = M3u8::ByteRange.parse values
135
136
  end
136
137
 
137
138
  def parse_session_data(line)
138
- item = M3u8::SessionDataItem.parse line
139
- playlist.items.push item
139
+ item = M3u8::SessionDataItem.parse(line)
140
+ playlist.items << item
141
+ end
142
+
143
+ def parse_session_key(line)
144
+ item = M3u8::SessionKeyItem.parse(line)
145
+ playlist.items << item
140
146
  end
141
147
 
142
148
  def parse_media(line)
143
149
  self.open = false
144
- self.item = M3u8::MediaItem.parse line
145
- playlist.items.push item
150
+ self.item = M3u8::MediaItem.parse(line)
151
+ playlist.items << item
146
152
  end
147
153
 
148
154
  def parse_time(line)
149
155
  self.open = false
150
- playlist.items.push M3u8::TimeItem.parse line
156
+ playlist.items << M3u8::TimeItem.parse(line)
151
157
  end
152
158
 
153
159
  def parse_next_line(line)
154
- value = line.gsub "\n", ''
160
+ value = line.delete("\n")
155
161
  if master
156
162
  item.uri = value
157
163
  else
158
164
  item.segment = value
159
165
  end
160
- playlist.items.push item
166
+ playlist.items << item
161
167
  self.open = false
162
168
  end
163
169
  end
@@ -0,0 +1,23 @@
1
+ module M3u8
2
+ # KeyItem represents a set of EXT-X-SESSION-KEY attributes
3
+ class SessionKeyItem
4
+ include Encryptable
5
+ extend M3u8
6
+
7
+ def initialize(params = {})
8
+ options = convert_key_names(params)
9
+ options.merge(params).each do |key, value|
10
+ instance_variable_set("@#{key}", value)
11
+ end
12
+ end
13
+
14
+ def self.parse(text)
15
+ attributes = parse_attributes(text)
16
+ SessionKeyItem.new(attributes)
17
+ end
18
+
19
+ def to_s
20
+ "#EXT-X-SESSION-KEY:#{attributes_to_s}"
21
+ end
22
+ end
23
+ end
@@ -1,4 +1,4 @@
1
1
  # M3u8 provides parsing, generation, and validation of m3u8 playlists
2
2
  module M3u8
3
- VERSION = '0.6.1'
3
+ VERSION = '0.6.2'
4
4
  end
@@ -18,7 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency 'bundler', '>= 1.6'
22
- spec.add_development_dependency 'rake', '~> 10.0'
23
- spec.add_development_dependency 'rspec', '>= 2.11'
21
+ spec.add_development_dependency 'bundler'
22
+ spec.add_development_dependency 'rake'
23
+ spec.add_development_dependency 'rspec', '~> 3.3'
24
24
  end
@@ -1,4 +1,5 @@
1
1
  #EXTM3U
2
+ #EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"
2
3
  #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=5042000
3
4
  hls/1080-7mbps/1080-7mbps.m3u8
4
5
  #EXT-X-STREAM-INF:PROGRAM-ID=1,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=4853000
@@ -162,7 +162,7 @@ describe M3u8::PlaylistItem do
162
162
 
163
163
  options = { profile: 'high', level: 4.1 }
164
164
  item = M3u8::PlaylistItem.new options
165
- expect(item.codecs).to eq 'avc1.640028'
165
+ expect(item.codecs).to eq 'avc1.640029'
166
166
  end
167
167
 
168
168
  it 'should raise error if codecs are missing' do
@@ -39,7 +39,7 @@ describe M3u8::Playlist do
39
39
 
40
40
  output = "#EXTM3U\n" \
41
41
  '#EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,' +
42
- %(CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=50000\n) +
42
+ %(CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000\n) +
43
43
  "playlist_url\n"
44
44
 
45
45
  expect(playlist.to_s).to eq output
@@ -58,7 +58,7 @@ describe M3u8::Playlist do
58
58
  output = "#EXTM3U\n" +
59
59
  %(#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34") +
60
60
  ",BANDWIDTH=6400\nplaylist_url\n#EXT-X-STREAM-INF:PROGRAM-ID=2," +
61
- %(RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2") +
61
+ %(RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2") +
62
62
  ",BANDWIDTH=50000\nplaylist_url\n"
63
63
  expect(playlist.to_s).to eq output
64
64
  end
@@ -208,7 +208,7 @@ describe M3u8::Playlist do
208
208
  file = File.open 'spec/fixtures/master.m3u8'
209
209
  playlist = M3u8::Playlist.read file
210
210
  expect(playlist.master?).to be true
211
- expect(playlist.items.size).to eq 6
211
+ expect(playlist.items.size).to eq(7)
212
212
  end
213
213
 
214
214
  it 'should return the total duration of a playlist' do
@@ -222,6 +222,6 @@ describe M3u8::Playlist do
222
222
  item = M3u8::SegmentItem.new(duration: 8.790, segment: 'test_04.ts')
223
223
  playlist.items.push item
224
224
 
225
- expect(playlist.duration.round(3)).to eq 40.228
225
+ expect(playlist.duration.round(3)).to eq(40.228)
226
226
  end
227
227
  end
@@ -8,6 +8,11 @@ describe M3u8::Reader do
8
8
  expect(playlist.master?).to be true
9
9
 
10
10
  item = playlist.items[0]
11
+ expect(item).to be_a(M3u8::SessionKeyItem)
12
+ expect(item.method).to eq('AES-128')
13
+ expect(item.uri).to eq('https://priv.example.com/key.php?r=52')
14
+
15
+ item = playlist.items[1]
11
16
  expect(item).to be_a(M3u8::PlaylistItem)
12
17
  expect(item.uri).to eq('hls/1080-7mbps/1080-7mbps.m3u8')
13
18
  expect(item.program_id).to eq('1')
@@ -19,7 +24,7 @@ describe M3u8::Reader do
19
24
  expect(item.iframe).to be false
20
25
  expect(item.average_bandwidth).to be_nil
21
26
 
22
- item = playlist.items[5]
27
+ item = playlist.items[6]
23
28
  expect(item).to be_a(M3u8::PlaylistItem)
24
29
  expect(item.uri).to eq('hls/64k/64k.m3u8')
25
30
  expect(item.program_id).to eq('1')
@@ -31,7 +36,7 @@ describe M3u8::Reader do
31
36
  expect(item.iframe).to be false
32
37
  expect(item.average_bandwidth).to be_nil
33
38
 
34
- expect(playlist.items.size).to eq 6
39
+ expect(playlist.items.size).to eq(7)
35
40
 
36
41
  item = playlist.items.last
37
42
  expect(item.resolution).to be_nil
@@ -43,7 +48,7 @@ describe M3u8::Reader do
43
48
  playlist = reader.read file
44
49
  expect(playlist.master?).to be true
45
50
 
46
- expect(playlist.items.size).to eq 7
51
+ expect(playlist.items.size).to eq(7)
47
52
 
48
53
  item = playlist.items[1]
49
54
  expect(item).to be_a(M3u8::PlaylistItem)
@@ -169,12 +174,12 @@ describe M3u8::Reader do
169
174
  reader = M3u8::Reader.new
170
175
  playlist = reader.read file
171
176
 
172
- expect(playlist.items.size).to eq 6
177
+ expect(playlist.items.size).to eq(7)
173
178
 
174
179
  file = File.open 'spec/fixtures/master.m3u8'
175
180
  playlist = reader.read file
176
181
 
177
- expect(playlist.items.size).to eq 6
182
+ expect(playlist.items.size).to eq(7)
178
183
  end
179
184
 
180
185
  it 'should parse playlist with session data' do
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe M3u8::SessionKeyItem do
4
+ it 'should initialize with hash' do
5
+ hash = { method: 'AES-128', uri: 'http://test.key',
6
+ iv: 'D512BBF', key_format: 'identity',
7
+ key_format_versions: '1/3' }
8
+ item = M3u8::SessionKeyItem.new(hash)
9
+ expect(item.method).to eq 'AES-128'
10
+ expect(item.uri).to eq 'http://test.key'
11
+ expect(item.iv).to eq 'D512BBF'
12
+ expect(item.key_format).to eq 'identity'
13
+ expect(item.key_format_versions).to eq '1/3'
14
+ end
15
+
16
+ it 'should provide m3u8 format representation' do
17
+ hash = { method: 'AES-128', uri: 'http://test.key',
18
+ iv: 'D512BBF', key_format: 'identity',
19
+ key_format_versions: '1/3' }
20
+ item = M3u8::SessionKeyItem.new(hash)
21
+ expected = %(#EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://test.key",) +
22
+ %(IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3")
23
+ expect(item.to_s).to eq expected
24
+
25
+ hash = { method: 'AES-128', uri: 'http://test.key' }
26
+ item = M3u8::SessionKeyItem.new(hash)
27
+ expected = %(#EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://test.key")
28
+ expect(item.to_s).to eq expected
29
+
30
+ hash = { method: 'NONE' }
31
+ item = M3u8::SessionKeyItem.new(hash)
32
+ expected = '#EXT-X-SESSION-KEY:METHOD=NONE'
33
+ expect(item.to_s).to eq expected
34
+ end
35
+
36
+ it 'should parse m3u8 format into instance' do
37
+ format = %(#EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://test.key",) +
38
+ %(IV=D512BBF,KEYFORMAT="identity",KEYFORMATVERSIONS="1/3")
39
+ item = M3u8::KeyItem.parse format
40
+ expect(item.method).to eq 'AES-128'
41
+ expect(item.uri).to eq 'http://test.key'
42
+ expect(item.iv).to eq 'D512BBF'
43
+ expect(item.key_format).to eq 'identity'
44
+ expect(item.key_format_versions).to eq '1/3'
45
+ end
46
+ end
@@ -41,7 +41,7 @@ describe M3u8::Writer do
41
41
 
42
42
  output = "#EXTM3U\n" \
43
43
  '#EXT-X-STREAM-INF:PROGRAM-ID=2,RESOLUTION=1920x1080,' +
44
- %(CODECS="avc1.640028,mp4a.40.2",BANDWIDTH=50000\n) +
44
+ %(CODECS="avc1.640029,mp4a.40.2",BANDWIDTH=50000\n) +
45
45
  "playlist_url\n"
46
46
 
47
47
  io = StringIO.open
@@ -67,7 +67,7 @@ describe M3u8::Writer do
67
67
  output = "#EXTM3U\n" +
68
68
  %(#EXT-X-STREAM-INF:PROGRAM-ID=1,CODECS="mp4a.40.34") +
69
69
  ",BANDWIDTH=6400\nplaylist_url\n#EXT-X-STREAM-INF:PROGRAM-ID=2," +
70
- %(RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2") +
70
+ %(RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2") +
71
71
  ",BANDWIDTH=50000\nplaylist_url\n" +
72
72
  %(#EXT-X-SESSION-DATA:DATA-ID="com.test.movie.title",) +
73
73
  %(VALUE="Test",URI="http://test",LANGUAGE="en"\n)
File without changes
@@ -2,10 +2,10 @@ require 'm3u8'
2
2
  require 'simplecov'
3
3
  require 'coveralls'
4
4
 
5
- SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
5
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
6
6
  SimpleCov::Formatter::HTMLFormatter,
7
7
  Coveralls::SimpleCov::Formatter
8
- ]
8
+ ])
9
9
  SimpleCov.start
10
10
 
11
11
  # This file was generated by the `rspec --init` command. Conventionally, all
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.6.1
4
+ version: 0.6.2
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-08-15 00:00:00.000000000 Z
11
+ date: 2016-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '1.6'
19
+ version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '1.6'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ">="
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '2.11'
47
+ version: '3.3'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ">="
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '2.11'
54
+ version: '3.3'
55
55
  description: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
56
56
  email:
57
57
  - seth@deckard.me
@@ -63,15 +63,18 @@ files:
63
63
  - ".hound.yml"
64
64
  - ".rspec"
65
65
  - ".rubocop.yml"
66
+ - ".ruby-version"
66
67
  - ".travis.yml"
67
68
  - CHANGELOG.md
68
69
  - Gemfile
70
+ - Guardfile
69
71
  - LICENSE.txt
70
72
  - README.md
71
73
  - Rakefile
72
74
  - lib/m3u8.rb
73
75
  - lib/m3u8/byte_range.rb
74
76
  - lib/m3u8/discontinuity_item.rb
77
+ - lib/m3u8/encryptable.rb
75
78
  - lib/m3u8/error.rb
76
79
  - lib/m3u8/key_item.rb
77
80
  - lib/m3u8/map_item.rb
@@ -81,12 +84,11 @@ files:
81
84
  - lib/m3u8/reader.rb
82
85
  - lib/m3u8/segment_item.rb
83
86
  - lib/m3u8/session_data_item.rb
87
+ - lib/m3u8/session_key_item.rb
84
88
  - lib/m3u8/time_item.rb
85
89
  - lib/m3u8/version.rb
86
90
  - lib/m3u8/writer.rb
87
91
  - m3u8.gemspec
88
- - spec/byte_range_spec.rb
89
- - spec/discontinuity_item_spec.rb
90
92
  - spec/fixtures/encrypted.m3u8
91
93
  - spec/fixtures/iframes.m3u8
92
94
  - spec/fixtures/map_playlist.m3u8
@@ -97,18 +99,21 @@ files:
97
99
  - spec/fixtures/session_data.m3u8
98
100
  - spec/fixtures/variant_angles.m3u8
99
101
  - spec/fixtures/variant_audio.m3u8
100
- - spec/key_item_spec.rb
101
- - spec/m3u8_spec.rb
102
- - spec/map_item_spec.rb
103
- - spec/media_item_spec.rb
104
- - spec/playlist_item_spec.rb
105
- - spec/playlist_spec.rb
106
- - spec/reader_spec.rb
107
- - spec/segment_item_spec.rb
108
- - spec/session_data_item_spec.rb
102
+ - spec/lib/m3u8/byte_range_spec.rb
103
+ - spec/lib/m3u8/discontinuity_item_spec.rb
104
+ - spec/lib/m3u8/key_item_spec.rb
105
+ - spec/lib/m3u8/map_item_spec.rb
106
+ - spec/lib/m3u8/media_item_spec.rb
107
+ - spec/lib/m3u8/playlist_item_spec.rb
108
+ - spec/lib/m3u8/playlist_spec.rb
109
+ - spec/lib/m3u8/reader_spec.rb
110
+ - spec/lib/m3u8/segment_item_spec.rb
111
+ - spec/lib/m3u8/session_data_item_spec.rb
112
+ - spec/lib/m3u8/session_key_item_spec.rb
113
+ - spec/lib/m3u8/time_item_spec.rb
114
+ - spec/lib/m3u8/writer_spec.rb
115
+ - spec/lib/m3u8_spec.rb
109
116
  - spec/spec_helper.rb
110
- - spec/time_item_spec.rb
111
- - spec/writer_spec.rb
112
117
  homepage: https://github.com/sethdeckard/m3u8
113
118
  licenses:
114
119
  - MIT
@@ -129,13 +134,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
129
134
  version: '0'
130
135
  requirements: []
131
136
  rubyforge_project:
132
- rubygems_version: 2.4.5
137
+ rubygems_version: 2.4.5.1
133
138
  signing_key:
134
139
  specification_version: 4
135
140
  summary: Generate and parse m3u8 playlists for HTTP Live Streaming (HLS).
136
141
  test_files:
137
- - spec/byte_range_spec.rb
138
- - spec/discontinuity_item_spec.rb
139
142
  - spec/fixtures/encrypted.m3u8
140
143
  - spec/fixtures/iframes.m3u8
141
144
  - spec/fixtures/map_playlist.m3u8
@@ -146,15 +149,18 @@ test_files:
146
149
  - spec/fixtures/session_data.m3u8
147
150
  - spec/fixtures/variant_angles.m3u8
148
151
  - spec/fixtures/variant_audio.m3u8
149
- - spec/key_item_spec.rb
150
- - spec/m3u8_spec.rb
151
- - spec/map_item_spec.rb
152
- - spec/media_item_spec.rb
153
- - spec/playlist_item_spec.rb
154
- - spec/playlist_spec.rb
155
- - spec/reader_spec.rb
156
- - spec/segment_item_spec.rb
157
- - spec/session_data_item_spec.rb
152
+ - spec/lib/m3u8/byte_range_spec.rb
153
+ - spec/lib/m3u8/discontinuity_item_spec.rb
154
+ - spec/lib/m3u8/key_item_spec.rb
155
+ - spec/lib/m3u8/map_item_spec.rb
156
+ - spec/lib/m3u8/media_item_spec.rb
157
+ - spec/lib/m3u8/playlist_item_spec.rb
158
+ - spec/lib/m3u8/playlist_spec.rb
159
+ - spec/lib/m3u8/reader_spec.rb
160
+ - spec/lib/m3u8/segment_item_spec.rb
161
+ - spec/lib/m3u8/session_data_item_spec.rb
162
+ - spec/lib/m3u8/session_key_item_spec.rb
163
+ - spec/lib/m3u8/time_item_spec.rb
164
+ - spec/lib/m3u8/writer_spec.rb
165
+ - spec/lib/m3u8_spec.rb
158
166
  - spec/spec_helper.rb
159
- - spec/time_item_spec.rb
160
- - spec/writer_spec.rb