Authoring Wired Movies and Sprite Animations

This chapter describes how you can author wired movies and sprite animations using the sprite media handler. You need to be familiar with the material in the previous two chapters, Chapter 2, QuickTime Sprites, Sprite Animation and Wired Movies, and Chapter 3, Sprite Media Handler, in order to take advantage of the techniques described in this chapter.

You use the functions provided by the sprite media handler to create and manipulate a sprite animation as a track in a QuickTime movie. You can also use the functions provided by the sprite media handler to create and manipulate a wired sprite movie, with various types of user interactivity. The chapter is illustrated with code snippets from the sample program, QTWiredSprites.c, which is listed in full in Appendix A of this book.

The chapter is divided into the following major sections:

For API reference information about the constants and functions available to your application, refer to the QuickTime API Reference, which is available at

http://developer.apple.com/documentation/Quicktime/QuickTime.html

Authoring Movies With the Sprite Media Handler

The sprite media handler provides functions that allow an application to create and manipulate a sprite animation as a track in a QuickTime movie.

The following sections are illustrated with code from the sample program QTWiredSprites.c, which creates a 320 by 240 pixel QuickTime movie with one sprite track. The sprite track contains six sprites, including two penguins and four buttons. The sample program, which takes advantage of wired sprites, is explained in greater detail Authoring Wired Movies. A partial code listing is available in Appendix A. You can download the full sample code at QuickTime website at

http://www.apple.com/quicktime/developers/samplecode.html#sprites.

Defining a Key Frame Sample

To create a sprite track in a QuickTime movie, you must first create the movie itself, a track to contain the sprites, and the track’s media.

After doing this, you can define a key frame sample. A key frame sample defines the number of sprites, their initial property values, and the shared image data used by the sprites in the key frame sample and in all override samples that follow the key frame sample. The sample code discussed in this section creates a single key frame sample and shows how to add images for other sprites, as well as actions for other sprites.

Creating the Movie, Sprite Track, and Media

Listing 4-1 shows a code fragment from the sample code QTWiredSprites.c. This sample code, which is available in Appendix A, illustrates how you can create a new movie file that calls a sample code function, AddSpriteTrackToMovie, which is responsible for creating a sprite track and adding it to the movie.

Listing 4-1  Creating a sprite track movie

// Create a QuickTime movie containing a wired sprites track
.
.
.
    // create a new movie file and set its controller type
    // ask the user for the name of the new movie file
    StandardPutFile("\pSprite movie file name:", "\pSprite.mov",
                    &myReply);
    if (!myReply.sfGood)
        goto bail;
 
    // create a movie file for the destination movie
    myErr = CreateMovieFile(&myReply.sfFile, FOUR_CHAR_CODE('TVOD'), 0,
                            myFlags, &myResRefNum, &myMovie);
    if (myErr != noErr)
        goto bail;
 
    // select the "no controller" movie controller
    myType = EndianU32_NtoB(myType);
    SetUserDataItem(GetMovieUserData(myMovie), &myType, sizeof(myType),
                    kUserDataMovieControllerType, 1);

The following code fragment from AddSpriteTrackToMovie (Listing 4-2) creates a new track and new media, and creates an empty key frame sample. AddSpriteTrackToMovie then calls BeginMediaEdits (Listing 4-4) to prepare to add samples to the track’s media.

Listing 4-2  Creating a track and media

// create the sprite track and media
 
    myTrack = NewMovieTrack(myMovie, ((long)kSpriteTrackWidth << 16),
                            ((long)kSpriteTrackHeight << 16), kNoVolume);
    myMedia = NewTrackMedia(myTrack, SpriteMediaType,
                            kSpriteMediaTimeScale, NULL, 0);
 
// create a new, empty key frame sample
    myErr = QTNewAtomContainer(&mySample);
    if (myErr != noErr)
        goto bail;
 
    myKeyColor.red = 0xffff;    // white
    myKeyColor.green = 0xffff;
    myKeyColor.blue = 0xffff;

Adding Images to the Key Frame Sample

The AddPICTImageToKeyFrameSample function (Listing 4-3) adds images to the key frame sample.

Listing 4-3  Adding images to the key frame sample

// add images to the key frame sample
    AddPICTImageToKeyFrameSample(mySample, kGoToBeginningButtonUp,
                &myKeyColor, kGoToBeginningButtonUpIndex, NULL, NULL);
    AddPICTImageToKeyFrameSample(mySample, kGoToBeginningButtonDown,
                &myKeyColor, kGoToBeginningButtonDownIndex, NULL, NULL);
    AddPICTImageToKeyFrameSample(mySample, kGoToEndButtonUp, &myKeyColor,
                                    kGoToEndButtonUpIndex, NULL, NULL);
...
    AddPICTImageToKeyFrameSample(mySample, kPenguinForward, &myKeyColor,
                                    kPenguinForwardIndex, NULL, NULL);
    AddPICTImageToKeyFrameSample(mySample, kPenguinLeft, &myKeyColor,
                                    kPenguinLeftIndex, NULL, NULL);
    AddPICTImageToKeyFrameSample(mySample, kPenguinRight, &myKeyColor,
                                    kPenguinRightIndex, NULL, NULL);
    AddPICTImageToKeyFrameSample(mySample, kPenguinClosed, &myKeyColor,
                                    kPenguinClosedIndex, NULL, NULL);
 
    for (myIndex = kPenguinDownRightCycleStartIndex, myResID =
            kWalkDownRightCycleStart;
         myIndex <= kPenguinDownRightCycleEndIndex;
         myIndex++, myResID++)
    AddPICTImageToKeyFrameSample(mySample, myResID, &myKeyColor, myIndex,
                                 NULL, NULL);

Adding More Images for Other Sprites

To add more images to other sprites, you assign group IDs to those images, using the AssignImageGroupIDsToKeyFrame function. You then create the sprite track, add it to theMovie, and then begin to add samples to the tracks’ media, as shown in Listing 4-4.

Listing 4-4  Adding more images to other sprites and specifying button actions

// assign group IDs to the images
    AssignImageGroupIDsToKeyFrame(mySample);
 
    // add samples to the sprite track's media
    //
 
 
    BeginMediaEdits(myMedia);
 
    // go to beginning button with no actions
    myErr = QTNewAtomContainer(&myBeginButton);
    if (myErr != noErr)
        goto bail;
    myLocation.h    = (1 * kSpriteTrackWidth / 8) - (kStartEndButtonWidth
                                                    / 2);
    myLocation.v    = (4 * kSpriteTrackHeight / 5) -
                                    (kStartEndButtonHeight / 2);
    isVisible       = false;
    myLayer         = 1;
    myIndex         = kGoToBeginningButtonUpIndex;
    myErr = SetSpriteData(myBeginButton, &myLocation, &isVisible,
                            &myLayer, &myIndex, NULL, NULL, myActions);
    if (myErr != noErr)
        goto bail;

Adding Sprites to the Key Frame Sample

The AddSpriteTrackToMovie function adds the sprites with their initial property values to the key frame sample, as shown in Listing 4-5. The key frame contains four buttons and two penguins.

If the withBackgroundPicture parameter is true, the function adds a background sprite. The function initializes the background sprite’s properties, including setting the layer property to kBackgroundSpriteLayerNum to indicate that the sprite is a background sprite. The function calls SetSpriteData (Listing 4-6), which adds the appropriate property atoms to the spriteData atom container. Then, AddSpriteTrackToMovie calls AddSpriteToSample (Listing 4-7) to add the atoms in the spriteData atom container to the key frame sample atom container.

