package edu.washington.cs.oneswarm.ui.gwt.server.ffmpeg;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.InvalidPropertiesFormatException;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SystemProperties;
import org.gudy.azureus2.plugins.torrent.TorrentException;
import sun.awt.AWTAutoShutdown;
import edu.washington.cs.oneswarm.ui.gwt.CoreInterface;
import edu.washington.cs.oneswarm.ui.gwt.server.ffmpeg.FFMpegException.ErrorType;
public class FFMpegTools {
private static final int PREVIEW_IMAGE_AT_TIME = 30;
private static String linuxPath = null;
private static Logger logger = Logger.getLogger(FFMpegTools.class.getName());
public static String getFFMpegPath() throws FFMpegException {
String os = System.getProperty("os.name");
if (os.contains("Mac OS")) {
try {
File appDir = new File(SystemProperties.getApplicationPath());
logger.fine("App dir='" + appDir.getCanonicalPath() + "'");
File ffmpegBin = new File(appDir, "bin/ffmpeg");
logger.fine("ffmpeg bin='" + ffmpegBin.getCanonicalPath() + "'");
return ffmpegBin.getCanonicalPath();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Debug.out("problem locating ffmpeg, faling back to ffmpeg in path");
} else if (os.contains("Windows")) {
return "bin" + File.separator + "ffmpeg.exe";
} else if (os.contains("Linux")) {
return linuxGetFFMpegPath();
} else {
Debug.out("unknown os: '" + os + "' --ffmpeg must be in path");
}
return "ffmpeg";
}
/**
*
* Blocking method for getting media information about a specific file
*
* @param file
* @return
* @throws FFMpegException
*/
static MovieStreamInfo getMovieInfo(byte[] infohash, File file) throws FFMpegException {
logger.finest("getting movie info for: " + file);
/*
* start by checking if we already done this
*/
MovieStreamInfo cached = readCachedMovieInfo(infohash, file);
if (cached != null) {
return cached;
} else {
MovieStreamInfo m = createMovieInfo(infohash, file);
return m;
}
}
static MovieStreamInfo readCachedMovieInfo(byte[] infohash, File file) {
String pathHash = Integer.toHexString(file.getPath().hashCode());
File metainfoDir;
try {
metainfoDir = CoreInterface.getMetaInfoDir(infohash);
File existingInfoFile = new File(metainfoDir, "movieInfo_" + pathHash + ".xml");
if (existingInfoFile.exists()) {
try {
MovieStreamInfo i = new MovieStreamInfo(existingInfoFile);
logger.fine("loaded mediainfo from existing file: " + i);
return i;
} catch (InvalidPropertiesFormatException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (TorrentException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
return null;
}
static MovieStreamInfo createMovieInfo(byte[] infohash, File file) throws FFMpegException {
logger.finest("using ffmpeg to create movie info for: " + file);
final Process ffmpeg;
try {
ffmpeg = createFFmpegProcess(new String[] { "-i", file.getCanonicalPath() }, 5 * 1000);
} catch (IOException e1) {
throw new FFMpegException(ErrorType.FILE_NOT_FOUND, "got error when getting file: "
+ file, e1);
}
BufferedReader stdErr = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()));
// dump anything showing up no stdout (shouldn't be anything
new StreamDumper(ffmpeg.getInputStream(), 0, false);
// and read from stderr at the same time
StringBuffer ffmpegStdErr = new StringBuffer();
try {
String line;
while ((line = stdErr.readLine()) != null) {
ffmpegStdErr.append(line + "\n");
}
stdErr.close();
} catch (IOException e) {
throw new FFMpegException(FFMpegException.ErrorType.OTHER,
"Got IO error while reading from ffmpeg", e);
}
try {
ffmpeg.waitFor();
} catch (InterruptedException e) {
throw new FFMpegException(FFMpegException.ErrorType.INTERUPT,
"Got interupted while waiting for ffmpeg to complete", e);
}
MovieStreamInfo m = new MovieStreamInfo(ffmpegStdErr.toString());
logger.finest("movie info created successfully, saving to disk");
/*
* try to write it down to disk
*/
try {
String pathHash = Integer.toHexString(file.getPath().hashCode());
File metainfoDir = CoreInterface.getMetaInfoDir(infohash);
File existingInfoFile = new File(metainfoDir, "movieInfo_" + pathHash + ".xml");
File parent = existingInfoFile.getParentFile();
if (!parent.isDirectory()) {
parent.mkdirs();
}
m.writeToFile(existingInfoFile);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (TorrentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return m;
}
static Process createFFmpegProcess(String[] parameters, final int killAfterSeconds)
throws FFMpegException {
try {
String[] allParameters = new String[parameters.length + 1];
allParameters[0] = FFMpegTools.getFFMpegPath();
System.arraycopy(parameters, 0, allParameters, 1, parameters.length);
StringBuilder ffmpegParameters = new StringBuilder();
for (String string : allParameters) {
ffmpegParameters.append("'" + string + "' ");
}
logger.finest("executing: " + ffmpegParameters.toString());
final Process ffmpeg = Runtime.getRuntime().exec(allParameters);
/*
* add a thread that kill ffmpeg if it runs for too long
*/
if (killAfterSeconds > 0) {
Thread ffmpegTerminatorThread = new Thread(new Runnable() {
public void run() {
long totalSlept = 0;
try {
if (totalSlept >= killAfterSeconds) {
if (ffmpeg != null) {
logger.finest("ffmpeg not completed after " + totalSlept
+ "s, killing");
ffmpeg.destroy();
return;
}
}
Thread.sleep(1000);
totalSlept++;
try {
ffmpeg.exitValue();
logger.finest("ffmpeg process completed, stopping ffmpeg kill thread");
// check if the process terminated
// if we get an exit value the process
// terminated,
// then kill this thread
return;
} catch (IllegalThreadStateException e) {
// this is expected
}
} catch (InterruptedException e) {
// interrupted, just kill the thread
}
}
});
ffmpegTerminatorThread.setName("FFMpeg terminator thread");
ffmpegTerminatorThread.setDaemon(true);
ffmpegTerminatorThread.start();
}
return ffmpeg;
} catch (IOException e1) {
throw new FFMpegException(ErrorType.OTHER, "unable to create ffmpeg process", e1);
}
}
static void createPreviewImage(byte[] infohash, File mediaFile, File imageFile)
throws FFMpegException {
logger.finer("trying to create preview image, file=" + mediaFile + " destination="
+ imageFile);
// check if we have video in the file
MovieStreamInfo fileInfo = getMovieInfo(infohash, mediaFile);
if (!fileInfo.hasVideo()) {
throw new FFMpegException(ErrorType.FORMAT_ERROR,
"unable to create preview image, no video stream found");
}
/*
* calc seek to
*/
double duration = fileInfo.getDuration();
int seekTo;
if (duration < 1) {
seekTo = 0;
} else if (duration > 2 * PREVIEW_IMAGE_AT_TIME) {
seekTo = PREVIEW_IMAGE_AT_TIME;
} else {
seekTo = (int) Math.floor(duration / 2);
}
/*
* create the ffmpeg process
*/
final Process ffmpeg;
try {
ffmpeg = createFFmpegProcess(new String[] { "-i", mediaFile.getCanonicalPath(),
"-vcodec", "png", "-ss", seekTo + "", "-vframes", "1", "-f", "rawvideo", "-" },
30);
} catch (IOException e) {
throw new FFMpegException(ErrorType.FILE_NOT_FOUND, "got error when getting file: "
+ mediaFile, e);
}
StreamReader ffmpegStdOutReader = new StreamReader(ffmpeg.getInputStream());
BufferedReader stdErr = new BufferedReader(new InputStreamReader(ffmpeg.getErrorStream()));
// and read from stderr at the same time
StringBuffer ffmpegStdErr = new StringBuffer();
byte[] ffmpegStdOut;
/*
* ffmpeg is running, read the output
*/
int lineNum = 0;
try {
String line;
while ((line = stdErr.readLine()) != null) {
// ffmpeg can send out megabytes on stderr, only save first 100
// lines
if (lineNum++ < 100) {
ffmpegStdErr.append(line + "\n");
}
}
stdErr.close();
ffmpegStdOut = ffmpegStdOutReader.read();
} catch (IOException e) {
throw new FFMpegException(FFMpegException.ErrorType.OTHER,
"Got IO error while reading from ffmpeg", e);
} catch (InterruptedException e) {
throw new FFMpegException(FFMpegException.ErrorType.INTERUPT,
"Got interupted while reading from ffmpeg", e);
}
/*
* wait for ffmpeg to complete
*/
try {
int exitVal = ffmpeg.waitFor();
if (exitVal != 0) {
throw new FFMpegException(FFMpegException.ErrorType.OTHER, exitVal,
ffmpegStdErr.toString());
}
} catch (InterruptedException e) {
throw new FFMpegException(FFMpegException.ErrorType.INTERUPT,
"Got interupted while waiting for ffmpeg to complete", e);
}
/*
* and write the file
*/
try {
writeTransformedImage(new ByteArrayInputStream(ffmpegStdOut), imageFile, true);
} catch (IOException e) {
throw new FFMpegException(FFMpegException.ErrorType.OTHER,
"Got IO error when writing preview image", e);
}
}
private static String linuxGetFFMpegPath() throws FFMpegException {
// check if we already did this
if (linuxPath != null) {
return linuxPath;
}
System.out.println("linux: trying to find the ffmpeg path");
// first, try the libc2.7 ffmpeg
File binDir = new File(SystemProperties.getApplicationPath() + File.separator + "bin");
File ffmpeg27 = new File(binDir, "ffmpeg_glibc2.7");
File ffmpeg26 = new File(binDir, "ffmpeg_glibc2.6");
try {
String[] paths = { ffmpeg27.getCanonicalPath(), ffmpeg26.getCanonicalPath(),
"/usr/bin/ffmpeg", "ffmpeg" };
for (String path : paths) {
try {
boolean works = testFFMpeg(path);
if (works) {
linuxPath = path;
return path;
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
throw new FFMpegException(ErrorType.FFMPEG_BIN_ERROR,
"Unable to find a working ffmpeg binary");
}
private final static HashSet<File> failedPreviews = new HashSet<File>();
static void setPrevImageGenerationFailed(File imageFile) {
failedPreviews.add(imageFile);
try {
File failFile = new File(imageFile.getCanonicalPath() + ".failed");
failFile.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
static boolean checkPrevImageGenerationFailed(File imageFile) {
if (failedPreviews.contains(imageFile)) {
return true;
}
try {
File failFile = new File(imageFile.getCanonicalPath() + ".failed");
return failFile.isFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
private static boolean testFFMpeg(String path) throws IOException, InterruptedException {
Process p = Runtime.getRuntime().exec(new String[] { path, "-h" });
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
logger.finer("testing ffmpeg: '" + path + "'");
while (in.readLine() != null) {
}
int returnValue = p.waitFor();
logger.finer("got return value: " + returnValue);
if (returnValue == 0) {
return true;
}
return false;
}
/**
* instead of resizing on the client we do it here
*
* @param inputStream
* @param imgFile
* @throws IOException
*/
static void writeTransformedImage(InputStream inputStream, File imgFile, boolean fromVideo)
throws IOException {
logger.fine("writing transformed image to: " + imgFile.getAbsolutePath());
FileOutputStream out = new FileOutputStream(imgFile);
ImageIO.setUseCache(false);
if (inputStream.available() <= 0) {
throw new IOException("tried to transform a zero-size image.");
}
Image base = ImageIO.read(inputStream);
if (base == null) {
throw new IOException("unable to read image");
}
double resizeFactorWidth = base.getWidth(null) / (double) 128;
double resizeFactorHeight = base.getHeight(null) / (double) 128;
double resizeFactor = Math.max(resizeFactorWidth, resizeFactorHeight);
// System.out.println("resize factor: " + resizeFactor);
base = base.getScaledInstance((int) (base.getWidth(null) / resizeFactor),
(int) (base.getHeight(null) / resizeFactor), Image.SCALE_SMOOTH);
BufferedImage image = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
Graphics2D graph = image.createGraphics();
// first, fill the background with transparent white
Color transparent = new Color(255, 255, 255, 0);
graph.setColor(transparent);
graph.fill(new Rectangle(0, 0, 128, 128));
// then create a 4x3 rectangle with black to make all strange 16x9,
// 16x10,1:2.09 formats don't look uneven
// but only if the image is wider that it is high and we got the image
// from video
int width = base.getWidth(null);
int height = base.getHeight(null);
if (width > height && fromVideo) {
BufferedImage padding = new BufferedImage(128, 96, BufferedImage.TYPE_INT_ARGB);
Graphics2D paddGraph = padding.createGraphics();
paddGraph.setColor(Color.black);
paddGraph.fill(new Rectangle(0, 0, 128, 96));
AffineTransform padTrans = new AffineTransform();
padTrans.setToTranslation(0, (128 - 96) / 2);
graph.drawImage(padding, padTrans, null);
paddGraph.dispose();
padding.flush();
}
int x = (128 - width) / 2;
int y = (128 - height) / 2;
logger.finer("x: " + x + " y: " + y + " width: " + width + " height: " + height);
AffineTransform trans = new AffineTransform();
trans.setToTranslation(x, y);
graph.drawImage(base, trans, null);
ImageIO.write(image, "png", out);
out.close();
graph.dispose();
image.flush();
base.flush();
/**
* There seems to be a weird interaction with using ImageIO if we don't
* mark this thread as free. (The AWT-Shutdown thread will hang around
* forever when we try to quit until it is forceably shutdown). Calling
* this seems to prevent that, although I'm not sure if we've addressed
* the root problem.
*/
try {
AWTAutoShutdown.notifyToolkitThreadFree();
} catch (Exception e) {
e.printStackTrace();
}
}
}