TweetFollow Us on Twitter

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.

 
AAPL
$102.24
Apple Inc.
+0.45
MSFT
$46.73
Microsoft Corpora
+0.05
GOOG
$591.40
Google Inc.
+2.13

MacTech Search:
Community Search:

Software Updates via MacUpdate

Attachment Tamer 3.1.14b9 - Take control...
Attachment Tamer gives you control over attachment handling in Apple Mail. It fixes the most annoying Apple Mail flaws, ensures compatibility with other email software, and allows you to set up how... Read more
Duplicate Annihilator 5.0 - Find and del...
Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes. Duplicate Annihilator detects... Read more
jAlbum Pro 12.2 - Organize your digital...
jAlbum Pro has all the features you love in jAlbum, but comes with a commercial license. With jAlbum, you can create gorgeous custom photo galleries for the Web without writing a line of code!... Read more
jAlbum 12.2 - Create custom photo galler...
With jAlbum, you can create gorgeous custom photo galleries for the Web without writing a line of code! Beginner-friendly, with pro results Simply drag and drop photos into groups, choose a design... Read more
Quicken 2015 2.0.4 - Complete personal f...
Quicken 2015 helps you manage all your personal finances in one place, so you can see where you're spending and where you can save. Quicken automatically categorizes your financial transactions,... Read more
iMazing 1.0 - Complete iOS device manage...
iMazing (formerly DiskAid) is the ultimate iOS device manager with capabilities far beyond what iTunes offers. With iMazing and your iOS device (iPhone, iPad, or iPod), you can: Copy music to and... Read more
Xcode 6.0.1 - Integrated development env...
Apple Xcode is Apple Computer's integrated development environment (IDE) for OS X. The full Xcode package is free to ADC members and includes all the tools you need to create, debug, and optimize... Read more
Apple Safari 7.1 - Apple's Web brow...
Apple Safari in OS X Mavericks brings you all-new ways to find and enjoy the best of the web. It works with iCloud to give you a seamless browsing experience across all your devices. It looks out for... Read more
Delivery Status 6.1.2 - Check delivery s...
Delivery Status displays delivery status of packages for a variety of shipment services. Can't wait for your packages to arrive? Don't waste your time checking the site constantly, just open this all... Read more
Mavericks Cache Cleaner 8.0.9 - Clear ca...
Mavericks Cache Cleaner is an award-winning general purpose tool for OS X. MCC makes system maintenance simple with an easy point-and-click interface to many OS X functions. Novice and expert users... Read more

Latest Forum Discussions

See All

MUJO Review
MUJO Review By Campbell Bird on September 19th, 2014 Our Rating: :: ASSEMBLE THE GODSUniversal App - Designed for iPhone and iPad This match-three game has collectible and role-playing elements that make it continually satisfying... | Read more »
Project Life (Photography)
Project Life 1.0 Device: iOS Universal Category: Photography Price: $2.99, Version: 1.0 (iTunes) Description: Imagine scrapbooking without scissors or adhesive or tools … or without having to print photos! Never before has... | Read more »
Skater (Games)
Skater 1.0 Device: iOS iPhone Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: All of Skateboarding In The Palm Of Your Hand Designed by skaters for skaters, we teamed up with 17 of the most prominent brands in... | Read more »
Huerons (Games)
Huerons 1.1 Device: iOS Universal Category: Games Price: $.99, Version: 1.1 (iTunes) Description: EXCLUSIVE LAUNCH PRICE! Huerons is 50% off until September 20th! Huerons are tiny colored circles. Merge them by clicking on an empty... | Read more »
Down Among the Dead Men (Games)
Down Among the Dead Men 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: Avast! Take to the high seas in a fully interactive piratical tale of broadsides and buccaneers. From author Dave... | Read more »
Sling Adds Chromecast Support Through Sl...
Sling Adds Chromecast Support Through Slingplaye​r Mobile Apps Posted by Jessica Fisher on September 18th, 2014 [ permalink ] | Read more »
How to Completely Delete Your iPhone’s C...
The iPhone 6 is out tomorrow, and plenty of people are excited about it. So much so that they’re planning to – or already have – traded in their old iPhone to go towards it. The thing about trading in hardware is it’s very important to make sure... | Read more »
Dragon Quest I Review
Dragon Quest I Review By Andrew Fisher on September 18th, 2014 Our Rating: :: THINE QUEST AWAITETHUniversal App - Designed for iPhone and iPad Its historical significance aside, Dragon Quest 1 is a fun, campy, difficult, thoroughly... | Read more »
It Came From Canada: Overkill 3
Overkill 3 is like every trope of big modern gaming rolled into one. It’s a sequel to an action-packed military shooter. It’s flashy and scripted and flaunts its sophisticated graphics. And it’s a mobile game with a heavy emphasis on in-app... | Read more »
Down Among the Dead Men Review
Down Among the Dead Men Review By Jennifer Allen on September 18th, 2014 Our Rating: :: AHOY, ME HEARTIESUniversal App - Designed for iPhone and iPad Down Among the Dead is a pirate themed gamebook that’s quite delightful, if short... | Read more »

