TweetFollow Us on Twitter

Feb 00 QTToolkit

Volume Number: 16 (2000)
Issue Number: 2
Column Tag: QuickTime Toolkit

Movie Controller Potpourri

by Tim Monroe

More Adventures with QuickTime's Movie Controllers

Introduction

In the previous article, we learned how to open QuickTime movies and display them in windows on the screen. We also learned how to create movie controllers and pass events to them so that the user can interact with the movies in the most basic ways. For normal "linear" QuickTime movies, these basic interactions include starting and stopping the movie, moving quickly forward or backward in the movie, adjusting the volume of the sound track, and performing simple editing operations on the movie. For QuickTime VR movies, the basic interactions include changing the pan and tilt angles, zooming in or out, moving from one node to another, and displaying the visible hot spots. All of this interaction is provided, with virtually no programming on our part, by the movie controller associated with the movie.

In this article, we're going to continue working with movie controllers. Now that we've done all the work necessary to associate a movie controller with a movie and to draw the movie and movie controller in a window on the screen, there is a tremendous amount of "low-hanging fruit" that we can pick with a very small amount of code. Here we'll see how to hide and show the movie controller bar, hide and show particular buttons in the bar, attach a pop-up menu to the recently-introduced custom button in the controller bar, and perform several other tasks on the controller bar. Toward the end of this article, we'll also throw in a few goodies that are only slightly more complicated, like getting a movie controller to open a URL in the user's default web browser.

Along the way, we'll shift our focus slightly and take a look at movie user data, which is some custom data that can be attached to a movie. You can use a movie's user data to specify lots of information about a movie, including the initial position of the movie window, the movie's looping state, and the movie's copyright information. This isn't completely unrelated to our main topic in this article, because movie controllers and movie user data are linked in one important way: for a movie to use a special movie controller (that is, any controller other than the standard linear movie controller), the movie's user data must include a piece of information that specifies which other controller to use. When you call NewMovieController to associate a movie controller with an open movie, QuickTime looks for that piece of user data and, if it finds it, opens the specified movie controller. Here we'll see how we too can inspect that data and use it for our own purposes.

Before we begin, it's worthwhile to emphasize once again the distinction between a movie controller and a movie controller bar. A movie controller is a software component that you can use to manage the user's interaction with a QuickTime movie. A movie controller bar is a visible set of controls and other user-interface elements that is displayed by a movie controller (usually along the bottom of the movie window) and that provides some ways for the user to interact with the movie. The movie controller typically supports some forms of user interaction that are not associated with the controller bar (for instance, hitting the space bar starts or stops a linear QuickTime movie). Moreover, it's possible to have a movie controller associated with a movie but no visible movie controller bar (as we'll see shortly); even when the movie controller bar is hidden, the user can still interact with the movie. This distinction needs emphasizing only because it's not uncommon to hear people talk about the movie controller when they really mean the movie controller bar.

Managing the Controller Bar

Showing and Hiding the Controller Bar

So let's get started picking some of that low-hanging fruit. One of the simplest things we can do is hide the movie controller bar, with this single line of code:

MCSetVisible(myMC, false);

(Here myMC refers to a movie controller.) The MCSetVisible function sets the controller bar to be visible or invisible, according to the Boolean value you pass it. Figure 1 shows a movie window with the controller bar hidden.


Figure 1. A movie window with a hidden movie controller bar

Let's take a moment to define a couple of functions that call MCSetVisible with the appropriate parameter, mainly to give ourselves a more uniform naming convention and to make our code a bit more readable. Listing 1 defines the function QTUtils_HideControllerBar and Listing 2 defines the opposite function, QTUtils_ShowControllerBar.


Listing 1: Hiding the controller bar

QTUtils_HideControllerBar
void QTUtils_HideControllerBar (MovieController theMC) 
{
	MCSetVisible(theMC, false);
}

Listing 2: Showing the controller bar

QTUtils_ShowControllerBar
void QTUtils_ShowControllerBar (MovieController theMC) 
{
	MCSetVisible(theMC, true);
}

We can call the MCGetVisible function to determine whether the controller bar is currently visible, as shown in Listing 3.


Listing 3: Determining whether the controller bar is visible

QTUtils_IsControllerBarVisible
Boolean QTUtils_IsControllerBarVisible (MovieController theMC) 
{
	return((Boolean)MCGetVisible(theMC));
}
And we can put this all together to define a function that toggles the current visibility state of the controller bar, as shown in Listing 4.

Listing 4: Toggling the visibility state of the controller bar

QTUtils_ToggleControllerBar
void QTUtils_ToggleControllerBar (MovieController theMC) 
{
	if (QTUtils_IsControllerBarVisible(theMC))
		QTUtils_HideControllerBar(theMC);
	else
		QTUtils_ShowControllerBar(theMC);
}

No doubt there are more than a few of you out there scratching your heads and wondering why we didn't simply define QTUtils_ToggleControllerBar with this single line of code:

MCSetVisible(theMC, !(Boolean)MCGetVisible(theMC));

We surely could have done that, but personally I find the definition in Listing 4 to be a tad more readable.

Using Badges

While we're on the topic of hiding and showing the controller bar, it's worth mentioning a useful but little-used user interface element known as a badge. The idea behind badges is that a movie window that has no controller bar and whose movie is not actively playing looks remarkably like a window containing a still picture. (Take another look at Figure 1.) To help the user realize that the window is actually a movie window containing a stopped movie with no visible controller bar, the movie controller can be made to display a badge, as seen in Figure 2.


Figure 2. A movie window displaying a badge

The movie controller displays the badge whenever the movie is stopped and there is no visible movie controller bar, but only if you have previously configured the movie controller to do so, like this:

MCDoAction(myMC, mcActionSetUseBadge, (void *)true);

A useful feature of a badge is that the user can make the controller bar reappear by clicking in the badge. If you want, you can explicitly disable the movie controller from using a badge by executing this line of code:

MCDoAction(myMC, mcActionSetUseBadge, (void *)false);

You'll find that line in the function QTFrame_SetupController in our source code file ComApplication.c to suppress badge display for any movies we display.

Attaching and Detaching the Controller Bar