AddSpriteTrackToMovie adds the other sprites to the key frame sample and then calls AddSpriteSampleToMedia (Listing 4-8) to add the key frame sample to the media.

Listing 4-5  Creating more key frame sprite media

// add actions to the six sprites
    //
    // add go to beginning button
    myErr = QTCopyAtom(myBeginButton, kParentAtomIsContainer,
                        &myBeginActionButton);
    if (myErr != noErr)
        goto bail;
 
    AddSpriteSetImageIndexAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseClick, 0, NULL, 0, 0, NULL,
                                kGoToBeginningButtonDownIndex, NULL);
    AddSpriteSetImageIndexAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseClickEnd, 0, NULL, 0, 0,
                                NULL, kGoToBeginningButtonUpIndex, NULL);
    AddMovieGoToBeginningAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseClickEndTriggerButton);
    AddSpriteSetVisibleAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseEnter, 0, NULL, 0, 0, NULL,
                                true, NULL);
    AddSpriteSetVisibleAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseExit, 0, NULL, 0, 0, NULL,
                                false, NULL);
    AddSpriteToSample(mySample, myBeginActionButton,
                        kGoToBeginningSpriteID);
    QTDisposeAtomContainer(myBeginActionButton);
 
    // add go to prev button
    myErr = QTCopyAtom(myPrevButton, kParentAtomIsContainer,
                        &myPrevActionButton);
    .
    .
    .
 
    AddSpriteSetImageIndexAction(myPrevActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseClick, 0, NULL, 0, 0, NULL,
                                kGoToPrevButtonDownIndex, NULL);
    AddSpriteSetImageIndexAction(myPrevActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseClickEnd, 0, NULL, 0, 0,
                                NULL, kGoToPrevButtonUpIndex, NULL);
    AddMovieStepBackwardAction(myPrevActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseClickEndTriggerButton);
    AddSpriteSetVisibleAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseEnter, 0, NULL, 0, 0, NULL,
                                true, NULL);
    AddSpriteSetVisibleAction(myBeginActionButton,
                                kParentAtomIsContainer,
                                kQTEventMouseExit, 0, NULL, 0, 0, NULL,
                                false, NULL);
    AddSpriteToSample(mySample, myPrevActionButton, kGoToPrevSpriteID);
 
    QTDisposeAtomContainer(myPrevActionButton);
 
    // add go to next button
    myErr = QTCopyAtom(myNextButton, kParentAtomIsContainer,
                        &myNextActionButton);
    .
    .
    .

For each new property value that is passed into it as a parameter, the SetSpriteData function (Listing 4-6) calls QTFindChildByIndex to find the appropriate property atom. If the property atom already exists in the QT atom container, SetSpriteData calls QTSetAtomData to update the property’s value. If the property atom does not exist in the container, SetSpriteData calls QTInsertChild to insert a new property atom.

Listing 4-6  The SetSpriteData function

OSErr SetSpriteData (QTAtomContainer sprite, Point *location,
    short *visible, short *layer, short *imageIndex)
{
    OSErr   err = noErr;
    QTAtom  propertyAtom;
 
    if (location) {
        MatrixRecordmatrix;
 
        // set up the value for the matrix property
        SetIdentityMatrix (&matrix);
        matrix.matrix[2][0] = ((long)location->h << 16);
        matrix.matrix[2][1] = ((long)location->v << 16);
 
        // if no matrix atom is in the container, insert a new one
        if ((propertyAtom = QTFindChildByIndex (sprite, 0,
            kSpritePropertyMatrix, 1, nil)) == 0)
            FailOSErr (QTInsertChild (sprite, 0, kSpritePropertyMatrix,
                1, 0, sizeof(MatrixRecord), &matrix, nil))
        // otherwise, replace the atom’s data else
            FailOSErr (QTSetAtomData (sprite, propertyAtom,
                sizeof(MatrixRecord), &matrix));
    }
 
    // ...
    // handle other properties in a similar fashion
    // ...
 
    return err;
}

The AddSpriteToSample function (Listing 4-7) checks to see whether a sprite has already been added to a sample. If not, the function calls QTInsertChild to create a new sprite atom in the atom container that represents the sample. Then, AddSpriteToSample calls QTInsertChildren to insert the atoms in the sprite atom container as children of the newly created atom in the sample container.

Listing 4-7  The AddSpriteToSample function

OSErr AddSpriteToSample (QTAtomContainer theSample,
    QTAtomContainer theSprite, short spriteID)
{
    OSErr err = noErr;
    QTAtom newSpriteAtom;
 
    FailIf (QTFindChildByID (theSample, 0, kSpriteAtomType, spriteID,
                                nil), paramErr);
 
    FailOSErr (QTInsertChild (theSample, 0, kSpriteAtomType, spriteID,
                                0, 0, nil, &newSpriteAtom)); // index of
                                                    // zero means append
    FailOSErr (QTInsertChildren (theSample, newSpriteAtom, theSprite));
 
    .
    .
    .
}

The AddSpriteSampleToMedia function, shown in Listing 4-8, calls AddMediaSample to add either a key frame sample or an override sample to the sprite media.

Listing 4-8  The AddSpriteSampleToMedia function

OSErr AddSpriteSampleToMedia (Media theMedia, QTAtomContainer sample,
    TimeValue duration, Boolean isKeyFrame)
{
    OSErr err = noErr;
    SampleDescriptionHandle sampleDesc = nil;
 
    FailMemErr (sampleDesc = (SampleDescriptionHandle) NewHandleClear(
                                sizeof(SampleDescription)));
 
    FailOSErr (AddMediaSample (theMedia, (Handle) sample, 0,
                                GetHandleSize(sample), duration,
                                sampleDesc, 1,
                                isKeyFrame ? 0 : mediaSampleNotSync,
                                nil));
 
bail:
    if (sampleDesc)
        DisposeHandle ((Handle)sampleDesc);
 
    return err;
}

Adding More Actions to Other Sprites

To set the movie’s looping mode to palindrome, you add an action that is triggered when the key frame is loaded, as shown in Listing 4-9. This action is triggered every time the key frame is reloaded.

Listing 4-9  Adding more actions to other sprites

        loopingFlags = loopTimeBase | palindromeLoopTimeBase;
        FailOSErr( AddMovieSetLoopingFlagsAction( sample,
                    kParentAtomIsContainer,
                    kQTEventFrameLoaded, loopingFlags ) )

Adding Sample Data in Compressed Form

To add the sample data in a compressed form, you use a QuickTime DataCodec to perform the compression, as shown in Listing 4-10. You replace the sample utility AddSpriteSampleToMedia call with a call to the sample utility AddCompressedSpriteSampleToMedia.

Listing 4-10  Adding the key frame sample in compressed form

/* AddSpriteSampleToMedia(myMedia, mySample, kSpriteMediaFrameDuration,
            true, NULL);    */
AddCompressedSpriteSampleToMedia(myMedia, mySample,
            kSpriteMediaFrameDuration, true, zlibDataCompressorSubType,
            NULL);

Defining Override Samples

Once you have defined a key frame sample for the sprite track, you can add any number of override samples to modify sprite properties.

Listing 4-11 shows the portion of the AddSpriteTrackToMovie function that adds override samples to the sprite track to make the first penguin sprite appear to waddle and move across the screen. For each override sample, the function modifies the first penguin sprite’s image index and location. The function calls SetSpriteData to update the appropriate property atoms in the sprite atom container. Then, the function calls AddSpriteToSample to add the sprite atom container to the sample atom container. After all of the modifications have been made to the override sample, the function calls AddSpriteSampleToMedia to add the override sample to the media.

After adding all of the override samples to the media, AddSpriteTrackToMovie calls EndMediaEdits to indicate that it is done adding samples to the media. Then, AddSpriteTrackToMovie calls InsertMediaIntoTrack to insert the new media segment into the track.

Listing 4-11  Adding override samples to move penguin one and change its image index

// original penguin one location
    myLocation.h    = (3 * kSpriteTrackWidth / 8) - (kPenguinWidth / 2);
    myLocation.v    = (kSpriteTrackHeight / 4) - (kPenguinHeight / 2);
 
    myDelta = (kSpriteTrackHeight / 2) / kNumOverrideSamples;
    myIndex = kPenguinDownRightCycleStartIndex;
 
    for (i = 1; i <= kNumOverrideSamples; i++) {
        QTRemoveChildren(mySample, kParentAtomIsContainer);
        QTNewAtomContainer(&myPenguinOneOverride);
 
        myLocation.h += myDelta;
        myLocation.v += myDelta;
        myIndex++;
        if (myIndex > kPenguinDownRightCycleEndIndex)
            myIndex = kPenguinDownRightCycleStartIndex;
 
        SetSpriteData(myPenguinOneOverride, &myLocation, NULL, NULL,
                        &myIndex, NULL, NULL, NULL);
        AddSpriteToSample(mySample, myPenguinOneOverride,
                            kPenguinOneSpriteID);
        AddSpriteSampleToMedia(myMedia, mySample,
                                kSpriteMediaFrameDuration, false, NULL);
        QTDisposeAtomContainer(myPenguinOneOverride);
    }
 
    EndMediaEdits(myMedia);
 
    // add the media to the track
    InsertMediaIntoTrack(myTrack, 0, 0, GetMediaDuration(myMedia),
                            fixed1);

Setting Properties of the Sprite Track

Besides adding key frame samples and override samples to the sprite track, you may want to set one or more global properties of the sprite track. For example, if you want to define a background color for your sprite track, you must set the sprite track’s background color property. You do this by creating a leaf atom of type kSpriteTrackPropertyBackgroundColor whose data is the desired background color.

After adding the override samples, AddSpriteTrackToMovie adds a background color to the sprite track, as shown in Listing 4-12. The function calls QTNewAtomContainer to create a new atom container for sprite track properties. AddSpriteTrackToMovie adds a new atom of type kSpriteTrackPropertyBackgroundColor to the container and calls SpriteMediaSetSpriteProperty to set the sprite track’s property.

After adding a background color, AddSpriteTrackToMovie notifies the movie controller that the sprite track has actions. If the hasActions parameter is true, this function calls QTNewAtomContainer to create a new atom container for sprite track properties. AddSpriteTrackToMovie adds a new atom of type kSpriteTrackPropertyHasActions to the container and calls SpriteMediaSetSpriteProperty to set the sprite track’s property.

Finally, after specifying that the sprite track has actions, AddSpriteTrackToMovie notifies the sprite track to generate QTIdleEvents by adding a new atom of type kSpriteTrackPropertyQTIdleEventsFrequency to the container. This new atom specifies the frequency of QTEvent occurrences.

Listing 4-12  Adding sprite track properties, including a background color, actions, and frequency

{
        QTAtomContainer     myTrackProperties;
        RGBColor            myBackgroundColor;
 
        // add a background color to the sprite track
        myBackgroundColor.red = EndianU16_NtoB(0x8000);
        myBackgroundColor.green = EndianU16_NtoB(0);
        myBackgroundColor.blue = EndianU16_NtoB(0xffff);
 
        QTNewAtomContainer(&myTrackProperties);
        QTInsertChild(myTrackProperties, 0,
                        kSpriteTrackPropertyBackgroundColor, 1, 1,
                        sizeof(RGBColor), &myBackgroundColor, NULL);
 
        // tell the movie controller that this sprite track has actions
        hasActions = true;
        QTInsertChild(myTrackProperties, 0,
                        kSpriteTrackPropertyHasActions, 1, 1,
                        sizeof(hasActions), &hasActions, NULL);
 
        // tell the sprite track to generate QTIdleEvents
        myFrequency = EndianU32_NtoB(60);
        QTInsertChild(myTrackProperties, 0,
                    kSpriteTrackPropertyQTIdleEventsFrequency, 1, 1,
                        sizeof(myFrequency), &myFrequency, NULL);
        myErr = SetMediaPropertyAtom(myMedia, myTrackProperties);
        if (myErr != noErr)
            goto bail;
 
        QTDisposeAtomContainer(myTrackProperties);
    }

Retrieving Sprite Data From a Modifier Track

The sample program AddReferenceTrack.c illustrates how you can modify a movie to use a modifier track for a sprite’s image data. The sample program prompts the user for a movie that contains a single sprite track. Then, it adds a track from a second movie to the original movie as a modifier track. The modifier track overrides the image data for a selected image index.

Listing 4-13 shows the first part of the main function of the sample program. It performs the following tasks:

  • It loads the movie containing the sprite track.

  • It calls GetMovieTrackCount to determine the total number of tracks in the sprite track movie.

  • It loads the movie containing the modifier track (movieB).



    Listing 4-13  Loading the movies

    OSErr               err;
    short               movieResID = 0, resFref, resID = 0, resRefNum;
    StandardFileReply   reply;
    SFTypeList          types;
    Movie               m;
    FSSpec              fss;
    Movie               movieB;
    long                origTrackCount;
     
    // prompt for a movie containing a sprite track and load it
    types[0] = MovieFileType;
    StandardGetFilePreview (nil, 1, types, &reply);
    if (!reply.sfGood) return;
     
    err = OpenMovieFile (&reply.sfFile, &resFref, fsRdPerm);
    if (err) return;
     
    err = NewMovieFromFile (&m, resFref, &movieResID, (StringPtr)nil,
        newMovieActive, ni);
    if (err) return;
     
    CloseMovieFile (resFref);
     
    // get the number of tracks
    origTrackCount = GetMovieTrackCount (m);
     
    // load the movie to be used as a modifier track
    FSMakeFSSpec (reply.sfFile.vRefNum, reply.sfFile.parID, "\pAdd Me",
        &fss);
     
    err = OpenMovieFile (&fss, &resFref, fsRdPerm);
    if (err) return;
     
    err = NewMovieFromFile (&movieB, resFref, &resID, (StringPtr)nil, 0,
        nil);
    if (err) return;
     
    CloseMovieFile (resFref);

Once the two movies have been loaded, the sample program retrieves the first track, which is the sprite track, from the original movie, and sets the selection to the start of the movie (Listing 4-14). The sample program iterates through all the tracks in the modifier movie, disposing of all non-video tracks.

Next, the sample program calls AddMovieSelection to add the modifier track to the original movie. Finally, the sample program calls AddTrackReference to associate the modifier track with the sprite track it will modify. AddTrackReference returns an index of the added reference in the referenceIndex variable.

Listing 4-14  Adding the modifier track to the movie

Movie           m;
TimeValue       oldDuration;
Movie           movieB;
long            i, origTrackCount, referenceIndex;
Track           newTrack, spriteTrack;
 
