/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2006 Daniel Shiffman
With minor modifications by Ben Fry for Processing 0125+
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General
Public License along with this library; if not, write to the
Free Software Foundation, Inc., 59 Temple Place, Suite 330,
Boston, MA 02111-1307 USA
*/
package processing.video;
import java.io.File;
import quicktime.*;
import quicktime.io.*;
import quicktime.qd.*;
import quicktime.std.*;
import quicktime.std.image.*;
import quicktime.std.movies.Movie;
import quicktime.std.movies.Track;
import quicktime.std.movies.media.VideoMedia;
import quicktime.util.*;
import processing.core.*;
/**
* Library to create a QuickTime movie from a Processing pixel array.
* Written by <A HREF="http://www.shiffman.net">Daniel Shiffman</A>.
* Thanks to Dan O'Sullivan and Shawn Van Every.
* <BR> <BR>
* Please note that some constructors and variable names were altered
* slightly when the library was added to the Processing distribution.
* <PRE>
* // Declare MovieMaker object
* MovieMaker mm;
*
* void setup() {
* size(320, 240);
*
* // Create MovieMaker object with size, filename,
* // compression codec and quality, framerate
* mm = new MovieMaker(this, width, height, "drawing.mov", 30,
* MovieMaker.H263, MovieMaker.HIGH);
* background(160, 32, 32);
* }
*
* void draw() {
* stroke(7, 146, 168);
* strokeWeight(4);
*
* // Draw if mouse is pressed
* if (mousePressed) {
* line(pmouseX, pmouseY, mouseX, mouseY);
* }
*
* // Add window's pixels to movie
* mm.addFrame();
* }
*
* void keyPressed() {
* // Finish the movie if space bar is pressed!
* if (key == ' ') {
* mm.finish();
* }
* }
* </PRE>
*/
@SuppressWarnings("deprecation")
public class MovieMaker {
public static final int RAW = StdQTConstants.kRawCodecType;
public static final int ANIMATION = StdQTConstants.kAnimationCodecType;
public static final int BASE = StdQTConstants.kBaseCodecType;
public static final int BMP = StdQTConstants.kBMPCodecType;
public static final int CINEPAK = StdQTConstants.kCinepakCodecType;
public static final int COMPONENT = StdQTConstants.kComponentVideoCodecType;
public static final int CMYK = StdQTConstants.kCMYKCodecType;
public static final int GIF = StdQTConstants.kGIFCodecType;
public static final int GRAPHICS = StdQTConstants.kGraphicsCodecType;
public static final int H261 = StdQTConstants.kH261CodecType;
public static final int H263 = StdQTConstants.kH263CodecType;
// H.264 encoding, added because no constant is available in QTJava
public static final int H264 = QTUtils.toOSType("avc1");
public static final int JPEG = StdQTConstants.kJPEGCodecType;
public static final int MS_VIDEO = StdQTConstants.kMicrosoftVideo1CodecType;
public static final int MOTION_JPEG_A = StdQTConstants.kMotionJPEGACodecType;
public static final int MOTION_JPEG_B = StdQTConstants.kMotionJPEGBCodecType;
public static final int SORENSON = StdQTConstants.kSorensonCodecType;
public static final int VIDEO = StdQTConstants.kVideoCodecType;
public static final int WORST = StdQTConstants.codecMinQuality;
public static final int LOW = StdQTConstants.codecLowQuality;
public static final int MEDIUM = StdQTConstants.codecNormalQuality;
public static final int HIGH = StdQTConstants.codecHighQuality;
public static final int BEST = StdQTConstants.codecMaxQuality;
public static final int LOSSLESS = StdQTConstants.codecLosslessQuality;
private int width;
private int height;
private boolean readyForFrames;
// Changed from 1000 to 600 in release 0154 to enable exact 30 fps output.
// http://dev.processing.org/bugs/show_bug.cgi?id=988
private int TIME_SCALE = 600;
// QT Stuff
private VideoMedia videoMedia;
private Track videoTrack;
private Movie movie;
private QTFile movFile;
private CSequence seq;
private QTHandle imageHandle;
private QDGraphics gw;
private QDRect bounds;
private ImageDescription imgDesc;
private RawEncodedImage compressedImage;
private int rate;
private int keyFrameRate = 15;
private int codecType, codecQuality;
// my hack to make sure we don't get error -8691
private boolean temporalSupported = true;
private PApplet parent;
/**
* Create a movie with the specified width, height, and filename.
* The movie will be created at 15 frames per second.
* The codec will be set to RAW and quality set to HIGH.
*/
public MovieMaker(PApplet p, int _w, int _h, String _filename) {
this(p, _w, _h, _filename, 30, RAW, HIGH, 15);
}
/**
* Create a movie with the specified width, height, filename, and frame rate.
* The codec will be set to RAW and quality set to HIGH.
*/
public MovieMaker(PApplet p, int _w, int _h, String _filename, int _rate) {
this(p, _w, _h, _filename, _rate, RAW, HIGH, 15);
}
/**
* Create a movie with the specified width, height, filename, frame rate,
* and codec type and quality. Key frames will be set at 15 frames.
*/
public MovieMaker(PApplet p, int _w, int _h, String _filename, int _rate,
int _codecType, int _codecQuality) {
this(p, _w, _h, _filename, _rate, _codecType, _codecQuality, 15);
}
/**
* Create a movie with the specified width, height, filename, frame rate,
* codec type and quality, and key frame rate.
*/
public MovieMaker(PApplet p, int _w, int _h, String _filename, int _rate,
int _codecType, int _codecQuality,
int _keyFrameRate) {
parent = p;
width = _w;
height = _h;
rate = _rate;
try {
QTSession.open();
} catch (QTException e1) {
e1.printStackTrace();
}
try {
ImageDescription imgD = null;
if (quicktime.util.EndianOrder.isNativeLittleEndian()) {
imgD = new ImageDescription(QDConstants.k32BGRAPixelFormat);
} else {
imgD = new ImageDescription(QDGraphics.kDefaultPixelFormat);
}
imgD.setWidth(width);
imgD.setHeight(height);
gw = new QDGraphics(imgD, 0);
} catch (QTException e) {
e.printStackTrace();
}
codecType = _codecType;
codecQuality = _codecQuality;
keyFrameRate = _keyFrameRate;
initMovie(_filename);
parent.registerDispose(this);
}
private void initMovie(String filename) {
try {
String path = parent.savePath(filename);
movFile = new QTFile(new File(path));
movie = Movie.createMovieFile(movFile, StdQTConstants.kMoviePlayer, StdQTConstants.createMovieFileDeleteCurFile);
int timeScale = TIME_SCALE; // 100 units per second
videoTrack = movie.addTrack(width, height, 0);
videoMedia = new VideoMedia(videoTrack, timeScale);
videoMedia.beginEdits();
bounds = new QDRect(0, 0, width, height);
int rawImageSize = QTImage.getMaxCompressionSize(gw, bounds, gw.getPixMap().getPixelSize(), codecQuality, codecType, CodecComponent.anyCodec);
imageHandle = new QTHandle(rawImageSize, true);
imageHandle.lock();
compressedImage = RawEncodedImage.fromQTHandle(imageHandle);
seq = new CSequence(gw, bounds, gw.getPixMap().getPixelSize(), codecType, CodecComponent.bestFidelityCodec, codecQuality, codecQuality, keyFrameRate, null, 0);
imgDesc = seq.getDescription();
readyForFrames = true;
} catch (QTException e) {
if (e.errorCode() == Errors.noCodecErr) {
if (imageHandle == null) {
// This means QTImage.getMaxCompressionSize() failed
System.err.println("The specified codec is not supported, " +
"please ensure that the parameters are valid, " +
"and in the correct order.");
} else {
// If it's a -8961 error, quietly do it the other way
// (this happens when RAW is specified)
temporalSupported = false;
readyForFrames = true;
}
} else if (e.errorCode() == Errors.fBsyErr) {
System.err.println("The movie file already exists. " +
"Please delete it first.");
} else {
e.printStackTrace();
}
}
}
// A simple add function to just add whatever is in the parent window
public void addFrame() {
// http://dev.processing.org/bugs/show_bug.cgi?id=692
parent.flush();
parent.loadPixels();
addFrame(parent.pixels, parent.width, parent.height);
}
public void addFrame(int[] _pixels, int w, int h) {
if (readyForFrames){
RawEncodedImage pixelData = gw.getPixMap().getPixelData();
int rowBytes = pixelData.getRowBytes() / 4;
int[] newpixels = new int[rowBytes*h];
for (int i = 0; i < rowBytes; i++) {
for (int j = 0; j < h; j++) {
if (i < w) {
newpixels[i+j*rowBytes] = _pixels[i+j*w];
} else {
newpixels[i+j*rowBytes] = 0;
}
}
}
pixelData.setInts(0,newpixels);
compressAndAdd();
}
}
private void compressAndAdd() {
try {
if (temporalSupported) {
CompressedFrameInfo cfInfo = seq.compressFrame(gw, bounds, StdQTConstants.codecFlagUpdatePrevious, compressedImage);
boolean syncSample = cfInfo.getSimilarity() == 0; // see developer.apple.com/qa/qtmcc/qtmcc20.html
videoMedia.addSample(imageHandle, 0, cfInfo.getDataSize(), TIME_SCALE/rate, imgDesc, 1, syncSample ? 0 : StdQTConstants.mediaSampleNotSync);
} else {
imgDesc = QTImage.fCompress(gw,gw.getBounds(),32,codecQuality,codecType, CodecComponent.anyCodec, null, 0, RawEncodedImage.fromQTHandle(imageHandle));
boolean syncSample = true; // UM, what the hell should this be???
videoMedia.addSample(imageHandle, 0, imgDesc.getDataSize(), TIME_SCALE/rate, imgDesc, 1, syncSample ? 0 : StdQTConstants.mediaSampleNotSync);
}
} catch (QTException e) {
e.printStackTrace();
}
}
/**
* Close out and finish the movie file.
*/
public void finish() {
try {
if (readyForFrames) {
//System.out.println("Finishing movie file.");
readyForFrames = false;
videoMedia.endEdits();
videoTrack.insertMedia(0, 0, videoMedia.getDuration(), 1);
OpenMovieFile omf = OpenMovieFile.asWrite(movFile);
movie.addResource(omf, StdQTConstants.movieInDataForkResID,
movFile.getName());
}
} catch (StdQTException se) {
se.printStackTrace();
} catch (QTException qe) {
qe.printStackTrace();
}
}
public void dispose() {
if (readyForFrames) finish();
try {
QTSession.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}