The default behavior of a movie controller is to draw the movie controller bar contiguous to, and just underneath, the movie that it is controlling. In virtually all cases, this results in exactly the appearance for movie windows that users expect. But for some purposes, it might be preferable to position the movie controller bar elsewhere. QuickTime allows us to do this quite easily. The first thing we need to do is detach the movie controller from the movie (a new movie controller is, by default, attached to its movie). We can do this by calling MCSetControllerAttached like this:

MCSetControllerAttached(myMC, false);

The next thing we need to do is specify the desired new locations of the movie and movie controller bar. Listing 5 defines a function, QTUtils_PutControllerBarOnTop, that places the controller bar above the movie.


Listing 5: Putting the controller bar above a movie

QTUtils_PutControllerBarOnTop
void QTUtils_PutControllerBarOnTop (MovieController theMC) 
{
	if (theMC == NULL)
		return;

	if (MCIsControllerAttached(theMC) == 1) {
		Rect		myMCRect;
		Rect		myMovieRect;

		MCGetControllerBoundsRect(theMC, &myMCRect);
		myMovieRect = myMCRect;
		myMCRect.bottom = myMCRect.top + QTUtils_GetControllerBarHeight(theMC);
		myMovieRect.top = myMCRect.bottom + 1;

		MCSetControllerAttached(theMC, false);
		MCPositionController(theMC, &myMovieRect, &myMCRect, 0L);
	}
}

Figure 3 shows a sample movie window after the QTUtils_PutControllerBarOnTop function has been called on it. In this case, the controller bar is still contiguous to the movie, but it is no longer attached to it.


Figure 3. A controller on top of a movie

In general, as I've said, it's best to retain the standard appearance of movie windows, where the controller is underneath and attached to the window. But it's still sometimes useful to detach a movie controller from its movie, even if just briefly. One example of this is if you wanted to determine the height of the movie controller bar (which, incidentally, we needed to do in Listing 5). There is no programming interface for getting this information directly. QuickTime does however supply the function MCGetControllerBoundsRect, which returns the rectangle that encloses the movie controller. The only "gotcha" here is that the movie controller rectangle is defined as the rectangle that encloses the movie controller bar and the movie, if the movie controller is attached to the movie; otherwise, if the movie controller is detached from the movie, the movie controller rectangle encloses just the movie controller bar. So, to determine the height of the movie controller bar, we first need to detach the controller bar, if it's currently attached, and then retrieve the movie controller rectangle. Listing 6 shows our complete function for doing this.


Listing 6: Finding the height of the controller bar

QTUtils_GetControllerBarHeight
short QTUtils_GetControllerBarHeight (MovieController theMC)
{
	Boolean		wasAttached = false;
	Rect				myRect;

	// if the controller bar is attached, detach it (and remember we did so)
	if (MCIsControllerAttached(theMC) == 1) {
		wasAttached = true;
		MCSetControllerAttached(theMC, false);
	}

	// get the rectangle of the controller
	MCGetControllerBoundsRect(theMC, &myRect);

	// now reattach the controller bar, if it was originally attached
	if (wasAttached)
		MCSetControllerAttached(theMC, true);

	return(myRect.bottom - myRect.top);
}

Managing Controller Bar Buttons

Now that we've played a little bit with the movie controller bar, let's consider how to work with individual buttons in the controller bar. In general, the only thing we can do with the individual buttons is hide and show them. We cannot disable or enable them, and we cannot directly handle clicks on them. The only exception to this concerns the controller bar's custom button; we can intercept and react to clicks on the custom button, as we'll see later in "Using the Controller Bar Custom Button".

Managing the Standard Controller Buttons

Let's begin by considering the standard QuickTime movie controller bar. There are only four buttons that QuickTime allows us to show or hide: the volume control (sometimes also called the speaker button), the step-forward and step-backward buttons, and the custom button. By default, the volume control is displayed if and only if the associated movie contains a sound or music track. We can hide the volume control, if we choose, but we cannot make it visible if the movie does not have a sound or music track. All the other buttons are displayed all the time, unless we programmatically suppress them. We can suppress the display of some of these buttons by manipulating the movie's control flags, a 32-bit value whose bits encode the settings of various movie display and playback options. QuickTime defines these constants for showing and hiding buttons in the standard movie controller bar:

enum {
	mcFlagSuppressStepButtons			= 1 << 1,
	mcFlagSuppressSpeakerButton		= 1 << 2,
	mcFlagsUseCustomButton				= 1 << 5
};

So, for instance, we can hide the two step buttons by executing this code:

MCDoAction(myMC, mcActionGetFlags, &myControllerFlags);
MCDoAction(myMC, mcActionSetFlags, (void *)(myControllerFlags | mcFlagSuppressStepButtons));

The idea here is simple: we get the current control flags, set the bit in those flags that suppresses display of the step buttons, and then send the updated flags back to the movie controller. Naturally, to show a button that's been hidden we do the opposite; namely, we get the current control flags, clear the appropriate bit in those flags, and then send the updated flags back to the movie controller:

MCDoAction(myMC, mcActionGetFlags, &myControllerFlags);
MCDoAction(myMC, mcActionSetFlags, (void *)(myControllerFlags & ~mcFlagSuppressStepButtons));

One thing to watch out for is that the semantics for the custom button are the reverse of those for the other buttons: setting the flag for the custom button results in that button being displayed, while clearing that flag results in the button being hidden. This is because the custom button is, by default, not displayed.

Managing the QuickTime VR Controller Buttons

The QuickTime VR movie controller is more generous than the standard movie controller, in that it allows us to show or hide any of the controls in its controller bar (including the volume control). But this generosity comes at the price of a slight increase in complexity. The simple clearing and setting of bits in the control flags shown above does not always give the desired result. This is because the QuickTime VR movie controller sometimes suppresses buttons even when those buttons have not been explicitly suppressed in the control flags. For example, if a particular QuickTime VR movie does not contain a sound track, then the movie controller automatically suppresses the volume control. Likewise, if a QuickTime VR movie does contain a sound track, then the volume control is automatically displayed, again without regard to the actual value of the mcFlagSuppressSpeakerButton flag in the control flags.