// get the first track in original movie and position at the start
spriteTrack = GetMovieIndTrack (m, 1);
SetMovieSelection (m, 0 ,0);
 
// remove all tracks except video in modifier movie
for (i = 1; i <= GetMovieTrackCount (movieB); i++)
{
    Track t = GetMovieIndTrack (movieB, i);
    OSType aType;
 
    GetMediaHandlerDescription (GetTrackMedia(t), &aType, nil, nil);
    if (aType != VideoMediaType)
    {
        DisposeMovieTrack (t);
        i--;
    }
}
 
// add the modifier track to original movie
oldDuration = GetMovieDuration (m);
AddMovieSelection (m, movieB);
DisposeMovie (movieB);
 
// truncate the movie to the length of the original track
DeleteMovieSegment (m, oldDuration,
    GetMovieDuration (m) - oldDuration);
 
// associate the modifier track with the original sprite track
newTrack = GetMovieIndTrack (m, origTrackCount + 1);
AddTrackReference (spriteTrack, newTrack, kTrackModifierReference,
    &referenceIndex);

Besides adding a reference to the modifier track, the sample program must update the sprite media’s input map to describe how the modifier track should be interpreted by the sprite track. The sample program performs the following tasks (Listing 4-15):

  • It retrieves the sprite track’s media by calling GetTrackMedia.

  • It calls GetMediaInputMap to retrieve the media’s input map.

  • It adds a parent atom to the input map of type kTrackModifierInput. The ID of the atom is the reference index retrieved by the AddTrackReference function.

  • It adds two child atoms, one that specifies that the input type of the modifier track is of type kTrackModifierTypeImage, and one that specifies the index of the sprite image to override.

  • It calls SetMediaInputMap to update the media’s input map.



    Listing 4-15  Updating the media’s input map

    #define kImageIndexToOverride 1
     
    Movie           m, movieB;
    long            referenceIndex, imageIndexToOverride;
    Track           spriteTrack;
    QTAtomContainer inputMap;
    QTAtom          inputAtom;
    OSType          inputType;
    Media           spriteMedia;
     
    // get the sprite media’s input map
    spriteMedia = GetTrackMedia (spriteTrack);
    GetMediaInputMap (spriteMedia, &inputMap);
     
    // add an atom for a modifier track
    QTInsertChild (inputMap, kParentAtomIsContainer,
        kTrackModifierInput, referenceIndex, 0, 0, nil, &inputAtom);
     
    // add a child atom to specify the input type
    inputType = kTrackModifierTypeImage;
    QTInsertChild (inputMap, inputAtom, kTrackModifierType, 1, 0,
        sizeof(inputType), &inputType, nil);
     
    // add a second child atom to specify index of image to override
    imageIndexToOverride = kImageIndexToOverride;
    QTInsertChild (inputMap, inputAtom, kSpritePropertyImageIndex, 1, 0,
        sizeof(imageIndexToOverride), &imageIndexToOverride, nil);
     
    // update the sprite media’s input map
    SetMediaInputMap (spriteMedia, inputMap);
    QTDisposeAtomContainer (inputMap);

Once the media’s input map has been updated, the application can save the movie.

Authoring Wired Movies

In addition to providing functions that allow you to create and manipulate a sprite animation as a track in a QuickTime movie, the sprite media handler also provides functions that allow your application to create and manipulate a wired sprite movie, with various types of user interactivity.

The following sections are illustrated, as with the previous section, with code from the sample program QTWiredSprites.c, which shows how to create a sample wired sprite movie containing one sprite track. (A partial code listing is available in Appendix A. You can download the full sample code at QuickTime Web site at http://www.apple.com/quicktime/developers/samplecode.html#sprites.)

The sample code creates a 320 by 240 pixel wired movie with one sprite track that contains six sprites, two of which are penguins and four of which are buttons. Figure 4-1 shows two penguins at the outset of the movie, with the buttons invisible.

Figure 4-1  Two penguins from a sample program
Two penguins from a sample program

Initially, the four buttons in the wired movie are invisible. When the mouse enters or “rolls over” a button, it appears, as shown in Figure 4-2.

Figure 4-2  Two penguins and four buttons, indicating various directions in the movie
Two penguins and four buttons, indicating various directions in the movie

When the mouse is clicked inside a button, its images change to its “pressed” image. When the mouse is released, its image is changed back to its “unpressed” image. If the mouse is released inside the button, it triggers an action. The buttons perform the following set of actions:

Actions of the First Penguin

The first penguin shows all of the buttons when the mouse enters it, and hides them when the mouse exits. The first penguin is the only sprite which has properties that are overriden by the override sprite samples. These samples override its matrix in order to move it, and its image index in order to make it waddle.

When you mouse-click on the second penguin, the penguin changes its image index to its “eyes closed” image. When the mouse is released, it changes back to its normal image. This makes the penguin’s eyes appear to blink when clicked on. When the mouse is released over the penguin, several other actions are triggered. Both penguins’ graphics states are toggled between copyMode and blendMode, and the movie’s rate is toggled between 0 and 1.

Actions of the Second Penguin

The second penguin moves once per second. This occurs whether the movie’s rate is currently 0 or 1 because it is being triggered by a QuickTime idle event. When the penguin receives the idle event, it changes its matrix using an action which uses min, max, delta, and wraparound options.

The movie’s looping mode is set to palindrome by a kQTEventFrameLoaded event.

Creating a Wired Sprite Movie

The following tasks are performed in order to create the wired sprite movie:

  • You create a new movie file with a single sprite track, as explained in the section Creating the Movie, Sprite Track, and Media.

  • You assign the “no controller” movie controller to the movie.

  • You set the sprite track’s background color, idle event frequency, and hasActions properties.

  • You convert PICT resources to animation codec images with transparency.

  • A key frame sample containing six sprites and all of their shared images is created. The sprites are assigned initial property values. A frameLoaded event is created for the key frame.

  • You create some override samples which override the matrix and image index properties of the first penguin sprite.

Assigning the No Controller to the Movie

The following code fragment (in Listing 4-16) assigns the “no controller” movie controller to the movie. You make this assignment if you don’t want the standard QuickTime movie controller to appear. In this code sample, you want to create a set of sprite buttons in order to control user interaction with the penguins in the movie.

There may also be other occasions when it is useful to make a “no controller” assignment. For example, if you are creating non-linear movies—such as Hypercard stacks where you access the cards in the stack by clicking on buttons by sprites—you may wish to create your own sprite buttons.

Listing 4-16  Assigning the no controller movie controller

    // select the "no controller" movie controller
    myType = EndianU32_NtoB(myType);
    SetUserDataItem(GetMovieUserData(myMovie), &myType, sizeof(myType),
                        kUserDataMovieControllerType, 1);

Setting Up the Sprite Track’s Properities

Listing 4-17 shows a code fragment that sets the sprite track’s background color, idle event frequency and its hasActions properties.

Listing 4-17  Setting the background color, idle event frequency and hasActions properties of the sprite track

// set the sprite track properties
    {
        QTAtomContainer     myTrackProperties;
        RGBColor            myBackgroundColor;
 
        // add a background color to the sprite track
        myBackgroundColor.red = EndianU16_NtoB(0x8000);
        myBackgroundColor.green = EndianU16_NtoB(0);
        myBackgroundColor.blue = EndianU16_NtoB(0xffff);
 
        QTNewAtomContainer(&myTrackProperties);
        QTInsertChild(myTrackProperties, 0,
                        kSpriteTrackPropertyBackgroundColor, 1, 1,
                        sizeof(RGBColor), &myBackgroundColor, NULL);
 
// tell the movie controller that this sprite track has actions
        hasActions = true;
        QTInsertChild(myTrackProperties, 0,
                        kSpriteTrackPropertyHasActions, 1, 1,
                    sizeof(hasActions), &hasActions, NULL);
 
        // tell the sprite track to generate QTIdleEvents
        myFrequency = EndianU32_NtoB(60);
        QTInsertChild(myTrackProperties, 0,
                        kSpriteTrackPropertyQTIdleEventsFrequency, 1, 1,
                            sizeof(myFrequency), &myFrequency, NULL);
        myErr = SetMediaPropertyAtom(myMedia, myTrackProperties);
        if (myErr != noErr)
            goto bail;
 
        QTDisposeAtomContainer(myTrackProperties);
    }

Adding an Event Handler to the Penguin

The AddPenguinTwoConditionalActions routine adds logic to our penguin. Using this routine, you can transform the penguin into a two-state button that plays/pauses the movie.

We are relying on the fact that a GetVariable for a variableID which has never been set will return 0. If we need another default value, we could initialize it using the frameLoaded event.

A higher level description of the logic is:

    On MouseUpInside
       If (GetVariable(DefaultTrack, 1) = 0)
          SetMovieRate(1)
          SetSpriteGraphicsMode(DefaultSprite, { blend, grey } )
          SetSpriteGraphicsMode(GetSpriteByID(DefaultTrack, 5), {
                                ditherCopy, white } )
          SetVariable(DefaultTrack, 1, 1)
       ElseIf (GetVariable(DefaultTrack, 1) = 1)
          SetMovieRate(0)
          SetSpriteGraphicsMode(DefaultSprite, { ditherCopy, white })
          SetSpriteGraphicsMode(GetSpriteByID(DefaultTrack, 5), { blend,
                                                    grey })
          SetVariable(DefaultTrack, 1, 0)
       Endif
    End

Adding a Series of Actions to the Penguins

The following code fragment in Listing 4-18 shows how you can add a key frame with four buttons, enabling our penguins to move through a series of actions.

Listing 4-18  Adding a key frame with four buttons, enabling a series of actions for our two penguins

// add actions to the six sprites
    // add go to beginning button
    myErr = QTCopyAtom(myBeginButton, kParentAtomIsContainer,
                        &myBeginActionButton);
    if (myErr != noErr)
        goto bail;
 
    AddSpriteSetImageIndexAction(myBeginActionButton,
                    kParentAtomIsContainer, kQTEventMouseClick, 0, NULL,
                    0, 0, NULL, kGoToBeginningButtonDownIndex, NULL);
    AddSpriteSetImageIndexAction(myBeginActionButton,
                    kParentAtomIsContainer, kQTEventMouseClickEnd, 0,
                    NULL, 0, 0, NULL, kGoToBeginningButtonUpIndex, NULL);
    AddSpriteToSample(mySample, myPrevActionButton, kGoToPrevSpriteID);
                        NULL);
    AddSpriteToSample(mySample, myNextActionButton, kGoToNextSpriteID);
    QTDisposeAtomContainer(myNextActionButton);
