lib/xmorph/base.rb in xmorph-0.1.14 vs lib/xmorph/base.rb in xmorph-0.1.16

- old
+ new

@@ -1,25 +1,44 @@ module XMorph + require_relative '../xmorph' class Base - IGNORE = "ignore" - VALIDATE = "validate" + @@host = nil + @@domain = nil + @@feed = nil + @@account_name = nil + @@volt_tag = nil - 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" + attr_accessor :logger, :host, :domain, :account_name, :volt_tag, :asset_path, :mediainfo_output, :error - 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" + def self.initialize(volt_tag, host, domain, account_name, feed=nil) + @@volt_tag = volt_tag + @@host = host + @@domain = domain + @@account_name = account_name + @@feed = feed + end + + def self.volt_tag + @@volt_tag + end + + def self.host + @@host + end - attr_accessor :logger, :asset_path, :mediainfo_output, :profiles, :profile_name, :error, :default_video_checks, :default_audio_checks + def self.domain + @@domain + end + + def self.account_name + @@account_name + end + + def self.feed + @@feed + end def self.root File.dirname __dir__ end @@ -29,78 +48,124 @@ def self.logger=(logger) @@logger = logger end - def initialize(asset_path) - self.asset_path = asset_path - self.set_profiles + def self.get_xmorph_release() + version_flie = File.join(File.dirname(self.root), "version") + if File.exists? version_flie + tag = File.read(version_flie) + return tag.present? ? tag.strip : "master" + else + return nil + end 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}" + def get_asset_details(option="") + mediainfo_command = "#{File.dirname(self.class.root)}/bin/mediainfo --output=XML #{option} #{self.asset_path}" success, response = Util.run_cmd_with_response(mediainfo_command) - raise TranscoderError.new("Failed to get mediainfo for the asset.") unless success + return false unless success begin - self.mediainfo_output = Util.mediainfo_xml_to_hash(response)[1] + status, self.mediainfo_output = Util.mediainfo_xml_to_hash(response) + return status rescue => e - raise TranscoderError.new("Failed to get mediainfo for the asset.") + XMorph::Base.logger.info("XMorph#get_asset_details: Could not get mediainfo for #{self.asset_path}. Error: #{e.message}") + return false end - set_validations end + end + + class BaseValidator < XMorph::Base + + attr_accessor :transcoded, :default_video_checks, :default_audio_checks + + 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" + ALLOWED_SAMPLING_RATE = "allowed_sampling_rate" + + def initialize(asset_path) + self.asset_path = asset_path + end + + def self.get_validator(asset_path) + file_path = File.join(XMorph::Base.root, "xmorph", "customers", XMorph::Base.host, XMorph::Base.domain, "ingest", "validate.rb") + return nil unless File.exist? file_path + XMorph::Base.logger.info("XMorph#get_validator: loading file #{file_path} to validate #{asset_path}") + load file_path + return Validate.new(asset_path) + end + def set_validations - begin - self.default_video_checks = self.video_checks - rescue => e - raise TranscoderError.new("Default video validation requirements are not defined.") + self.default_video_checks = nil + self.default_audio_checks = nil + if self.transcoded + begin + self.default_video_checks = self.video_checks_after_transcoding + rescue => e + XMorph::Base.logger.info("XMorph#set_validations_after_trans: Default video validation checks are not defined.") + end + begin + self.default_audio_checks = self.audio_checks_after_transcoding + rescue => e + XMorph::Base.logger.info("XMorph#set_validations_after_trans: Default audio validation checks are not defined.") + end + else + begin + self.default_video_checks = self.video_checks_before_transcoding + rescue => e + raise ValidatorError.new("Default video validation checks before transcoding are not defined.") + end + begin + self.default_audio_checks = self.audio_checks_before_transcoding + rescue => e + raise ValidatorError.new("Default audio validation checks before transcoding are not defined.") + end 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 validate_asset(transcoded=false) + self.transcoded = transcoded + self.error = nil + raise ValidatorError.new("Could not get mediainfo for the asset #{self.asset_path}") unless self.get_asset_details + self.set_validations - 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.") + if self.transcoded + raise ValidatorError.new(self.error || "Failed to perform default video validations after transcoding.") if self.default_video_checks and not perform_default_video_validations + raise ValidatorError.new(self.error || "Failed to perform default audio validations after transcoding.") if self.default_audio_checks and not perform_default_audio_validations + else + raise ValidatorError.new(self.error || "Failed to perform default video validations before transcoding.") unless perform_default_video_validations + raise ValidatorError.new(self.error || "Failed to perform default audio validations before transcoding.") unless perform_default_audio_validations 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] + return true 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 + def validate_asset_before_transcoding() + validate_asset(false) + return true end - def post_process - #do some post processing + def validate_asset_after_transcoding() + validate_asset(true) + return true 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 # { @@ -121,10 +186,28 @@ # ALLOWED_AUDIO_BIT_RATE => (120..317) || [192, 317] || IGNORE, # ALLOWED_NUMBER_OF_AUDIO_CHANNELS => (1..16) || [1, 2] || IGNORE, # } #end + def convert_bit_rate(bit_rate, from, to) + from.downcase! + return bit_rate if from == to + if to == "mbps" + if from == "bps" + bit_rate = (bit_rate / 1000000) + elsif from == "kbps" + bit_rate = (bit_rate / 1000) + end + elsif to == "kbps" + if from == "bps" + bit_rate = (bit_rate / 1000) + elsif from == "mbps" + bit_rate = (bit_rate * 1000) + end + end + end + def perform_default_video_validations mediainfo = self.mediainfo_output video_info = mediainfo["Video"] default_checks = self.default_video_checks @@ -135,10 +218,11 @@ 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" @@ -163,29 +247,31 @@ 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" + missing << "Frame rate" unless self.transcoded 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 + unit = bit_rate.split(" ")[-1] + bit_rate = bit_rate.gsub(/ /,"").scan(/\.|\d/).join('').to_f + bit_rate = convert_bit_rate(bit_rate, unit, "mbps") + errors << "Unexpected Video Bit rate #{bit_rate} Mbps. We support #{default_checks[ALLOWED_VIDEO_BIT_RATE]} (Mbps)" unless default_checks[ALLOWED_VIDEO_BIT_RATE].include? bit_rate else - missing << "Bit rate" + missing << "Video 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 + scan_type.downcase! + errors << "Unexpected Scan type #{scan_type}. We support #{default_checks[ALLOWED_SCAN_TYPE]}" unless default_checks[ALLOWED_SCAN_TYPE].any?{|check| check.downcase == scan_type} 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? @@ -200,14 +286,16 @@ 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_codecs = []; bit_rate = []; sampling_rate = []; number_of_audio_channels = []; unit = [] 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? + unit << (a["Bit_rate"] || a["Nominal_bit_rate"]).split(" ")[-1] if (a["Bit_rate"] || a["Nominal_bit_rate"]).present? + bit_rate << (a["Bit_rate"] || a["Nominal_bit_rate"]).gsub(/ /,"").scan(/\.|\d/).join('').to_f if (a["Bit_rate"] || a["Nominal_bit_rate"]).present? + sampling_rate << a["Sampling_rate"].gsub(/ /,"").scan(/\.|\d/).join('').to_f if a["Sampling_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) @@ -219,33 +307,44 @@ 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_SAMPLING_RATE] != IGNORE + if sampling_rate.compact.empty? + missing << "Sampling rate" + else + unsupported_sampling_rate = sampling_rate.map{|s| s unless default_checks[ALLOWED_SAMPLING_RATE].include? s} + errors << "Unexpected Sampling rate #{unsupported_sampling_rate}. We support #{default_checks[ALLOWED_SAMPLING_RATE]}" unless unsupported_sampling_rate.compact.empty? + end + end if default_checks[ALLOWED_AUDIO_BIT_RATE] != IGNORE - if bit_rate.empty? || (bit_rate.uniq.include? nil) - missing << "Bit rate" + if bit_rate.compact.empty? + missing << "Audio Bit rate" unless self.transcoded else + bit = [] + bit_rate.each_with_index{|b, index| bit << convert_bit_rate(b, unit[index], "kbps")} + bit_rate = bit 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? + errors << "Unexpected Audio Bit rate #{unsupported_bit_rate} Kbps. We support #{default_checks[ALLOWED_AUDIO_BIT_RATE]} (Kbps)" unless unsupported_bit_rate.compact.empty? end end if default_checks[ALLOWED_AUDIO_CODECS] != IGNORE - if audio_codecs.empty? || (audio_codecs.uniq.include? nil) + if audio_codecs.compact.empty? 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? + errors << "Unexpected audio codecs #{unsupported_audio_codecs}. We support #{default_checks[ALLOWED_AUDIO_CODECS]}" unless unsupported_audio_codecs.compact.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) + if number_of_audio_channels.compact.empty? 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? + 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.compact.empty? end end end unless missing.empty? self.error = "Couldn't find #{missing.join(",")} of the Video correctly" @@ -255,7 +354,162 @@ self.error = errors.join("\n") return false end return true end + + def validate_based_on_mediainfo(file_path, transcoded) + info = File.read(file_path) + begin + status, self.mediainfo_output = Util.mediainfo_xml_to_hash(info) + rescue => e + puts e + return false, "Could not get mediainfo for #{self.asset_path}. Error: #{e.message}" + end + begin + transcoded ? self.validate_asset_after_transcoding : self.validate_asset_before_transcoding + return true, nil + rescue ValidatorError => e + return false, e.message + end + end + + end + + class BaseTranscoder < XMorph::Base + + attr_accessor :profiles, :profile_name, :transcoded_path + + def initialize(asset_path) + self.asset_path = asset_path + self.set_profiles + end + + def self.get_transcode_template(asset_path) + file_path = File.join(XMorph::Base.root, "xmorph", "customers", XMorph::Base.host, XMorph::Base.domain, "ingest", "transcode.rb") + if File.exist? file_path + XMorph::Base.logger.info("XMorph#get_transcode_template: loading file #{file_path} to transcode #{asset_path}.") + load file_path + return Transcode.new(asset_path) + else + XMorph::Base.logger.info("XMorph#get_transcode_template: Could not find transcoding template - #{file_path} to transcode.") + raise TranscoderError.new("Could not find transcoding template.") + end + end + + def get_profile(return_profile_name=false) + self.set_profile_name + unless (self.profile_name and self.profiles[self.profile_name]) + raise TranscoderError.new(self.error || "Media doesnt match any transcoding profiles.") + end + return self.profiles[self.profile_name], self.profile_name if return_profile_name + return true + end + + def get_executable_commands(commands) + commands = commands.join("$") if commands.is_a? Array + tmp_paths = commands.scan(/%{(.+?)}/).uniq.flatten.select{|c| c.match(/TEMP/)} + tmp_replacements = {} + tmp_paths.each{|t| tmp_replacements[t.to_sym] = Tempfile.new(['',t]).path} + replace = {IP_MOUNT_PATH: File.dirname(self.asset_path), OP_MOUNT_PATH: File.dirname(self.transcoded_path), DOCKER_IMAGE: XMorph::Base.volt_tag, IN: self.asset_path, OUT: self.transcoded_path} + commands = commands % (replace.merge(tmp_replacements)) + return commands.split("$") + end + + def transcode(transcoded_path) + self.transcoded_path = transcoded_path + get_asset_details() + get_profile() + commands = get_executable_commands(self.profiles[self.profile_name]) + commands.each do |cmd| + status, response = Util.run_cmd_with_response(cmd) + raise TranscoderError.new(response.split("\n").last) unless status + end + return true + end + + def get_profile_based_on_mediainfo(file_path) + info = File.read(file_path) + begin + status, self.mediainfo_output = Util.mediainfo_xml_to_hash(info) + rescue => e + return false, "Could not get mediainfo for #{self.asset_path}. Error: #{e.message}" + end + get_profile() + if self.error + return false, self.error + else + return true, self.profile_name + end + end + + end + + class BaseProcessor < XMorph::Base + + attr_accessor :meta_data, :output_path, :volt_commands + + def initialize(asset_path) + self.asset_path = asset_path + self.meta_data = {} + end + + def self.load_processor(processor_file) + file_path = File.join(XMorph::Base.root, "xmorph", "customers", XMorph::Base.host, XMorph::Base.domain, "ingest", processor_file) + unless File.exist? file_path + XMorph::Base.logger.info("XMorph#load_processor: Skipping processing, Template doesnt exist for #{XMorph::Base.account_name}.") + return false + end + XMorph::Base.logger.info("XMorph#load_processor: loading file #{file_path} to process.") + load file_path + return true + end + + def process_asset + return true, {} + end + + def get_meta_data + return true, {} + end + + def transform_volt_commands + self.set_volt_commands + replace = {IP_MOUNT_PATH: File.dirname(self.asset_path), DOCKER_IMAGE: XMorph::Base.volt_tag, IN_FILE: self.asset_path} + self.volt_commands.each{|action, cmd| self.volt_commands[action] = cmd % replace} + end + + def self.get_meta_data(asset_path) + return true, {} unless self.load_processor("pre_processor.rb") + begin + pre_processor = PreProcessor.new(asset_path) + rescue + XMorph::Base.logger.info("PreProcessor class doesn't exist for account #{XMorph::Base.account_name}.") + return true, {} + end + return pre_processor.get_meta_data() + end + + def self.process_before_transcoding(asset_path) + return true, {} unless self.load_processor("pre_processor.rb") + begin + pre_processor = PreProcessor.new(asset_path) + rescue + XMorph::Base.logger.info("PreProcessor class doesn't exist for account #{XMorph::Base.account_name}.") + return true, {} + end + return pre_processor.process_asset() + end + + def self.process_after_transcoding(asset_path) + return true, {} unless self.load_processor("post_processor.rb") + begin + post_processor = PostProcessor.new(asset_path) + rescue + XMorph::Base.logger.info("PostProcessor class doesn't exist for account #{XMorph::Base.account_name}.") + return true, {} + end + return post_processor.process_asset() + end + end end \ No newline at end of file