|
Volume Number: 18 (2002)
Issue Number: 04
Column Tag: QuickTime Toolkit
Big
by Tim Monroe
Playing QuickTime Movies Fullscreen
Introduction
It's sometimes remarked that when QuickTime was first introduced, it was able to play movies that were only about the size of a postage stamp. It turns out that this isn't quite true. The very first QuickTime CDs distributed to software developers contained a number of sample movies that are 320 by 240 pixels, or just under 4.5 by 3.5 inches (which is significantly larger than any postage stamp I've ever seen). What is true is that certain kinds of movies — in particular, full-motion movies encoded using the video compressor — had to be kept small in order to achieve a reasonable frame rate on playback. These kinds of movies typically had to be somewhere in the order of 160 by 120 pixels to get a playback rate of about 12 frames per second. Not great, but pretty good for a software-only movie playback system on a Mac II in 1991.
Since then, computer hardware and software technologies have advanced to the point that QuickTime can achieve smooth playback for much larger movies, at frames rates of 24 to 30 frames per second (or even greater). From QuickTime version 2.1 onward, it has also been possible to play QuickTime movies back fullscreen, so that the movie occupies an entire screen. Figure 1 shows a frame of a movie being played back fullscreen; as you can see, there is no menu bar and the window that contains the movie does not have a window frame or title bar. In addition, the control strip (on classic Mac operating systems), the dock (on Mac OS X), or the taskbar (on Windows operating systems) is hidden while a movie is playing fullscreen. At its natural proportions, this movie does not completely fill the screen, so it's centered horizontally with the edges drawn in black.
Figure 1: A QuickTime movie played fullscreen
Fullscreen movie playback is fairly common in games, especially for the cut-scenes that occur between game levels. It has also recently become popular for many of the movie trailers posted to the web in QuickTime format.
In this article, we're going to learn how to play QuickTime movies fullscreen. I mentioned in an earlier article ("The Flash", in MacTech, January 2002) that fullscreen movie playback is accomplished primarily using the two functions BeginFullScreen and EndFullScreen. When using these functions to integrate fullscreen playback into our sample applications, however, we'll need to pay attention to a number of issues, including saving and restoring the state of our movie windows and their associated movie controllers. We also want to take a look at the wired action introduced in QuickTime 5.0.1 that a movie can use to begin and end fullscreen playback. Along the way, we'll touch on a few topics of general interest to QuickTime developers, including time base callback functions.
Our sample application this month is called QTBigScreen. As usual, it's based on the QTShell sample application and adds support for entering and exiting fullscreen mode. The Test menu of QTBigScreen is shown in Figure 2; it contains just one item, which allows the user to put the frontmost movie window into fullscreen mode.
Figure 2: The Test menu of QTBigScreen
There is no menu item for exiting fullscreen mode; instead, we'll follow the example of QuickTime Player and return to the normal windowed mode when the user types the Escape key or when a non-interactive and non-looped movie reaches the end.
The Theory
Let's begin by taking a look at the BeginFullScreen and EndFullScreen functions. As I mentioned, things tend to get a tad lengthy when we use these calls in a real-life application, so it's good to have a firm grasp on how they work before we try to do that. In this article, we are going to focus on using these functions to play QuickTime movies fullscreen, but they can in fact be used to display any kind of content in a fullscreen window. Moreover, we can use these functions simply to change the resolution of a screen, without wanting to take over the entire screen. In short, there's a lot going on with these two functions that we need to understand clearly before we attempt to use them in our applications.
Entering and Exiting Fullscreen Mode
The BeginFullScreen function is declared essentially like this:
OSErr BeginFullScreen ( Ptr *restoreState, GDHandle whichGD, short *desiredWidth, short *desiredHeight, WindowRef *newWindow, RGBColor *eraseColor, long flags);
The key input parameters are desiredWidth and desiredHeight, which are pointers to the width and height of the movie (or image, or other content) that we want to display fullscreen. BeginFullScreen creates a new window that is at least that large and returns a window pointer for that window in the location pointed to by the newWindow parameter. BeginFullScreen also erases the screen (using the color specified by the eraseColor parameter) and (depending on the value of the flags parameter, which we'll consider in a moment) hides the menu bar and control strip. The whichGD parameter indicates which graphics device we want to put into fullscreen mode; we shall always pass the value NULL to select the main screen.
BeginFullScreen also returns, through the restoreState parameter, a pointer to a block of memory that contains information on how to return from fullscreen mode to normal windowed mode. We exit fullscreen mode by passing that pointer to EndFullScreen, which is declared like this:
OSErr EndFullScreen (Ptr fullState, long flags);
Note that the restoreState (or fullState) pointer is opaque and is owned by QuickTime; we shouldn't do anything with it except pass it to EndFullScreen when we are ready to exit fullscreen mode. Similarly, the newWindow window pointer is owned by QuickTime, which will dispose of it after we call EndFullScreen.
The flags parameter passed to EndFullScreen is unused and should be set to 0. The flags parameter passed to BeginFullScreen controls several aspects of fullscreen mode. Currently we can use these constants to set the value of this parameter:
enum { fullScreenHideCursor = 1L << 0, fullScreenAllowEvents = 1L << 1, fullScreenDontChangeMenuBar = 1L << 2, fullScreenPreflightSize = 1L << 3 };
The fullScreenHideCursor flag indicates that BeginFullScreen should hide the cursor while the screen is in fullscreen mode. This is useful if we just want to play a movie fullscreen from start to finish, but it's less useful for interactive movies played fullscreen. In QTBigScreen, we will not set this flag when we call BeginFullScreen. The fullScreenAllowEvents flag indicates that our application intends to allow other open applications to receive processing time; in general, we should set this flag. The fullScreenDontChangeMenuBar flag indicates that BeginFullScreen should not hide the menu bar.
The fullScreenPreflightSize flag is of particular interest. If we set this flag, then BeginFullScreen does not change any screen settings and does not return a new window to us. Instead, it returns, through the desiredWidth and desiredHeight parameters, the width and height that the screen would have been set to if the fullScreenPreflightSize flag had not been set. We can use that flag to determine the size of the fullscreen window without actually entering fullscreen mode.
Changing the Screen Resolution
Why is this useful? Recall that BeginFullScreen will create a window that is at least as large as the height and width we pass it. In addition, BeginFullScreen will change the screen resolution to the closest resolution that contains that window. If, for instance, the main screen is originally set to a resolution of 1024 by 768 and if we pass a width and height of (say) 762 and 560, then BeginFullScreen will change the resolution of the screen to 800 by 600 (assuming that the monitor supports that resolution).
We can exploit this behavior in several ways. First, we can use BeginFullScreen to determine the current screen resolution. If we set 0 to be the value pointed to by both the desiredWidth and desiredHeight parameters, then BeginFullScreen leaves the dimensions of the screen unchanged but returns to us the current dimensions of the screen in the locations pointed to by those parameters. If we also pass the fullScreenPreflightSize flag, then BeginFullScreen doesn't change any of the current screen settings. The end result is that we are given the current height and width of the screen (that is, its resolution). Listing 1 defines the function QTUtils_GetScreenResolution, which retrieves the current resolution of the main screen.
Listing 1: Getting the current resolution of the main screen
OSErr QTUtils_GetScreenResolution (short *thePixelsHoriz, short *thePixelsVert) { Ptr myDummyPtr = NULL; OSErr myErr = noErr; if ((thePixelsHoriz == NULL) || (thePixelsVert == NULL)) return(paramErr); *thePixelsHoriz = 0; *thePixelsVert = 0; myErr = BeginFullScreen(&myDummyPtr, NULL, thePixelsHoriz, thePixelsVert, NULL, NULL, fullScreenPreflightSize); return(myErr); }
Notice that we need to pass the address of a variable of type Ptr as the first parameter to BeginFullScreen, even though it does not return an actual pointer in that location. If we look at the value of myDummyPtr after calling BeginFullScreen here, we'll see that it's still NULL. So there is no need to call EndFullScreen to dispose of that pointer.
We can also use BeginFullScreen to change the screen resolution without hiding the menu bar. We simply pass in the desired height and width, and we set the flags parameter so that the menu bar is not hidden. In this case, we need to pass the value NULL for the newWindow parameter, indicating that we don't want a new window to be created. Here's how we could set the main screen to a resolution of 800 by 600:
myHorizPixels = 800; myVertPixels = 600; myErr = BeginFullScreen(&gRestoreState, NULL, &myHorizPixels, &myVertPixels, NULL, NULL, fullScreenDontChangeMenuBar);
We need to keep track of the restore state so that we can undo the resolution change at some future time. You should keep in mind that there is no way to suppress the hiding of the control strip (or dock or taskbar) when you call BeginFullScreen. If you want the control strip (or dock or taskbar) to remain visible after you adjust the resolution, you'll need to programmatically reshow it. (For instance, on Windows you could call the QTML function ShowHideTaskBar to reshow the taskbar.) It would be nice if QuickTime defined a flag that would allow us to request that the control strip (or its ilk) remain visible after a call to BeginFullScreen.
Scaling the Movie
In QTBigScreen, we don't want the resolution of the main screen to be changed when we play a movie fullscreen. We can accomplish this by scaling the movie so that the requested size is large enough to occupy all or almost all of the main screen at its current resolution. So we're going to end up calling BeginFullScreen twice, once to get the current resolution (as we did in Listing 1) and again to put a movie window into fullscreen mode. But before we call BeginFullScreen the second time, we need to do a little mathematics to figure out how to scale the movie so that it retains its original aspect ratio but fills as much of the screen as possible.
We begin by calling BeginFullScreen to get the current resolution of the screen:
short myScreenWidth = 0; short myScreenHeight = 0; myErr = BeginFullScreen(&(**myAppData).fRestoreState, NULL, &myScreenWidth, &myScreenHeight, NULL, NULL, fullScreenPreflightSize);
As you can see, we are storing the restore state in the application data record. Ultimately we're going to have to maintain about a dozen pieces of data in that record (which we'll encounter shortly). Once we've retrieved the current resolution, let's make a copy of that information:
myOrigScreenHeight = myScreenHeight; myOrigScreenWidth = myScreenWidth;
Now we need to get the natural size of the movie we want to play fullscreen. We can do this by calling GetMovieNaturalBoundsRect:
GetMovieNaturalBoundsRect(myMovie, &myRect); MacOffsetRect(&myRect, -myRect.left, -myRect.top); myMovieWidth = myRect.right; myMovieHeight = myRect.bottom;
(Calling GetMovieBox here wouldn't work quite right, since the user might have resized the movie window before putting it into fullscreen mode.)
And now we can calculate the aspect ratios of the screen and the movie:
myMovieRatio = FixRatio(myMovieWidth, myMovieHeight); myScreenRatio = FixRatio(myScreenWidth, myScreenHeight);
We use these ratios to determine which dimension of the movie should be scaled to completely fill the corresponding dimension of the screen. The math required is simple:
if (myMovieRatio > myScreenRatio) { myMovieHeight = (myScreenWidth * myMovieHeight) / myMovieWidth; myMovieWidth = myScreenWidth; } else { myMovieWidth = (myScreenHeight * myMovieWidth) / myMovieHeight; myMovieHeight = myScreenHeight; }
At this point, we know the desired size of the movie and hence we can call BeginFullScreen once again:
myScreenWidth = myMovieWidth; myScreenHeight = myMovieHeight; myErr = BeginFullScreen(&(**myAppData).fRestoreState, NULL, &myScreenWidth, &myScreenHeight, &(**myAppData).fFullScreenWindow, &myColor, fullScreenAllowEvents);
If BeginFullScreen returns successfully, then (**myAppData).fFullScreenWindow contains a window pointer to the new fullscreen window and myScreenWidth and myScreenHeight point to the actual width and height of that window. If the aspect ratio of the movie does not exactly match that of the screen, then we need to move the movie down or to the right so that it is centered in the fullscreen window (see Figure 1 again). First we set the movie's rectangle:
MacSetRect(&myRect, 0, 0, myMovieWidth, myMovieHeight);
And then we nudge the movie down or to the right to center it on the screen:
MacOffsetRect(&myRect, (myScreenWidth - myMovieWidth) / 2, (myScreenHeight - myMovieHeight) / 2); SetMovieBox(myMovie, &myRect);
There is one final "gotcha" we need to watch out for. Although we scaled the movie up so that one of its dimensions extends for the full width or height of the screen, it's possible that the expanded movie size exactly matches a screen resolution that is not the same as the original screen resolution. Suppose, for instance, that our movie has a natural size of 480 by 360 pixels. This movie, when scaled up, will exactly fill a screen that is 1024 by 768. However, the "megawide" screen on the Titanium PowerBook has a natural resolution of 1152 by 768, and it also supports the resolution of 1024 by 768 (by blanking 64 pixels on the left and right sides of the screen). If we play this movie fullscreen on that PowerBook, our existing code will result in the screen resolution being changed to 1024 by 768; in that case, there is no need to nudge the movie down or to the right.
So before we adjust the movie's rectangle, we should check to see whether the screen resolution did in fact change. We cleverly saved the original screen resolution, and our second call to BeginFullScreen gives us back the current screen resolution; we can compare the current with the original and then nudge the movie only if the resolution has not changed:
if ((myScreenWidth == myOrigScreenWidth) && (myScreenHeight == myOrigScreenHeight)) MacOffsetRect(&myRect, (myScreenWidth - myMovieWidth) / 2, (myScreenHeight - myMovieHeight) / 2);
Once again, it might be nice if BeginFullScreen supported a flag that instructed it not to change the screen resolution.
The Practice
So, we now understand how to use BeginFullScreen and EndFullScreen to enter and exit fullscreen mode. We've seen how to request a fullscreen window size that makes our movie as large as possible while preserving its original aspect ratio and also preventing changes in the screen resolution (whenever possible). And we've seen how to adjust the movie box — the rectangle in which the movie is drawn inside its window — so that the movie is nicely centered in the fullscreen window. Aren't we done yet?
No. There are still a couple of issues we need to address. Our basic sample application framework was not developed with the intention of supporting fullscreen movie playback, so a couple of our framework functions need some minor tweaking. More importantly, we need to keep track of some information (including the restore information and the new window pointer) for each movie window we put into fullscreen mode. And of course, we need to make sure that events get passed to a movie playing fullscreen. In this section we'll tackle these issues.
Initializing the Movie Window Data
Ideally, we'd like the user to be able to put any of our application's movie windows into fullscreen mode and then back into normal mode. We therefore need to keep track of the restore state information and the new window pointer returned by BeginFullScreen, as well as a few other pieces of information. We'll define a custom application data record, like this:
typedef struct ApplicationDataRecord { WindowReference fOrigWindow; WindowPtr fFullScreenWindow; Ptr fRestoreState; GWorldPtr fOrigMovieGWorld; Rect fOrigMovieRect; Rect fOrigControllerRect; Boolean fOrigControllerVis; Boolean fOrigControllerAttached; QTCallBack fCallBack; QTCallBackUPP fCallBackUPP; Boolean fEndFullscreenNeeded; Boolean fDestroyWindowNeeded; } ApplicationDataRecord, *ApplicationDataPtr, **ApplicationDataHdl;
The fOrigWindow field will contain the original window reference — that is, a reference to the window that the user puts into fullscreen mode. (Remember that the actual type of this object varies; on Macintosh systems, it's of type WindowPtr, while on Windows systems it's of type HWND.) The fRestoreState and fFullScreenWindow fields hold the restore state and window pointer returned by BeginFullScreen. The next five fields hold information about the state of the movie window at the time it's put into fullscreen mode; for instance, the fOrigMovieRect field holds the movie rectangle, and fOrigControllerVis indicates whether the controller bar was visible when we called BeginFullScreen. We'll need these pieces of information when we restore the movie to its normal windowed state.
The fCallBack and fCallBackUPP fields hold information related to a time base callback function; we use this function to automatically return a movie from fullscreen state to normal state when the movie reaches the end, to mimic the behavior of QuickTime Player. (I personally don't like this behavior, but it's useful to know how to implement it. I've conditionalized the callback code using the compiler macro END_FULLSCREEN_AT_MOVIE_END, so it's easy enough to turn off.)
The last two fields of our application data structure are Boolean values that indicate whether the associated movie window should be returned from fullscreen mode to normal mode and whether the associated movie window should be closed. Later we'll see why we need these fields.
I'm also going to introduce a global variable that holds the window object for the window that is currently in fullscreen mode:
WindowObject gFullScreenWindowObject = NULL;
It would be possible to determine which, if any, movie window is in fullscreen mode by iterating through all open movie windows and checking their application data record (to see if fFullScreenWindow is non-NULL), but using a global variable will simplify our code.
When we first open a movie window, we shall call QTBig_InitWindowData (Listing 2) to create this custom application data record and initialize its fields.
Listing 2: Initializing application-specific window data
ApplicationDataHdl QTBig_InitWindowData (WindowObject theWindowObject) { ApplicationDataHdl myAppData = NULL; // if we already have some window data, dump it myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); if (myAppData != NULL) QTBig_DumpWindowData(theWindowObject); // allocate a new application data handle myAppData = (ApplicationDataHdl) NewHandleClear(sizeof(ApplicationDataRecord)); return(myAppData); }
And when we close a movie window, we want to deallocate the application data record. For this, we call the function QTBig_DumpWindowData, defined in Listing 3. Notice that we first check to see whether the window is in fullscreen mode; if it is, we call our function QTBig_StopFullscreen to return it to normal mode.
Listing 3: Destroying application-specific window data
void QTBig_DumpWindowData (WindowObject theWindowObject) { ApplicationDataHdl myAppData = NULL; myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); if (myAppData != NULL) { if ((**myAppData).fFullScreenWindow != NULL) QTBig_StopFullscreen(theWindowObject); DisposeHandle((Handle)myAppData); (**theWindowObject).fAppData = NULL; } }
Entering Fullscreen Mode
Let's revisit for a moment our basic scheme for opening movie windows and keeping track of window-specific data. On Macintosh systems, we call NewCWindow to obtain a window pointer to a new window. We allocate a new window object record and store a handle to that record as the window reference constant (by calling SetWRefCon). This allows us to retrieve our window-specific data (the movie being displayed in the window, the movie controller associated with the movie, and so forth) if we are given the window pointer. On Windows operating systems, we call CreateWindowEx to create a new window and SetWindowLong to store a handle to the window record in the window's data. (See "QuickTime 101" in MacTech, January 2000 for a more complete account of all this.)
Now, our entire application framework assumes that each window displayed by our application is associated with a window object record. We need the data in that record to know which movie or image is contained in the window, whether the movie or image has been changed since it was last saved, and similar information. So, when BeginFullScreen returns to us a new fullscreen window, we need to attach a window object to that window. We could, of course, create a new window object and attach it to the window, but things actually work much better if we borrow the window object from the movie window we want to put into fullscreen mode. We'll do that like this:
#if TARGET_OS_MAC SetWRefCon((**myAppData).fFullScreenWindow, (long)theWindowObject); #endif #if TARGET_OS_WIN32 SetWindowLong(GetPortNativeWindow( (GrafPtr)(**myAppData).fFullScreenWindow), GWL_USERDATA, (LPARAM)theWindowObject); #endif
The Macintosh code is straightforward: simply set the reference constant of the window created by BeginFullScreen to the window object passed to QTBig_StartFullscreen. The Windows code is a little more involved, since BeginFullScreen returns to us a window pointer (of type WindowPtr) but our Windows application expects a movie window to be of type HWND. Accordingly, we need to call GetPortNativeWindow to get the native window (of type HWND) that is associated with the window pointer. This window was created automatically by QuickTime when it created the fullscreen window.
Keep in mind that we now have two windows to worry about: (1) the original movie window (which is either of type WindowPtr or HWND, depending on the native operating system) and (2) a fullscreen window (which is always of type WindowPtr but which on Windows is also associated with a window of type HWND). We are attaching the window object associated with the original movie window to the fullscreen window; this lets us know which movie to play fullscreen and how to control that movie; it's also important to have a window object available when we want to pass events to the fullscreen movie (as we'll see shortly).
It seems reasonable that once we've created the fullscreen window, we should hide the original movie window; after all, we won't be able to redraw the original movie window or let the user move it around. We can hide the movie window like this:
QTFrame_SetWindowVisState(theWindowObject, false);
The QTFrame_SetWindowVisState function is a new function that we need to add to our framework; it's defined in Listing 4.
Listing 4: Showing or hiding a movie window
void QTFrame_SetWindowVisState (WindowObject theWindowObject, Boolean theState) { // make sure we have a non-NULL window object and window if (theWindowObject == NULL) return; if ((**theWindowObject).fWindow == NULL) return; // set the visibility state of the window #if TARGET_OS_MAC if (theState) MacShowWindow((**theWindowObject).fWindow); else HideWindow((**theWindowObject).fWindow); #endif #if TARGET_OS_WIN32 ShowWindow((**theWindowObject).fWindow, theState); #endif }
(It's interesting to note that QuickTime Player does not hide the original movie window after putting it into fullscreen mode. What's up with that?)
There is one final set of tasks we need to attend to when putting a movie window into fullscreen mode. As we've seen, we are using the same movie and movie controller in the new fullscreen window that we use in the original movie window. But of course the fullscreen window has a different graphics world and a different size. So we need to save the graphics world, size, and a few other pieces of information before we go into fullscreen mode and later restore them when we return to normal mode. QTBig_StartFullscreen contains these lines to save the relevant information in our application data record:
GetMovieGWorld(myMovie, &(**myAppData).fOrigMovieGWorld, NULL); GetMovieBox(myMovie, &(**myAppData).fOrigMovieRect); MCGetControllerBoundsRect(myMC, &(**myAppData).fOrigControllerRect); (**myAppData).fOrigControllerVis = MCGetVisible(myMC); (**myAppData).fOrigControllerAttached = MCIsControllerAttached(myMC); (**myAppData).fOrigWindow = (**theWindowObject).fWindow;
Once we've successfully entered fullscreen mode, we need to set the movie graphics world, movie controller bounds, and so forth for the new fullscreen window:
SetGWorld(GetWindowPort((**myAppData).fFullScreenWindow), NULL); SetMovieGWorld(myMovie, GetWindowPort((**myAppData).fFullScreenWindow), NULL); MCSetControllerPort(myMC, GetWindowPort((**myAppData).fFullScreenWindow)); MCSetControllerAttached(myMC, false); MCSetControllerBoundsRect(myMC, &myRect); MCSetVisible(myMC, false); MCActivate(myMC, (**myAppData).fFullScreenWindow, true);
We also need to set the fWindow field of the window object record to the new fullscreen window:
(**theWindowObject).fWindow = QTFrame_GetWindowReferenceFromWindow( (**myAppData).fFullScreenWindow);
And we want to keep track of the fullscreen window in a global variable:
gFullScreenWindowObject = theWindowObject;
We are finally ready to take a look at the complete definition of QTBig_StartFullscreen, shown in Listing 5.
Listing 5: Entering fullscreen mode
OSErr QTBig_StartFullscreen (WindowObject theWindowObject) { MovieController myMC = NULL; Movie myMovie = NULL; ApplicationDataHdl myAppData = NULL; long myFlags = fullScreenAllowEvents; OSErr myErr = paramErr; if (theWindowObject == NULL) goto bail; myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); myMovie = (**theWindowObject).fMovie; myMC = (**theWindowObject).fController; if ((myAppData == NULL) || (myMovie == NULL) || (myMC == NULL)) goto bail; if ((**myAppData).fFullScreenWindow == NULL) { short myOrigScreenWidth = 0; short myOrigScreenHeight = 0; short myNewScreenWidth = 0; short myNewScreenHeight = 0; short myScreenWidth = 0; short myScreenHeight = 0; short myMovieWidth = 0; short myMovieHeight = 0; Fixed myScreenRatio; Fixed myMovieRatio; Rect myRect; RGBColor myColor = {0x0000, 0x0000, 0x0000};// black // remember some of the current state GetMovieGWorld(myMovie, &(**myAppData).fOrigMovieGWorld, NULL); GetMovieBox(myMovie, &(**myAppData).fOrigMovieRect); MCGetControllerBoundsRect(myMC, &(**myAppData).fOrigControllerRect); (**myAppData).fOrigControllerVis = MCGetVisible(myMC); (**myAppData).fOrigControllerAttached = MCIsControllerAttached(myMC); (**myAppData).fOrigWindow = (**theWindowObject).fWindow; // get the current screen resolution myErr = BeginFullScreen(&(**myAppData).fRestoreState, NULL, &myScreenWidth, &myScreenHeight, NULL, NULL, fullScreenPreflightSize); if (myErr != noErr) goto bail; // keep track of the original screen resolution myOrigScreenHeight = myScreenHeight; myOrigScreenWidth = myScreenWidth; // calculate the destination rectangle GetMovieNaturalBoundsRect(myMovie, &myRect); MacOffsetRect(&myRect, -myRect.left, -myRect.top); myMovieWidth = myRect.right; myMovieHeight = myRect.bottom; myMovieRatio = FixRatio(myMovieWidth, myMovieHeight); myScreenRatio = FixRatio(myScreenWidth, myScreenHeight); // scale the movie rectangle to fit the screen ratio if (myMovieRatio > myScreenRatio) { myMovieHeight = (myScreenWidth * myMovieHeight) / myMovieWidth; myMovieWidth = myScreenWidth; } else { myMovieWidth = (myScreenHeight * myMovieWidth) / myMovieHeight; myMovieHeight = myScreenHeight; } MacSetRect(&myRect, 0, 0, myMovieWidth, myMovieHeight); myScreenWidth = myMovieWidth; myScreenHeight = myMovieHeight; // begin full-screen display myErr = BeginFullScreen(&(**myAppData).fRestoreState, NULL, &myScreenWidth, &myScreenHeight, &(**myAppData).fFullScreenWindow, &myColor, myFlags); if (myErr != noErr) goto bail; // determine whether the resolution changed; if it has changed, we must have // passed in a supported resolution, so we want the movie to fill the screen; // otherwise, we need to offset the movie to center it in the screen if ((myScreenWidth == myOrigScreenWidth) && (myScreenHeight == myOrigScreenHeight)) MacOffsetRect(&myRect, (myScreenWidth - myMovieWidth) / 2, (myScreenHeight - myMovieHeight) / 2); #if TARGET_OS_WIN32 // on Windows, set a window procedure for the new window QTMLSetWindowWndProc((**myAppData).fFullScreenWindow, QTBig_HandleMessages); #endif // hide the original window QTFrame_SetWindowVisState(theWindowObject, false); // attach the existing window object to the new window #if TARGET_OS_MAC SetWRefCon((**myAppData).fFullScreenWindow, (long)theWindowObject); #endif #if TARGET_OS_WIN32 SetWindowLong(GetPortNativeWindow( (GrafPtr)(**myAppData).fFullScreenWindow), GWL_USERDATA, (LPARAM)theWindowObject); #endif // set the movie and movie controller state to the new window and rectangle SetGWorld(GetWindowPort((**myAppData).fFullScreenWindow), NULL); SetMovieGWorld(myMovie, GetWindowPort((**myAppData).fFullScreenWindow), NULL); MCSetControllerPort(myMC, GetWindowPort((**myAppData).fFullScreenWindow)); MCSetControllerAttached(myMC, false); MCSetControllerBoundsRect(myMC, &myRect); SetMovieBox(myMovie, &myRect); MCSetVisible(myMC, false); MCActivate(myMC, (**myAppData).fFullScreenWindow, true); (**theWindowObject).fWindow = QTFrame_GetWindowReferenceFromWindow( (**myAppData).fFullScreenWindow); #if TARGET_API_MAC_CARBON HiliteWindow((**myAppData).fFullScreenWindow, true); #endif #if END_FULLSCREEN_AT_MOVIE_END // install a callback procedure to return linear, non-looping movies // to normal mode at the end of the movie if (QTBig_MovieIsStoppable(myMC)) QTBig_InstallCallBack(theWindowObject); #endif } gFullScreenWindowObject = theWindowObject; bail: return(myErr); }
You'll notice a few lines of code we haven't discussed yet. The Windows-only call to QTMLSetWindowWndProc is necessary to handle messages targeted at the fullscreen window, as we'll see in the next section. The Carbon-only call to HiliteWindow ensures that the window returned by BeginFullScreen is highlighted. (Apparently, in some circumstances, that window is occasionally unhighlighted, which blocks any interactivity in the movie.) And the code selected by the END_FULLSCREEN_AT_MOVIE_END flag installs a time base callback function that returns a non-interactive, non-looping movie to normal mode when the movie reaches its end. We'll delve into that issue toward the end of this article.
Handling Events for the Fullscreen Window
Now we've created a fullscreen window, attached a movie and movie controller to it, and positioned the movie box so that the movie appears centered in the fullscreen window. We've also set the movie and movie controller graphics ports to the fullscreen window and hidden the original movie window. So things are looking great. We'd also like them to start acting great. That is, we want the fullscreen movie to respond in the normal ways to mouse movements, mouse clicks, and key presses, and we want it to jump out of fullscreen mode if the user presses the Escape key.
This Escape key behavior is quite easy to implement. Our application framework contains a stub function QTApp_HandleKeyPress that we can use to intercept key presses. In QTBigScreen, QTApp_HandleKeyPress is defined as shown in Listing 6. As you can see, we look for presses on the Escape key while a fullscreen window is active; if we find one, we call QTBig_StopFullscreen.
Listing 6: Handling key presses
Boolean QTApp_HandleKeyPress (char theCharCode) { Boolean isHandled = false; switch (theCharCode) { // Escape key during fullscreen display means to restore the window to its original // state case kEscapeCharCode: if (gFullScreenWindowObject != NULL) isHandled = (QTBig_StopFullscreen (gFullScreenWindowObject) == noErr); break; } return(isHandled); }
All the other important behaviors we care about are provided by the movie controller. So we need to make sure that we call MCIsPlayerEvent for any events our application receives while there is a fullscreen movie. Again we'll insert some code into another application function, QTApp_HandleEvent:
if ((**myAppData).fFullScreenWindow != NULL) return(MCIsPlayerEvent((**myWindowObject).fController, theEvent));
On the Macintosh, this is pretty much all we need to do to make the fullscreen movie behave as expected. That's because our Mac framework calls QTApp_HandleEvent for every event it receives, and it calls QTApp_HandleKeyPress on every key event. On Windows, things are a bit trickier. Our Windows framework calls these functions only for MDI child movies — that is, windows using the QTFrame_MovieWndProc window procedure. But the fullscreen window is not an MDI child window. The easy solution here is to define a custom window procedure for the fullscreen window that calls QTApp_HandleEvent and QTApp_HandleKeyPress at the appropriate times. (We install this window procedure by calling QTMLSetWindowWndProc, as you saw in Listing 5.) Listing 7 shows our definition of QTBig_HandleMessages.
Listing 7: Handling fullscreen window messages
LRESULT CALLBACK QTBig_HandleMessages (HWND theWnd, UINT theMessage, UINT wParam, LONG lParam) { MovieController myMC = NULL; Movie myMovie = NULL; WindowObject myWindowObject = NULL; MSG myMsg = {0}; EventRecord myMacEvent; LONG myPoints = GetMessagePos(); BOOL myIsHandled = false; if (gFullScreenWindowObject == NULL) goto bail; // make sure we don't get called while the movie is returning to normal state if (gEndingFullScreen) goto bail; // get the window object, movie, and movie controller for this window myWindowObject = gFullScreenWindowObject; myMC = (**myWindowObject).fController; myMovie = (**myWindowObject).fMovie; // give the movie controller this message first if (myMC != NULL) { LONG myPoints = GetMessagePos(); myMsg.hwnd = theWnd; myMsg.message = theMessage; myMsg.wParam = wParam; myMsg.lParam = lParam; myMsg.time = GetMessageTime(); myMsg.pt.x = LOWORD(myPoints); myMsg.pt.y = HIWORD(myPoints); // 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); } switch (theMessage) { case WM_CHAR: // do any application-specific key press handling myIsHandled = QTApp_HandleKeyPress((char)wParam); break; } bail: return(DefWindowProc(theWnd, theMessage, wParam, lParam)); }
There's nothing too extravagant here. Notice, however, that we look at the gEndingFullScreen global variable to see whether we got this message during a call to EndFullScreen. If so, we don't want to pass the event to the movie controller.
We need to make a couple of other small changes to get everything working satisfactorily, now that our application has to support fullscreen windows. For instance, in the function QTFrame_SizeWindowToMovie, we use code like this to get the size of a movie that is associated with a movie controller:
if (myMC != NULL) if (MCGetVisible(myMC)) MCGetControllerBoundsRect(myMC, &myRect);
This assumes that the controller is attached to the movie, which is always true for our movie windows but false for our fullscreen movie. So we need to revise that code like so:
if (myMC != NULL) if (MCGetVisible(myMC)) if (MCIsControllerAttached(myMC) MCGetControllerBoundsRect(myMC, &myRect);
Exiting Fullscreen Mode
Returning from fullscreen mode to normal windowed mode is a breeze. The main step is of course to call EndFullScreen. To prevent QTBig_HandleMessages from being called while EndFullScreen is executing, we bracket our call with setting and unsetting the gEndingFullScreen global variable:
gEndingFullScreen = true; myErr = EndFullScreen((**myAppData).fRestoreState, 0L); gEndingFullScreen = false;
We also need to restore the settings we saved previously, such as the movie and movie controller graphics ports and the visibility state of the controller bar. It's all pretty much what you'd expect (Listing 8).
Listing 8: Returning to normal window mode
OSErr QTBig_StopFullscreen (WindowObject theWindowObject) { ApplicationDataHdl myAppData = NULL; OSErr myErr = paramErr; if (theWindowObject == NULL) goto bail; myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); if (myAppData != NULL) { if ((**myAppData).fFullScreenWindow != NULL) { // restore the original settings (**theWindowObject).fWindow = (**myAppData).fOrigWindow; MacSetPort(QTFrame_GetPortFromWindowReference( (**theWindowObject).fWindow)); SetMovieGWorld((**theWindowObject).fMovie, (CGrafPtr)(**myAppData).fOrigMovieGWorld, GetGWorldDevice( (CGrafPtr)(**myAppData).fOrigMovieGWorld)); SetMovieBox((**theWindowObject).fMovie, &(**myAppData).fOrigMovieRect); MCSetControllerPort((**theWindowObject).fController, (CGrafPtr)(**myAppData).fOrigMovieGWorld); MCSetControllerAttached( (**theWindowObject).fController, (**myAppData).fOrigControllerAttached); MCSetVisible((**theWindowObject).fController, (**myAppData).fOrigControllerVis); MCSetControllerBoundsRect( (**theWindowObject).fController, &(**myAppData).fOrigControllerRect); gEndingFullScreen = true; // end fullscreen playback myErr = EndFullScreen((**myAppData).fRestoreState, 0L); gEndingFullScreen = false; // empty out the data structures and global variables (**myAppData).fOrigWindow = NULL; (**myAppData).fFullScreenWindow = NULL; (**myAppData).fRestoreState = NULL; (**myAppData).fOrigMovieGWorld = NULL; gFullScreenWindowObject = NULL; #if END_FULLSCREEN_AT_MOVIE_END // dispose of the CallMeWhen callback and the callback UPP if ((**myAppData).fCallBack != NULL) DisposeCallBack((**myAppData).fCallBack); if ((**myAppData).fCallBackUPP != NULL) DisposeQTCallBackUPP((**myAppData).fCallBackUPP); (**myAppData).fCallBack = NULL; (**myAppData).fCallBackUPP = NULL; #endif // make sure the movie window is the correct size and then show it again QTFrame_SizeWindowToMovie(theWindowObject); QTFrame_SetWindowVisState(theWindowObject, true); } } bail: return(myErr); }
For now, don't worry about the code in the END_FULLSCREEN_AT_MOVIE_END block; we'll discuss that a bit later.
Flash Application Messages
In a recent article ("The Flash", cited earlier), we learned that Flash movies can send messages to the playback application requesting that it perform specific actions such as quitting or launching some other application. Of the five standard Flash application messages, or FSCommands, one is relevant to our current concern: fullscreen. This command can have one of two arguments, either true or false. If the argument is true, then the playback application should put the window containing the Flash movie into fullscreen mode; if it's false, then the application should put the window into normal mode. We are now able to upgrade the sample application we built in that article, QTFlash, to support the fullscreen application message.
QuickTime's Flash media handler intercepts Flash application messages and repackages them into movie controller actions of type mcActionDoScript, which are then sent to the application's movie controller action filter function. In QTFlash, we responded to those actions by calling the application function QTFlash_DoFSCommand, like so:
case mcActionDoScript: isHandled = QTFlash_DoFSCommand(theMC, (QTDoScriptPtr)theParams, myWindowObject); break;
QTFlash_DoFSCommand, in turn, inspected the command and argument fields of the specified QTDoScriptRecord to determine which command to perform. We can now add the lines in Listing 9 to support entering and exiting fullscreen mode:
Listing 9: Handling the fullscreen application message
if (strcmp(theScriptPtr->command, “fullscreen”) == 0) { if (strcmp(theScriptPtr->arguments, “true”) == 0) QTBig_StartFullscreen(theWindowObject); if (strcmp(theScriptPtr->arguments, “false”) == 0) QTBig_StopFullscreen(theWindowObject); isHandled = true; }
As you can see, we call our new functions QTBig_StartFullscreen and QTBig_StopFullscreen. Of course we also need to add to QTFlash all the additional code in QTBigScreen that handles fullscreen mode (such as the code in QTApp_HandleEvent).
QuickTime Application Messages
Interestingly, QuickTime 5.0.1 introduced a mechanism for sending messages from a QuickTime movie to the playback application that is strongly reminiscent of the FSCommand capability in Flash movies. These QuickTime application messages provide a way for a movie to request fullscreen mode for the window containing the movie, close the window containing the movie, and perform several other tasks.
The standard way to send an application message from a QuickTime movie to the playback application is by using the kActionSendAppMessage wired action. This action requires one parameter, which is of type long and which specifies the message to send to the application. Currently QuickTime defines five public application messages (in the file Movies.h):
enum { kQTAppMessageSoftwareChanged = 1, kQTAppMessageWindowCloseRequested = 3, kQTAppMessageExitFullScreenRequested = 4, kQTAppMessageDisplayChannels = 5, kQTAppMessageEnterFullScreenRequested = 6 };
The kQTAppMessageSoftwareChanged message indicates that some part of the QuickTime software has been updated, and the kQTAppMessageDisplayChannels message requests that the QuickTime Player application display its user interface for selecting one of the QuickTime channels; neither of these messages is relevant to our sample applications and we shall blithely ignore them. The kQTAppMessageEnterFullScreenRequested and kQTAppMessageExitFullScreenRequested messages request that the playback application put the associated movie window into fullscreen or normal mode, and the kQTAppMessageWindowCloseRequested message requests that the playback application close the window containing the movie.
We insert one of these application messages into a movie by inserting a wired action in the standard manner. For instance, Listing 10 shows how we can make a mouse click on a sprite close the movie that contains it.
Listing 10: Adding a window-close request to a sprite
myErr = QTInsertChild(mySpriteAtom, kParentAtomIsContainer, kQTEventType, kQTEventMouseClickEndTriggerButton, 1, 0, NULL, &myEventAtom); if (myErr != noErr) goto bail; // add an action atom to the event handler myErr = QTInsertChild(mySpriteAtom, myEventAtom, kAction, 0, 0, 0, NULL, &myActionAtom); if (myErr != noErr) goto bail; myAction = EndianU32_NtoB(kActionSendAppMessage); myErr = QTInsertChild(mySpriteAtom, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); if (myErr != noErr) goto bail; // add a parameter atom to specify which action to perform myMsg = EndianU32_NtoB(kQTAppMessageWindowCloseRequested); myErr = QTInsertChild(mySpriteAtom, myActionAtom, kActionParameter, 0, 1, sizeof(myMsg), &myMsg, NULL);
We add an event atom whose atom ID is kQTEventMouseClickEndTriggerButton to the sprite atom (mySpriteAtom). Then we insert an action atom into that event atom. The action atom is given two children, one of type kWhichAction whose atom data is kActionSendAppMessage, and one of type kActionParameter whose atom data is kQTAppMessageWindowCloseRequested.
Keep in mind that a movie controller cannot handle these application messages by itself. The actions requested here (entering or exiting fullscreen mode, and closing the window containing a movie) require assistance from the playback application. Accordingly, the movie controller informs the application of the request by sending it a movie controller action of type mcActionAppMessageReceived. The application is free to act on that request or ignore it entirely. In this section, we'll see how to handle the fullscreen and close-window application messages.
Handling Fullscreen Messages
It's fairly simple to add support for the kQTAppMessageEnterFullScreenRequested and kQTAppMessageExitFullScreenRequested application messages to QTBigScreen. Listing 11 shows the code we'll add to our movie controller action filter procedure to handle these messages.
Listing 11: Handling application messages
case mcActionAppMessageReceived: switch ((long)theParams) { case kQTAppMessageEnterFullScreenRequested: QTBig_StartFullscreen(myWindowObject); isHandled = true; break; case kQTAppMessageExitFullScreenRequested: QTBig_StopFullscreen(myWindowObject); isHandled = true; break; } break;
Handling Close-Window Messages
Now let's see how to handle the kQTAppMessageWindowCloseRequested application message. It's tempting perhaps to handle this message by adding these few lines of code to the switch statement in Listing 11:
case kQTAppMessageWindowCloseRequested: QTFrame_DestroyMovieWindow((**myWindowObject).fWindow); isHandled = true; break;
Here, we simply call the framework function QTFrame_DestroyMovieWindow to close the window associated with the specified movie. This function checks to see whether the movie has been changed since it was opened or last saved and, if so, prompts the user to save or discard any changes; once the changes have been saved or discarded, QTFrame_DestroyMovieWindow calls QTFrame_CloseWindowObject to close the movie file and dispose of the movie and movie controller. Then QTFrame_DestroyMovieWindow calls DisposeWindow (on Macintosh systems) or SendMessage with the WM_MDIDESTROY message (on Windows systems) to actually close and destroy the movie window.
There is a problem with this simple approach, however. As noted, QTFrame_CloseWindowObject disposes of the movie controller associated with the movie, and experience tells me that this is definitely a bad thing to do inside of a movie controller action filter procedure. So we need to adopt a different approach: in response to the kQTAppMessageWindowCloseRequested message, we'll set a flag in our application data record that indicates that we need to destroy the window object. Listing 12 shows the code we'll add.
Listing 12: Handling close-window messages
case kQTAppMessageWindowCloseRequested: myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(myWindowObject); if (myAppData != NULL) (**myAppData).fDestroyWindowNeeded = true; isHandled = true; break;
Later, in the QTApp_HandleEvent function, we'll need to cycle through all open movie windows to see whether any of them needs to be closed. We'll add a while loop, as shown in Listing 13.
Listing 13: Looking for windows to close
myWindow = QTFrame_GetFrontMovieWindow(); while (myWindow != NULL) { myNextWindow = QTFrame_GetNextMovieWindow(myWindow); myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindow(myWindow); if (myAppData != NULL) { if ((**myAppData).fDestroyWindowNeeded) { (**myAppData).fDestroyWindowNeeded = false; QTFrame_DestroyMovieWindow(myWindow); } } myWindow = myNextWindow; }
Presentation Movie User Data
A QuickTime movie file can contain a piece of movie user data of type ‘ptv ' that specifies that the movie should be presented — that is, displayed at a certain size against a solid black background. When a movie is presented, it's drawn without the normal window frame or controller bar. Most often, this feature is used to put a movie into fullscreen mode automatically when the movie file is opened. QuickTime Player looks for and interprets this user data item, and in this section we'll see how to handle it in QTBigScreen (at least in part).
The data contained in a ‘ptv ' user data item occupies 8 bytes; we can exhibit the structure of this data using a QTPFSDataRec structure, defined like this:
typedef struct { UInt16 fSize; UInt16 fUnused1; UInt16 fUnused2; Boolean fPlaySlideShow; Boolean fPlayOnOpen; } QTPFSDataRec;
This structure is defined in our file QTBigScreen.h; there is no ‘ptv ' item structure defined in any of the public QuickTime header files. (By the way, "ptv" stands for "print to video"; this is because the presenting capability was intended also to provide a non-windowed version of a movie that could be written out to videotape.)
The fSize field contains a 16-bit integer that specifies the desired size of the presented movie. The value 0 indicates that the movie should be presented at its normal size. The value 1 indicates that the movie should be presented at twice its normal size. The value 2 indicates that the movie should be presented at half its normal size. The value 3 indicates that the movie should be presented fullscreen. QuickTime Player calls BeginFullScreen with the appropriate width and height to present the movie. As we've seen, however, this can cause the resolution of the screen to be changed, in which case the actual size of the presented movie might not be what we would expect. To force QuickTime Player to present the movie at its normal size, we can specify the value 4. We'll define these constants for the fSize field:
enum { kSizeNormal = 0, kSizeDouble = 1, kSizeHalf = 2, kSizeFullScreen = 3, kSizeCurrent = 4 };
The values of these constants appear to derive from the order of the items in the Movie Size pop-up menu in the dialog box displayed by QuickTime Player when the user selects the "Present Movie..." item in the Movie menu (shown in Figure 3).
Figure 3: QuickTime Player's Present Movie dialog box
The fPlaySlideShow field contains a Boolean value that indicates whether the movie should be played in slide-show mode; in slide-show mode, the movie advances to another frame only when the user presses the right-arrow or left-arrow key (which move the movie forward one frame or backward one frame, respectively). Any sounds tracks in the movie are ignored during slide-show mode. Finally, the fPlayOnOpen field contains a Boolean value that indicates whether the movie should begin playing automatically when the movie is presented.
Listing 14 defines a function that we can use to add a ‘ptv ' user data item to a movie.
Listing 14: Adding a play-fullscreen item to a movie
#define kPTVItemType FOUR_CHAR_CODE(‘ptv ‘) OSErr QTBig_AddPTVItemToMovie (Movie theMovie) { UserData myUserData = NULL; short myCount = 0; QTPFSDataRec myPFSData; OSErr myErr = noErr; // get the movie's user data list myUserData = GetMovieUserData(theMovie); if (myUserData == NULL) return(paramErr); // we want to end up with at most one user data item of type ‘ptv ‘, // so let's remove any existing ones myCount = CountUserDataType(myUserData, kPTVItemType); while (myCount—) RemoveUserData(myUserData, kPTVItemType, 1); // add a new user data item of type ‘ptv ‘ myPFSData.fSize = EndianU16_NtoB(kSizeFullScreen); myPFSData.fUnused1 = 0; myPFSData.fUnused2 = 0; myPFSData.fPlaySlideShow = false; myPFSData.fPlayOnOpen = true; myErr = SetUserDataItem(myUserData, &myPFSData, sizeof(myPFSData), kPTVItemType, 0); return(myErr); }
QTBig_AddPTVItemToMovie adds to the specified movie a ‘ptv ' user data item that requests that the movie be played back fullscreen and that the movie start playing as soon as it's opened. (It's worth mentioning that a movie can also contain a user data item of type ‘ptvc', whose data is an RGBColor structure that specifies the background color of a presented movie.)
Now, when QTBigScreen opens a movie file that contains a movie user data item of type ‘ptv ' and the value in the fSize field is kSizeFullScreen, we want to call our function QTBig_StartFullscreen to play it fullscreen. We also want to inspect the fPlayOnOpen field to see whether the movie should begin playing immediately. We'll add the lines of code in Listing 15 to our framework function QTFrame_OpenMovieInWindow.
Listing 15: Processing a ‘ptv ' user data item
UserData myUserData = NULL; QTPFSDataRec myRec; OSErr myErr = paramErr; // get the movie's user data list myUserData = GetMovieUserData(myMovie); if (myUserData != NULL) { myErr = GetUserDataItem(myUserData, &myRec, sizeof(myRec), FOUR_CHAR_CODE(‘ptv ‘), 0); if (myErr == noErr) { myRec.fSize = EndianU16_BtoN(myRec.fSize); if (myRec.fSize == kSizeFullScreen) MCDoAction(myMC, mcActionAppMessageReceived, (void *)kQTAppMessageEnterFullScreenRequested); if (myRec.fPlayOnOpen) MCDoAction(myMC, mcActionPrerollAndPlay, (void *)GetMoviePreferredRate(myMovie)); } }
Notice that we don't call QTBig_StartFullscreen directly; instead, we issue a movie controller action of type mcActionAppMessageReceived with the parameter kQTAppMessageEnterFullScreenRequested, which (as we've seen) causes QTBigScreen to call QTBig_StartFullscreen. Notice also that we ignore all size values except kSizeFullScreen; I'll leave it as an exercise for the motivated reader to modify QTBigScreen so that it can present a movie in any of the currently defined sizes.
Time Base Callback Functions
We noted earlier that QuickTime Player automatically returns from fullscreen mode to normal mode when it reaches the end of a non-interactive, non-looped movie. We can achieve this same behavior in QTBigScreen by installing a time base callback function, a function that is executed when a specific time in a movie is reached or when some other event related to the movie's time base occurs.
You may recall from previous articles that a movie's time base controls the direction and speed of movie playback, as well as its looping state and current movie time. A time base (of type TimeBase) is automatically created when we load a movie; we can retrieve a movie's time base at any time by calling the GetMovieTimeBase function. We can attach to a time base one or more callback functions that are triggered when a specific callback event occurs. Currently, five types of callback events are defined; we indicate a specific type of callback event using these constants:
enum { callBackAtTime = 1, callBackAtRate = 2, callBackAtTimeJump = 3, callBackAtExtremes = 4, callBackAtTimeBaseDisposed = 5 };
The callBackAtTime event causes a callback function to be called when a specific time value in the movie is reached (for instance, when the movie reaches the 2 second mark). The callBackAtRate event causes a callback function to be called when the movie begins to play at a specified rate (for instance, when the movie is played at twice the normal speed). The callBackAtTimeJump event causes a callback function to be called when a jump in time occurs; this means that the callback function is called whenever the movie time is set to a time different from what it would be set to under normal movie playback (for instance, if the user clicks in the movie controller bar to select a different movie time, or if a wired action changes the current movie time). The callBackAtTimeBaseDisposed event causes a callback function to be called when the time base is about to be disposed.
For present purposes, we are interested in the callBackAtExtremes event, which occurs at either the beginning or the end of the movie. As we'll see in a moment, we indicate that we want our callback function to be called at the end of the movie by passing a specific value when we activate the time base callback.
In QTBigScreen, we call the QTBig_InstallCallBack function to install a time base callback function to return from fullscreen mode to normal mode at the end of the movie. We add these lines of code to QTBig_StartFullscreen:
#if END_FULLSCREEN_AT_MOVIE_END // install a callback procedure to return linear, non-looping movies // to normal mode at the end of the movie if (QTBig_MovieIsStoppable(myMC)) QTBig_InstallCallBack(theWindowObject); #endif
The definition of QTBig_MovieIsStoppable is quite simple; we just call MCGetControllerInfo to determine whether the movie is looping or interactive, as shown in Listing 16.
Listing 16: Determining whether a movie should be stopped
Boolean QTBig_MovieIsStoppable (MovieController theMC) { long myFlags = 0L; MCGetControllerInfo(theMC, &myFlags); if ((myFlags & mcInfoIsLooping) || (myFlags & mcInfoMovieIsInteractive)) return(false); else return(true); }
Now we need to install a time base callback function and respond when the callback function is triggered.
Installing a Time Base Callback Function
To install a time base callback function, we create a time base callback and then activate it. We create a time base callback by calling the NewCallBack function, like this:
myCallBack = NewCallBack( GetMovieTimeBase((**theWindowObject).fMovie), callBackAtExtremes);
As you can see, the first parameter here is the movie's time base, and the second parameter is a constant that indicates the kind of callback we wish to create. We can set one or both of the two high-order bits in the second parameter to request that our callback function can be called at interrupt time or at deferred task time, using these constants:
enum { callBackAtInterrupt = 0x8000, callBackAtDeferredTask = 0x4000 };
Setting these bits results in more accurate timing, but the callBackAtInterrupt flag requires that the callback function be interrupt-safe (in particular, that it not cause any memory to be allocated or moved). We don't need extremely accurate timing in returning the movie from fullscreen to normal mode, so we'll leave these bits clear when we call NewCallBack.
To activate a time base callback function, we call the CallMeWhen function, which is declared essentially like this:
OSErr CallMeWhen (QTCallBack cb, QTCallBackUPP callBackProc, long refCon, long param1, long param2, long param3);
Here, cb is the callback we created by calling NewCallBack, and callBackProc is a universal procedure pointer for the callback function. The refCon parameter is a reference constant that is passed to the callback function; as you might have guessed, we'll pass the window object for the fullscreen window as the refCon parameter. The last three parameters to CallMeWhen contain additional information required for the callback and vary depending on the type of the callback. For a callback of type callBackAtExtremes, only the param1 parameter is used; it indicates which movie extreme we want to target. We can use these constants to specify a movie extreme:
enum { triggerAtStart = 0x0001, triggerAtStop = 0x0002 };
In QTBigScreen, we want to keep track of the callback and the callback UPP. Accordingly, we'll add a couple of fields to the application data record (defined in the file ComApplication.h):
QTCallBack fCallBack; QTCallBackUPP fCallBackUPP;
Finally we can give the definition of the QTBig_InstallCallBack function (Listing 17). It calls NewCallBack and CallMeWhen, and it stores the callback identifier and the callback UPP in the application data record.
Listing 17: Installing a time base callback function
void QTBig_InstallCallBack (WindowObject theWindowObject) { ApplicationDataHdl myAppData = NULL; QTCallBack myCallBack = NULL; if (theWindowObject == NULL) return; if ((**theWindowObject).fMovie == NULL) return; myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); if (myAppData == NULL) return; myCallBack = NewCallBack( GetMovieTimeBase((**theWindowObject).fMovie), callBackAtExtremes); if (myCallBack != NULL) { (**myAppData).fCallBack = myCallBack; (**myAppData).fCallBackUPP = NewQTCallBackUPP(QTBig_FullscreenCallBack); CallMeWhen(myCallBack, (**myAppData).fCallBackUPP, (long)theWindowObject, triggerAtStop, 0, 0); } }
Handling a Time Base Callback
Now our callback is primed and ready to fire when the movie reaches its end. At that point, the function QTBig_FullscreenCallBack is executed. QTBig_FullscreenCallBack is declared like this:
PASCAL_RTN void QTBig_FullscreenCallBack (QTCallBack theCallBack, long theRefCon);
The first parameter is the callback identifier; the second parameter is the reference constant we passed to CallMeWhen, which is of course the window object for the fullscreen window. In general, it's best to keep callback functions short and sweet; the recommended practice is simply to set some flag that is inspected elsewhere in the application. Listing 18 gives our definition of QTBig_FullscreenCallBack.
Listing 18: Handling a time base callback
PASCAL_RTN void QTBig_FullscreenCallBack (QTCallBack theCallBack, long theRefCon) { WindowObject myWindowObject = (WindowObject)theRefCon; ApplicationDataHdl myAppData = NULL; QTCallBack myCallBack = NULL; if (myWindowObject == NULL) return; myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(myWindowObject); if (myAppData == NULL) return; if ((**myAppData).fCallBack != theCallBack) return; // mark this window for ending fullscreen mode (**myAppData).fEndFullscreenNeeded = true; // clean up the callback stuff if ((**myAppData).fCallBack != NULL) DisposeCallBack((**myAppData).fCallBack); if ((**myAppData).fCallBackUPP != NULL) DisposeQTCallBackUPP((**myAppData).fCallBackUPP); (**myAppData).fCallBack = NULL; (**myAppData).fCallBackUPP = NULL; }
First, we cast the theRefCon parameter to be of type WindowObject and make sure that we are given a non-NULL window object. Then we extract the application data associated with that window object and verify that the callback passed to the callback function (theCallBack) is the same as the callback stored in the application data record ((**myAppData).fCallBack). If everything checks out okay, we set the fEndFullscreenNeeded field to true. We'll add some code to the QTApp_HandleEvent function that checks this field and returns a window to normal mode if it is true. Remember that we already go looking for any windows that are marked for returning to normal mode, so we can rework the while loop as shown in Listing 19.
Listing 19: Looking for windows to return to normal mode
myWindow = QTFrame_GetFrontMovieWindow(); while (myWindow != NULL) { myNextWindow = QTFrame_GetNextMovieWindow(myWindow); myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindow(myWindow); if (myAppData != NULL) { if ((**myAppData).fEndFullscreenNeeded) { (**myAppData).fEndFullscreenNeeded = false; QTBig_StopFullscreen( QTFrame_GetWindowObjectFromWindow(myWindow)); } if ((**myAppData).fDestroyWindowNeeded) { (**myAppData).fDestroyWindowNeeded = false; QTFrame_DestroyMovieWindow(myWindow); } } myWindow = myNextWindow; }
Finally, since we are done with the callback, we call DisposeCallBack to dispose of the callback and DisposeQTCallBackUPP to dispose of the callback UPP. We finish up by clearing out the fields in the application data record that store the callback and callback UPP.
Conclusion
Let's quickly recap what we've learned here. A movie can contain a movie user data item of type ‘ptv ' that (usually) requests that the movie be played back fullscreen. A wired movie can issue an application message of type kQTAppMessageEnterFullScreenRequested, and a Flash movie can issue a Flash application message of type fullscreen with the argument true. In all these cases, or in response to the user selecting the "Play Fullscreen" menu item, our application QTBigScreen calls the QuickTime function BeginFullScreen to put the movie into fullscreen mode. The movie remains in fullscreen mode until it reaches its end (if it's a non-interactive, non-looping movie), until the user presses the Escape key, or until our application receives an application message that requests normal mode. At that point, we exit fullscreen mode by calling EndFullScreen.
Tim Monroe is 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.
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine