|
Volume Number: 17 (2001)
Issue Number: 06
Column Tag: QuickTime Toolkit
Moving Target
by Tim Monroe
Working with Wired Action Targets
Introduction
In the previous QuickTime Toolkit article, we got our first taste of working with wired actions. We attached some wired actions to sprites, first to construct a set of "movie controller" sprites, and then to make a sprite draggable. In both cases, we accomplished this by inserting one or more event atoms into the sprite atom and then inserting one or more action atoms into those event atoms. In several instances, we also inserted parameter atoms into the action atoms to specify additional information necessary for carrying out the action.
In this article, we're going to investigate wired action targets, which are the objects upon which wired actions are performed. Hitherto, we've relied on the fact that any action that has a target also has a default target, upon which the action will be performed if no object is explicitly targeted by that action. Here, we'll see how actions triggered by an event involving one sprite can be made to operate on another sprite, on another track, or indeed on a sprite or track in another movie. Along the way, we'll see how to set targets for operands and to add support in our applications for passing actions between movies.
Our sample application this time around is called QTActionTargets; its Test menu is shown in Figure 1. This first menu item creates a movie containing two sprites that target each other with actions. The second item adds a sprite track to an existing movie; this sprite track contains a single button that toggles the visibility state of the first text track in that movie. The next four items allow us to get and set movie names and IDs, which come into play when we want to send actions to external movies. The last menu item builds a sprite movie that can send actions to two external QuickTime VR movies at once.
Figure 1: The Test menu of QTActionTargets
Targets
Most wired actions perform some operation on an element of a QuickTime movie; this element is called the action's target. For instance, the kActionSpriteTranslate action alters the matrix of a sprite so as to translate it horizontally and vertically by specified distances. As we noted in the previous article, each wired action has one particular type of target (if it has a target at all, of course). QuickTime currently supports three basic kinds of targets: sprites, tracks, and movies.
We specify an action's target by including an atom of type kActionTarget in the action atom. The target atom in turn includes one or more children that specify the kind of target and the method of specifying that target. As we'll see, there are several ways to specify each kind of action target.
If an action atom does not contain a target atom, then the action is sent to the default target. The default target for an action that operates on a sprite is the sprite that contains the event atom that triggered the action. (Let's call this the default sprite.) Frame-loaded events have no default sprite, so sprite-related actions contained in a frame-loaded event's action atom must have an explicit target. On the other hand, idle events are sent to each sprite in a track, which is the default sprite for any sprite-related actions it contains.
For track actions, the default target is the track that contains the default sprite (or, in the case of frame-loaded events, the track that contains the key frame). For movie actions, the default target is the movie that contains the default track.
Targeting a Sprite
We can specify a sprite target in one of three ways: by name, by ID, or by index. The file Movies.h defines these constants for specifying a sprite target type:
enum { kTargetSpriteName = FOUR_CHAR_CODE(‘spna'), kTargetSpriteID = FOUR_CHAR_CODE(‘spid'), kTargetSpriteIndex = FOUR_CHAR_CODE(‘spin') };
When we target a sprite by name, QuickTime looks at each sprite atom in the target track for a child atom of type kSpriteNameAtomType. If it finds an atom of this type, it inspects the atom data to see whether it's identical to the name specified in the kTargetSpriteName atom. If it is, that sprite becomes the target sprite. If no sprite in the target track has the correct name, the action is not performed.
In general, I prefer to target sprites by ID, largely because we haven't bothered to name our sprites and because the sprite index depends on the order we insert sprites into the sprite track. Listing 1 shows one of the wired sprite utility functions, WiredUtils_AddSpriteIDActionTargetAtom, which we can use to build a sprite ID target atom and insert it into a given action atom.
Listing 1: Targeting a sprite by ID
OSErr WiredUtils_AddSpriteIDActionTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, QTAtomID theSpriteID, QTAtom *theNewTargetAtom) { QTAtom myTargetAtom = 0; OSErr myErr = noErr; if (theNewTargetAtom != NULL) *theNewTargetAtom = 0; myTargetAtom = QTFindChildByIndex(theContainer, theActionAtom, kActionTarget, 1, NULL); if (myTargetAtom == 0) { myErr = QTInsertChild(theContainer, theActionAtom, kActionTarget, 1, 1, 0, NULL, &myTargetAtom); if (myErr != noErr) goto bail; } theSpriteID = EndianU32_NtoB(theSpriteID); myErr = QTInsertChild(theContainer, myTargetAtom, kTargetSpriteID, 1, 1, sizeof(theSpriteID), &theSpriteID, NULL); bail: if (theNewTargetAtom != NULL) *theNewTargetAtom = myTargetAtom; return(myErr); }
WiredUtils_AddSpriteIDActionTargetAtom starts off by looking for an existing target atom in the specified action atom. If none is found, it adds a target atom. In either case, it then inserts a child of type kTargetSpriteID into the target atom, setting the atom data to the sprite ID we pass in (suitably converted to big-endian form, of course).
Let's use WiredUtils_AddSpriteIDActionTargetAtom to build a movie in which two sprites target each other. Figure 2 shows the movie we want to build; clicking on one sprite rotates the other sprite 90˚ clockwise. (In Figure 2, we've clicked on the left-hand sprite once.)
Figure 2: Two sprites targeting each other
We construct this movie in the standard way, creating a sprite track and media and then adding sprite images and samples to the media. Listing 2 shows the code we use to add actions to the left-hand sprite (which has ID 1).
Listing 2: Wiring the left-hand sprite
WiredUtils_AddQTEventAndActionAtoms(mySpriteData, kParentAtomIsContainer, kQTEventMouseClickEndTriggerButton, kActionSpriteRotate, &myActionAtom); myDegrees = EndianS32_NtoB(FixRatio(90, 1)); WiredUtils_AddActionParameterAtom(mySpriteData, myActionAtom, kFirstParam, sizeof(myDegrees), &myDegrees, NULL); WiredUtils_AddSpriteIDActionTargetAtom(mySpriteData, myActionAtom, 2, NULL);
We wire the right-hand sprite in exactly the same way, substituting target ID 1 for 2 in the last line of code.
Targeting a Track
We can specify a target track in one of four ways: by name, by ID, by index, or by index of a particular type. That is, we can look for the second track in a movie, say, or the second video track in that movie. Movies.h defines these constants for specifying track target types:
enum { kTargetTrackName = FOUR_CHAR_CODE(‘trna'), kTargetTrackID = FOUR_CHAR_CODE(‘trid'), kTargetTrackType = FOUR_CHAR_CODE(‘trty'), kTargetTrackIndex = FOUR_CHAR_CODE(‘trin') };
To specify a target by index of a type, we include both kTargetTrackType and kTargetTrackIndex atoms in the target atom.
Once again, we'll want to define some utilities to facilitate creating track targets. Listing 3 defines the WiredUtils_AddTrackNameActionTargetAtom function, which sets a track target, given a name.
Listing 3: Targeting a track by name
OSErr WiredUtils_AddTrackNameActionTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, Str255 theTrackName, QTAtom *theNewTargetAtom) { QTAtom myTargetAtom = 0; OSErr myErr = noErr; if (theNewTargetAtom != NULL) *theNewTargetAtom = 0; myTargetAtom = QTFindChildByIndex(theContainer, theActionAtom, kActionTarget, 1, NULL); if (myTargetAtom == 0) { myErr = QTInsertChild(theContainer, theActionAtom, kActionTarget, 1, 1, 0, NULL, &myTargetAtom); if (myErr != noErr) goto bail; } myErr = QTInsertChild(theContainer, myTargetAtom, kTargetTrackName, 1, 1, theTrackName[0] + 1, theTrackName, NULL); bail: if (theNewTargetAtom != NULL) *theNewTargetAtom = myTargetAtom; return(myErr); }
Listing 4 defines a more general function, WiredUtils_AddTrackTargetAtom, which calls WiredUtils_AddTrackNameActionTargetAtom and two other utilities (which we won't show here). Notice that if theTrackTargetType is kTargetTrackType and theTrackTypeIndex is non-zero, then WiredUtils_AddTrackTargetAtom inserts two target type atoms, as we mentioned above.
Listing 4: Targeting a track
OSErr WiredUtils_AddTrackTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, long theTrackTargetType, void *theTrackTarget, long theTrackTypeIndex) { OSErr myErr = noErr; // allow zero for the default target (the sprite track that received the event) if (theTrackTargetType != 0) { switch (theTrackTargetType) { case kTargetTrackName: { StringPtr myTrackName = (StringPtr)theTrackTarget; myErr = WiredUtils_AddTrackNameActionTargetAtom (theContainer, theActionAtom, myTrackName, NULL); break; } case kTargetTrackID: { long myTrackID = (long)theTrackTarget; myErr = WiredUtils_AddTrackIDActionTargetAtom (theContainer, theActionAtom, myTrackID, NULL); break; } case kTargetTrackType: { OSType myTrackType = (long)theTrackTarget; myErr = WiredUtils_AddTrackTypeActionTargetAtom (theContainer, theActionAtom, myTrackType, NULL); if (myErr != noErr) goto bail; if (theTrackTypeIndex != 0) myErr = WiredUtils_AddTrackIndexActionTargetAtom (theContainer, theActionAtom, theTrackTypeIndex, NULL); break; } case kTargetTrackIndex: { long myTrackIndex = (long)theTrackTarget; myErr = WiredUtils_AddTrackIndexActionTargetAtom (theContainer, theActionAtom, myTrackIndex, NULL); break; } default: myErr = paramErr; } } bail: return(myErr); }
To illustrate how to target a track, we'll create a sprite button that enables or disables the first text track in the movie. Figure 3 shows this button located in the upper-left corner of the movie; clicking it disables (and thus hides) the first text track in the movie, and clicking it again re-enables (and thus re-shows) that track.
Figure 3: A sprite button that targets a text track
Let's see how to wire the sprite to achieve this behavior. First, we want to change the sprite image on mouse-down and mouse-up events, like this:
WiredUtils_AddSpriteSetImageIndexAction(myTextButton, kParentAtomIsContainer, kQTEventMouseClick, 0, NULL, 0, 0, NULL, kTextDownIndex, NULL); WiredUtils_AddSpriteSetImageIndexAction(myTextButton, kParentAtomIsContainer, kQTEventMouseClickEnd, 0, NULL, 0, 0, NULL, kTextUpIndex, NULL);
Next, we want to attach some track enabling and disabling logic to the button. Here we have several possibilities. We could read the value of the kOperandTrackEnabled operand to determine whether the text track is currently enabled or disabled and then call kActionTrackSetEnabled with false or true, accordingly. Or, much more simply, we can add an action atom of type kActionTrackSetEnabled and then set that atom's action flags to toggle the value of the track's enabled state:
WiredUtils_AddQTEventAndActionAtoms(myTextButton, kParentAtomIsContainer, kQTEventMouseClickEndTriggerButton, kActionTrackSetEnabled, &myActionAtom); WiredUtils_AddActionParameterOptions(myTextButton, myActionAtom, 1, kActionFlagActionIsToggle, 0, NULL, 0, NULL);
We specify action flags by inserting an atom of type kActionFlags into the action atom. The atom ID should be the same as the ID of a parameter atom in that action atom, and the atom data is a big-endian long integer that has one or more of these flags set:
enum { kActionFlagActionIsDelta = 1L << 1, kActionFlagParameterWrapsAround = 1L << 2, kActionFlagActionIsToggle = 1L << 3 };
The flag kActionFlagActionIsDelta indicates that the value of the corresponding parameter atom is a relative value, not an absolute value. The flag kActionFlagParameterWrapsAround indicates that a value that exceeds the maximum possible value of the parameter is automatically set to the minimum value (and similarly for values that are less than the minimum value). We set a parameter's minimum and maximum values by inserting atoms of type kActionParameterMinValue and kActionParameterMaxValue into the action atom. The flag kActionFlagActionIsToggle indicates that the parameter value should be toggled between two possible states. In this case, the actual value specified in the parameter atom is ignored. As a result, we haven't bothered to actually add a parameter atom to the kActionTrackSetEnabled action atom.
Finally, we call WiredUtils_AddTrackTargetAtom to set the sprite action's target to the first text track in the movie, like this:
WiredUtils_AddTrackTargetAtom(myTextButton, myActionAtom, kTargetTrackType, (void *)TextMediaType, 1);
Movie-to-Movie Communication
Now we have a good idea of how to target specific sprites and tracks in a movie. One of the neat additions in QuickTime 4.0 was the ability also to target external movies. That is to say, events related to a sprite in one movie are able to trigger actions that operate on a sprite or track or movie property in another movie. This ability is called movie-to-movie communication or intermovie communication. In this section, we'll see how to target elements of external movies. On one level, this is a very simple thing to do, as it's just another exercise in adding target atoms to action atoms. It turns out, however, that the movie controller needs help resolving target atoms when they pick out external movies. As a result, we'll need to add some code to QTActionTargets to allow it to route actions from one open movie to another. This is a somewhat more complicated undertaking, so let's proceed carefully.
Adding External Movie Targets
First, the easy stuff. We can specify an external movie target in one of two ways: by name or by ID. The file Movies.h defines these two constants for specifying a movie target type:
enum { kTargetMovieName = FOUR_CHAR_CODE(‘mona'), kTargetMovieID = FOUR_CHAR_CODE(‘moid') };
To target an action at an object in an external movie specified by name, for instance, we add an atom of type kTargetMovieName to the action atom. The target atom data is the movie's target name, stored as a Pascal string. Listing 5 shows the function WiredUtils_AddMovieNameActionTargetAtom, which we can use to add the appropriate target atom to an action atom.
Listing 5: Targeting a movie by name
OSErr WiredUtils_AddMovieNameActionTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, Str255 theMovieName, QTAtom *theNewTargetAtom) { QTAtom myTargetAtom = 0; OSErr myErr = noErr; if (theNewTargetAtom != NULL) *theNewTargetAtom = 0; myTargetAtom = QTFindChildByIndex(theContainer, theActionAtom, kActionTarget, 1, NULL); if (myTargetAtom == 0) { myErr = QTInsertChild(theContainer, theActionAtom, kActionTarget, 1, 1, 0, NULL, &myTargetAtom); if (myErr != noErr) goto bail; } myErr = QTInsertChild(theContainer, myTargetAtom, kTargetMovieName, 1, 1, theMovieName[0] + 1, theMovieName, NULL); bail: if (theNewTargetAtom != NULL) *theNewTargetAtom = myTargetAtom; return(myErr); }
The function WiredUtils_AddMovieIDActionTargetAtom (not shown here) is entirely analogous, except that the target atom data is a signed long integer specifying the target movie's ID.
Specifying Movie Target Names and IDs
So how do we specify a movie's target name or ID? The movie target name is not the name of the movie file that it's contained in (which is good, since some movies exist only in memory or only as a network stream). Rather, the movie target name or ID is stored as a piece of movie user data of type ‘plug'. To specify a movie's target name, we add a piece of movie user data of type ‘plug' whose item data is a string of characters of this form:
moviename="name"
And to specify a movie's target ID, we add a piece of movie user data of type ‘plug' whose item data is a string of characters of this form:
movieid="ID"
User data of type ‘plug' was introduced originally for use by the QuickTime web browser plug-in, for storing parameters for movies embedded in web pages. For instance, to hide the movie controller bar of an embedded movie, we can attach a piece of ‘plug' user data to the movie whose item data is this string of characters:
CONTROLLER="False"
Similarly, we can make a web-based movie loop during playback by attaching a piece of ‘plug' user data with this data:
LOOP="True"
(QuickTime Player ignores all user data items of type ‘plug' except for moviename and movieid.)
The data in a user data item of type ‘plug' consists of a tag, the equality sign (=), and a value that is enclosed in quotation marks ("). (In fact, the quotation marks are optional, but they're highly recommended.) The tag and value are strings of characters; case does not matter in these strings, so "moviename" is treated the same as "MOVIENAME" (or as any of the other 510 possibilities). In our code, we'll always create all lowercase tags, using these constants:
#define kMovieNamePrefix "moviename=" #define kMovieIDPrefix "movieid="
Listing 6 defines the function QTUtils_SetMovieTargetName, which we can use to attach a specific target name to a movie. The meat of this function consists of concatenating the prefix kMovieNamePrefix, a quotation mark, the specified target name, and a terminating quotation mark into a single string, which we set as the user item data.
Listing 6: Setting a movie's target name
OSErr QTUtils_SetMovieTargetName (Movie theMovie, char *theTargetName) { UserData myUserData = NULL; char *myString = NULL; Handle myHandle = NULL; OSErr myErr = noErr; // make sure we've got a movie and a name if ((theMovie == NULL) || (theTargetName == NULL)) return(paramErr); // get the movie's user data list myUserData = GetMovieUserData(theMovie); if (myUserData == NULL) return(paramErr); // remove any existing movie target name while (QTUtils_FindUserDataItemWithPrefix(myUserData, FOUR_CHAR_CODE(‘plug'), kMovieNamePrefix) != 0) RemoveUserData(myUserData, FOUR_CHAR_CODE(‘plug'), QTUtils_FindUserDataItemWithPrefix(myUserData, FOUR_CHAR_CODE(‘plug'), kMovieNamePrefix)); // create the user data item data myString = malloc(strlen(kMovieNamePrefix) + strlen(theTargetName) + 2 + 1); if (myString != NULL) { myString[0] = ‘\0'; strcat(myString, kMovieNamePrefix); strcat(myString, "\""); strcat(myString, theTargetName); strcat(myString, "\""); // add in a new user data item PtrToHand(myString, &myHandle, strlen(myString)); if (myHandle != NULL) myErr = AddUserData(myUserData, myHandle, FOUR_CHAR_CODE(‘plug')); } else { myErr = memFullErr; } free(myString); if (myHandle != NULL) DisposeHandle(myHandle); return(myErr); }
Notice that QTUtils_SetMovieTargetName first removes any existing movie target name user items by repeatedly calling QTUtils_FindUserDataItemWithPrefix and RemoveUserData. QTUtils_FindUserDataItemWithPrefix is defined in Listing 7. It simply inspects all user data items of a specified type to see whether any of them begins with the specified string. When the first matching item is found, the index of that item is returned to the caller.
Listing 7: Finding a user data item
static long QTUtils_FindUserDataItemWithPrefix (UserData theUserData, OSType theType, char *thePrefix) { Handle myData = NULL; long myCount = 0; long myIndex = 0; long myItemIndex = 0; OSErr myErr = noErr; // make sure we've got some valid user data if (theUserData == NULL) goto bail; // allocate a handle for GetUserData myData = NewHandle(0); if (myData == NULL) goto bail; myCount = CountUserDataType(theUserData, theType); for (myIndex = 1; myIndex <= myCount; myIndex++) { myErr = GetUserData(theUserData, myData, theType, myIndex); if (myErr == noErr) { if (GetHandleSize(myData) < strlen(thePrefix)) continue; // see if the user data begins with the specified prefix if (IdenticalText(*myData, thePrefix, strlen(thePrefix), strlen(thePrefix), NULL) == 0) { myItemIndex = myIndex; goto bail; } } } bail: if (myData != NULL) DisposeHandle(myData); return(myItemIndex); }
IdenticalText (né IUMagIDPString) is a Text Utilities function that compares two strings to see whether they are identical; it is case-insensitive and ignores any diacritical marks (such as the accent in "né").
Retrieving Movie Target Names and IDs
We know how to set a movie's target name or ID, so that actions in other movies can be targeted at it. Let's now see how to get a movie's target name or ID. The basic idea is simple enough: just loop through all user data items of type ‘plug' until we find one that begins with either kMovieNamePrefix or kMovieIDPrefix; then extract the value of that user data item. Listing 8 shows the QTUtils_GetMovieTargetName function. (QTUtils_GetMovieTargetID is entirely analogous, except that it also returns a Boolean value that indicates whether the specified movie actually has a movie target ID.)
Listing 8: Getting a movie's target name
char *QTUtils_GetMovieTargetName (Movie theMovie) { UserData myUserData = NULL; char *myString = NULL; // 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) goto bail; // find the value of the user data item of type ‘plug' that begins with // the string "moviename=" myString = QTUtils_GetUserDataPrefixedValue(myUserData, FOUR_CHAR_CODE(‘plug'), kMovieNamePrefix); bail: return(myString); }
As you can see, QTUtils_GetMovieTargetName calls QTUtils_GetUserDataPrefixedValue, defined in Listing 9. QTUtils_GetUserDataPrefixedValue calls QTUtils_FindUserDataItemWithPrefix to find the index of the ‘plug' user data item that begins with the specified prefix. If it finds one, then it returns the value in the item's data, making sure to strip off any quotation marks surrounding the value.
Listing 9: Getting a user data item value
static char *QTUtils_GetUserDataPrefixedValue (UserData theUserData, OSType theType, char *thePrefix) { long myIndex = 0; Handle myData = NULL; long myLength = 0; long myOffset = 0; char *myString = NULL; OSErr myErr = noErr; if (theUserData == NULL) goto bail; // allocate a handle for GetUserData myData = NewHandle(0); if (myData == NULL) goto bail; myIndex = QTUtils_FindUserDataItemWithPrefix(theUserData, theType, thePrefix); if (myIndex > 0) { myErr = GetUserData(theUserData, myData, theType, myIndex); if (myErr == noErr) { if ((*myData)[strlen(thePrefix)] == ‘"‘) { myLength = GetHandleSize(myData) - strlen(thePrefix) - 2; myOffset = 1; } else { myLength = GetHandleSize(myData) - strlen(thePrefix); myOffset = 0; } myString = malloc(myLength + 1); if (myString != NULL) { memcpy(myString, *myData + strlen(thePrefix) + myOffset, myLength); myString[myLength] = ‘\0'; } } } bail: if (myData != NULL) DisposeHandle(myData); return(myString); }
Finding Movie Targets
Remember that we don't just want our applications to be able to create wired actions with external movie targets, but we also want them to be able to play those movies back correctly, routing actions to the appropriate external targets. This raises a complication: although a movie controller is able to find target sprites and tracks inside of the movie it's associated with, it isn't able to find targets in external movies. For that, it needs help from the application.
When a movie controller determines that it needs to find an external movie target for some wired action, it sends itself the mcActionGetExternalMovie movie controller action. The idea is that the application will intercept that action in its movie controller action filter function, find the specified target movie, and return information about that movie to the movie controller. Let's see how QTActionTargets handles all this.
When our movie controller action filter function receives the mcActionGetExternalMovie action, the first parameter is the movie controller issuing the action and the third parameter is a pointer to an external movie record, of type QTGetExternalMovieRecord:
struct QTGetExternalMovieRecord { long targetType; StringPtr movieName; long movieID; MoviePtr theMovie; MovieControllerPtr theController; };
The targetType field specifies the kind of target that the movie controller wants information about; on entry to our movie controller action filter function, it's set to either kTargetMovieName or kTargetMovieID. If targetType is kTargetMovieName, then the movieName field specifies the name of the movie to look for. If targetType is kTargetMovieID, then the movieID field specifies the ID of the movie to look for.
Our job is to find the target movie and return both it and its associated movie controller in the theMovie and theController fields of the external movie record. If we cannot find the target movie, then we should set both theMovie and theController to NULL. Listing 10 shows the movie controller action filter function in QTActionTargets.
Listing 10: Handling movie controller actions
PASCAL_RTN Boolean QTApp_MCActionFilterProc (MovieController theMC, short theAction, void *theParams, long theRefCon) { Boolean isHandled = false; WindowObject myWindowObject = NULL; myWindowObject = (WindowObject)theRefCon; if (myWindowObject == NULL) return(isHandled); switch (theAction) { // handle window resizing case mcActionControllerSizeChanged: QTFrame_SizeWindowToMovie(myWindowObject); break; // handle idle events case mcActionIdle: QTApp_Idle((**myWindowObject).fWindow); break; // handle get-external-movie requests case mcActionGetExternalMovie: QTFrame_FindExternalMovieTarget(theMC, (QTGetExternalMoviePtr)theParams); break; // some lines missing here; see Listings 13 and 14 default: break; } // switch (theAction) return(isHandled); }
As you can see, when we receive an mcActionGetExternalMovie action, we call the application-defined function QTFrame_FindExternalMovieTarget. It loops through all open movie windows belonging to our application and (depending on the value of the targetType field) looks for a movie having the correct name or ID. QTFrame_FindExternalMovieTarget is defined in Listing 11.
Listing 11: Finding a target movie
void QTFrame_FindExternalMovieTarget (MovieController theMC, QTGetExternalMoviePtr theEMRecPtr) { WindowReference myWindow = NULL; Movie myTargetMovie = NULL; MovieController myTargetMC = NULL; Boolean myFoundIt = false; if (theEMRecPtr == NULL) return; // loop through all open movies until we find the one requested myWindow = QTFrame_GetFrontMovieWindow(); while (myWindow != NULL) { Movie myMovie = NULL; MovieController myMC = NULL; myMC = QTFrame_GetMCFromWindow(myWindow); #if ALLOW_SELF_TARGETING if (myMC != NULL) { #else if ((myMC != NULL) && (myMC != theMC)) { #endif myMovie = MCGetMovie(myMC); if (theEMRecPtr->targetType == kTargetMovieName) { char *myStr = NULL; myStr = QTUtils_GetMovieTargetName(myMovie); if (myStr != NULL) { if (IdenticalText(&theEMRecPtr->movieName[1], myStr, theEMRecPtr->movieName[0], strlen(myStr), NULL) == 0) myFoundIt = true; free(myStr); } } if (theEMRecPtr->targetType == kTargetMovieID) { long myID = 0; Boolean myMovieHasID = false; myID = QTUtils_GetMovieTargetID(myMovie, &myMovieHasID); if ((theEMRecPtr->movieID == myID) && myMovieHasID) myFoundIt = true; } if (myFoundIt) { myTargetMovie = myMovie; myTargetMC = myMC; break; // break out of while loop } } myWindow = QTFrame_GetNextMovieWindow(myWindow); } // while *theEMRecPtr->theMovie = myTargetMovie; *theEMRecPtr->theController = myTargetMC; }
QTFrame_FindExternalMovieTarget uses our framework functions QTFrame_GetFrontMovieWindow and QTFrame_GetNextMovieWindow to loop through the open movie windows; it also uses the QTActionTargets functions QTUtils_GetMovieTargetName and QTUtils_GetMovieTargetID to find the target name or ID of the movie in each of those windows. (These latter two functions are defined in the file QTActionTargets.c, but they are generally useful and should migrate to QTUtilities.c.)
There is one interesting question we need to consider: should a movie be allowed to target itself using external movie targets? That is to say, when we attach a target atom of type kTargetMovieName or kTargetMovieID to some wired action, should we allow the targeted movie to be the same movie that contains that wired action? On one hand, targeting oneself using movie targets is outrageously inefficient; the movie controller needs to call the application's movie controller action filter function, and the application needs to go looking at the user data of all open movie windows (at least until it finds the targeted movie). If our goal is to target the movie that contains the action, we're better off just omitting any movie target atom from that action and thus using the default movie target. On the other hand, we might think: "why not allow it?" After all, a target ID might be obtained dynamically by evaluating some expression, which (as it happens at run time) picks out the very movie that contains the wired action container. As long as we're clever about it, efficiency should not be a real problem. And no doubt some interesting effects can be achieved if we allow self-targeting.
Personally, I'm inclined to think that a movie should be able to target itself. Unfortunately, QuickTime Player seems to think differently; it does not currently allow wired actions to target objects in the same movie using an explicit movie target. So I've written QTFrame_FindExternalMovieTarget to provide either capability, depending on the setting of the compiler flag ALLOW_SELF_TARGETING. You decide how you want your application to behave.
Controlling External Movies
Let's build a movie with some sprites that target an external movie. In fact, let's build a movie with some sprites that target two external movies at the same time. Figure 4 illustrates what I want to achieve. This is a web browser window that contains two QuickTime VR panorama movies and a sprite movie. The top movie is a panorama of the Donner Lake area (in California) shot during the summer. The bottom movie is a panorama of the same area shot during the winter. Each of these panoramas can be individually controlled in the standard ways (for example, panned left or right by dragging the cursor horizontally). But these panoramas can also be controlled in tandem using the middle movie, which is a sprite movie containing six sprites.
Figure 4: A sprite movie controlling two QuickTime VR movies
This sprite movie consists of a single sprite track with buttons that perform these actions (from left to right): pan left, tilt down, zoom out, zoom in, tilt up, and pan right. The VR controller sprite track is constructed in exactly the same way as the linear controller sprite track we built in the previous QuickTime Toolkit article, except that the sprite images are different and the actions issued by each button are VR actions (for instance, kActionQTVRSetTiltAngle). Also, I've wired the buttons to respond to mouse-over events, not mouse-click events.
Targeting two external movies with the same sprite action is extremely simple. Let's suppose that the summer and winter panoramas have the target names "Summer" and "Winter", respectively. Then we can wire the "Pan Left" sprite using the code shown in Listing 12.
Listing 12: Wiring a sprite to control two external movies
#define kTarget1 "Summer" #define kTarget2 "Winter" StringPtr myMovieNames[2]; myMovieNames[0] = QTUtils_ConvertCToPascalString(kTarget1); myMovieNames[1] = QTUtils_ConvertCToPascalString(kTarget2); for (myCount = 0; myCount <= 1; myCount++) { SpriteUtils_SetSpriteData(myPanLeftButton, &myLocation, &isVisible, &myLayer, &myIndex, NULL, NULL, NULL); WiredUtils_AddSpriteSetImageIndexAction(myPanLeftButton, kParentAtomIsContainer, kQTEventMouseEnter, 0, NULL, 0, 0, NULL, kPanLeftDownIndex, NULL); WiredUtils_AddSpriteSetImageIndexAction(myPanLeftButton, kParentAtomIsContainer, kQTEventMouseExit, 0, NULL, 0, 0, NULL, kPanLeftUpIndex, NULL); WiredUtils_AddSpriteTrackSetVariableAction(myPanLeftButton, kParentAtomIsContainer, kQTEventMouseEnter, kMouseOverPanLeftVariableID, 1, 0, NULL, 0); WiredUtils_AddSpriteTrackSetVariableAction(myPanLeftButton, kParentAtomIsContainer, kQTEventMouseExit, kMouseOverPanLeftVariableID, 0, 0, NULL, 0); QTTarg_AddIdleEventVarTestAction(myPanLeftButton, kParentAtomIsContainer, kMouseOverPanLeftVariableID, 1, kActionQTVRSetTiltAngle, &myActionAtom); WiredUtils_AddActionParameterAtom(myPanLeftButton, myActionAtom, kFirstParam, sizeof(myDeltaAngle), &myDeltaAngle, NULL); WiredUtils_AddActionParameterOptions(myPanLeftButton, myActionAtom, 1, kActionFlagActionIsDelta, 0, NULL, 0, NULL); WiredUtils_AddMovieNameActionTargetAtom(myPanLeftButton, myActionAtom, myMovieNames[myCount], NULL); }
The key element here is the call to WiredUtils_AddMovieNameActionTargetAtom, which adds the appropriate movie name target atom to the action atom. You'll also notice that we call the application function QTTarg_AddIdleEventVarTestAction, which installs an idle event action that checks to see whether the specified variable (in this case, the one with ID kMouseOverPanLeftVariableID) has the value specified in the fourth parameter; whenever it does, the specified action is executed. (See QTActionTargets.c for the complete listing of QTTarg_AddIdleEventVarTestAction.)
Retrieving a Movie's Own Target Name and ID
QuickTime 5 recently introduced two new operands related to movie target names and IDs, kOperandMovieName and kOperandMovieID. Wired actions can use these operands to determine the name and ID of a target movie. Once again, the movie controller handling these operands needs some help from our application to get the desired information. (In a sense, this is the flip-side of the task we considered earlier. In that case, a movie controller had a movie's target name or ID and we needed to find the movie and movie controller associated with that name or ID. In the present case, the movie controller is known and we need to find the movie target name or ID associated with that movie controller.)
When a movie controller encounters the kOperandMovieName or kOperandMovieID operand and hence needs to get a movie's name or ID, it sends the mcActionGetMovieName or mcActionGetMovieID movie controller action to the movie controller associated with that movie. Our application should respond to these actions by returning the movie target name or ID. Let's consider first the mcActionGetMovieID action. When our movie controller action filter function receives this action, the theParams parameter is a pointer to a long integer. If the movie indeed has a movie target ID, we should return that ID in that long integer and also return true as the return value of our filter function. Otherwise, we should return false as the return value of our filter function. Listing 13 shows the lines of code in our movie controller action filter function that handle the mcActionGetMovieID action.
Listing 13: Getting a movie's target ID
case mcActionGetMovieID: myMovie = (**myWindowObject).fMovie; if (myMovie != NULL) { long myID; Boolean myMovieHasID = false; // get the target name of the current movie myID = QTUtils_GetMovieTargetID(myMovie, &myMovieHasID); if (myMovieHasID) { *(long *)theParams = myID; isHandled = true; } } break;
As you can see, this code uses the QTUtils_GetMovieTargetID function to get the target ID of the current movie. If the movie has no target ID, we don't put any value into theParams and we allow the filter function to return the default value of false.
When our movie controller action filter function receives the mcActionGetMovieName action, the theParams parameter is a handle to a Pascal string. In this case, we need to determine the target name of the movie and copy it into that Pascal string. Listing 14 shows the code that handles this.
Listing 14: Getting a movie's target ID
case mcActionGetMovieName: myMovie = (**myWindowObject).fMovie; if (myMovie != NULL) { char *myName = NULL; Handle myNameHandle = (Handle)theParams; // get the target name of the current movie myName = QTUtils_GetMovieTargetName(myMovie); if ((myName != NULL) || (myNameHandle != NULL)) { // reset the size of the handle passed to us to hold the movie target name SetHandleSize(myNameHandle, strlen(myName + 1)); if (MemError() == noErr) { // copy the movie target name into the resized handle BlockMove(myName, *myNameHandle + 1, strlen(myName)); *myNameHandle[0] = strlen(myName); isHandled = true; } } free(myName); } break;
Here, we use our function QTUtils_GetMovieTargetName (defined in Listing 8) to get the movie target name. Notice that we need to resize the handle passed to us in order to ensure that it's large enough to hold the name we pass back.
Operand Targets
Recall that wired action operands typically retrieve properties of some element of a movie, such as a track's height, a movie's duration, a sprite's image index, a sprite track variable's value, and so forth. The element of the movie whose property is to be retrieved is the operand target. As with actions, each operand that has a target also has a default target. But also as with actions, it's possible to specify some other target for an operand by inserting a target atom into the operand atom. So, we can say things like: get me the image index of the sprite named "Old QuickTime icon" in the second sprite track of the movie whose target ID is 11. In this section, we're going to play a little with operand targets; along the way, we'll discover that they can be a bit trickier than they seem.
Let's begin by targeting a wired action (and not an operand) at an object in an external movie. Consider once again the moving icon movie that we constructed in "A Goofy Movie" (in MacTech, March 2001), shown in Figure 5. Let's assume that this movie contains the ‘plug' user data necessary to set its target name to "Icon Movie".
Figure 5: The moving icon movie
Now suppose that we've created some other movie with a sprite track, maybe the one shown earlier in Figure 3. (Let's also assume that the target name of this movie is "Button Movie".) We want to rewire the sprite button so that clicking on the button changes the image index of the sprite in the moving icon movie. When we first open the moving icon movie, the image index of the icon sprite is 1. So let's reset that index to 2.
With the machinery we have at hand, this is pretty straightforward: we just insert a kActionSpriteSetImageIndex action atom into the button sprite atom, together with a parameter atom whose atom data is the constant 2. We also add to the action atom a target atom that picks out the sprite with ID 1 in the first sprite track in the movie whose target name is "Icon Movie". Listing 15 shows our revised wiring.
Listing 15: Setting the image index of an external sprite
#define kTargetName "Icon Movie" WiredUtils_AddSpriteSetImageIndexAction(myTextButton, kParentAtomIsContainer, kQTEventMouseClick, 0, NULL, 0, 0, NULL, kTextDownIndex, NULL); WiredUtils_AddSpriteSetImageIndexAction(myTextButton, kParentAtomIsContainer, kQTEventMouseClickEnd, 0, NULL, 0, 0, NULL, kTextUpIndex, NULL); WiredUtils_AddQTEventAndActionAtoms(myTextButton, kParentAtomIsContainer, kQTEventMouseClickEndTriggerButton, kActionSpriteSetImageIndex, &myActionAtom); myIndex = EndianS16_NtoB(2); WiredUtils_AddActionParameterAtom(myTextButton, myActionAtom, kFirstParam, sizeof(myIndex), &myIndex, NULL) WiredUtils_AddTrackAndSpriteTargetAtoms(myTextButton, myActionAtom, kTargetTrackType, (void *) SpriteMediaType, 1, kTargetSpriteID, (void *)1); myString = QTUtils_ConvertCToPascalString(kTargetName); WiredUtils_AddMovieTargetAtom(myTextButton, myActionAtom, kTargetMovieName, (void *)myString); free(myString);
So far, so good. Now let's see what happens if we try to specify the new image index using an expression that contains an operand. For instance, let's try to read the image index from a sprite track variable. First, of course, we need to initialize the variable; we can do this with a frame-loaded event action, like so:
float myVariableValue = 2; QTAtomID myVariableID = 1234; WiredUtils_AddSpriteTrackSetVariableAction(mySample, kParentAtomIsContainer, kQTEventFrameLoaded, myVariableID, myVariableValue, 0, NULL, 0);
Now we need to revise the sprite button wiring to use that variable instead of the hard-coded constant 2. Listing 16 shows our first try.
Listing 16: Setting the image index of an external sprite using a variable
WiredUtils_AddQTEventAndActionAtoms(myTextButton, kParentAtomIsContainer, kQTEventMouseClickEndTriggerButton, kActionSpriteSetImageIndex, &myActionAtom); QTInsertChild(myTextButton, myActionAtom, kActionParameter, 0, kFirstParam, 0, NULL, &myParamAtom); QTInsertChild(myTextButton, myParamAtom, kExpressionContainerAtomType, 1, 1, 0, NULL, &myExpressionAtom); QTInsertChild(myTextButton, myExpressionAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom); QTInsertChild(myTextButton, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, 0, NULL, &myOperandTypeAtom); myVariableID = EndianU32_NtoB(myVariableID); QTInsertChild(myTextButton, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); WiredUtils_AddTrackAndSpriteTargetAtoms(myTextButton, myActionAtom, kTargetTrackType, (void *) SpriteMediaType, 1, kTargetSpriteID, (void *)1); myString = QTUtils_ConvertCToPascalString(kTargetName); WiredUtils_AddMovieTargetAtom(myTextButton, myActionAtom, kTargetMovieName, (void *)myString); free(myString);
There's only one problem: this doesn't work. The reason is that the sprite button movie's movie controller sees that the kActionSpriteSetImageIndex action is targeted at the icon movie, so it sends the entire action atom over to the movie controller of the icon movie for handling. When the movie controller of the icon movie tries to get the value of the sprite track variable with ID 1234, it discovers that there is no such variable defined yet. In that case, it sets the icon's image to 0 (which is the default value of all uninitialized sprite track variables). Instead of changing the image index to 2, the movie controller changes it to 0 and the icon sprite promptly disappears. Oops.
This is where operand targets come into play. What we need to do is attach a target atom to the kOperandSpriteTrackVariable operand that picks out the sprite track that contains the sprite button (that is, the track in which we set the value of the variable with ID 1234). Listing 17 shows the essential part of our re-revised wiring.
Listing 17: Adding a target to an operand atom
#define kButtonName "Button Movie" // explicitly target the sprite track and movie for the operand myString = QTUtils_ConvertCToPascalString(kButtonName); WiredUtils_AddTrackAndSpriteTargetAtoms(myTextButton, myOperandTypeAtom, kTargetTrackType, (void *)SpriteMediaType, 1, kTargetSpriteID, (void *)1); WiredUtils_AddMovieTargetAtom(myTextButton, myOperandTypeAtom, kTargetMovieName, (void *)myString); free(myString);
This works fine, but it seems to violate the "no self-targeting" rule adopted by QuickTime Player. After all, the sprite track in the movie "Button Movie" explicitly targets the movie "Button Movie". So we might expect this wiring to work in QTActionTargets but not in QuickTime Player, right? Nope, it works fine in both applications. Remember that when the movie controller of the button movie detects an action targeted at an external movie, it sends the action atom off to the movie controller of that movie. The target atom in the operand atom is evaluated and resolved only by the movie controller of the icon movie, in which case it's not a self-reference at all; rather, it's now targeting the sprite button movie. So both applications, QTActionTargets and QuickTime Player, are able to find the track containing the relevant variable and retrieve its value.
There are a couple of important lessons to take away from all this. First and foremost, action targets and operand targets are resolved only at run time. As a result, the target of an action or operand can change if some of the properties of the movie change. (For instance, the sprite with a certain ID might differ from the sprite with that same ID, if the movie time is changed so that a new key frame of the sprite track is loaded.) Also, a target atom contained in an action or operand atom is always interpreted relative to any target atoms contained in the parents of that action or operand atom. If a parent action or operand atom changes its target to some other sprite, track, or movie, then all its children will inherit that change. The only way to be absolutely sure that we know what an action or operand is targeting is to build the atom hierarchy very carefully or to give a full, explicit target.
Movie-in-Movie Communication
Let's finish up with something perhaps a little less dizzying. In an earlier article ("The Atomic Café" in MacTech, September 2000), we learned how use the movie media handler to embed one movie (called the child movie) inside of another movie (the parent movie). The main advantage of using this movie-in-movie capability is that the parent and child movies can have independent playback characteristics. This means that the looping state or the playback rate of the parent movie can differ from the looping state or the playback rate of the child movie.
We embed one movie inside of another movie by creating a movie track, whose samples are atom containers that contain atoms that pick out the child movies and specify their playback characteristics. (See the earlier article for more complete details on creating movie tracks.) In this section, we'll see how to work with embedded movies and wired actions. Things work pretty much as you'd expect, but there are a couple of new capabilities that deserve attention.
Specifying Movie-in-Movie Targets
We can send wired actions from a parent movie to a child movie and from a child movie to a parent movie. If we want to send a wired action from a child to a parent, we can target the parent using one of these two target types:
enum { kTargetRootMovie = FOUR_CHAR_CODE(‘moro'), kTargetParentMovie = FOUR_CHAR_CODE(‘mopa') };
A child's parent movie, of course, is the movie that contains the movie track that references the child movie. The root movie is the movie at the top of the parent/child hierarchy. When a parent movie contains a child movie that does not itself contain any movie tracks, then the parent and the root are the same. But a child movie can easily contain its own children (it is after all a movie, so it can contain movie tracks), and this containment hierarchy can go indefinitely deep. So it's useful to have a way to target the movie at the root of the hierarchy, and that's what kTargetRootMovie gives us.
A child movie is a movie that's picked out by a data reference contained in a sample in a movie track. So it has a sort of "dual nationality": we can target it as if it were a movie or as if it were a track. To target it as a movie, we can target its movie ID or its movie name. And to target it as a track, we can target its track ID, its track name, or its track index. There are therefore five ways for a parent movie (or some element of a parent movie) to target a child movie. The file Movies.h defines these five constants for specifying a child movie target type:
enum { kTargetChildMovieTrackName = FOUR_CHAR_CODE(‘motn'), kTargetChildMovieTrackID = FOUR_CHAR_CODE(‘moti'), kTargetChildMovieTrackIndex = FOUR_CHAR_CODE(‘motx'), kTargetChildMovieMovieName = FOUR_CHAR_CODE(‘momn'), kTargetChildMovieMovieID = FOUR_CHAR_CODE(‘momi') };
These are all easy enough to figure out, except perhaps for kTargetChildMovieTrackIndex. An atom of this type targets a track with a specific index in a movie, which had better be a movie track (that is, a track of type MovieMediaType). As far as I know, we cannot directly target a child movie by its index among just the movie tracks.
Using Movie-Loaded Events
QuickTime 4.1, which introduced the movie-in-movie capability and the five new target types, also introduced a new kind of event for triggering wired actions, the movie-loaded event. A movie-loaded event (of type kQTEventMovieLoaded) is issued whenever a child movie is loaded into a parent movie. We can construct wired event handlers to look for this event and execute wired actions when it is received. The movie-loaded event is particularly useful for configuring the child movie based on some of the current properties of its parent, or conversely for configuring the parent based on some of the properties of the child.
Event atoms of type kQTEventMovieLoaded can be put into the parent movie or into a child movie. In a parent movie, a movie-loaded event atom is placed into the atom container that comprises a sample in a movie track, as a sibling of the kMovieMediaDataReference atom (which picks out the child movie). In a child movie, a movie-loaded event atom is inserted into the movie property atom. A movie property atom is an atom container that contains information about the movie's properties, in just the same way that a track's media property atom (which we encountered in "A Goofy Movie", cited earlier) contains information about the properties of the track and its media. We can insert a movie-loaded event and its accompanying actions into a movie property atom by calling the GetMoviePropertyAtom function, inserting the atom, and then calling SetMoviePropertyAtom.
If a parent movie and a child movie both contain movie-loaded event atoms, then the actions in the child movie's event atom are executed first, followed by those in the parent movie's event atom. That is to say, the parent gets the final word here. (Who says movie don't accurately portray reality?) For instance, if both the child movie and the parent movie set the value of some sprite track variable, then the parent's assignment overrules the child's.
Conclusion
In this article, we've touched on just about every possible way of targeting a wired action or operand at some element of a movie, no matter where that element resides. The target could be in the same track as the event atom that triggers the action or in another track; it could also be in an external movie or in a child or parent movie. In all these cases, we link the action or operand to the target by inserting an atom of type kActionTarget into the action or operand atom. The movie controller is responsible for resolving these target atoms at run time and dispatching the actions or operands to the appropriate target. We've also seen that the movie controller sometimes needs our help in finding targets; if an action or operand targets an external movie, our application needs to step in and find the specified target movie.
In the next article, we'll take one final look at wired actions, at least for the moment. We'll see that wired action atoms can be associated with objects other than sprites, and we'll investigate some of the newer event types and actions.
Credits
Thanks are due to Steve Dawson for permission to use his excellent panoramas of the Donner Lake area.
Tim Monroe works in Apple's QuickTime engineering group. You can contact him at monroe@apple.com.
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine