lib/mediainfo.rb in mediainfo-0.6.2 vs lib/mediainfo.rb in mediainfo-0.7.0

- old
+ new

@@ -1,5 +1,6 @@ +require "forwardable" require "mediainfo/string" require "mediainfo/attr_readers" =begin # Mediainfo @@ -53,184 +54,289 @@ Previous versions of this gem(<= 0.5.1) worked against v0.7.11, which did not generate XML output, and is no longer supported. =end class Mediainfo + extend Forwardable extend AttrReaders - SECTIONS = %w( audio video image ) # and General + class Error < StandardError; end + class ExecutionError < Error; end + class IncompatibleVersionError < Error; end - ### GENERAL + def self.delegate(method_name, stream_type = nil) + if stream_type == :general + def_delegator :"@#{stream_type}_stream", method_name + else + def_delegator :"@#{stream_type}_stream", method_name, "#{stream_type}_#{method_name}" + end + end - mediainfo_attr_reader :codec_id, "Codec ID" + def self.version + @version ||= `#{path} --Version`[/v([\d.]+)/, 1] + end - mediainfo_duration_reader :duration + # AttrReaders depends on this. + def self.supported_attributes; @supported_attributes ||= []; end - mediainfo_attr_reader :format - mediainfo_attr_reader :format_profile - mediainfo_attr_reader :format_info - mediainfo_attr_reader :overall_bit_rate - mediainfo_attr_reader :writing_application - mediainfo_attr_reader :writing_library + SECTIONS = [:general, :video, :audio, :image] + NON_GENERAL_SECTIONS = SECTIONS - [:general] + attr_reader :streams + + # Size of source file as reported by File.size. + # Returns nil if you haven't yet fired off the system command. def size; File.size(@full_filename) if @full_filename; end - mediainfo_date_reader :mastered_date - mediainfo_date_reader :tagged_date - mediainfo_date_reader :encoded_date + class StreamProxy + def initialize(mediainfo, stream_type) + unless Mediainfo::SECTIONS.include? stream_type + raise ArgumentError, "invalid stream_type: #{stream_type.inspect}" + end + + @stream_type = stream_type + @mediainfo = mediainfo + @streams = @mediainfo.streams.select { |x| x.send("#{stream_type}?") } + end + + def [](id); @streams[id]; end + def count; @streams.size; end + attr_reader :streams + attr_reader :stream_type + + class SingleStreamAPIError < RuntimeError; end + class NoStreamsForProxyError < NoMethodError; end + + def method_missing(m, *a, &b) + if streams.size > 1 + raise SingleStreamAPIError, "You cannot use the single stream, convenience API on a multi-stream file." + else + if relevant_stream = streams.detect { |s| s.respond_to?(m) } + relevant_stream.send(m, *a, &b) + else + raise NoStreamsForProxyError, "there are no :#{stream_type} streams to send :#{m} to" + end + end + end + end - ### VIDEO + class Stream + class InvalidStreamType < Mediainfo::Error; end + + def self.inherited(stream_type) + stream_type.extend(AttrReaders) + + def stream_type.method_added(method_name) + if stream_type = name[/[^:]+$/][/^(#{SECTIONS.map { |x| x.to_s.capitalize } * '|'})/] + stream_type.downcase! + stream_type = stream_type.to_sym + else + raise "could not determine stream type, please report bug!" + end + + Mediainfo.delegate(method_name, stream_type) + end + end + + def self.create(stream_type) + raise ArgumentError, "need a stream_type, received #{stream_type.inspect}" if stream_type.nil? + + stream_class_name = "#{stream_type}Stream" + + if Mediainfo.const_defined?(stream_class_name) + Mediainfo.const_get(stream_class_name).new(stream_type) + else + raise InvalidStreamType, "bad stream type: #{stream_type.inspect}" + end + end + + def initialize(stream_type) + raise ArgumentError, "need a stream_type, received #{stream_type.inspect}" if stream_type.nil? + + @stream_type = stream_type.downcase.to_sym + + # TODO @parsed_response is not the best name anymore, but I'm leaving it + # alone to focus on refactoring the interface to the streams + # before I refactor the attribute reader implementations. + @parsed_response = { @stream_type => {} } + end + + def [](k); @parsed_response[@stream_type][k]; end + def []=(k,v); @parsed_response[@stream_type][k] = v; end + + Mediainfo::SECTIONS.each { |t| define_method("#{t}?") { t == @stream_type } } + end - mediainfo_section_query :video + class GeneralStream < Stream + mediainfo_attr_reader :codec_id, "Codec ID" + + mediainfo_duration_reader :duration + + mediainfo_attr_reader :format + mediainfo_attr_reader :format_profile + mediainfo_attr_reader :format_info + mediainfo_attr_reader :overall_bit_rate + mediainfo_attr_reader :writing_application + mediainfo_attr_reader :writing_library + + mediainfo_date_reader :mastered_date + mediainfo_date_reader :tagged_date + mediainfo_date_reader :encoded_date + end - mediainfo_attr_reader :video_stream_id, "ID" + class VideoStream < Stream + mediainfo_attr_reader :stream_id, "ID" + + mediainfo_duration_reader :duration + + mediainfo_attr_reader :stream_size + mediainfo_attr_reader :bit_rate + mediainfo_attr_reader :nominal_bit_rate + + mediainfo_attr_reader :bit_rate_mode + def cbr?; video? and "Constant" == bit_rate_mode; end + def vbr?; video? and not cbr?; end + + mediainfo_attr_reader :scan_order + mediainfo_attr_reader :scan_type + def interlaced?; video? and "Interlaced" == scan_type; end + def progressive?; video? and not interlaced? end + + mediainfo_int_reader :resolution + + mediainfo_attr_reader :colorimetry + alias_method :colorspace, :colorimetry + + mediainfo_attr_reader :format + mediainfo_attr_reader :format_info + mediainfo_attr_reader :format_profile + mediainfo_attr_reader :format_version + mediainfo_attr_reader :format_settings_cabac, "Format settings, CABAC" + mediainfo_attr_reader :format_settings_reframes, "Format settings, ReFrames" + mediainfo_attr_reader :format_settings_matrix, "Format settings, Matrix" + # Format settings, BVOP : Yes + # Format settings, QPel : No + # Format settings, GMC : No warppoints + # mediainfo_attr_reader :format_settings_qpel, "Format settings, QPel" + mediainfo_attr_reader :color_primaries + mediainfo_attr_reader :transfer_characteristics + mediainfo_attr_reader :matrix_coefficients + + mediainfo_attr_reader :codec_id, "Codec ID" + mediainfo_attr_reader :codec_info, "Codec ID/Info" + alias_method :codec_id_info, :codec_info + + mediainfo_attr_reader :frame_rate + def fps; frame_rate[/[\d.]+/].to_f if frame_rate; end + alias_method :framerate, :fps + + mediainfo_attr_reader :minimum_frame_rate + def min_fps; minimum_frame_rate[/[\d.]+/].to_f if video?; end + alias_method :min_framerate, :min_fps + + mediainfo_attr_reader :maximum_frame_rate + def max_fps; maximum_frame_rate[/[\d.]+/].to_f if video?; end + alias_method :max_framerate, :max_fps + + mediainfo_attr_reader :frame_rate_mode + + mediainfo_attr_reader :display_aspect_ratio + # alias_method :display_aspect_ratio, :display_aspect_ratio + + mediainfo_attr_reader :bits_pixel_frame, "Bits/(Pixel*Frame)" + + mediainfo_int_reader :width + mediainfo_int_reader :height + + def frame_size; "#{width}x#{height}" if width or height; end + + mediainfo_date_reader :encoded_date + mediainfo_date_reader :tagged_date + + mediainfo_attr_reader :standard + end - mediainfo_duration_reader :video_duration - - mediainfo_attr_reader :video_stream_size - mediainfo_attr_reader :video_bit_rate - mediainfo_attr_reader :video_nominal_bit_rate - - mediainfo_attr_reader :video_bit_rate_mode - def cbr?; video? and "Constant" == video_bit_rate_mode; end - def vbr?; video? and not cbr?; end - - mediainfo_attr_reader :video_scan_order - mediainfo_attr_reader :video_scan_type - def interlaced?; video? and "Interlaced" == video_scan_type; end - def progressive?; video? and not interlaced? end - - mediainfo_int_reader :video_resolution - - mediainfo_attr_reader :video_colorimetry - alias_method :video_colorspace, :video_colorimetry - - mediainfo_attr_reader :video_format - mediainfo_attr_reader :video_format_profile - mediainfo_attr_reader :video_format_version - mediainfo_attr_reader :video_format_settings_cabac, "Format settings, CABAC" - mediainfo_attr_reader :video_format_settings_reframes, "Format settings, ReFrames" - mediainfo_attr_reader :video_format_settings_matrix, "Format settings, Matrix" - # Format settings, BVOP : Yes - # Format settings, QPel : No - # Format settings, GMC : No warppoints - # mediainfo_attr_reader :video_format_settings_qpel, "Format settings, QPel" - mediainfo_attr_reader :video_color_primaries - mediainfo_attr_reader :video_transfer_characteristics - mediainfo_attr_reader :video_matrix_coefficients - - mediainfo_attr_reader :video_codec_id, "Codec ID" - mediainfo_attr_reader :video_codec_info, "Codec ID/Info" - - mediainfo_attr_reader :video_frame_rate - def fps; video_frame_rate[/[\d.]+/].to_f if video?; end - alias_method :framerate, :fps - - mediainfo_attr_reader :video_minimum_frame_rate - def min_fps; video_minimum_frame_rate[/[\d.]+/].to_f if video?; end - alias_method :min_framerate, :min_fps - - mediainfo_attr_reader :video_maximum_frame_rate - def max_fps; video_maximum_frame_rate[/[\d.]+/].to_f if video?; end - alias_method :max_framerate, :max_fps - - mediainfo_attr_reader :video_frame_rate_mode - - mediainfo_attr_reader :video_display_aspect_ratio - alias_method :display_aspect_ratio, :video_display_aspect_ratio - - mediainfo_attr_reader :video_bits_pixel_frame, "Bits/(Pixel*Frame)" - - mediainfo_int_reader :video_width - mediainfo_int_reader :video_height - - def resolution; "#{width}x#{height}" if video? or image?; end - def width; if video?; video_width; elsif image?; image_width; end; end - def height; if video?; video_height; elsif image?; image_height; end; end - - mediainfo_date_reader :video_encoded_date - mediainfo_date_reader :video_tagged_date - - ### AUDIO - - mediainfo_section_query :audio - - mediainfo_attr_reader :audio_stream_id, "ID" - - mediainfo_duration_reader :audio_duration - - mediainfo_attr_reader :audio_sampling_rate - def audio_sample_rate - return unless rate = audio_sampling_rate_before_type_cast - number = rate.gsub(/[^\d.]+/, "").to_f - number = case rate - when /KHz/ then number * 1000 - when /Hz/ then number - else - raise "unhandled sample rate! please report bug!" + class AudioStream < Stream + mediainfo_attr_reader :stream_id, "ID" + + mediainfo_duration_reader :duration + + mediainfo_attr_reader :sampling_rate + def sample_rate + return unless rate = sampling_rate_before_type_cast + number = rate.gsub(/[^\d.]+/, "").to_f + number = case rate + when /KHz/ then number * 1000 + when /Hz/ then number + else + raise "unhandled sample rate! please report bug!" + end + number.to_i end - number.to_i + alias_method :sampling_rate, :sample_rate + + mediainfo_attr_reader :stream_size + mediainfo_attr_reader :bit_rate + mediainfo_attr_reader :bit_rate_mode + mediainfo_attr_reader :interleave_duration, "Interleave, duration" + + mediainfo_int_reader :resolution + alias_method :sample_bit_depth, :resolution + + mediainfo_attr_reader :format + mediainfo_attr_reader :format_profile + mediainfo_attr_reader :format_version + mediainfo_attr_reader :format_info, "Format/Info" + mediainfo_attr_reader :format_settings_sbr, "Format settings, SBR" + mediainfo_attr_reader :format_settings_endianness, "Format settings, Endianness" + mediainfo_attr_reader :format_settings_sign, "Format settings, Sign" + mediainfo_attr_reader :codec_id, "Codec ID" + mediainfo_attr_reader :codec_info, "Codec ID/Info" + mediainfo_attr_reader :codec_id_hint + mediainfo_attr_reader :channel_positions + + mediainfo_int_reader :channels, "Channel(s)" + def stereo?; 2 == channels; end + def mono?; 1 == channels; end + + mediainfo_date_reader :encoded_date + mediainfo_date_reader :tagged_date end - alias_method :audio_sampling_rate, :audio_sample_rate - mediainfo_attr_reader :audio_stream_size - mediainfo_attr_reader :audio_bit_rate - mediainfo_attr_reader :audio_bit_rate_mode - mediainfo_attr_reader :audio_interleave_duration, "Interleave, duration" + class ImageStream < Stream + mediainfo_attr_reader :resolution + mediainfo_attr_reader :format + + mediainfo_int_reader :width + mediainfo_int_reader :height + + def frame_size; "#{width}x#{height}" if width or height; end + end - mediainfo_int_reader :audio_resolution - alias_method :audio_sample_bit_depth, :audio_resolution + Mediainfo::SECTIONS.each do |stream_type| + class_eval %{ + def #{stream_type}; @#{stream_type}_proxy ||= StreamProxy.new(self, :#{stream_type}); end + def #{stream_type}?; streams.any? { |x| x.#{stream_type}? }; end + }, __FILE__, __LINE__ + end - mediainfo_attr_reader :audio_format - mediainfo_attr_reader :audio_format_info, "Format/Info" - mediainfo_attr_reader :audio_format_settings_endianness, "Format settings, Endianness" - mediainfo_attr_reader :audio_format_settings_sign, "Format settings, Sign" - mediainfo_attr_reader :audio_codec_id, "Codec ID" - mediainfo_attr_reader :audio_codec_info, "Codec ID/Info" - mediainfo_attr_reader :audio_codec_id_hint - mediainfo_attr_reader :audio_channel_positions - - mediainfo_int_reader :audio_channels, "Channel(s)" - def stereo?; 2 == audio_channels; end - def mono?; 1 == audio_channels; end - - mediainfo_date_reader :audio_encoded_date - mediainfo_date_reader :audio_tagged_date - - ### IMAGE - - mediainfo_section_query :image - mediainfo_attr_reader :image_resolution - mediainfo_attr_reader :image_format - - mediainfo_int_reader :image_width - mediainfo_int_reader :image_height - ### attr_reader :raw_response, :parsed_response, :full_filename, :filename, :path, :escaped_full_filename ### - class Error < StandardError; end - class ExecutionError < Error; end - class IncompatibleVersionError < Error; end - - def self.version - @version ||= `#{path} --Version`[/v([\d.]+)/, 1] - end - - ### - def initialize(full_filename = nil) if mediainfo_version < "0.7.25" raise IncompatibleVersionError, "Your version of mediainfo, #{mediainfo_version}, " + "is not compatible with this gem. >= 0.7.25 required." end + @streams = [] + if full_filename @full_filename = File.expand_path full_filename @path = File.dirname @full_filename @filename = File.basename @full_filename @@ -278,18 +384,16 @@ def xml_parser; self.class.xml_parser; end def self.default_mediainfo_path!; self.path = "mediainfo"; end default_mediainfo_path! unless path - def mediainfo_version - self.class.version - end + def mediainfo_version; self.class.version; end attr_reader :last_command def inspect - super.sub /@raw_response=".+?", @/, %{@raw_response="...", @} + super.sub(/@raw_response=".+?", @/, %{@raw_response="...", @}) end private def mediainfo! @last_command = "#{path} #{@escaped_full_filename} --Output=XML" @@ -310,50 +414,46 @@ self.class.load_xml_parser! else require "rexml/document" end - @parsed_response = {} - case xml_parser when "nokogiri" Nokogiri::XML(@raw_response).xpath("//track").each { |t| - bucket = bucket_for t['type'] - + s = Stream.create(t['type']) t.xpath("*").each do |c| - bucket[key_for(c)] = c.content.strip + s[key_for(c)] = c.content.strip end + @streams << s } when "hpricot" Hpricot::XML(@raw_response).search("track").each { |t| - bucket = bucket_for t['type'] - + s = Stream.create(t['type']) t.children.select { |n| n.is_a? Hpricot::Elem }.each do |c| - bucket[key_for(c)] = c.inner_html.strip + s[key_for(c)] = c.inner_html.strip end + @streams << s } else REXML::Document.new(@raw_response).elements.each("/Mediainfo/File/track") { |t| - bucket = bucket_for t.attributes['type'] - + s = Stream.create(t.attributes['type']) t.children.select { |n| n.is_a? REXML::Element }.each do |c| - bucket[key_for(c)] = c.text.strip + s[key_for(c)] = c.text.strip end + @streams << s } end + + SECTIONS.each do |section| + default_target_stream = if send("#{section}?") + send(section).streams.first + else + Mediainfo.const_get("#{section.to_s.capitalize}Stream").new(section.to_s.capitalize) + end + instance_variable_set "@#{section}_stream", default_target_stream + end end def key_for(attribute_node) attribute_node.name.downcase.gsub(/_+/, "_").gsub(/_s(\W|$)/, "s").strip - end - - def bucket_for(section) - section = section.downcase if section - - if section == "general" - @parsed_response - else - @parsed_response[section] ||= {} - @parsed_response[section] - end end end