For my current timelapse video project, I collect over 12,000 images almost every day, and need to automate the processing as much as possible. In my notes on the first video, Mountain View Rainstorm, I explained why I chose to create a QuickTime movie from the still image sequence before video editing, and in this article I explore how to automate the QuickTime movie production from the stills.
I thought this article would be short and simple, but it raised many questions ... "You take the red pill - you stay in Wonderland and I show you how deep the rabbit-hole goes." -- The Matrix 1999
.
Try Built-In Automation First
QuickTime Pro already provides a very convenient way to make a movie from a sequence of stills. If you're only doing this operation occasionally, you don't need to automate it further. Simply open QuickTime and use the "Open Image Sequence" command. When it asks you to open a file: give it the
first file in your sequence. For example, if my files are named "MTV_0001" to "MTV_12345", I specify "MTV_0001".
The "Open A File" dialog is also where you specify the framerate of your movie.
Yes, your files must be numbered. If you're trying to make a movie out of "red.jpg", "orange.jpg", "yellow.jpg" etc. it won't work. Fix the names.
For most of us that's all we need. Thanks for stopping by!
Scripting QuickTime Operations
A Google search for "
quicktime automate" leads very quickly Luc-Eric Rousseau's 2006 XSIBlog post
Automating QuickTime at the Command Line on Windows in which he provides a script to do almost exactly what I want. Hooray! But there were a few tricks involved in making it work the way I wanted -- truthfully it took me several days -- and in the process I learned more about QuickTime, its COM interface, and the "
Windows Script Host" (WSH) facility.
Using qtMovieFromStills.js
My adaptation of Rousseau's script is called qtMovieFromStills.js. Like his, it is written in
JScript and runs from a Windows command shell using WSH. Here's how to use the script:
- Copy qtMovieFromStills.js from the bottom of this post, paste it into an editor (e.g. Notepad) and save it somewhere convenient -- I keep a "scripts" folder inside my project folder.
- Open a command window and run it using WSH, i.e. with the cscript command.
- qtMovieFromStills has two required and one optional argument:
- Windows file path to the first image in the sequence.
E.g. "L:\Images\2010-01-19\MTV_0001.jpg"
- Windows file path to the desired output file.
E.g. "L:\Images\MTV_2010-01-19.mov"
- Optionally, specify the framerate in frames-per-second. E.g. "60". It defaults to 30. A list of supported frame rates appears in the QuickTime "Open A File" dialog.
- So, your shell command looks something like this.
cscript qtMovieFromStills.js L:\Images\01-19\MTV_0001.jpg L:\Images\MV01-19.mov 60
Difficulties
The first problem I ran into turned out to be very simple: I was using relative file paths in the arguments passed to QuickTime and this does not work well. Apparently the QuickTime components do not recognize your current working directory, nor did the script provide useful error messages. You must
provide full paths (Like "C:\bletch\srcfile.jpg") rather than relative paths to the source and dest files.
The second problem is that the QuickTime export operation will fail beyond a certain number of frames. And once again there is no useful error message. The exact number depends on your machine resources -- on my larger machine the export was successful with 9,000 frames but failed with 12,000. On my laptop it fails for 7,000 frames. The workaround is to split them into two (or more) folders and generate separate movies.
The original script had little error detection and reporting. I've added a lot more, but it doesn't yet work as well as I would like.
Automating the Conversion Process
Entering the script command itself is
not easier than using the QuickTime GUI. Where it pays off is in running it repeatedly over a batch of folders or automatically as part of a more general workflow automation.
Here's a little bash shell script I use with Cygwin to process a series of directories. Note how the Windows-style paths must be escaped to the bash shell.
frameRate=60
basedir="L:\\Graphics\\Images\\SX110\\MTV-TL01"
dirs="2009-12-09 2009-12-10 2009-12-11 2009-12-12 2009-12-14"
script="$basedir\\scripts\\qtMovieFromStills.js"
for d in $dirs; do
date
echo $d
src="$basedir\\$d\\MTV-TL01_0001.JPG"
dst="$basedir\\MTV-TL01_$d.mov"
cscript $script $src $dst $frameRate
done
Internals of qtMovieFromStills.js
If you just want to use the script, you can stop reading here.
At this point, most of the script is error checking and reporting, and unraveling the QuickTime object hierarchy. The essentials are in only
two lines.
qtControl.CreateNewMovieFromImages(sourcePath, frameRate, true);
This function does essentially what the QuickTime GUI does: makes a movie out of an image sequence.
qtExporter.BeginExport();
The Exporter takes QuickTime's internal representation of your movie and renders it to a file. This takes the most time.
The one other piece that's worth some explanation is the way the script saves your export codec settings for reuse. They are saved as an opaque blob of binary data in the XML file:
C:\qtMovieFromStillsCodecInfo.xml
Whenever you want to change the settings, just delete the file. Details on how this works are
discussed by Cromie in a post to the quicktime-api mail list, as well as by Rousseau in his blog post.
References
- Automating QuickTime at the Command Line on Windows… XSIBlog article by Luc-Eric Rousseau, 2006. This excellent post got me started in this direction.
- QuickTime for .NET and COM Developers by John Cromie (Elsevier 2006). The sample scripts in Chapter 8 were used by Rousseau in developing his script.
- "Scripting the COM/ActiveX Control in QuickTime 7 for Windows", Apple Developer Connection (2005-09-12). This is the only official Apple documentation on this scripting interface that I have been able to find so far. It refers to the "JavaScript Scripting Guide for QuickTime" but the current version of that document does not include the host-based scripting.
- "Programmatic Export using QTControl", John Cromie comments in Apple quicktime-api mail list (October 2006).
- QuickTime 7 for Windows Update Guide. This "legacy" Apple document proved quite elusive! I finally found a link to it from the collected listing of "What's New in QuickTime". Unfortunately, it still doesn't have what we want. It says "Note: The API for controlling the QuickTime ActiveX browser plug-in using JavaScript can be found here." However the link is bad!
- "Windows Script Host" (WSH) Microsoft MSDN.
- JScript (Windows Script Technologies) Microsoft MSDN. Essential reference for the JavaScript dialect used in WSH.
- QuickTime Overview, Mac OS X Reference Library. Apple's starting point for QuickTime developers.
- JavaScript Scripting Guide for QuickTime, Apple Mac OS X Reference Library. Not as useful for our purposes as it sounds -- the current (2008) version of this document is about controlling a QuickTime plugin within a browser, not host-based scripting. The document itself says in its Introduction "If you want to develop Windows desktop applications or server-side scripts on the Windows OS using JavaScript and QuickTime, see QuickTime 7 for Windows Update Guide." However, when I finally found that document I discovered that the JavaScript scripting section was not there.
- Apple Technical Note 2120: QuickTime for Windows ActiveX/COM Frequently Asked Questions (2006-05), Rousseau mentions this FAQ in his code. It is an excellent source of examples in C# and VBScript using the host-based script interface.
- "Using OLEView to Explore the QuickTime COM Interface", Chip Chapin January 2010.
- Microsoft Windows OLEView. The link is for the Windows 2000 Resource Kit version. You can also get it as part of the Windows Server 2003 Resource Kit.
- "Exploiting Perl on Windows with OLE/COM" (PowerPoint) presentation by Roth Consulting provides an excellent overview of Component Object Model (COM) and how it relates to WSH scripting.
Source Code: qtMovieFromStills.js
Copy the code and paste it into an editor. Save it as qtMovieFromStills.js.
// Windows Script Host JScript
// Create a QuickTime movie from a sequence of still images.
// Run from the command line on Windows using WSH:
// cscript qtMovieFromStills.js sourcepath destpath [framerate]
//
// Authors:
// Chip Chapin <cchapin@gmail.com> has extended a script by
// by Luc-Eric Rousseau (XSIBlog 2006, http://www.xsi-blog.com/archives/103)
// which was in turn based on sample code by John Cromie that
// accompanied his book "QuickTime for .NET and COM Developers"
// (Elsevier 2006, http://www.skylark.ie/qt4.net/samplecode.asp).
//
// My changes provide
// -- Additional error checking/reporting of various kinds.
// -- The script can be run in a loop without crashing.
// -- Verbose progress reporting.
// Last Update: 2010-01-30
// Be sure to use FULLY-QUALIFIED Windows file paths, even from Cygwin.
// Relative paths don't work, and there is no error report from QT.
// TODO: can we get a progress bar during the initial rendering stage?
/*
* Check existence of the parent folder of a file path.
* @param {fso} fso
* @param {string} fname
* @return {boolean}
*/
function CheckFileFolder(fso, fname) {
s = fso.GetParentFolderName(fname);
return(fso.FolderExists(s));
}
// Get script arguments
if (WScript.Arguments.Length >= 2) {
sourcePath = WScript.Arguments(0);
destPath = WScript.Arguments(1);
if (WScript.Arguments.Length >= 3) {
frameRate = WScript.Arguments(2);
} else {
frameRate = 30;
}
WScript.Echo("FrameRate: " + frameRate);
} else {
WScript.Echo("not enough parameters");
WScript.Quit();
}
var fso = WScript.CreateObject("Scripting.FileSystemObject");
if (!fso.FileExists(sourcePath)) {
WScript.Echo("Missing source file '" + sourcePath +"'");
WScript.Quit();
}
if (!CheckFileFolder(fso, destPath)) {
WScript.Echo("Bad destination file path '" + destPath +"'");
WScript.Quit();
}
// Launch QuickTime Player Application
var qtPlayerApp = WScript.CreateObject("QuickTimePlayerLib.QuickTimePlayerApp");
WScript.Sleep(8000); // Give it time to launch.
if (!qtPlayerApp) {
WScript.Echo("ERROR Failed to launch QuickTime Player App.");
WScript.Quit();
}
// Get the QuickTime player and its associated controller.
var qtPlayer = qtPlayerApp.Players(1);
if (qtPlayer == null) {
WScript.Echo("ERROR Failed to get QuickTime Player.");
WScript.Quit();
}
WScript.Echo("Got player '" + qtPlayer.Caption + "'");
var qtControl = qtPlayer.QTControl;
// Now create the movie.
WScript.Echo("Creating new movie from stills at '" + sourcePath + "'...");
try {
qtControl.CreateNewMovieFromImages(sourcePath,
frameRate,
true ); // rate is in frames per second
}
catch (e) {
WScript.Echo("ERROR creating movie: " + e.number +
" (" + (e.number>>16 & 0x1FFF) +
"-" + (e.number & 0xffff) + ")");
WScript.Echo(e.description);
// TODO: Find more reliable error reporting.
// The following doesn't work if the QTControl object is gone.
WScript.Echo("QuickTime error " + qtControl.ErrorCode);
var qte = qtControl.QuickTime.Error;
WScript.Echo(" " + qte.ErrorCode + ", " + qte.Description);
WScript.Echo(" " + qte.SourceReference);
WScript.Quit();
}
var qtMovie = qtControl.Movie;
if (!qtMovie) {
WScript.Echo("ERROR: No movie created (" + qtControl.ErrorCode + ")");
WScript.Quit();
}
var duration = qtMovie.Duration;
if (duration == 0) {
// This test isn't as helpful as I thought it would be. I thought it
// would catch the case where QT does not have a valid input file, but in
// that case it seems to create a two-second empty movie
// (ie. duration = 20*framerate).
WScript.Echo("ERROR: Movie has duration 0.");
WScript.Quit();
}
// Duration appears to be the number of frames * 10.
WScript.Echo("Created new movie, duration " + duration);
// Set up the exporter.
var qt = qtPlayer.QTControl.QuickTime;
if (qt.Exporters.Count == 0) {
// Only add an exporter if needed.
qt.Exporters.Add();
WScript.Echo("Adding new Exporter.");
} else {
WScript.Echo("Using existing Exporter.");
}
var qtExporter = qt.Exporters(1);
if (!qtExporter) {
WScript.Echo("ERROR: Unable to get Exporter.");
WScript.Quit();
}
qtExporter.TypeName = "QuickTime Movie";
qtExporter.SetDataSource( qtMovie );
// Configure the exporter.
var CodecInfoFileName = "C:\\qtMovieFromStillsCodecInfo.xml";
var CodecFileInfo;
if (fso.FileExists(CodecInfoFileName)) {
WScript.Echo("Reading codec config from '" + CodecInfoFileName + "'");
CodecFileInfo = fso.OpenTextFile( CodecInfoFileName );
}
if (CodecFileInfo) {
var xmlCodecInfoText = CodecFileInfo.ReadAll();
// cause the exporter to be reconfigured
// http://developer.apple.com/technotes/tn2006/tn2120.html
var tempSettings = qtExporter.Settings;
tempSettings.XML = xmlCodecInfoText;
qtExporter.Settings = tempSettings;
} else {
qtExporter.ShowSettingsDialog();
var xmlCodecInfoText = qtExporter.Settings.XML;
CodecFileInfo = fso.CreateTextFile( CodecInfoFileName );
if (CodecFileInfo) {
CodecFileInfo.WriteLine(xmlCodecInfoText);
CodecFileInfo.Close();
} else {
WScript.Echo("Warning: failed to save codec info to '"
+ CodecInfoFileName + "'");
WScript.Echo("continuing ...");
}
}
// Export the movie.
WScript.Echo("Exporting ...");
try {
qtExporter.DestinationFileName = destPath;
qtExporter.ShowProgressDialog = true;
// Uncomment this line if you want the export dialog box to appear.
// qtExporter.ShowExportDialog();
qtExporter.BeginExport();
WScript.Echo("Exported to '" + destPath + "'");
}
catch (e) {
WScript.Echo("ERROR " + (e.number>>16 & 0x1FFF) +
"-" + (e.number & 0xffff) +
" exporting '" + destPath + "'");
WScript.Echo(e.description);
// JSON object only available in WSH 5.8+
// WScript.Echo(JSON.stringify(e, null, 2));
var qte = qt.Error;
WScript.Echo("QuickTime Error " + qte.ErrorCode + ", " + qte.Description);
//WScript.Echo(JSON.stringify(qte, null, 2));
}
// Close the player?
// Note: closing the player results in failures for subsequent invocations.
//qtPlayer.Close();
// End.