So far, this behavior is identical to that of the standard movie controller. The main difference between the two controllers is that the QuickTime VR movie controller does provide a way for us to override its default behavior. We might want to do this, for instance, if our application has loaded a sound from a resource or some external file and we'd like to allow the user to adjust the volume of that sound using the volume control. (Wait a second! Didn't I just say that we can't intercept mouse clicks on the controller bar buttons? If so, then how are we supposed to respond to the user's adjusting the volume using the volume control? The answer is that we can't directly determine that the user has clicked on the volume control, but we can wait for the movie controller to tell use that the user has changed the volume, by looking for movie controller actions of type mcActionSetVolume. So, indirectly, we can accomplish what we want here.)

To let us override its default behavior, the QuickTime VR movie controller maintains two sets of flags, a set of control flags (with which we are already familiar) and a set of explicit flags. The explicit flags indicate which bits in the control flags are to be used explicitly (that is, overriding any default behaviors of the movie controller). If a particular bit in the explicit flags is set, then the corresponding bit in the control flags is interpreted as the desired setting for that feature. Conversely, if a particular bit in the explicit flags is clear, then the corresponding bit in the control flags is interpreted as it normally is. In this way, the explicit flags operate as a sort of mask for the control bits.

To make this clearer, let's consider a concrete example. The QuickTime VR movie controller defines these constants for working with its controller bar buttons:

enum {
	mcFlagQTVRSuppressBackBtn				= 1L << 16,
	mcFlagQTVRSuppressZoomBtns			= 1L << 17,
	mcFlagQTVRSuppressHotSpotBtn			= 1L << 18,
	mcFlagQTVRSuppressTranslateBtn		= 1L << 19,
	mcFlagQTVRSuppressHelpText			= 1L << 20,
	mcFlagQTVRSuppressHotSpotNames		= 1L << 21,
	mcFlagQTVRExplicitFlagSet				= 1L << 31
};

If, for example, bit 17 is set in a movie's explicit flags and bit 17 is clear in that movie's control flags, then the zoom buttons are displayed (that is, they are not suppressed). Similarly, if bit 2 is set in a movie's explicit flags and bit 2 is clear in that movie's control flags, then the volume control is displayed, whether or not the movie contains a sound track.

There is one final element to this whole story. We need a way of getting and setting a movie controller's explicit flags. Rather than introduce any new movie controller actions, the QuickTime VR engineers decided to use the existing actions (mcActionGetFlags and mcActionSetFlags) but to have those actions operate on the explicit flags or the control flags, depending upon the setting of bit 31 (defined as mcFlagQTVRExplicitFlagSet) in the value passed to MCDoAction. To get or set a bit in a movie's explicit flags, you must set the flag mcFlagQTVRExplicitFlagSet in the parameter you pass to mcActionGetFlags or mcActionSetFlags. To get or set a bit in a movie's control flags, you must clear the flag mcFlagQTVRExplicitFlagSet in the parameter you pass to mcActionGetFlags or mcActionSetFlags.

So let's put this all together and see how we can force the volume control to be displayed in the controller bar of a QuickTime VR movie that does not contain a sound track. First, we need to get the movie's explicit flags and set the bit in those explicit flags that corresponds to the volume control:

myControllerFlags = mcFlagQTVRExplicitFlagSet;
MCDoAction(myMC, mcActionGetFlags, &myControllerFlags);
MCDoAction(myMC, mcActionSetFlags, (void *)((myControllerFlags | mcFlagSuppressSpeakerButton) | mcFlagQTVRExplicitFlagSet));

Note that when you use the defined constants to set values in the explicit flags, the constant names might be a bit confusing. For instance, setting the bit mcFlagSuppressSpeakerButton in a movie's explicit flags doesn't cause the speaker to be suppressed; it just means: "use the actual value of the mcFlagSuppressSpeakerButton bit in the control flags".

Next, we need to set the appropriate bit in the movie's control flags, like this:

myControllerFlags = 0;
MCDoAction(myMC, mcActionGetFlags, &myControllerFlags);
MCDoAction(myMC, mcActionSetFlags, (void *)(myControllerFlags & ~mcFlagSuppressSpeakerButton & ~mcFlagQTVRExplicitFlagSet));

Once we've executed these six lines of code, the volume control will appear in the controller bar, whether or not the associated QuickTime VR movie has a sound track.

Listing 7 combines the results of our discussions in this section and the previous section into a single, unified function that we can use to show buttons in a movie controller bar.


Listing 7: Showing a button in the controller bar

QTUtils_ShowControllerButton
void QTUtils_ShowControllerButton (MovieController theMC, long theButton) 
{
	long				myControllerFlags;

	// handle the custom button separately
	if (theButton == mcFlagsUseCustomButton) {
		MCDoAction(theMC, mcActionGetFlags, &myControllerFlags);
		MCDoAction(theMC, mcActionSetFlags, 
				(void *)(myControllerFlags | theButton));
	} else {
		// get the current explicit flags and set the explicit flag for the specified button
		myControllerFlags = mcFlagQTVRExplicitFlagSet;
		MCDoAction(theMC, mcActionGetFlags, &myControllerFlags);
		MCDoAction(theMC, mcActionSetFlags,
			(void *)((myControllerFlags | theButton) | 
					mcFlagQTVRExplicitFlagSet));

		// get the current control flags and clear the suppress flag for the specified button
		myControllerFlags = 0;
		MCDoAction(theMC, mcActionGetFlags, &myControllerFlags);
		MCDoAction(theMC, mcActionSetFlags, 
			(void *)(myControllerFlags & ~theButton & 
				~mcFlagQTVRExplicitFlagSet));
	}
}

The corresponding function to hide a button in the movie controller bar is exactly parallel; if you're interested, take a look at QTUtils_HideControllerButton in the file QTUtilities.c.

Currently, only the QuickTime VR movie controller maintains a separate set of explicit flags and hence knows how to interpret the mcFlagQTVRExplicitFlagSet flag. This means that the standard QuickTime movie controller will actually be getting and setting the movie's control flags when we make the calls to get and set the explicit flags. We've carefully written QTUtils_ShowControllerButton and QTUtils_HideControllerButton so that no harm is done if a movie controller does not support any explicit flags.

