# XMorph Morphs things from one kind to another. Transcodes, in local speak, for example.. # Features - XMorph can validate asset before transcoding against given set of validations (set of video and audio parameters). - Perform pre transcode processing on an asset, for example meta data extraction on mxf file - Can choose a pre-defined profile based on asset's mediainfo, and transcode using the profiles corresponding command. - Perform validations after transcoding - Perform post transcode processing - if you want to extract something for a ts file. # Installation: ```sh $ gem install xmorph ``` For a specific version: ```sh $ gem install xmorph -v 0.1.15 ``` # Adding new XMorph profile for a customer ```sh $ cd xmorph/lib/xmorph/customers $ mkdir -p HOST/ACCOUNT_DOMAIN/Ingest $ cd HOST/ACCOUNT_DOMAIN/Ingest $ vi validate.rb #mandatory $ vi transcode.rb #optional $ vi pre_processor.rb #optional $ vi post_processor.rb #optional ``` how to get HOST, ACCOUNT DOMAIN ? http://**gusto**[*host*].amagi.tv/**gusto**[*account_domain*]/**GUSTO**[*feed_code*]/medias ## File Contents ```ruby #validate.rb class Validate < XMorph::BaseValidator #values for the sollowing validations can be Range, Array or IGNORE(upcase) #Range - (n1..n2) - applicable if the values are numbers, validates if value x is between n1 and n2, it also includes fractions. ex 5: in (1..10) -> true and 29.97 in (25..30) -> true #Array - [n1,n2] - validates if value x is in the given array. ex: 5 in [1,10] -> false and 1 in [1,10] -> true #IGNORE will skip validation for the corresponding parameter. it will not even check if media has that parameter. i.e if mediainfo is not able to read parameter x for the asset, its validation will still return true. #These methods are to be implemented in the sub class, there's no defaut method defined in the base class, exception is raised if these methods are not found. #Checks before transcoding are mandatory #ValidatorError will be raised if video_checks_before_transcoding and audio_checks_before_transcoding functions are not defined def video_checks_before_transcoding { 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'] || IGNORE, } end def audio_checks_before_transcoding { PRESENCE_OF_AUDIO_TRACK => IGNORE || VALIDATE, ALLOWED_NUMBER_OF_AUDIO_TRACKS => (1..16)|| [2, 4, 6, 8] || IGNORE, ALLOWED_AUDIO_CODECS => ['aac', 'ac-3', 'mp2', 'mp4', 'dolby e', 'mpeg audio'] || IGNORE, ALLOWED_AUDIO_BIT_RATE => (120..317) || [192, 317] || IGNORE, ALLOWED_NUMBER_OF_AUDIO_CHANNELS => (1..16) || [1, 2] || IGNORE, ALLOWED_SAMPLING_RATE => (41..48) || [41, 48] || IGNORE, } end #The below functions are optional #We are ignoring Frame rate check after transcoding, since mediainfo is not able to read frame rate of the transcoded asset. def video_checks_after_transcoding { ALLOWED_ASPECT_RATIO => ["4:3", "16:9"] || IGNORE, ALLOWED_HEIGHT => (400..1080) || [720, 1080, 546] || IGNORE, ALLOWED_WIDTH => (640..720) || [720, 1920] || IGNORE, ALLOWED_VIDEO_BIT_RATE => (8..32) || [8, 32] || IGNORE, ALLOWED_SCAN_TYPE => ['progressive', 'interlaced'] || IGNORE, } end def audio_checks_after_transcoding { PRESENCE_OF_AUDIO_TRACK => IGNORE || VALIDATE, ALLOWED_NUMBER_OF_AUDIO_TRACKS => (1..16)|| [2, 4, 6, 8] || IGNORE, ALLOWED_AUDIO_CODECS => ['aac', 'ac-3', 'mp2', 'mp4', 'dolby e', 'mpeg audio'] || IGNORE, ALLOWED_AUDIO_BIT_RATE => (120..317) || [192, 317] || IGNORE, ALLOWED_NUMBER_OF_AUDIO_CHANNELS => (1..16) || [1, 2] || IGNORE, ALLOWED_SAMPLING_RATE => (41..48) || [41, 48] || IGNORE, } end end ``` ```ruby #transcode.rb class Transcode < XMorph::BaseTranscoder PRO_1080_16_TRACKS = "1080_16" PRO_720_STEREO = "720_stereo" def set_profiles self.profiles = { PRO_1080_16_TRACKS => "ffmpeg -y -i %{IN} -vf \"fps=25.000000,scale=1920x1080\" -filter_complex \"[0:a:8][0:a:9]amerge=inputs=2[aout0]\" -pix_fmt yuv420p -vcodec h264 -g 13 -bf 2 -x264opts nal-hrd=cbr -profile:v high -flags +ilme+ildct -top 1 -vb 10000000 -minrate:v 10000000 -maxrate:v 10000000 -bufsize:v 20000000 -acodec libfdk_aac -profile:a aac_low -ab 192k -map 0:v -map \"[aout0]\" -muxrate 11211200 -streamid 0:2064 -streamid 1:2068 %{OUT} 2>&1", #If you have sequence of commands to be executed #Make sure you folow file naming convention PRO_720_STEREO => ["ffmpeg -y -i %{IN} -s 1920x1080 -vcodec h264 -profile:v high -r 25 -g 13 -pix_fmt yuv420p -bf 2 -x264opts nal-hrd=cbr -vb 15000000 -map 0:v -streamid 0:2064 -bufsize:v 24000000 -acodec libfdk_aac -ac 2 -ar 48000 -profile:a aac_low -ab 192k -map 0:a:0 -muxrate 13411200 -streamid 1:2068 -vsync 1 -async 1 -v verbose %{TEMPFILE_1.ts}", "../transcoder -if %{TEMPFILE_1.ts} -of %{TEMPFILE_2.ts} -vp -acm \"1,2;1,2;1,2\" -ac \"aac;ac3;pass\" -lt -24 -ltp -2 -ac3_dialnorm -24", "../transcoder -if %{TEMPFILE_2.ts} -of %{OUT} -vp -acm \"1,2;3,4;5,6;5,6\" -ac \"pass;pass;pass;aac\" -lt -12 -ltp -2"], } end #Write summary as what are the combinations of profiles we get for this customer, and based on which parameters we choose the profile. # have a set of conditional statements to choose a profile defined above. def set_profile_name self.profile_name = nil self.error = nil mediainfo = self.mediainfo_output video_info = mediainfo["Video"] audio_tracks = mediainfo["Audio"] #Once you have videoinfo and audiotracks info, write a set of if else statements to choose a profile from the above defined #Assign profile name to self.profile_name #Assign any error messages you want to display on UI to self.error, make sure error messages are unambiguous #EX: self.error = "Got unexpected width-#{width} for video with AR-#{aspect_ratio}, expected width: 640 or 720" #Ensure there's only if..elseif statements rather than if..else XMorph::Base.logger.debug("XMorph#set_profile_name#Cinedigm: using profile #{self.profile_name}") unless self.profile_name.nil? return true end ``` ```ruby #pre_processor.rb or post_processor.rb class PreProcessor < XMorph::BaseProcessor EXTRACT_META_DATA = "extract_meta_data" def set_volt_commands self.volt_commands = { EXTRACT_META_DATA => "docker run --rm -v %{IP_MOUNT_PATH}:%{IP_MOUNT_PATH} %{DOCKER_IMAGE} ./volt/extract_mxf_meta.sh %{IN_FILE}" } end def process_asset #To convert the above mentioned commands to executable ones self.transform_volt_commands status, response = XMorph::Util.run_cmd_with_response(self.volt_commands[EXTRACT_META_DATA]) raise ProcessorError.new(response) unless status #Write code to convert response to hash which can be consumed by blip/end user end end ``` # Usage Once you have written XMorph profile for a customer, test if it's working fine. ```ruby #Install latest xmorph gem require 'xmorph' #Initialise XMorph as XMorph::Base.initialize(VOLT_TAG, HOST, DOMAIN, ACCOUNT_NAME) XMorph::Base.initialize("amagidevops/volt:mr_2.1.14", "gusto", "gusto", "amagi") #For validations before transcoding #validator can be nil, if validations are not defined #This function will validate the asset against the requirements mentioned in video_checks_before_transcoding and audio_checks_before_transcoding methods for a given customer. It will either return true or raise exception incase of failure. validator = XMorph::BaseValidator.get_validator(ASSET_PATH) #this function gives ValidatorError if any validation fails. validator.validate_asset_before_transcoding #pre transcode processing #metadata should be a hash consisting of data we expect to be extracted out of the asset status, metadata = XMorph::BaseProcessor.process_before_transcoding(ASSET_PATH) #Transcoding #This will raise TranscoderError if template is not found, if it's not able to get transcoding profile or if transcoding fails. transcoder = XMorph::BaseTranscoder.get_transcode_template(ASSET_PATH) transcoder.transcode(ASSET_PATH) #post transcode validations #performs validations if defined, raises ValidatorError, if validations are not met validator.validate_asset_after_transcoding #post transcode processing status, metadata = XMorph::BaseProcessor.process_after_transcoding(ASSET_PATH) ``` # Trying to keep things simple Dump mediainfo of an asset to a file and execute a command to check what profile it chooses to transcode. Write mediainfo to a file: ``` sh $ mediainfo --output=XML #{ASSET_PATH} > ./xmorph/spec/xmorph/customers/{HOST}/{ACCOUNT_DOMAIN}/#{ASSET_NAME}.xml ``` Change the below template to be used for a new customer, change contents within %{ } and file_names and profiles within data **To verify validations:** ```ruby #In ./xmorph/spec/xmorph/customers/%{gusto}/%{gusto}/transcode_spec.rb require "spec_helper" describe 'Transcode' do before (:each) do load "./lib/xmorph/customers/%{gusto}/%{gusto}/ingest/validate.rb" @validator = Validate.new('/home/www/EXP/Media/asd.mp4') end #have mediainfo xml files in same dir as _spec files within a customer specific directory. #./xmorph/spec/xmorph/customers/%{gusto}/%{gusto}/#{ASSET_NAME}.xml it "should perform validations for a given mediainfo" do data = [ {"mediainfo_filepath" => "sample.xml", "transcoded" => false, "valid" => true, "error" => nil}, ] validate(Validate.new(""), data) #,true) end end ``` And RUN: ```sh $ bundle exec rspec spec/xmorph/customers/%{gusto}/%{gusto}/transcode_spec.rb ``` **To verify transcoding profile:** ```ruby #In ./xmorph/spec/xmorph/customers/%{gusto}/%{gusto}/transcode_spec.rb require "spec_helper" describe 'Transcode' do before (:each) do load "./lib/xmorph/customers/%{gusto}/%{gusto}/ingest/transcode.rb" #give correct load path @transcoder = Transcode.new('/home/www/EXP/Media/asd.mp4') #Asset path can be anything end #have mediainfo xml files in same dir as _spec files within a customer specific directory. #./xmorph/spec/xmorph/customers/gusto/gusto/#{ASSET_NAME}.xml it "should choose corresponding profiles for a given mediainfo" do data = [ {"mediainfo_filepath" => "Ep1004_1_TomatoOnionRaita_Clean.xml", "profile" => Transcode::PRO_720_STEREO}, ] #pass true at the end for dry run. i.e. it will not run rspec instead it will print matched profile/error for a given mediainfo get_profiles(Transcode.new('sample_asset'), data) #,true) end end ``` And RUN: ```sh $ bundle exec rspec spec/xmorph/customers/%{gusto}/%{gusto}/transcode_spec.rb ``` This will tell, if the profile you mentioned was matched or no, and if it wasn't matched it prints error message telling why it did not match. # Deployment Gem: ```sh $ cd ./xmorph $ gem build xmorph.gemspec $ gem push xmorph-.gem #This will be availble in rubygems.org from where you can install henceforth ``` Deploy GIT TAG: ```sh $ cd ./xmorph $ cap production deploy Please enter server (newproduction.amagi.tv): Please enter ssh_port (22): Please enter ssh_password: Please enter branch ():