package org.korsakow.services.encoders.video.ffmpeg;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.korsakow.services.encoders.EncoderException;
import org.korsakow.services.encoders.UnsupportedFormatException;
import org.korsakow.services.encoders.video.AudioCodec;
import org.korsakow.services.encoders.video.ContainerFormat;
import org.korsakow.services.encoders.video.FileExternalVideoEncoder;
import org.korsakow.services.encoders.video.VideoCodec;
import org.korsakow.services.encoders.video.VideoEncoder;
import org.korsakow.services.encoders.video.VideoEncoderException;
public abstract class FFMpegEncoder extends FileExternalVideoEncoder
{
public static final String OPTION_PRESET = "vpre";
// options
protected AudioCodec audioCodec;
protected Integer audioSamplingRate;
protected Integer audioBitRate;
protected VideoCodec videoCodec;
protected ContainerFormat containerFormat;
protected Integer videoBitRate;
protected Integer videoBitRateTolerance;
protected Boolean deinterlace;
protected Integer width;
protected Integer height;
protected Integer paddingTop;
protected Integer paddingBottom;
protected Integer paddingLeft;
protected Integer paddingRight;
protected Integer paddingColor;
protected String presetsFile;
protected boolean twoPass = false;
protected Long frameCount;
protected Long offsetMillis;
protected int currentPass;
protected File passlogfile;
public abstract static class FFMpegEncoderDescription implements VideoEncoder.VideoEncoderDescription
{
private static final Collection<VideoCodec> inputFormats = Collections.unmodifiableCollection(Arrays.asList(
VideoCodec.FLV, VideoCodec.H264, VideoCodec.JPG
));
private static final Collection<VideoCodec> outputFormats = Collections.unmodifiableCollection(Arrays.asList(
VideoCodec.FLV, VideoCodec.H264, VideoCodec.JPG
));
public Collection<VideoCodec> getSupportedInputFormats() {
return inputFormats;
}
public Collection<VideoCodec> getSupportedOutputFormats() {
return outputFormats;
}
}
public void setEncoderSpecificOption(Object name, Object value) throws UnsupportedOperationException
{
if (OPTION_PRESET.equals(name))
presetsFile = value.toString();
else
throw new UnsupportedOperationException(String.format("can't set '%s'", name!=null?name.toString():null));
}
public Object getEncoderSpecificOption(Object name) throws UnsupportedOperationException
{
throw new UnsupportedOperationException(String.format("can't get '%s'", name!=null?name.toString():null));
}
public void setSize(Integer width, Integer height)
{
if ((width == null || height == null) && (width != height))
throw new NullPointerException("both width and height may be null to leave the original size, but only one was null!");
this.width = width;
this.height = height;
}
public Integer getWidth()
{
return width;
}
public Integer getHeight()
{
return height;
}
public void setPadding(Integer color, Integer top, Integer right, Integer bottom, Integer left)
{
paddingColor = color;
paddingBottom = bottom;
paddingTop = top;
paddingLeft = left;
paddingRight = right;
}
public Integer getAudioSamplingRate()
{
return audioSamplingRate;
}
public void setAudioSamplingRate(Integer rate)
{
audioSamplingRate = rate;
}
public void setAudioBitRate(Integer rate) throws UnsupportedOperationException
{
audioBitRate = rate;
}
public void setVideoBitRate(Integer rate) throws UnsupportedOperationException
{
videoBitRate = rate;
}
public void setVideoBitRateTolerance(Integer tolerance) throws UnsupportedOperationException
{
videoBitRateTolerance = tolerance;
}
public void setDeinterlace(Boolean deinterlace) throws UnsupportedOperationException
{
this.deinterlace = deinterlace;
}
public void setAudioCodec(AudioCodec codec)
{
audioCodec = codec;
}
public void setVideoCodec(VideoCodec codec)
{
videoCodec = codec;
}
public void setContainerFormat(ContainerFormat format)
{
containerFormat = format;
}
public void setPresetsFile(String presets)
{
presetsFile = presets;
}
public void setFrameCount(Long frameCount)
{
this.frameCount = frameCount;
}
public void setOffset(Long offsetMillis)
{
this.offsetMillis = offsetMillis;
}
protected List<String> createCommandLine(VideoCodec sourceFormat, File srcFile, File dstFile)
{
List<String> cmds = new ArrayList<String>();
cmds.add("-i");
cmds.add(srcFile.getAbsolutePath());
cmds.add("-y");
boolean isFinalPass = !twoPass || currentPass > 1;
if (isFinalPass) {
if (audioCodec != null) {
if (audioCodec == AudioCodec.NONE) {
cmds.add("-an");
} else {
final String aCodec = getACodec(audioCodec);
cmds.add("-acodec");
cmds.add(aCodec);
}
}
if (audioSamplingRate != null) {
cmds.add("-ar");
cmds.add(""+audioSamplingRate);
}
if (audioBitRate != null) {
cmds.add("-ab");
cmds.add(""+(audioBitRate/1024)+"k");
}
} else {
cmds.add("-an");
}
if (frameCount != null) {
cmds.add("-vframes");
cmds.add(""+frameCount);
}
if (offsetMillis != null) {
cmds.add("-ss");
cmds.add(""+(int)(offsetMillis/1000.0));
}
if (presetsFile != null) {
cmds.add("-fpre");
cmds.add(presetsFile);
}
if (videoBitRate != null) {
cmds.add("-b");
cmds.add(""+(videoBitRate/1024)+"k");
}
if (videoBitRateTolerance != null) {
cmds.add("-bt");
cmds.add(""+(videoBitRateTolerance/1024)+"k");
}
if (deinterlace != null) {
cmds.add("-deinterlace");
}
if (width != null && height != null)
{
// ffmpeg requries the size to be a multiple of 2
int w = width;
int h = height;
if (w%2!=0) --w;
if (h%2!=0) --h;
cmds.add("-s");
cmds.add(w+"x"+h);
}
if (paddingLeft != null && paddingLeft != 0) {
cmds.add("-padleft");
cmds.add("" + multof(paddingLeft, 2));
}
if (paddingRight != null && paddingRight != 0) {
cmds.add("-padright");
cmds.add("" + multof(paddingRight, 2));
}
if (paddingTop != null && paddingTop != 0) {
cmds.add("-padtop");
cmds.add("" + multof(paddingTop, 2));
}
if (paddingBottom != null && paddingBottom != 0) {
cmds.add("-padbottom");
cmds.add("" + multof(paddingBottom, 2));
}
if (paddingColor != null) {
cmds.add("-padcolor");
cmds.add("" + paddingColor);
}
if (videoCodec != null) {
final String vCodec = getVCodec(videoCodec);
cmds.add("-vcodec");
cmds.add(vCodec);
}
if (containerFormat != null) {
cmds.add("-f");
cmds.add(getVFormat(containerFormat));
}
// current builds dont support threads: verify x264, mjpeg, ffmpeg
// cmds.add("-threads");
// cmds.add("2");
cmds.add("-g");
cmds.add("12"); // keyframes
// cmds.add("-r");
// cmds.add("29.97");
if (twoPass) {
cmds.add("-pass");
cmds.add(""+currentPass);
cmds.add("-passlogfile");
cmds.add(passlogfile.getAbsolutePath());
}
if (isFinalPass)
cmds.add(dstFile.getAbsolutePath());
else
cmds.add(getNullDevice());
return cmds;
}
public String getFileExtension(ContainerFormat format)
{
switch (format)
{
case FLV:
return "flv";
case MP4:
return "mp4";
case JPG:
return "jpg";
default:
throw new IllegalArgumentException();
}
}
/**
* See ffmpeg -formats
* @param codec
* @return null to indicate the parameter should not be specified. this is not the same as VideoCodec being null
*/
protected String getVCodec(VideoCodec codec)
{
switch (codec)
{
case FLV:
return "flv";
case H264:
return "libx264";
case JPG:
return "mjpeg";
default:
throw new IllegalArgumentException();
}
}
protected String getVFormat(ContainerFormat format)
{
switch (format)
{
case FLV:
return "flv";
case MP4:
return "mp4";
case JPG:
return "image2";
default:
throw new IllegalArgumentException();
}
}
/**
*
* @param codec
*/
protected String getACodec(AudioCodec codec)
{
switch (codec)
{
case MP3:
return "libmp3lame";
case AAC:
return "libfaac";
default:
throw new IllegalArgumentException();
}
}
protected abstract String getNullDevice();
@Override
public void encode(VideoCodec srcFormat, File srcFile, File destFile) throws EncoderException, InterruptedException
{
try {
if (twoPass)
{
try {
passlogfile = File.createTempFile("FFMpegEncoder", "passlogfile");
} catch (IOException e) {
throw new EncoderException(e, destFile);
}
}
encodeOnePass(1, srcFormat, srcFile, destFile);
if (twoPass)
encodeOnePass(2, srcFormat, srcFile, destFile);
} finally {
if (twoPass) {
passlogfile.delete();
passlogfile = null;
}
}
}
public void encodeOnePass(int pass, VideoCodec srcFormat, File srcFile, File destFile) throws EncoderException, InterruptedException
{
currentPass = pass;
Process process = null;
try {
process = createProcess(srcFormat, srcFile, destFile);
} catch (IOException e) {
throw new VideoEncoderException(e, srcFile);
}
encode(process, srcFile, destFile);
}
@Override
protected void encode(Process process, File sourceFile, File destFile) throws EncoderException, InterruptedException
{
try {
super.encode(process, sourceFile, destFile);
} catch (EncoderException e) {
// lame output buffer too small is not a real error. the file is fine.
if (e.getMessage().contains("lame: output buffer too small"))
return;
// catch TMCD data track errors and report them better Unknown format is not supported as input pixel format
if (e.getMessage().contains("Unknown format") && e.getMessage().contains("Data: tmcd"))
throw new UnsupportedFormatException("Quicktime TimeCode tracks (TMCD) are not supported.", e.getDetails(), e.getFile());
throw e;
}
}
private static int multof(double num, int factor)
{
return (int)(num + num%factor);
}
}