diff --git a/lib/m3uzi.rb b/lib/m3uzi.rb index d8729cb..5a414ea 100644 --- a/lib/m3uzi.rb +++ b/lib/m3uzi.rb @@ -2,23 +2,24 @@ require 'm3uzi/tag' require 'm3uzi/file' require 'm3uzi/stream' +require 'm3uzi/comment' require 'm3uzi/version' class M3Uzi - # Unsupported: PROGRAM-DATE-TIME DISCONTINUITY - VALID_TAGS = %w{TARGETDURATION MEDIA-SEQUENCE ALLOW-CACHE ENDLIST KEY} + # Tags not supported for writing: PROGRAM-DATE-TIME DISCONTINUITY ENDLIST - attr_accessor :files, :streams - attr_accessor :tags, :comments + # Header tags are only supported once per file. Specifying it multiple times + # will override previous values. + HEADER_TAGS = %w{TARGETDURATION MEDIA-SEQUENCE ALLOW-CACHE} + + attr_accessor :header_tags, :playlist_items attr_accessor :final_media_file attr_accessor :version def initialize - @files = [] - @streams = [] - @tags = [] - @comments = [] + @header_tags = {} + @playlist_items = [] @final_media_file = true @version = 1 end @@ -72,53 +73,45 @@ def initialize # end def write_to_io(io_stream) + prev_encryption_key = nil + prev_encryption_iv = nil + check_version_restrictions io_stream << "#EXTM3U\n" io_stream << "#EXT-X-VERSION:#{@version.to_i}\n" if @version > 1 - comments.each do |comment| - io_stream << "##{comment}\n" - end - tags.each do |tag| - next if %w{M3U ENDLIST}.include?(tag.name.to_s.upcase) - if VALID_TAGS.include?(tag.name.to_s.upcase) - io_stream << "#EXT-X-#{tag.name.to_s.upcase}" - else - io_stream << "##{tag.name.to_s.upcase}" - end - tag.value && io_stream << ":#{tag.value}" - io_stream << "\n" + + @header_tags.each do |item| + io_stream << (item.format + "\n") if item.valid? end - files.each do |file| - io_stream << "#EXTINF:#{file.attribute_string}" - io_stream << "\n#{file.path}\n" + @playlist_items.each do |item| + io_stream << (item.format + "\n") if item.valid? end - streams.each do |stream| - io_stream << "#EXT-X-STREAM-INF:#{stream.attribute_string}" - io_stream << "\n#{stream.path}\n" - end - io_stream << "#EXT-X-ENDLIST\n" if files.length > 0 && final_media_file + + io_stream << "#EXT-X-ENDLIST\n" if items(File).length > 0 && @final_media_file end def write(path) - check_version_restrictions - f = ::File.open(path, "w") - write_to_io(f) - f.close() + ::File.open(path, "w") { |f| write_to_io(f) } end + def items(kind) + @playlist_items.select { |item| item.kind_of?(kind) } + end #------------------------------------- # Files #------------------------------------- - def add_file(&block) + def add_file(path = nil, duration = nil) new_file = M3Uzi::File.new - yield(new_file) - @files << new_file + new_file.path = path + new_file.duration = duration + yield(new_file) if block_given? + @playlist_items << new_file end def filenames - files.map { |file| file.path } + items(File).map { |file| file.path } end @@ -126,14 +119,16 @@ def filenames # Streams #------------------------------------- - def add_stream(&block) + def add_stream(path = nil, bandwidth = nil) new_stream = M3Uzi::Stream.new - yield(new_stream) - @streams << new_stream + new_stream.path = path + new_stream.bandwidth = bandwidth + yield(new_stream) if block_given? + @playlist_items << new_stream end def stream_names - streams.map { |stream| stream.path } + items(Stream).map { |stream| stream.path } end @@ -141,56 +136,73 @@ def stream_names # Tags #------------------------------------- - def add_tag(&block) + def add_tag(name = nil, value = nil) new_tag = M3Uzi::Tag.new - yield(new_tag) - @tags << new_tag - end - - def [](key) - tag_name = key.to_s.upcase.gsub("_", "-") - obj = tags.detect { |tag| tag.name == tag_name } - obj && obj.value - end - - def []=(key, value) - add_tag do |tag| - tag.name = key - tag.value = value + new_tag.name = name + new_tag.value = value + yield(new_tag) if block_given? + if HEADER_TAGS.include?(new_tag.name.to_s.upcase) + @header_tags[new_tag.name.to_s.upcase] = new_tag + else + @playlist_items << new_tag end end + # def [](key) + # tag_name = key.to_s.upcase.gsub("_", "-") + # obj = tags.detect { |tag| tag.name == tag_name } + # obj && obj.value + # end + # + # def []=(key, value) + # add_tag do |tag| + # tag.name = key + # tag.value = value + # end + # end + #------------------------------------- # Comments #------------------------------------- - def add_comment(comment) - @comments << comment + def add_comment(comment = nil) + new_comment = M3Uzi::Comment.new + new_commant.text = comment + yield(new_commant) if block_given? + @playlist_items << new_comment end - def <<(comment) - add_comment(comment) - end + # def <<(comment) + # add_comment(comment) + # end def check_version_restrictions @version = 1 + # # Version 2 Features - if @tags.detect { |tag| tag.name == 'KEY' && tag.value.to_s =~ /,IV=/ } - @version = 2 if @version < 2 + # + + # Check for custom IV + current_iv = 0 + items(File).each do |item| + if item.encryption_iv && item.encryption_iv.to_s.downcas != format_iv(current_iv) + @version = 2 if @version < 2 + end + current_iv += 1 end # Version 3 Features - if @files.detect { |file| file.duration.kind_of?(Float) } + if items(File).detect { |item| item.duration.kind_of?(Float) } @version = 3 if @version < 3 end # Version 4 Features - if @files.detect { |file| file.byterange } + if items(File).detect { |item| item.byterange } @version = 4 if @version < 4 end - if @tags.detect { |tag| ['MEDIA','I-FRAMES-ONLY'].include?(tag.name) } + if items(Tag).detect { |item| ['MEDIA','I-FRAMES-ONLY'].include?(item.name) } @version = 4 if @version < 4 end @@ -235,4 +247,7 @@ def check_version_restrictions # match.scan(/([A-Z-]+)\s*=\s*("[^"]*"|[^,]*)/) # return attributes as array of arrays # end + def format_iv(num) + num.to_s(16).rjust(32,'0') + end end diff --git a/lib/m3uzi/comment.rb b/lib/m3uzi/comment.rb new file mode 100644 index 0000000..c49225d --- /dev/null +++ b/lib/m3uzi/comment.rb @@ -0,0 +1,10 @@ +class M3Uzi + class Comment < Item + + attr_accessor :text + + def format + "# #{text}" + end + end +end diff --git a/lib/m3uzi/file.rb b/lib/m3uzi/file.rb index 00d4e65..0161d28 100644 --- a/lib/m3uzi/file.rb +++ b/lib/m3uzi/file.rb @@ -1,7 +1,7 @@ class M3Uzi - class File + class File < Item - attr_accessor :path, :duration, :description, :byterange + attr_accessor :path, :duration, :description, :byterange, :encryption_key, :encryption_iv def attribute_string if duration.kind_of?(Float) @@ -11,5 +11,10 @@ def attribute_string end end + def format + # Need to add key info if appropriate? + "#EXTINF:#{attribute_string}\n#{path}" + end + end end diff --git a/lib/m3uzi/item.rb b/lib/m3uzi/item.rb new file mode 100644 index 0000000..52d9150 --- /dev/null +++ b/lib/m3uzi/item.rb @@ -0,0 +1,9 @@ +class M3Uzi + class Item + + def valid? + true + end + + end +end \ No newline at end of file diff --git a/lib/m3uzi/stream.rb b/lib/m3uzi/stream.rb index d492d52..d61fcc3 100644 --- a/lib/m3uzi/stream.rb +++ b/lib/m3uzi/stream.rb @@ -1,6 +1,5 @@ class M3Uzi - - class Stream + class Stream < Item attr_accessor :path, :bandwidth, :program_id, :codecs, :resolution @@ -12,6 +11,10 @@ def attribute_string s << "RESOLUTION=#{resolution}" if resolution s.join(',') end + + def format + "#EXT-X-STREAM-INF:#{attribute_string}\n#{path}" + end end end diff --git a/lib/m3uzi/tag.rb b/lib/m3uzi/tag.rb index c574d90..996dda5 100644 --- a/lib/m3uzi/tag.rb +++ b/lib/m3uzi/tag.rb @@ -1,14 +1,23 @@ class M3Uzi - - class Tag + class Tag < Item attr_reader :name attr_accessor :value + VALID_TAGS = %w{TARGETDURATION MEDIA-SEQUENCE ALLOW-CACHE} + def name=(n) @name = n.to_s.upcase.gsub("_", "-") end + def format + string = '#' + string << "EXT-X-" if VALID_TAGS.include?(name) + string << name + string << ":#{value}" if value + string + end + end end diff --git a/lib/m3uzi/version.rb b/lib/m3uzi/version.rb index 329b00e..2ec0188 100644 --- a/lib/m3uzi/version.rb +++ b/lib/m3uzi/version.rb @@ -1,3 +1,3 @@ class M3Uzi - VERSION = '0.2.0' + VERSION = '0.3.0' end