. . .
    // add go to end button
    myErr = QTCopyAtom(myEndButton, kParentAtomIsContainer,
                        &myEndActionButton);
    if (myErr != noErr)
        goto bail;
        kParentAtomIsContainer, kQTEventMouseExit, 0, NULL, 0, 0, NULL,
            false,
. . .
 
    // add penguin one
    myErr = QTCopyAtom(myPenguinOne, kParentAtomIsContainer,
                        &myPenguinOneAction);
    if (myErr != noErr)
    AddSpriteSetVisibleAction(myBeginActionButton, goto bail;
 
    // show the buttons on mouse enter and hide them on mouse exit
    AddSpriteSetVisibleAction(myPenguinOneAction, kParentAtomIsContainer,
                                kQTEventMouseEnter, 0, NULL, 0,
                                kTargetSpriteID, (void
                                *)kGoToBeginningSpriteID, true, NULL);
    AddSpriteSetVisibleAction(myPenguinOneAction, kParentAtomIsContainer,
                                kQTEventMouseExit, 0, NULL, 0,
                                kTargetSpriteID, (void
                                *)kGoToBeginningSpriteID, false, NULL);
. . .
    // add penguin two
    myErr = QTCopyAtom(myPenguinTwo, kParentAtomIsContainer,
                        &myPenguinTwoAction);
    if (myErr != noErr)
        goto bail;
 
    // blink when clicked on
    AddSpriteSetImageIndexAction(myPenguinTwoAction,
                                kParentAtomIsContainer,
                                kQTEventMouseClick, 0, NULL, 0, 0, NULL,
                                kPenguinClosedIndex, NULL);
. . .
    // add go to next button
    myErr = QTCopyAtom(myNextButton, kParentAtomIsContainer,
                        &myNextActionButton);
    if (myErr != noErr)
        goto bail;

Important Things to Note in the Sample Code

You should note the following in MakeActionSpriteMovie.c sample code:

  • There are event types other than mouse-related events (for example, Idle and frameLoaded).

  • Idle events are independent of the movie’s rate, and can be gated, so they are sent at most every n ticks. (The second penguin moves when the movie’s rate is 0, and moves only once per second because of the value of the sprite tracks idleEventFrequency property.)

  • Multiple actions may be executed in response to a single event (for example, rolling over the first penguin shows and hides four different buttons).

  • Actions may target any sprite or track in the movie (for example, clicking on one penguin changes the graphics mode of the other).

  • Conditional and looping control structures are supported. (The second penguin uses the “case statement” action.)

  • Sprite track variables that have not been set have a default value of 0. (The second penguin’s conditional code relies on this.)

Authoring Movies With External Movie Targets

QuickTime enables you to author movies with external movie targets. To accomplish this, two target atoms were introduced in QuickTime 4, as explained in the following section.

Specifying an External Movie Target for an Action

In order to specify that an action is to target an element of an external movie, the external movie must be identified by either its name or its ID. To do this, two new target atom types have been introduced; these atoms are used in addition to the existing target atoms, which may specify that the element is a particular Track or object within a Track such as a Sprite.

The additional target atoms provided in QuickTime 4:

[(ActionTargetAtoms)] =
    <kActionTarget>
 
        <kTargetMovieName>
            [Pstring MovieName]
        OR
        <kTargetMovieID>
            [long MovieID]
            OR
            [(kExpressionAtoms)]

To tag a movie with a name or ID, you add a user data item of type 'plug' to the movie’s user data. The index of the user data does not matter. The data specifies the name or ID.

You add a user data item of type 'plug' to the movie’s user data with its data set to

"Movieid=MovieName"

where MovieName is the name of the movie.

You add a user data item of type'plug' to the movie’s user data with its data set to

"Movieid=MovieID"

where the ID is a signed long integer.

The QuickTime plug-in additionally supports EMBED tag parameters, which allow you to override a movie’s name or ID within an HTML page.

Target Atoms for Embedded Movies

Target atoms accommodate embedded movies. These target atoms allow for paths to be specified in a hierarchical movie tree.

Target movies may be an external movie, the default movie, or any movie embedded within another movie. Targets are specified by using a movie path that may include parent and child movie relationships, and may additionally include track and track object target atoms as needed.

By using embedded kActionTarget atoms along with parent and child movie target atoms, you can build up paths for movie targets. QuickTime looks for these embedded kActionTarget atoms only when evaluating a movie target, and any movie target type may contain a sibling kActionTarget atom.

Paths begin from the current movie, which is the movie containing the object that is handling an event. You may go up the tree using a kTargetParentMovie atom, or down the tree using one of five new child movie atoms. You may use a kTargetRootMovie atom as a shortcut to get to the top of the tree containing an embedded movie and may use the movieByName and movieByID atoms to specify a root external movie.

The target atoms are

  • kTargetRootMovie (leaf atom, no data). This is the root movie containing the action handler.

  • kTargetParentMovie (leaf atom, no data). This is the parent movie.

There are five ways to specify an embedded child movie. Three of them specify movie track properties. Two specify properties of the currently loaded movie in a movie track.

  • kTargetChildMovieTrackName. A child movie track specified by track name.

  • kTargetChildMovieTrackID. A child movie track specified by track ID.

  • kTargetChildMovieTrackIndex. A child movie track specified by track index.

  • kTargetChildMovieMovieName. A child movie specified by the currently loaded movie’s movie name. The child movie must contain movieName user data with the specified name.

  • kTargetChildMovieMovieID. A child movie specified by the currently loaded movie’s movie ID. The child movie must contain movieID user data with the specified ID.

Supporting External Movie Targets

If you want your application to support external movie targets, you need to resolve the movie names or IDs to actual movie references. This is accomplished by installing a Movie Controller filter proc for each Movie Controller that filters for the mcActionGetExternalMovie action.

The parameter to the mcActionGetExternalMovie Movie Controller action is a pointer to a QTGetExternalMovieRecord.

First, you look at the targetType field. If it is set to kTargetMovieName, then return the movie and Movie Controller for the movie named by the MovieName field. If it is set to kTargetMovieID, then return the movie and Movie Controller for the movie with ID equal to the MovieID field.

If you cannot find a matching movie, then set theMovie and theController to nil.

Specifying String Parameters to Actions and Operands

While QuickTime 3 supported only leaf data constants for string parameters, QuickTime 4 lets you can specify string parameters using leaf data constants, a numeric expression, or a sprite track variable.

Expressions are converted to strings, as are sprite track variable s if they contain a floating-point number instead of a string. Note that although string variables are stored as C strings, they will be automatically converted to Pascal strings as needed.

This applies to the following action and operand parameters:

    C String Parameters
        kActionGoToURL, 1
        kActionStatusString, 1
        kActionAddChannelSubscription, 2
        kActionAddChannelSubscription, 3
        kActionRemoveChannelSubscription, 1
        kOperandSubscribedToChannel, 1
 
    Pascal String Parameters
        kActionMovieGoToTimeByName, 1
        kActionMovieSetSelectionByName, 1
        kActionMovieSetSelectionByName, 2
        kActionPushCurrentTimeWithLabel, 1
        kActionPopAndGotoLabeledTime, 1
        kActionDebugStr, 1
        kActionAddChannelSubscription, 1
 
 
    Extended Grammar:
        [(C String Parameter)] =
                [CString]
            OR
                [(ExpressionAtoms)]
            OR
                [(SpriteTrackVariableOperandAtoms)]
 
        [(Pascal String Parameter)] =
                [Pascal String]
            OR
                [(ExpressionAtoms)]
            OR
                [(SpriteTrackVariableOperandAtoms)]
 
 
        [(SpriteTrackVariableOperandAtoms)] =
            <kOperandSpriteTrackVariable>, 1, 1
                [(ActionTargetAtoms)]
                kActionParameter, 1, 1
                    [(spriteVariableID)]
                    OR
                    [(ExpressionAtoms)]

Wiring a Movie by Adding an HREF Track

QuickTime enables you to wire a movie by adding an HREF track to it. An HREF track is a specially named text track that contains hypertext references (HREFs). These references are in the form of URLs and can point to any kind of data that a URL can specify, such as a Web page, a QuickTime movie, or a JavaScript script.

A URL can be invoked automatically when the movie plays, or it can be invoked interactively when the user clicks the mouse inside a movie. The URL can be targeted to a particular frame, or to the QuickTime movie itself, or it can be untargeted, in which case it will load in the default browser window.

You can only add one HREF track per movie. If you add more than one HREF track to a movie, QuickTime uses the first one it finds and ignore any others.

The syntax for an HREF track sample is:

[nn:nn:nn.n]
A<URL> T<frame>

where [nn:nn:nn.n] is a timestamp in hours:minutes:seconds.timeunits and where timeunits are in the time scale of the track. The URL becomes active at the time specified and remains active until the next timestamp. If there is no following timestamp, the URL remains active. The URL is active even if the movie is not playing.

A<URL> specifies the URL. If the A is omitted, the user must click inside the movie during the period when the URL is active to cause the URL to load. If the A is present, the URL loads automatically when the movie is played.

The URL can be absolute or relative to the movie.

The URL can include an internal anchor name as well as a path and filename (for example, “../HTML/Page1.htm#StepOne”).

T<frame> is an optional parameter. If a target frame is specified, the URL is targeted to that frame. If the frame does not exist, it is created.

If no target frame is specified, the URL will load in the default browser frame. If the movie is playing in a Web page, the Web page is replaced by the specified URL. If the movie is playing in an application such as QuickTime Player, the URL loads in the default browser (which will be invoked if it is not already running).

If the target frame “myself” is specified, the URL is opened by QuickTime within the application playing the movie, such as the QuickTime plug-in or QuickTime Player. This works properly only if the URL points to a file that QuickTime can handle directly, such as a QuickTime movie or an AIFF audio file.

Creating an HREF Track

An HREF track can be created using a text editor and imported using a movie import component, or it can be created programmatically.

You create an HREF track programmatically by creating a text track, adding the URLs as text samples, then setting the name of the track to “HREFTrack”. The track name is stored in the user data atom. The following code snippet shows how to change the name of a text track to “HREFTrack”.

Str255  trackNameStr = “HREFTrack”
UserDat ud = GetTrackUserData(theTrack);
OSType  dataType = FOUR_CHAR_CODE('name');
 
RemoveUserData(ud, dataType, 1);    // remove existing name, if any
SetUserDataItem(ud, &trackNameStr[1], trackNameStr[0], dataType, 0);

Because the HREF track is a text track, it will display its samples as on-screen text while the movie plays. This is normally desirable during debugging. To turn the text off, you disable the track by calling the SetTrackEnabled function.

When flattening a movie that contains an HREF track, be careful not to accidentally delete the disabled text track by calling FlattenMovie with the flattenActiveTracksOnly flag.

Adding Hypertext Links to a QuickTime Movie With a Text Track

This section illustrates how you can add a few wired actions to a movie that has a text track. In particular, it adds two go-to-URL actions to parts of the text track.

A text media sample is a 16-bit length word followed by the text of that sample. Optionally, one or more atoms of additional data (called text atom extensions) may follow the text in the sample. The length word specifies the total number of bytes in the text and in any text atom extensions that follow the text. These text atom extensions are organized as “classic” atom structures: a 32-bit length field, followed by a 32-bit type field, followed by the data in the atom. Here, the length field specifies the total length of the atom (that is, 8 plus the number of bytes in the data.) All the data in the text extension atom must be in big-endian format.

To add hypertext actions to a text sample, you simply add a text atom extension of the type kHyperTextTextAtomType to the end of the sample; the data of the text atom extension is the container that holds the information about the actions. The programming tasks are outlined in these steps.

Step #1––Creating a Movie With a Text Track and Hypertext Links

You create a text movie with hypertext links.

    if (myReply.sfGood) {
        myErr = AddHTAct_CreateTextMovie(&myReply.sfFile);
        if (myErr == noErr)
            myErr = AddHTAct_AddHyperTextToTextMovie(&myReply.sfFile);
    }

To create a movie with a text track, you call AddHTAct_CreateTextMovie.

static OSErr AddHTAct_CreateTextMovie (FSSpec *theFSSpec)
{
    short           myResRefNum = -1;
    short           myResID = movieInDataForkResID;
    Movie           myMovie = NULL;
    Track           myTrack = NULL;
    Media           myMedia = NULL;
    MediaHandler    myHandler = NULL;
    Rect            myTextBox;
    RGBColor        myTextColor =   {0x6666, 0xCCCC, 0xCCCC};
    RGBColor        myBackColor =   {0x3333, 0x6666, 0x6666};
    RGBColor        myHiliteColor = {0x0000, 0x0000, 0x0000};
    long            myDisplayFlags = 0;
    short           myHiliteStart = 0;
    short           myHiliteEnd = 0;
    TimeValue       myNewMediaTime;
    TimeValue       myScrollDelay = 0;
#if TARGET_OS_MAC
    char            myText[] = “\rPlease take me to Apple or CNN”;
#else
    char            myText[] = “\nPlease take me to Apple or CNN”;
#endif
    long            myFlags = createMovieFileDeleteCurFile | createMovieFileDontCreateResFile | newMovieActive;
    OSErr           myErr = noErr;

Step #2––Creating a New Text Movie

Now you create a new text movie.

    myErr = CreateMovieFile(theFSSpec, FOUR_CHAR_CODE(‘TVOD’), 0,
                            myFlags, &myResRefNum, &myMovie);
    .
    .
    .
    myTrack = NewMovieTrack(myMovie, FixRatio(kWidth320, 1),
                            FixRatio(kHeight240, 1), kTrackVolumeZero);
 
    myMedia = NewTrackMedia(myTrack, TextMediaType, kTimeScale600, NULL,
                            0);
    if ((myTrack == NULL) || (myMedia == NULL))
        goto bail;
 
    myErr = BeginMediaEdits(myMedia);
    .
    .
    .
 
    myHandler = GetMediaHandler(myMedia);
    .
    .
    .

Step #3––Adding a Text Sample to the Movie

You add a text sample to the movie, as follows:

    MacSetRect(&myTextBox, 0, 0, kWidth320, kHeight240);
    MacInsetRect(&myTextBox, kTextBoxInset, kTextBoxInset);
 
    myErr = (OSErr)TextMediaAddTextSample(  myHandler,
                                            myText,
                                            strlen(myText),
                                            kFontIDSymbol,
                                            kSize48,
                                            kFacePlain,
                                            &myTextColor,
                                            &myBackColor,
                                            teCenter,
                                            &myTextBox,
                                            myDisplayFlags,
                                            myScrollDelay,
                                            myHiliteStart,
                                            myHiliteEnd,
                                            &myHiliteColor,
                                            kTimeScale600,
                                            &myNewMediaTime);
    if (myErr != noErr)
        goto bail;
 
    myErr = EndMediaEdits(myMedia);
    .
    .
    .
 
    // add the media to the track, at time 0
    myErr = InsertMediaIntoTrack(myTrack, kTrackStartTimeZero,
                                myNewMediaTime, kTimeScale600, fixed1);
    .
    .
    .
 
    // add the movie resource
    myErr = AddMovieResource(myMovie, myResRefNum, &myResID, NULL);
    .
    .
    .
 
bail:
    if (myResRefNum != -1)
        CloseMovieFile(myResRefNum);
 
    if (myMovie != NULL)
        DisposeMovie(myMovie);
 
    return(myErr);
}

You call AddHTAct_CreateHyperTextActionContainer, which returns, through the theActions parameter, an atom container that contains some hypertext actions.

static OSErr AddHTAct_CreateHyperTextActionContainer (QTAtomContainer *theActions)
{
    QTAtom          myEventAtom = 0;
    QTAtom          myActionAtom = 0;
    QTAtom          myHyperTextAtom = 0;
    QTAtom          myWiredObjectsAtom = 0;
    long            myAction;
    long            mySelStart1 = 19;
    long            mySelEnd1 = 24;
    long            mySelStart2 = 28;
    long            mySelEnd2 = 31;
    long            myValue;
    char            myAppleURL[64] = “\pwww.apple.com\0”;
    char            myCnnURL[64] = “\pwww.cnn.com\0”;
    OSErr           myErr = noErr;
 
    myErr = QTNewAtomContainer(theActions);
    .
    .
    .
    // create a wired objects atom
    myErr = QTInsertChild(*theActions, kParentAtomIsContainer,
                            kTextWiredObjectsAtomType, kIndexOne,
                            kIndexZero, kZeroDataLength, NULL,
                            &myWiredObjectsAtom);
 

Step #4––Adding a Hypertext Link

Now you add a hypertext link to the wired objects atom: ID 1

    myErr = QTInsertChild(*theActions, myWiredObjectsAtom, kHyperTextItemAtomType, kIDOne, kIndexZero, kZeroDataLength, NULL, &myHyperTextAtom);
    .
    .
    .
 
    myValue = EndianS32_NtoB(mySelStart1);
    myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeStart,
                            kIDOne, kIndexZero, sizeof(long), &myValue,
                            NULL);
    .
    .
    .
    myValue = EndianS32_NtoB(mySelEnd1);
    myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeEnd,
                            kIDOne, kIndexZero, sizeof(long), &myValue,
                            NULL);
    .
    .
    .
 
    // add an event atom to the hypertext atom;
    // the event type can be any of the five mouse events: down, up,
    // trigger, enter, exit
    myErr = QTInsertChild(*theActions, myHyperTextAtom, kQTEventType,
                            kQTEventMouseClick, kIndexZero,
                            kZeroDataLength, NULL, &myEventAtom);
    .
    .
    .
    myErr = QTInsertChild(*theActions, myEventAtom, kAction, kIndexOne,
                        kIndexOne, kZeroDataLength, NULL, &myActionAtom);
    .
    .
    .
    myAction = EndianS32_NtoB(kActionGoToURL);
    myErr = QTInsertChild(*theActions, myActionAtom, kWhichAction,
                            kIndexOne, kIndexOne, sizeof(long),
                            &myAction, NULL);
    .
    .
    .
    myErr = QTInsertChild(*theActions, myActionAtom, kActionParameter,
                            kIndexOne, kIndexOne, myAppleURL[0] + 1,
                            &myAppleURL[1], NULL);

Now we add a hypertext link to the wired objects atom: ID 2

    myErr = QTInsertChild(*theActions, myWiredObjectsAtom,
                            kHyperTextItemAtomType, kIDTwo, kIndexZero,
                            kZeroDataLength, NULL, &myHyperTextAtom);
    .
    .
    .
 
    myValue = EndianS32_NtoB(mySelStart2);
    myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeStart,
                            kIDOne, kIndexZero, sizeof(long), &myValue,
                            NULL);
    .
    .
    .
 
    myValue = EndianS32_NtoB(mySelEnd2);
    myErr = QTInsertChild(*theActions, myHyperTextAtom, kRangeEnd,
                            kIDOne, kIndexZero, sizeof(long), &myValue,
                            NULL);
    .
    .
    .
 
    // add an event atom to the hypertext atom;
    // the event type can be any of the five mouse events: down, up,
    // trigger, enter, exit
    myErr = QTInsertChild(*theActions, myHyperTextAtom, kQTEventType,
                            kQTEventMouseClick, kIndexZero,
                            kZeroDataLength, NULL, &myEventAtom);
    .
    .
    .
    myErr = QTInsertChild(*theActions, myEventAtom, kAction, kIndexOne,
                            kIndexOne, kZeroDataLength, NULL,
                            &myActionAtom);
    .
    .
    .
    myAction = EndianS32_NtoB(kActionGoToURL);
    myErr = QTInsertChild(*theActions, myActionAtom, kWhichAction,
                            kIndexOne, kIndexOne, sizeof(long),
                            &myAction, NULL);
    .
    .
    .
    myErr = QTInsertChild(*theActions, myActionAtom, kActionParameter,
                            kIndexOne, kIndexOne, myCnnURL[0] + 1,
                            &myCnnURL[1], NULL);
    .
    .
    .
}

