m3uzi 0.2.1 → 0.4.2

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.
data/lib/m3uzi.rb CHANGED
@@ -1,24 +1,20 @@
1
1
  $:<< File.dirname(__FILE__)
2
+ require 'm3uzi/item'
2
3
  require 'm3uzi/tag'
3
4
  require 'm3uzi/file'
4
5
  require 'm3uzi/stream'
6
+ require 'm3uzi/comment'
5
7
  require 'm3uzi/version'
6
8
 
7
9
  class M3Uzi
8
10
 
9
- # Unsupported: PROGRAM-DATE-TIME DISCONTINUITY
10
- VALID_TAGS = %w{TARGETDURATION MEDIA-SEQUENCE ALLOW-CACHE ENDLIST KEY}
11
-
12
- attr_accessor :files, :streams
13
- attr_accessor :tags, :comments
11
+ attr_accessor :header_tags, :playlist_items
14
12
  attr_accessor :final_media_file
15
13
  attr_accessor :version
16
14
 
17
15
  def initialize
18
- @files = []
19
- @streams = []
20
- @tags = []
21
- @comments = []
16
+ @header_tags = {}
17
+ @playlist_items = []
22
18
  @final_media_file = true
23
19
  @version = 1
24
20
  end
@@ -28,78 +24,161 @@ class M3Uzi
28
24
  # Read/Write M3U8 Files
29
25
  #-------------------------------------
30
26
 
31
- def self.read(path)
32
- m3u = self.new
33
- lines = ::File.readlines(path)
34
- lines.each_with_index do |line, i|
35
- case type(line)
36
- when :tag
37
- name, value = parse_general_tag(line)
38
- m3u.add_tag do |tag|
39
- tag.name = name
40
- tag.value = value
41
- end
42
- when :info
43
- duration, description = parse_file_tag(line)
44
- m3u.add_file do |file|
45
- file.path = lines[i+1].strip
46
- file.duration = duration
47
- file.description = description
48
- end
49
- m3u.final_media_file = false
50
- when :stream
51
- attributes = parse_stream_tag(line)
52
- m3u.add_stream do |stream|
53
- stream.path = lines[i+1].strip
54
- attributes.each_pair do |k,v|
55
- k = k.to_s.downcase.sub('-','_')
56
- next unless [:bandwidth, :program_id, :codecs, :resolution].include?(k)
57
- v = $1 if v.to_s =~ /^"(.*)"$/
58
- stream.send("#{k}=", v)
59
- end
60
- end
61
- when :final
62
- m3u.final_media_file = true
63
- else
64
- next
65
- end
66
- end
67
- m3u
68
- end
27
+ ##
28
+ ## For now, reading m3u8 files is not keeping up to date with writing, so we're
29
+ ## disabling it in this version. (Possibly to be re-introduced in the future.)
30
+ ##
31
+ # def self.read(path)
32
+ # m3u = self.new
33
+ # lines = ::File.readlines(path)
34
+ # lines.each_with_index do |line, i|
35
+ # case type(line)
36
+ # when :tag
37
+ # name, value = parse_general_tag(line)
38
+ # m3u.add_tag do |tag|
39
+ # tag.name = name
40
+ # tag.value = value
41
+ # end
42
+ # when :info
43
+ # duration, description = parse_file_tag(line)
44
+ # m3u.add_file do |file|
45
+ # file.path = lines[i+1].strip
46
+ # file.duration = duration
47
+ # file.description = description
48
+ # end
49
+ # m3u.final_media_file = false
50
+ # when :stream
51
+ # attributes = parse_stream_tag(line)
52
+ # m3u.add_stream do |stream|
53
+ # stream.path = lines[i+1].strip
54
+ # attributes.each_pair do |k,v|
55
+ # k = k.to_s.downcase.sub('-','_')
56
+ # next unless [:bandwidth, :program_id, :codecs, :resolution].include?(k)
57
+ # v = $1 if v.to_s =~ /^"(.*)"$/
58
+ # stream.send("#{k}=", v)
59
+ # end
60
+ # end
61
+ # when :final
62
+ # m3u.final_media_file = true
63
+ # else
64
+ # next
65
+ # end
66
+ # end
67
+ # m3u
68
+ # end
69
69
 
