Sunday, December 7, 2008

Programming: Lossless editing (cut/paste) of quicktime movies

Modern cameras use the quicktime format, which is very easy to cut/paste losslessly.
This means that one can edit the movies shot during the vacations without worsening the already (relatively) low quality.

Specifically, it takes one or more files with the timings (start/end) and merges them into a destination file.

It's not usable by a non-programmer, but actually it's very easy to create a gui for it; unfortunately java doesn't help deadly simple applications deploying.


/*
* License: you can do what the heck you want with this file, as long as you
* reference me as starting author.
*
* @author Saverio Miroddi (com.inbox@pub.saverio - reverse the tokens to get
* the email)
*/
import quicktime.QTException;
import quicktime.QTSession;
import quicktime.io.IOConstants;
import quicktime.io.OpenMovieFile;
import quicktime.io.QTFile;
import quicktime.std.StdQTConstants;
import quicktime.std.movies.Movie;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

class SourceData
{
public final String fileName;
public final float start;
/** -1 = end */
public final float end;

public SourceData(String fileName, float start, float end)
{
this.fileName = fileName;
this.start = start;
this.end = end;
}
}

/**
* Edits (cuts/pastes) pieces of quicktime movies into a destination quicktime
* file.

* The advantage of using this method is that it's lossless - very useful when
* editing movies shot with a portable camera, which are already (relatively)
* low quality.
*


* Requires quicktime wrapper (QTJava.zip), that is installed along with
* QuickTime, in the lib/ext folder of the JRE); at the time of writing, the QT
* version is 7.5
*


* Note: I never tried executing it from the commandline, I always edited the
* params directly in the code, but it should work fine.
*/
public class QuickTimeEditing
{
private static final int SOURCE_TIMESCALE = 30;
private static final String REF_FILENAME = "qt_edit.ref.tmp";

public static void main(String[] args) throws Exception
{
args = new String[] {
"d:/tmp/male_singer_falsetto.mov",
"d:/desktop/videos/heli0.mov", "0", "-1",
"d:/desktop/videos/heli1.mov", "0", "-1",
"d:/desktop/videos/heli8.mov", "0", "-1",
"d:/desktop/videos/heli9.mov", "0", "-1",
"d:/desktop/videos/heli10.mov", "0", "-1",
};

String destFile = args[0];
SourceData[] sourceData = extractSourceData(args);

new QuickTimeEditing().edit(destFile, sourceData);
}

/**
* @param args first is skipped
*/
private static SourceData[] extractSourceData(String... args)
{
List sourceData = new ArrayList();

for (int i = 1; i < args.length; )
{
String filename = args[i++];
float start = Float.parseFloat(args[i++]);
float end = Float.parseFloat(args[i++]);

sourceData.add(new SourceData(filename, start, end));
}

return sourceData.toArray(new SourceData[0]);
}

// PUBLIC INSTANCE METHODS /////////////////////////////////////////////////

public void edit(String destFile, SourceData... sourcesData) throws Exception
{
String refFile = createRefFilename(destFile);

try {
System.out.println("Opening...");

QTSession.open();

joinSourceMovies(destFile, refFile, sourcesData);
}
finally {
System.out.println("Closing...");

QTSession.close();

System.out.println("Cleaning up...");

cleanupRefFiles(refFile);
}
}

// PRIVATE INSTANCE METHODS /////////////////////////////////////////////////

private final String createRefFilename(String destFile) throws IOException
{
File destFileDir = new File(destFile).getParentFile();
return new File(destFileDir, REF_FILENAME).getCanonicalPath();
}

private void joinSourceMovies(String destFile, String refFile, SourceData... sourcesData) throws QTException, IOException
{
Movie refMovie = Movie.createMovieFile(new QTFile(refFile),
StdQTConstants.kMoviePlayer, StdQTConstants.createMovieFileDeleteCurFile);
refMovie.setTimeScale(SOURCE_TIMESCALE);

int lastTimeScaled = 0;

for (SourceData sourceData : sourcesData)
{
System.out.println("Processing '" + sourceData.fileName + "' (" + sourceData.start + "->" + sourceData.end + ")...");

Movie sourceMovie = Movie.fromFile(OpenMovieFile.asRead(new QTFile(sourceData.fileName)));
int timeScale = sourceMovie.getTimeScale();

if (timeScale != SOURCE_TIMESCALE) throw new RuntimeException("Too lazy to process time scales != " + SOURCE_TIMESCALE + ": " + timeScale);

int startTimeScaled = (int)(sourceData.start * timeScale);
int lengthScaled = (int)(sourceData.end > 0 ?
sourceData.end * timeScale :
sourceMovie.getDuration() - startTimeScaled);

sourceMovie.insertSegment(refMovie, startTimeScaled, lengthScaled, lastTimeScaled);

lastTimeScaled += lengthScaled;
}

refMovie.flatten (0, // movieFlattenFlags
new QTFile(destFile),
StdQTConstants.kMoviePlayer, // creator
IOConstants.smSystemScript, // scriptTag
StdQTConstants.createMovieFileDeleteCurFile, // createQTFileFlags
StdQTConstants.movieInDataForkResID, // resId
destFile);

}

private void cleanupRefFiles(String refFile)
{
new File(refFile).delete();
new File(refFile + ".#res").delete();
}
}

No comments: