TweetFollow Us on Twitter

Loaded

Volume Number: 18 (2002)
Issue Number: 9
Column Tag: QuickTime Toolkit

Loaded

Loading QuickTime Movies Asynchronously

by Tim Monroe

Introduction

Typically, we use the NewMovieFromFile function to load a QuickTime movie from a file stored locally. In an earlier article, when we were investigating data references, we saw how to use the NewMovieFromDataRef function to load a movie specified by a URL that picks out a file located on a remote server. (See "Somewhere I'll Find You" in MacTech, October 2000.) In all cases, we have called these functions synchronously. This means that we wait until the function completes and returns a movie to us before we continue on to attach a movie controller to the movie, create a window to hold the movie and the movie controller bar, size and position the movie window appropriately, and so forth.

The drawback with opening movies synchronously is that there may be a perceptible lag as QuickTime retrieves the movie data from the specified location, especially if the data is on a remote server and the user has a relatively slow connection to the network. In version 4.1, QuickTime introduced a mechanism for loading movies asynchronously. In this case, the movie-loading functions (NewMovieFromFile, NewMovieFromDataRef, and their siblings) return almost immediately with a valid movie identifier. This movie, however, might be empty or incomplete, if its data is still being retrieved from a remote location (or a particularly slow local storage device). Before we can actually do anything with the newly-opened movie, we need to wait until at least the movie atom has become available. We can determine this by continually checking the movie load state, an indication of the state of the loading process and hence what operations we can safely perform on the movie. When the movie load state reaches a particular threshold, we can continue on with our standard procedure for displaying the movie in a window on the screen and allowing the user to interact with the movie.

In this article, we're going to learn how to load movies asynchronously. We'll see how to modify our calls to the NewMovieFrom... functions and how to check the movie load state as the movie data loads. The end result of this tinkering should be a more responsive application, since the user will no longer have to wait until a movie is sufficiently loaded for our application to continue processing. Instead, the user can work with any movies that are already open, or indeed open still other movies.

We'll also take the opportunity to add a few bells and whistles to our application. In particular, we'll see how to display a progress bar that indicates how much of the movie data has become available, as shown in Figure 1. (The purple bar grows from left to right in proportion to the amount of movie data loaded.) We do this by drawing on top of each movie frame, using a movie drawing-complete procedure. Inside this procedure, we call the GetMaxLoadedTimeInMovie function to determine how much of the movie has loaded, and then we scale the progress bar accordingly.


Figure 1: An application movie loading progress bar

It's also possible for a QuickTime movie to provide its own loading progress bar, without any assistance from the playback application. We can accomplish this by adding a wired sprite track or a wired Flash track to the movie. The key element here is the kOperandMaxLoadedTimeInMovie wired operand, which returns the same information as the GetMaxLoadedTimeInMovie function. Figure 2 shows a QuickTime movie that draws its own loading progress bar; here the loading progress bar is a sprite whose horizontal position changes based on the movie load time.


Figure 2: A sprite movie loading progress bar

Our sample application this month is called QTAsynchLoad; it's based on the version of QTShell that uses Carbon events (which we developed in "Event Horizon", MacTech, May 2002). Figure 3 shows the Test menu of QTAsynchLoad.


Figure 3: The Test menu of QTAsynchLoad

The first menu item prompts the user for a URL and then opens the movie file picked out by that URL. (I borrowed the code for this directly from the QTDataRef application we developed in the data references article cited earlier.) This is useful for testing that our new code really does load remote movies asynchronously. The second menu item adds to an existing movie a sprite track that displays the movie loading progress bar shown in Figure 2. The third menu item exports a QuickTime VR panorama movie as a Fast Start movie file with a low-resolution image track; this track is loaded first and acts as a kind of preview track for the entire panorama, as we'll see later.

Asynchronous Movie Loading

Let's begin by reviewing our current movie loading strategy. When a user selects the "Open..." command in the File menu, we present a file-opening dialog box and then pass the selected file to the OpenMovieFile function, like this:

myErr = OpenMovieFile(&myFSSpec, &myRefNum, fsRdWrPerm);

OpenMovieFile opens the file with the specified access permissions (here, with read-write access). If it's successful, we then call NewMovieFromFile to load the movie in that file:

myErr = NewMovieFromFile(&myMovie, myRefNum, &myResID, NULL, 
            newMovieActive, NULL);

If NewMovieFromFile is successful, we then perform these actions:

  • Create a window to hold the movie and movie controller bar.

  • Create a window object to hold information about the movie and movie window.

  • Set the movie graphics world to the new movie window.

  • Create a new movie controller for the new movie.

  • Resize the window to exactly fit the movie and movie controller bar.

  • Set the window position from information stored in the movie user data.

  • Make the window visible.

  • Start the movie playing, if it's an auto-play movie.

Currently, all this is accomplished inside of our framework function QTFrame_OpenMovieInWindow.

Allowing Asynchronous Movie Loading

The important point here is that our call to NewMovieFromFile will not return until enough of the movie is available that QuickTime considers the movie to be configurable and playable. We can override this default behavior by including the newMovieAsyncOK flag when we call NewMovieFromFile, like this:

myErr = NewMovieFromFile(&myMovie, myRefNum, &myResID, NULL, 
            newMovieActive + newMovieAsyncOK, NULL);

The newMovieAsyncOK flag tells NewMovieFromFile that it should create a new movie and return it to the caller as quickly as possible, even if the new movie is empty (that is, contains no tracks, no movie atom, no user data, and so forth). In general, for local movie files the new movie is a fully valid movie, complete with tracks and movie user data. That is to say, the movie loading occurs exactly as if the newMovieAsyncOK flag had not been specified. This flag simply grants permission to QuickTime to use asynchronous loading if possible; it does not force the movie to be loaded asynchronously.

The more interesting case is therefore when we open a movie stored remotely, which we can do by passing a URL to the QTDR_GetMovieFromURL function we defined in an earlier article. To enable asynchronous movie loading, we once again add the newMovieAsyncOK flag, as shown in Listing 1.

Listing 1: Opening a movie specified by a URL

QTDR_GetMovieFromURL
Movie QTDR_GetMovieFromURL (char *theURL)
{
   Movie      myMovie = NULL;
   Handle      myDataRef = NULL;
   short      myFlags = newMovieActive;
#if ALLOW_ASYNCH_LOADING
   myFlags += newMovieAsyncOK;
#endif
   myDataRef = QTDR_MakeURLDataRef(theURL);
   if (myDataRef != NULL) {
      NewMovieFromDataRef(&myMovie, myFlags, NULL, myDataRef, 
            URLDataHandlerSubType);
      DisposeHandle(myDataRef);
   }
   return(myMovie);
}

You'll notice the ALLOW_ASYNCH_LOADING compiler flag; once again, I'm going to try to rework our source code so that the existing synchronous movie-loading behavior can be re-enabled by changing the value of that flag.

Checking the Movie Load State