Using the Controller Bar Custom Button

In QuickTime 3, movie controllers gained the ability to display and manage a custom controller bar button. As you can see in Figure 4, the custom button is displayed on the right side of the controller bar, just to the left of the grow box.


Figure 4. The controller bar's custom button

The button contains a downward-pointing triangle, which is suggestive of the triangle contained in a standard pop-up menu. This is largely because the main intended use of the custom button was to allow the QuickTime plug-in for web browsers to display a pop-up menu of commands. Figure 5 shows a typical QuickTime movie embedded in a web page, with the pop-up menu popped up.


Figure 5. The pop-up menu of the QuickTime browser plug-in

The QuickTime public headers and libraries contain everything that we need to put this button to work in our own applications. Let's see how to do that. The first thing we need to do is display the custom button in the controller bar that's attached to our movies. We can use the function QTUtils_ShowControllerButton (defined in Listing 7) with the constant mcFlagsUseCustomButton to show the custom button, like this:

QTUtils_ShowControllerButton(mcFlagsUseCustomButton);

Then, all we need to do is intercept the user's clicks on the custom button and react accordingly. This is a task for our movie controller action filter function. We can add these lines to our ever-expanding switch statement in the filter function QTApp_MCActionFilterProc:

case mcActionCustomButtonClick:
	QTCustom_HandleCustomButtonClick(theMC, (EventRecord *)theParams, theRefCon);
	break;

So, when the function QTCustom_HandleCustomButtonClick gets called, we know that the user has clicked on the custom button in the controller bar. The theParams parameter to our movie controller action filter function points to an event record describing that click. As you can see, we've cast the theParams parameter to a pointer to an event record and passed it to our click-handling function.

At this point, we can call the Menu Manager function PopUpMenuSelect to display a pop-up menu at the point specified in the event record. PopUpMenuSelect is very much like the function MenuSelect, in that it takes care of drawing the menu on the screen, tracking mouse movements within the menu, and then returning to our application an indication of which item (if any) in the menu was selected. If you've written any Macintosh applications, you are probably already familiar with MenuSelect, since you've almost certainly used it to handle your application's pull-down menus in the Macintosh menu bar. But you might never have used PopUpMenuSelect, since it's called internally by the Control Manager when you handle user actions on pop-up menu controls. At any rate, it's just the function we need here.

Before we can call PopUpMenuSelect, however, we need to do a little work. First, the point contained in the event record passed to our action filter function is in coordinates that are local to the window associated with the movie controller. PopUpMenuSelect wants its input to be in global screen coordinates. So we need to call the LocalToGlobal function to convert from the one coordinate system to the other. Second, and more importantly, we need to get a handle to the menu that we want to be popped up at that location on the screen. We can do this in several ways. We could just read the menu from a resource file (using the MacGetMenu function), or we could build the menu programmatically on the fly (using the NewMenu and AppendMenu functions). To make things easy for the moment, we'll use the latter method and build the menu from scratch. The complete QTCustom_HandleCustomButtonClick function is shown in Listing 8.


Listing 8: Handling clicks on the custom button in the controller bar

QTCustom_HandleCustomButtonClick
void QTCustom_HandleCustomButtonClick (MovieController theMC, EventRecord *theEvent, long theRefCon)
{
#pragma unused(theMC)
	MenuHandle		myMenu = NULL;
	WindowObject	myWindowObject = NULL;
	StringPtr 		myMenuTitle = QTUtils_ConvertCToPascalString(kMenuTitle);
	StringPtr 		myItem1Text = QTUtils_ConvertCToPascalString(kItem1Text);
	StringPtr 		myItem2Text = QTUtils_ConvertCToPascalString(kItem2Text);
	StringPtr 		myItem3Text = QTUtils_ConvertCToPascalString(kItem3Text);

	myWindowObject = (WindowObject)theRefCon;
	if (myWindowObject == NULL)
		goto bail;

	// make sure we got a valid event
	if (theEvent == NULL)
		goto bail;

	// create a new menu
	myMenu = NewMenu(kCustomButtonMenuID, myMenuTitle);
	if (myMenu != NULL) {
		long				myItem = 0;
		Point			myPoint;

		// add some items to the menu
		MacAppendMenu(myMenu, myItem1Text);
		MacAppendMenu(myMenu, myItem2Text);
		MacAppendMenu(myMenu, myItem3Text);

		// insert the menu into the menu list
		MacInsertMenu(myMenu, hierMenu);

		// by default, MacAppendMenu enables the item;
		// do any desired menu item disabling here
		if (!(**myWindowObject).fIsDirty)
			DisableMenuItem(myMenu, kSaveItemIndex);

		// find the location of the mouse click;
		// the top-left corner of the pop-up menu is anchored at this point
		myPoint = theEvent->where;
		LocalToGlobal(&myPoint);

		// display the pop-up menu and handle the item selected
		myItem = PopUpMenuSelect(myMenu, myPoint.v, 
				myPoint.h, myItem);
		switch (MENU_ITEM(myItem)) {
			case kItem1Index:
				QTFrame_Beep();
				break;
			case kItem2Index:
				QTFrame_ShowAboutBox();
				break;
			case kItem3Index:
				QTFrame_UpdateMovieFile((**myWindowObject).fWindow);
				break;
		}

		// remove the menu from the menu list
		MacDeleteMenu((**myMenu).menuID);

		// dispose of the menu
		DisposeMenu(myMenu);
	}

bail:
	free(myMenuTitle);
	free(myItem1Text);
	free(myItem2Text);
	free(myItem3Text);
}

Figure 6 shows the pop-up menu displayed by a call to QTCustom_HandleCustomButtonClick.


Figure 6. Our application's custom pop-up menu.

Now keep in mind that this is all being accomplished using APIs that belong to QuickTime and to the Macintosh Menu Manager. But the code we've developed compiles, links, and executes just fine under Windows as well. I find this just simply amazing (but maybe I'm easy to amaze). At the very least, this is one more example of the excellent work done by the engineers who designed and implemented the QuickTime Media Layer (QTML) on Windows, which provides support under Windows for the parts of the Macintosh Operating System and User Interface Toolbox that are used by QuickTime.

