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