Since the movie returned by NewMovieFromDataRef (or its siblings) may be empty, we can't just launch into our standard sequence of calls for creating and configuring a new movie window. Instead, we need to wait until the movie reaches the proper load state. We can get the current load state by calling GetMovieLoadState, like this:

myLoadState = GetMovieLoadState(myMovie);
GetMovieLoadState currently returns one of these values:
enum {
   kMovieLoadStateError                     = -1L,
   kMovieLoadStateLoading                  = 1000,
   kMovieLoadStateLoaded                     = 2000,
   kMovieLoadStatePlayable                  = 10000,
   kMovieLoadStatePlaythroughOK            = 20000,
   kMovieLoadStateComplete                  = 100000L
};

If GetMovieLoadState returns kMovieLoadStateError, then an error has occurred during the movie load process. This probably means that a URL could not be successfully resolved, but it could also mean that the indicated file is not a QuickTime movie file (or other file type that can be imported as a QuickTime movie).

If GetMovieLoadState returns kMovieLoadStateLoading, then QuickTime has found the specified file (or data stream) and is searching for the movie atom in it. If GetMovieLoadState returns kMovieLoadStateLoaded, then QuickTime has found the movie atom, but there may not be enough media data available to begin playing the movie. (It's not clear to me that GetMovieLoadState ever actually returns kMovieLoadStateLoaded; I suspect that this state has fallen into disuse.)

When a movie's load state reaches kMovieLoadStatePlayable, the movie atom is available and enough of the media data is available that the movie can be prerolled and started. This is the threshold that is most interesting to us, since we can then look at the movie user data (which is stored in the movie atom) to determine the desired window location of the movie window; we can also make Movie Toolbox calls like GetMovieTrackCount to determine the number of tracks in the movie or GetMovieBox to determine the size of the movie.

GetMovieLoadState returns kMovieLoadStatePlaythroughOK when QuickTime thinks that it has enough of the media data that the entire movie can be played through to the end without stopping. Finally, GetMovieLoadState returns kMovieLoadStateComplete when all of the movie's media data is available.

Modifying the Application Framework

If we allow a movie to be opened asynchronously, then we need to wait until the movie reaches at least the kMovieLoadStatePlayable load state before we assign a movie controller to it and undertake any other configuration of the movie or movie controller. The first thing we need to do, then, is revise our framework function QTFrame_OpenMovieInWindow. Listing 2 shows the relevant portions of our new version of this function. As you can see, we open a new, invisible window, allocate a new window object, and set the movie (or graphics importer) graphics world to the new window. Then, if the user is opening an image file instead of a movie file, we proceed to set up the image window, as we did previously.

Listing 2: Opening a movie in a new window

QTFrame_OpenMovieInWindow
// create a new window to display the movie in
myWindow = QTFrame_CreateMovieWindow();
if (myWindow == NULL)
   goto bail;
myWindowObject = QTFrame_GetWindowObjectFromWindow(myWindow);
if (myWindowObject == NULL)
   goto bail;
// set the window title
QTFrame_SetWindowTitleFromFSSpec(myWindow, &myFSSpec, true);
// make sure the movie or image file uses the window GWorld
if (myMovie != NULL)
   SetMovieGWorld(myMovie, 
         (CGrafPtr)QTFrame_GetPortFromWindowReference(myWindow), 
         NULL);
if (myImporter != NULL)
   GraphicsImportSetGWorld(myImporter, 
         (CGrafPtr)QTFrame_GetPortFromWindowReference(myWindow), 
         NULL);
// store movie info in the window record
(**myWindowObject).fMovie = myMovie;
(**myWindowObject).fController = NULL;
(**myWindowObject).fGraphicsImporter = myImporter;
(**myWindowObject).fFileResID = myResID;
(**myWindowObject).fFileRefNum = myRefNum;
(**myWindowObject).fCanResizeWindow = true;
(**myWindowObject).fIsDirty = false;
(**myWindowObject).fIsQTVRMovie = false;
(**myWindowObject).fInstance = NULL;
(**myWindowObject).fAppData = NULL;
(**myWindowObject).fFileFSSpec = myFSSpec;
if ((**myWindowObject).fGraphicsImporter != NULL) {
   Point         myPoint = {kDefaultWindowX, kDefaultWindowY};
   // do any application-specific window object initialization
   QTApp_SetupWindowObject(myWindowObject);
   // size the window to fit the image
   QTFrame_SizeWindowToMovie(myWindowObject);
   // show the window
   QTFrame_ShowWindowAtPoint(myWindow, &myPoint);
}

Mostly we've just removed our movie-specific processing, which we want to defer until we know the movie is playable. We've also added a call to the QTFrame_ShowWindowAtPoint function, which makes the image or movie window visible. Listing 3 shows our definition of QTFrame_ShowWindowAtPoint.

Listing 3: Making a window visible

QTFrame_ShowWindowAtPoint
void QTFrame_ShowWindowAtPoint 
               (WindowReference theWindow, Point *thePoint)
{
#if TARGET_OS_MAC
   Rect         myRect;
#endif
   if ((theWindow == NULL) || (thePoint == NULL))
      return;
#if TARGET_OS_MAC
   MoveWindow(theWindow, thePoint->h, thePoint->v, false);
   MacShowWindow(theWindow);
   SelectWindow(theWindow);
   InvalWindowRect(theWindow, 
            GetWindowPortBounds(theWindow, &myRect));
#endif
#if TARGET_OS_WIN32
   SetWindowPos(theWindow, 0, thePoint->h, thePoint->v, 0, 0, 
            SWP_NOZORDER | SWP_NOSIZE);
   ShowWindow(theWindow, SW_SHOW);
   UpdateWindow(theWindow);
#endif
}

At this point, we need to decide where in our application code we want to call GetMovieLoadState. On Macintosh operating systems, our applications call QTFrame_CheckMovieControllers to give each open movie controller a chance to handle an event. This is also a good place to check the current load state of a movie and to respond to any changes in the movie load state. Listing 4 shows our current version of QTFrame_CheckMovieControllers.

Listing 4: Checking the movie controllers (original)

QTFrame_CheckMovieControllers
static Boolean QTFrame_CheckMovieControllers 
            (EventRecord *theEvent)
{   
   WindowPtr            myWindow = NULL;
   MovieController      myMC = NULL;
   myWindow = QTFrame_GetFrontMovieWindow();
   while (myWindow != NULL) {
      myMC = QTFrame_GetMCFromWindow(myWindow);
      if (myMC != NULL)
         if (MCIsPlayerEvent(myMC, theEvent))
            return(true);
      myWindow = QTFrame_GetNextMovieWindow(myWindow);
   }
   return(false);
}

Listing 5 shows our revised version of QTFrame_CheckMovieControllers. We've added a call to QTFrame_CheckMovieLoadState, and we've revised the looping so that QTFrame_CheckMovieLoadState can safely destroy the movie window if it determines that an error has occurred in the movie loading (that is, if GetMovieLoadState returns kMovieLoadStateError).

Listing 5: Checking the movie controllers (revised)

QTFrame_CheckMovieControllers
static Boolean QTFrame_CheckMovieControllers 
            (EventRecord *theEvent)
{   
   WindowPtr            myWindow = NULL;
   MovieController      myMC = NULL;
   myWindow = QTFrame_GetFrontMovieWindow();
   while (myWindow != NULL) {
      WindowPtr         myNextWindow = QTFrame_GetNextMovieWindow
                                                   (myWindow);
      QTFrame_CheckMovieLoadState(
                  QTFrame_GetWindowObjectFromWindow(myWindow));
      myMC = QTFrame_GetMCFromWindow(myWindow);
      if (myMC != NULL)
         if (MCIsPlayerEvent(myMC, theEvent))
            return(true);
      myWindow = myNextWindow;
   }
   return(false);
}

On Windows operating systems, messages are sent directly to the window procedure of a movie window, where we translate the message into a Mac-style event. Here we'll call QTFrame_CheckMovieLoadState from within QTFrame_MovieWndProc, as shown in Listing 6.

Listing 6: Checking the movie controller (Windows)

QTFrame_MovieWndProc
// translate a Windows event to a Mac event
WinEventToMacEvent(&myMsg, &myMacEvent);
// let the application-specific code have a chance to intercept the event
myIsHandled = QTApp_HandleEvent(&myMacEvent);
if (myWindowObject != NULL) {
   QTFrame_CheckMovieLoadState(myWindowObject);
   if (myWindowObject == NULL)
      return(0);
   myMC = (**myWindowObject).fController;      // refresh our local variable
}
// pass the Mac event to the movie controller
if (!myIsHandled)
   if (myMC != NULL)
      if (!IsIconic(theWnd))
         myIsHandled = MCIsPlayerEvent(myMC, 
                                 (EventRecord *)&myMacEvent);

As you can see, we call QTFrame_CheckMovieLoadState and then immediately check to see whether the window object has been destroyed. Again, this is to protect ourselves in case an error occurs when attempting to load the movie. Also, we refresh the local variable myMC, since QTFrame_CheckMovieLoadState might have created a new movie controller for the movie.

Handling Load State Changes

So, all we need to do now is define the function QTFrame_CheckMovieLoadState. This function calls GetMovieLoadState and then inspects the returned movie load state to determine how to proceed. As we've already mentioned, we want to close the movie window that we created in QTFrame_OpenMovieInWindow if the movie load state is kMovieLoadStateError. Listing 7 shows the code we execute in this case.

Listing 7: Handling a movie load error

QTFrame_CheckMovieLoadState
if (myLoadState <= kMovieLoadStateError) {
   // close the movie window
   if ((**theWindowObject).fWindow != NULL)
      QTFrame_DestroyMovieWindow((**theWindowObject).fWindow);
   myErr = invalidMovie;
}

Here we test to see whether the movie load state is less than or equal to kMovieLoadStateError. Apple reserves the right to define other movie load states in the future, so all that really matters is whether the load state has reached a certain threshold. That's why we'll test for ranges of values in our code.

When an error has not occurred, we next want to see whether we've reached the playable state. If not, we need to task the movie so that it gets time to load:

else if (myLoadState < kMovieLoadStatePlayable) {
   // task the movie so it gets time to load
   MoviesTask(myMovie, 1);
}

The first stage at which we do something interesting is when the movie is playable. At this point, we need to create and configure a movie controller if it has not already been created. Here's where we'll put most of the code that we cut out of our synchronous version of QTFrame_OpenMovieInWindow. Listing 8 shows our complete definition of QTFrame_CheckMovieLoadState.

Listing 8: Checking the movie load state

QTFrame_CheckMovieLoadState
OSErr QTFrame_CheckMovieLoadState 
            (WindowObject theWindowObject)
{
   Movie                     myMovie = NULL;
   MovieController         myMC = NULL;
   long                        myLoadState = 0L;
   long                        myPrevState = 0L;
   OSErr                     myErr = noErr;
   if (theWindowObject == NULL)
      return(paramErr);
   // if the window contains an image, we can return
   if ((**theWindowObject).fGraphicsImporter != NULL)
      return(noErr);
   // if the window does not contain a movie, we can return
   myMovie = (**theWindowObject).fMovie;
   if (myMovie == NULL)
      return(paramErr);
#if TARGET_OS_WIN32
   // if we are adjusting the window location or size, don't go any further
   if (gWeAreSizingWindow)
      return(noErr);
#endif
   myMC = (**theWindowObject).fController;
#if ALLOW_ASYNCH_LOADING
   // get the previous load state
   myPrevState = (**theWindowObject).fLoadState;
#endif
   // if we're already fully loaded and configured, we can return
   if ((myPrevState >= kMovieLoadStateComplete) && 
            (myMC != NULL))
      return(noErr);
   // get the current load state
   myLoadState = GetMovieLoadState(myMovie);
#if ALLOW_ASYNCH_LOADING
   // remember the new state
   (**theWindowObject).fLoadState = myLoadState;
#endif
   // process the movie according to its current load state
   if (myLoadState <= kMovieLoadStateError) {
      // an error occurred while attempting to load the movie; close the movie window
      if ((**theWindowObject).fWindow != NULL)
         QTFrame_DestroyMovieWindow(
               (**theWindowObject).fWindow);
      myErr = invalidMovie;
   } else if (myLoadState < kMovieLoadStatePlayable) {
      // we're not playable yet; task the movie so it gets time to load
      MoviesTask(myMovie, 1);
   } else {
      // we are now playable;
      // if we haven't set up the movie and movie controller, do so now
      if (myMC == NULL) {
         WindowReference      myWindow = 
                                          (**theWindowObject).fWindow;
         Point            myPoint;
         if (myWindow == NULL)
            return(paramErr);
         // set the default progress procedure for the movie
         SetMovieProgressProc(myMovie, (MovieProgressUPP)-1, 0);
         // make sure that the movie is active
         SetMovieActive(myMovie, true);
         // create and configure the movie controller
         myMC = QTFrame_SetupController(myMovie, myWindow, 
            true);
         (**theWindowObject).fController = myMC;
         (**theWindowObject).fIsQTVRMovie = 
            QTUtils_IsQTVRMovie(myMovie);
         // do any application-specific window object initialization
         QTApp_SetupWindowObject(theWindowObject);
         // size the window to fit the movie and controller
         QTFrame_SizeWindowToMovie(theWindowObject);
         // set the movie's play hints to allow dynamic resizing
         SetMoviePlayHints(myMovie, hintsAllowDynamicResize, 
               hintsAllowDynamicResize);
         // set the movie's position, if it has a 'WLOC' user data atom
         QTUtils_GetWindowPositionFromFile(myMovie, &myPoint);
         // show the window
         QTFrame_ShowWindowAtPoint(myWindow, &myPoint);
         if (myMC != NULL) {
            // if the movie is a play-all-frames movie, tell the movie controller
            if (QTUtils_IsPlayAllFramesMovie(myMovie))
               MCDoAction(myMC, mcActionSetPlayEveryFrame, 
                           (void *)true);
#if !ALLOW_ASYNCH_LOADING
            // if the movie is an autoplay movie, then start it playing immediately
            if (QTUtils_IsAutoPlayMovie(myMovie))
               MCDoAction(myMC, mcActionPrerollAndPlay, 
                           (void *)GetMoviePreferredRate(myMovie));
#endif
         }
      }
#if ALLOW_ASYNCH_LOADING
      // if we can play through to the end and we have an autoplay movie, start it playing
      if (myLoadState >= kMovieLoadStatePlaythroughOK) {
         if ((myPrevState < kMovieLoadStatePlaythroughOK) && 
            (myMC != NULL)) {
            // if the movie is an autoplay movie, then start it playing immediately
            if (QTUtils_IsAutoPlayMovie(myMovie))
               MCDoAction(myMC, mcActionPrerollAndPlay, 
                           (void *)GetMoviePreferredRate(myMovie));
         }
      }
#endif
   }
   // do any application-specific processing
   if (myErr == noErr)
      myErr = QTApp_CheckMovieLoadState(theWindowObject, 
               myLoadState, myPrevState);
   return(myErr);
}

You'll notice that, on Windows, we check the global variable gWeAreSizingWindow to see whether we are in the middle of resizing a movie window; if so, we don't continue. This helps us avoid problems when we call QTFrame_ShowWindowAtPoint (which, on Windows, will cause several messages to get sent to the movie, which will eventually trigger a stack overflow).

Notice also that, near the end of QTFrame_CheckMovieLoadState, we call the function QTApp_CheckMovieLoadState, which is defined in the file ComApplication.c. This provides an easy way for us to implement application-specific movie loading behaviors without having to change the underlying framework. In a moment, we'll see how to use this function to control the loader progress bar we draw on top of loading movies.

Adjusting Menu Items

There is one final change we should make to support asynchronous movie loading: we need to prevent the user from exporting or saving a movie file if the movie is not yet fully loaded. That is to say, we should count a movie as savable only if all the media data is available. So we'll add a few lines of code to the QTFrame_AdjustMenus function. Listing 9 shows the segment of QTFrame_AdjustMenus that adjusts the Save and Save As menu items.

Listing 9: Adjusting the Save and Save As menu items

QTFrame_AdjustMenus
if (myWindowObject != NULL) {
   QTFrame_SetMenuItemState(myMenu, IDM_FILESAVEAS, 
            kEnableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_FILESAVE, 
            (**myWindowObject).fIsDirty ? kEnableMenuItem : 
                                                         kDisableMenuItem);
#if ALLOW_ASYNCH_LOADING
   // a movie is savable only if it's completely loaded
   if ((**myWindowObject).fMovie != NULL) {
      if (GetMovieLoadState((**myWindowObject).fMovie) 
                                 < kMovieLoadStateComplete) {
         QTFrame_SetMenuItemState(myMenu, IDM_FILESAVEAS, 
            kDisableMenuItem);
         QTFrame_SetMenuItemState(myMenu, IDM_FILESAVE, 
            kDisableMenuItem);
      }
   }
#endif
} else {
   QTFrame_SetMenuItemState(myMenu, IDM_FILESAVEAS, 
            kDisableMenuItem);
   QTFrame_SetMenuItemState(myMenu, IDM_FILESAVE, 
            kDisableMenuItem);
}

If your application supports other menu items that require all the media data to be available (such as Export or Publish), you should adjust those items as well.

Movie Drawing-Complete Procedures

We've learned how to load QuickTime movies asynchronously, on both Macintosh and Windows operating systems. Now let's see how to add the movie loading progress bar we illustrated earlier, in Figure 1. Figure 4 shows another example of the progress bar at work.


Figure 4: A movie load progress bar on a QuickTime movie

You might have noticed that the standard movie controller already gives us this information, by filling in the time slider rectangle in the movie controller bar. Our progress bar is somewhat more general, however, since it also works for movies that use the QuickTime VR movie controller or the no-interface movie controller. Figure 5 shows our load progress bar drawn on top of a QuickTime VR movie.


Figure 5: A movie load progress bar on a QuickTime VR movie

It's actually quite simple to determine the size of the progress bar while a movie is downloading, using the GetMaxLoadedTimeInMovie function. This function returns the duration of the part of the movie that has already downloaded. For example:

GetMaxLoadedTimeInMovie(theMovie, &myTimeValue);

Here, myTimeValue will contain the duration, in movie time units, of the part of the movie that is available. For example, if the movie's duration is 18000 (say, 30 seconds long with a time scale of 600), then GetMaxLoadedTimeInMovie will return 4500 if the movie is one-quarter downloaded. So if we know the dimensions of the movie (myMovieRect), we can calculate the rectangle for the progress bar like this:

myLoadRect.left = myMovieRect.left;
myLoadRect.bottom = myMovieRect.bottom;
myLoadRect.top = myLoadRect.bottom - kLoaderBarHeight;
myLoadRect.right = myLoadRect.left + 
            (((myMovieRect.right - myMovieRect.left) * 
            myTimeValue) / GetMovieDuration(theMovie));

GetMaxLoadedTimeInMovie returns positive values only once the movie has become playable. If the movie load state is less than kMovieLoadStatePlayable, the returned value is always 0. And of course once the load state reaches kMovieLoadStateComplete, the returned value will be the duration of the movie.

Drawing on Top of a Movie

We can draw our progress bar on top of a movie using a movie drawing-complete procedure. This is a callback function that is executed whenever QuickTime has finished drawing a new frame of a movie. A movie drawing-complete procedure can do all sorts of fun things. In the present case, we'll just fill the progress bar rectangle with a solid color:

PaintRect(&myLoadRect);

Then we'll outline the rectangle with a different color:

MacFrameRect(&myLoadRect);

Our movie drawing-complete procedure is declared like this:

PASCAL_RTN OSErr QTAL_MovieDrawingCompleteProc 
            (Movie theMovie, long theRefCon)

The first parameter is the movie, of course, and the second parameter is an application-specific reference constant. As usual, we'll specify the movie's window object as the reference constant. Listing 10 shows our movie drawing-complete procedure.

Listing 10: Drawing on top of a movie

QTAL_MovieDrawingCompleteProc
PASCAL_RTN OSErr QTAL_MovieDrawingCompleteProc 
            (Movie theMovie, long theRefCon)
{
   Rect                  myMovieRect;
   Rect                  myLoadRect;
   TimeValue         myTimeValue = 0L;
   RGBColor            myOrigColor;
   RGBColor            myLoadColor = {0x6666, 0x6666, 0xcccc};
   RGBColor            myRectColor = {0xeeee, 0xeeee, 0xeeee};
   GrafPtr             mySavedPort;
   WindowObject      myWindowObject = (WindowObject)theRefCon;
   if (myWindowObject == NULL)
      return(paramErr);
   if ((**myWindowObject).fWindow == NULL)
      return(paramErr);
   GetPort(&mySavedPort);
   MacSetPort(QTFrame_GetPortFromWindowReference(
            (**myWindowObject).fWindow));
   GetMovieBox(theMovie, &myMovieRect);
   if (!EmptyRect(&myMovieRect)) {
      GetForeColor(&myOrigColor);   
      RGBForeColor(&myLoadColor);
      GetMaxLoadedTimeInMovie(theMovie, &myTimeValue);
      // calculate the loading progress bar rectangle
      myLoadRect.left = myMovieRect.left;
      myLoadRect.bottom = myMovieRect.bottom;
      myLoadRect.top = myLoadRect.bottom - kLoaderBarHeight;
      myLoadRect.right = myLoadRect.left + 
            (((myMovieRect.right - myMovieRect.left) * 
            myTimeValue) / GetMovieDuration(theMovie));
      PaintRect(&myLoadRect);
      RGBForeColor(&myRectColor);
      MacFrameRect(&myLoadRect);
      RGBForeColor(&myOrigColor);
   }
   MacSetPort(mySavedPort);
   return(noErr);
}

We take the trouble to set the current graphics port (first saving and later restoring the current graphics port) to ensure that we're drawing into our movie's graphics port. In all likelihood, the current port when our procedure is called is indeed the movie's graphics port; but the documentation doesn't guarantee this, so it's good to be careful.

Installing a Drawing-Complete Procedure

Now we need to see how to activate and deactivate our movie drawing-complete procedure. To activate our procedure, we'll call the SetMovieDrawingCompleteProc function, passing it the movie identifier, a flag, a universal procedure pointer for our procedure, and the desired reference constant:

(**myAppData).fDrawCompleteUPP = 
   NewMovieDrawingCompleteUPP(QTAL_MovieDrawingCompleteProc);
SetMovieDrawingCompleteProc(myMovie, movieDrawingCallAlways, 
         (**myAppData).fDrawCompleteUPP, (long)theWindowObject);

The second parameter indicates how often we want our drawing-complete procedure to be called; it should be one of these two values:

enum {
   movieDrawingCallWhenChanged      = 0,
   movieDrawingCallAlways            = 1
};

The movieDrawingCallAlways flag indicates that we want QuickTime to call our procedure every time the movie is tasked (that is, every time our application calls MoviesTask, either directly or indirectly). The movieDrawingCallWhenChanged flag indicates that we want QuickTime to call our procedure only when the movie has changed (that is, something new was actually drawn into the movie's graphics world). As you can see, we use the movieDrawingCallAlways flag so that our procedure is called as often as possible, whether or not the movie image has changed.

We want to install our movie drawing-complete procedure when the movie first becomes playable, and we want to uninstall it when the movie data is finished downloading. We can do all of this inside our application-specific function QTApp_CheckMovieLoadState, as shown in Listing 11.

Listing 11: Installing and uninstalling the movie drawing-complete procedure

QTApp_CheckMovieLoadState
OSErr QTApp_CheckMovieLoadState 
            (WindowObject theWindowObject, long theLoadState, 
            long thePrevState)
{
#pragma unused(thePrevState)
   ApplicationDataHdl      myAppData = NULL;
   Movie                        myMovie = NULL;
   Rect                           myRect;
   OSErr                        myErr = noErr;
   if (theWindowObject == NULL)
      return(paramErr);
   myMovie = (**theWindowObject).fMovie;
   if (myMovie == NULL)
      return(paramErr);
   myAppData = (ApplicationDataHdl)
            (**theWindowObject).fAppData;
   if (myAppData == NULL)
      return(paramErr);
   // we don't care about the early stages
   if (theLoadState < kMovieLoadStatePlayable)
      return(noErr);
#if ALLOW_ASYNCH_LOADING
   // display a load-progress bar, until the movie is completely loaded
   if (theLoadState < kMovieLoadStateComplete) {
      if ((**myAppData).fDrawCompleteUPP == NULL) {
         (**myAppData).fDrawCompleteUPP = 
            NewMovieDrawingCompleteUPP
                        (QTAL_MovieDrawingCompleteProc);
         SetMovieDrawingCompleteProc(myMovie, 
            movieDrawingCallAlways, 
            (**myAppData).fDrawCompleteUPP, 
            (long)theWindowObject);
      }
   } else {
      if ((**myAppData).fDrawCompleteUPP != NULL) {
         // make sure the loading progress bar reaches the end
         QTAL_MovieDrawingCompleteProc(myMovie, 
            (long)theWindowObject);
         // remove the drawing-complete procedure
         SetMovieDrawingCompleteProc(myMovie, 0L, NULL, 0L);
         DisposeMovieDrawingCompleteUPP(
            (**myAppData).fDrawCompleteUPP);
         (**myAppData).fDrawCompleteUPP = NULL;
         // erase the loading progress bar, now that we are at the end
         GetMovieBox(myMovie, &myRect);
         myRect.top = myRect.bottom - kLoaderBarHeight;
#if TARGET_OS_MAC
         InvalWindowRect(
            QTFrame_GetWindowFromWindowReference(
            (**theWindowObject).fWindow), &myRect);
#endif
#if TARGET_OS_WIN32
         {
            RECT      myWinRect;
            QTFrame_ConvertMacToWinRect(&myRect, &myWinRect);
            InvalidateRect((**theWindowObject).fWindow, 
               &myWinRect, false);
         }
#endif
      }
   }
#endif
   return(myErr);
}

Once the movie data is completely downloaded, we remove the movie drawing-complete procedure and then erase the progress bar rectangle. Otherwise the progress bar would remain visible until the movie was next redrawn.

You should be aware that some of the media handlers used to play back a movie may need to use less efficient code paths when a movie drawing-complete procedure is installed. In the current case, since we are waiting for the movie's media data to download and are probably not playing the movie yet, this is less of a concern.

Loader Tracks

Now let's see how we can attach a progress bar to a movie, so that it displays its own status as the movie data is downloaded to the user's computer. The basic idea is extremely simple: we'll create a new sprite track that contains a single wired sprite. The image for this sprite is just the progress bar, shown in Figure 6.


Figure 6: The sprite image for the loader sprite

We'll set the sprite's initial position so that the right side of the loader bar is at the left edge of the movie box. Then we'll attach some wiring to the sprite that, on idle events, checks the amount of movie data currently loaded and moves the sprite to the right by the appropriate amount. Finally, when the movie data is fully downloaded, the sprite will deactivate its own track, so that the loader sprite disappears.

Creating the Sprite Track

We've had plenty of experience creating sprite tracks and adding wired actions to them, so we can be brief here. (If you need a refresher, see the QuickTime Toolkit articles from March to July, 2001.) We create the sprite track by calling NewMovieTrack and NewTrackMedia, using the dimensions of the original movie to determine the size of the sprite track:

GetMovieBox(theMovie, &myRect);
myWidth = Long2Fix(myRect.right - myRect.left);
myHeight = Long2Fix(kLoaderBarHeight);

Then we adjust the sprite track matrix so that the progress bar is drawn at the bottom of the movie box:

GetTrackMatrix(myTrack, &myMatrix);
TranslateMatrix(&myMatrix, 0, 
            Long2Fix(myRect.bottom - kLoaderBarHeight));
SetTrackMatrix(myTrack, &myMatrix);

At this point, we call an application function to add the sprite samples to the new sprite media:

myErr = QTAL_AddSpriteLoaderSamplesToMedia(myMedia, 
            myDuration, myRect.right - myRect.left);

Then we call InsertMediaIntoTrack, as usual, to add the new media samples to the track. We finish up by adjusting the sprite track properties so that the new sprite track is the frontmost track (that is, has the lowest track layer) and so that that track is loaded before any other tracks in the movie.

QTAL_SetTrackProperties(myMedia, 15);
SetTrackLayer(myTrack, kMaxLayerNumber);
SetTrackLayer(myTrack, QTAL_GetLowestLayerInMovie(theMovie) - 
            1);
myErr = QTAL_SetTrackToPreload(myTrack);

QTAL_SetTrackProperties and QTAL_GetLowestLayerInMovie are versions of functions that we've encountered previously, in the aforementioned articles. QTAL_SetTrackToPreload is a very simple function that sets the specified track to preload -- that is, to be loaded entirely into memory when the movie is opened. This by itself isn't such a big deal, as our sprite loader track will be fairly small (barely a thousand bytes) and would probably have been loaded entirely into RAM anyway. The main advantage to setting a track to preload is that FlattenMovieData places the data for any tracks marked to preload before the data for other tracks in the movie. This means that our sprite loader track will be downloaded first and hence able to display its progress bar as early as possible. Listing 12 shows our definition of QTAL_SetTrackToPreload.

Listing 12: Setting a track to preload

QTAL_SetTrackToPreload
OSErr QTAL_SetTrackToPreload (Track theTrack)
{
   TimeValue      myTime = 0L;
   TimeValue      myDuration = 0L;
   long               myFlags = 0L;
   long               myHints = 0L;
   OSErr            myErr = noErr;
   if (theTrack == NULL)
      return(invalidTrack);
   // get the current track load settings
   GetTrackLoadSettings(theTrack, &myTime, &myDuration, 
            &myFlags, &myHints);
   myErr = GetMoviesError();
   if (myErr != noErr)
      goto bail;
   myFlags = preloadAlways;
   myTime = -1;
   // set the new track load settings
   SetTrackLoadSettings(theTrack, myTime, myDuration, myFlags, 
            myHints);
   myErr = GetMoviesError();
bail:
   return(myErr);
}

The key step here is calling SetTrackLoadSettings with the myFlags parameter set to include the preloadAlways flag.

Adding the Loader Sprite Image

The QTAL_AddSpriteLoaderSamplesToMedia function performs two main tasks: (1) it adds the progress bar image to the sprite sample, and (2) it adds wiring to the loader sprite. Let's tackle the first task here.

When we've previously constructed sprite tracks, we've usually read the sprite images from an existing location (typically the application's resource fork). In this case, however, we don't know the width of the sprite image in advance, so we'll need to create it dynamically, once we know the width of the movie we are adding the loader track to. We'll adapt the existing utility ICUtils_RecompressPictureWithTransparency to fit our needs; the resulting function, QTAL_AddLoaderBarPICTToKeyFrameSample, is shown in Listing 13. Parts of this function will remind you of QTAL_MovieDrawingCompleteProc (Listing 10).

Listing 13: Adding the image for the loader bar sprite

QTAL_AddLoaderBarPICTToKeyFrameSample
OSErr QTAL_AddLoaderBarPICTToKeyFrameSample 
            (QTAtomContainer theKeySample, long theBarWidth, 
            RGBColor *theKeyColor, QTAtomID theID, 
            FixedPoint *theRegistrationPoint, 
            StringPtr theImageName)
{
   Rect                     myRect;
   RGBColor               myOrigColor;
   RGBColor               myLoadColor = {0x6666, 0x6666, 0xcccc};
   RGBColor               myRectColor = {0xeeee, 0xeeee, 0xeeee};
   PicHandle            myPicture = NULL;
   Handle                  myCompressedPicture = NULL;
   ImageDescriptionHandle   
                           myImageDesc = NULL;
   OSErr                  myErr = noErr;
   // set up the PICT rectangle
   myRect.top = 0;
   myRect.left = 0;
   myRect.right = theBarWidth;
   myRect.bottom = kLoaderBarHeight;
   // create the loader bar PICT
   myPicture = OpenPicture(&myRect);
   if (myPicture != NULL) {
      GetForeColor(&myOrigColor);   
      RGBForeColor(&myLoadColor);
      PaintRect(&myRect);
      RGBForeColor(&myRectColor);
      MacFrameRect(&myRect);
      RGBForeColor(&myOrigColor);
      ClosePicture();
      // convert it to image data compressed by the animation compressor
      myErr = 
            ICUtils_RecompressPictureWithTransparency(myPicture, 
            theKeyColor, NULL, &myImageDesc, 
            &myCompressedPicture);
      if (myErr != noErr)
         goto bail;
      // add it to the key sample
      HLock(myCompressedPicture);
      myErr = SpriteUtils_AddCompressedImageToKeyFrameSample(
            theKeySample, myImageDesc, 
            GetHandleSize(myCompressedPicture), 
            *myCompressedPicture, theID, theRegistrationPoint, 
            theImageName);
   }
bail:
   if (myPicture != NULL)
      KillPicture(myPicture);
   if (myCompressedPicture != NULL)
      DisposeHandle(myCompressedPicture);
   if (myImageDesc != NULL)
      DisposeHandle((Handle)myImageDesc);
   return(myErr);
}

Wiring the Loader Sprite

All that remains is to add the appropriate wiring to the sprite, to cause it to move gradually to the right as the media data is downloaded; we also need to disable the sprite track once the media data is fully downloaded. In pseudo-code, our wiring will look like this:

if kOperandMaxLoadedTimeInMovie < kOperandMovieDuration
   TranslateSprite xPos, 0, true
else
   EnableTrack false

Here, the horizontal position of the sprite (or xPos, in the pseudo-code) is calculated thus:

xPos = (kOperandMaxLoadedTimeInMovie / myDurPerPixel) - theWidth)

where myDurPerPixel is simply the movie duration divided by the movie width. Listing 14 shows the portion of QTAL_AddSpriteLoaderSamplesToMedia that constructs the wired action atom.

Listing 14: Adding actions to the loader bar sprite

QTAL_AddSpriteLoaderSamplesToMedia
WiredUtils_AddQTEventAndActionAtoms(mySpriteData, 
            kParentAtomIsContainer, kQTEventIdle, kActionCase, 
            &myActionAtom);
if (myActionAtom != 0) {
   QTAtom         myParamAtom = 0;
   QTAtom         myConditionalAtom = 0;
   QTAtom         myExpressionAtom = 0;
   // add a parameter atom to the kActionCase action atom; this will serve as a parent to hold the 
   expression and action atoms
   
   WiredUtils_AddActionParameterAtom(mySpriteData, 
            myActionAtom, kFirstParam, 0, NULL, &myParamAtom);
   if (myParamAtom != 0) {
      // if ...
      WiredUtils_AddConditionalAtom(mySpriteData, myParamAtom, 
            1, &myConditionalAtom);
      if (myConditionalAtom != 0) {
         WiredUtils_AddExpressionContainerAtomType(mySpriteData, 
            myConditionalAtom, &myExpressionAtom);
         if (myExpressionAtom != 0) {
            QTAtom      myOperatorAtom = 0;
            // ... kOperandMaxLoadedTimeInMovie < kOperandMovieDuration
            myErr = WiredUtils_AddOperatorAtom(mySpriteData, 
                  myExpressionAtom, kOperatorLessThan, 
                  &myOperatorAtom);
            if (myOperatorAtom != 0) {
               WiredUtils_AddOperandAtom(mySpriteData, 
                  myOperatorAtom, kOperandMaxLoadedTimeInMovie, 1, 
                  NULL, 0);
               WiredUtils_AddOperandAtom(mySpriteData, 
                  myOperatorAtom, kOperandMovieDuration, 2, NULL, 
                  0);
            }
         }
         //       TranslateSprite ...
         WiredUtils_AddActionListAtom(mySpriteData, 
            myConditionalAtom, &myActionListAtom);
         if (myActionListAtom != 0) {
            WiredUtils_AddActionAtom(mySpriteData, 
                  myActionListAtom, kActionSpriteTranslate, 
                  &myNewActionAtom);
            if (myNewActionAtom != 0) {
               QTAtom   myNewParamAtom = 0;
               long   myDurPerPixel = theDuration / theWidth;
               // first parameter: (kOperandMaxLoadedTimeInMovie / myDurPerPixel) 
               // - theWidth
               WiredUtils_AddActionParameterAtom(mySpriteData, 
                  myNewActionAtom, kFirstParam, 0, NULL, 
                  &myNewParamAtom);
               if (myNewParamAtom != 0) {
                  QTAtom   myExpressionAtomSub = 0;
                  QTAtom   myExpressionAtomMin = 0;
                  QTAtom   myOperatorAtomSub = 0;
                  QTAtom   myOperatorAtomDiv = 0;
                  QTAtom   myOperandAtom = 0;
                  QTAtom   myNewOperandAtom = 0;
                  WiredUtils_AddExpressionContainerAtomType
                  (mySpriteData, myNewParamAtom, 
                  &myExpressionAtomSub);
                  if (myExpressionAtomSub != 0) {
                     WiredUtils_AddOperatorAtom(mySpriteData, 
                        myExpressionAtomSub, kOperatorSubtract, 
                        &myOperatorAtomSub);
                     if (myOperatorAtomSub != 0) {
                        // the minuend
                        QTInsertChild(mySpriteData, 
                        myOperatorAtomSub, kOperandAtomType, 1, 1, 
                        0, NULL, &myOperandAtom);
                        if (myOperandAtom != 0) {
                        QTInsertChild(mySpriteData, myOperandAtom, 
                              kOperandExpression, 1, 1, 0, NULL, 
                              &myNewOperandAtom);
                           WiredUtils_AddExpressionContainerAtomType
                              (mySpriteData, myNewOperandAtom, 
                              &myExpressionAtomMin);
                           if (myExpressionAtomMin != 0) {
                           WiredUtils_AddOperatorAtom(mySpriteData, 
                              myExpressionAtomMin, kOperatorDivide, 
                              &myOperatorAtomDiv);
                              if (myOperatorAtomDiv != 0) {
                                 WiredUtils_AddOperandAtom
                                    (mySpriteData, myOperatorAtomDiv, 
                                    kOperandMaxLoadedTimeInMovie, 1, 
                                    NULL, 0);
                                 WiredUtils_AddOperandAtom
                                    (mySpriteData, myOperatorAtomDiv, 
                                    kOperandConstant, 2, NULL, 
                                    (float)myDurPerPixel);
                                 }
                              }
                           }
                           // the subtrahend
                           WiredUtils_AddOperandAtom(mySpriteData, 
                              myOperatorAtomSub, kOperandConstant, 2, 
                              NULL, (float)theWidth);
                        }
                     }
                  }
               // second parameter: 0
               myFixed = EndianU32_NtoB(0);
               WiredUtils_AddActionParameterAtom(mySpriteData, 
                     myNewActionAtom, kSecondParam, sizeof(Fixed), 
                     &myFixed, NULL);
               // third parameter: true
               myBoolean = true;
               WiredUtils_AddActionParameterAtom(mySpriteData, 
                  myNewActionAtom, kThirdParam, sizeof(myBoolean), 
                  &myBoolean, NULL);
            }
         }
      }
      // else if ...
      WiredUtils_AddConditionalAtom(mySpriteData, myParamAtom, 
            2, &myConditionalAtom);
      if (myConditionalAtom != 0) {
         // ... (1)
         WiredUtils_AddExpressionContainerAtomType(mySpriteData, myConditionalAtom, 
         &myExpressionAtom);
         
            if (myExpressionAtom != 0)
               WiredUtils_AddOperandAtom(mySpriteData, myExpressionAtom, kOperandConstant, 1, NULL, 
               1.0);
               
         //      kActionTrackSetEnabled false
         WiredUtils_AddActionListAtom(mySpriteData, 
               myConditionalAtom, &myActionListAtom);
         if (myActionListAtom != 0)
            WiredUtils_AddTrackSetEnabledAction(mySpriteData, 
               myActionListAtom, 0, 0, NULL, 0, false);
      }
   }
}

QuickTime VR Movie Loading

Before we close, let's take a quick look at one additional topic related to loading movies, how to specify a preview track in a QuickTime VR panoramic movie. As you know (at least if you read "Virtuosity" in MacTech, June 2002), the image data for a panorama is contained in a panorama image track inside a QuickTime VR movie file. Each sample in the panorama image track represents one section, or tile, of the image data. For cylindrical panoramas, a tile is a vertical slice of the image. For cubic panoramas, a tile is usually an entire face of the cube. Chopping the panorama image data into tiles allows QuickTime VR to display parts of the panorama to the user without having the entire image data in memory.

This is relevant to us now because QuickTime downloads a panoramic movie one tile at a time. This means that, on suitably slow network connections and with suitably narrow tiles, the panorama image data that's been downloaded at some point might fill only part of the movie window. By default, QuickTime fills the remainder of the movie window with a black and gray grid pattern, as shown in Figure 7. (This pattern is sometimes called the "holodeck" pattern, after a similar grid effect seen in some Star Trek episodes.) As new tiles are downloaded, they overlay the grid.


Figure 7: The grid pattern for unloaded tiles

It's possible to achieve a better user experience by including a low-resolution image track in the panorama. This is a video track that (typically) shows the same location as the full resolution track but which occupies a small fraction of the space of the full resolution track. The low-resolution image track is loaded fairly quickly and is hence often called a low-resolution preview track (or just preview track). As the high-resolution tiles arrive, they are drawn on top of the low-resolution track, in just the same way that the high-resolution tiles are drawn on top of the grid. Figure 8 shows a low-resolution image track (on the left) and the high-resolution tiles (on the right).


Figure 8: A low-resolution image track and some

high-resolution tiles

Hot spots in the panorama will be active under the low-resolution track but inactive under the grid pattern. For this reason at least, it's usually best to include a low-resolution image track in any panoramas we create. Most QuickTime VR authoring tools provide some means of attaching these low-resolution tracks to a movie, but it's easy enough to do it ourselves. If we've got a panorama that does not contain a low-resolution image track, we can add one to it by exporting the movie using the QuickTime VR flattener. This is a movie export component that prepares a QuickTime VR movie for Fast Start downloading and provides the option of including a low-resolution image track. Listing 15 shows our definition of the QTVRUtils_FlattenMovieForStreaming function, which exports the specified movie into a new file. (See "In and Out" in MacTech, May 2000 for a more extensive discussion of movie exporting.)

Listing 15: Flattening a QuickTime VR movie

QTVRUtils_FlattenMovieForStreaming
OSErr QTVRUtils_FlattenMovieForStreaming (Movie theMovie, 
            FSSpecPtr theFSSpecPtr)
{
   ComponentDescription      myCompDesc;
   MovieExportComponent      myExporter = NULL;
   long                              myFlags = 
            createMovieFileDeleteCurFile | showUserSettingsDialog 
            | movieFileSpecValid;
   ComponentResult               myErr = badComponentType;
   // find and open a movie exporter that can flatten a QuickTime VR movie file
   myCompDesc.componentType = MovieExportType;
   myCompDesc.componentSubType = MovieFileType;
   myCompDesc.componentManufacturer = 
            kQTVRFlattenerManufacturer;
   myCompDesc.componentFlags = 0;
   myCompDesc.componentFlagsMask = 0;
   myExporter = OpenComponent(
            FindNextComponent(NULL, &myCompDesc));
   if (myExporter == NULL)
      goto bail;
   // use the default progress procedure
   SetMovieProgressProc(theMovie, (MovieProgressUPP)-1L, 0);
   // export the movie into a file
   myErr = ConvertMovieToFile(   theMovie, NULL, theFSSpecPtr,
                  MovieFileType, sigMoviePlayer, smSystemScript, 
                  NULL, myFlags, myExporter);
bail:
   // close the movie export component
   if (myExporter != NULL)
      CloseComponent(myExporter);
   return((OSErr)myErr);
}

Since we include the showUserSettingsDialog flag in the myFlags parameter passed to ConvertMovieToFile, the user will be presented with the export settings dialog box, shown in Figure 9.


Figure 9: The export settings dialog box

If the user clicks the Options button, the dialog box shown in Figure 10 will be presented, allowing the user to determine the resolution of the preview track and whether it is blurred or pixilated. If the user unchecks the "Create Preview" button, no preview track is created.


Figure 10: The Options dialog box

Conclusion

The theme of this article was loading QuickTime movies. We've learned how to modify our basic QuickTime movie playback application to support loading movies asynchronously. The changes required here are indeed fairly simple, but they pay big dividends. First and foremost, our application can now continue processing while a movie is being loaded. Also, we've now got the machinery in place to do application-specific processing while a movie loads, such as displaying a progress bar showing how much of the movie is loaded.

We've also taken a look at a couple of ways to enhance a movie's own loading behavior. We saw how to add a sprite track that displays a loader bar, and we saw how to add a low-resolution preview track to a QuickTime VR panorama.

Credits

The technique for adding the sprite loader bar to a QuickTime movie is based on some wiring developed by Bill Meikle and Gary Alexander.


Tim Monroe in a member of the QuickTime engineering team. You can contact him at monroe@apple.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Aether Gazer unveils Chapter 16 of its m...
After a bit of maintenance, Aether Gazer has released Chapter 16 of its main storyline, titled Night Parade of the Beasts. This big update brings a new character, a special outfit, some special limited-time events, and, of course, an engaging... | Read more »
Challenge those pesky wyverns to a dance...
After recently having you do battle against your foes by wildly flailing Hello Kitty and friends at them, GungHo Online has whipped out another surprising collaboration for Puzzle & Dragons. It is now time to beat your opponents by cha-cha... | Read more »
Pack a magnifying glass and practice you...
Somehow it has already been a year since Torchlight: Infinite launched, and XD Games is celebrating by blending in what sounds like a truly fantastic new update. Fans of Cthulhu rejoice, as Whispering Mist brings some horror elements, and tests... | Read more »
Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »

Price Scanner via MacPrices.net

Free iPhone 15 plus Unlimited service for $60...
Boost Infinite, part of MVNO Boost Mobile using AT&T and T-Mobile’s networks, is offering a free 128GB iPhone 15 for $60 per month including their Unlimited service plan (30GB of premium data).... Read more
$300 off any new iPhone with service at Red P...
Red Pocket Mobile has new Apple iPhones on sale for $300 off MSRP when you switch and open up a new line of service. Red Pocket Mobile is a nationwide MVNO using all the major wireless carrier... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, available for $759 for 8-Core CPU/7-Core GPU/256GB models and $929 for 8-Core CPU/8-Core GPU/512GB models. Apple’s one-year warranty is... Read more
Updated Apple MacBook Price Trackers
Our Apple award-winning MacBook Price Trackers are continually updated with the latest information on prices, bundles, and availability for 16″ and 14″ MacBook Pros along with 13″ and 15″ MacBook... Read more
Every model of Apple’s 13-inch M3 MacBook Air...
Best Buy has Apple 13″ MacBook Airs with M3 CPUs in stock and on sale today for $100 off MSRP. Prices start at $999. Their prices are the lowest currently available for new 13″ M3 MacBook Airs among... Read more
Sunday Sale: Apple iPad Magic Keyboards for 1...
Walmart has Apple Magic Keyboards for 12.9″ iPad Pros, in Black, on sale for $150 off MSRP on their online store. Sale price for online orders only, in-store price may vary. Order online and choose... Read more
Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more

Jobs Board

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
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
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
IT Systems Engineer ( *Apple* Platforms) - S...
IT Systems Engineer ( Apple Platforms) at SpaceX Hawthorne, CA SpaceX was founded under the belief that a future where humanity is out exploring the stars is Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.