Aug 00 QTToolkit
Volume Number: 16 (2000)
Issue Number: 8
Column Tag: QuickTime Toolkit
The Informant
By Tim Monroe
Getting and Setting Movie Information
Introduction
In the previous QuickTime Toolkit article, we saw how to create a QuickTime movie file that contains a single video track. We also learned a fair bit about the structure of QuickTime movies (as collections of tracks) and QuickTime movie files (as collections of atoms). In this article, we'll continue on with the general topic of creating and configuring QuickTime movie files. We'll see how to get various pieces of information about QuickTime movies and movie files; we'll also see how to add information to a QuickTime movie to help the user determine what's in a movie.
To get an idea of what we're going to accomplish here, let's suppose that we're running some version of the MoviePlayer application (the predecessor to the current QuickTime Player application). MoviePlayer's Movie menu contains the item "Show Copyright...". If we select that item immediately after having opened the movie file we created in the previous article, we'll see the movie information dialog box shown in Figure 1.
Figure 1. The movie information dialog box for our new movie
As you can see, this is not particularly helpful. The only real "information" visible to the user is the first frame of the movie, which happens to be a solid white rectangle. It would be better to display some other frame of the movie and to add some descriptive information to the other panes of the dialog box. Figure 2 shows a much more useful movie information dialog box.
Figure 2. The revised movie information dialog box for our new movie
Part of our task here will be to see how to modify the movie file so that selecting "Show Copyright..." displays the dialog box in Figure 2 rather than the one in Figure 1. In a nutshell, this involves setting the movie poster to some frame other than the first frame, which is the default poster frame; it also involves attaching three new pieces of movie user data to the movie file. Along the way, we'll also learn how to set the preview that is contained in the file-opening dialog boxes displayed by calls to the StandardGetFilePreview and NavGetFile functions. Figure 3 shows a typical file-opening dialog box with a preview.
Figure 3. A preview contained in a file-opening dialog box
Our sample application this time around is called QTInfo. As usual, it's based directly on the QTShell sample application that we've developed previously. QTInfo is just QTShell with one additional source code file (QTInfo.c) and some additional resources. Figure 4 shows the Test menu supported by QTInfo.
Figure 4. The Test menu in QTInfo
As you can see, QTInfo provides the "Show Copyright..." menu item, as well as a number of other items that allow us to get and set various kinds of movie information. It turns out that we can handle the "Show Copyright..." item with a single line of code:
ShowMovieInformation(myMovie, gModalFilterUPP, 0L);
The ShowMovieInformation function was introduced in QuickTime version 2.0, but has (to my knowledge) never been documented. ShowMovieInformation simply displays the movie information dialog box, which includes the movie poster image, the name of the movie, the movie's copyright information, and some other information. If you pass a universal procedure pointer to a modal dialog event filter function in the second parameter, you'll get a movable modal dialog box; otherwise, you'll get a standard non-movable modal dialog box, as shown in Figure 5.
Figure 5. A non-movable movie information dialog box
Movie Posters
A movie poster image (or, more briefly, a movie poster) is a single image that represents a QuickTime movie. The images in the top-left panes of Figures 1, 2, and 5 are movie posters, suitably resized to fit into the available space in the movie information dialog box. A movie poster is defined by specifying a movie poster time and one or more movie poster tracks. The movie poster time specifies the time in the movie at which the image is to found, and the movie poster tracks specify which tracks in the movie are to be used to create the movie poster. Typically a single track is used as the movie poster track, but in theory two or more video tracks (or other tracks with visible data) could contribute to the final movie poster image. If a movie has no track designated as a movie poster track, then the movie won't have a poster, no matter what the movie poster time is set to. Let's see how to work with poster times and tracks.
Getting and Setting Movie Poster Times
The default movie poster time is 0, which picks out the first frame in the movie. As we saw earlier, it's sometimes useful to designate some other time as the movie poster time. The function QTInfo_SetPosterToFrame, defined in Listing 1, sets the currently-displayed movie frame to be the movie poster image. (QTInfo calls QTInfo_SetPosterToFrame in response to the "Set Poster Frame" menu item.)
Listing 1: Setting the movie poster time to the current movie time
QTInfo_SetPosterToFrame
OSErr QTInfo_SetPosterToFrame
(Movie theMovie,
MovieController theMC)
{
TimeValue myTime;
ComponentResult myErr = noErr;
// stop the movie from playing
myErr = MCDoAction(theMC, mcActionPlay, (void *)0L);
if (myErr != noErr)
goto bail;
myTime = GetMovieTime
(theMovie, NULL);
SetMoviePosterTime
(theMovie, myTime);
myErr = MCMovieChanged
(theMC, theMovie);
bail:
return((OSErr)myErr);
}
As you can see, QTInfo_SetPosterToFrame first calls MCDoAction to set the movie play rate to 0, which effectively stops the movie from playing. (If the movie is already stopped, this call has no effect.) Then QTInfo_SetPosterToFrame retrieves the current movie time by calling the GetMovieTime function and sets the movie poster time to the current movie time by calling the SetMoviePosterTime function.
QTInfo_SetPosterToFrame finishes up by calling the MCMovieChanged function, which informs the movie controller that we've made changes to the movie using the Movie Toolbox. As we've seen in past articles, there are often two ways to change some characteristic of a movie: using Movie Toolbox functions and using movie controller functions. When a movie is associated with a movie controller and when we make a change to the movie using the Movie Toolbox, it's usually necessary to keep things in sync by calling MCMovieChanged. For example, if we change the size of the movie by calling SetMovieBox, we'd need to call MCMovieChanged so that the movie controller can update itself appropriately.
In the present case, there is no movie controller action to set the poster frame, so we used the Movie Toolbox function SetMoviePosterTime. Then we called MCMovieChanged on the offhand chance that the movie controller might actually care about the poster time. I've tried running QTInfo without the call to MCMovieChanged here and no harm appears to result, but it's better to be safe than sorry. As a general rule, if you have a movie controller associated with a movie and you use the Movie Toolbox to effect some change in the movie, call MCMovieChanged to inform the movie controller of the change.
QTInfo supports the "Go To Poster Frame" menu item, which sets the current movie time to the movie poster time. Listing 2 defines the QTInfo_GoToPosterFrame function, which does just that.
Listing 2: Setting the current movie time to the movie poster time
QTInfo_GoToPosterFrame
OSErr QTInfo_GoToPosterFrame (Movie theMovie,
MovieController theMC)
{
TimeRecord myTimeRecord;
ComponentResult myErr = noErr;
// stop the movie from playing
myErr = MCDoAction(theMC, mcActionPlay, (void *)0L);
if (myErr != noErr)
goto bail;
// set up a time record with the desired movie time, scale, and base
myTimeRecord.value.hi = 0;
myTimeRecord.value.lo =
GetMoviePosterTime(theMovie);
myTimeRecord.base = GetMovieTimeBase(theMovie);
myTimeRecord.scale = GetMovieTimeScale(theMovie);
myErr = MCDoAction(theMC, mcActionGoToTime, &myTimeRecord);
bail:
return((OSErr)myErr);
}
In this case, there is a movie controller action that we can use to set the current movie time, namely mcActionGoToTime. As a result, there is no need to call MCMovieChanged after we've made this change to the movie (since we made the change using movie controller actions, not the Movie Toolbox). We did of course use Movie Toolbox functions to get information needed to fill in the TimeRecord structure whose address we pass to MCDoAction, but those functions didn't make any changes to the movie; they simply gave us information about the movie.
Working with Movie Poster Tracks
I mentioned earlier that a movie's poster image is determined both by the movie poster time and by the movie poster tracks. Each track in a movie has a track usage, which indicates whether the track is used in the movie, the movie poster, the movie preview, or any combination of these. For instance, a movie can include a video track that consists of a single frame, and that track can be the only one in the movie that is used in the movie poster. In this way, it's possible to have a movie poster that is not simply one of the frames in the movie, but is some other image altogether.
We can query a track's usage by calling the GetTrackUsage function. GetTrackUsage returns a long integer whose bits encode the track usage. Currently, these three bits are defined:
enum {
trackUsageInMovie = 1 << 1,
trackUsageInPreview = 1 << 2,
trackUsageInPoster = 1 << 3
};
By default, a track can be used in any of these three ways, so calling GetTrackUsage on most tracks will return a value of 0x0000000E (that is, binary 1110). But we can change this default setting by calling SetTrackUsage, passing it a long integer that has the appropriate flags set or clear. We'll see some calls to GetTrackUsage and SetTrackUsage in a moment. For now, it's important to understand that a track usage value indicates a track's potential use, not its actual use. That is to say, if a particular track has a track usage value with the trackUsageInPoster flag set, the poster image might not actually include any data from that track. This might happen if the movie poster time is set to a time at which that track has no data (perhaps the track offset is greater than the movie poster time). Similarly, a track's usage value can have the trackUsageInPreview flag set, even if the movie has no preview. To repeat, the track usage determines the uses a track can have, not the uses it actually has.
Let's see how this works in practice. When QTInfo wants to adjust the state of its menus, it needs to know whether the movie in the frontmost window has a poster image. If there is no poster image, then it should disable the "Go To Poster Frame" menu item. To determine whether a movie has a poster image, QTInfo calls the QTInfo_MovieHasPoster function defined in Listing 3. Essentially, QTInfo_MovieHasPoster looks at each track in the movie, retrieves the track's usage value, and checks to see whether the trackUsageInPoster flag is set in that value. If there is at least one track that is capable of contributing data to the movie poster image, we'll happily count the movie as having a poster image.
Listing 3: Determining whether a movie has a poster image
QTInfo_MovieHasPoster
Boolean QTInfo_MovieHasPoster (Movie theMovie)
{
long myCount = 0L;
long myIndex = 0L;
Track myTrack = NULL;
long myUsage = 0L;
Boolean myHasPoster = true;
// make sure that some track is used in the movie poster
myCount = GetMovieTrackCount(theMovie);
for (myIndex = 1; myIndex <= myCount; myIndex++) {
myTrack = GetMovieIndTrack(theMovie, myIndex);
if (myTrack == NULL)
continue;
myUsage = GetTrackUsage(myTrack);
if (myUsage & trackUsageInPoster)
break;
// we found a track with the trackUsageInPoster flag set; break out of the loop
}
if (myIndex > myCount)
myHasPoster = false;
// we went thru all tracks without finding one with a poster usage
return(myHasPoster);
}
The QTInfo_MovieHasPoster function is instructive for other reasons as well, in particular because it shows how to iterate through all tracks in a movie. As you can see, it begins by calling the GetMovieTrackCount function to determine how many tracks the specified movie contains. Then it repeatedly calls the GetMovieIndTrack function to get a track identifier for each of those tracks. The Movie Toolbox also supplies the GetMovieIndTrackType function, which allows us to iterate through all tracks of a specific type (say, all video tracks). We won't have occasion to use GetMovieIndTrackType in this article, but we will in the future.
Movie Previews
A movie preview is a short, dynamic representation of a QuickTime movie. Typically, a movie preview is an excerpt of the movie itself (for example, the first few seconds of the movie). But, like a movie poster, a movie preview can consist of data that is not used in the normal playback of the movie. Once again, the usage values of the tracks in the movie determine the actual contents of the movie preview.
Defining Movie Previews
We specify a movie preview by giving its start time, its duration, and its tracks. The recommended duration is about 3 to 5 seconds, but you are free to use a longer or shorter duration if you wish. An easy way to let the user specify a movie preview is to provide the "Set Preview to Selection" menu item, which uses the start time and duration of the current movie selection as the start time and duration of the movie preview. Listing 4 shows how to set the movie preview to the current movie selection.
Listing 4: Setting the movie preview to the current movie selection
QTInfo_SetPreviewToSelection
OSErr QTInfo_SetPreviewToSelection (Movie theMovie,
MovieController theMC)
{
TimeValue myStart;
TimeValue myDuration;
ComponentResult myErr = noErr;
GetMovieSelection(theMovie, &myStart, &myDuration);
SetMoviePreviewTime(theMovie, myStart, myDuration);
myErr = MCMovieChanged(theMC, theMovie);
return((OSErr)myErr);
}
The QTInfo_SetPreviewToSelection function is simplicity itself. We just call GetMovieSelection to get the current movie start time and duration, and then we pass those same values to the SetMoviePreviewTime function. We need to call MCMovieChanged here because we changed the characteristics of the movie (in particular, its movie preview) using the Movie Toolbox.
As we've seen, QTInfo also provides the "Set Selection to Preview" menu item, which sets the movie's selection to the current movie preview. Listing 5 defines the function QTInfo_SetSelectionToPreview, which performs this operation.
Listing 5: Setting the current movie selection to the movie preview
QTInfo_SetSelectionToPreview
OSErr QTInfo_SetSelectionToPreview (Movie theMovie,
MovieController theMC)
{
TimeValue myStart;
TimeValue myDuration;
ComponentResult myErr = noErr;
GetMoviePreviewTime(theMovie, &myStart, &myDuration);
SetMovieSelection(theMovie, myStart, myDuration);
myErr = MCMovieChanged(theMC, theMovie);
return((OSErr)myErr);
}
We need to enable or disable the "Set Preview to Selection" and "Set Selection to Preview" menu items, depending on whether a movie has a selection or preview. It's easy to determine whether a movie has a selection: we can simply call GetMovieSelection and check to see whether the duration returned to us is greater than 0, like this:
GetMovieSelection(myMovie, &myStart, &myDuration);
myHasSelection = (myDuration > 0);
But it's a bit more complicated to determine whether a movie has a movie preview. We need to check to see both whether the movie has a non-zero movie preview duration and whether any tracks in the movie are used in the movie preview. Listing 6 defines the QTInfo_MovieHasPreview function, which performs both of these checks. As you can see, QTInfo_MovieHasPreview is very similar to QTInfo_MovieHasPoster (Listing 3).
Listing 6: Determining whether a movie has a preview.
QTInfo_MovieHasPreview
Boolean QTInfo_MovieHasPreview (Movie theMovie)
{
TimeValue myStart;
TimeValue myDuration;
long myCount = 0L;
long myIndex = 0L;
Track myTrack = NULL;
long myUsage = 0L;
Boolean myHasPreview = false;
// see if the movie has a positive preview duration
GetMoviePreviewTime(theMovie, &myStart, &myDuration);
if (myDuration > 0)
myHasPreview = true;
// make sure that some track is used in the movie preview
myCount = GetMovieTrackCount(theMovie);
for (myIndex = 1; myIndex <= myCount; myIndex++) {
myTrack = GetMovieIndTrack(theMovie, myIndex);
if (myTrack == NULL)
continue;
myUsage = GetTrackUsage(myTrack);
if (myUsage & trackUsageInPreview)
break;
// we found a track with the trackUsageInPreview flag set; break out of the loop
}
if (myIndex > myCount)
myHasPreview = false;
// we went thru all tracks without finding one with a preview usage
return(myHasPreview);
}
Playing Movie Previews
The Movie Toolbox provides an easy way to show the user the exact contents of a movie preview. We can call the PlayMoviePreview function, like this:
PlayMoviePreview(myMovie, NULL, 0L);
When we execute PlayMoviePreview, the Movie Toolbox puts our movie into preview mode, plays the movie preview in the movie's graphics port, and then sets the movie back into normal playback mode. When the movie returns to normal playback mode, the current movie time is set to the end of the movie preview.
The second parameter to PlayMoviePreview is a universal procedure pointer to a movie callout function, which the Movie Toolbox calls repeatedly while the preview is playing. We might use a movie callout function to provide a way for the user to stop the preview from playing (perhaps by checking the event queue for some particular key press). If we don't use a movie callout function, then the call to PlayMoviePreview is essentially synchronous: no other events will be processed until the movie preview finishes playing.
The Movie Toolbox provides a way to play a movie preview without blocking other processing. We can call SetMoviePreviewMode with its second parameter set to true to put a particular movie into preview mode. SetMoviePreviewMode restricts the active segment of the movie to the segment of the movie picked out by the preview's start time and duration; it also restricts the active tracks to those that have the trackUsageInPreview flag set in their track usage values. Once a movie has been set into preview mode, we can start it and stop it by calling the StartMovie and StopMovie functions. To exit movie preview mode, we can call SetMoviePreviewMode with its second parameter set to false. (Note that QTInfo does not illustrate this method of playing movie previews; it calls PlayMoviePreview.)
Clearing Movie Previews
Sometimes it's useful to clear a movie preview from a movie. We can do this by setting both the start time and duration of the movie preview to 0, like this:
SetMoviePreviewTime(theMovie, 0, 0);
Executing this line alone effectively prevents any movie preview from being displayed. But we also want to perform a few other actions. For one thing, we should remove any tracks from the movie that are used only in the movie preview. We can do this by examining the track usage value for each track in the movie and, if the usage value indicates that a track is used in the movie preview but not in the movie or the movie poster, calling DisposeMovieTrack to remove the track from the movie.
Also, once we've removed any tracks that were used only in the movie preview, we should go back through the remaining tracks and reset their track usage values so that they can be used as part of a movie preview, if one is subsequently added. If we don't do this, the user might be unable to create a new movie preview, since it's possible that none of the remaining tracks in the movie has the trackUsageInPreview flag set in its track usage value.
Listing 7 defines the QTInfo_ClearPreview function, which performs all three of these actions.
Listing 7: Clearing a movie preview
QTInfo_ClearPreview
OSErr QTInfo_ClearPreview (Movie theMovie,
MovieController theMC)
{
long myCount = 0L;
long myIndex = 0L;
Track myTrack = NULL;
long myUsage = 0L;
ComponentResult myErr = noErr;
// set the movie preview start time and duration to 0
SetMoviePreviewTime(theMovie, 0, 0);
// remove all tracks that are used *only* in the movie preview
myCount = GetMovieTrackCount(theMovie);
for (myIndex = myCount; myIndex >= 1; myIndex-) {
myTrack = GetMovieIndTrack(theMovie, myIndex);
if (myTrack == NULL)
continue;
myUsage = GetTrackUsage(myTrack);
myUsage &= trackUsageInMovie | trackUsageInPreview | trackUsageInPoster;
if (myUsage == trackUsageInPreview)
DisposeMovieTrack(myTrack);
}
// add trackUsageInPreview to any remaining tracks that are in the movie
// (so that subsequently setting the preview to a selection will include
// these tracks)
myCount = GetMovieTrackCount(theMovie);
for (myIndex = 1; myIndex <= myCount; myIndex++) {
myTrack = GetMovieIndTrack(theMovie, myIndex);
if (myTrack == NULL)
continue;
myUsage = GetTrackUsage(myTrack);
if (myUsage & trackUsageInMovie)
SetTrackUsage(myTrack, myUsage | trackUsageInPreview);
}
myErr = MCMovieChanged(theMC, theMovie);
return((OSErr)myErr);
}
File Previews
Now consider this question: when we call StandardGetFilePreview (or NavGetFile with the preview pane enabled), what is displayed in the preview section of the file-opening dialog box? Before you answer, take a look back at Figure 3. I suspect you're inclined to say that it's the movie preview. But before you make that your final answer, take a look at Figure 6, which shows another file-opening dialog box.
Figure 6. A poster contained in a file-opening dialog box
And then take a look at Figure 7, which shows yet another file-opening dialog box.
Figure 7. A description contained in a file-opening dialog box
Thoroughly confused? I thought so.
The correct answer to our little quiz is that the preview displayed in a file-opening dialog box is what's called a file preview, which is any information that gives the user an idea of what's in the file. As we've seen, the file preview can be a movie poster or a movie preview (if the file is a movie file) or any other data that describes or represents the file. On Macintosh computers, the default file preview for a QuickTime movie file is a miniature version of the movie poster frame, while on Windows computers it's the first 10 seconds of the movie. But we are free to specify some other information as the file preview, if we so desire. Let's see how file previews are stored and created, to make this all perhaps a bit clearer.
Accessing File Previews
On Macintosh computers, when StandardGetFilePreview or NavGetFile needs to display a file preview for a QuickTime movie file, it first checks to see whether the file is a double-fork or single-fork movie file. If the selected file is a double-fork movie file, StandardGetFilePreview or NavGetFile looks in the resource fork for a resource of type 'pnot'. The data in a 'pnot' resource is organized as a preview resource record, which is defined in ImageCompression.h like this:
struct PreviewResourceRecord {
unsigned long modDate;
short version;
OSType resType;
short resID;
};
The resType and resID fields specify the type and ID of some other resource, which contains the actual file preview data or which itself indicates where to find that data. (Let's call that other resource the preview data resource.) For instance, if resType and resID pick out a resource of type 'PICT', then the picture in that resource will be used as the file preview (as in Figure 6). Similarly, if resType and resID pick out a resource of type 'TEXT', then the text in that resource will be used as the file preview (as in Figure 7). If resType and resID pick out a resource of type 'moov', then the movie preview start time and duration specified in that resource will be used to pick out the file preview (as in Figure 3). If there is no movie resource in the resource fork, then resID should be set to -1 (0xFFFF), which tells StandardGetFilePreview to use the movie preview whose start time and duration are stored in the movie atom in the file's data fork.
In single-fork movie files, there is no resource fork. So StandardGetFilePreview opens the data fork and looks for an atom of type 'pnot', which it interprets in the same way as a 'pnot' resource, with one small difference: the resID field is interpreted as a 1-based index of atom types in the movie file. For example, if the resType field in the 'pnot' atom in a single-fork movie file is 'PICT' and the resID field is 1, then StandardGetFilePreview looks for the first atom in that file of type 'PICT', which it then uses as the file preview.
There are a couple of "gotchas" here that you should be aware of. First, the NavGetFile function currently seems to work only with file previews specified by 'pnot' resources. If you're creating single-fork movie files (as I have recommended), don't expect them to have file previews in the file-opening dialog boxes displayed by NavGetFile. Worse yet, NavGetFile doesn't seem to know how to handle movie previews as file previews, even in double-fork movie files. Finally, StandardGetFilePreview doesn't seem to know how to handle movie previews as file previews when stored in single-fork movies. (At least, I haven't been able to get them to work.) Our strategy below will be to create single-fork movie files with 'pnot' atoms in the format that is publicly documented. Then we'll just have to wait until StandardGetFilePreview and NavGetFile to catch up to us (as I expect they will).
(By the way, you might be wondering why file preview resources and atoms have the type 'pnot'. The 'p' of course is for "preview'; but what's the 'not' all about? The constant assigned to the component that displays file previews is of no help in deciphering this:
enum {
ShowFilePreviewComponentType = FOUR_CHAR_CODE('pnot')
};
I'm told, by a knowledgeable source, that early versions of the QuickTime software - prior to version 1.0 - contained a preview component that wasn't very good. When the replacement was written, it was given the type 'pnot' as an abbreviation for "Preview? Not!")
Creating File Previews
Ideally, we'd like the QuickTime movie files that we create to have file previews, so that the user can get a reasonable idea of what's in those files when they appear in the list of files in the file-opening dialog box. The Image Compression Manager provides the MakeFilePreview function, which we can use to create file previews. Inside Macintosh recommends calling MakeFilePreview whenever we save a movie file. So we can insert a call to MakeFilePreview in the two functions QTFrame_UpdateMovieFile and QTFrame_SaveAsMovieFile (both in the file ComFramework.c) which handle the "Save" and "Save As" menu commands:
MakeFilePreview(myRefNum, (ICMProgressProcRecordPtr)-1);
MakeFilePreview sets the file preview for the file specified by the myRefNum parameter to be the current movie preview, if one exists; if the movie does not have a movie preview, then MakeFilePreview creates a thumbnail version of the movie poster image and sets it to be the file preview. (A thumbnail is a small copy of an image, typically 80 pixels on the longer side.) If we want to create a file preview using some other type of data (for instance, text data), we can call the ICM function AddFilePreview, which allows us to specify the type of data we want to use.
But there is one big problem here: MakeFilePreview and AddFilePreview always add the file preview information to the movie file's resource fork. Indeed, MakeFilePreview and AddFilePreview will go so far as to create a resource fork for the movie file if it doesn't already have one, so that they have a place to put the file preview they create. Needless to say, this behavior is going to wreak havoc with our desire to create only single-fork movie files. So, however tempting it might be to use MakeFilePreview to create our file previews, we're just going to have to resist that temptation.
In short, QuickTime does not currently provide any API to add a file preview to a single-fork movie file. But based on what we learned above about the way file previews are stored in single-fork files and on what we learned in the previous article about the general structure of QuickTime movie files, it won't be too hard for us to do this ourselves. For, we know that a single-fork movie file is just a collection of atoms. And a file preview can be stored in a single-fork movie as an atom of type 'pnot' together with a preview data atom that holds the actual preview data. So all we really need to do is append an atom or two to a single-fork movie file. Figure 8 shows a single-fork movie file with no file preview (top) and that same file with a file preview (bottom). We'll define a function QTInfo_MakeFilePreview that we can use to turn the top file into the bottom file.
Figure 8. A single-fork movie file before and after adding a file preview
QTInfo_MakeFilePreview is declared like this:
OSErr QTInfo_MakeFilePreview (Movie theMovie,
short theRefNum, ICMProgressProcRecordPtr theProgressProc)
As you can see, QTInfo_MakeFilePreview has the same parameters as MakeFilePreview, except that we also pass the movie identifier to QTInfo_MakeFilePreview. The second parameter to QTInfo_MakeFilePreview is the file reference number of the open movie file. If QTInfo_MakeFilePreview is passed a reference to a resource fork, then it can just call MakeFilePreview to add the required preview resources to that resource fork, like this:
if (QTInfo_IsRefNumOfResourceFork(theRefNum)) {
myErr = MakeFilePreview(theRefNum, theProgressProc);
goto bail;
}
But if QTInfo_MakeFilePreview is passed the file reference number of a data fork, then we'll assume that we must add the file preview information to the data fork. This involves adding a 'pnot' atom to the data fork, as well as a preview data atom. Recall that an atom consists of an atom header and some atom data. For a 'pnot' atom, the atom data is a record of type PreviewResourceRecord. So we can construct the 'pnot' atom like this:
PreviewResourceRecord myPNOTRecord;
unsigned long myAtomHeader[2]; // an atom header
// fill in the 'pnot' atom header
myAtomHeader[0] = EndianU32_NtoB(sizeof(myAtomHeader) +
sizeof(myPNOTRecord));
myAtomHeader[1] =
EndianU32_NtoB(ShowFilePreviewComponentType);
// fill in the 'pnot' atom data
GetDateTime(&myModDate);
myPNOTRecord.modDate = EndianU32_NtoB(myModDate);
myPNOTRecord.version = EndianS16_NtoB(0);
myPNOTRecord.resType = EndianU32_NtoB(myPreviewType);
myPNOTRecord.resID = EndianS16_NtoB(1);
All data in predefined QuickTime movie atoms must be in big-endian format, so here we use the macros EndianU32_NtoB and EndianS16_NtoB to convert from the computer's native-endian format into big-endian format.
Notice that the resType field is set to myPreviewType. We'll create a file preview that is either a movie preview or a movie poster thumbnail, depending on whether the movie has a movie preview:
if (QTInfo_MovieHasPreview(theMovie))
myPreviewType = MovieAID;
else
myPreviewType = kQTFileTypePicture;
The next thing we need to do is write the 'pnot' atom data onto the end of the movie file. We can use the File Manager functions GetEOF, SetEOF, SetFPos, and FSWrite to do this. See Listing 8 below for the exact steps involved in writing the data into the file.
Now we need to write the actual preview data into an atom of the appropriate type. For a movie preview, we can just point to the 'moov' atom, which contains the start time and duration of the movie preview. For a file preview that contains a thumbnail of the movie poster frame, we need to retrieve the movie poster frame, create a thumbnail image from it, and write the atom onto the end of the movie file. We can call GetMoviePosterPict to get the movie poster image:
myPicture = GetMoviePosterPict(theMovie);
Then we can call the ICM function MakeThumbnailFromPicture to reduce the poster image to a thumbnail image:
myThumbnail = (PicHandle)NewHandleClear(4);
myErr = MakeThumbnailFromPicture(myPicture, 0, myThumbnail,
theProgressProc);
If MakeThumbnailFromPicture successfully creates the thumbnail image, we need to fill in an atom header and write the header and thumbnail data into the movie file as an atom of type 'PICT', like this:
myAtomHeader[0] = EndianU32_NtoB(sizeof(myAtomHeader) +
GetHandleSize((Handle)myThumbnail));
myAtomHeader[1] = EndianU32_NtoB(myPreviewType);
// write the atom header into the file
mySize = sizeof(myAtomHeader);
myErr = FSWrite(theRefNum, &mySize, myAtomHeader);
if (myErr == noErr) {
// write the atom data into the file
mySize = GetHandleSize((Handle)myThumbnail);
myErr = FSWrite(theRefNum, &mySize, *myThumbnail);
}
Listing 8 brings all of this together into a single function that writes the appropriate file preview into the resource fork or the data fork, depending on the kind of file reference number passed to it in the second parameter.
Listing 8: Creating a file preview
QTInfo_MakeFilePreview
OSErr QTInfo_MakeFilePreview (Movie theMovie,
short theRefNum, ICMProgressProcRecordPtr theProgressProc)
{
unsigned long myModDate;
PreviewResourceRecord myPNOTRecord;
long myEOF;
long mySize;
unsigned long myAtomHeader[2];// an atom header
OSType myPreviewType;
OSErr myErr = noErr;
// determine whether theRefNum is a file reference number of a data fork or
// a resource fork; if it's a resource fork, then we'll just call the existing ICM function
// MakeFilePreview
if (QTInfo_IsRefNumOfResourceFork(theRefNum)) {
myErr = MakeFilePreview(theRefNum, theProgressProc);
goto bail;
}
// if the movie has a movie preview, use that as the file preview; otherwise use
// a thumbnail of the movie poster frame as the file preview
if (QTInfo_MovieHasPreview(theMovie))
myPreviewType = MovieAID;
else
myPreviewType = kQTFileTypePicture;
// construct the 'pnot' atom; fill in the 'pnot' atom header
myAtomHeader[0] = EndianU32_NtoB(sizeof(myAtomHeader) +
sizeof(myPNOTRecord));
myAtomHeader[1] =
EndianU32_NtoB(ShowFilePreviewComponentType);
// fill in the 'pnot' atom data
GetDateTime(&myModDate);
myPNOTRecord.modDate = EndianU32_NtoB(myModDate); myPNOTRecord.version = EndianS16_NtoB(0);
myPNOTRecord.resType = EndianU32_NtoB(myPreviewType);
myPNOTRecord.resID = EndianS16_NtoB(1);
// write the 'pnot' atom at the end of the data fork
// get the current logical end-of-file and extend it by the desired amount
myErr = GetEOF(theRefNum, &myEOF);
if (myErr != noErr)
goto bail;
myErr = SetEOF(theRefNum,
myEOF + sizeof(myAtomHeader) + sizeof(myPNOTRecord));
if (myErr != noErr)
goto bail;
// set the file mark
myErr = SetFPos(theRefNum, fsFromStart, myEOF);
if (myErr != noErr)
goto bail;
// write the atom header into the file
mySize = sizeof(myAtomHeader);
myErr = FSWrite(theRefNum, &mySize, myAtomHeader);
if (myErr != noErr)
goto bail;
// write the atom data into the file
mySize = sizeof(myPNOTRecord);
myErr = FSWrite(theRefNum, &mySize, &myPNOTRecord);
if (myErr != noErr)
goto bail;
// write the preview data atom at the end of the data fork
if (myPreviewType == MovieAID) {
// the 'pnot' atom refers to the existing 'moov' atom
// so no other preview data atom is required
}
if (myPreviewType == kQTFileTypePicture) {
PicHandle myPicture = NULL;
PicHandle myThumbnail = NULL;
// get the poster frame picture
myPicture = GetMoviePosterPict(theMovie);
if (myPicture != NULL) {
// create a thumbnail
myThumbnail = (PicHandle)NewHandleClear(4);
if (myThumbnail != NULL) {
myErr = MakeThumbnailFromPicture(myPicture, 0,
myThumbnail, theProgressProc);
if (myErr == noErr) {
myAtomHeader[0] =
EndianU32_NtoB(sizeof(myAtomHeader) +
GetHandleSize((Handle)myThumbnail));
myAtomHeader[1] = EndianU32_NtoB(myPreviewType);
// write the atom header into the file
mySize = sizeof(myAtomHeader);
myErr = FSWrite(theRefNum, &mySize, myAtomHeader);
if (myErr == noErr) {
// write the atom data into the file
mySize = GetHandleSize((Handle)myThumbnail);
myErr = FSWrite(theRefNum, &mySize,
*myThumbnail);
}
}
KillPicture(myThumbnail);
}
KillPicture(myPicture);
}
}
bail:
return(myErr);
}
I should point out that QTInfo_MakeFilePreview is not terribly smart about adding file previews to single-fork files. In particular, QTInfo_MakeFilePreview doesn't bother to check whether the movie file already contains a 'pnot' atom. Instead, it simply appends a new 'pnot' atom and its associated preview data atom to the file. One consequence of this is that each time the user changes any aspect of the movie and saves it, a new thumbnail is appended to the movie file; but that thumbnail might never be used, since StandardGetFilePreview will always find the first 'pnot' atom and the first preview data atom. In the next article, we'll address this issue and see how to replace an existing 'pnot' atom and its associated preview data atom.
Movie Annotations
A QuickTime movie file can include zero or more movie annotations, which provide descriptive information about the movie contained in the file. For example, movie annotations can indicate the names of the performers in the movie, the software that was used to create the movie, the names of the movie's writer and director, general information about the movie, and so forth. The header file Movies.h defines over two dozen kinds of movie annotations. For the present, we'll be concerned with only three of them, picked out by these constants:
enum {
kUserDataTextFullName = FOUR_CHAR_CODE('©nam'),
kUserDataTextCopyright = FOUR_CHAR_CODE('©cpy'),
kUserDataTextInformation = FOUR_CHAR_CODE('©inf')
}
These are the three movie annotations that appear in the movie information dialog box displayed by the ShowMovieInformation function (see Figure 2). What we want to do now is show how to add these three kinds of movie annotations to a QuickTime movie file; or, if a movie file already contains annotations of these sorts, we want to show how to edit those annotations. We'll handle both of these tasks by displaying an Edit Annotation dialog box that contains an editable text field in which the user can add or edit an annotation. For example, if the user selects "Add Information..." in the Test menu of QTInfo but the frontmost movie has no information annotation, we'll display the dialog box shown in Figure 9.
Figure 9. QTInfo's Edit Annotation dialog box
As you might have guessed from the constants listed above, a movie annotation is stored in a QuickTime movie file as a piece of movie user data. We've already worked a little with the GetMovieUserData, GetUserDataItem, and SetUserDataItem functions for getting and setting a piece of a movie's user data (see "Movie Controller Potpourri" in MacTech, February 2000). Because the data for a movie annotation is always text data, here we'll use the GetUserDataText and AddUserDataText functions, which are specialized versions of GetUserDataItem and SetUserDataItem.
When the user selects one of our three menu items for adding or editing a movie annotation, QTInfo executes the QTInfo_EditAnnotation function, passing it a movie identifier and the type of annotation to add or edit. For instance, if the user selects the "Add Information..." item, QTInfo executes this block of code:
case IDM_ADD_INFORMATION:
myIsChanged = QTInfo_EditAnnotation(myMovie,
kUserDataTextInformation);
if (myIsChanged)
(**myWindowObject).fIsDirty = true;
myIsHandled = true;
break;
In the QTInfo_EditAnnotation function, we need to display the Edit Annotation dialog box, put the current movie annotation of the selected kind (if one exists) into the editable text field, allow the user to alter the annotation as desired, and then - if the user clicks the OK button - retrieve the new or edited annotation and attach it to the movie file as a piece of movie user data. Let's consider each of these steps.
Creating the Edit Annotation Dialog Box
Our Edit Annotation dialog box contains four items, as shown in the ResEdit version of our dialog item list ('DITL') resource depicted in Figure 10.
Figure 10. The dialog item list for the Edit Annotation dialog box
To be honest, I must admit that I simply "borrowed" this item list from the resource fork of the QuickTime Player application (and I was even too lazy to renumber it). To refer to the items in this dialog box, we'll define these constants:
#define kEditTextResourceID 548
#define kEditTextItemOK 1
#define kEditTextItemCancel 2
#define kEditTextItemEditBox 3
#define kEditTextItemEditLabel 4
Our resource fork also contains a 'DLOG' resource of the same ID (again "borrowed" from QuickTime Player) that uses this dialog item list. We can open the Edit Annotation dialog box, therefore, by executing this code:
myDialog = GetNewDialog(kEditTextResourceID, NULL,
(WindowPtr)-1L);
The dialog box is initially invisible, so that we have an opportunity to configure it before displaying it on the screen. For instance, we want to set both the default button (which is outlined in bold and activated when the user types the Return or Enter key) and the cancel button (which is activated when the user types the Escape key or the Command-period key combination). We can do this as follows:
SetDialogDefaultItem(myDialog, kEditTextItemOK);
SetDialogCancelItem(myDialog, kEditTextItemCancel);
Next, we want to set the static text item (item 4) to indicate which type of movie annotation is being added or edited. I've added a resource of type 'STR#' that contains three strings, one for each of the types of movie annotation that QTInfo can handle. We'll use these constants to access those strings:
#define kTextKindsResourceID 2000
#define kTextKindsFullName 1
#define kTextKindsCopyright 2
#define kTextKindsInformation 3
We'll simply retrieve one of these strings from that resource, according to the type of annotation that QTInfo_EditAnnotation is asked to handle, as shown in Listing 9.
Listing 9: Setting the label for a movie annotation
QTInfo_EditAnnotation
// get a string for the specified annotation type
switch (theType) {
case kUserDataTextFullName:
GetIndString(myString, kTextKindsResourceID,
kTextKindsFullName);
break;
case kUserDataTextCopyright:
GetIndString(myString, kTextKindsResourceID,
kTextKindsCopyright);
break;
case kUserDataTextInformation:
GetIndString(myString, kTextKindsResourceID,
kTextKindsInformation);
break;
}
GetDialogItem(myDialog, kEditTextItemEditLabel, &myItemKind,
&myItemHandle, &myItemRect);
SetDialogItemText(myItemHandle, myString);
As you can see, we call GetDialogItem to get a handle to the static text item and SetDialogItemText to set the string as the text of that item.
Showing the Current Annotation
We also want to call SetDialogItemText to set the current annotation, if one exists, as the text of the editable text item. First, however, we need to find the current annotation of the specified type. As mentioned earlier, we'll use the GetUserDataText function to do this. GetUserDataText reads the movie annotation of a specified type from a user data item list, which we first obtain by calling GetMovieUserData, like this:
myUserData = GetMovieUserData(theMovie);
GetUserDataText returns the requested information in a handle, which it resizes as necessary to exactly hold the text. So we can retrieve the current movie annotation of the desired type using code like this:
myHandle = NewHandleClear(4);
if (myHandle != NULL) {
myErr = GetUserDataText(myUserData, myHandle, theType, 1,
GetScriptManagerVariable(smRegionCode));
// some lines omitted here
}
The final parameter passed to GetUserDataText is a region code, which specifies a version of a written language of a particular region in the world. It's possible to have several movie annotations of the same type, which differ only in their region code - that is to say, their language. Here we're using the Script Manager function GetScriptManagerVariable to get the region code associated with the user's current script system.
Once we've called GetUserDataText to get the current annotation of the specified type, we need to copy the text in myHandle into a Pascal string. That's because SetDialogItemText takes a Pascal string as a parameter, not a handle. We can use the function QTInfo_TextHandleToPString, defined in Listing 10, to make this conversion.
Listing 10: Copying text from a handle into a Pascal string
QTInfo_TextHandleToPString
void QTInfo_TextHandleToPString (Handle theHandle,
Str255 theString)
{
short myCount;
myCount = GetHandleSize(theHandle);
if (myCount > 255)
myCount = 255;
theString[0] = myCount;
BlockMoveData(*theHandle, &(theString[1]), myCount);
}
So now we are finally ready to insert the existing annotation into the Edit Annotation dialog box. We can do this with these two lines of code:
GetDialogItem(myDialog, kEditTextItemEditBox, &myItemKind,
&myItemHandle, &myItemRect);
SetDialogItemText(myItemHandle, myString);
The last thing we need to do before displaying the dialog box to the user is set the current selection range of the annotation text. When QuickTime Player displays its Edit Annotation dialog box, it selects all the text in the editable text item. We'll follow this example by calling SelectDialogItemText like this:
SelectDialogItemText(myDialog, kEditTextItemEditBox, 0, myString[0]);
At this point, the Edit Annotation dialog box is fully configured. Its static text item has been updated to indicate which type of movie annotation is being edited, and the current annotation of that type has been inserted into the editable text item. We can finish up by actually showing the dialog box to the user:
MacShowWindow(GetDialogWindow(myDialog));
Retrieving the Edited Annotation
We allow the user to interact with the items in the Edit Annotation dialog box by calling ModalDialog:
do {
ModalDialog(gModalFilterUPP, &myItem);
} while ((myItem != kEditTextItemOK)
&& (myItem != kEditTextItemCancel));
As you can see, ModalDialog is called continually until the user clicks the OK or Cancel button (or types a key or key combination that is interpreted as a click on one of those buttons). If the user clicks the Cancel button, we should just exit the QTInfo_EditAnnotation function after disposing of the Edit Annotation dialog box and performing any other necessary clean-up.
if (myItem != kEditTextItemOK)
goto bail;
But if the user clicks the OK button, we need to retrieve the text in the editable text item and set it as the movie annotation of the specified type. We can get the edited text like this:
GetDialogItem(myDialog, kEditTextItemEditBox, &myItemKind,
&myItemHandle, &myItemRect);
GetDialogItemText(myItemHandle, myString);
We want to call AddUserDataText to insert the user's edited annotation into the movie user data list. To do this, we first need to convert the Pascal string returned by GetDialogItemText into a handle. We can use the QTInfo_PStringToTextHandle function, defined in Listing 11, to handle this conversion.
Listing 11: Copying text from a Pascal string into a handle
QTInfo_PStringToTextHandle
void QTInfo_PStringToTextHandle (Str255 theString, Handle theHandle)
{
SetHandleSize(theHandle, theString[0]);
if (GetHandleSize(theHandle) != theString[0])
return;
BlockMoveData(&(theString[1]), *theHandle, theString[0]);
}
Now we are ready to call AddUserDataText:
myErr = AddUserDataText(myUserData, myHandle, theType, 1,
GetScriptManagerVariable(smRegionCode));
Again, we're calling GetScriptManagerVariable to get the user's current region code, so that the annotation is written into the movie file in a form recognizable to the user. Listing 12 shows the complete function QTInfo_EditAnnotation.
Listing 12: Editing a movie annotation
QTInfo_EditAnnotation
Boolean QTInfo_EditAnnotation (Movie theMovie, OSType theType)
{
DialogPtr myDialog = NULL;
short myItem;
short mySavedResFile;
GrafPtr mySavedPort;
Handle myHandle = NULL;
short myItemKind;
Handle myItemHandle;
UserData myUserData = NULL;
Rect myItemRect;
Str255 myString;
Boolean myIsChanged = false;
OSErr myErr = noErr;
// save the current resource file and graphics port
mySavedResFile = CurResFile();
GetPort(&mySavedPort);
// set the application's resource file
UseResFile(gAppResFile);
// get the movie user data
myUserData = GetMovieUserData(theMovie);
if (myUserData == NULL)
goto bail;
// create the dialog box in which the user will add or edit the annotation
myDialog = GetNewDialog(kEditTextResourceID, NULL,
(WindowPtr)-1L);
if (myDialog == NULL)
goto bail;
#if TARGET_API_MAC_CARBON
SetPortDialogPort(myDialog);
#else
MacSetPort(myDialog);
#endif
SetDialogDefaultItem(myDialog, kEditTextItemOK);
SetDialogCancelItem(myDialog, kEditTextItemCancel);
// get a string for the specified annotation type
switch (theType) {
case kUserDataTextFullName:
GetIndString(myString, kTextKindsResourceID,
kTextKindsFullName);
break;
case kUserDataTextCopyright:
GetIndString(myString, kTextKindsResourceID,
kTextKindsCopyright);
break;
case kUserDataTextInformation:
GetIndString(myString, kTextKindsResourceID,
kTextKindsInformation);
break;
}
GetDialogItem(myDialog, kEditTextItemEditLabel,
&myItemKind, &myItemHandle, &myItemRect);
SetDialogItemText(myItemHandle, myString);
// set the current annotation of the specified type, if it exists
myHandle = NewHandleClear(4);
if (myHandle != NULL) {
myErr = GetUserDataText(myUserData, myHandle, theType, 1,
GetScriptManagerVariable(smRegionCode));
if (myErr == noErr) {
QTInfo_TextHandleToPString(myHandle, myString);
GetDialogItem(myDialog, kEditTextItemEditBox,
&myItemKind, &myItemHandle, &myItemRect);
SetDialogItemText(myItemHandle, myString);
SelectDialogItemText(myDialog, kEditTextItemEditBox, 0,
myString[0]);
}
DisposeHandle(myHandle);
}
MacShowWindow(GetDialogWindow(myDialog));
// display and handle events in the dialog box until the user clicks OK or Cancel
do {
ModalDialog(gModalFilterUPP, &myItem);
} while ((myItem != kEditTextItemOK)
&& (myItem != kEditTextItemCancel));
// handle the selected button
if (myItem != kEditTextItemOK)
goto bail;
// retrieve the edited text
myHandle = NewHandleClear(4);
if (myHandle != NULL) {
GetDialogItem(myDialog, kEditTextItemEditBox,
&myItemKind, &myItemHandle, &myItemRect);
GetDialogItemText(myItemHandle, myString);
QTInfo_PStringToTextHandle(myString, myHandle);
myErr = AddUserDataText(myUserData, myHandle, theType, 1,
GetScriptManagerVariable(smRegionCode));
myIsChanged = (myErr == noErr);
DisposeHandle(myHandle);
}
bail:
// restore the previous resource file and graphics port
MacSetPort(mySavedPort);
UseResFile(mySavedResFile);
if (myDialog != NULL)
DisposeDialog(myDialog);
return(myIsChanged);
}
Note that QTInfo_EditAnnotation returns a Boolean value that indicates whether the user clicked the OK button and the specified movie annotation was successfully updated. QTInfo uses that value to determine whether it should mark the movie as dirty (and hence in need of saving). It's possible, however, that the user clicked the OK button without having altered the movie annotation in the editable text item. In that case, the movie would be marked as dirty even though its user data has not actually changed. It would be easy to modify QTInfo_EditAnnotation so that it compares the original annotation and the annotation later retrieved from the text box to see whether they differ. This enhancement is left as an exercise for the reader. (It's worth noting, however, that the behavior of QTInfo in this regard is identical to that of QuickTime Player.)
Conclusion
In this article, we've learned how to get and set some of the information that's stored in a QuickTime movie file. We've seen how to work with movie posters and movie previews, and we've seen how to add file previews to both double-fork and single-fork QuickTime movie files. We still have a little bit of work to do on the QTInfo_MakeFilePreview function (which we've deferred until the following article), but already it can write file previews into single-fork movie files.
We've also seen how to add annotations to a movie file and edit a file's existing annotations. Our sample application QTInfo allows the user to edit any of the three kinds of annotations displayed in the movie information dialog box. With just a little bit of work, however, the QTInfo_EditAnnotation function could be modified to support editing any type of movie annotation. So what we've got are the essential elements of a general-purpose tool for adding and changing any text-based movie user data.
But, as I've said, we still have some work to do to clean up one or two loose ends in this month's code. Next time we'll see how to find specific atoms in a QuickTime movie file. We'll also discover another kind of atom structure that can be found lurking deep inside QuickTime movie files.
Credits
Thanks are due to Jim Luther for pointing me in the right direction in the QTInfo_IsRefNumOfResourceFork function (in the file QTInfo.c) and to Brian Friedkin for clarifying the behavior of StandardGetFilePreview under Windows.
Tim Monroe has worked at Apple for over 10 years, first as a technical writer in the Inside Macintosh group and later as a software engineer in the QuickTime group. Currently he is developing sample code and utilities for the QuickTime software development kit. You can reach him at monroe@apple.com.