Sep 00 QTToolkit
Volume Number: 16 (2000)
Issue Number: 9
Column Tag: QuickTime Toolkit
The Atomic Cafe
by Tim Monroe
Working With Atoms and Atom Containers
Introduction
In the past two QuickTime Toolkit articles, we've been concerned at least in part with atoms, the basic building-blocks of QuickTime movie files. Atoms are utterly simple in structure (a 4-byte length field, followed by a 4-byte type field, followed by some data), and this utter simplicity means that atoms can be used for a very wide range of tasks. Indeed, the atom-based structure used by QuickTime movie files is so general and so flexible that it has been adopted by the International Standards Organization (ISO) as the basis for the development of a unified digital media storage format for the emerging MPEG-4 specification.
In this article, we're going to continue investigating the basic structure of QuickTime files as sequences of atoms. You might recall that in the previous article we left some unfinished business lying around. Namely, we need to see how to replace an existing atom of a particular type instead of just adding a new atom of that type. It turns out that handling this task in the general case is reasonably difficult, since we can't safely move the movie data atom around in a movie file without doing a lot of work. For the present, we'll be content to see how to amend the QTInfo_MakeFilePreview function we developed last time so that there is at most one 'pnot' atom in a QuickTime movie file.
Because an atom can contain any kind of data whatsoever, it can contain data that consists of one or more other atoms. So, atoms can be arranged hierarchically. We'll take a few moments to consider the hierarchical arrangement of a movie atom (the main repository of bookkeeping information in a QuickTime movie file). Then we'll show how to put our atom-fusing powers to work to create a shortcut movie file, a QuickTime movie file that does nothing more than point to some other QuickTime movie file.
Once we've played with atoms for a while, we're going to shift gears rather abruptly to consider a second kind of atom-based structure, which we'll call an atom container. Atom containers are structures of type QTAtomContainer that are often used inside of QuickTime movie data atoms to store various kinds of information (for example, media samples). They were developed primarily to address some of the shortcomings of atoms. In particular, the Movie Toolbox provides an extensive API for working with atom containers; among other things, this API makes it easy to create and access data arranged hierarchically within atom containers.
We'll get some hands-on experience with atom containers in two ways. First, we'll see how to get and set the user's Internet connection speed preference, which is a piece of information that QuickTime stores internally and happily gives, in the form of an atom container, to anyone who asks. Second, we'll see how to add a movie track to a QuickTime movie. By using movie tracks, we can embed entire QuickTime movies inside of other QuickTime movies. The movie media handler, which manages movie tracks, is one of the most exciting new features of QuickTime 4.1. Once we understand how to work with atom containers, it'll be easy to add movie tracks to an existing QuickTime movie.
Before we begin, though, a word about terminology. As you've been warned, this article is going to discuss two different ways of organizing data, both of which are (for better or worse) called "atoms". The first kind of atom is the one that's used to define the basic structure of QuickTime movie files. The second kind of atom is the one that was introduced in QuickTime 2.1 for storing data in some kinds of media samples (and for other tasks as well); these kinds of atoms are structures of type QTAtom that are stored inside of an atom container.
Some of Apple's QuickTime documentation refers to the first kind of atom as a classic atom (perhaps in the same spirit that one refers to a classic car: it's been around a while) and to the second kind of atom as a QT atom (drawing of course on the data type QTAtom). Some other documentation refers to the first kind of atom as a chunk atom (perhaps because it's just a chunk of data?). I'm not particularly happy with any of these terms, so I'm going to refer to the first kind of atom simply as an atom and to the second kind as an atom container atom. In other words, an atom container atom (of type QTAtom) is always found inside of an atom container (of type QTAtomContainer). Generally, here and in the future, the context will make it clear which kind of atom we're considering, so we can usually get by just talking about atoms.
File Previews: The Sequel
In the previous article ("The Informant" in MacTech, July 2000), we saw how to add a 'pnot' atom to a QuickTime movie file, to create a single-fork movie file with a file preview. (A file preview is the movie clip, image, text, or other data that appears in the preview pane of the file-opening dialog box displayed by a call to StandardGetFilePreview or NavGetFile.) Our strategy was simple: each time the user saves a movie, add a preview atom and (if necessary) a preview data atom to the QuickTime movie file. But we recognized that ultimately we would need to refine this strategy to avoid ending up with multiple preview atoms and preview data atoms. It's time to make some changes to our QTInfo application. In this section, we'll see how to upgrade QTInfo into a nearly-identical application, called QTInfoPlus, that handles file preview atoms correctly.
Removing Existing Previews
In fact, we can solve this little problem by adding a single line of code to the QTInfo_MakeFilePreview function. Immediately after determining that the file reference number passed to QTInfo_MakeFilePreview picks out a data file, we can execute this code:
QTInfo_RemoveAllPreviewsFromFile(theRefNum);
The QTInfo_RemoveAllPreviewsFromFile function looks through the specified open data file and removes any existing preview atoms (that is, atoms of type 'pnot') from that file. In addition, this function removes any preview data atoms referenced by those preview atoms, unless the preview data atoms are of type 'moov'. (We don't want to remove atoms of type 'moov', of course, since they contain essential information about the movie file.) QTInfo_RemoveAllPreviewsFromFile is defined in Listing 1.
Listing 1: Removing all preview atoms from a QuickTime movie file
QTInfo_RemoveAllPreviewsFromFile
OSErr QTInfo_RemoveAllPreviewsFromFile (short theRefNum)
{
long myAtomType = 0L;
short myAtomIndex = 0;
short myCount = 0;
OSErr myErr = noErr;
// count the preview atoms in the file
myCount = QTInfo_CountAtomsOfTypeInFile(theRefNum, 0L,
ShowFilePreviewComponentType);
while (myCount > 0) {
// get the preview data atom targeted by this preview atom
myAtomType = ShowFilePreviewComponentType;
myAtomIndex = myCount;
myErr = QTInfo_FindPreviewAtomTarget(theRefNum,
&myAtomType, &myAtomIndex);
// if the preview data atom is the last atom in the file, remove it
// (unless it's a 'moov' atom)
if (myErr == noErr)
if (myAtomType != MovieAID)
if (QTInfo_IsLastAtomInFile(theRefNum,
myAtomType, myAtomIndex))
QTInfo_RemoveAtomFromFile(theRefNum,
myAtomType, myAtomIndex);
// remove or free the preview atom
if (QTInfo_IsLastAtomInFile(theRefNum,
ShowFilePreviewComponentType, myCount))
QTInfo_RemoveAtomFromFile(theRefNum,
ShowFilePreviewComponentType, myCount);
else
QTInfo_FreeAtomInFile(theRefNum,
ShowFilePreviewComponentType, myCount);
// if the preview data atom still exists, remove or free it (unless it's a 'moov' atom)
if (myErr == noErr)
if (myAtomType != MovieAID)
if (QTInfo_IsLastAtomInFile(theRefNum,
myAtomType, myAtomIndex))
QTInfo_RemoveAtomFromFile(theRefNum,
myAtomType, myAtomIndex);
else
QTInfo_FreeAtomInFile(theRefNum,
myAtomType, myAtomIndex);
myCount-;
}
return(myErr);
}
As you can see, QTInfo_RemoveAllPreviewsFromFile calls a handful of other functions defined by our application. These other functions do things like count the number of existing preview atoms, find the preview data atom that is the target of a preview atom, determine whether a given atom is the last atom in the file, and so forth.
QTInfo_RemoveAllPreviewsFromFile puts these functions to work like this: for each preview atom in the file (starting with the one nearest the end of the file), find the preview data atom that is referenced by the preview atom. If that target atom isn't a movie atom and it's the last atom in the file, remove it from the file. Then, if the preview atom is the last atom in the file, remove it as well. If the preview atom isn't the last atom in the file, then change it into a free atom (that is, an atom whose type is FreeAtomType). By changing the atom type, we're converting the preview atom into a block of unused space at its current location in the movie file. QuickTime simply ignores any atoms of type FreeAtomType that it encounters when reading through a movie file.
You might think that we could just remove a preview atom from the file and, if it isn't the last atom in the file, move any following atoms up in the file. This would avoid creating "islands" of unused space in our file, but it would be a dangerous thing to do. That's because some atoms in a QuickTime file reference data in other atoms by storing offsets from the beginning of the file. In general, we want to avoid moving atoms around in a QuickTime movie file. It's safer just to convert any unwanted atoms that are not at the end of the file into free atoms.
Once we've removed a preview atom (if it's the last atom in the file) or converted it into a free atom (if it isn't), we then look once again to see if the preview data atom is the last item in the file. This might happen if the preview data atom originally preceded the preview atom and the preview atom was the last atom in the file. If the preview data atom is now the last atom in the file, it's removed; otherwise, it's converted into a free atom.
The net result of all this is to remove any existing preview and preview data atoms from the file, either by truncating the file to exclude those atoms or by converting them into free atoms. At this point, QTInfo_MakeFilePreview can safely add a new preview atom and (if necessary) a preview data atom to that file. So, when all is said and done, the QuickTime movie file will end up with exactly one preview atom and one preview data atom. In the next few subsections, we'll consider how to define the various QTInfoPlus functions called by QTInfo_RemoveAllPreviewsFromFile.
Finding and Counting Atoms
The most fundamental thing we need to be able to do, when working with a file that's composed of atoms, is find an atom of a specific type and index in that file. For instance, we might need to find the first movie atom, or the third preview atom, or the third 'PICT' atom in the file. So we want to devise a function that takes an atom type and an index and then returns to us the position in the file at which that atom begins, if there is an atom of that type and index in the file. Otherwise, the function should return some error.
This task is reasonably straightforward. All we need to do is start at the beginning of the file (or at some other offset in the file specified by the caller) and inspect the type of the atom at that location. If the desired index is 1 and the desired atom type is the type of that atom, we're done: we've found the desired atom. Otherwise, we need to keep looking. We can find the next atom in the file by moving forward in the file by the size of the atom currently under consideration. We continue inspecting each atom and moving forward in the file until we find the atom of the specified type and index or until we reach the end of the file. Listing 2 defines the function QTInfo_FindAtomOfTypeAndIndexInFile, which is our basic atom-finding tool.
Listing 2: Finding an atom in a QuickTime movie file
QTInfo_FindAtomOfTypeAndIndexInFile
OSErr QTInfo_FindAtomOfTypeAndIndexInFile (short theRefNum,
long *theOffset, long theAtomType, short theIndex,
long *theDataSize, Ptr *theDataPtr)
{
short myIndex = 1;
long myFileSize;
long myFilePos = 0L;
long myAtomHeader[2];
long mySize = 0L;
OSType myType = 0L;
Ptr myDataPtr = NULL;
Boolean isAtomFound = false;
OSErr myErr = paramErr;
if (theOffset == NULL)
goto bail;
if (QTInfo_IsRefNumOfResourceFork(theRefNum))
goto bail;
myFilePos = *theOffset;
// get the total size of the file
GetEOF(theRefNum, &myFileSize);
while (!isAtomFound) {
myErr = SetFPos(theRefNum, fsFromStart, myFilePos);
if (myErr != noErr)
goto bail;
// read the atom header at the current file position
mySize = sizeof(myAtomHeader);
myErr = FSRead(theRefNum, &mySize, myAtomHeader);
if (myErr != noErr)
goto bail;
mySize = EndianU32_BtoN(myAtomHeader[0]);
myType = EndianU32_BtoN(myAtomHeader[1]);
if ((myIndex == theIndex) &&
((theAtomType == myType)
|| (theAtomType == kQTInfoAnyAtomType))) {
// we found an atom of the specified type and index;
// return the atom if the caller wants it
if (theDataPtr != NULL) {
myDataPtr = NewPtrClear(mySize);
if (myDataPtr == NULL) {
myErr = MemError();
goto bail;
}
// back up to the beginning of the atom
myErr = SetFPos(theRefNum, fsFromStart, myFilePos);
if (myErr != noErr)
goto bail;
myErr = FSRead(theRefNum, &mySize, myDataPtr);
if (myErr != noErr)
goto bail;
}
isAtomFound = true;
} else {
// we haven't found an atom of the specified type and index; keep on looking
myFilePos += mySize;
if ((theAtomType == myType) ||
(theAtomType == kQTInfoAnyAtomType))
myIndex++;
// make sure we're moving forward in the file, but not too far...
if ((mySize <= 0) ||
(myFilePos > (myFileSize - sizeof(myAtomHeader)))) {
myErr = cannotFindAtomErr;
goto bail;
}
}
} // while (!isAtomFound)
// if we got to here, we found the correct atom
if (theOffset != NULL)
*theOffset = myFilePos;
if (theDataPtr != NULL)
*theDataPtr = myDataPtr;
if (theDataSize != NULL)
*theDataSize = mySize;
bail:
if (myErr != noErr)
if (myDataPtr != NULL)
DisposePtr(myDataPtr);
return(myErr);
}
QTInfo_FindAtomOfTypeAndIndexInFile returns to its caller the offset within the file of the beginning of the atom of the desired type and index. In addition, if the caller passes in non-NULL values in the theDataPtr or theDataSize parameters, QTInfo_FindAtomOfTypeAndIndexInFile returns a copy of the entire atom (including the atom header) or the atom size to the caller. The returned offset, data, and atom size can be used for a variety of purposes. For instance, Listing 3 defines the QTInfo_CountAtomsOfTypeInFile function, which counts the number of atoms of a specific type in a file.
Listing 3: Counting the atoms in a QuickTime movie file
QTInfo_CountAtomsOfTypeInFile
short QTInfo_CountAtomsOfTypeInFile (short theRefNum,
long theOffset, long theAtomType)
{
short myIndex = 0;
long myFilePos = theOffset;
long myAtomSize = 0L;
OSErr myErr = noErr;
if (QTInfo_IsRefNumOfResourceFork(theRefNum))
return(myIndex);
while (myErr == noErr) {
myErr = QTInfo_FindAtomOfTypeAndIndexInFile(theRefNum,
&myFilePos, theAtomType, 1, &myAtomSize, NULL);
if (myErr == noErr)
myIndex++;
myFilePos += myAtomSize;
// avoid an infinite loop...
if (myAtomSize <= 0)
break;
}
return(myIndex);
}
QTInfo_CountAtomsOfTypeInFile uses the offset and atom size returned by QTInfo_FindAtomOfTypeAndIndexInFile to walk through the file looking for atoms of the specified type.
Similarly, it's easy to use QTInfo_FindAtomOfTypeAndIndexInFile to determine whether a particular atom is the last atom in a file. We simply call QTInfo_FindAtomOfTypeAndIndexInFile to get the offset in the file of the given atom and then call it again to see if there are any atoms of any kind following that atom. Listing 4 defines a function that does precisely this.
Listing 4: Determining whether an atom is the last atom in a file
QTInfo_IsLastAtomInFile
Boolean QTInfo_IsLastAtomInFile (short theRefNum, long theAtomType, short theIndex)
{
Boolean isLastAtom = false;
long myOffset = 0L;
long myAtomSize = 0L;
OSErr myErr = noErr;
// find the offset and size of the atom of the specified type and index in the file
myErr = QTInfo_FindAtomOfTypeAndIndexInFile(theRefNum,
&myOffset, theAtomType, theIndex, &myAtomSize, NULL);
if (myErr == noErr) {
// look for an atom of any type following that atom
myOffset += myAtomSize;
myErr = QTInfo_FindAtomOfTypeAndIndexInFile(theRefNum,
&myOffset, kQTInfoAnyAtomType, 1, NULL, NULL);
if (myErr != noErr)
isLastAtom = true;
}
return(isLastAtom);
}
Finding the Preview Data Atom
Given a preview atom, we sometimes need to know which other atom is the target of that atom. In other words, we want to find the atom that we've been calling the preview data atom - the atom that contains the data for the file preview. This is fairly easy: we just need to read the data in the preview atom, which has the structure of a PreviewResourceRecord record. Listing 5 defines the QTInfo_FindPreviewAtomTarget function, which does this.
Listing 5: Finding the target of a preview atom
QTInfo_FindPreviewAtomTarget
OSErr QTInfo_FindPreviewAtomTarget (short theRefNum,
long *theAtomType, short *theIndex)
{
long myOffset = 0L;
PreviewResourceRecord myPNOTRecord;
long mySize;
OSErr myErr = noErr;
if ((theAtomType == NULL) || (theIndex == NULL))
return(paramErr);
// find the offset of the atom of the specified type and index in the file
myErr = QTInfo_FindAtomOfTypeAndIndexInFile(theRefNum,
&myOffset, *theAtomType, *theIndex, NULL, NULL);
if (myErr == noErr) {
// set the file mark to the beginning of the atom data
myErr = SetFPos(theRefNum, fsFromStart,
myOffset + (2 * sizeof(long)));
if (myErr == noErr) {
// read the atom data
mySize = sizeof(myPNOTRecord);
myErr = FSRead(theRefNum, &mySize, &myPNOTRecord);
if (myErr == noErr) {
*theAtomType = EndianU32_BtoN(myPNOTRecord.resType);
*theIndex = EndianS16_BtoN(myPNOTRecord.resID);
}
}
}
return(myErr);
}
QTInfo_FindPreviewAtomTarget calls QTInfo_FindAtomOfTypeAndIndexInFile to find the location of the atom whose type and index are passed to it in the theAtomType and theIndex parameters. Then it advances the file mark to the beginning of the atom data and reads the atom data into myPNOTRecord. The type and index of the preview data atom (suitably converted from big-endian to native-endian form) are then returned to the caller.
Removing and Freeing Atoms
It's quite easy to remove an atom from a file or convert it into a free atom. Listing 6 shows how we define the QTInfo_RemoveAtomFromFile function, which removes an atom from a file by truncating the file at the beginning of the atom (by calling SetEOF). Note that any atoms that follow the specified atom are also removed from the file. To avoid any problems, we should always call QTInfo_IsLastAtomInFile to make sure that the atom to be removed from the file is the last atom in the file.
Listing 6: Removing an atom from a file
QTInfo_RemoveAtomFromFile
OSErr QTInfo_RemoveAtomFromFile (short theRefNum,
long theAtomType, short theIndex)
{
long myOffset = 0L;
long myAtomSize = 0L;
OSErr myErr = noErr;
// find the offset of the atom of the specified type and index in the file
myErr = QTInfo_FindAtomOfTypeAndIndexInFile(theRefNum,
&myOffset, theAtomType, theIndex, &myAtomSize, NULL);
if (myErr == noErr)
myErr = SetEOF(theRefNum, myOffset);
return(myErr);
}
It's almost as easy to convert an atom into a free atom. All we need to do is position the file mark to the beginning of the type field in the atom header and write the value 'free' into that field. Listing 7 shows how we do this.
Listing 7: Freeing an atom in a file
QTInfo_FreeAtomInFile
OSErr QTInfo_FreeAtomInFile (short theRefNum,
long theAtomType, short theIndex)
{
OSType myType = EndianU32_NtoB(FreeAtomType);
long mySize = sizeof(myType);
long myOffset = 0L;
OSErr myErr = noErr;
// find the offset of the atom of the specified type and index in the file
myErr = QTInfo_FindAtomOfTypeAndIndexInFile(theRefNum,
&myOffset, theAtomType, theIndex, NULL, NULL);
if (myErr == noErr) {
// change the atom type to 'free'
myErr = SetFPos(theRefNum, fsFromStart,
myOffset + sizeof(long));
if (myErr == noErr)
myErr = FSWrite(theRefNum, &mySize, &myType);
}
return(myErr);
}
So far, then, we've managed to define a handful of utility functions that allow us to find atoms in files, get the sizes of those atoms, remove atoms from files, count the number of atoms of a specific type in a file, and so forth. These are precisely the functions called by the QTInfo_RemoveAllPreviewsFromFile function, which we're using to make sure that any QuickTime movie file we create or edit has at most one preview resource. It would be easy (and fun) to define an entire library of atom utilities, but we'll have to restrain our programming urges here. We've got other work to do.
Shortcut Movie Files
I mentioned earlier that an atom can contain any kind of data, and in particular it can contain other atoms. That is to say, atoms can be arranged hierarchically. Up to now, however, we've worked with a QuickTime movie file as a mere concatenation of atoms. We've looked at the data of several of those atoms, but we haven't yet met any atoms that contain other atoms. It's time for that to change.
A good example of an atom that contains other atoms is the movie atom itself. A typical movie atom contains a track atom (of type TrackAID) for each track in the QuickTime movie, along with other atoms that contain the movie metadata. A movie atom can also contain a movie user data atom (of type UserDataAID), which contains the movie user data. A track atom, in turn, contains other atoms that define the track characteristics and that point to the media data. And so on, as deep as is necessary to completely characterize a movie and its data.
An atom that contains no other atoms is called a leaf atom. A leaf atom may or may not actually contain any data. Typically a leaf atom does contain data, but it's possible that the very presence in the file of the atom has significance. In that case, the leaf atom consists solely of the 8-byte atom header. An atom that contains one or more other atoms is called a container atom. A movie atom is a container atom. By contrast, a preview atom is a leaf atom, since it contains data but no other atoms. (A preview atom points to or references another atom, but it does not contain it.)
Let's build a container atom. Now, it's beyond our current capabilities to build a typical movie atom, with all its complicated subatoms and subsubatoms. But there is a kind of QuickTime movie file that consists entirely of a movie atom and which is simple enough for us to build; this kind of file is called a shortcut movie file. A shortcut movie file is a movie file that picks out a single other movie file. It's rather like an alias file in the Macintosh file system or a shortcut on Windows. Opening a shortcut movie file using the Movie Toolbox function OpenMovieFile causes QuickTime to look for the file that is referred to by the shortcut movie file; if that target file can be found, then OpenMovieFile opens it and returns to the caller a file reference number for that target file.
Shortcut movie files provide a cross-platform mechanism for referring to QuickTime movie files. They can be useful in all the same ways that alias files (on Mac) or shortcuts (on Windows) can be useful, at least when working with QuickTime movies. For instance, a web page might contain an embedded URL to a shortcut movie file. If the webmaster wants to update the movie displayed in the web page, he or she needs only to create a new shortcut movie file that refers to the updated movie and then put the new shortcut movie file in the location occupied by the previous shortcut movie file. In this way, the contents of the web page can be changed without altering the actual HTML tags of the page.
QuickTime has supported shortcut movie files since version 3.0. QuickTime version 4.0 introduced the Movie Toolbox function CreateShortcutMovieFile, which we can use to create a shortcut movie file. But the structure of shortcut movie files is so simple that we can build them ourselves. Let's see how to do this.
The format of shortcut movie files is (to my knowledge) currently undocumented, but it's quite simple: the shortcut movie file consists entirely of a single movie atom, which in turn contains a movie data reference alias atom (of type MovieDataRefAliasAID). This atom contains a single data reference atom (of type DataRefAID). Finally, the data reference atom is a leaf atom that contains the type of the data reference followed immediately by the data reference itself. Figure 1 shows the general structure of a shortcut movie file.
Figure 1. The structure of a shortcut movie file.
It's easy enough to create a file that has this structure. The only thing we don't yet know is what a data reference is, to put into the data reference atom. In the next QuickTime Toolkit article, we'll take a long look at data references; for the moment, we'll just suppose that a suitable data reference is passed to us. (To look ahead a bit, a data reference is a handle to some data that picks out a movie file or other file. For instance, a URL data reference is a handle to the NULL-terminated string of characters in the URL.) Listing 8 shows the function QTShortCut_CreateShortcutMovieFile that takes that data reference and data reference type and then builds a shortcut movie file.
Listing 8: Creating a shortcut movie file
QTShortCut_CreateShortcutMovieFile
OSErr QTShortCut_CreateShortcutMovieFile (Handle theDataRef,
OSType theDataRefType, FSSpecPtr theFSSpecPtr)
{
long myVersion = 0L;
OSErr myErr = noErr;
myErr = Gestalt(gestaltQuickTime, &myVersion);
if (myErr != noErr)
goto bail;
if (((myVersion >> 16) & 0xffff) >= 0x0400) {
// we're running under QuickTime 4.0 or greater
myErr = CreateShortcutMovieFile(theFSSpecPtr,
sigMoviePlayer,
smCurrentScript,
createMovieFileDeleteCurFile |
createMovieFileDontCreateResFile,
theDataRef,
theDataRefType);
} else {
// we're running under a version of QuickTime prior to 4.0
OSType myDataRefType;
unsigned long myAtomHeaderSize;
Ptr myData = NULL;
Handle myAtom = NULL;
// create the atom data that goes into a data reference atom (we will create this
// atom's header when we create the movie atom that contains it); the atom data is
// the data reference type followed by the data reference itself
myDataRefType = EndianU32_NtoB(theDataRefType);
myAtomHeaderSize = 2 * sizeof(long);
// allocate a data block and copy the data reference type and data reference into it
myData = NewPtrClear(sizeof(OSType) +
GetHandleSize(theDataRef));
if (myData == NULL)
goto bail;
BlockMove(&myDataRefType, myData, sizeof(OSType));
BlockMove(*theDataRef, (Ptr)(myData + sizeof(OSType)),
GetHandleSize(theDataRef));
// create a handle to contain the size and type fields of the movie atom, as well as
// the size and type fields of the movie data reference alias atom contained in it
// and of the data reference atom contained in the movie data reference alias atom
myAtom = NewHandleClear(3 * myAtomHeaderSize);
if (myAtom == NULL)
goto bail;
// fill in the size and type fields of the three atoms
*((long *)(*myAtom + 0x00)) = EndianU32_NtoB((3 *
myAtomHeaderSize) + GetPtrSize(myData));
*((long *)(*myAtom + 0x04)) = EndianU32_NtoB(MovieAID);
*((long *)(*myAtom + 0x08)) = EndianU32_NtoB((2 *
myAtomHeaderSize) + GetPtrSize(myData));
*((long *)(*myAtom + 0x0C)) =
EndianU32_NtoB(MovieDataRefAliasAID);
*((long *)(*myAtom + 0x10)) = EndianU32_NtoB((1 *
myAtomHeaderSize) + GetPtrSize(myData));
*((long *)(*myAtom + 0x14)) = EndianU32_NtoB(DataRefAID);
// concatenate the data in myData onto the end of the movie atom
myErr = PtrAndHand(myData, myAtom, GetPtrSize(myData));
if (myErr != noErr)
goto bail;
// create the shortcut movie file
myErr = QTShortCut_WriteHandleToFile(myAtom,
theFSSpecPtr);
bail:
if (myData != NULL)
DisposePtr(myData);
if (myAtom!= NULL)
DisposeHandle(myAtom);
}
return(myErr);
}
Our function QTShortCut_CreateShortcutMovieFile calls the Movie Toolbox function CreateShortcutMovieFile if it's available; otherwise it creates the movie atom itself, by building up three consecutive atom headers and then appending the data reference type and data onto the end of those atom headers. It writes the entire movie atom into the specified file by calling the QTShortCut_WriteHandleToFile function. (We've already encountered a version of this function, called QTDX_WriteHandleToFile; see "In and Out" in MacTech, May 2000.)
Atom Containers
QuickTime version 2.1 introduced a new way to store information that greatly facilitates creating hierarchical collections of data and retrieving data from those collections. The basic ideas are very simple: at the root of the data hierarchy is an object called an atom container. Inside of an atom container are other objects, called atoms. (If we need to distinguish these atoms from the ones we've been considering up to now, we'll call them atom container atoms.) An atom can contain other atoms, in which case it is a parent atom. The atoms that are contained within a parent atom are called its child atoms. If an atom contains only data (and no other atoms), it is a leaf atom. The data in a leaf atom is always in big-endian format. (Well, almost always; we'll encounter an exception to this rule in a little while.)
An atom has an atom type and an atom ID. A parent atom can contain any number of children of any type and ID. The only restriction is that, for a given parent, no two children can have the same type and ID. So we can uniquely identify an atom by specifying its parent, its type, and its ID. (For an atom that is contained directly in the atom container, the atom container is considered to be the atom's parent; the special constant kParentAtomIsContainer is used to signal this fact.) We can also identify a particular atom by specifying its parent, its type, and an index of atoms of that type in that parent. (QuickTime supports yet a third method of identifying atoms, using the atom's position in the atom container, called its offset; we won't consider this way of identifying atoms here.)
Let's consider a few examples. Figure 2 shows a very simple collection of data, where the atom container has just two children, each of which holds a long integer that represents the length (in millimeters) of a lizard. These leaf atoms both have the atom type 'lzln'.
Figure 2. A simple atom container.
Figure 3 shows a more complicated arrangement of data. In this case, the root atom container contains two parent atoms, both of type 'ldat' (for "lizard data"). Within each 'ldat' atom are two children, which have different types. The atom of type 'lzln' contains a long integer (as in Figure 2) and the atom of type 'lnam' (for "lizard name") contains a string of characters. (This is neither a C string nor a Pascal string; it's just the characters themselves.) Notice that both 'lzln' atoms have an atom ID of 1; this is okay, since those atoms have different parents.
Figure 3. A more complex atom container.
Atom containers can be vastly more complicated than the ones shown in Figures 2 and 3, and they don't have to exhibit the kind of nice symmetry we see there. On the other hand, some real-life atom containers are just that simple. But no matter how complicated they are, we'll use the same functions to build atoms and atom containers and to retrieve data from atom containers. Let's see how to accomplish these tasks.
Creating Atom Containers
The file Movies.h defines these types for working with atoms and atom containers:
typedef Handle QTAtomContainer;
typedef long QTAtom;
typedef long QTAtomType;
typedef long QTAtomID;
Notice that an atom container is just a handle to some data (structured in a specific way, to be sure). This means that we can determine the size of an atom container by using the function GetHandleSize. That's about as much as we need to know about the way an atom container is stored in memory. The actual structure of an atom container is publicly documented, but thankfully we will not need to learn anything about that structure. The Movie Toolbox provides all the functions we'll need in order to create and use atoms and atom containers.
We create an atom container by calling the QTNewAtomContainer function, like this:
QTAtomContainer myAtomContainer = NULL;
myErr = QTNewAtomContainer(&myAtomContainer);
If QTNewAtomContainer completes successfully, then the value of the variable myAtomContainer is a new, empty atom container. We can then add atoms to that container by calling QTInsertChild. For instance, to add the two children shown in Figure 2 to this atom container, we could execute this code:
myLong = EndianU32_NtoB(1029);
myErr = QTInsertChild( myAtomContainer,
kParentAtomIsContainer,
kLizardLength, 1, 0,
sizeof(myLong), &myLong, NULL);
myLong = EndianU32_NtoB(1253);
myErr = QTInsertChild( myAtomContainer,
kParentAtomIsContainer,
kLizardLength, 2, 0,
sizeof(myLong), &myLong, NULL);
The second parameter to the QTInsertChild function specifies the parent atom of the child we're inserting. Here, you'll notice, we're using the constant kParentAtomIsContainer to indicate that the parent atom is the atom container itself. The third and fourth parameters specify the type and ID of the new atom. The fifth parameter is the desired index of the new atom within the parent atom; we don't care about the index here, so we pass the value 0 to indicate that the new atom is to be inserted as the last child of the specified type in the parent atom.
The sixth and seventh parameters to QTInsertChild specify the number of bytes of data to be added to the atom, along with a pointer to the atom data itself. The last parameter is a pointer to a variable of type QTAtom, in which QTInsertChild will return to us an identifier for the new atom; we don't need that information here, so we pass NULL in that parameter.
We can create a hierarchy within an atom container by inserting parent atoms and then adding some children to those parents. We also call QTInsertChild to insert a parent atom, but we do not need to specify any data or data size; instead, we specify a variable of type QTAtom in which the identifier of the new parent atom is returned to us. Here's an example:
QTAtom myLizardAtom;
myErr = QTInsertChild( myAtomContainer,
kParentAtomIsContainer,
kLizardData, 1, 1, 0, NULL,
&myLizardAtom);
Then we can insert a child atom into this parent atom, like this:
myErr = QTInsertChild( myAtomContainer,
myLizardAtom,
kLizardName, 1, 1,
strlen(theLizardName),
theLizardName, NULL);
Note that the second parameter here is the parent atom that we just created. If we insert another atom (this time of type 'lzln') into the parent and then repeat the whole process for the second lizard, we'd have the atom structure shown in Figure 3.
Finding Atoms in Atom Containers
If we are given an atom container, it's almost as easy to get data out of it as it is to put data into it. First we need to find the atom whose data we want. The standard way to do this is to start at the top of the hierarchy and gradually descend until we find the parent of the desired atom. Then we can get an atom identifier of the target atom by calling the QTFindChildByID function. For example, if myLizardAtom is the parent atom for the atoms that hold the data about our lizard Avril, then we can get the name atom by executing this code:
myNameAtom = QTFindChildByID(myAtomContainer, myLizardAtom,
kLizardName, 1, NULL);
QTFindChildByID actually inspects both the type and ID passed to it (not just the ID, as the name might suggest).
The Movie Toolbox provides a number of other functions that are useful for finding specific atoms, including QTCountChildrenOfType, QTFindChildByIndex, QTGetNextChildType, and QTNextChildAnyType.
Getting Atom Data
Once we've found a leaf atom, we can get the data from that atom in several ways. If we want a copy of the atom data that will persist even after we've disposed of the atom container, we can call QTCopyAtomDataToHandle or QTCopyAtomDataToPtr, passing in a handle or pointer to a block of memory that's big enough to hold the leaf atom data. If, on the other hand, we just want to look at the atom data and don't need a copy of it, we can call the QTGetAtomDataPtr function, which returns a pointer to the actual leaf atom data. If you plan to make calls that might move memory, then you should call QTLockContainer before calling QTGetAtomDataPtr; then call QTUnlockContainer when you are done with the data pointer.
If we want to retrieve our lizard's name, we could make this call:
QTGetAtomDataPtr(myAtomContainer, myNameAtom, &myNameSize, amp;myNameData);
If QTGetAtomDataPtr completes successfully, then myNameData points to the string of characters that make up the name, and myNameSize contains the size of that name.
Internet Connection Speed
For our first real-life encounter with atom containers, let's consider how to get and set the user's Internet connection speed preference. The user can set a preference in the Connection Speed panel of the QuickTime[TM] Settings control panel, shown in Figure 4.
Figure 4. The Connection Speed panel.
QuickTime uses this setting for various purposes. For instance, if a user wants to play an alternate data rate movie file located on a remote server, QuickTime uses this connection speed to select the correct target movie. (An alternate data rate movie file is a movie file that references other movies, each tailored for downloading across a connection of a certain speed.)
We can retrieve the user's current QuickTime preferences by calling the GetQuickTimePreference function, like this:
myErr = GetQuickTimePreference(ConnectionSpeedPrefsType,
&myPrefsContainer);
The first parameter specifies the kind of preference we wish to retrieve, and the second parameter is the address of an atom container in which the requested preference data is returned. It's up to us to dispose of that atom container when we are done reading data from it. In the present case, when we retrieve the Internet connection speed, the atom container contains an atom of type ConnectionSpeedPrefsType whose data is structured as a record of type ConnectionSpeedPrefsRecord, defined like this:
struct ConnectionSpeedPrefsRecord {
long connectionSpeed;
};
This record contains a single field that indicates the number of bytes per second that the user's Internet connection can support. The file MoviesFormat.h defines a set of common values:
enum {
kDataRate144ModemRate = 1400,
kDataRate288ModemRate = 2800,
kDataRateISDNRate = 5600,
kDataRateDualISDNRate = 11200,
kDataRateT1Rate = 150000L,
kDataRateInfiniteRate = 0x7FFFFFFF
};
Once we've received an atom container from GetQuickTimePreference, we can use the QTFindChildByID function to find the child atom of type ConnectionSpeedPrefsType. Then we get the atom data by calling the QTGetAtomDataPtr function. Finally, we can read the value stored in the connectionSpeed field, to find the current connection speed preference. We'll return this value as the function result, whether or not it's one of the predefined common values. If any error occurs, however, we'll return the value kDataRate288ModemRate, which is a reasonable default. Listing 9 shows the complete function QTUtils_GetUsersConnectionSpeed.
Listing 9: Getting the user's Internet connection speed preference
QTUtils_GetUsersConnectionSpeed
long QTUtils_GetUsersConnectionSpeed (void)
{
QTAtomContainer myPrefsContainer = NULL;
QTAtom myPrefsAtom = 0;
ConnectionSpeedPrefsRecord myPrefsRec;
long myDataSize = 0L;
long mySpeed =
kDataRate288ModemRate;
Ptr myAtomData = NULL;
OSErr myErr = noErr;
myErr = GetQuickTimePreference(ConnectionSpeedPrefsType,
&myPrefsContainer);
if (myErr == noErr) {
// find the atom of the desired type
myPrefsAtom = QTFindChildByID(myPrefsContainer,
kParentAtomIsContainer,
ConnectionSpeedPrefsType, 1, NULL);
if (myPrefsAtom != 0) {
// read the data contained in that atom and verify that the data is of the
// size we are expecting
QTGetAtomDataPtr(myPrefsContainer, myPrefsAtom,
&myDataSize, &myAtomData);
if (myDataSize == sizeof(ConnectionSpeedPrefsRecord)) {
// read the connection speed
myPrefsRec =
*(ConnectionSpeedPrefsRecord *)myAtomData;
mySpeed = myPrefsRec.connectionSpeed;
}
}
QTDisposeAtomContainer(myPrefsContainer);
}
return(mySpeed);
}
Note that we haven't performed any endian-swapping on the value we read from the connection speed preferences record. That's because the data in this particular atom container is stored in its native-endian format. This is an exception to the general rule that data in atom containers is big-endian. A user's QuickTime preferences are not designed to be moved from machine to machine, so there is no need to enforce big-endian byte ordering.
Listing 10 shows how we can set a user's Internet connection speed. In general, we should let the user decide the connection speed preference, but it can sometimes be useful to do this programmatically.
Listing 10: Setting the user's Internet connection speed preference
QTUtils_SetUsersConnectionSpeed
OSErr QTUtils_SetUsersConnectionSpeed (long theSpeed)
{
QTAtomContainer myPrefsContainer = NULL;
ConnectionSpeedPrefsRecord myPrefsRec;
OSErr myErr = noErr;
myErr = QTNewAtomContainer(&myPrefsContainer);
if (myErr == noErr) {
myPrefsRec.connectionSpeed = theSpeed;
myErr = QTInsertChild(myPrefsContainer,
kParentAtomIsContainer,
ConnectionSpeedPrefsType, 1, 0,
sizeof(ConnectionSpeedPrefsRecord),
&myPrefsRec, NULL);
if (myErr == noErr)
myErr = SetQuickTimePreference(
ConnectionSpeedPrefsType, myPrefsContainer);
QTDisposeAtomContainer(myPrefsContainer);
}
return(myErr);
}
QTUtils_SetUsersConnectionSpeed creates a new atom container, inserts a single child atom into the container that holds the desired speed, and then passes that container to the SetQuickTimePreference function. Once SetQuickTimePreference returns, we can safely call QTDisposeAtomContainer to dispose of the atom container we created.
Movie Tracks
You might recall that in an earlier article ("Opening The Toolbox" in MacTech, March 2000), we saw how to write an application that plays one QuickTime movie inside of another QuickTime movie. The embedded movie (what we then called the "picture-in-picture movie") could have looping characteristics different from those of the main movie. For example, the embedded movie could keep looping over and over while the main movie played though once and then stopped. And, in theory, the embedded movie could play at twice its normal speed while the main movie played at, say, half its normal speed. (We didn't actually provide this alternate speed capability, but it would be easy enough to add.)
The only drawback to this was that we needed the special playback application QTMooVToolbox to make it all happen. Wouldn't it be nice if we could create movie files with these capabilities, so that they would play back using any QuickTime-savvy application? This is precisely what's offered in QuickTime 4.1 with the introduction of movie tracks managed by the movie media handler. By adding movie tracks to an existing QuickTime movie, we can effectively embed an entire QuickTime movie into that movie. (This capability is sometimes called the movie-in-movie capability; the embedded movie is also called the child movie, while the main movie is also called the parent movie.) Figure 5 shows one movie embedded within another movie using a movie track.
Figure 5. A child movie inside of a parent movie.
Remember that the looping characteristics and playback rate of a movie are associated with the movie's time base. Prior to QuickTime 4.1, it was possible to create movies with overlaid video tracks, but all the tracks in the movie shared the same time base. The time base of the overlaid track is slaved to that of the other tracks. What movie tracks bring to the table is the ability to have non-slaved time bases in a single movie. That is to say, each child movie can have its own time base, resulting in looping and playback rate characteristics independent of those of the parent movie.
Adding a Movie Track to a Movie
So let's see how to create movie tracks. Suppose that theWindowObject is a window object for an open QuickTime movie file and that theDataRef and theDataRefType are a data reference and a data reference type for some other QuickTime movie file. Then we can call the QTMIM_AddMovieTrack function defined in Listing 11 to add to that open movie a movie track that references that file. (Once again, we'll postpone discussing data references until the next article; for now, all we need to know is that they pick out QuickTime movie files, either on the local machine or elsewhere on the Internet.)
Listing 11: Adding a movie track to a QuickTime movie
QTMIM_AddMovieTrack
OSErr QTMIM_AddMovieTrack (WindowObject theWindowObject,
OSType theDataRefType, Handle theDataRef)
{
Movie myMovie = NULL; // the parent movie
Track myTrack = NULL; // the movie track
Media myMedia = NULL; // the movie track's media
OSErr myErr = paramErr;
if ((theWindowObject == NULL) ||
(theDataRef == NULL))
goto bail;
myMovie = (**theWindowObject).fMovie;
// create the movie track and media
myTrack = NewMovieTrack(myMovie,
FixRatio(kChildMovieWidth, 1),
FixRatio(kChildMovieHeight, 1),
kFullVolume);
myErr = GetMoviesError();
if (myErr != noErr)
goto bail;
myMedia = NewTrackMedia(myTrack, MovieMediaType,
kMovieTimeScale, NULL, 0);
myErr = GetMoviesError();
if (myErr != noErr)
goto bail;
// create the media sample(s)
myErr = BeginMediaEdits(myMedia);
if (myErr != noErr)
goto bail;
myErr = QTMIM_AddMovieTrackSampleToMedia(theWindowObject,
myMedia, theDataRefType, theDataRef);
if (myErr != noErr)
goto bail;
myErr = EndMediaEdits(myMedia);
if (myErr != noErr)
goto bail;
// add the media to the track
myErr = InsertMediaIntoTrack(myTrack, 0, 0,
GetMediaDuration(myMedia), fixed1);
bail:
return(myErr);
}
There is absolutely nothing new about this function. It's virtually identical to the function QTMM_CreateVideoMovie that we encountered in an earlier article (see "Making Movies" in MacTech, June 2000). The only real difference is that we've created a media of type MovieMediaType; also, here we call QTMIM_AddMovieTrackSampleToMedia to add media samples to the new track, while earlier we called QTMM_AddVideoSamplesToMedia.
Creating a Movie Track Media Sample
By now you might be wondering what this has to do with atom containers. The answer is simple: the media sample for a movie track consists of an atom container whose atoms specify the movie to be embedded in the main movie, as well as some of the playback characteristics of the embedded movie. In other words, the function QTMIM_AddMovieTrackSampleToMedia needs only to create an appropriate atom container and pass that container to the AddMediaSample function.
We'll begin therefore by calling QTNewAtomContainer to create a new atom container; since this container will serve as our media sample, we'll call it mySample:
myErr = QTNewAtomContainer(&mySample);
Into this new atom container we want to put an atom of type kMovieMediaDataReference, whose data consists of the data reference type and the data reference of the movie file that is to be the embedded movie. We can create the atom data like this:
myData = NewPtrClear(sizeof(OSType) +
GetHandleSize(theDataRef));
myType = EndianU32_NtoB(theDataRefType);
BlockMove(&myType, myData, sizeof(OSType));
BlockMove(*theDataRef, myData + sizeof(OSType),
GetHandleSize(theDataRef));
Then we can insert the atom into the atom container by calling QTInsertChild:
myErr = QTInsertChild(mySample, kParentAtomIsContainer,
kMovieMediaDataReference, 1, 1,
GetPtrSize(myData), myData, NULL);
At this point, we could call AddMediaSample to add the atom container mySample as the single media sample of the movie track. But we'd like the embedded movie to start playing automatically when the parent movie reaches the start time of the movie track, which is not the default behavior. To have the embedded movie automatically start playing, we need to add another atom to the atom container, of type kMovieMediaAutoPlay.
myBoolean = true;
myErr = QTInsertChild(mySample, kParentAtomIsContainer,
kMovieMediaAutoPlay, 1, 1,
sizeof(myBoolean), &myBoolean, NULL);
Now we can create a sample description and add the atom container to the movie track media. Listing 12 shows our function QTMIM_AddMovieTrackSampleToMedia for adding a media sample to a movie track.
Listing 12: Adding a sample to the movie track media
QTMIM_AddMovieTrackSampleToMedia
OSErr QTMIM_AddMovieTrackSampleToMedia
(WindowObject theWindowObject, Media theMedia,
OSType theDataRefType, Handle theDataRef)
{
#pragma unused(theWindowObject)
QTAtomContainer mySample = NULL;
QTAtom myRegionAtom;
SampleDescriptionHandle myImageDesc = NULL;
Ptr myData = NULL;
OSType myType;
Boolean myBoolean;
OSErr myErr = paramErr;
// create a new atom container to hold the sample data
myErr = QTNewAtomContainer(&mySample);
if (myErr != noErr)
goto bail;
// concatenate the data reference type and data reference
// into a single block of data
myData = NewPtrClear(sizeof(OSType) +
GetHandleSize(theDataRef));
if (myData == NULL)
goto bail;
// convert the data to big-endian format
myType = EndianU32_NtoB(theDataRefType);
BlockMove(&myType, myData, sizeof(OSType));
BlockMove(*theDataRef, myData + sizeof(OSType),
GetHandleSize(theDataRef));
// add an atom of type kMovieMediaDataReference
// to the atom container
myErr = QTInsertChild(mySample,
kParentAtomIsContainer,
kMovieMediaDataReference, 1, 1,
GetPtrSize(myData), myData, NULL);
if (myErr != noErr)
goto bail;
// add an auto-start atom
myBoolean = true;
myErr = QTInsertChild
(mySample, kParentAtomIsContainer,
kMovieMediaAutoPlay, 1, 1,
sizeof(myBoolean), &myBoolean, NULL);
if (myErr != noErr)
goto bail;
// create a sample description
myImageDesc = (SampleDescriptionHandle)
NewHandleClear(sizeof(SampleDescription));
if (myImageDesc == NULL)
goto bail;
(**myImageDesc).descSize = sizeof(SampleDescription);
(**myImageDesc).dataFormat = MovieMediaType;
myErr = AddMediaSample(
theMedia,
mySample,
0,
GetHandleSize((Handle)mySample),
GetMovieDuration(GetTrackMovie(GetMediaTrack
(theMedia))), myImageDesc,
1,
0,
NULL);
bail:
if (myData != NULL)
DisposePtr(myData);
if (myImageDesc != NULL)
DisposeHandle((Handle)myImageDesc);
if (mySample != NULL)
QTDisposeAtomContainer(mySample);
return(myErr);
}
Notice that, as promised earlier, we call GetHandleSize to get the size of the atom container when we call AddMediaSample.
This Month's Code
This month, our sample code is scattered across four different sets of files. The updated version of QTInfo is called QTInfoPlus and includes a new version of the QTInfo_MakeFilePreview function and all the utilities that we used to access the atoms in a QuickTime movie file. The code for creating shortcut movie files is contained in the snippet QTShortcut.c. The code for getting and setting the user's Internet connection speed preference is contained in the file QTUtilities.c (which has been part of every project we've developed so far). Finally, the code for adding movie tracks to QuickTime movies is contained in the project for the sample application QTMovieTrack. QTMovieTrack allows the user to configure a large number of settings for the new movie track, in addition to the auto-playback setting (which we considered earlier). Figure 6 shows the dialog box that QTMovieTrack displays to allow the user to configure a new movie track.
Figure 6. QTMovieTrack's Movie Track Properties dialog box.
For a complete explanation of the slaving and scaling options illustrated in Figure 6, see the document "QuickTime 4.1", downloadable in PDF form from the location <http://developer.apple.com/techpubs/quicktime/qtdevdocs/RM/rmWhatsnewQT.htm>.
Conclusion
QuickTime tries very hard to insulate us from having to work directly with the kinds of atoms that comprise movie files (the so-called "classic" or "chunk" atoms). The Movie Toolbox provides an extensive set of high-level routines that we can use to create new movie files, open existing movie files, edit movie files, and so forth. Nonetheless, we've seen that there are occasions when we do need to interact with atoms directly. A good example of this concerns adding a file preview to a single-fork movie file. QuickTime currently provides no API to do this, so we are forced to work with the file data - the atoms - directly. Similarly, if we want to create a shortcut movie file on a machine that's running a version of QuickTime prior to 4.0, we need to work with atoms.
By contrast, we'll encounter atom containers and their associated atom container atoms at virtually every step we take forward in our journey through QuickTime. Atom containers and their children provide an easy way to maintain hierarchical data, and they're backed by an extensive programming interface. So atom containers are now the repository of choice for storing and exchanging data. We'll see them used in media samples, tween tracks, input maps, musical instruments, wired actions, video effect tracks, and for a large number of other uses. In other words, Internet connection speed preferences and movie tracks are just the tip of the ice floe.
Credits
The functions for getting and setting a user's Internet connection speed preferences are based on code by Mike Dodd in the Letter from the Ice Floe, Dispatch 17 (found at <http://developer.apple.com/quicktime/icefloe/dispatch017.html>. Thanks are due to Ken Doyle for reviewing this article and offering some helpful comments.
Tim Monroe recently acquired 3 pet lizards (green anoles), not realizing that he'd need to feed them things like crickets, mealworms, and spiders. His house is now surprisingly spider-free, however. You can send your bugs to him at monroe@apple.com.