70
70
  def write_to_io(io_stream)
71
+ reset_encryption_key_history
72
+ reset_byterange_history
73
+
71
74
  check_version_restrictions
72
75
  io_stream << "#EXTM3U\n"
73
76
  io_stream << "#EXT-X-VERSION:#{@version.to_i}\n" if @version > 1
74
- comments.each do |comment|
75
- io_stream << "##{comment}\n"
77
+
78
+ if items(File).length > 0
79
+ max_duration = valid_items(File).map { |f| f.duration.to_f }.max || 10.0
80
+ io_stream << "#EXT-X-TARGETDURATION:#{max_duration.ceil}\n"
76
81
  end
77
- tags.each do |tag|
78
- next if %w{M3U ENDLIST}.include?(tag.name.to_s.upcase)
79
- if VALID_TAGS.include?(tag.name.to_s.upcase)
80
- io_stream << "#EXT-X-#{tag.name.to_s.upcase}"
81
- else
82
- io_stream << "##{tag.name.to_s.upcase}"
82
+
83
+ @header_tags.each do |item|
84
+ io_stream << (item.format + "\n") if item.valid?
85
+ end
86
+
87
+ @playlist_items.each do |item|
88
+ next unless item.valid?
89
+
90
+ if item.kind_of?(File)
91
+ encryption_key_line = generate_encryption_key_line(item)
92
+ io_stream << (encryption_key_line + "\n") if encryption_key_line
93
+
94
+ byterange_line = generate_byterange_line(item)
95
+ io_stream << (byterange_line + "\n") if byterange_line
83
96
  end
84
- tag.value && io_stream << ":#{tag.value}"
85
- io_stream << "\n"
97
+
98
+ io_stream << (item.format + "\n")
86
99
  end
87
- files.each do |file|
88
- io_stream << "#EXTINF:#{file.attribute_string}"
89
- io_stream << "\n#{file.path}\n"
100
+
101
+ io_stream << "#EXT-X-ENDLIST\n" if items(File).length > 0 && @final_media_file
102
+ end
103
+
104
+ def write(path)
105
+ ::File.open(path, "w") { |f| write_to_io(f) }
106
+ end
107
+
108
+ def items(kind)
109
+ @playlist_items.select { |item| item.kind_of?(kind) }
110
+ end
111
+
112
+ def valid_items(kind)
113
+ @playlist_items.select { |item| item.kind_of?(kind) && item.valid? }
114
+ end
115
+
116
+ #-------------------------------------
117
+ # Playlist generation helpers.
118
+ #-------------------------------------
119
+
120
+ def reset_encryption_key_history
121
+ @encryption_key_url = nil
122
+ @encryption_iv = nil
123
+ @encryption_sequence = 0
124
+ end
125
+
126
+ def generate_encryption_key_line(file)
127
+ generate_line = false
128
+
129
+ default_iv = @encryption_iv || format_iv(@encryption_sequence)
130
+
131
+ if (file.encryption_key_url != :unset) && (file.encryption_key_url != @encryption_key_url)
132
+ @encryption_key_url = file.encryption_key_url
133
+ generate_line = true
90
134
  end
91
- streams.each do |stream|
92
- io_stream << "#EXT-X-STREAM-INF:#{stream.attribute_string}"
93
- io_stream << "\n#{stream.path}\n"
135
+
136
+ if @encryption_key_url && file.encryption_iv != @encryption_iv
137
+ @encryption_iv = file.encryption_iv
138
+ generate_line = true
139
+ end
140
+
141
+ @encryption_sequence += 1
142
+
143
+ if generate_line
144
+ if @encryption_key_url.nil?
145
+ "#EXT-X-KEY:METHOD=NONE"
146
+ else
147
+ attrs = ['METHOD=AES-128']
148
+ attrs << 'URI="' + @encryption_key_url.gsub('"','%22').gsub(/[\r\n]/,'').strip + '"'
149
+ attrs << "IV=#{@encryption_iv}" if @encryption_iv
150
+ '#EXT-X-KEY:' + attrs.join(',')
151
+ end
152
+ else
153
+ nil
94
154
  end
