module XMorph class Base IGNORE = "ignore" VALIDATE = "validate" ALLOWED_ASPECT_RATIO = "allowed_aspect_ratio" ALLOWED_HEIGHT = "allowed_height" ALLOWED_WIDTH = "allowed_width" ALLOWED_FRAME_RATE = "allowed_frame_rate" ALLOWED_VIDEO_BIT_RATE = "allowed_video_bit_rate" ALLOWED_SCAN_TYPE = "allowed_scan_type" PRESENCE_OF_AUDIO_TRACK = "presence_of_audio_track" ALLOWED_NUMBER_OF_AUDIO_TRACKS = "allowed_number_of_audio_tracks" ALLOWED_AUDIO_CODECS = "allowed_audio_codecs" ALLOWED_AUDIO_BIT_RATE = "allowed_audio_bit_rate" ALLOWED_NUMBER_OF_AUDIO_CHANNELS = "allowed_number_of_audio_channels" attr_accessor :logger, :asset_path, :mediainfo_output, :profiles, :profile_name, :error, :default_video_checks, :default_audio_checks def self.root File.dirname __dir__ end def self.logger @@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) end def self.logger=(logger) @@logger = logger end def initialize(asset_path) self.asset_path = asset_path self.set_profiles end def self.get_transcode_template(host, domain, name, asset_path, return_loaded_filepath=false) return nil unless domain file_path = File.join(self.root, "xmorph", "customers", host, domain, "transcode.rb") raise TranscoderError.new("Transcoding profile doesn't exist for account, #{name}.") unless File.exist? file_path XMorph::Base.logger.debug("XMorph#get_transcode_template: loading file #{file_path} to transcode #{asset_path}") load file_path return Transcode.new(asset_path), file_path if return_loaded_filepath return Transcode.new(asset_path) end def get_asset_details mediainfo_command = "#{File.dirname(self.class.root)}/bin/mediainfo --output=XML #{self.asset_path}" success, response = Util.run_cmd_with_response(mediainfo_command) raise TranscoderError.new("Failed to get mediainfo for the asset.") unless success begin self.mediainfo_output = Util.mediainfo_xml_to_hash(response)[1] rescue => e raise TranscoderError.new("Failed to get mediainfo for the asset.") end set_validations end def set_validations begin self.default_video_checks = self.video_checks rescue => e raise TranscoderError.new("Default video validation requirements are not defined.") end begin self.default_audio_checks = self.audio_checks rescue => e raise TranscoderError.new("Default audio validation requirements are not defined.") end return true end def validate_asset raise TranscoderError.new(self.error || "Failed to perform default video validations.") unless perform_default_video_validations raise TranscoderError.new(self.error || "Failed to perform default audio validations.") unless perform_default_audio_validations return true end def get_profile(return_profile_name=false) self.set_profile_name if (self.profile_name.blank? or self.profiles[self.profile_name].blank?) raise TranscoderError.new(self.error || "Media doesnt match any transcoding profiles.") end XMorph::Base.logger.debug("XMorph#get_profile: Using ffmpeg command: #{self.profiles[self.profile_name]} to transcode #{asset_path}") return self.profile_name, self.profiles[self.profile_name] if return_profile_name return self.profiles[self.profile_name] end def transcode(cmd) raise TranscoderError.new("Command passed to transcode is empty.") unless cmd status, response = Util.run_cmd_with_response(cmd) raise TranscoderError.new(self.error || response.split("\n").last) unless status return status end def post_process #do some post processing end #These are the default validations performed on assets #override in sub class with new configs #allowed values: Range, Array, IGNORE(presnce of that config will not be checked) #def video_checks # { # ALLOWED_ASPECT_RATIO => ["4:3", "16:9"] || IGNORE, # ALLOWED_HEIGHT => (400..1080) || [720, 1080, 546] || IGNORE, # ALLOWED_WIDTH => (640..720) || [720, 1920] || IGNORE, # ALLOWED_FRAME_RATE => (25..30) || [25, 29.970, 24] || IGNORE, # ALLOWED_VIDEO_BIT_RATE => (8..32) || [8, 32] || IGNORE, # ALLOWED_SCAN_TYPE => ['progressive', 'interlaced', 'interlaced_mbaff'] || IGNORE, # } #end #def audio_checks # { # PRESENCE_OF_AUDIO_TRACK => IGNORE || VALIDATE, # ALLOWED_NUMBER_OF_AUDIO_TRACKS => (1..16)|| [2, 4, 6, 8] || IGNORE, # ALLOWED_AUDIO_CODECS => ['aac', 'ac3', 'mp2', 'mp4'] || IGNORE, # ALLOWED_AUDIO_BIT_RATE => (120..317) || [192, 317] || IGNORE, # ALLOWED_NUMBER_OF_AUDIO_CHANNELS => (1..16) || [1, 2] || IGNORE, # } #end def perform_default_video_validations mediainfo = self.mediainfo_output video_info = mediainfo["Video"] default_checks = self.default_video_checks aspect_ratio = video_info["Display_aspect_ratio"] height = (video_info["Original_height"] || video_info["Height"]) width = (video_info["Original_width"] || video_info["Width"]) frame_rate = video_info["Frame_rate"] bit_rate = video_info["Bit_rate"] || video_info["Nominal_bit_rate"] scan_type = video_info["Scan_type"] errors = [] missing = [] if default_checks[ALLOWED_ASPECT_RATIO] != IGNORE if aspect_ratio.present? errors << "Unexpected Aspect ratio #{aspect_ratio}. We support #{default_checks[ALLOWED_ASPECT_RATIO]}" unless default_checks[ALLOWED_ASPECT_RATIO].include? aspect_ratio else missing << "Aspect ratio" end end if default_checks[ALLOWED_HEIGHT] != IGNORE if height.present? height = height.split("pixels")[0].gsub(/ /,"").to_i errors << "Unexpected Height #{height}. We support #{default_checks[ALLOWED_HEIGHT]}" unless default_checks[ALLOWED_HEIGHT].include? height else missing << "Height" end end if default_checks[ALLOWED_WIDTH] != IGNORE if width.present? width = width.split("pixels")[0].gsub(/ /,"").to_i errors << "Unexpected Width #{width}. We support #{default_checks[ALLOWED_WIDTH]}" unless default_checks[ALLOWED_WIDTH].include? width else missing << "Width" end end if default_checks[ALLOWED_FRAME_RATE] != IGNORE if frame_rate.present? frame_rate = frame_rate.split(" ")[0].to_f errors << "Unexpected Frame rate #{frame_rate}. We support #{default_checks[ALLOWED_FRAME_RATE]}" unless default_checks[ALLOWED_FRAME_RATE].include? frame_rate else missing << "Frame rate" end end if default_checks[ALLOWED_VIDEO_BIT_RATE] != IGNORE if bit_rate.present? bit_rate = bit_rate.split(" ")[0].to_f errors << "Unexpected Bit rate #{bit_rate}. We support #{default_checks[ALLOWED_VIDEO_BIT_RATE]}" unless default_checks[ALLOWED_VIDEO_BIT_RATE].include? bit_rate else missing << "Bit rate" end end if default_checks[ALLOWED_SCAN_TYPE] != IGNORE if scan_type.present? errors << "Unexpected Scan type #{scan_type}. We support #{default_checks[ALLOWED_SCAN_TYPE]}" unless default_checks[ALLOWED_SCAN_TYPE].include? scan_type.downcase else missing << "Scan type" end end unless missing.empty? self.error = "Couldn't find #{missing.join(",")} of the Video correctly" return false end unless errors.empty? self.error = errors.join("\n") return false end return true end def perform_default_audio_validations mediainfo = self.mediainfo_output audio_tracks = mediainfo["Audio"] default_checks = self.default_audio_checks number_of_audio_tracks = audio_tracks.count if number_of_audio_tracks > 0 audio_codecs = []; bit_rate = []; number_of_audio_channels = [] audio_tracks.each do |a| audio_codecs << a["Format"] bit_rate << (a["Bit_rate"] || a["Nominal_bit_rate"]).split(" ")[0].to_f if (a["Bit_rate"] || a["Nominal_bit_rate"]).present? number_of_audio_channels << a["Channel_s_"].split(" ")[0].to_i if a["Channel_s_"].present? end end if (default_checks[PRESENCE_OF_AUDIO_TRACK] != IGNORE) and (number_of_audio_tracks <= 0) self.error = "No audio tracks found" return false end errors = [] missing = [] if number_of_audio_tracks > 0 if default_checks[ALLOWED_NUMBER_OF_AUDIO_TRACKS] != IGNORE errors << "Unexpected number of audio tracks #{number_of_audio_tracks}. We support #{default_checks[ALLOWED_NUMBER_OF_AUDIO_TRACKS]} tracks" unless default_checks[ALLOWED_NUMBER_OF_AUDIO_TRACKS].include? number_of_audio_tracks end if default_checks[ALLOWED_AUDIO_BIT_RATE] != IGNORE if bit_rate.empty? || (bit_rate.uniq.include? nil) missing << "Bit rate" else unsupported_bit_rate = bit_rate.map{|b| b unless default_checks[ALLOWED_AUDIO_BIT_RATE].include? b} errors << "Unexpected Bit rate #{unsupported_bit_rate}. We support #{default_checks[ALLOWED_AUDIO_BIT_RATE]}" unless unsupported_bit_rate.empty? end end if default_checks[ALLOWED_AUDIO_CODECS] != IGNORE if audio_codecs.empty? || (audio_codecs.uniq.include? nil) missing << "audio codecs" else audio_codecs = audio_codecs.map{|a| a.downcase} unsupported_audio_codecs = audio_codecs - default_checks[ALLOWED_AUDIO_CODECS].to_a errors << "Unexpected audio codecs #{unsupported_audio_codecs}. We support #{default_checks[ALLOWED_AUDIO_CODECS]}" unless unsupported_audio_codecs.empty? end end if default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS] != IGNORE if number_of_audio_channels.empty? || (number_of_audio_channels.uniq.include? nil) missing << "number of audio channels" else unsupported_number_of_audio_channels = number_of_audio_channels - default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS].to_a errors << "Unexpected number of audio channels #{unsupported_number_of_audio_channels}. We support #{default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS]} channels" unless unsupported_number_of_audio_channels.empty? end end end unless missing.empty? self.error = "Couldn't find #{missing.join(",")} of the Video correctly" return false end unless errors.empty? self.error = errors.join("\n") return false end return true end end end