Selecting an Entire Movie

You might have noticed, when we were discussing movie editing in the previous article, that there are movie controller functions for handling all the standard menu items in the Edit menu, except for the Select All item. To handle that item, we simply called the application-defined function QTUtils_SelectAllMovie, which we did not discuss further. It's time to do so now. The linear movie controller supports two actions that are useful here, mcActionSetSelectionBegin and mcActionSetSelectionDuration. To select the entire movie, as you've probably guessed, we can use mcActionSetSelectionBegin to set the beginning of the movie selection to the beginning of the movie, and then we can use mcActionSetSelectionDuration to set the selection duration to the duration of the entire movie.

The only issue left to consider, then, is how to specify times in a movie. Time management is generally a big concern for multimedia content (as should be clear from the emphasis on time in the product name "QuickTime"). At the most basic level, robust time services are necessary to get a video track to play back at the right speed no matter what the processing speed of the user's computer. They are also necessary to maintain a precise synchronization between sound and video tracks in a movie. QuickTime provides a number of time-related services, so time is something that we'll need to discuss more than once in this series of articles. For the moment, we'll look only at what we need in order to solve the problem at hand, and defer a more exhaustive treatment of QuickTime's time concepts to a future article.

The mcActionSetSelectionBegin and

mcActionSetSelectionDuration controller actions both take a parameter of type TimeRecord, which is declared like this:

struct TimeRecord {
	CompTimeValue 			value;	
	TimeScale 					scale;
	TimeBase 					base;
};

A movie's time scale is the number of units that elapse every second. Figuratively speaking, the time scale is the ruler by which the movie is measured. A movie's time scale is set at the time the movie is created and can be determined programmatically using the GetMovieTimeScale function. Like the distance between markings on a ruler, the number you use for a time scale is somewhat arbitrary. A standard value is 600, which permits non-fractional values for most of the common movie rates (for example, a movie playing at 30 frames per second has 20 units per frame, if the time scale is 600).

The values we specify in the value field are interpreted relative to the time scale specified in the scale field. The CompTimeValue structure contains two fields, hi and lo, which specify (respectively) the high- and low-order 32-bits of our time value. To specify the beginning of the movie, we need to set both of these fields to 0. To specify the desired duration of the movie, we can set the lo field to the value returned by the GetMovieDuration function. Luckily, GetMovieDuration returns the length of the specified movie in units relative to the movie's time scale. (The base field is not used here, so we'll set it to 0.) Listing 9 defines the function QTUtils_SelectAllMovie.


Listing 9: Selecting an entire movie

QTUtils_SelectAllMovie
OSErr QTUtils_SelectAllMovie (MovieController theMC)
{
	TimeRecord					myTimeRecord;
	Movie 							myMovie = NULL;
	ComponentResult			myErr = noErr;

	if (theMC == NULL)
		return(paramErr);

	myMovie = MCGetMovie(theMC);
	if (myMovie == NULL)
		return(paramErr);

	myTimeRecord.value.hi = 0;
	myTimeRecord.value.lo = 0;
	myTimeRecord.base = 0;
	myTimeRecord.scale = GetMovieTimeScale(myMovie);
	myErr = MCDoAction(theMC, mcActionSetSelectionBegin, &myTimeRecord);
	if (myErr != noErr)
		return((OSErr)myErr);

	myTimeRecord.value.hi = 0;
	myTimeRecord.value.lo = GetMovieDuration(myMovie);
	myTimeRecord.base = 0;
	myTimeRecord.scale = GetMovieTimeScale(myMovie);
	myErr = MCDoAction(theMC, mcActionSetSelectionDuration, &myTimeRecord);

	return((OSErr)myErr);
}

Working With Movie User Data

A QuickTime movie file contains a list of data, called movie user data, which you can read and manipulate. This list consists of individual items, each of which has an associated type. Some types of user data are predefined by QuickTime, such as the movie's name, copyright information, or author; other types can be defined by specific applications and used for their own purposes. For instance, if you wanted to store the geographical location of a QuickTime VR panorama, you could attach to the movie a piece of movie user data specifying the location's latitude and longitude.

A user data item type is a four-character code that looks like a resource type. For instance, the movie name user data item has the type '©nam', and the movie copyright user data item has the type '©cpy'. In addition, there can be more than one user data item of a particular type. If so, those items are distinguished from one another by their index (which starts at 1).

To retrieve a movie's user data item of a specific type and index, you first need to call the GetMovieUserData function to get the entire user data list from the movie. GetMovieUserData returns a reference to the user data list; you can pass this reference to any of several other functions that retrieve individual items from the list. For instance, if you know that the item contains text data, you can pass the user data reference to the GetUserDataText function to get that text data. Or, you can pass that reference to the GetUserDataItem function to get any kind of data (including text data) from the user data list. By convention, the predefined types of movie user data are always stored in a big-endian format, so we need to remember to convert the data from big-endian format to the native-endian format when we read the user data; we also need to convert the data from the native-endian format to big-endian format when we write the user data.

Specifying the Controller Type

As mentioned earlier, any QuickTime movie that uses a special movie controller needs to contain a user data item that specifies which movie controller to use. The type of this user data item is kUserDataMovieControllerType (defined in the header file Movies.h as 'ctyp'). Listing 10 defines the function QTUtils_GetControllerType, which returns the controller type specified in a movie's user data, or kUnknownType if no controller type is specified in that user data.


Listing 10: Getting a movie's controller type

QTUtils_GetControllerType
OSType QTUtils_GetControllerType (Movie theMovie)
{
	UserData		myUserData = NULL;
	OSType			myType = kUnknownType;
	OSErr			myErr = noErr;

	// make sure we've got a movie
	if (theMovie == NULL)
		return(myType);

	myUserData = GetMovieUserData(theMovie);
	if (myUserData != NULL) {
		myErr = GetUserDataItem(myUserData, &myType, 
					sizeof(myType), kUserDataMovieControllerType, 0);
		if (myErr == noErr)
			myType = EndianU32_BtoN(myType);
	}

	return(myType);
}

