package nodebox.movie;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.Random;
public class Movie {
private static final File FFMPEG_BINARY;
private static final String TEMPORARY_FILE_PREFIX = "sme";
public static final String FFMPEG_PRESET_TEMPLATE = "res/ffpresets/libx264-%s.ffpreset";
public static final ArrayList<VideoFormat> VIDEO_FORMATS;
public static final VideoFormat DEFAULT_FORMAT = MP4VideoFormat.MP4Format;
static {
String osName = System.getProperty("os.name").split("\\s")[0];
// If we provide a binary for this system, use it. Otherwise, see if a default "ffmpeg" binary exists.
String binaryName = "ffmpeg";
if (osName.equals("Windows"))
binaryName = "ffmpeg.exe";
File packagedBinary = new File(String.format("bin/%s", binaryName));
if (!packagedBinary.exists()) {
packagedBinary = nodebox.util.FileUtils.getApplicationFile(String.format("bin/%s", binaryName));
}
if (packagedBinary.exists()) {
FFMPEG_BINARY = packagedBinary;
} else {
FFMPEG_BINARY = new File("/usr/bin/ffmpeg");
}
VIDEO_FORMATS = new ArrayList<VideoFormat>();
VIDEO_FORMATS.add(AndroidVideoFormat.DefaultFormat);
VIDEO_FORMATS.add(AndroidVideoFormat.NexusFormat);
VIDEO_FORMATS.add(AndroidVideoFormat.DroidFormat);
VIDEO_FORMATS.add(AppleVideoFormat.DefaultFormat);
VIDEO_FORMATS.add(AppleVideoFormat.IpadFormat);
//VIDEO_FORMATS.add(PSPVideoFormat.PSPFormat);
VIDEO_FORMATS.add(MP4VideoFormat.MP4Format);
VIDEO_FORMATS.add(WebmVideoFormat.WebmFormat);
}
private String movieFilename;
private VideoFormat videoFormat;
private int width, height;
private boolean verbose;
private int frameCount = 0;
private String temporaryFileTemplate;
public Movie(String movieFilename, VideoFormat format, int width, int height) {
this(movieFilename, format, width, height, false);
}
public Movie(String movieFilename, VideoFormat format, int width, int height, boolean verbose) {
this.movieFilename = movieFilename;
this.videoFormat = format;
this.width = width;
this.height = height;
this.verbose = verbose;
// Generate the prefix for a temporary file.
// We generate a temporary file, then use that as the prefix for our own files.
try {
File tempFile = File.createTempFile(TEMPORARY_FILE_PREFIX, "");
temporaryFileTemplate = tempFile.getPath() + "-%05d.png";
tempFile.delete();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public boolean isVerbose() {
return verbose;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public int getFrameCount() {
return frameCount;
}
public String getMovieFilename() {
return movieFilename;
}
public File getMovieFile() {
return new File(movieFilename);
}
public File temporaryFileForFrame(int frame) {
return new File(String.format(temporaryFileTemplate, frame));
}
/**
* Add the image to the movie.
* <p/>
* The image size needs to be exactly the same size as the movie.
* <p/>
* Internally, this saves the image to a temporary image and increases the frame counter. Temporary images are
* cleaned up when calling save() or if an error occurs.
*
* @param img the image to add to the movie.
*/
public void addFrame(RenderedImage img) {
if (img.getWidth() != width || img.getHeight() != height) {
throw new RuntimeException("Given image does not have the same size as the movie.");
}
try {
ImageIO.write(img, "png", temporaryFileForFrame(frameCount));
frameCount++;
} catch (IOException e) {
cleanupAndThrowException(e);
}
}
public void save() {
save(new StringWriter());
}
/**
* Finishes the export and save the movie.
*/
public void save(StringWriter sw) {
PrintWriter out = new PrintWriter(sw, true);
ArrayList<String> commandList = new ArrayList<String>();
commandList.add(FFMPEG_BINARY.getAbsolutePath());
commandList.add("-y"); // Overwrite target if exists
commandList.add("-i");
commandList.add(temporaryFileTemplate); // Input images
commandList.addAll(videoFormat.getArgumentList(this)); // Video format specific arguments
commandList.add(movieFilename); // Target file name
ProcessBuilder pb = new ProcessBuilder(commandList);
if (verbose) {
for (String cmd : pb.command()) {
System.out.print(cmd + " ");
}
System.out.println();
}
pb.redirectErrorStream(true);
Process p;
try {
p = pb.start();
p.getOutputStream().close();
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = in.readLine()) != null)
out.println(line);
p.waitFor();
if (verbose) {
System.out.println(sw.toString());
}
} catch (IOException e) {
cleanupAndThrowException(e);
} catch (InterruptedException e) {
cleanupAndThrowException(e);
}
cleanup();
}
/**
* Cleans up the temporary images.
* <p/>
* Normally you should not call this method as it is called automatically when running finish() or if an error
* occurred. The only reason to call it is if you have added images and then decide you don't want to generate
* a movie. In that case, instead of calling finish(), call cleanup().
*
* @see #save()
*/
public void cleanup() {
for (int i = 0; i < frameCount; i++) {
temporaryFileForFrame(i).delete();
}
}
private void cleanupAndThrowException(Throwable t) {
cleanup();
throw new RuntimeException(t);
}
public static void main(String[] args) {
int width = 640;
int height = 480;
// Create a new movie.
Movie movie = new Movie("test.mov", MP4VideoFormat.MP4Format, width, height);
movie.setVerbose(true);
/// Initialize an image to draw on.
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = (Graphics2D) img.getGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
for (int frame = 0; frame < 20; frame++) {
System.out.println("frame = " + frame);
// Clear the canvas and draw some simple circles.
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
Random r = new Random(0);
for (int j = 0; j < 100; j++) {
g.setColor(new Color(r.nextInt(255), 255, r.nextInt(255)));
g.fillOval(r.nextInt(width) + frame, r.nextInt(height) + frame, 30, 30);
}
// Add the image to the movie.
movie.addFrame(img);
}
// Export the movie.
movie.save();
}
}