module XMorph require_relative '../xmorph' class Base @@host = nil @@domain = nil @@feed = nil @@account_name = nil @@volt_tag = nil attr_accessor :logger, :host, :domain, :account_name, :volt_tag, :asset_path, :mediainfo_output, :error 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 def self.domain @@domain end def self.account_name @@account_name end def self.feed @@feed end 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 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 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) return false unless success begin status, self.mediainfo_output = Util.mediainfo_xml_to_hash(response) return status rescue => e XMorph::Base.logger.info("XMorph#get_asset_details: Could not get mediainfo for #{self.asset_path}. Error: #{e.message}") return false end 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 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 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 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 return true end def validate_asset_before_transcoding() validate_asset(false) return true end 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 # { # 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 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 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" unless self.transcoded end end if default_checks[ALLOWED_VIDEO_BIT_RATE] != IGNORE if bit_rate.present? 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 << "Video Bit rate" end end if default_checks[ALLOWED_SCAN_TYPE] != IGNORE if scan_type.present? 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? 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 = []; sampling_rate = []; number_of_audio_channels = []; unit = [] audio_tracks.each do |a| audio_codecs << a["Format"] 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) 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_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.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 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.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.compact.empty? end end if default_checks[ALLOWED_NUMBER_OF_AUDIO_CHANNELS] != IGNORE 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.compact.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 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