Price Scanner via MacPrices.net

Previous-generation 15-inch 2.0GHz Retina Mac...
B&H Photo has leftover previous-generation 15″ 2.0GHz Retina MacBook Pros now available for $1599 including free shipping plus NY sales tax only. Their price is $400 off original MSRP. B&H... Read more
21″ 2.7GHz iMac available for $1179, save $12...
Adorama has 21″ 2.7GHz Hawell iMacs on sale for $1179.99 including free shipping. Their price is $120 off MSRP. NY and NJ sales tax only. Read more
iOS 8 Adoption Rate Slower than iOS 7, 6, Hit...
Apple began pushing out iOS 8 updates to eligible devices around 1pm ET on September 17, 2014. However, unlike with iOS 7, which boasted a wide variety of differences from its predecessor iOS 6, in... Read more
LIkely Final Definitive OS X 10.9.5 Mavericks...
Apple has released what will almost certainly be the last incremental version number update of OS X 10.9 Mavericks (save for futire security updates) before OS X 10.10 Yosemite is released next month... Read more
Fingerprints, Apple Pay and Identity Theft Wa...
On Sep 9th, CEO Tim Cook unveiled Apple Pay, along with the new iPhone 6 and iWatch. Apple Pay is a newly developed technology that utilizes a near field communication (NFC) to enable customer... Read more
Amazon Introduces Two All-New Kindles
Amazon on Thursday introduced the 7th generation of its Kindle dedicated e-reader device: Kindle Voyage, its top-of-the-line e-reader, and the new $79 Kindle, with a 20% faster processor, twice the... Read more
Save up to $300 on the price of a new Mac wit...
Purchase a new Mac or iPad at The Apple Store for Education and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free,... Read more
13-inch 2.8GHz Retina MacBook Pro available f...
B&H Photo has the new 2014 13″ 2.8GHz Retina MacBook Pro on sale for $1699.99 including free shipping plus NY sales tax only. They’ll also include free copies of Parallels Desktop and LoJack for... Read more
16GB iPad Air on sale for $449, save $50
Walmart has the 16GB iPad Air WiFi on sale for $449 on their online store for a limited time. Choose free home shipping or free local store pickup. Their price represents a $50 savings over standard... Read more
13-inch 256GB MacBook Air on sale for $1099,...
B&H Photo has the 2014 13″ 1.4GHz 256GB MacBook Air on sale for $1099.99. Shipping is free, and B&H charges NY sales tax only. Their price is $100 off MSRP. Read more

Jobs Board

Project Manager, *Apple* Financial Services...
**Job Summary** Apple Financial Services (AFS) offers consumers, businesses and educational institutions ways to finance Apple purchases. We work with national and Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.