QTUtils_GetControllerType just assembles the pieces we've talked about above: it calls GetMovieUserData to get the movie's user data list; then it calls GetUserDataItem to retrieve the movie controller type user data item; finally, it converts the big-endian user data to native-endian data using the macro EndianU32_BtoN.

Let's look at a sample use of the QTUtils_GetControllerType function. Our basic framework needs to know whether a movie uses the QuickTime VR movie controller, since the QuickTime VR movie controller does a lot of its own cursor management. When our application starts up, and after it has opened the movie file, it calls the QTUtils_IsQTVRMovie function defined in Listing 11 to determine whether the movie is a QuickTime VR movie. As you can see, QTUtils_IsQTVRMovie calls QTUtils_GetControllerType to determine the movie's controller type and then compares that type against the controller types that we know are used by QuickTime VR movies. (kQTVRQTVRType is the controller type of all version 2.0 and later QuickTime VR files, while kQTVROldObjectType and kQTVROldPanoType are controller types used by version 1.0 object and panorama movies.)


Listing 11: Determining if a movie is a QuickTime VR movie

QTUtils_IsQTVRMovie
Boolean QTUtils_IsQTVRMovie (Movie theMovie) 
{
	Boolean		myIsQTVRMovie = false;
	OSType			myType;

	// QTVR movies have a special piece of user data identifying the movie controller type
	myType = QTUtils_GetControllerType(theMovie);

	if ((myType == kQTVRQTVRType) || 
				(myType == kQTVROldPanoType) || 
						(myType == kQTVROldObjectType))
		myIsQTVRMovie = true; 

	return(myIsQTVRMovie);
}

It's really just as easy to set a movie's controller type as it is to get a movie's controller type. Instead of calling GetUserDataItem, we need to call SetUserDataItem, as shown in Listing 12. Notice that we make sure to convert the type passed to us into big-endian format.


Listing 12: Setting a movie's controller type

QTUtils_SetControllerType
OSErr QTUtils_SetControllerType (Movie theMovie, OSType theType)
{
	UserData		myUserData;
	OSErr			myErr = noErr;

	// make sure we've got a movie
	if (theMovie == NULL)
		return(paramErr);

	// get the movie's user data list
	myUserData = GetMovieUserData(theMovie);
	if (myUserData == NULL)
		return(paramErr);

	theType = EndianU32_NtoB(theType);
	myErr = SetUserDataItem(myUserData, 
								&theType, sizeof(theType), 
												kUserDataMovieControllerType, 0);

	return(myErr);
}

There are a couple of points to keep in mind here, however. First, the SetUserDataItem function changes only the copy of the movie's user data that's in memory (and to which myUserData is a reference). If you also want to change the user data stored in the movie file, you need to update the movie file (usually by calling the UpdateMovieResource function). If you were to call QTUtils_SetControllerType to change the movie's controller type and then immediately closed the movie file, the new controller type would not be stored in the movie file.

Second, calling QTUtils_SetControllerType does not change the movie controller associated with an open movie, unless you call it before you call NewMovieController. The reason, of course, is that NewMovieController looks at the movie's user data at the time you call it; any subsequent changes to the movie data do not automatically change the movie controller assigned to a movie.

It occasionally happens that you do want to change a movie's controller type on the fly. To do this, you need to change the movie's user data, close the existing movie controller, and then open a new movie controller. You can accomplish all this by calling the QTUtils_ChangeControllerType function defined in Listing 13.

Listing 13: Changing a movie's controller type dynamically

QTUtils_ChangeControllerType
MovieController QTUtils_ChangeControllerType (MovieController theMC, OSType theType, long theFlags)
{
	MovieController			myMC = NULL;
	Movie							myMovie = NULL;
	Rect								myRect;
	OSErr							myErr = noErr;

	// make sure we've got a movie controller
	if (theMC == NULL)
		return(NULL);

	// get the movie associated with that controller
	myMovie = MCGetMovie(theMC);
	if (myMovie == NULL)
		return(NULL);

	GetMovieBox(myMovie, &myRect);

	// set the new controller type in the movie's user data list
	myErr = QTUtils_SetControllerType(myMovie, theType);
	if (myErr != noErr)
		return(NULL);

	// dispose of the existing controller
	DisposeMovieController(theMC);

	// create a new controller of the specified type
	myMC = NewMovieController(myMovie, &myRect, theFlags);

	return(myMC);
}

You'll notice that QTUtils_ChangeControllerType takes a movie controller, and not a movie, as an input parameter. The reason for this is that we need to use both the movie and the original movie controller within the function, and that we can always get the movie currently associated with a movie controller by calling the MCGetMovie function. There is no easy way to go in the reverse direction and obtain the movie controller (if any) currently associated with a movie.

Manipulating a Movie's Looping State

Another standard use for movie user data is to specify a movie's looping state. There are three possibilities here. First, a movie can play forward from beginning to end and then stop; a movie like this is said to have no looping. Second, a movie can play forward from beginning to end and then return to the beginning and play forward again, and so on; a movie like this is said to have normal looping. Finally, a movie can play forward from beginning to end and then play backwards from end to beginning, and then play forward from beginning to end, and so on; a movie like this is said to have palindrome looping. (Go hang a salami; I'm a lasagna hog!)

A movie's looping state is specified by a user data item of type 'LOOP'. If the movie doesn't contain an item of this type, then we'll assume that its looping state is no looping. If it does contain an item of this type, then the item data (a long integer) is 0 for normal looping and 1 for palindrome looping. To make our work more readable, let's define three looping state constants:

enum {
	kNormalLooping						= 0,
	kPalindromeLooping				= 1,
	kNoLooping								= 2
};

Now we can define (in Listing 14) a function QTUtils_GetMovieFileLoopingInfo that returns one of these constants, depending on whether it finds a movie user data item of type 'LOOP' and (if there is one) on what the data in that item is.


Listing 14: Getting a movie's looping state