Step #5––Adding a Specified Atom Container

You call AddHTAct_AddHyperActionsToSample to add the specified atom container to the end of the specified media sample.

Hypertext actions are stored at the end of the sample as a normal text atom extension. In this case, the text atom type is kHyperTextTextAtomType and the data is the container data.

static OSErr AddHTAct_AddHyperActionsToSample (Handle theSample, QTAtomContainer theActions)
{
    Ptr         myPtr = NULL;
    long        myHandleLength;
    long        myContainerLength;
    long        myNewLength;
    OSErr       myErr = noErr;
 
    myHandleLength = GetHandleSize(theSample);
    myContainerLength = GetHandleSize((Handle)theActions);
 
    myNewLength = (long)(sizeof(long) + sizeof(OSType) +
                    myContainerLength);
 
    SetHandleSize(theSample, (myHandleLength + myNewLength));
    myErr = MemError();
    if (myErr != noErr)
        goto bail;
 
    HLock(theSample);
 
    // get a pointer to the beginning of the new block of space added to
    // the sample by the previous call to SetHandleSize; we need to
    // format that space as a text atom extension
    myPtr = *theSample + myHandleLength;
 
    // set the length of the text atom extension
    *(long *)myPtr = EndianS32_NtoB((long)(sizeof(long) + sizeof(OSType)
                                    + myContainerLength));
    myPtr += (sizeof(long));
 
    // set the type of the text atom extension
    *(OSType *)myPtr = EndianS32_NtoB(kHyperTextTextAtomType);
    myPtr += (sizeof(OSType));
 
    // set the data of the text atom extension;
    // we assume that this data is already in big-endian format
    HLock((Handle)theActions);
    BlockMove(*theActions, myPtr, myContainerLength);
 
    HUnlock((Handle)theActions);
    HUnlock(theSample);
    .
    .
    .
}

