/**
*
*/
package org.korsakow.services.export.task;
import java.awt.Component;
import java.awt.Dimension;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.log4j.Logger;
import org.korsakow.domain.interf.IVideo;
import org.korsakow.ide.Application;
import org.korsakow.ide.DialogOptions;
import org.korsakow.ide.io.NullOutputStream;
import org.korsakow.ide.resources.media.MediaFactory;
import org.korsakow.ide.resources.media.PlayableVideo;
import org.korsakow.ide.task.AbstractTask;
import org.korsakow.ide.task.TaskException;
import org.korsakow.ide.util.ExternalsResourceManager;
import org.korsakow.ide.util.FileUtil;
import org.korsakow.ide.util.Platform;
import org.korsakow.services.encoders.EncoderException;
import org.korsakow.services.encoders.video.VideoEncoder;
import org.korsakow.services.encoders.video.VideoEncoderException;
import org.korsakow.services.encoders.video.VideoEncoderFactory;
import org.korsakow.services.encoders.video.ffmpeg.FFMpegEncoder;
import org.korsakow.services.export.ExportException;
import org.korsakow.services.export.ExportOptions;
import org.korsakow.services.export.IVideoEncodingProfile;
public class VideoExportTask extends AbstractTask
{
private final IVideoEncodingProfile encodingProfile;
private final IVideo video;
private final File srcFile;
private final File destFile;
private final File rootDir;
private Integer maxWidth = null;
private Integer maxHeight = null;
public VideoExportTask(ExportOptions options, IVideoEncodingProfile encodignProfile, IVideo video, File destFile, File rootDir) throws FileNotFoundException
{
super(options);
encodingProfile = encodignProfile;
this.video = video;
this.destFile = destFile;
this.rootDir = rootDir;
srcFile = new File(video.getAbsoluteFilename());
}
public void setMaxSize(int width, int height)
{
maxWidth = width;
maxHeight = height;
}
@Override
public String getTitleString()
{
return srcFile.getName();
}
@Override
public void runTask() throws TaskException, InterruptedException
{
// TODO: I think this is outdated 2013/01/20 // the length check is because in creating the unique export filename we actually reserve the physical file
if (destFile.exists() && destFile.length() > 0) {
Boolean overwriteOption;
synchronized (exportOptions) {
overwriteOption = exportOptions.overwriteExisting;
}
// if option already set to false, then abort
if (overwriteOption == Boolean.FALSE)
return;
// if undecided, ask
if (overwriteOption == null) {
DialogOptions dialogOptions = Application.getInstance().showFileOverwriteDialog("File exists", destFile.getName() + " already exists, YES to overwrite or NO to skip.");
if (dialogOptions.applyToAll) {
// apply to all means set the global option
synchronized (exportOptions) {
exportOptions.overwriteExisting = dialogOptions.dialogResult;
}
}
if (!dialogOptions.dialogResult)
return;
}
}
if (!srcFile.exists())
throw new TaskException(new FileNotFoundException(srcFile.getPath()));
try {
boolean encode;
synchronized (exportOptions) {
encode = exportOptions.encodeVideo;
}
if (encode)
encodeVideo(srcFile, destFile, encodingProfile, maxWidth, maxHeight);
else
copyVideo(srcFile, destFile);
} catch (EncoderException e) {
throw new TaskException(e);
} catch (IOException e) {
throw new TaskException(e);
}
if (!destFile.exists() || destFile.length() == 0)
throw new TaskException(new ExportException("Video File Missing:" + destFile.getPath(), rootDir));
}
public static void encodeVideo(File srcFile, File destFile, IVideoEncodingProfile encodingProfile, Integer maxWidth, Integer maxHeight) throws IOException, InterruptedException, EncoderException {
destFile.getParentFile().mkdirs();
VideoEncoder videoEncoder = getEncoder(encodingProfile);
if (maxWidth != null && maxHeight != null) {
Dimension size = calculateVideoSize(srcFile, maxWidth, maxHeight);
if (size != null)
videoEncoder.setSize(size.width, size.height);
}
videoEncoder.encode(null, srcFile, destFile);
if (!destFile.exists() || destFile.length() ==0)
throw new IOException("video file was not created?");
switch (encodingProfile.getContainerFormat())
{
case FLV:
fixFLVMetaData(destFile);
break;
case MP4:
fixH264Streaming(destFile);
break;
}
}
private static void copyVideo(File srcFile, File destFile) throws IOException {
FileUtil.copyFile(srcFile, destFile);
}
/**
* @return null if the dimensions can't be calculated or would result in a size larger than the media's actual size
*/
public static Dimension calculateVideoSize(File srcFile,
int maxWidth,
int maxHeight)
{
Dimension d = null;
try {
PlayableVideo playable = (PlayableVideo)MediaFactory.getMedia(srcFile.getAbsolutePath());
Component comp = playable.getComponent();
Dimension pref = comp.getPreferredSize();
// don't enlarge.
if (maxWidth < pref.width && maxHeight < pref.height)
d = playable.getAspectRespectingDimension(new Dimension(maxWidth, maxHeight));
playable.dispose();
} catch (Exception e) {
Logger.getLogger(VideoExportTask.class).error("", e);
}
return d;
}
public static VideoEncoder getEncoder(IVideoEncodingProfile profile) throws IOException, VideoEncoderException
{
VideoEncoderFactory fac = VideoEncoderFactory.getNewFactory();
// fac.addRequiredInputFormat(profile.getVideoCodec());
if (profile.getVideoCodec() != null) fac.addRequiredOutputFormat(profile.getVideoCodec());
VideoEncoder encoder = fac.createVideoEncoder();
if (profile.getAudioBitRate() != null) encoder.setAudioBitRate(profile.getAudioBitRate());
if (profile.getAudioSamplingRate() != null) encoder.setAudioSamplingRate(profile.getAudioSamplingRate());
if (profile.getAudioCodec() != null) encoder.setAudioCodec(profile.getAudioCodec());
if (profile.getVideoCodec() != null) encoder.setVideoCodec(profile.getVideoCodec());
if (profile.getContainerFormat() != null) encoder.setContainerFormat(profile.getContainerFormat());
if (profile.getVideoBitRate() != null) encoder.setVideoBitRate(profile.getVideoBitRate());
if (profile.getVideoBitRateTolerance() != null) encoder.setVideoBitRateTolerance(profile.getVideoBitRateTolerance());
Collection<String> keys = profile.getEncoderSpecificKeys();
for (String key : keys) {
String value = profile.getEncoderSpecificValue(key);
// TODO: think of a better way to handle this
if (encoder instanceof FFMpegEncoder && FFMpegEncoder.OPTION_PRESET.equals(key)) {
value = ExternalsResourceManager.getExternalFile(ExternalsResourceManager.FFMPEG_PRESETS + File.separator + value).getAbsolutePath();
}
encoder.setEncoderSpecificOption(key, value);
}
return encoder;
}
/**
* Yamdi is a handy little tool that makes sure our FLV has all the proper metadata.
* In particular this ensures our FLV is nice and seekable.
* @param file
* @throws IOException
* @throws InterruptedException
*/
private static void fixFLVMetaData(File file) throws IOException, InterruptedException
{
// Yamdi doesn't work in-place on files, so we create a new one and swap
// c.f. #1051
// c.f. http://java.sun.com/j2se/1.5.0/docs/api/java/io/File.html#renameTo%28java.io.File%29
// basically we can't assume too much so its best to create the temp file in the same directory as the one we
// want to do the renameTo() on.
File tempFile = File.createTempFile("videoExportTask", "YAMDI", file.getParentFile());
File execFile = null;
switch (Platform.getOS())
{
case MAC:
execFile = ExternalsResourceManager.getExternalFile(ExternalsResourceManager.YAMDI_OSX);
break;
case WIN:
execFile = ExternalsResourceManager.getExternalFile(ExternalsResourceManager.YAMDI_WIN);
break;
}
List<String> cmds = new ArrayList<String>();
cmds.add(execFile.getAbsolutePath());
cmds.add("-i");
cmds.add(file.getAbsolutePath());
cmds.add("-o");
cmds.add(tempFile.getAbsolutePath());
ProcessBuilder pb = new ProcessBuilder(cmds);
Process p = pb.start();
ByteArrayOutputStream errBuff = new ByteArrayOutputStream();
OutputStream outBuff = new NullOutputStream();
int exitCode = FileUtil.executeProcess(p, outBuff, errBuff);
if (exitCode != 0) {
throw new IOException(errBuff.toString("UTF-8"));
}
FileUtil.delete(file);
if (!tempFile.renameTo(file))
throw new IOException(String.format("Failed to rename '%s' to '%s'", tempFile.getAbsolutePath(), file.getAbsolutePath()));
}
/**
* MP4Box fixes MP4 files so they can be streamed
* @param file
* @throws IOException
* @throws InterruptedException
*/
private static void fixH264Streaming(File file) throws IOException, InterruptedException
{
File execFile = null;
switch (Platform.getOS())
{
case MAC:
execFile = ExternalsResourceManager.getExternalFile(ExternalsResourceManager.MP4BOX_OSX);
break;
case WIN:
execFile = ExternalsResourceManager.getExternalFile(ExternalsResourceManager.MP4BOX_WIN);
break;
}
File tmpDir = FileUtil.createTempDirectory(".mp4box", "tmp", file.getParentFile());
tmpDir.deleteOnExit();
try {
List<String> cmds = new ArrayList<String>();
cmds.add(execFile.getAbsolutePath());
cmds.add("-isma");
cmds.add("-hint");
// setting the tmp dir to the same location as the file avoids issues as with #1051
cmds.add("-tmp"); cmds.add(tmpDir.getAbsolutePath());
cmds.add(file.getAbsolutePath());
ProcessBuilder pb = new ProcessBuilder(cmds);
System.out.println(pb.command());
Process p = pb.start();
ByteArrayOutputStream errBuff = new ByteArrayOutputStream();
OutputStream outBuff = System.out;
int exitCode = FileUtil.executeProcess(p, outBuff, errBuff);
if (exitCode != 0) {
throw new IOException(errBuff.toString("UTF-8"));
}
} finally {
tmpDir.delete();
}
}
}