QTUtils_GetMovieFileLoopingInfo
OSErr QTUtils_GetMovieFileLoopingInfo (Movie theMovie, long *theLoopInfo)
{
	UserData			myUserData = NULL;
	long					myLoopInfo = kNoLooping;
	OSErr				myErr = paramErr;

	// make sure we've got a movie
	if (theMovie == NULL)
		goto bail;

	// get the movie's user data list
	myUserData = GetMovieUserData(theMovie);
	if (myUserData != NULL) {
		myErr = GetUserDataItem(myUserData, &myLoopInfo, 
						sizeof(myLoopInfo), FOUR_CHAR_CODE('LOOP'), 0);
		if (myErr == noErr)
			myLoopInfo = EndianS32_BtoN(myLoopInfo);
	}

bail:
	*theLoopInfo = myLoopInfo;

	return(myErr);
}

When our application opens a movie, it would be nice to set its looping state to the state indicated in its 'LOOP' movie user data item (or indicated by the absence of that item). Happily, the movie controller supports two actions, mcActionSetLooping and mcActionSetLoopIsPalindrome, that we can issue to set the looping state to the correct state. The mcActionSetLooping action enables looping of any variety, and the mcActionSetLoopIsPalindrome action enables palindrome looping. Listing 15 defines a function that we can use to read the looping information from the file and set the appropriate looping state on the specified movie.


Listing 15: Setting a movie's looping state

QTUtils_SetLoopingStateFromFile
OSErr QTUtils_SetLoopingStateFromFile (Movie theMovie, MovieController theMC)
{
	long 			myLoopInfo = kNoLooping;
	OSErr			myErr = noErr;

	myErr = QTUtils_GetMovieFileLoopingInfo (theMovie, &myLoopInfo);
	switch (myLoopInfo) {

		case kNormalLooping:
			MCDoAction(theMC, mcActionSetLooping, (void *)true);
			MCDoAction(theMC, mcActionSetLoopIsPalindrome, (void *)false);
			break;

		case kPalindromeLooping:
			MCDoAction(theMC, mcActionSetLooping, (void *)true);
			MCDoAction(theMC, mcActionSetLoopIsPalindrome, (void *)true);
			break;

		case kNoLooping:
		default:
			MCDoAction(theMC, mcActionSetLooping, (void *)false);
			MCDoAction(theMC, mcActionSetLoopIsPalindrome, (void *)false);
			break;
	}

	return(myErr);
}

The last thing we might want to do is store a movie's current looping state in the movie file. We've already seen how to set a movie's controller type (Listing 12), so we might expect to set the looping state in roughly the same manner. However, we cannot just add a 'LOOP' movie user data item with the appropriate data, since the no-looping state is indicated by the absence of such a user data item. What we need to do therefore is first remove any existing user data items of type 'LOOP'; then, for normal looping or palindrome looping, we can add a user data item of the appropriate type. It's easy enough to remove all existing items of type 'LOOP': just keep removing the first such item until there are no more remaining, like this:

myCount = CountUserDataType(myUserData, FOUR_CHAR_CODE('LOOP'));
	while (myCount-)
		RemoveUserData(myUserData, FOUR_CHAR_CODE('LOOP'), 1);

The complete function for setting a movie's looping state is shown in Listing 16.
Listing 16: Setting the looping state of a movie file
QTUtils_SetMovieFileLoopingInfo
OSErr QTUtils_SetMovieFileLoopingInfo (Movie theMovie, long theLoopInfo)
{
	UserData			myUserData = NULL;
	long					myLoopInfo;
	short				myCount = 0;
	OSErr				myErr = paramErr;

	// get the movie's user data
	myUserData = GetMovieUserData(theMovie);
	if (myUserData == NULL)
		goto bail;

	// we want to end up with at most one user data item of type 'LOOP',
	// so let's remove any existing ones
	myCount = CountUserDataType(myUserData, FOUR_CHAR_CODE('LOOP'));
	while (myCount-)
		RemoveUserData(myUserData, FOUR_CHAR_CODE('LOOP'), 1);

	// make sure we're writing big-endian data
	myLoopInfo = EndianU32_NtoB(theLoopInfo);

	switch (theLoopInfo) {
		case kNormalLooping:
		case kPalindromeLooping:
			myErr = SetUserDataItem(myUserData, &myLoopInfo, 
									sizeof(long), FOUR_CHAR_CODE('LOOP'), 0);
			break;

		case kNoLooping:
		default:
			myErr = noErr;
			break;
	}

bail:
	return(myErr);
}

Once again, we'd need to make sure to call UpdateMovieResource to update the user data in the movie file or our changes to the user data will be lost when we close the movie.

Getting a Movie's Stored Window Position

To finish off our adventures with a movie's user data, let's consider briefly how to get the stored screen position of a movie window. A movie's user data list can contain an item of type 'WLOC', whose accompanying data consists of a 32-bit value that is interpreted as a point that specifies the position of the upper-left corner of the movie window. Listing 17 shows the function QTUtils_GetWindowPositionFromFile, whose definition should be completely familiar to you by now.


Listing 17: Getting a movie's stored screen position

QTUtils_GetWindowPositionFromFile
OSErr QTUtils_GetWindowPositionFromFile (Movie theMovie, Point *thePoint)
{
	UserData		myUserData = NULL;
	Point			myPoint = {kDefaultWindowX, kDefaultWindowY};
	OSErr			myErr = paramErr;
	// make sure we've got a movie
	if (theMovie == NULL)
		goto bail;
	// get the movie's user data list
	myUserData = GetMovieUserData(theMovie);
	if (myUserData != NULL) {
		myErr = GetUserDataItem(myUserData, &myPoint, 
								sizeof(Point), FOUR_CHAR_CODE('WLOC'), 0);
		if (myErr == noErr) {
			myPoint.v = EndianS16_BtoN(myPoint.v);
			myPoint.h = EndianS16_BtoN(myPoint.h);
		}
	}

bail:
	*thePoint = myPoint;
	return(myErr);
}

In theory, before opening the movie at the window position returned by QTUtils_GetWindowPositionFromFile, you should verify that that position is reasonable for the computer on which the movie is being opened. Otherwise, the new movie window might not be visible. This sanity check is left as an exercise for the reader.