To add some hypertext actions to the specified text movie, you call AddHTAct_AddHyperTextToTextMovie.

static OSErr AddHTAct_AddHyperTextToTextMovie (FSSpec *theFSSpec)
{
    short                           myResID = 0;
    short                           myResRefNum = -1;
    Movie                           myMovie = NULL;
    Track                           myTrack = NULL;
    Media                           myMedia = NULL;
    TimeValue                       myTrackOffset;
    TimeValue                       myMediaTime;
    TimeValue                       mySampleDuration;
    TimeValue                       mySelectionDuration;
    TimeValue                       myNewMediaTime;
    TextDescriptionHandle           myTextDesc = NULL;
    Handle                          mySample = NULL;
    short                           mySampleFlags;
    Fixed                           myTrackEditRate;
    QTAtomContainer                 myActions = NULL;
    OSErr                           myErr = noErr;

At this point you open the movie file and get the first text track from the movie and then find the first text track in the movie.

    myErr = OpenMovieFile(theFSSpec, &myResRefNum, fsRdWrPerm);
    if (myErr != noErr)
        goto bail;
 
    myErr = NewMovieFromFile(&myMovie, myResRefNum, &myResID, NULL,
                                newMovieActive, NULL);
    if (myErr != noErr)
        goto bail;
 
 
    myTrack = GetMovieIndTrackType(myMovie, kIndexOne, TextMediaType,
                                    movieTrackMediaType);
    if (myTrack == NULL)
        goto bail;

Step #6––Getting the First Media Sample

Now you get the first media sample in the text track.

    myMedia = GetTrackMedia(myTrack);
    .
    .
    .
 
    myTrackOffset = GetTrackOffset(myTrack);
    myMediaTime = TrackTimeToMediaTime(myTrackOffset, myTrack);
 
    // allocate some storage to hold the sample description for the text
    // track
    myTextDesc = (TextDescriptionHandle)NewHandle(4);
    if (myTextDesc == NULL)
        goto bail;
 
    mySample = NewHandle(0);
    if (mySample == NULL)
        goto bail;
 
    myErr = GetMediaSample(myMedia, mySample, 0, NULL, myMediaTime, NULL,
                            &mySampleDuration,
                            (SampleDescriptionHandle)myTextDesc, NULL, 1,
                            NULL, &mySampleFlags);
    if (myErr != noErr)
        goto bail;

Step #7––Adding Hypertext Actions

Now you add hypertext actions to the first media sample.

    // create an action container for hypertext actions
    myErr = AddHTAct_CreateHyperTextActionContainer(&myActions);
    .
    .
    .
 
    // add hypertext actions actions to sample
    myErr = AddHTAct_AddHyperActionsToSample(mySample, myActions);
    .
    .
    .

Step #8––Replacing the Sample

Now you replace sample in the media.

    myTrackEditRate = GetTrackEditRate(myTrack, myTrackOffset);
    .
    .
    .
 
    GetTrackNextInterestingTime(myTrack, nextTimeMediaSample |
                                nextTimeEdgeOK, myTrackOffset, fixed1,
                                NULL, &mySelectionDuration);
    .
    .
    .
 
    myErr = DeleteTrackSegment(myTrack, myTrackOffset,
                                mySelectionDuration);
    .
    .
    .
    myErr = BeginMediaEdits(myMedia);
    .
    .
    .
 
    myErr = AddMediaSample( myMedia,
                            mySample,
                            0,
                            GetHandleSize(mySample),
                            mySampleDuration,
                            (SampleDescriptionHandle)myTextDesc,
                            1,
                            mySampleFlags,
                            &myNewMediaTime);
    .
    .
    .
 
    myErr = EndMediaEdits(myMedia);
    .
    .
    .
    // add the media to the track
    myErr = InsertMediaIntoTrack(myTrack, myTrackOffset, myNewMediaTime,
                                    mySelectionDuration,myTrackEditRate);
    .
    .
    .

Step #9––Updating the Movie Resource

Finally, you update the movie resource.

    myErr = UpdateMovieResource(myMovie, myResRefNum, myResID, NULL);
    .
    .
    .
 
    // close the movie file
    myErr = CloseMovieFile(myResRefNum);
 
bail:
    if (myActions != NULL)
        (void)QTDisposeAtomContainer(myActions);
 
    if (mySample != NULL)
        DisposeHandle(mySample);
 
    if (myTextDesc != NULL)
        DisposeHandle((Handle)myTextDesc);
 
    if (myMovie != NULL)
        DisposeMovie(myMovie);
 
    return(myErr);
}