Mar 00 QTToolkit
Volume Number: 16 (2000)
Issue Number: 3
Column Tag: QuickTime Toolkit
Opening the Toolbox
by Tim Monroe
Controlling QuickTime Movies With the Movie Toolbox
Introduction
In the previous two QuickTime Toolkit articles, we've focused almost exclusively on playing QuickTime movies using movie controllers. This is by far the easiest way to manage movies and to support the standard types of user interaction with movies. Indeed, some types of QuickTime movies cannot be presented to or managed by the user without a movie controller. But there are other types of QuickTime movies (most notably, standard linear movies) that can be played back without using a movie controller. In this article, we'll see how to accomplish this. In effect, our goal will be to provide all the capabilities that are provided by our basic application QTShell - for linear movies at least - but without the assistance of movie controllers. We'll show how to start and stop movies, provide basic editing services, and set various looping states.
Why might we want to do this? Well, there are situations where you want to play QuickTime movies but don't necessarily want the user to be able to manipulate those movies in the ways typically afforded by movie controllers. Perhaps the simplest example of this is found quite often in games, where you might play what's called a cut movie or a cut scene when the user completes one level and advances to the next. The cut movie serves as a transition from one level to the next, and you generally don't want the user interrupting or (heaven forbid!) editing your cut movie. So using the full-fledged movie controller interface is definitely not the best way to handle QuickTime movie playback here.
There are also more complicated examples of using controller-less QuickTime movies. For instance, you might want to use a linear QuickTime movie as a texture for a 3D object that's being displayed by your application. Figure 1 shows a window of the application TextureEyes, which uses QuickDraw 3D to define and render three-dimensional objects. In this window, we've created a cube and set its surface texture to be a QuickTime movie.
Figure 1.A QuickTime movie used as a texture on a 3D object.
Similarly, we might want to embed a QuickTime movie within another QuickTime movie. For instance, QuickTime VR allows us to draw into a panoramic node. We can use this capability to draw the individual frames of a QuickTime movie into a panorama, as shown in Figure 2. Here, the toy train on the table isn't part of the original panorama. Rather, we're drawing the frames of a standard QuickTime movie (one of those frames in shown in Figure 3) into the panorama, using the QuickTime VR application programming interfaces and some QuickDraw routines to drop out the black background. The net effect is that the train appears to travel in a circle on the table, thereby imparting some motion and sound to the otherwise static and silent panorama.
Figure 2.A QuickTime movie embedded into a QuickTime VR panorama.
Figure 3.The QuickTime movie embedded in Figure 2.
We might also want to embed a linear QuickTime movie into another linear QuickTime movie, as shown in Figure 4.
Figure 4.A QuickTime movie embedded into a QuickTime movie.
The point is not that we couldn't have used movie controllers to help us display and manage the cut scenes or the textured or embedded QuickTime movies in these sorts of cases. Rather, the point is that we don't need to do so. In the examples we've mentioned, we just need a way to manage the display of the movie frames at the appropriate time (and to play back the movie's sound track, if it has one). So in those cases the only services we really need are QuickTime's imaging, sound producing, and timing services. We don't need the robust user-interaction services provided by the standard linear movie controller.
It's really a question of levels: the movie controller interface for controlling movies is a higher-level interface than what we need to accomplish the tasks outlined above. Now we'll drop down a level and directly use the services of that part of QuickTime known as the Movie Toolbox. The Movie Toolbox is really the heart and soul of QuickTime. It provides services for opening movie files and extracting movies from them, playing movies, editing movies, creating movies, and much more. Indeed, the original Inside Macintosh documentation for the Movie Toolbox (that is, circa 1993) ran to well over 400 printed pages. To a large degree, programming with QuickTime is using the Movie Toolbox.
So here's what we want to accomplish in this article: we want to modify our basic application QTShell so that it can open and play back linear QuickTime movies without using movie controllers. Let's call this new application QTMooVToolbox, in honor of the fact that we'll be doing everything using the Movie Toolbox. Since we're not using a movie controller, there will be no movie controller bar in our movie windows. A typical movie window will therefore look like the one in Figure 5.
Figure 5.A movie window displayed by QTMooVToolbox.
As indicated above, we want to support the standard movie editing capabilities, so our application's Edit menu will be unchanged (though of course the code providing the editing capabilities will change quite a bit). We also want to provide the user a way to start and stop the movie and to set normal and palindrome looping. Figure 6 shows the Test menu that we want to support.
Figure 6.The Test menu for QTMooVToolbox.
The last menu item here ("Add Picture In Picture...") is particularly interesting. It allows us to embed one linear QuickTime movie inside of another linear QuickTime movie (as shown in Figure 4). As we'll see, it's fairly easy to use the Movie Toolbox to accomplish this.
Getting Started
Setting Up to Use QuickTime
Before moving forward with our QTMooVToolbox application, we need to back up a step or two to fill in a few details that we skipped over in the first two articles. In order to use any of the QuickTime services, we need to make sure that the QuickTime client software is available on the target machine. On Windows computers, we can do this by trying to initialize the QuickTime Media Layer, like this:
myErr = InitializeQTML(0L);
If InitializeQTML completes successfully (that is, if myErr is noErr), then we know that QuickTime is available. On the other hand, if InitializeQTML doesn't complete successfully, then either QuickTime or some other essential component is missing, in which case we will just exit the application (perhaps after informing the user that QuickTime needs to be installed).
On Macintosh computers, we can call the Gestalt function to determine whether QuickTime is available, as shown in Listing 1:
Listing 1: Determining whether QuickTime is available.
QTUtils_IsQuickTimeInstalled
Boolean QTUtils_IsQuickTimeInstalled (void)
{
long myAttrs;
OSErr myErr = noErr;
myErr = Gestalt(gestaltQuickTime, &myAttrs);
return(myErr == noErr);
}
On either Windows or Macintosh computers, once we've determined that QuickTime is indeed available in the current operating environment, we need to call the EnterMovies function, like so:
myErr = EnterMovies();
EnterMovies initializes the Movie Toolbox, performing any necessary set-up for our application (such as allocating any private storage it might need to use on our behalf). As with InitializeQTML, we will check the value returned by EnterMovies and exit the application if an error occurs.
When we are finished using QuickTime services - usually, when our application is about to terminate - we should undo the work done by EnterMovies and (on Windows) InitializeQTML. We can call the ExitMovies function to terminate our connection with the Movie Toolbox. On Windows, we also need to call TerminateQTML to unregister our application from the QuickTime Media Layer.
Using Application-Specific Data
Suppose now that our application is up and running, that we've successfully initialized QuickTime and the Movie Toolbox, and that the user has decided to open a movie file (perhaps using the Open command in the File menu). Our basic cross-platform framework calls the QTFrame_OpenMovieInWindow function to prompt the user for the movie file to open, create a window object record to hold basic information about the movie and the file it is contained in, open a new window on the screen, adjust the size of that window to fit the movie's natural size, and do some other useful set-up before the user can begin to interact with the movie.
QTFrame_OpenMovieInWindow also calls the function QTApp_SetupWindowObject, defined in the file ComApplication.c, to allow our application to perform any application-specific actions on the new movie, before it is displayed to the user. Both of our previous applications (namely, QTShell and QTController) have not put any code at all into QTApp_SetupWindowObject because the default configuration was sufficient for our purposes then. Here, for QTMooVToolbox, we'll need to change that, for two reasons. First, we need to maintain some information for each open movie window beyond what is contained in the window object record. Second, since we are not using movie controllers in the QTMooVToolbox application, we need to do some additional work to prepare our movies for playback. Let's tackle the first task here and defer the second until a bit later, in "Preparing Movies For Playback".
We already have the apparatus in place to support adding application-specific data to our movie windows. As you may recall, the window object record includes the field fAppData, which is declared to be of type Handle. We put this field there precisely to provide ourselves a place to store any information that is specific to a particular application (since we didn't want to be changing the framework files all the time). For QTMooVToolbox, we'll define a structure of type ApplicationDataRecord that contains three fields:
typedef struct ApplicationDataRecord {
MovieEditState fMovieEditState; // the edit state of the movie
Movie fPIPMovie; // the picture-in-picture movie
Rect fPIPRect; // the picture-in-picture rectangle
} ApplicationDataRecord, *ApplicationDataPtr, **ApplicationDataHdl;
We don't need to explain these fields quite yet (though the comments should give you a good enough idea of what they are for). But we do need to show how the application data record gets created and attached to the window object. We do this in the function QTApp_SetupWindowObject defined in Listing 2.
Listing 2: Setting up a window object.
QTApp_SetupWindowObject
void QTApp_SetupWindowObject (WindowObject theWindowObject)
{
Movie myMovie = NULL;
ApplicationDataHdl myAppData = NULL;
OSErr myErr = noErr;
if (theWindowObject != NULL) {
// get rid of the movie controller that's already been attached to this window object;
// we do this to illustrate how to manage movies without using a movie controller
if ((**theWindowObject).fController != NULL) {
MCSetActionFilterWithRefCon((**theWindowObject).fController, NULL, 0L);
DisposeMovieController((**theWindowObject).fController);
(**theWindowObject).fController = NULL;
}
// allocate an application-specific data record
myAppData = (ApplicationDataHdl)NewHandleClear
(sizeof(ApplicationDataRecord));
(**theWindowObject).fAppData = (Handle)myAppData;
if (myAppData != NULL) {
(**myAppData).fMovieEditState = NULL;
(**myAppData).fPIPMovie = NULL;
MacSetRect(&(**myAppData).fPIPRect, 0, 0, 0, 0);
}
// some lines omitted here
}
}
Notice that the first thing we do in QTApp_SetupWindowObject is get rid of the movie controller that has already been created by the time QTApp_SetupWindowObject is called. That's because (as you already know) we want to illustrate how to manage movies without using a movie controller. We might as well just dispose of the controller, so we're sure it's not doing any work for us.
The second thing we do in QTApp_SetupWindowObject is call NewHandleClear to allocate an application data record and initialize its fields. (Of course, the initialization here is overkill, since NewHandleClear zeros the bytes in the newly-allocated block; nonetheless, it's useful to make this initialization explicit.) Then we set the fAppData field of the window object passed to QTApp_SetupWindowObject to the newly-allocated handle. This allows us, as we intended, to keep a copy of this record for each movie window.
The file ComFramework.c defines a couple of functions that facilitate retrieving the application-specific data attached to a movie. For instance, we can use the function QTFrame_GetAppDataFromWindowObject, defined in Listing 3, to retrieve the application-specific data associated with a window object.
Listing 3: Getting the application-specific data attached to a window object.
QTFrame_GetAppDataFromWindowObject
Handle QTFrame_GetAppDataFromWindowObject (WindowObject theWindowObject)
{
// make sure this is a window object belonging to our application
if (!QTFrame_IsWindowObjectOurs(theWindowObject))
return(NULL);
return((**theWindowObject).fAppData);
}
Similarly, we can use the function QTFrame_GetAppDataFromWindow, defined in Listing 4, to retrieve the application-specific data attached to a particular movie window.
Listing 4: Getting the application-specific data attached to a movie window.
QTFrame_GetAppDataFromWindow
Handle QTFrame_GetAppDataFromWindow (WindowReference theWindow)
{
WindowObject myWindowObject = NULL;
myWindowObject = QTFrame_GetWindowObjectFromWindow(theWindow);
if (myWindowObject == NULL)
return(NULL);
return(QTFrame_GetAppDataFromWindowObject(myWindowObject));
}
We'll use these functions extensively throughout the rest of this article.
Updating the Application Framework
The basic framework for playing QuickTime movies that we've used for the QTShell and QTController sample applications makes extensive use of movie controller components to manage the movie playback and any user interaction with the movie. It uses movie controllers to help set the window size, handle movie editing, process movie-related events, and a handful of other important tasks. So, I must admit that I was more than a little worried when I set about to develop the QTMooVToolbox application for this article. After all, the very first application-specific thing we do (in Listing 2) is throw away the movie controller that the framework so kindly created and configured for us. How much of the entire framework was dependent upon work done by that movie controller? Or, asked another way, how much of the basic QTShell framework would we need to rewrite in order to play back movies using only the Movie Toolbox?
Surprisingly, very little had to change. In fact, only three functions needed reworking to handle the new assumption that the movie window might no longer have a movie controller associated with it. First of all, the QTFrame_SizeWindowToMovie function originally contained these lines:
myMC = (**theWindowObject).fController;
myMovie = (**theWindowObject).fMovie;
if (MCGetVisible(myMC))
MCGetControllerBoundsRect(myMC, &myMovieBounds);
else
GetMovieBox(myMovie, &myMovieBounds);
This just says: if the movie has a visible movie controller bar, then get the desired window size by calling MCGetControllerBoundsRect (which returns the rectangle that encloses the movie and the movie controller bar); otherwise, call GetMovieBox (which returns the rectangle that encloses just the movie). To allow for the case where there is no movie controller at all, we can rewrite the key lines like this:
GetMovieBox(myMovie, &myMovieBounds);
if (myMC != NULL)
if (MCGetVisible(myMC))
MCGetControllerBoundsRect(myMC, &myMovieBounds);
Second, the framework function QTFrame_HandleEditMenuItem also assumed that every movie window had a movie controller associated with it, in which case it could use movie controller functions like MCCut and MCCopy to handle the Edit menu items. In QTMooVToolbox, this is no longer true, so we need to provide a way for an application to intercept the Edit menu commands and do its own type of movie editing. We can do this very simply by inserting a call to QTApp_HandleMenu before QTFrame_HandleEditMenuItem handles the menu items. If QTApp_HandleMenu returns the value true to indicate that it has handled the menu item, then we just skip the rest of QTFrame_HandleEditMenuItem. We'll see later, in "Editing Movies", how our application actually handles the Edit menu items; for the moment, at least we have successfully provided a way to prevent the framework from trying to handle them using movie controller functions.
The last important change we need to make to our basic framework is to provide a way for both our Macintosh and Windows applications to tell the Movie Toolbox to play any movies that are open. When we're using QuickTime, we don't just start a movie playing and then forget about it, expecting perhaps that it will play through until the end and then stop. Instead, from time to time we need to explicitly allocate some processor time to the Movie Toolbox so that it can do whatever it needs to do in order to keep the movie playing. This periodic allocation of processor time to the Movie Toolbox is known as tasking a movie, and we can accomplish it by calling the MoviesTask function over and over until the movie is done playing. So what we need to do now is provide a way for QTMooVToolbox to call MoviesTask periodically. (When we are using movie controllers, we don't need to ever call MoviesTask, because the MCIsPlayerEvent function calls it internally for us.)
A standard way to do this, on the Macintosh, is to insert a call to MoviesTask in the application's main event loop. For several reasons (which we don't need to go into), we'll adopt a slightly different technique in QTMooVToolbox; we'll call MoviesTask in response to null events (which occur often enough to keep the movie playing smoothly). For Windows applications, however, this is slightly trickier, since there is no Windows equivalent of the Macintosh null or idle event (and there is no main event loop). Probably the best solution for Windows applications would be to install a timer that calls MoviesTask periodically. For the moment, however, we'll adopt a different (and much simpler) approach. Our basic Windows framework already calls the function QTApp_HandleEvent every time a movie window receives a message to process. So we'll just look for null events there and call QTApp_Idle (which in turn calls MoviesTask) for every open movie window. Listing 5 shows the changes we can make to QTApp_HandleEvent to keep our Windows movies running smoothly.
Listing 5: Getting Windows applications to call QTApp_Idle.
QTApp_HandleEvent
Boolean QTApp_HandleEvent (EventRecord *theEvent)
{
Boolean myIsHandled = false;
#if TARGET_OS_WIN32
WindowReference myWindow = NULL;
if (theEvent != NULL)
if (theEvent->what == nullEvent) {
myWindow = QTFrame_GetFrontMovieWindow();
while (myWindow != NULL) {
QTApp_Idle(myWindow);
myWindow = QTFrame_GetNextMovieWindow(myWindow);
}
}
#endif
return(myIsHandled);
}
Keep in mind that these strategies might not work for more complicated or more specialized applications. On the Macintosh, for instance, an application might be doing so much processing that there are not enough null events generated to keep the movie playing smoothly. Conversely, our Windows strategy might consume too much processor time with its almost constant calls to QTApp_Idle. So QTMooVToolbox most certainly does not provide a universal solution to the problem of when and how to call MoviesTask. The strategies that we've adopted here are designed only to keep the movies running smoothly, with a minimum of changes to our basic QuickTime framework.
Preparing Movies For Playback
So far, our application can open QuickTime movie files, read the movies from those files, and create appropriately-sized windows to hold those movies. Are we finally ready to begin playing the movies? In other words, when the user indicates that he or she wants the movie to begin playing, can we just start the ball rolling by calling the StartMovie function and then let our framework task the movie periodically?
The answer here is: yes and no. We could indeed just call StartMovie, and most movies would start playing pretty much immediately, as desired. Other movies might require a small but noticeable amount of time to perform some necessary set-up activities, such as allocating buffers to hold data from the movie file and setting up various QuickTime components to handle the process of decompressing the data in the file. This set-up is unavoidable; however, if our application doesn't start the movie playing immediately, we can perform this set-up right after the user opens the movie and before the user actually starts playing the movie, so that it is effectively unnoticeable. This process is called prerolling the movie.
For some movies - in particular, for streamed movies located remotely on the web or on file servers - prerolling alone is not sufficient to guarantee an optimal user experience. That's because some actions need to occur even before the movie can successfully be prerolled. QuickTime needs to set up connections to the remote servers and negotiate one or more protocols for exchanging data; it also needs to get information about the types of data in those remote movies. This process is called preprerolling the movie (because it needs to occur prior to the prerolling process). Unless we prepreroll movies stored remotely, the movie playback would be annoyingly choppy and some of the data in those movies (for instance, the sound) might not play correctly at all.
So we need a more general method of starting up movies that will accommodate both local and remote QuickTime movies. We need to prepreroll and preroll our movies. Let's consider these operations in reverse order.
Prerolling Movies
To preroll a movie, we can use the Movie Toolbox function PrerollMovie. As indicated above, PrerollMovie does whatever it can to prepare the movie for immediate playback once the movie is started. This involves determining what kinds of data are in the movie and setting up the appropriate software components to handle those kinds of data. Prerolling might also involve actually reading some of the movie's data and getting the relevant decompressors ready to decompress that data. Because of this, PrerollMovie needs to know the current location in the movie and the rate at which the movie is to be played. We can get the current location in the movie by calling GetMovieTime, and we can get the movie's default playback rate by calling GetMoviePreferredRate. So we can preroll a movie by executing these lines of code:
myTimeValue = GetMovieTime(myMovie, NULL);
myPlayRate = GetMoviePreferredRate(myMovie);
PrerollMovie(myMovie, myTimeValue, myPlayRate);
If for some reason you want to change the current location in the movie or the movie's default playback rate, you should do it before calling PrerollMovie so that you don't invalidate the work done by PrerollMovie. You can set the current location in the movie by calling SetMovieTime, and you can set the default playback rate by calling SetMoviePreferredRate. The default playback rate is the rate that StartMovie uses when it starts a movie playing. When you open a movie from a movie file, the actual playback rate is 0 (that is, the movie is not playing).
Preprerolling Movies
To prepreroll a movie, we can use the Movie Toolbox function PrePrerollMovie, which establishes any required network connections between the computer running our application and the computer that contains the movie being preprerolled. If the movie resides on the same computer as the application (that is, if the movie is a local movie), then PrePrerollMovie has nothing to do and it returns immediately. As a result, there is no harm in calling PrePrerollMovie for every movie our application opens.
On the other hand, if the movie being preprerolled is not a local movie, then preprerolling it can take a significant amount of time, particularly if the local computer needs to access the remote computer over a dial-up line. To keep your application from stalling while QuickTime negotiates a connection with the remote computer, you can call PrePrerollMovie asynchronously, in which case PrePrerollMovie returns almost immediately and then executes an application-defined completion function when the preprerolling is finished. PrePrerollMovie has the same calling interface as PrerollMovie, except that you must also specify a movie preprerolling completion routine and an optional reference constant that is passed to the completion routine:
myPlayRate = GetMoviePreferredRate(myMovie);
PrePrerollMovie(myMovie, 0, myPlayRate, gMoviePPRollCompleteProc, (void *)theWindowObject);
Here, gMoviePPRollCompleteProc is a universal procedure pointer (UPP) to the movie preprerolling completion routine. QTMooVToolbox allocates this UPP when it starts up, in the function QTMT_Init defined in Listing 6.
Listing 6: Allocating a routine descriptor for QTMT_MoviePrePrerollCompleteProc.
QTMT_Init
void QTMT_Init (void)
{
// allocate a routine descriptor for our prepreroll completion routine
gMoviePPRollCompleteProc = NewMoviePrePrerollCompleteProc(QTMT_MoviePrePrerollCompleteProc);
}
When the preprerolling is complete, the Movie Toolbox executes our movie preprerolling completion function, defined in Listing 7.
Listing 7: Responding when a movie is done preprerolling.
QTMT_MoviePrePrerollCompleteProc
PASCAL_RTN void QTMT_MoviePrePrerollCompleteProc (Movie theMovie, OSErr thePrerollErr, void *theRefCon)
{
#pragma unused(thePrerollErr)
WindowObject myWindowObject = (WindowObject)theRefCon;
ApplicationDataHdl myAppData = NULL;
TimeValue myTimeValue;
Fixed myPlayRate;
myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject (myWindowObject);
if ((theMovie == NULL) || (myWindowObject == NULL) || (myAppData == NULL))
return;
// preroll the movie, so that it's ready to play when we call StartMovie
myTimeValue = GetMovieTime(theMovie, NULL);
myPlayRate = GetMoviePreferredRate(theMovie);
PrerollMovie(theMovie, myTimeValue, myPlayRate);
// for the picture-in-picture movie, start it playing in a loop
if (theMovie == (**myAppData).fPIPMovie) {
QTMT_SetMovieLoopState(theMovie, kNormalLooping);
StartMovie(theMovie);
}
}
As you can see, our movie preprerolling completion function is quite simple. It really just prerolls the movie, thereby making the movie ready for playback. For some movies (namely, for QTMooVToolbox's "picture-in-picture movies"), it also starts the movies playing in a loop. We'll discuss picture-in-picture movies later (in "Playing Picture-In-Picture Movies").
Note that preprerolling and prerolling are required only for movies that we are managing directly, using the Movie Toolbox. If we use the movie controller interface for managing movies, the movie controller automatically preprerolls and prerolls the movies for us.
Playing Movies
Now we are finally ready to start our movie playing. Assuming that a movie has already been preprerolled and prerolled, we can set it in motion by setting its rate to a non-zero value, using the SetMovieRate function. Or, we can simply call the StartMovie function, which sets the movie rate to the default (or "preferred") movie rate. QTMooVToolbox defines the function QTMT_StartMovie (shown in Listing 8) to start a movie.
Listing 8: Starting a movie playing.
QTMT_MoviePrePrerollCompleteProc
void QTMT_StartMovie (Movie theMovie)
{
if (theMovie == NULL)
return;
// if we aren't looping and we're at the end of the movie,
// return to the beginning of the movie
if (!QTMT_IsMovieLooping(theMovie))
if (GetMovieTime(theMovie, NULL) ==
GetMovieDuration(theMovie))
GoToBeginningOfMovie(theMovie);
StartMovie(theMovie);
}
QTMT_StartMovie checks to see whether a non-looping movie has reached its end; if it has, QTMT_StartMovie calls the Movie Toolbox function GoToBeginningOfMovie to reset the current movie time to the beginning of the movie. If we had been playing the movie using a movie controller, the movie controller would have done this for us. Since we're programming at a slightly lower level, we need to do some of this housekeeping ourselves.
Similarly, when we are playing a non-looping movie using the Movie Toolbox and the movie reaches its end, its rate is not automatically set to 0. Again, this is some minor housekeeping that we need to take care of ourselves. In this case, we'll do the appropriate check in our QTApp_Idle routine, defined in Listing 9.
Listing 9: Performing idle-time tasks.
QTMT_MoviePrePrerollCompleteProc
void QTApp_Idle (WindowReference theWindow)
{
GrafPtr mySavedPort;
WindowObject myWindowObject = NULL;
ApplicationDataHdl myAppData = NULL;
GetPort(&mySavedPort);
MacSetPort(QTFrame_GetPortFromWindowReference(theWindow));
myWindowObject =
QTFrame_GetWindowObjectFromWindow(theWindow);
myAppData =
(ApplicationDataHdl)QTFrame_GetAppDataFromWindow (theWindow);
if ((myWindowObject != NULL) && (myAppData != NULL)) {
if (!QTMT_IsMovieLooping((**myWindowObject).fMovie))
if (GetMovieTime((**myWindowObject).fMovie, NULL) ==
GetMovieDuration((**myWindowObject).fMovie))
if (GetMovieRate((**myWindowObject).fMovie) > 0)
SetMovieRate((**myWindowObject).fMovie, 0);
if ((**myWindowObject).fMovie != NULL)
MoviesTask((**myWindowObject).fMovie, DoTheRightThing);
if ((**myAppData).fPIPMovie != NULL)
QTMT_DrawPictureInPictureMovie(myWindowObject);
}
MacSetPort(mySavedPort);
}
QTApp_Idle does three main things. It sets the movie rate of the movie in the specified window to 0 if it isn't looping and it has played to its end; it tasks the movie; and it draws any picture-in-picture movie in the specified window. (Once again, we'll discuss picture-in-picture movies at more length later.) As you can see, MoviesTask takes two parameters; these are the movie to be tasked and a maximum number of milliseconds that MoviesTask should spend doing its work. Movies.h defines the constant DoTheRightThing as 0, which means to service the specified movie once and then return.
So, at last, we have at hand all the essential elements of playing movies using the Movie Toolbox. First, we open a movie file and load the movie from it (using the same code that we used when playing movies using movie controllers). Then, we prepreroll and preroll that movie. Then, we start the movie playing by calling StartMovie. And finally, we periodically call MoviesTask to give the movie some time to play. It's okay to call MoviesTask on a movie that has finished playing, so in QTApp_Idle we don't bother to check whether the movie is actually playing or not. In fact, we shouldn't do that, since we need to call MoviesTask on a movie while it is preprerolling as well as while it is playing.
Editing Movies
Editing movies is one of the tasks at which movie controllers excel. A movie controller keeps track of a movie's current selection and allows us (for instance) to cut that selection from that movie by calling the function MCCut, like this:
case IDM_EDITCUT:
myEditMovie = MCCut(myMC);
break;
As you can see, MCCut also returns to us a movie that is the cut selection, which we can put on the scrap (to make it available for subsequent pasting) by calling PutMovieOnScrap. Moreover, the movie controller keeps track of the current edit state of the movie that exists before each editing operation, so that it can restore that state if we subsequently call MCUndo. A movie's current edit state consists of all the information that describes the movie's contents, such as the sound and video data in the movie; the current edit state also includes the movie's current selection.
The Movie Toolbox provides a set of high-level movie editing functions, which are just as easy to use as the functions provided by movie controllers. For instance, to cut the current movie selection, we can call the CutMovieSelection function.
case IDM_EDITCUT:
myEditMovie = CutMovieSelection(myMovie);
break;
Similarly, we can use the ClearMovieSelection function to clear the current movie selection (that is, remove it from the movie without retaining the removed data).
case IDM_EDITCLEAR:
ClearMovieSelection(myMovie);
break;
Moreover, the Movie Toolbox provides the SetMovieSelection function, which we can use to handle the Select All menu item in the Edit menu, like this:
case IDM_EDITSELECTALL:
SetMovieSelection(myMovie, 0L, GetMovieDuration(myMovie));
break;
There is one complication here: if we want to provide the standard movie editing behavior without using a movie controller, then we also need to keep track of the movie's current edit state whenever we perform any editing operations. As we've seen, the application data record for QTMooVToolbox contains a field fMovieEditState, of type MovieEditState, which we'll use to hold the current edit state. Before we call any Movie Toolbox function that changes the contents of a movie, we can call the NewMovieEditState function to create a new movie edit state, as in this snippet from QTApp_HandleMenu:
if ((theMenuItem == IDM_EDITCUT) ||
(theMenuItem == IDM_EDITPASTE) ||
(theMenuItem == IDM_EDITCLEAR)) {
if ((**myAppData).fMovieEditState != NULL)
DisposeMovieEditState((**myAppData).fMovieEditState);
(**myAppData).fMovieEditState = NewMovieEditState(myMovie);
}
Notice that we need to dispose of any existing edit state (by calling DisposeMovieEditState) before creating a new one. Otherwise, we'd be leaking memory.
If the user selects the Undo menu item in the Edit menu, we need to reinstate the movie edit state that we have stored in our application data record. We do this by calling UseMovieEditState, like this:
case IDM_EDITUNDO:
if ((**myAppData).fMovieEditState == NULL)
break;
UseMovieEditState(myMovie, (**myAppData).fMovieEditState);
DisposeMovieEditState((**myAppData).fMovieEditState);
(**myAppData).fMovieEditState = NULL;
break;
Once we have used an edit state, we want to dispose of it and set the stored edit state reference to NULL so that we don't reinstate the same edit state twice. QTMooVToolbox supports only one level of undo, just like the standard linear movie controller. In theory, however, you can NewMovieEditState and UseMovieEditState to support multiple levels of undo. Implementing this feature is left as an exercise for the reader.
There is one final complication to consider. If the user selects the entire movie and then cuts the current selection, the resulting movie is empty. It still exists, but it has no video or audio data in it. Since the movie has no video data, the movie box is empty. Now, if we had performed this cut using movie controller functions, the movie controller would have detected that the size of the movie had changed and would have notified our application by sending it the mcActionControllerSizeChanged movie controller action; in response to this action we would have called our application's QTFrame_SizeWindowToMovie function. But we didn't use movie controller functions to perform the cut, so we need to handle this possibility ourselves.
The simplest way to handle this situation is just to call QTFrame_SizeWindowToMovie whenever we perform any editing operation that might have changed the size of the movie, like this:
if ((theMenuItem == IDM_EDITUNDO) || (theMenuItem == IDM_EDITCUT) || (theMenuItem == IDM_EDITPASTE) || (theMenuItem == IDM_EDITCLEAR)) {
QTFrame_SizeWindowToMovie(myWindowObject);
(**myWindowObject).fIsDirty = true;
}
We now have all the pieces that we need to handle the Edit menu commands without using movie controllers. Listing 10 shows the QTApp_HandleMenu function for QTMooVToolbox. (For readability, I've cut out the Test menu handling.)
Listing 10: Handling Edit menu items using the Movie Toolbox.
QTApp_HandleMenu
Boolean QTApp_HandleMenu (UInt16 theMenuItem)
{
WindowObject myWindowObject = NULL;
ApplicationDataHdl myAppData = NULL;
Movie myMovie = NULL;
Movie myEditMovie = NULL;
Boolean myIsHandled = false;
myWindowObject = QTFrame_GetWindowObjectFromFrontWindow();
if (myWindowObject != NULL) {
myAppData =
(ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject (myWindowObject);
myMovie = (**myWindowObject).fMovie;
}
if ((myWindowObject == NULL) || (myAppData == NULL) || (myMovie == NULL))
return(myIsHandled);
// before the Cut, Paste, and Clear commands, get the current edit state,
// after disposing of the currently-saved edit state
if ((theMenuItem == IDM_EDITCUT) || (theMenuItem == IDM_EDITPASTE) || (theMenuItem == IDM_EDITCLEAR)) {
if ((**myAppData).fMovieEditState != NULL)
DisposeMovieEditState((**myAppData).fMovieEditState);
(**myAppData).fMovieEditState = NewMovieEditState(myMovie);
}
switch (theMenuItem) {
// Edit menu items
// Here we are overriding the framework's Edit menu handling
// (which uses movie controllers).
case IDM_EDITUNDO:
// restore the movie using the currently-saved edit state, then dispose of it
if ((**myAppData).fMovieEditState == NULL)
break;
UseMovieEditState(myMovie, (**myAppData).fMovieEditState);
DisposeMovieEditState((**myAppData).fMovieEditState);
(**myAppData).fMovieEditState = NULL;
break;
case IDM_EDITCUT:
myEditMovie = CutMovieSelection(myMovie);
break;
case IDM_EDITCOPY:
myEditMovie = CopyMovieSelection(myMovie);
break;
case IDM_EDITPASTE:
myEditMovie = NewMovieFromScrap(newMovieActive);
if (myEditMovie != NULL) {
PasteMovieSelection(myMovie, myEditMovie);
DisposeMovie(myEditMovie);
myEditMovie = NULL;
}
break;
case IDM_EDITCLEAR:
ClearMovieSelection(myMovie);
break;
case IDM_EDITSELECTALL:
SetMovieSelection(myMovie, 0L, GetMovieDuration(myMovie));
break;
// Test menu items omitted here
default:
break;
} // switch (theMenuItem)
// place any cut or copied movie segment onto the scrap
if (myEditMovie != NULL) {
PutMovieOnScrap(myEditMovie, 0L);
DisposeMovie(myEditMovie);
}
// after any commands that might have changed the movie, sync up the movie // window size and mark the window as dirty
if ((theMenuItem == IDM_EDITUNDO) || (theMenuItem ==
IDM_EDITCUT) || (theMenuItem == IDM_EDITPASTE) || (theMenuItem == IDM_EDITCLEAR)) {
QTFrame_SizeWindowToMovie(myWindowObject);
(**myWindowObject).fIsDirty = true;
}
// for any Edit menu command, indicate that we handled it
if (MENU_ID(theMenuItem) == kEditMenuResID)
myIsHandled = true;
return(myIsHandled);
}
All in all, handling the Edit menu items using Movie Toolbox functions is not too much more complicated than doing it using the movie controller functions. We need to manage the movie edit states ourselves, and we need to make sure to resize a changed movie. But otherwise the code is quite reminiscent of the movie controller editing code.
Looping Movies
In the previous QuickTime Toolkit article, you might recall, we saw how to read a movie's looping state from some user data stored in the movie file. If a movie file contains a user data item of type 'LOOP', then the data in that item determines whether the looping state is normal looping (data is 0) or palindrome looping (data is 1). If there is no such user data item in the movie file, then we assume that the movie does not loop. To read a movie's looping state, we defined the function QTUtils_GetMovieFileLoopingInfo, which returns one of three constants defined in our header file QTUtilities.h:
enum {
kNormalLooping = 0,
kPalindromeLooping = 1,
kNoLooping = 2
};
To set a movie's looping state based on the looping state stored in the file, we called the MCDoAction function, passing the two movie controller actions mcActionSetLooping and mcActionSetLoopIsPalindrome with appropriate combinations of true and false. Now we want to see how to accomplish the same thing without using these movie controller actions.
To manipulate a movie's looping state without using movie controller functions, we need to work with the movie's time base, which defines the time coordinate system of the movie. As Inside Macintosh nicely phrases it, we can think of a movie's time base like a vector that defines the direction and velocity of time for a movie. A movie's time base also defines the current movie time. So it makes sense that time bases are associated with looping: normal looping involves simply resetting the current movie time to 0 once the movie time reaches the end, while palindrome looping involves reversing the direction of time while keeping the velocity the same.
The Movie Toolbox creates a time base for us when we load a movie. Thereafter, we can retrieve a movie's time base by calling the GetMovieTimeBase function. The movie's looping state is determined by some of the time base control flags, a 32-bit value which we can get and set by calling the functions GetTimeBaseFlags and SetTimeBaseFlags. Movies.h defines these two time base flags related to looping:
enum {
loopTimeBase = 1,
palindromeLoopTimeBase = 2
};
These constants are interpreted as bit masks for the time base control flags. In other words, you should think of loopTimeBase as 1L<<0 and palindromeLoopTimeBase as 1L<<1. As you might have guessed, we can set a movie's looping state by getting the current time base control flags, setting the appropriate bits on or off, and then setting the revised time base control flags. Here's how we would enable palindrome looping:
TimeBase myTimeBase = GetMovieTimeBase(theMovie);
long myFlags = GetTimeBaseFlags(myTimeBase);
myFlags |= loopTimeBase;
myFlags |= palindromeLoopTimeBase;
SetTimeBaseFlags(myTimeBase, myFlags);
For optimal playback performance, however, we should take one further step, which is to set the movie's playback hints to reflect the desired looping state. A movie's playback hints specify one or more optimizations that the Movie Toolbox should use during movie playback. Once again, the playback hints are defined using individual bits in a 32-bit long word. These two bits are defined for setting movie looping hints:
enum {
hintsLoop = 1 << 1,
hintsPalindrome = 1 << 9
};
The Movie Toolbox provides the SetMoviePlayHints function for setting a movie's playback hints, but there is no corresponding GetMoviePlayHints function to get the current playback hints. Instead, SetMoviePlayHints requires us to pass it a 32-bit mask that determines which of the bits in the 32-bit play hints long word are to be considered. For example, to set the palindrome looping play hint on, we could do this:
SetMoviePlayHints(theMovie, hintsLoop, hintsLoop);
SetMoviePlayHints(theMovie, hintsPalindrome, hintsPalindrome);
These two lines of code set both the looping and palindrome looping play hints to be active. (Remember that looping must be enabled in order for palindrome looping to be enabled.) And to set the movie play hints to disable any looping, we could execute this line:
SetMoviePlayHints(theMovie, 0L, hintsLoop | hintsPalindrome);
So, putting this all together, we see that to set the looping state of a movie, we need to set the time base control flags appropriately; we also should set the movie's playback hints to optimize playback performance. Listing 11 defines our complete QTMT_SetMovieLoopState for setting a movie's looping state.
Listing 11: Setting a movie's looping state.
QTMT_SetMovieLoopState
void QTMT_SetMovieLoopState (Movie theMovie, UInt8 theLoopState)
{
TimeBase myTimeBase;
long myFlags = 0L;
if (theMovie == NULL)
return;
myTimeBase = GetMovieTimeBase(theMovie);
myFlags = GetTimeBaseFlags(myTimeBase);
switch (theLoopState) {
case kNormalLooping:
SetMoviePlayHints(theMovie, hintsLoop, hintsLoop);
SetMoviePlayHints(theMovie, 0L, hintsPalindrome);
myFlags |= loopTimeBase;
myFlags &= ~palindromeLoopTimeBase;
break;
case kPalindromeLooping:
SetMoviePlayHints(theMovie, hintsLoop, hintsLoop);
SetMoviePlayHints(theMovie, hintsPalindrome, hintsPalindrome);
myFlags |= loopTimeBase;
myFlags |= palindromeLoopTimeBase;
break;
case kNoLooping:
default:
myFlags &= ~loopTimeBase;
myFlags &= ~palindromeLoopTimeBase;
SetMoviePlayHints(theMovie, 0L, hintsLoop | hintsPalindrome);
break;
}
SetTimeBaseFlags(myTimeBase, myFlags);
}
For certain purposes, we sometimes want to know whether a movie is looping and whether it is palindrome looping. Based on what we've learned so far, we can easily define the function QTMT_IsMovieLooping shown in Listing 12, which returns a Boolean value that indicates whether looping (either normal or palindrome) is enabled for the specified movie.
Listing 12: Determining whether a movie is looping.
QTMT_IsMovieLooping
Boolean QTMT_IsMovieLooping (Movie theMovie)
{
TimeBase myTimeBase;
long myFlags = 0L;
if (theMovie == NULL)
return(false);
myTimeBase = GetMovieTimeBase(theMovie);
myFlags = GetTimeBaseFlags(myTimeBase);
return((Boolean)(myFlags & loopTimeBase));
}
And we can easily define the function QTMT_IsMoviePalindromeLooping, shown in Listing 13, to tell us whether palindrome looping is enabled for the specified movie.
Listing 13: Determining whether a movie is palindrome looping.
QTMT_IsMoviePalindromeLooping
Boolean QTMT_IsMoviePalindromeLooping (Movie theMovie)
{
TimeBase myTimeBase;
long myFlags = 0L;
if (theMovie == NULL)
return(false);
myTimeBase = GetMovieTimeBase(theMovie);
myFlags = GetTimeBaseFlags(myTimeBase);
return((Boolean)((myFlags & loopTimeBase) && (myFlags & palindromeLoopTimeBase)));
}
Our application QTMooVToolbox uses these functions to set the appropriate check marks on the "Loop" and "Loop Back And Forth" menu items in the Test menu, like this:
QTFrame_SetMenuItemCheck(myMenu, IDM_SET_LOOPING, QTMT_IsMovieLooping(myMovie) && !QTMT_IsMoviePalindromeLooping(myMovie));
QTFrame_SetMenuItemCheck(myMenu, IDM_SET_PALINDROME_LOOPING, QTMT_IsMoviePalindromeLooping(myMovie));
Playing Picture-In-Picture Movies
Last Christmas, I was hoping that Santa Claus would bring me a new television set, perhaps a bit larger than the one I have now, and possibly also with the so-called "picture-in-picture" capability, where you can tune into a second channel that's displayed in a smaller box in the corner of the main picture. Well, Santa wasn't that generous this year, so I decided to see whether QuickTime could help satisfy my craving to watch two video sources at once. (See Figure 4 once again.) It turns out that it's surprisingly straightforward to use the Movie Toolbox to embed one movie inside of another movie. Let's call the movie that we are embedding the "picture-in-picture movie" and the movie we're embedding it into the "main movie".
In the broadest outlines, we want to open the movie file that contains the picture-in-picture movie, set that movie's graphics port to be the window that already contains the main movie, and then set the picture-in-picture movie's enclosing rectangle to fit within the main movie window. Then we need to ensure that the main movie doesn't draw on top of the picture-in-picture movie; we do this by setting the main movie's display clipping region to exclude the rectangle occupied by the picture-in-picture movie. Finally, we need to task both movies in order to update the movie images and play the movies' sound. Let's see how to do these things.
Setting the Picture-In-Picture Movie Geometry
Since we want to draw the picture-in-picture movie in the same window as the main movie - and indeed on top of the main movie - we need to set the graphics port of the picture-in-picture movie to the same graphics port used by the main movie. We can do that quite easily, like this:
GetMovieGWorld(myMainMovie, &myGWorld, NULL);
SetMovieGWorld(myPIPMovie, myGWorld, NULL);
But, we don't want to draw the picture-in-picture movie at its natural size. Instead, we want it to occupy some smaller portion of the main movie. For simplicity, we'll set the picture-in-picture movie to have a width that is one-third the width of the main movie, and a height that is one-third the height of the main movie.
GetMovieBox(myMainMovie, &myRect);
myValue = (myRect.bottom - myRect.top) / 3;
myRect.top += myValue;
myRect.bottom -= myValue;
myValue = (myRect.right - myRect.left) / 3;
myRect.left += myValue;
myRect.right -= myValue;
SetMovieBox(myPIPMovie, &myRect);
Note that this strategy will result in the picture-in-picture movie being distorted, unless its original dimensions are proportional to the dimensions of the main movie. It would be easy, however, to modify this code to retain the original proportions of the picture-in-picture movie while limiting its largest dimension to one-third the height or width of the main movie. (This refinement is left as an exercise for the reader.) Finally, let's keep track of the picture-in-picture movie and its new rectangle in the application data structure attached to the main movie window.
(**myAppData).fPIPRect = myRect;
(**myAppData).fPIPMovie = myPIPMovie;
Setting the Main Movie's Display Clipping Region
If we simply started playing the picture-in-picture movie and the main movie using the geometry we've set up so far, we would get an annoying flickering caused by the main movie image being drawn over the existing picture-in-picture movie image, which in turn is then refreshed over the main movie image. To avoid this, we want to set up the main movie so that it doesn't draw within the rectangle that encloses the picture-in-picture movie. We can do this by defining a new display clipping region for the main movie.
A movie's display clipping region is the portion of the movie box in which the movie image is to be drawn. That is to say, the display clipping region defines a clipping region that is applied to a movie just before it is displayed. (A movie also has a clipping region, but that region is applied much earlier in the imaging pipeline and isn't relevant to us here.) By default, a movie's display clipping region exactly coincides with its movie box, but it's possible to set any subregion of the movie box as the display clipping region. In the current situation, we want the main movie's display clipping region to be the rectangle occupied by the main movie minus the rectangle occupied by the picture-in-picture movie.
To calculate this clipping region, we can use standard QuickDraw functions to get the regions corresponding to the two movie boxes and then subtract one region from the other. We set this region as the main movie's display clipping region using the SetMovieDisplayClipRgn function. This is all accomplished in the function QTMT_SetMovieClipRegion defined in Listing 14. The Movie Toolbox copies the display clipping region that we pass it, so we can safely dispose of that region, as well as the two intermediate regions we use to calculate the display clipping region, before exiting QTMT_SetMovieClipRegion.
Listing 14: Setting the display clipping region of the main movie.
QTMT_SetMovieClipRegion
OSErr QTMT_SetMovieClipRegion (WindowObject theWindowObject)
{
Rect myRect;
RgnHandle myMovieRegion;
RgnHandle myPIPRegion;
RgnHandle myClipRegion;
ApplicationDataHdl myAppData = NULL;
// get the application-specific data
myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject(theWindowObject);
if (myAppData == NULL)
return(paramErr);
myMovieRegion = NewRgn();
myPIPRegion = NewRgn();
myClipRegion = NewRgn();
GetMovieBox((**theWindowObject).fMovie, &myRect);
RectRgn(myMovieRegion, &myRect);
myRect = (**myAppData).fPIPRect;
if (!EmptyRect(&myRect))
MacInsetRect(&myRect, -kPIPBorderSize, -kPIPBorderSize);
RectRgn(myPIPRegion, &myRect);
DiffRgn(myMovieRegion, myPIPRegion, myClipRegion);
SetMovieDisplayClipRgn((**theWindowObject).fMovie, myClipRegion);
DisposeRgn(myMovieRegion);
DisposeRgn(myPIPRegion);
DisposeRgn(myClipRegion);
return(noErr);
}
Notice that we call MacInsetRect inside of QTMT_SetMovieClipRegion to expand the picture-in-picture movie box by a preset amount (kPIPBorderSize) before converting that box to a region. We do this to provide a few pixels of space in which to draw a border around the picture-in-picture movie, which gives it a somewhat better appearance. (See Listing 15 below for the code that draws the border.)
There is one final thing to keep in mind: if the main movie were being played using a movie controller component, then we should set the movie display clipping region using the function MCSetClip instead of SetMovieDisplayClipRgn.
Playing the Picture-In-Picture Movie
Now that we've opened the picture-in-picture movie and positioned it within the main movie window, we need to start the picture-in-picture movie playing and keep it playing. All the lessons we learned earlier in "Preparing Movies For Playback" apply here as well, so we'll kick things off by preprerolling the picture-in-picture movie, like this:
PrePrerollMovie(myPIPMovie, 0, GetMoviePreferredRate(myPIPMovie), gMoviePPRollCompleteProc, (void *)theWindowObject);
We're using the same movie preprerolling completion function that we used for the main movie, which prerolls the movie as soon as the preprerolling is completed. But you'll recall from Listing 7 that the completion procedure includes a few extra lines that apply only to the picture-in-picture movie:
if (theMovie == (**myAppData).fPIPMovie) {
QTMT_SetMovieLoopState(theMovie, kNormalLooping);
StartMovie(theMovie);
}
Since we're not going to provide a way for the user to start or stop the picture-in-picture movie, we call QTMT_SetMovieLoopState (defined in Listing 11) to set the movie looping state to normal looping. The movie will then keep playing, over and over, until we close the window or replace it with some other picture-in-picture movie. Then, we just call StartMovie to start the picture-in-picture movie playing.
So the only thing that remains for us to do is task the movie periodically to keep it playing back smoothly. As before, this is something we want to do in our idle routine (QTApp_Idle), where we are already calling MoviesTask on the main movie. We have several options here. First, we could just add these few lines of code to QTApp_Idle, to explicitly task the picture-in-picture movie:
if ((**myAppData).fPIPMovie != NULL)
MoviesTask((**myAppData).fPIPMovie, DoTheRightThing);
Or, we could pass the value NULL as the first parameter to MoviesTask, like this:
MoviesTask(NULL, DoTheRightThing);
When we specify NULL in place of a movie, we're instructing the Movie Toolbox to service all open movies belonging to the application. Remember, however, that we want to draw a border around the picture-in-picture movie. So the option we'll actually choose will be to call an application-defined function:
if ((**myAppData).fPIPMovie != NULL)
QTMT_DrawPictureInPictureMovie(myWindowObject);
QTMT_DrawPictureInPictureMovie simply calls MoviesTask on the picture-in-picture movie and draws a nice-looking, pseudo-grayscale border around the movie, as shown in Listing 15.
Listing 15: Drawing the picture-in-picture movie and its border.
QTMT_DrawPictureInPictureMovie
OSErr QTMT_DrawPictureInPictureMovie (WindowObject theWindowObject)
{
ApplicationDataHdl myAppData = NULL;
if (theWindowObject == NULL)
return(paramErr);
myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject(theWindowObject);
if (myAppData == NULL)
return(paramErr);
// draw the current frame of the picture-in-picture movie
if ((**myAppData).fPIPMovie != NULL)
MoviesTask((**myAppData).fPIPMovie, DoTheRightThing);
if (!EmptyRect(&(**myAppData).fPIPRect)) {
PenState myPenState;
Rect myRect;
myRect = (**myAppData).fPIPRect;
// draw a pseudo-grayscale border
GetPenState(&myPenState);
MacInsetRect(&(**myAppData).fPIPRect, -kPIPBorderSize, - kPIPBorderSize);
RGBForeColor(&gBlack);
MacFrameRect(&(**myAppData).fPIPRect);
MacInsetRect(&(**myAppData).fPIPRect, 1, 1);
RGBForeColor(&gWhite);
MacFrameRect(&(**myAppData).fPIPRect);
MacInsetRect(&(**myAppData).fPIPRect, 1, 1);
RGBForeColor(&gLGray);
MacFrameRect(&(**myAppData).fPIPRect);
(**myAppData).fPIPRect = myRect;
SetPenState(&myPenState);
}
return(noErr);
}
Moving the Picture-In-Picture Movie
Let's finish up by adding a simple enhancement to our picture-in-picture movie playback capability: whenever the user clicks the mouse button when the cursor is inside of the main movie window, let's move the picture-in-picture movie so that its upper-left corner is at the location of the click. Our basic cross-platform QuickTime framework passes mouse button clicks to the application function QTApp_HandleContentClick whenever those clicks fall within the window's content area. So all we need to do is add some code to QTApp_HandleContentClick.
Moving the picture-in-picture movie rectangle is very simple. First, we need to convert the point that's passed to us in the event record into local coordinates:
myPoint = theEvent->where;
GlobalToLocal(&myPoint);
Then we need to calculate the new movie box for the picture-in-picture movie. The size of the movie box isn't changing, only its upper-left corner. So we can call MacOffsetRect to figure out where to place the movie box and SetMovieBox to actually move it.
MacOffsetRect(&(**myAppData).fPIPRect,
myPoint.h - (**myAppData).fPIPRect.left,
myPoint.v - (**myAppData).fPIPRect.top);
SetMovieBox((**myAppData).fPIPMovie, &(**myAppData).fPIPRect);
Finally, we need to recalculate the display clipping region of the main movie, like this:
QTMT_SetMovieClipRegion(myWindowObject);
The complete procedure for moving the picture-in-picture movie in response to mouse clicks is shown in Listing 16.
Listing 16: Moving the picture-in-picture movie within the main movie.
QTApp_HandleContentClick
void QTApp_HandleContentClick (WindowReference theWindow, EventRecord *theEvent)
{
GrafPtr mySavedPort;
WindowObject myWindowObject = NULL;
ApplicationDataHdl myAppData = NULL;
GetPort(&mySavedPort);
MacSetPort(QTFrame_GetPortFromWindowReference(theWindow));
myWindowObject = QTFrame_GetWindowObjectFromWindow(theWindow);
myAppData = (ApplicationDataHdl)QTFrame_GetAppDataFromWindow(theWindow);
if ((myWindowObject == NULL) || (myAppData == NULL))
return;
// move the picture-in-picture movie to the specified location
if ((**myAppData).fPIPMovie != NULL) {
Point myPoint;
myPoint = theEvent->where;
GlobalToLocal(&myPoint);
// if the main movie isn't playing, erase the rectangle currently occupied
// by the picture-in-picture movie
if (!QTMT_IsMoviePlaying((**myWindowObject).fMovie)) {
Rect myRect;
myRect = (**myAppData).fPIPRect;
MacInsetRect(&myRect, -kPIPBorderSize, - kPIPBorderSize);
EraseRect(&myRect);
}
MacOffsetRect(&(**myAppData).fPIPRect,
myPoint.h - (**myAppData).fPIPRect.left,
myPoint.v - (**myAppData).fPIPRect.top);
SetMovieBox((**myAppData).fPIPMovie, &(**myAppData).fPIPRect);
QTMT_SetMovieClipRegion(myWindowObject);
}
MacSetPort(mySavedPort);
}
Conclusion
Keep in mind that the point of this article is not that you should be playing movies using the Movie Toolbox alone, without the assistance of movie controllers. Indeed, as we've seen, movie controllers do lots and lots of useful work for us, as well as a good deal of humdrum housekeeping (such as setting the movie rate to 0 when the movie reaches the end). And movie controllers have a certain measure of built-in "future compatibility", as evidenced by the fact that movie controllers now prepreroll movies automatically. This means that, if you had written a movie-playing application several years ago that used movie controllers, you wouldn't have had to change it much, if at all, to support the new streaming movies introduced in QuickTime 4.
Still, there are occasions when you might need to manage a movie without using a movie controller, and we've seen (fairly exhaustively, I hope) what you need to do to accomplish that. But, our larger goal here was to get acquainted with the Movie Toolbox itself, to get a taste of its capabilities and to see how to operate on movies directly. As we steadily progress in our journey through QuickTime, we'll come to depend more and more on the Movie Toolbox and less and less on movie controllers.
Credits
Thanks are due to Sam Bushell and Peter Hoddie for clarifying several issues about the Movie Toolbox for me. And, thanks to Eric Blanpied for providing the rendered toy train movie shown in Figure 3.
Tim Monroe (monroe@apple.com) is a software engineer on Apple's QuickTime team. He is currently developing sample code and utilities for the QuickTime software development kit.