Принято считать, что для большого объёма рутинных работ по перекодированию обычно берут например ffmpeg или vlc. И то и другое довльно нудные и капризные утилиты. Да и влом мне было изучать параметры их командной строки, так как в изначальном материале присутствовали атрибуты эфирного вещания: кроме разрешения, ещё и чересстрочная картинка. Таким образом требовалось не только кодировать видео, но и предварительно убрать interlacing, то есть наложить фильтр.
В результате тестирования, выяснилось, что deinterlace-фильтры у vlc как ни старайся в более-менее динамических сценах оставляют т. н. Расчёску. Однако у avidemux есть tdeint — отличный фильтр, который практически лишён этого недостатка.
Итак, решено — используем avidemux, в качестве утилиты для перекодирования. Avidemux поддерживает автоматизацию, скажу больше, в него встроена поддержка языка javascript для этих целей, что конечно является overkill-ом в данном случае, ибо движком js задействован spidermonkey от mozilla. Правда глубоко погружаться в дебри js от нас не требуется, что радует, ибо уже в wiki есть полуработающий пример нужного нам скрипта.
Заглядываем на
http://www.avidemux.org/admWiki/doku.php?id=tutorial:scripting_tutorial
http://www.avidemux.org/admWiki/doku.php?id=using:scripting
http://www.avidemux.org/admWiki/doku.php?id=tutorial:batch_processing
это всё что (нам нужно знать) есть по скриптованию на js для рутинной обработки видео в avidemux.
Есть тонкость - avidemux имеет неприятную особенность перед каждым роликом из последовательности роликов объявлять, что нашёл серию эпизодов и предлагать объединить в один большой файл. Этого нам не надо. Впрочем, даже если оно и понадобилось, всё ровно при перекодировании каждого ролика отвечать на этот вопрос как-то не хочется - какая же тогда автоматизация получается? специально для таких целей разработчики avidemux придумали опцию --nogui, она отключает интерактивные окошки и в случае вопроса обединения отвечает, что объединять серию эпизодов в один файл не надо. Но (как всегда есть своё "но") эта опция работает только для gui-версии перекодировщика, а для cli - нет . А gui-версии в свою очередь похищают фокус на каждом новом задании, то есть всплывают на активный десктоп на передний план. Иными словами работать на компе будет совсем неприятно.
Итак, сам скрипт:
//AD <-
/*
Simple script that scans the orgDir directory
and loads all .xyz files and encodes them to Xvid+MP3 AVI
The resulting file is put in destDir directory
Using new directorySearch API
*/
var app = new Avidemux();
var ds = new DirectorySearch();
var srcDir;
var dstDir;
var reg = new RegExp(".$");
var extReg = new RegExp(".*[.](.+)$");
//this is the directory separator char for WINDOWS! for *nix, it should be: sep = "/";
var sep = "/";
var fileExt;
var pickedExt;
var dstPath;
var filesDone = 0;
var filesSkip = 0;
var filesErrd = 0;
//displayInfo("Pick any file in the source directory. All files of that type (.xyz) will be processed. Filename will be ignored but the file extension will be used! \r\n\r\ne.g. If you want to process AVIs, be sure to pick any .avi file!");
//pop up a dialog asking for a source file. We will remove the file name leaving just the dir
//srcDir = fileReadSelect();
srcDir = "/mnt/array/Public/Фильмы/Cartoons/series"
//run the extReg regex designed to pull out just the extension of this file
//pickedExt = srcDir.replace(extReg, "$1").toLowerCase();
pickedExt = "mpg";
//displayInfo(pickedExt + "Pick the destination directory and enter a random filename. Filename will be ignored");
//pop up a dialog asking for a destination file. We will remove the file name leaving just the dir
//dstDir = fileWriteSelect();
dstDir = "/mnt/array/Public/Фильмы/Cartoons/asd"
//extract just the path part of each picked file
//srcDir = pathOnly(srcDir);
//dstDir = pathOnly(dstDir);
//if the path ends with a directory separator characters, remove them
//it shouldnt be more than one, but a while loop will cater for any number of them
//so c:\temp\\\\ becomes c:\temp
while(srcDir.charAt(srcDir.length-1) == sep){
srcDir=srcDir.replace(reg,"");
}
while(dstDir.charAt(dstDir.length-1) == sep){
dstDir=dstDir.replace(reg,"");
}
//initalise the directory search by telling it the directory
if(ds.Init(srcDir))
{
//while we havent reached the end of the files list in the directory
// --> note that every call to ds.NextFile() moves it on by one
// --> until ds.NextFIle() returns false meaning it finished the list
while(ds.NextFile())
{
//pull the filename out (no path)
dstPath=ds.GetFileName();
//only process valid files that are not directories
if(!ds.isNotFile && !ds.isDirectory && !ds.isSingleDot && !ds.isDoubleDot)
{
//we want to do some work with the extension so pull it out now
ext=ds.GetExtension();
//is the extension of this file the same as the one we picked?
if(ext.toLowerCase() == pickedExt)
{
//build the output file name. To avoid ovwriting source file, if dir is the same prefix a _
if(srcDir.toLowerCase() == dstDir.toLowerCase()){
dstPath = dstDir + sep + "_" + ds.GetFileName() + ".mp4";
}else{
dstPath = dstDir + sep + ds.GetFileName() + ".mp4";
}
//build the source file path
srcPath = srcDir + sep + ds.GetFileName();
//we set this option first. If Avidemux loads a file it thinks need unpacking/time mapping
//then setting this option now will ensure that operation is done when we load
//app.forceUnpack();
//load the file. this action also purges all the video filters and codec settings
app.load(srcPath);
app.append("/dev/null");
//see if the audio is VBR. if it is, this builds a time map. if it is not, this does nothing
app.audio.scanVbr();
//this might not need doing if forceUnpack has been stated. I dont think it can be done
//twice though so its safe to repeat here
app.rebuildIndex();
//** Postproc **
app.video.setPostProc(3,3,0);
//** Filters **
app.video.addFilter("tdeint","mode=0","order=1","field=1","mthreshL=6","mthreshC=6","map=0","type=2","debug=0","mtnmode=1","sharp=0","full=1","cthresh=6","blockx=16","blocky=16","chroma=0","MI=64","tryWeave=0","link=2","denoise=1","AP=254","APType=1");
//all the code in this section was generated by the app. if you want to write a script and use filters
//you can get the app to do the hard work by:
// load one of the files you will script
// set all the video and audio options you want
// on file menu, pick Save Project as (note: NOT save avi file!!)
//the job file you save will have a section detailing the filters. copy it
//re-encode to xvid 2 pass, bitrate = 1000 (plus 128 for audio = 1128 kbps, 507.6 megs per hour)
//all options are set to simple, no GMC, no Qpel etc so that standalone simple players are ok
app.video.codecPlugin("32BCB447-21C9-4210-AE9A-4FCE6C8588AE", "x264", "AQ=26", "<?xml version='1.0'?><x264Config><x264Options><fastFirstPass>true</fastFirstPass><threads>0</threads><deterministic>true</deterministic><sliceThreading>false</sliceThreading><threadedLookahead>-1</threadedLookahead><idcLevel>-1</idcLevel><vui><sarAsInput>true</sarAsInput><sarHeight>1</sarHeight><sarWidth>1</sarWidth><overscan>undefined</overscan><videoFormat>undefined</videoFormat><fullRangeSamples>true</fullRangeSamples><colorPrimaries>undefined</colorPrimaries><transfer>undefined</transfer><colorMatrix>smpte240m</colorMatrix><chromaSampleLocation>0</chromaSampleLocation></vui><referenceFrames>3</referenceFrames><gopMaximumSize>250</gopMaximumSize><gopMinimumSize>10</gopMinimumSize><scenecutThreshold>40</scenecutThreshold><periodicIntraRefresh>true</periodicIntraRefresh><bFrames>3</bFrames><adaptiveBframeDecision>2</adaptiveBframeDecision><bFrameBias>0</bFrameBias><bFrameReferences>strict</bFrameReferences><loopFilter>true</loopFilter><loopFilterAlphaC0>0</loopFilterAlphaC0><loopFilterBeta>0</loopFilterBeta><cabac>true</cabac><openGop>bluray</openGop><interlaced>disabled</interlaced><constrainedIntraPrediction>true</constrainedIntraPrediction><cqmPreset>flat</cqmPreset><intra4x4Luma><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value></intra4x4Luma><intraChroma><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value></intraChroma><inter4x4Luma><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value></inter4x4Luma><interChroma><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value></interChroma><intra8x8Luma><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value></intra8x8Luma><inter8x8Luma><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</value><value>16</
//** Audio **
app.audio.reset();
app.audio.codec("Faac",128,4,"80 00 00 00 ");
app.audio.normalizeMode=0;
app.audio.normalizeValue=0;
app.audio.delay=0;
app.audio.mixer="NONE";
app.setContainer("MP4");
//write that file
app.save(dstPath);
} else{ //we decide not to process this file
//some diagnostic info incase you wonder why a whole dir of files arent processing
whyNot = dstPath + " wasnt processed because: ";
if(ds.isNotFile)
whyNot += "it is not a file, ";
if(ds.isDirectory)
whyNot += "it is a directory, ";
if(ds.isHidden)
whyNot += "it is hidden, ";
if(ds.isArchive)
whyNot += "it is marked for archive, ";
if(ds.isSingleDot)
whyNot += "it is a meta-ref to the current dir (.), ";
if(ds.isDoubleDot)
whyNot += "it is a meta-ref to the parent dir (..), ";
//whats that reason now?
print(whyNot);
}
}
}
ds.Close();
}
собственно, запускаем сам проект:
avidemux2_gtk --nogui --run proj.js
И идём спать.