Keep in mind that you can look for any of the predefined types of movie user data, and you can define your own types. You'd retrieve your own custom types of user data items in exactly the same way that we've retrieved the types defined by QuickTime. The only restriction is that Apple has reserved for itself all four-character types that consist solely of lower-case letters and special characters. So, for instance, you shouldn't use 'bill' as a custom user data type, but you're free to use 'BILL'.

I know what you're thinking: what about 'WLOC' and 'LOOP'? Shouldn't they instead be 'wloc' and 'loop', to fit within Apple's reserved types? Indeed they should be, if they were predefined user data types. But they aren't; you won't find 'WLOC' or 'LOOP' defined in any QuickTime header file. Rather, these types were used as custom types early on in QuickTime's history by the application MoviePlayer. Other applications followed suit, and these two types have become accepted ways of saving a movie's location and looping state.

Opening URLs

Let's finish up with something completely cool and incredibly easy to do. Namely, let's have our application tell the user's default web browser to open a web page in a browser window. There are quite a few reasons you might want to do this. For one, your application might have a "Help" menu item (or perhaps a button in your About box) to send the user to your own web site, so that you can provide up-to-date information about your product. Or, you might detect that the user's machine is lacking a decompressor needed for some data in the movies you want to play, so you want the user to download the necessary files. For whatever reason, it's just plain useful to have at your disposal an easy way to open a web browser and display a given URL.

How easy can this be? Well, assuming that myMC is an open movie controller and that myHandle is a handle to block of memory that contains a null-terminated string representing the URL, it's just this easy:

MCDoAction(myMC, mcActionLinkToURL, (void *)myHandle);

That's right: with one line of code, you've instructed the movie controller to launch the user's default web browser (if it isn't already open) and display the specified URL in a new window. Of course, there is undoubtedly some more code required to get the URL into a handle (supposing that it began its life as a typical C string of type char *). And, as we've seen, there's a fair amount of code required to open a movie and associate a movie controller with it. But once we've done all that work, it's nice to know that we can leverage it to good effect.

But you might be wondering: what if we don't have a movie controller at hand (maybe we don't have any movie windows open yet)? What do we do then? Well, we can simply open a movie controller. As we mentioned earlier, a movie controller is a software component that manages a user's interaction with a movie. Accordingly, we can use the Component Manager to open a movie controller, whether or not we have a movie to attach it to. To do this, we just call the Component Manager's OpenADefaultComponent function and specify that we want an instance of a component of type MovieControllerComponentType. Listing 18 shows the complete function that takes a C string specifying a URL and does all the work necessary to open the specified URL in the user's default web browser.

HR>

Listing 18: Opening a URL

QTApp_HaveBrowserOpenURL
OSErr QTApp_HaveBrowserOpenURL (char *theURL)
{
	MovieController		myMC = NULL;
	Handle						myHandle = NULL;
	Size							mySize = 0;
	OSErr						myErr = noErr;

	// copy the specified URL into a handle
	mySize = (Size)strlen(theURL) + 1;
	if (mySize == 0)
		goto bail;

	// allocate a new handle
	myHandle = NewHandleClear(mySize);
	if (myHandle == NULL)
		goto bail;

	// copy the URL into the handle
	BlockMove(theURL, *myHandle, mySize);

	// instantiate a movie controller and send it an mcActionLinkToURL message
	myErr = OpenADefaultComponent
									(MovieControllerComponentType, 0, &myMC);
	if (myErr != noErr)
		goto bail;

	myErr = MCDoAction(myMC, mcActionLinkToURL, (void *)myHandle);

bail:
	if (myHandle != NULL)
		DisposeHandle(myHandle);

	if (myMC != NULL)
		CloseComponent(myMC);

	return(myErr);
}

This is a generally useful technique to add to your bag of tricks: if there's a movie controller action that you'd like to issue but you don't have a movie controller close at hand, just call OpenADefaultComponent to get yourself a movie controller, issue the action, and then dispose of the controller. Movie controllers don't actually have to be controlling movies for us to put them to work.

This Month's Code

The Code folder accompanying this article contains the project files, source code and resource data of a sample application called QTController, for both Macintosh and Windows. QTController is just like the QTShell application we considered last month, except that the Test menu includes items that illustrate many of the techniques involving movie controllers that we've learned here. Figure 7 shows the Test menu of QTController when a QuickTime movie is in the frontmost window.


Figure 7. The Test menu in QTController

The source code for QTController is just the source code for QTShell, with modifications made to two functions only, QTApp_HandleMenu and QTApp_AdjustMenus (in ComApplication.c). In addition, the file QTCustomButton.c has been added to the projects. In all other respects, QTShell and QTController are identical. (Well, actually that isn't quite true; I took the opportunity to fix a few bugs in the underlying framework. If you've started your own projects based on QTShell, you should use the files in the Common Files folder included this month instead of the files provided last month.)

Conclusion

After all this discussion of movie controllers and the things you can do with them, you might be wondering whether it's possible to play back movies without the assistance of a movie controller. In other words, do we always have to have a movie controller lurking around somewhere? The answer is: yes and no. Some types of QuickTime movies require a movie controller while others do not. Standard linear QuickTime movies can be played back without the assistance of a movie controller, but of course you'll have to do a whole lot of work to replicate the user interaction normally provided by the movie controller. Other types of QuickTime movies, such as QuickTime VR and wired sprite movies, must have a movie controller associated with them.

What's distinctive about the types of QuickTime movies that require movie controllers is that they are by nature highly interactive. You don't just sit back and watch a QuickTime VR movie; instead, you actively navigate within the node and within the multinode scene. The same is true for wired sprite movies: by and large, the real impact of a wired sprite movie arises from actions the user performs with the mouse. This isn't to deny that you could construct a QuickTime VR movie that navigated itself, sort of like a guided tour through a house or museum and which therefore was minimally interactive. But you couldn't manage the self-navigation without a movie controller.

Still, some kinds of movies can be played back à la carte, as it were, without the assistance of a movie controller. In the next article, we'll take a look at how to use QuickTime's Movie Toolbox to do this and at some of the reasons you might want to do this in the first place.


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.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.