95
- io_stream << "#EXT-X-ENDLIST\n" if files.length > 0 && final_media_file
96
155
  end
97
156
 
98
- def write(path)
99
- check_version_restrictions
100
- f = ::File.open(path, "w")
101
- write_to_io(f)
102
- f.close()
157
+ def reset_byterange_history
158
+ @prev_byterange_endpoint = nil
159
+ end
160
+
161
+ def generate_byterange_line(file)
162
+ line = nil
163
+
164
+ if file.byterange
165
+ if file.byterange_offset && file.byterange_offset != @prev_byterange_endpoint
166
+ offset = file.byterange_offset
167
+ elsif @prev_byterange_endpoint.nil?
168
+ offset = 0
169
+ else
170
+ offset = nil
171
+ end
172
+
173
+ line = "#EXT-X-BYTERANGE:#{file.byterange_offset.to_i}"
174
+ line += "@#{offset}" if offset
175
+
176
+ @prev_byterange_endpoint = offset + file.byterange
177
+ else
178
+ @prev_byterange_endpoint = nil
179
+ end
180
+
181
+ line
103
182
  end
104
183
 
105
184
 
@@ -107,14 +186,16 @@ class M3Uzi
107
186
  # Files
108
187
  #-------------------------------------
109
188
 
110
- def add_file(&block)
189
+ def add_file(path = nil, duration = nil)
111
190
  new_file = M3Uzi::File.new
112
- yield(new_file)
113
- @files << new_file
191
+ new_file.path = path if path
192
+ new_file.duration = duration if duration
193
+ yield(new_file) if block_given?
194
+ @playlist_items << new_file
114
195
  end
115
196
 
116
197
  def filenames
117
- files.map { |file| file.path }
198
+ items(File).map { |file| file.path }
118
199
  end
119
200
 
120
201
 
@@ -122,14 +203,16 @@ class M3Uzi
122
203
  # Streams
123
204
  #-------------------------------------
124
205
 
125
- def add_stream(&block)
206
+ def add_stream(path = nil, bandwidth = nil)
126
207
  new_stream = M3Uzi::Stream.new
127
- yield(new_stream)
128
- @streams << new_stream
208
+ new_stream.path = path
209
+ new_stream.bandwidth = bandwidth
210
+ yield(new_stream) if block_given?
211
+ @playlist_items << new_stream
129
212
  end
130
213
 
131
214
  def stream_names
132
- streams.map { |stream| stream.path }
215
+ items(Stream).map { |stream| stream.path }
133
216
  end
134
217
 
135
218
 
@@ -137,56 +220,65 @@ class M3Uzi
137
220
  # Tags
138
221
  #-------------------------------------
139
222
 
140
- def add_tag(&block)
223
+ def add_tag(name = nil, value = nil)
141
224
  new_tag = M3Uzi::Tag.new
142
- yield(new_tag)
143
- @tags << new_tag
144
- end
145
-
146
- def [](key)
147
- tag_name = key.to_s.upcase.gsub("_", "-")
148
- obj = tags.detect { |tag| tag.name == tag_name }
149
- obj && obj.value
225
+ new_tag.name = name
226
+ new_tag.value = value
227
+ yield(new_tag) if block_given?
228
+ @header_tags[new_tag.name] = new_tag
150
229
  end
151
230
 
152
- def []=(key, value)
153
- add_tag do |tag|
154
- tag.name = key
155
- tag.value = value
156
- end
157
- end
231
+ # def [](key)
232
+ # tag_name = key.to_s.upcase.gsub("_", "-")
233
+ # obj = tags.detect { |tag| tag.name == tag_name }
234
+ # obj && obj.value
235
+ # end
236
+ #
237
+ # def []=(key, value)
238
+ # add_tag do |tag|
239
+ # tag.name = key
240
+ # tag.value = value
241
+ # end
242
+ # end
158
243
 
159
244
 
160
245
  #-------------------------------------
161
246
  # Comments
162
247
  #-------------------------------------
163
248
 
164
- def add_comment(comment)
165
- @comments << comment
249
+ def add_comment(comment = nil)
250
+ new_comment = M3Uzi::Comment.new
251
+ new_comment.text = comment
252
+ yield(new_comment) if block_given?
253
+ @playlist_items << new_comment
166
254
  end
167
255
 
168
- def <<(comment)
169
- add_comment(comment)
170
- end
256
+ # def <<(comment)
257
+ # add_comment(comment)
258
+ # end
171
259
 
172
260
  def check_version_restrictions
173
261
  @version = 1
174
262
 
263
+ #
175
264
  # Version 2 Features
176
- if @tags.detect { |tag| tag.name == 'KEY' && tag.value.to_s =~ /,IV=/ }
265
+ #
266
+
267
+ # Check for custom IV
268
+ if valid_items(File).detect { |item| item.encryption_key_url && item.encryption_iv }
177
269
  @version = 2 if @version < 2
178
270
  end
179
271
 
180
272
  # Version 3 Features
181
- if @files.detect { |file| file.duration.kind_of?(Float) }
273
+ if valid_items(File).detect { |item| item.duration.kind_of?(Float) }
182
274
  @version = 3 if @version < 3
183
275
  end
184
276
 
185
277
  # Version 4 Features
186
- if @files.detect { |file| file.byterange }
278
+ if valid_items(File).detect { |item| item.byterange }
187
279
  @version = 4 if @version < 4
188
280
  end
189
- if @tags.detect { |tag| ['MEDIA','I-FRAMES-ONLY'].include?(tag.name) }
281
+ if valid_items(Tag).detect { |item| ['MEDIA','I-FRAMES-ONLY'].include?(item.name) }
190
282
  @version = 4 if @version < 4
191
283
  end
192
284
 
@@ -199,36 +291,43 @@ class M3Uzi
199
291
 
200
292
  protected
201
293
 
