I hope to write up an explanation of the code, but I promised the User Group that I’d post the code, and I had better do it now while I’m thinking about it (only 2 weeks late!)
One important note: copy the .ffmpeg directory to your /user/ directory. Use ProcMon to see exactly where (on windows). If I were to release this as an actual app, I’d re-compile ffmpeg to look in the current directory.
Also, I didn’t include anything in the media folder, you’ll have to find your own AVI file to transcode.
Code below the fold.
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.controls.Alert;
import mx.events.CloseEvent;
import mx.events.FlexEvent;
[Bindable] protected var process:NativeProcess;
[Bindable] protected var outputText:String = "";
protected var lengthRegExp:RegExp = /Duration: (.*), start: (.*), bitrate: (.*)/m;
[Bindable] protected var lengthStr:String = "";
[Bindable] protected var lengthNum:Number = 0;
protected var sizeRegExp:RegExp = /Video(.*) (\d+)x(\d+)/m;
[Bindable] protected var sizeStr:String = "";
protected var curTimeRegExp:RegExp = /time=(\d+).(\d+) /m;
[Bindable] protected var currentTime:Number = 0;
protected var startTime:Date = new Date();
[Bindable] protected var profiles:ArrayCollection = new ArrayCollection([
{label:"Baseline", data:"baseline"},
{label:"Default", data:"default"},
{label:"Very Slow", data:"veryslow"},
{label:"Slower", data:"slower"},
{label:"Slow", data:"slow"},
{label:"Fast", data:"fast"},
{label:"Faster", data:"faster"},
{label:"Super Fast", data:"superfast"},
{label:"Ultra Fast", data:"ultrafast"},
{label:"IPod Low", data:"ipod320"},
{label:"IPod High", data:"ipod640"},
{label:"High Quality", data:"hq"},
{label:"Lossless - Slow", data:"lossless_slow"},
{label:"Lossless - Ultrafast", data:"lossless_ultrafast"}]);
protected function button1_clickHandler(event:MouseEvent):void
{
if(!NativeProcess.isSupported) return;
if(process && process.running){
trace("FFmpeg is already running");
return;
}
startTime = new Date();
var vid:File = File.applicationDirectory.resolvePath('media\\P7030615.avi');
var nativeProcessStartupInfo:NativeProcessStartupInfo = new NativeProcessStartupInfo();
var f:File = new File(File.applicationDirectory.nativePath + "\\NativeApps\\windows\\ffmpeg\\bin\\ffmpeg.exe");
nativeProcessStartupInfo.executable = f;
var args:Vector.<String> = new Vector.<String>();
args.push("-i");
args.push(vid.nativePath);
args.push("-acodec");
args.push("libmp3lame");
args.push("-ab");
args.push("128k");
args.push("-vcodec");
args.push("libx264");
args.push("-vpre");
args.push(profileDDL.selectedItem.data);
args.push("-crf");
args.push("22");
args.push("-threads");
args.push("0");
args.push(vid.nativePath + '.mp4');
nativeProcessStartupInfo.arguments = args;
//ffmpeg -i ..\..\media\P7030615.avi -acodec libmp3lame -ab 128k -vcodec libx264 -vpre slow -crf 22 -threads 0 ..\..\media\P7030615.mp4
process = new NativeProcess();
process.start(nativeProcessStartupInfo);
process.addEventListener(ProgressEvent.STANDARD_ERROR_DATA, handleFFmpegError);
process.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA, handleFFmpegOutput);
process.addEventListener(NativeProcessExitEvent.EXIT, handleFFmpegExit);
}
protected function parseOutput(output:String):void{
if(lengthRegExp.test(output)){
var results:Array = lengthRegExp.exec(output);
lengthStr = results[1];
var lengthArr:Array = String(results[1]).split(":");
lengthNum = parseFloat(lengthArr[2]) +
(parseFloat(lengthArr[1]) * 60);
}
if(sizeRegExp.test(output)){
var resultsSize:Array = sizeRegExp.exec(output);
sizeStr = resultsSize[2] + " x " + resultsSize[3];
trace(sizeStr);
}
if(curTimeRegExp.test(output)){
var resultsTime:Array = curTimeRegExp.exec(output);
currentTime = parseFloat(resultsTime[1] + "." + resultsTime[2]);
var percentDone:Number = Math.floor(currentTime / lengthNum * 100)
progress.setProgress(percentDone, 100);
var now:Date = new Date();
var diff:Number = now.valueOf() - startTime.valueOf();
var total:Number = diff / (percentDone / 100);
var remaining:Date = new Date(total - diff);
progress.label = percentDone + "% Complete / " + remaining.toUTCString().split(" ")[3] + " remaining";
}
}
protected function handleFFmpegOutput(e:ProgressEvent):void{
var output:String = process.standardOutput.readUTFBytes(process.standardOutput.bytesAvailable);
outputText += output;
parseOutput(output);
textReceived.scrollToRange(-1);
if(output.indexOf("already exists") > -1){
Alert.show("Overwrite File?", "File Exists", 4, null, handleOverwriteClosed)
}
}
public function ffmpegcommand(cmdStr:String):void
{
if(process && process.running){
process.standardInput.writeUTFBytes(cmdStr + "\n");
}
}
protected function handleOverwriteClosed(e:CloseEvent):void{
if(e.detail == Alert.YES){
ffmpegcommand("y");
startTime = new Date();
}else{
ffmpegcommand("n");
}
}
protected function handleFFmpegError(e:ProgressEvent):void{
var errorData:String = process.standardError.readUTFBytes(process.standardError.bytesAvailable);
outputText += errorData;
parseOutput(errorData);
textReceived.scrollToRange(-1);
if(errorData.indexOf("already exists") > -1){
Alert.show("Overwrite File?", "File Exists", Alert.YES|Alert.NO, null, handleOverwriteClosed)
}
}
protected function handleFFmpegExit(e:NativeProcessExitEvent):void{
trace("FFmpeg Exited: " + e.exitCode);
trace(e.toString());
}
protected function traceError(e:Event):void{
trace(e);
}
protected function tr_updateCompleteHandler(evt:FlexEvent):void {
textReceived.scroller.verticalScrollBar.value = textReceived.scroller.verticalScrollBar.maximum;
}
]]>
</fx:Script>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<s:layout>
<s:VerticalLayout paddingTop="5" paddingRight="5" paddingLeft="5" gap="0" />
</s:layout>
<s:TabBar dataProvider="{mainView}" />
<mx:ViewStack id="mainView" width="100%" height="100%" borderStyle="solid" borderVisible="true" creationPolicy="all">
<s:NavigatorContent label="Convert">
<s:VGroup width="100%" height="100%">
<mx:Text height="40" id="infoText" width="100%"
text="Duration: {lengthStr}
Size: {sizeStr}" />
<s:HGroup width="100%">
<mx:ProgressBar width="100%" id="progress" mode="manual" direction="right" />
<s:Button label="Cancel" click="if(process)process.exit(true);" />
</s:HGroup>
<s:DropDownList id="profileDDL" width="200"
dataProvider="{profiles}"
prompt="Choose an encoding profile" />
<s:Button label="Start Conversion" enabled="{profileDDL.selectedIndex > -1}"
click="button1_clickHandler(event)" />
</s:VGroup>
</s:NavigatorContent>
<s:NavigatorContent label="Log">
<s:TextArea id="textReceived" width="100%" height="100%"
text="{outputText}"
updateComplete="tr_updateCompleteHandler(event)" />
</s:NavigatorContent>
</mx:ViewStack>
</s:WindowedApplication>