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.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Whitethorn Games combines two completely...
If you have ever gone fishing then you know that it is a lesson in patience, sitting around waiting for a bite that may never come. Well, that's because you have been doing it wrong, since as Whitehorn Games now demonstrates in new release Skate... | Read more »
Call of Duty Warzone is a Waiting Simula...
It's always fun when a splashy multiplayer game comes to mobile because they are few and far between, so I was excited to see the notification about Call of Duty: Warzone Mobile (finally) launching last week and wanted to try it out. As someone who... | Read more »
Albion Online introduces some massive ne...
Sandbox Interactive has announced an upcoming update to its flagship MMORPG Albion Online, containing massive updates to its existing guild Vs guild systems. Someone clearly rewatched the Helms Deep battle in Lord of the Rings and spent the next... | Read more »
Chucklefish announces launch date of the...
Chucklefish, the indie London-based team we probably all know from developing Terraria or their stint publishing Stardew Valley, has revealed the mobile release date for roguelike deck-builder Wildfrost. Developed by Gaziter and Deadpan Games, the... | Read more »
Netmarble opens pre-registration for act...
It has been close to three years since Netmarble announced they would be adapting the smash series Solo Leveling into a video game, and at last, they have announced the opening of pre-orders for Solo Leveling: Arise. [Read more] | Read more »
PUBG Mobile celebrates sixth anniversary...
For the past six years, PUBG Mobile has been one of the most popular shooters you can play in the palm of your hand, and Krafton is celebrating this milestone and many years of ups by teaming up with hit music man JVKE to create a special song for... | Read more »
ASTRA: Knights of Veda refuse to pump th...
In perhaps the most recent example of being incredibly eager, ASTRA: Knights of Veda has dropped its second collaboration with South Korean boyband Seventeen, named so as it consists of exactly thirteen members and a video collaboration with Lee... | Read more »
Collect all your cats and caterpillars a...
If you are growing tired of trying to build a town with your phone by using it as a tiny, ineffectual shover then fear no longer, as Independent Arts Software has announced the upcoming release of Construction Simulator 4, from the critically... | Read more »
Backbone complete its lineup of 2nd Gene...
With all the ports of big AAA games that have been coming to mobile, it is becoming more convenient than ever to own a good controller, and to help with this Backbone has announced the completion of their 2nd generation product lineup with their... | Read more »
Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »

Price Scanner via MacPrices.net

B&H has Apple’s 13-inch M2 MacBook Airs o...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock and on sale for up to $150 off Apple’s new MSRP, starting at only $849. Free 1-2 day delivery is available to most US... Read more
M2 Mac minis on sale for $100-$200 off MSRP,...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100-$200 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $... Read more
Mac Studios with M2 Max and M2 Ultra CPUs on...
B&H Photo has standard-configuration Mac Studios with Apple’s M2 Max & Ultra CPUs in stock today and on Easter sale for $200 off MSRP. Their prices are the lowest available for these models... Read more
Deal Alert! B&H Photo has Apple’s 14-inch...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on sale for $200-$300 off MSRP, starting at only $1399. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8... Read more
Department Of Justice Sets Sights On Apple In...
NEWS – The ball has finally dropped on the big Apple. The ball (metaphorically speaking) — an antitrust lawsuit filed in the U.S. on March 21 by the Department of Justice (DOJ) — came down following... Read more
New 13-inch M3 MacBook Air on sale for $999,...
Amazon has Apple’s new 13″ M3 MacBook Air on sale for $100 off MSRP for the first time, now just $999 shipped. Shipping is free: – 13″ MacBook Air (8GB RAM/256GB SSD/Space Gray): $999 $100 off MSRP... Read more
Amazon has Apple’s 9th-generation WiFi iPads...
Amazon has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80-$100 off MSRP, starting only $249. Their prices are the lowest available for new iPads anywhere: – 10″ 64GB WiFi iPad (Space Gray or... Read more
Discounted 14-inch M3 MacBook Pros with 16GB...
Apple retailer Expercom has 14″ MacBook Pros with M3 CPUs and 16GB of standard memory discounted by up to $120 off Apple’s MSRP: – 14″ M3 MacBook Pro (16GB RAM/256GB SSD): $1691.06 $108 off MSRP – 14... Read more
Clearance 15-inch M2 MacBook Airs on sale for...
B&H Photo has Apple’s 15″ MacBook Airs with M2 CPUs (8GB RAM/256GB SSD) in stock today and on clearance sale for $999 in all four colors. Free 1-2 delivery is available to most US addresses.... Read more
Clearance 13-inch M1 MacBook Airs drop to onl...
B&H has Apple’s base 13″ M1 MacBook Air (Space Gray, Silver, & Gold) in stock and on clearance sale today for $300 off MSRP, only $699. Free 1-2 day shipping is available to most addresses in... Read more

Jobs Board

Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Business Analyst | *Apple* Pay - Banco Popu...
Business Analyst | Apple PayApply now " Apply now + Apply Now + Start applying with LinkedIn Start + Please wait Date:Mar 19, 2024 Location: San Juan-Cupey, PR Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.