202
- def self.type(line)
203
- case line
204
- when /^\s*$/
205
- :whitespace
206
- when /^#(?!EXT)/
207
- :comment
208
- when /^#EXTINF/
209
- :info
210
- when /^#EXT(-X)?-STREAM-INF/
211
- :stream
212
- when /^#EXT(-X)?-ENDLIST/
213
- :final
214
- when /^#EXT(?!INF)/
215
- :tag
216
- else
217
- :file
218
- end
219
- end
220
-
221
- def self.parse_general_tag(line)
222
- line.match(/^#EXT(?:-X-)?(?!STREAM-INF)([^:\n]+)(:([^\n]+))?$/).values_at(1, 3)
294
+ # def self.type(line)
295
+ # case line
296
+ # when /^\s*$/
297
+ # :whitespace
298
+ # when /^#(?!EXT)/
299
+ # :comment
300
+ # when /^#EXTINF/
301
+ # :info
302
+ # when /^#EXT(-X)?-STREAM-INF/
303
+ # :stream
304
+ # when /^#EXT(-X)?-ENDLIST/
305
+ # :final
306
+ # when /^#EXT(?!INF)/
307
+ # :tag
308
+ # else
309
+ # :file
310
+ # end
311
+ # end
312
+ #
313
+ # def self.parse_general_tag(line)
314
+ # line.match(/^#EXT(?:-X-)?(?!STREAM-INF)([^:\n]+)(:([^\n]+))?$/).values_at(1, 3)
315
+ # end
316
+ #
317
+ # def self.parse_file_tag(line)
318
+ # line.match(/^#EXTINF:[ \t]*(\d+),?[ \t]*(.*)$/).values_at(1, 2)
319
+ # end
320
+ #
321
+ # def self.parse_stream_tag(line)
322
+ # match = line.match(/^#EXT-X-STREAM-INF:(.*)$/)[1]
323
+ # match.scan(/([A-Z-]+)\s*=\s*("[^"]*"|[^,]*)/) # return attributes as array of arrays
324
+ # end
325
+
326
+ def self.format_iv(num)
327
+ '0x' + num.to_s(16).rjust(32,'0')
223
328
  end
224
329
 
225
- def self.parse_file_tag(line)
226
- line.match(/^#EXTINF:[ \t]*(\d+),?[ \t]*(.*)$/).values_at(1, 2)
330
+ def format_iv(num)
331
+ self.class.format_iv(num)
227
332
  end
228
-
229
- def self.parse_stream_tag(line)
230
- match = line.match(/^#EXT-X-STREAM-INF:(.*)$/)[1]
231
- match.scan(/([A-Z-]+)\s*=\s*("[^"]*"|[^,]*)/) # return attributes as array of arrays
232
- end
233
-
234
333
  end
@@ -0,0 +1,10 @@
1
+ class M3Uzi
2
+ class Comment < Item
3
+
4
+ attr_accessor :text
5
+
6
+ def format
7
+ "##{text}"
8
+ end
9
+ end
10
+ end
data/lib/m3uzi/file.rb CHANGED
@@ -1,15 +1,41 @@
1
1
  class M3Uzi
2
- class File
2
+ class File < Item
3
3
 
4
- attr_accessor :path, :duration, :description, :byterange
4
+ attr_accessor :path, :duration, :description, :byterange, :byterange_offset, :encryption_key_url, :encryption_iv
5
+
6
+ # Unsupported tags: PROGRAM-DATE-TIME, DISCONTINUITY, I-FRAMES-ONLY
7
+ # Autogenerated tags: EXTINF, BYTERANGE, KEY, VERSION, TARGETDURATION, ENDLIST
8
+
9
+ def initialize
10
+ @encryption_key_url = :unset
11
+ end
5
12
 
6
13
  def attribute_string
7
14
  if duration.kind_of?(Float)
8
- "#{sprintf('%0.4f',duration)},#{description}"
15
+ "#{sprintf('%0.4f',duration)}," + description.to_s.gsub(/[\r\n]/,' ').strip
9
16
  else
10
- "#{duration},#{description}"
17
+ "#{duration.to_i.round}," + description.to_s.gsub(/[\r\n]/,' ').strip
11
18
  end
12
19
  end
13
20
 
21
+ def format
22
+ # Need to add key info if appropriate?
23
+ "#EXTINF:#{attribute_string}\n#{path}"
24
+ end
25
+
26
+ def encryption_iv=(value)
27
+ if value.to_s =~ /^0x/i
28
+ value = $'
29
+ end
30
+
31
+ if value.kind_of?(String)
32
+ raise "Invalid encryption_iv given" unless value.length <= 32 && value =~ /^[0-9a-f]+$/i
33
+ @encryption_iv = '0x' + value.downcase.rjust(32,'0')
34
+ elsif value.nil?
35
+ @encryption_iv = nil
36
+ else
37
+ @encryption_iv = M3Uzi.format_iv(value.to_i)
38
+ end
39
+ end
14
40
  end
15
41
  end
data/lib/m3uzi/item.rb ADDED
@@ -0,0 +1,9 @@
1
+ class M3Uzi
2
+ class Item
3
+
4
+ def valid?
5
+ true
6
+ end
7
+
8
+ end
9
+ end
data/lib/m3uzi/stream.rb CHANGED
@@ -1,17 +1,27 @@
1
1
  class M3Uzi
2
-
3
- class Stream
2
+ class Stream < Item
4
3
 
5
4
  attr_accessor :path, :bandwidth, :program_id, :codecs, :resolution
6
5
 
6
+ # Unsupported tags: EXT-X-MEDIA, EXT-X-I-FRAME-STREAM-INF
7
+ # Unsupported attributes of EXT-X-STREAM-INF: AUDIO, VIDEO
8
+
7
9
  def attribute_string
8
10
  s = []
9
- s << "PROGRAM-ID=#{program_id || 1}"
10
- s << "BANDWIDTH=#{bandwidth}" if bandwidth
11
+ s << "PROGRAM-ID=#{program_id.to_i || 1}"
12
+ s << "BANDWIDTH=#{bandwidth.to_i}"
11
13
  s << "CODECS=\"#{codecs}\"" if codecs
12
14
  s << "RESOLUTION=#{resolution}" if resolution
13
15
  s.join(',')
14
16
  end
17
+
18
+ def format
19
+ "#EXT-X-STREAM-INF:#{attribute_string}\n#{path}"
20
+ end
21
+
22
+ def valid?
23
+ !!(path && bandwidth)
24
+ end
15
25
  end
16
26
 
17
27
  end
data/lib/m3uzi/tag.rb CHANGED
@@ -1,14 +1,34 @@
1
1
  class M3Uzi
2
-
3
- class Tag
2
+ class Tag < Item
4
3
 
5
4
  attr_reader :name
6
5
  attr_accessor :value
7
6
 
7
+ VALID_TAGS = %w{PLAYLIST-TYPE ALLOW-CACHE}
8
+
9
+ # Unsupported tags: MEDIA-SEQUENCE, I-FRAMES-ONLY, PLAYLIST-TYPE
10
+ # Autogenerated tags: EXTM3U, VERSION, ENDLIST, BYTERANGE, TARGETDURATION
11
+
8
12
  def name=(n)
9
13
  @name = n.to_s.upcase.gsub("_", "-")
10
14
  end
11
15
 
16
+ def format
17
+ string << "#EXT-X-#{name}"
18
+ string << ":#{value}" if value
19
+ string
20
+ end
21
+
22
+ def valid?
23
+ case name
24
+ when 'PLAYLIST-TYPE'
25
+ ['EVENT','VOD'].include?(value)
26
+ when 'ALLOW-CACHE'
27
+ ['YES','NO'].include?(value)
28
+ else
29
+ true
30
+ end
31
+ end
12
32
  end
13
33
 
14
34
  end
data/lib/m3uzi/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class M3Uzi
2
- VERSION = '0.2.1'
2
+ VERSION = '0.4.2'
3
3
  end
data/test/m3uzi_test.rb CHANGED
@@ -12,18 +12,18 @@ class M3UziTest < Test::Unit::TestCase
12
12
  assert_equal M3Uzi, m3u.class
13
13
  end
14
14
 
15
- should "read in an index file" do
16
- m3u = M3Uzi.read(File.join(File.dirname(__FILE__), "fixtures/index.m3u8"))
17
- assert_equal M3Uzi, m3u.class
18
- end
15
+ # should "read in an index file" do
16
+ # m3u = M3Uzi.read(File.join(File.dirname(__FILE__), "fixtures/index.m3u8"))
17
+ # assert_equal M3Uzi, m3u.class
18
+ # end
19
19
  end
20
20
 
21
21
  context "with protocol versions" do
22
22
  should "set version 2 for encryption IV" do
23
23
  m3u = M3Uzi.new
24
- m3u['KEY'] = "0x1234567890abcdef1234567890abcdef"
24
+ m3u.add_file('1.ts',10) { |f| f.encryption_key_url = "key.dat" }
25
25
  assert_equal 1, m3u.check_version_restrictions
26
- m3u['KEY'] = "0x1234567890abcdef1234567890abcdef,IV=0x1234567890abcdef1234567890abcdef"
26
+ m3u.add_file('1.ts',10) { |f| f.encryption_key_url = "key.dat"; f.encryption_iv = "0x1234567890abcdef1234567890abcdef" }
27
27
  assert_equal 2, m3u.check_version_restrictions
28
28
 
29
29
  output_stream = StringIO.new
@@ -53,7 +53,7 @@ class M3UziTest < Test::Unit::TestCase
53
53
 
54
54
  output_stream = StringIO.new
55
55
  m3u.write_to_io(output_stream)
56
- assert output_stream.string =~ /:10\.000,/
56
+ assert output_stream.string =~ /:10\.0000,/
57
57
  assert output_stream.string !~ /:10,/
58
58
  end
59
59
 
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 4
7
8
  - 2
8
- - 1
9
- version: 0.2.1
9
+ version: 0.4.2
10
10
  platform: ruby
11
11
  authors:
12
12
  - Brandon Arbini
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-10-26 00:00:00 -05:00
18
+ date: 2011-12-12 00:00:00 -08:00
19
19
  default_executable:
20
20
  dependencies: []
21
21
 
@@ -30,7 +30,9 @@ extensions: []
30
30
  extra_rdoc_files: []
31
31
 
32
32
  files:
33
+ - lib/m3uzi/comment.rb
33
34
  - lib/m3uzi/file.rb
35
+ - lib/m3uzi/item.rb
34
36
  - lib/m3uzi/stream.rb
35
37
  - lib/m3uzi/tag.rb
36
38
  - lib/m3uzi/version.rb