header={ "recipe_version": "1.1924", "title": "ProRes to SpotMXF", "description": "ProRes to SpotMXF", "tags": "default", "chef": "BeatRig", "dependencies": "ffmpeg,ffprobe", "spice": "BQ==:G+Hy36kJcnQQYYavb/JV2Il6eCYr6h4hRJHCF9rRbhX5BcazL6cuTgUMdz0xA50VWA5PmmV0Q7aWkiUtNFtbk991l+t0e8IBXMReMIT1GNjnZZDv7n2YRAo0xcSQbY0ixjG6LLH4wGKnspKAoA/v69iLn6N2IH0XzIZwTGdHSo4=", "flavour": "z8IaPpOD3b7ap9j4weAYAqKLVxFHwKMwdO1l1lMR5FONIq6nCEx4RkS6BtqVTbzSIy/XCNfPiOg4Z8JeEiRjO4YlRciFi704Rpr3gN+UenYt1oh/1skKwHdr5y9GUMq8dEk+6mb+9WPhgru6p3rwUf1LfjWAwG+2CGKmgO3E7CA=", "time": 1695416491, "core_version": "0.5.5", "magnetron_version": "1.0.243", "functions": "main,onConfig,onAbout", "uuid": "8ea3aedcf04a483cabb8da6108642f17", "instructions": "start recipe to select files", "type": "default", "os": "macOS", "palette": "Clean Slate" }; // ============================================================================ // GLOBALS // ============================================================================ // NUMBER OF FRAMES IS NEEDED IN PROGRESS UPDATE FUNCTIONS var framecount = 0; // SHORTHAND FOR THE / OR \ DEPENDING ON THE FILESYSTEM var PSS = ''; // AUTO FILL THE EXPORT FOLDER TO THE SAME AS THE VIDEO FILE var updateExportFolder = true; // ============================================================================= // ON ABOUT, SHOW POPUP WITH BUTTON TO OPEN WEBSITE // ============================================================================ function onAbout() { var title = "About this recipe"; var msg = "This recipe will create an MXF and XML file according to"+ "\nthe S.P.O.T. recommendations. More details can be found"+ "\non the recipe page on our website."; var args = { "showpage" : { "type" : "button", "label" : "show page", "returns" : 1 }, "close" : { "type" : "button", "label" : "close", "returns" : 0 } }; var r = dialog(title, msg, "w", args); if (r.showpage == 1) launchInBrowser('https://magnetron.app/recipes/prores-to-spotmxf/'); } // ============================================================================ // ON CONFIG, SHOW POPUP WITH BUTTON TO OPEN CONFIG FILE // ============================================================================ function onConfig() { var title = "Default values"; var msg = "This recipe uses a text file for storing the default"+ "\nvalues. This file will be created the first time you"+ "\nstart the recipe and can be customized in a plain text editor."; var args = { "showfile" : { "type" : "button", "label" : "show defaults file", "returns" : 1 }, "close" : { "type" : "button", "label" : "close", "returns" : 0 } }; var r = dialog(title, msg, "w", args); if (r.showfile == 1) revealPath(folders.content+'/default.txt'); } // ============================================================================ // THE RECIPE // ============================================================================ function main() { // ----------------------------- // CHECK IF FFMPEG AND FFPROBE ARE AVAILABLE if(getAllowedApps("ffmpeg") == '' ) abort("FFMPEG is not available"); if(getAllowedApps("ffprobe") == '' ) abort("FFMPEG is not available"); // ----------------------------- // SHORTHAND PSS = gvar.PathSeparatorString; // ----------------------------- // A FILE WITH DEFAULT VALUES var deffile = folders.content+'/default.txt'; // TEMP FILES FOR AUDIO ANALYZING / PROCESSING var tempStereo = folders.temp+PSS+"_temp_audio_stereo.wav"; var tempSurround = folders.temp+PSS+"_temp_audio_surround.wav"; var tempStereoFix = folders.temp+PSS+"_temp_audio_stereo_fix.wav"; var tempSurroundFix = folders.temp+PSS+"_temp_audio_surround_fix.wav"; // ----------------------------- // DEFAULT VALUES var v = { // TRACK ERRORS "errors" : "", // VALIDATION "allow_lowloud" : 0, "ignore_errors" : 0, // INPUT FILES "video" : "", "stereo" : "", "surround" : "", // EXPORT FOLDER "export_folder" : "", // META DATA FOR XML "meta_product" : "", "meta_title" : "", "meta_version" : 1, "meta_advertiser" : "", "meta_agency" : "", "meta_prodcomp" : "", "meta_comments" : "", "meta_project" : "", "meta_email" : "", "meta_date" : epochToFormatted(getCurrentEpoch(), "%d-%m-%Y"), // HIDE/SHOW SOME OPTIONS "show_optional" : false, }; // LOAD DEFAULT VALUES if ( fileExists(deffile) ){ // READ DEFAULTS FILE var t = stringToObject( readFile(deffile) ); // REPLACE VALUES BY THE DEFAULT VALUES IN THE FILE v.meta_product = t.meta_product; v.meta_title = t.meta_title; v.meta_advertiser = t.meta_advertiser; v.meta_agency = t.meta_agency; v.meta_prodcomp = t.meta_prodcomp; v.meta_email = t.meta_email; v.export_folder = t.export_folder; v.ignore_errors = t.ignore_errors; v.allow_lowloud = t.allow_lowloud; // DO NOT FILL IN THE EXPORT PATH BASED ON THE VIDEO PATH // BECAUSE IT IS ALREADY SPECIFIED IN THE CONFIG FILE if(v.export_folder != "") updateExportFolder = false; } // NO DEFAULT FILE, CREATE ONE else{ writeFile(deffile,objectToString({ "meta_product" : "", "meta_title" : "", "meta_advertiser" : "", "meta_agency" : "", "meta_prodcomp" : "", "meta_email" : "", "export_folder" : "", "ignore_errors" : false, "allow_lowloud" : false, })); } // KEEP DEFAULT VALUES AVAILABLE AS defaults // IN CASE THE FIELDS WHERE HIDDEN AND SHOW AGAIN var defaults = v; // KEEP TRACK OF ERRORS var err = []; var wrn = []; // ----------------------------- // KEEP GOING BACK TO THE DIALOG WHILE THERE ARE ERRORS // AND NOT PRESSED CANCEL while (1) { // ----------------------------- // IMPLODE ERRORS TO A STRING v.errors = ( err.length > 0 ? "- " + err.join("\n- ") : "" ) +( wrn.length > 0 ? "\n- " + wrn.join("\n- ") : "" ); // SHOW THE DIALOG v = showDialog(v); // ----------------------------- // CHECK IF VALUES ARE SET, IF SHOW_OPTIONAL IS FALSE // SOME VALUES WILL _NOT_ BEEN SET. ALSO // EMPTY PATHS ARE SET TO ROOT, BUT WE WANT THE DIALOG TO // OPEN IN THE DEFAULT LOCATION, NOT THE ROOT v.extra1 = setVal(v.extra1, ""); v.extra2 = setVal(v.extra2, ""); v.extra3 = setVal(v.extra3, ""); v.extra4 = setVal(v.extra4, ""); v.extra5 = setVal(v.extra5, ""); v.video = setVal(v.video, ""); v.stereo = setVal(v.stereo, ""); v.surround = setVal(v.surround, ""); v.export_folder = setVal(v.export_folder, ""); v.allow_lowloud = setVal(v.allow_lowloud, defaults.allow_lowloud); v.ignore_errors = setVal(v.ignore_errors, defaults.ignore_errors); v.meta_project = setVal(v.meta_project, ""); v.meta_comments = setVal(v.meta_comments, ""); // ----------------------------- // RESET ERRORS AFTER SUBMITTING THE DIALOG err = []; wrn = []; // ----------------------------- // WHEN SHOW/HIDE OPTION IS TOGGLED RELOAD THE DIALOG if (v.returns == 2) continue; // ----------------------------- // WHEN CANCELLED WAS PRESSED STOP THE WHILE if (v.cancel || isCanceled()) break; // ----------------------------- // VALIDATE META DATA // NOT EMPTY if(v.meta_title == "") err.push("Specify a title"); if(v.meta_product == "") err.push("Specify a product"); if(v.meta_advertiser == "") err.push("Specify an advertiser"); if(v.meta_agency == "") err.push("Specify an agency"); if(v.meta_prodcomp == "") err.push("Specify a production company"); if(v.meta_email== "") err.push("Specify an email address"); // INVALID CHARS if(!validText(v.meta_title)) err.push("Title name contains invalid characters"); if(!validText(v.meta_product)) err.push("Product name contains invalid characters"); if(!validText(v.meta_advertiser)) err.push("Advertiser contains invalid characters"); if(!validText(v.meta_agency)) err.push("Agency name contains invalid characters"); if(!validText(v.meta_prodcomp)) err.push("Production company name contains invalid characters"); if(!validText(v.meta_comments)) err.push("Comments name contains invalid characters"); // IF THE META DATA FAILS, NO NEED TO CONTINUE if(err.length > 0) continue; // ----------------------------- // CHECK IF VIDEO IS SET AND EXIST if( v.video == PSS || !fileExists(v.video )){ err.push("Select a video file"); continue; } // ----------------------------- // VIDEO SPECS setMainMessage("Analyzing source video"); var specs = filespecs(v.video); // DURATION VIDEO if( specs.duration%5 != 0) err.push("Invalid length ("+specs.duration+'sec.)'); // CHECK HEIGHT AND WIDTH if( specs.streams.video[0].height != 1080 || specs.streams.video[0].width != 1920) wrn.push("Invalid resolution "+specs.streams.video[0].height+"x"+specs.streams.video[0].width); // CHECK PIXEL FORMAT if( ['yuv422p10le','yuv422p','yuvj420p'].indexOf(specs.streams.video[0].pix_fmt) == -1 ) wrn.push("Invalid pixel format ("+specs.streams.video[0].pix_fmt+")"); // CHECK FRAMERATE if( specs.streams.video[0].r_frame_rate != '25/1' ) wrn.push("Invalid frame rate"); // USED FOR PROGRESS DURING PROCESSING IN FFMPEG framecount = specs.duration * 25; // ----------------------------- // FILENAME var filename = v.meta_product+'_'+v.meta_title+'_'+parseInt(specs.duration)+'_version'+v.meta_version+'_'+v.meta_date+'_HD'; var mxffile = v.export_folder+PSS+filename+".mxf"; var xmlfile = v.export_folder+PSS+filename+".xml"; var mp4file = v.export_folder+PSS+filename+".mp4"; var extrafile1 = v.export_folder+PSS+filename+".001"; var extrafile2 = v.export_folder+PSS+filename+".002"; var extrafile3 = v.export_folder+PSS+filename+".003"; var extrafile4 = v.export_folder+PSS+filename+".004"; var extrafile5 = v.export_folder+PSS+filename+".005"; if( fileExists(mxffile) ){ err.push('The MXF file exists!'); continue; } if( fileExists(xmlfile) ){ err.push('The xml file exists!'); continue; } if( fileExists(mp4file) ){ err.push('The mp4 file exists!'); continue; } // ----------------------------- // CHECK STEREO AUDIO FILE IS PROVIDED var stereofile = false; if( v.stereo != "" && v.stereo != PSS ) { stereofile = v.stereo; setMainMessage("Analyzing audio (stereo)"); var x = filespecs(stereofile); var dur = x.duration; var stream = x.streams.audio[0]; if(parseFloat(dur) != parseFloat(specs.duration)) err.push('audio and video is not same length'); if( ['pcm_s24le','pcm_s24be'].indexOf(stream.codec_name) == -1 ) wrn.push('stereo: codec ('+stream.codec_name+')'); if( parseInt(stream.sample_rate) != 48000) wrn.push('stereo: sample rate ('+(stream.sample_rate/100.)+"kHz)"); if( parseInt(stream.bits_per_sample) != 24) wrn.push('stereo: bit rate ('+stream.bits_per_sample+"bits)"); } // ----------------------------- // CHECK SURROUND AUDIO FILE IS PROFIDED var surroundfile = false; if( v.surround != "" && v.surround != PSS ) { surroundfile = v.surround; setMainMessage("Analyzing audio (surround)"); var x = filespecs(surroundfile); var dur = x.duration; var stream = x.streams.audio[0]; if(parseFloat(dur) != parseFloat(specs.duration)) err.push('audio and video is not same length'); if( ['pcm_s24le','pcm_s24be'].indexOf(stream.codec_name) == -1 ) wrn.push('surround: codec ('+stream.codec_name+')'); if( parseInt(stream.sample_rate) != 48000) wrn.push('surround: sample rate ('+(stream.sample_rate/100.)+"kHz)"); if( parseInt(stream.bits_per_sample) != 24) wrn.push('surround: bit rate ('+stream.bits_per_sample+"bits)"); } // ----------------------------- // CHECK EMBEDDED AUDIO IF NEEDED if( !stereofile || !surroundfile ) { var x = specs.streams.audio; // IF THERE IS AT LEAST ONE EMBEDDED AUDIO STREAM if( x.length > 0) { // Check Audio Codec for all streams for (var i = 0; i < x.length; i++) { if( ['pcm_s24le','pcm_s24be'].indexOf(x[i].codec_name) == -1 ) { wrn.push('embedded: codec ('+x[i].codec_name+')'); continue; } } // Check Sample Rate for all streams for (var i = 0; i < x.length; i++) { if( parseInt(x[i].sample_rate) != 48000) { wrn.push('embedded: sample rate ('+(x[i].sample_rate/100.)+"kHz)"); continue; } } // Check Bith Depth for all streams for (var i = 0; i < x.length; i++) { if( parseInt(x[i].bits_per_sample) != 24){ wrn.push('embedded: bit rate ('+x[i].bits_per_sample+"bits)"); continue; } } // ---------------- // IF THERE IS ONLY ONE STREAM IN THE EMBEDDED FILE if(x.length == 1) { // if( !stereofile ){ if(!fileExists(folders.temp)) makeFolder(folders.temp); setMainMessage("Preparing Stereo Audio"); cmd( "ffmpeg", [ '-hide_banner', '-threads','0', '-i',v.video, '-vn', '-acodec','pcm_s24le', '-ac','2', '-ar','48000', tempStereo, '-y' ]); stereofile = ( fileExists(tempStereo) ? tempStereo : false ) ; } } // ---------------- // IF THERE ARE 8 MONO STREAMS IN THE EMBEDDED FILE else if(x.length == 8) { // IF NO STEREO FILE IS PROVIDED, EXTRACT THE EMBEDDED AUDIO STREAM if(!stereofile){ if(!fileExists(folders.temp)) makeFolder(folders.temp); setMainMessage("Preparing Stereo Audio"); cmd( "ffmpeg", [ '-hide_banner', '-threads','0', '-i',v.video, '-filter_complex','[a:0][a:1]amerge=inputs=2', '-vn', '-acodec','pcm_s24le', '-ac','2', '-ar','48000', tempStereo, '-y']); setProgress(100); stereofile = ( fileExists(tempStereo) ? tempStereo : false ) ; } // IF NO SURROUND FILE IS PROVIDED, EXTRACT THE EMBEDDED AUDIO STREAM if(!surroundfile){ if(!fileExists(folders.temp)) makeFolder(folders.temp); setMainMessage("Preparing Surround Audio"); cmd( "ffmpeg", [ '-hide_banner', '-threads','0', '-i',v.video, '-filter_complex','[a:2][a:3][a:4][a:5][a:6][a:7]amerge=inputs=6', '-vn', '-acodec','pcm_s24le', '-ac','6', '-ar','48000', tempSurround, '-y']); setProgress(100); surroundfile = ( fileExists(tempSurround) ? tempSurround : false ) ; } } // FOR ANY OTHER STREAM LAYOUT, GIVE AN ERROR else { err.push('Invalid audio layout'); continue; } } } // ----------------------------- // CHECK LOUDNESS var settings = {}; settings["AdjustTargetType"] = "LU"; settings["AdjustTargetLevel"] = 0.; settings["RelativeGate"] = -10.; settings["DialogGate"] = false; settings["CalibrationLU"] = -23.0; v.low_loudness = false; // CHECK STEREO LEVELS if( stereofile != false ){ var levels = levelFileAnalyse(stereofile, settings); var fix = 0; // CHECK IF FILE NEEDS FIXING if( levels.LUFS > -22.1 ) { fix++; wrn.push('stereo: Loudness exceeds -23LUFS ('+toFixed(levels.LUFS, 1)+"LUFS)");} if( levels.Peak > -1.1 ) { fix++; wrn.push('stereo: Max Peak Levels exceed -1dBFS ('+toFixed(levels.Peak, 1)+")");} // if( levels.maxM > 7.9) { wrn.push('stereo: Max M exceeds 8LU');} if( levels.maxS > 4.9) { wrn.push('stereo: Max S exceeds +5LU');} // LOW LOUDNESS IF ALLOWED if( levels.LUFS < -23.9 ) { if(v.allow_lowloudness){ v.low_loudness = true; } else{ fix++; wrn.push('stereo: Low loudness ('+toFixed(levels.LUFS,1)+'LUFS)'); } } // PROCESS THE FILE IF NEEDED if(fix > 0 && v.ignore_errors ) { levels["NeedLimiting"] = true; levels["LimitThresholddB"] = -1.; levelFileProcess(tempStereo, tempStereoFix, levels); if( fileExists(tempStereoFix) ) stereofile = tempStereoFix; } } // AT THIS POINT THERE SHOULD BE A VALID STEREO FILE else { err.push('No stereo audio'); continue; } // CHECK SURROUND LEVELS // MULTICHANNEL IS OPTIONAL BU SHOULD BE SET IN THE XML // IF THERE IS NO MULTI CHANNEL THE CHANNELS SHOULD STILL // BE PRESENT BUT MUTED! v.multichan = false; if( surroundfile != false ) { var levels = levelFileAnalyse(surroundfile, settings); if(levels.LUFS > -99) { v.multichan = true; var fix = 0; // if( levels.LUFS > -22.1 ) { fix++; wrn.push('surround: Loudness exceeds -23LUFS ('+toFixed(levels.LUFS, 1)+"LUFS)");} if( levels.Peak > -1.1 ) { fix++; wrn.push('surround: Max Peak Levels exceed -1dBFS ('+toFixed(levels.Peak, 1)+")");} // if( levels.maxM > 7.9) { wrn.push('surround: Max M exceeds 8LU');} if( levels.maxS > 4.9) { wrn.push('surround: Max S exceeds +5LU');} if( levels.LUFS < -23.9 ) { if(v.allow_lowloudness){ v.low_loudness = true; } else{ fix++; wrn.push('surround: Low loudness ('+toFixed(levels.LUFS,1)+'LUFS)'); } } // PROCESS THE FILE IF NEEDED if(fix > 0 && v.ignore_errors ) { levels["NeedLimiting"] = true; levels["LimitThresholddB"] = -1.; levelFileProcess(tempSurround, tempSurroundFix, levels); if( fileExists(tempStereoFix) ) surroundfile = tempSurroundFix; } } } // ----------------------------- // CALCULATE THE START OF THE LAST FRAME // THIS IS THE START POINT OF THE LAST FRAME, // SO FOR A 20SEC FILM, IT IS 00:00:19:24 var tc_out = endframe(specs.duration); // ----------------------------- // ERRORS CAN NEVER BE FIXED, WARNINGS CAN BE IGNORED if(err.length == 0 && (wrn.length == 0 || v.ignore_errors) ) { // ----------------------------- // MAKE THE XML generateXML(xmlfile, v, specs, tc_out); // MAKE THE MXF processMXF(mxffile, v, stereofile, surroundfile, specs); // MAKE THE MP4 createMP4(mp4file, mxffile); // COPY EXTRA FILES if(v.extra1 != ""){ copyFile(v.extra1, extrafile1); } if(v.extra2 != ""){ copyFile(v.extra2, extrafile2); } if(v.extra3 != ""){ copyFile(v.extra3, extrafile3); } if(v.extra4 != ""){ copyFile(v.extra4, extrafile4); } if(v.extra5 != ""){ copyFile(v.extra5, extrafile5); } // ----------------------------- // CHECK IF FILE WAS CREATED if( fileExists(xmlfile) && fileExists(mxffile) ){ var files = getFiles(); files.push({ "path": mxffile, 'file_icon' : "CheckSquare", 'file_icon_color' : "FF009900", 'file_status' : "Done", }); setFiles(files); // REMOVE TEMPORARY FILES if( fileExists(tempStereo) ) deleteFile(tempStereo); if( fileExists(tempSurround) ) deleteFile(tempSurround); if( fileExists(tempStereoFix) ) deleteFile(tempStereoFix); if( fileExists(tempSurroundFix) ) deleteFile(tempSurroundFix); break; // BREAK THE LOOP } // IF ERROR else{ err.push("File could not be created"); } } // ----------------------------- // SHOW ERRORS! } // ----------------------------- // ALL DONE setMainMessage("-"); setProgress(0); return true; } // ============================================================================= // DIALOG CALLBACK function dialog_callback(props) { // ---------------- // RESET BUTTONS if (props["name"] == "stereo_reset") var r = dialog({ "stereo" : { "path" : "" } }); if (props["name"] == "surround_reset") var r = dialog({ "surround" : { "path" : "" } }); if (props["name"] == "extra1_reset") var r = dialog({ "extra1" : { "path" : "" } }); if (props["name"] == "extra2_reset") var r = dialog({ "extra2" : { "path" : "" } }); if (props["name"] == "extra3_reset") var r = dialog({ "extra3" : { "path" : "" } }); if (props["name"] == "extra4_reset") var r = dialog({ "extra4" : { "path" : "" } }); if (props["name"] == "extra5_reset") var r = dialog({ "extra5" : { "path" : "" } }); // ---------------- // RELOAD DIALOG FOR SHOW/HIDE TOGGLES if (props["name"] == "show_optional" || props["name"] == "show_extra") { dialog({ "quit" : { "type" : "return", "value" : 2 }, }); } // ---------------- // WHEN YOU SELECT ANOTHER VIDEO INPUT FILE if (props["name"] == "video") { var path = getPathInfo(props["path"]); var outFile = path.folder+path.sep+path.basename+'-[replaced].'+path.ext; // CHECK IF THAT VIDEO ALLREADY HAS THE PROPER NAME FORMATTING // IF SO, WE CAN USE THIS TO SET SOME OF THE META DATA var el = getFilename(path.basename); var args = {}; if(el){ mergeObject(args,{ "meta_product" : { "default" : el.product }, "meta_title" : { "default" : el.title }, "meta_length" : { "default" : el.length }, "meta_version" : { "default" : el.version}, "meta_date" : { "default" : el.date }, }); } // CHECK IF AN OUTPUT WAS SET IN THE DEFAULT SETTINGS FILE // IF NOT, UPDATE IT TO THE ROOT OF THE VIDEO if(updateExportFolder){ mergeObject(args,{ "export_folder" : { "path" : path.folder } }); } // UPDATE DIALOG IF NEEDED if(getObjectSize(args) > 0){ dialog(args); } } // ---------------- } // ============================================================================= // FFMPEG PROCESS STATUS function cmd_callback(cmd_name, cmd_output){ if(cmd_name=="ffmpeg" && cmd_output.length > 0 && framecount > 0){ var progress = parseInt ( searchRegEx(cmd_output, "(?:frame=)(.*?)(?:fps=)", "i", 1)[0] ); progress = Math.round( progress/framecount* 100 ); setProgress(progress); } else{ setProgress(101); } return { "terminate": isCanceled(), // use this option to end the process if there is no better alternative "input": "" // send console input, like a quit signal (followed by a 'return' 0x0D) }; } // ============================================================================= // this funtion is called when levelFileAnalyse is busy as part of TestLevel() // this function gets called when busy LevelFileAnalyse. progress 0. to 1. function LevelFileAnalyse_Progress(file, progress) { setMainMessage("Analyzing Audio"); setProgress(progress*100.); } // ============================================================================= // this funtion is called when levelFileProcess is busy as part of TestLevel() // this function gets called when busy LevelFileProcess. progress 0. to 1. function LevelFileProcess_Progress(file, progress) { setMainMessage("Processiong Audio"); setProgress(progress*100.); } // ============================================================================= // EXPLODE FILENAME AND CHECK IF IT COULD BE A VALID // FORMATTED FILENAME. IF SO WE CAN SUBSTRACT SOME VALUES function getFilename(filename) { // IF NOT VALID FILENAME var rtn = false; // CHECK VALID FILENAME var el = filename.split('_'); if(el.length == 6){ rtn = { 'product' : el[0], 'title' : el[1], 'length' : el[2], 'version' : el[3].split("-")[1], // parseInt(el[3]), version- 'date' : el[4], }; } return rtn; } // ============================================================================= // FILE SPECS // ============================================================================= function filespecs(file) { setProgress(101); // ---------------------------------------- // GET VIDEO SPECS var ffprobe= cmd("ffprobe", [ '-v','error', // '-select_streams','v', // '-show_entries','stream=codec_type,codec_name,pix_fmt,width,height,r_frame_rate', '-show_entries','stream=codec_type,pix_fmt,width,height,r_frame_rate,codec_name,channels,sample_rate,bits_per_sample : format=duration', // '-of','default=noprint_wrappers=1', '-i',file ]); var streams = { audio: [], video: [], }; // DURATION var duration = parseFloat ( searchRegEx(ffprobe, "(?:duration=)(.*?)(?=[\n\r])", "i", 1)[0] ); // UGGLY FIX BECAUSE replaceRegEx DOESNT KNOW HOW TO HANDLE NEW LINES ffprobe = replaceRegEx(ffprobe, '[\n\r]', ',', "i"); // EXPLODE INTO STREAMS var ffprobe = searchRegEx(ffprobe, "(?:\\[STREAM\\],)(.*?)(?=,\\[\\/STREAM\\])", "i", 1); // LOOP OVER STREAMS for (var i = 0; i < ffprobe.length; i++) { // EXPLODE ENTRIES var entries = ffprobe[i].split(','); // STREAM DATA var stream = {}; for (var j = 0; j < entries.length; j++) { var l = entries[j].split('='); if ( l[0] == "codec_name" && l[0] != "unknown" ) l = { "codec_name" : l[1] }; else if ( l[0] == "codec_type" ) l = { "codec_type" : l[1] }; else if ( l[0] == "sample_rate" ) l = { "sample_rate" : l[1] }; else if ( l[0] == "channels" ) l = { "channels" : l[1] }; else if ( l[0] == "bits_per_sample" ) l = { "bits_per_sample" : l[1] }; else if ( l[0] == "width" ) l = { "width" : l[1] }; else if ( l[0] == "height" ) l = { "height" : l[1] }; else if ( l[0] == "pix_fmt" ) l = { "pix_fmt" : l[1] }; else if ( l[0] == "r_frame_rate" && l[0] != "0/0") l = { "r_frame_rate" : l[1] }; stream = mergeObject( stream, l ); } echo( objectToString(stream) ); // GROUP AUDIO AND VIDEO STREAMS if( stream.codec_type == "audio") { streams.audio.push(stream); } else if( stream.codec_type == "video") { streams.video.push(stream); } } setProgress(0); // ---------------------------------------- return { 'streams' : streams, 'duration' : duration }; // ---------------------------------------- } // ============================================================================= // CALCULATE END FRAME // ----------------------------------------------------------------------------- // the last frame can be calculated from the start tc and length // this is the frame before the last second // ============================================================================= function endframe(sec) { // frame rate frate = 25; // last frame number lastframe = ( parseInt(sec)* frate ) -1; if (lastframe > 0 && lastframe < 9000000){ var hr = parseInt(Math.floor(lastframe / (3600 * frate))); var min = parseInt(Math.floor(lastframe - (hr * 3600 * frate)) / (60 * frate)); var sec = parseInt(Math.floor(lastframe - (hr*3600*frate) - (min*60*frate)) / frate); var fr = parseInt(Math.floor(lastframe - (hr*3600*frate) - (min*60*frate) - (sec*frate))); // PRECEDING ZEROS if (hr < 10) { hr = "0" + hr; } if (min < 10) { min = "0" + min; } if (sec < 10) { sec = "0" + sec; } if (fr < 10) { fr = "0" + fr; } if (hr) { hr = "00"; } return hr+":"+min+":"+sec+":"+fr; } else{ return false; } } // ============================================================================= // VALID TEXT // ----------------------------------------------------------------------------- // check for invalid characters in a string // ============================================================================= function validText(v) { var invalid = replaceRegEx(v, "[\-\ A-Za-z0-9]", '', 'i').length; if(invalid > 0) return false; return true; } // ============================================================================= // GENERATE XML // ============================================================================= function generateXML(xmlfile, v, specs, tc_out) { setMainMessage("Generating XML"); setProgress(101); var extra1 = ( v.extra1 != "" ? getPathInfo(v.extra1) : { "filename" : "" } ); var extra2 = ( v.extra2 != "" ? getPathInfo(v.extra2) : { "filename" : "" } ); var extra3 = ( v.extra3 != "" ? getPathInfo(v.extra3) : { "filename" : "" } ); var extra4 = ( v.extra4 != "" ? getPathInfo(v.extra4) : { "filename" : "" } ); var extra5 = ( v.extra5 != "" ? getPathInfo(v.extra5) : { "filename" : "" } ); var xml = ''; xml += '\n'; xml += '\n\t'; xml += '\n\t'+v.meta_product+''; xml += '\n\t'+v.meta_title+''; xml += '\n\t'+v.meta_version+''; xml += '\n\t'+v.meta_date+''; xml += '\n\t00:00:00:00'; xml += '\n\t'+tc_out+''; xml += '\n\t'+parseInt(specs.duration)+''; xml += '\n\t16F16'; xml += '\n\t'+v.meta_advertiser+''; xml += '\n\t'+v.meta_agency+''; xml += '\n\t'+v.meta_prodcomp+''; xml += '\n\t'+v.meta_project+''; xml += '\n\t'+v.meta_email+''; xml += '\n\t'+extra1.filename+''; xml += '\n\t'+extra2.filename+''; xml += '\n\t'+extra3.filename+''; xml += '\n\t'+extra4.filename+''; xml += '\n\t'+extra5.filename+''; xml += '\n\t'+v.meta_comments+''; xml += '\n\tTRUE'; xml += '\n\t'+(v.multichan ? "TRUE" : "FALSE")+''; xml += '\n\t'+(v.low_loudness ? "TRUE" : "FALSE")+''; xml += '\n'; writeFile(xmlfile, xml); setProgress(0); } // ============================================================================= // PROCESS MXF FILE // ============================================================================= function processMXF(mxffile, videofile, stereofile, surroundfile, specs) { // -------------------------------- // IF SURROUND FILE if(surroundfile != false) { var mapping = [ '-i',stereofile, '-i',surroundfile, '-map','1:0', '-map','1:0', '-map','2:0', '-map','2:0', '-map','2:0', '-map','2:0', '-map','2:0', '-map','2:0', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','1.0.0:0.1', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','1.0.1:0.2', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','2.0.0:0.3', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','2.0.1:0.4', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','2.0.2:0.5', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','2.0.3:0.6', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','2.0.4:0.7', '-ar','48000', '-c:a','pcm_s24le', '-map_channel','2.0.5:0.8', ]; } // -------------------------------- // IF NO SURROUND FILE else { var mapping = [ '-i',stereofile, '-f','lavfi', '-i','anullsrc=cl=mono:sample_rate=48000', '-acodec','pcm_s24le', '-ar','48000', '-filter_complex','[1:a]channelsplit=channel_layout=stereo[left][right]', '-map','0:v', '-map','[left]', '-map','[right]', '-map','2:a', '-map','2:a', '-map','2:a', '-map','2:a', '-map','2:a', '-map','2:a', ]; } // -------------------------------- // CALL var ff = [ '-hide_banner', '-threads','0', '-i',v.video ]; // AUDIO CHANNEL MAPPING ff = concatArray(ff, mapping); // ENDING ff = concatArray(ff, [ '-filter_complex','setfield=1', '-r','25', '-flags','+ilme+ildct', '-top','1', '-c:v','mpeg2video', '-b:v','50000k', '-minrate','50000k', '-maxrate','50000k', '-bufsize','3835k', '-pix_fmt','yuv422p', '-profile:v','0', '-level:v','2', '-t',specs.duration, '-timecode','00:00:00:00', mxffile, '-y' ]); // -------------------------------- // lets go setMainMessage("Generating MXF"); cmd( "ffmpeg", ff); // -------------------------------- } // ============================================================================= // MP4 PREVIEW // ----------------------------------------------------------------------------- // // ============================================================================= function createMP4(mp4file, mxffile) { var call = [ '-hide_banner', '-threads','0', '-i',mxffile, '-filter_complex','[0:1][0:2]amerge=inputs=2', '-c:v','libx264', '-pix_fmt','yuv420p', '-preset','slow', '-crf','22', '-c:a','aac', '-ac','2', '-strict','-2', '-ab','128k', '-profile:v','main', '-level','3.1', '-movflags','faststart', '-aspect','16:9', mp4file, '-y' ]; // -------------------------------- setMainMessage("Generating MP4"); cmd( "ffmpeg", call); } // ============================================================================= // SETVAL // ----------------------------------------------------------------------------- // // ============================================================================= function setVal(v, def){ if( v === undefined) return def; if( v === "/") return ""; else return v; } // ============================================================================= // SHOW DIALOG // ----------------------------------------------------------------------------- // // ============================================================================= function showDialog(v) { var form = {}; mergeObject(form, { // VIDEO "video_label" : { "type" : "text", "label" : "video file", "just" : "l", "bounds" : { "w" : 100 }, }, "video" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "*.mov, *.mp4", "label" : "video file", "bounds" : { "y" : -1, "x": 170, "w" : 410 }, "path" : v.video, }, // STEREO "stereo_label" : { "type" : "text", "label" : "stereo audio*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "stereo" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "*.aif, *.aiff, *.wav", "label" : "audio file", "bounds" : { "y": -1, "x": 170, "w" : 367 }, "path" : v.stereo, }, "stereo_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 537, "w" : 43 }, }, }); if(v.show_optional) { mergeObject(form, { "surround_label" : { "type" : "text", "label" : "surround audio*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "surround" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "*.aif, *.aiff, *.wav", "label" : "audio file", "bounds" : { "y": -1, "x": 170, "w" : 367 }, "path" : v.surround, }, "surround_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 537, "w" : 43 }, }, "spacer2" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 570 }, }, }); } // -------------------------------- // EXPORT FOLDER mergeObject(form, { "title_export" : { "type" : "text", "label" : "export folder", "just" : "l", "bounds" : { "x": 20, "w" : 150}, }, "export_folder" : { "type" : "fileselect", "editable" : false, "dir" : true, "saving" : true, "label" : "folder", "bounds" : { "y": -1, "x": 170, "w" : 410 }, "path" : v.export_folder, }, }); // -------------------------------- // VALIDATION if(v.show_optional) { mergeObject(form, { "lowloudness_label" : { "type" : "text", "label" : "allow lowloudness", "just" : "l", "bounds" : { "x": 20, "w" : 200 }, }, "lowloudness": { "type": "togglebox", "bounds" : { "y": -1, "x": 170, "w" : 300 }, "label" : "indicate low loudness as intentionally", "toggle" : v.allow_lowloud }, // CONTINUE ON ERRORS "ignore_errors_label" : { "type" : "text", "label" : "fix errors", "just" : "l", "bounds" : { "x": 20, "w" : 200 }, }, "ignore_errors": { "type": "togglebox", "bounds" : { "y": -1, "x": 170, "w" : 30 }, "label" : " ", "toggle" : v.ignore_errors }, }); } // -------------------------------- mergeObject(form, { // -------------------------------- // META DATA "spacer3" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 550 }, }, // PRODUCT "product_label" : { "type" : "text", "label" : "product", "just" : "l", "bounds" : { "x": 20, "w" : 100 }, }, "meta_product": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_product, }, // TITLE "title_label" : { "type" : "text", "label" : "title", "just" : "l", "bounds" : { "x": 20, "w" : 100 }, }, "meta_title": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_title, }, // DATE "date_label" : { "type" : "text", "label" : "date", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_date": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 100, }, "default" : v.meta_date, }, // VERSION "version_label" : { "type" : "text", "label" : "version", "just" : "l", "bounds" : { "y": -1, "x": 290, "w" : 150 }, }, "meta_version": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 440, "w" : 100, }, "default" : v.meta_version, }, // ADVERTISER "advertiser_label" : { "type" : "text", "label" : "advertiser", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_advertiser": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_advertiser, }, // AGENCY "agency_label" : { "type" : "text", "label" : "agency", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_agency": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_agency, }, // PRODUCTION COMPANY "prodcomp_label" : { "type" : "text", "label" : "prod. company", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_prodcomp": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_prodcomp, }, }); // -------------------------------- if(v.show_optional) { mergeObject(form, { // COMMENTS "comments_label" : { "type" : "text", "label" : "comments", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_comments": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_comments, }, }); } // -------------------------------- mergeObject(form, { // EMAIL "email_label" : { "type" : "text", "label" : "email", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_email": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_email, }, }); // -------------------------------- // PROJECT if(v.show_optional) { mergeObject(form, { "project_label" : { "type" : "text", "label" : "project*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "meta_project": { "type": "texteditor", "default" : "", "bounds" : { "y": -1, "x": 170, "w" : 410, }, "default" : v.meta_project, }, }); } // -------------------------------- if(v.show_optional) { mergeObject(form, { "spacer4" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 550 }, }, // Extra file 1 "extra1_label" : { "type" : "text", "label" : "extra file*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "extra1" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "", "label" : "select file", "bounds" : { "y": -1, "x": 170, "w" : 357 }, "path" : v.extra1, }, "extra1_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 527, "w" : 43 }, }, // Extra file 2 "extra2_label" : { "type" : "text", "label" : "extra file*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "extra2" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "", "label" : "select file", "bounds" : { "y": -1, "x": 170, "w" : 357 }, "path" : v.extra2, }, "extra2_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 527, "w" : 43 }, }, // Extra file 3 "extra3_label" : { "type" : "text", "label" : "extra file*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "extra3" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "", "label" : "select file", "bounds" : { "y": -1, "x": 170, "w" : 357 }, "path" : v.extra3, }, "extra3_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 527, "w" : 43 }, }, // Extra file 4 "extra4_label" : { "type" : "text", "label" : "extra file*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "extra4" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "", "label" : "select file", "bounds" : { "y": -1, "x": 170, "w" : 357 }, "path" : v.extra4, }, "extra4_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 527, "w" : 43 }, }, // Extra file 5 "extra5_label" : { "type" : "text", "label" : "extra file*", "just" : "l", "bounds" : { "x": 20, "w" : 150 }, }, "extra5" : { "type" : "fileselect", "editable" : false, "dir" : false, "saving" : false, "wildcard" : "", "label" : "select file", "bounds" : { "y": -1, "x": 170, "w" : 357 }, "path" : v.extra5, }, "extra5_reset" : { "type" : "button", "label" : "X", "bounds" : { "y": -1, "x": 527, "w" : 43 }, }, }); } // -------------------------------- // BUTTONS & ERRORS mergeObject(form, { "spacer5" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 550 }, }, "optional_label" : { "type" : "text", "label" : "show more fields", "just" : "l", "bounds" : { "x": 420, "w" : 150 }, }, "show_optional": { "type": "togglebox", "bounds" : { "y" : -1, "x": 555, "w" : 30 }, "visible": true, "label" : "", "toggle" : v.show_optional }, "cancel" : { "type" : "button", "label" : "cancel", "returns" : 0, "bounds" : { "x": 370, "w" : 100 }, }, "ok" : { "type" : "button", "label" : "Create", "returns" : 1, "bounds" : { "y": -1, "x": 480, "w" : 100 }, }, "spacer6" : { "type" : "text", "label" : "", "just" : "r", "bounds" : { "x": 20, "w" : 550 }, }, }); // -------------------------------- // SHOW THE DIALOG var title = "ProRes to Spot MXF / XML"; var msg = ( v.errors != "" ? v.errors : "Specify a video and the meta data fields shown below"+ "\nfield with * are optional"+ "\n" ); return dialog(title, msg, "w", form); }