TweetFollow Us on Twitter

Trading Places

Volume Number: 18 (2002)
Issue Number: 7
Column Tag: QuickTime Toolkit

Trading Places

Working with Alternate Tracks and Alternate Movies

by Tim Monroe

Introduction

QuickTime's original designers understood very clearly that QuickTime movies would be played back in quite varied environments -- on monitors with different pixel depths, on faster or slower machines, in Quebec or in Cupertino. They developed a mechanism by which QuickTime can choose one track from a group of tracks in a movie according to some characteristic of the playback environment. For instance, a movie might contain two sound tracks, one with an English narration and one with a French narration. At playback time, QuickTime selects the sound track whose language code matches that of the current operating system.

The group of tracks from which QuickTime selects is called an alternate track group, and a track in this group is called an alternate track. An alternate track is selected based on the language of the track's media or the quality of the track's media. The media quality is a relative indication of the track's sound or video quality (poor, good, better, or best). In addition, for visual media types, this media quality can specify which pixel depths for which the track is appropriate. Alternate track groups provide a simple but effective way to tailor a QuickTime movie for playback in different languages and on computers with different sound and video hardware.

QuickTime 3 introduced a way to tailor the movie data displayed to the user according to a much wider array of environmental characteristics, including the speed of the user's connection to the Internet, the version of QuickTime that is installed on the user's machine, and the availability or version of certain software components. This magic is accomplished not with alternate tracks but with alternate movies. An alternate movie is any one of a set of movies that contain media data appropriate for a specific characteristic or set of characteristics. For instance, one alternate movie might contain highly-compressed video and monophonic sound, while a second alternate movie contains higher-quality video and stereo sound. Because it is smaller in size, the first alternate movie would be appropriate for users with relatively slow Internet connections; the second movie would be appropriate for users with faster Internet connections. Similarly, one alternate movie might be appropriate for playback under QuickTime 6 and another might be appropriate for playback under all earlier versions of QuickTime.

Alternate movies are associated with one another using a movie file that contains data references to all of the alternate movies as well as information about the criteria by which QuickTime should select one of those alternate movies when that movie file is opened. This other movie file is called an alternate reference movie file (or, more briefly, a reference movie), since it refers to a set of alternate movies. When the principal selection criterion is the user's Internet connection speed, this movie is sometimes also called an alternate data rate movie.

In this article, we're going to take a look at alternate tracks and alternate movies. We'll see how to create alternate track groups in a movie and how to interact with QuickTime's alternate track selection either programmatically or using wired actions. Then we'll see how to create alternate reference movie files.

Alternate Tracks

As we've seen, tracks in a movie can be sorted into alternate track groups. At any time, at most one track in an alternate track group is enabled and the remaining tracks in the group are disabled. This enabling and disabling is called auto-alternating and is performed automatically by the Movie Toolbox, based either on the language of the tracks or the quality of the tracks. Tracks in an alternate track group can be of the same type (for example, all video tracks) or they can be of different types. Figure 1 shows a frame of a movie played on a computer with English system software. Figure 2 shows a frame of the very same movie when played on a computer with French system software. This movie contains two text tracks that are grouped into an alternate track group.


Figure 1: An English alternate track


Figure 2: A French alternate track

The Movie Toolbox provides about a dozen functions that we can use to work with alternate groups and with a media's language and quality. For instance, we can use these functions to create an alternate track group in a movie and to dynamically switch languages during movie playback. We can also use these functions to control QuickTime's alternate track selection process. We'll exercise some of these functions by adding a pop-up menu to the controller bar that allows the user to select from among the languages used in the movie. Figure 3 shows this pop-up menu at work.


Figure 3: The custom pop-up language menu

Getting and Setting a Media's Language

A media's language is specified using a 16-bit language code. (This is also sometimes called the region code.) Here are a few of the language codes defined in the header file Script.h:

enum {
   langEnglish                             = 0,
   langFrench                              = 1,
   langGerman                              = 2,
   langItalian                             = 3,
   langDutch                               = 4,
   langSwedish                             = 5,
   langSpanish                             = 6,
   langDanish                              = 7,
   langPortuguese                          = 8,
   langNorwegian                           = 9,
   langHebrew                              = 10,
   langJapanese                            = 11
};

When we create a new media (typically by calling NewTrackMedia), the language is set to 0, or langEnglish. We can, however, change the language by calling SetMediaLanguage. To set a media's language to French, we could use this line of code:

SetMediaLanguage(myMedia, langFrench);

When the movie's metadata is written to the movie file (typically by calling AddMovieResource or UpdateMovieResource), the language setting is saved in an atom (in particular, in the media header atom).

At playback time, we can call the GetMediaLanguage function to determine the language of a track's media, like this:

myLanguage = GetMediaLanguage(myMedia);

Listing 1 defines a function that calls GetMediaLanguage for each track in a movie, to determine which languages are used in a movie. If a language is used in the movie, the corresponding element in the array fLanguageMask is set to 1. (We'll use this array to determine which languages to put into our custom pop-up menu.)

Listing 1: Finding languages used in a movie

QTAlt_UpdateMovieLanguageMask
static OSErr QTAlt_UpdateMovieLanguageMask 
            (WindowObject theWindowObject)
{
   ApplicationDataHdl           myAppData = NULL;
   Movie                        myMovie = NULL;
   Track                        myTrack = NULL;
   Media                        myMedia = NULL;
   short                        myTrackCount, myIndex;
   OSErr                        myErr = noErr;
   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      return(paramErr);
   myMovie = (**theWindowObject).fMovie;
   if (myMovie == NULL)
      return(paramErr);
   // clear out the existing mask
   for (myIndex = 0; myIndex < kNumLanguages; myIndex++)
      (**myAppData).fLanguageMask[myIndex] = 0;
   // get the language of each track in the movie
   myTrackCount = GetMovieTrackCount(myMovie);
   for (myIndex = 1; myIndex <= myTrackCount; myIndex++) {
      myTrack = GetMovieIndTrack(myMovie, myIndex);
      if (myTrack != NULL) {
         short               myLanguage;
         myMedia = GetTrackMedia(myTrack);
         myLanguage = GetMediaLanguage(myMedia);
         if ((myLanguage >= 0) && (myLanguage < kNumLanguages))
            (**myAppData).fLanguageMask[myLanguage] = 1;
      }
   }
   return(myErr);
}

Notice that we use the language code as the index into the fLanguageMask array. This allows us to walk through that array to find the items that should be in the language pop-up menu, as illustrated by the QTAlt_GetMenuItemIndexForLanguageCode function (defined in Listing 2).

Listing 2: Finding a menu item index for a given language

QTAlt_GetMenuItemIndexForLanguageCode
UInt16 QTAlt_GetMenuItemIndexForLanguageCode 
            (WindowObject theWindowObject, short theCode)
{
   ApplicationDataHdl           myAppData = NULL;
   short                        myCount = 0;
   short                        myIndex = 0;
   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      return(myIndex);
   QTAlt_UpdateMovieLanguageMask(theWindowObject);
   for (myCount = 0; myCount <= theCode; myCount++) {
      if ((**myAppData).fLanguageMask[myCount] == 1)
         myIndex++;
   }
   return(myIndex);
}

Getting and Setting a Media's Quality

A track's media also has a quality, which is specified using a 16-bit value. Figure 4 shows how this value is interpreted.


Figure 4: A media quality value

The high-order byte is currently unused. Bits 6 and 7 of the low-order byte represent a relative quality. The Movie Toolbox defines these constants that we can use to get the relative quality from the entire quality value:

enum {
   mediaQualityDraft                     = 0x0000,
   mediaQualityNormal                    = 0x0040,
   mediaQualityBetter                    = 0x0080,
   mediaQualityBest                      = 0x00C0
};

Bits 0 through 5 of the low-order byte indicate, for visual media types, the pixel depths at which the track can be displayed. If bit n is set to 1, then the track can be displayed at pixel depth 2n. Thus, these 6 bits can indicate pixel depths from 1-bit (that is, 20) to 32-bit (25). More than one bit can be set, indicating that the track can be displayed at multiple pixel depths.

The Movie Toolbox provides two functions, GetMediaQuality and SetMediaQuailty, which we can use to get and set a media's quality value. Here's an example of setting a video track's quality to display at 16- and 32-bit pixel depths, at the highest quality:

short      myQuality = mediaQualityBest + 32 + 16;
SetMediaQuality(myMedia, myQuality);

If two or more tracks could be selected from a given alternate group (that is, their languages are the same and they are both valid at the current bit depth), QuickTime selects the track with the highest relative quality.

Creating Alternate Groups

Suppose now that we've assigned appropriate languages and quality values to some of the tracks in a movie. At this point, we can group these tracks into alternate track groups by calling SetTrackAlternate, which is declared essentially like this:

void SetTrackAlternate (Track theTrack, Track alternateT);

The first parameter, theTrack, is the track we want to add to a track group. The second parameter, alternateT, is a track that is already in that group. If theTrack is not already in a group and alternateT is in a group, then theTrack is added to the group that contains alternateT. If theTrack is already in a group but alternateT is not, then alternateT is added to the group that contains theTrack. If both theTrack and alternateT are already in groups, then the two groups are combined into one group. If neither theTrack nor alternateT is in a group, then a new group is created to hold them both. Finally, if alternateT is NULL, then theTrack is removed from the group that contains it.

In practice, this is actually much easier than it may sound from that description. For instance, if myTrack1 and myTrack2 are two video tracks with different media languages, we can group them into an alternate track group like this:

SetTrackAlternate(myTrack1, myTrack2);

And we can remove myTrack1 from its group like this:

SetTrackAlternate(myTrack1, NULL);

If we want to find all tracks in a particular alternate track group, we can use the GetTrackAlternate function, which is declared like this:

Track GetTrackAlternate (Track theTrack);

GetTrackAlternate returns the track identifier of the next track in the alternate track group. For instance, GetTrackAlternate(myTrack1) would return myTrack2, and GetTrackAlternate(myTrack2) would return myTrack1. As you can see, calling GetTrackAlternate repeatedly will eventually return the track identifier we started with. And if there is only one track in an alternate track group, GetTrackAlternate will return the track identifier we pass it.

Getting and Setting a Movie's Language

Recall that, when a movie file is first opened and prepared for playback, QuickTime selects the alternate track whose language code matches that of the current operating system. We can dynamically modify the language used by a movie by calling the SetMovieLanguage function. For instance, we can execute this code to set the language to French:

SetMovieLanguage(myMovie, (long)langFrench);

QuickTime inspects all alternate track groups in the movie and enables the track in each group that has that language. If no track in any alternate group has the specified language code, then the movie's language is not changed.

Interestingly, QuickTime does not provide a GetMovieLanguage function, even though it might sometimes be useful for us to know the current language being used in a movie. We can define our own function, however, to get this information. Listing 3 defines the QTUtils_GetMovieLanguage function, which looks at each enabled track and inspects the language of that track's media.

Listing 3: Finding a movie's current language

QTUtils_GetMovieLanguage
short QTUtils_GetMovieLanguage (Movie theMovie)
{
   Track            myTrack = NULL;
   Media            myMedia = NULL;
   short            myTrackCount, myIndex;
   short            myLanguage = -1;      // an invalid language code
   myTrackCount = GetMovieTrackCount(theMovie);
   for (myIndex = 1; myIndex <= myTrackCount; myIndex++) {
      myTrack = GetMovieIndTrack(theMovie, myIndex);
      if ((myTrack != NULL) && GetTrackEnabled(myTrack)) {
         Track      myAltTrack = NULL;
         myAltTrack = GetTrackAlternate(myTrack);
         if ((myAltTrack != NULL) && (myAltTrack != myTrack)) {
            myMedia = GetTrackMedia(myTrack);
            myLanguage = GetMediaLanguage(myMedia);
            break;
         }
      }
   }
   return(myLanguage);
}

You'll notice that we don't simply get the language of the first enabled track's media; rather, we call GetTrackAlternate to make sure that the track is contained in an alternate track group that contains at least two tracks. (A media's language is ignored by the Movie Toolbox if the corresponding track is not part of any alternate track group.)

Listing 4 shows the QTAlt_HandleCustomButtonClick function, which we use to handle user clicks on the custom button in the controller bar. We've encountered code like this before, so we'll be content here to just show the code.

Listing 4: Handling clicks on the custom controller bar button

QTAlt_HandleCustomButtonClick
void QTAlt_HandleCustomButtonClick (MovieController theMC, 
            EventRecord *theEvent, long theRefCon)
{
   MenuHandle               myMenu = NULL;
   WindowObject             myWindowObject = 
                     (WindowObject)theRefCon;
                     
   ApplicationDataHdl       myAppData = NULL;
   StringPtr                myMenuTitle = 
                     QTUtils_ConvertCToPascalString(kMenuTitle);
   
   UInt16                   myCount = 0;
   // make sure we got valid parameters
   if ((theMC == NULL) || (theEvent == NULL) || 
            (theRefCon == NULL))
      goto bail;
   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(myWindowObject);
   if (myAppData == NULL)
      goto bail;
   // create a new menu
   myMenu = NewMenu(kCustomButtonMenuID, myMenuTitle);
   if (myMenu != NULL) {
      long                     myItem = 0;
      Point                    myPoint;
      
      // add all track languages in the current movie to the pop-up menu
      myCount = QTAlt_AddMovieLanguagesToMenu(myWindowObject, 
            myMenu);
      if (((**myAppData).fCurrMovieIndex > 0) && 
            ((**myAppData).fCurrMovieIndex <= myCount))
         MacCheckMenuItem(myMenu, (**myAppData).fCurrMovieIndex, 
            true);
      // insert the menu into the menu list
      MacInsertMenu(myMenu, hierMenu);
      // find the location of the mouse click;
      // the top-left corner of the pop-up menu is anchored at this point
      myPoint = theEvent->where;
      LocalToGlobal(&myPoint);
      // display the pop-up menu and handle the item selected
      myItem = PopUpMenuSelect(myMenu, myPoint.v, myPoint.h, 
            myItem);
      if (myItem > 0) {
         (**myAppData).fCurrMovieIndex = myItem;
         SetMovieLanguage(MCGetMovie(theMC), 
            QTAlt_GetLanguageCodeForMenuItemIndex(myWindowObject, 
            myItem));
         UpdateMovie(MCGetMovie(theMC));
      }
      // remove the menu from the menu list
      MacDeleteMenu(GetMenuID(myMenu));
      // dispose of the menu
      DisposeMenu(myMenu);
   }
bail:
   free(myMenuTitle);
}

We've already seen all of the application-defined functions used here, except for QTAlt_AddMovieLanguagesToMenu. Listing 5 shows our definition of this function.

Listing 5: Adding languages to the pop-up menu

QTAlt_AddMovieLanguagesToMenu
static UInt16 QTAlt_AddMovieLanguagesToMenu (WindowObject theWindowObject, MenuHandle theMenu)
{
   ApplicationDataHdl           myAppData = NULL;
   Movie                        myMovie = NULL;
   Track                        myTrack = NULL;
   Media                        myMedia = NULL;
   short                        myIndex;
   UInt16                       myCount = 0;
   
   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      goto bail;
   // update the mask of movie languages
   QTAlt_UpdateMovieLanguageMask(theWindowObject);
   // add menu items
   for (myIndex = 0; myIndex < kNumLanguages; myIndex++) {
      if ((**myAppData).fLanguageMask[myIndex] == 1) {
         StringPtr       myItemText = 
      QTUtils_ConvertCToPascalString(gLanguageArray[myIndex]);
         MacAppendMenu(theMenu, myItemText);
         free(myItemText);
         myCount++;
      }
   }
bail:
   return(myCount);
}

Enabling and Disabling Alternate Track Selection

The selection of alternate tracks based on the media language and quality occurs automatically when a movie file is first opened. We can override this default behavior, however, by passing the newMovieDontAutoAlternates flag when we call NewMovieFromFile or NewMovieFromDataRef (or any of the other NewMovieFrom calls). Similarly, we can change the state of auto-alternating dynamically using the SetAutoTrackAlternatesEnabled function. For instance, we can turn off auto-alternating for a specific movie like this:

SetAutoTrackAlternatesEnabled(myMovie, false);

When auto-alternating is on, the Movie Toolbox rescans alternate track groups whenever we execute a function that might change one of the relevant selection criteria. For instance, if we change the movie's language (by calling SetMovieLanguage), the Movie Toolbox rescans all alternate track groups to make sure that the correct alternate track is enabled. Likewise, the Movie Toolbox performs this check if we change any of the relevant visual characteristics of the movie (by calling functions like SetMovieGWorld, UpdateMovie, or SetMovieMatrix). Moreover, the alternate track groups are rescanned if the user performs any action that causes QuickTime to call any of these functions internally, such as moving the movie window from one monitor to another (since the monitors may be set to different pixel depths).

If for some reason we want to explicitly force the Movie Toolbox to rescan the alternate groups in a file immediately, we can call the SelectMovieAlternates function, like so:

SelectMovieAlternates(myMovie);

Note, however, that if all the tracks in a particular alternate track group are disabled when we call SelectMovieAlternates, then none of the tracks in that group will be enabled.

Changing Alternate Tracks with Wired Actions

QuickTime provides a wired action, kActionMovieSetLanguage, which we can use in wired atoms to change a movie's language. This action takes a single parameter, which is a long integer that specifies the desired language. The function WiredUtils_AddMovieSetLanguage, defined in Listing 6, shows how we can add these wired actions to an atom container.

Listing 6: Adding a set-language action

WiredUtils_AddMovieSetLanguage
OSErr WiredUtils_AddMovieSetLanguage 
            (QTAtomContainer theContainer, QTAtom theAtom, 
               long theEvent, long theLanguage)
{
   QTAtom            myActionAtom = 0;
   OSErr             myErr = noErr;
   
   myErr = WiredUtils_AddQTEventAndActionAtoms(theContainer, 
         theAtom, theEvent, kActionMovieSetLanguage, 
         &myActionAtom);
   if (myErr != noErr)
      goto bail;
      
   theLanguage = EndianS32_NtoB(theLanguage);
   myErr = WiredUtils_AddActionParameterAtom(theContainer, 
         myActionAtom, kFirstParam, sizeof(theLanguage), 
         &theLanguage, NULL);
bail:
   return(myErr);
}

There is currently no wired action for setting a media's quality, and there are no wired operands for getting a movie's current language or quality. Sigh.

Alternate Movies

Let's turn now to consider alternate movies and alternate reference movies. An alternate reference movie file is a movie file that contains references to a set of alternate movies, together with the information that QuickTime should use to select one of those movies when the alternate reference movie file is opened. This information can rely on a wide range of features of the operating environment, including:

    * Internet connection speed.

    * Language of the operating system software.

    * CPU speed; this is a relative ranking, ranging from 1 (slowest) to 5 (fastest).

    * Installed version of QuickTime.

    * Installed version of a specific software component.

    * Network connection status.

To create an alternate reference movie, we need to know which alternate movies it should refer to and what criteria are to be used in selecting a movie from among that collection of alternate movies. Apple provides a utility called MakeRefMovie that is widely used for creating alternate reference movies. Figure 5 shows the MakeRefMovie main window, containing several panes that describe the alternate movies and their selection criteria.


Figure 5: The main window of MakeRefMovie

Each pane specifies either a local movie file or a URL to a movie file, together with the selection settings for that movie. Notice that each pane also contains a check box labeled "Flatten into output" (see Figure 6). If this box is checked, then the data for the specified movie will be included in toto in the alternate reference movie file. This movie (let's call it the contained movie) will be selected if the alternate reference movie file is opened under a version of QuickTime prior to 3 (which is the earliest version that supports alternate reference movies) or if none of the criteria for selecting one of the other alternate movies is met. Clearly, at most one alternate movie should be specified as the contained movie, and MakeRefMovie enforces this restriction by allowing at most one of these boxes to be checked at any one time.


Figure 6: An alternate movie pane

There are also other tools available for creating alternate reference movies. If you like to work with XML, you can use the XMLtoRefMovie utility written by former QuickTime engineer Peter Hoddie. XMLtoRefMovie converts a text file containing an XML-based description of alternate movies and selection criteria into an alternate reference movie file. Listing 7 shows the XML data for the information shown in Figure 5.

Listing 7: Specifying an alternate reference movie using XML

pitch_ref.xml
<qtrefmovie>
   <refmovie src="first_pitch_56.mov" data-rate="56k modem" />
   <refmovie src="first_pitch_t1.mov" data-rate="t1" />
</qtrefmovie>

And Listing 8 shows the XML data for an alternate reference movie that has a contained movie (or default movie, in XMLtoRefMovie parlance).

Listing 8: Including a contained movie using XML

pitch_ref.xml
<qtrefmovie>
   <default-movie src="file:///meditations/pitch.mov" />
   <refmovie src="first_pitch_56.mov" data-rate="56k modem" />
   <refmovie src="first_pitch_t1.mov" data-rate="t1" />
</qtrefmovie>

In the rest of this section, I'll assume that we already know which set of movies to use as alternate movies and what the selection criteria are. We want to see how to take that information and create a final alternate reference movie file.

Creating Alternate Reference Movies

We've learned in earlier articles that a QuickTime movie file is structured as a series of atoms. Each atom contains some data prefixed by a header. The header is 8 bytes long and specifies the length of the entire atom (header included) and the type of the atom. All of this data (header and atom data) is stored in big-endian format.

A typical QuickTime movie file contains a movie atom (of type MovieAID, or 'moov'), which contains a movie header atom (of type 'mvhd') and one or more track atoms (of type 'trak'). A self-contained movie file also contains a movie data atom (of type 'mdat'). Figure 7 illustrates this structure.


Figure 7: The structure of a standard QuickTime movie file

The movie atom contains the movie header atom and the track atom, so its total length is the lengths of those atoms plus the length of the movie atom header (that is, x + y + 8).

An alternate reference movie file likewise contains a movie atom, but it does not always contain a movie header atom or any track atoms. Instead, when there is no contained movie, the movie atom contains a single reference movie record atom (of type ReferenceMovieRecordAID, or 'rmra'); in turn, this reference movie record atom contains one reference movie descriptor atom (of type 'rmda') for each alternate movie described by the alternate reference movie. Figure 8 shows this general structure.


Figure 8: The structure of an alternate reference movie file

When an alternate reference movie file does have a contained movie, the movie header atom and the track atoms should follow the reference movie record atom, as shown in Figure 9.


Figure 9: An alternate reference movie file with a contained movie.

A reference movie descriptor atom describes a single alternate movie. It contains other atoms that indicate the selection criteria for that alternate movie, as well as a data reference atom that specifies the location of the alternate movie. The header file MoviesFormat.h contains a set of atom ID constants that we can use to indicate these atom types:

enum {
   ReferenceMovieRecordAID                = FOUR_CHAR_CODE('rmra'),
   ReferenceMovieDescriptorAID            = FOUR_CHAR_CODE('rmda'),
   ReferenceMovieDataRefAID               = FOUR_CHAR_CODE('rdrf'),
   ReferenceMovieVersionCheckAID          = FOUR_CHAR_CODE('rmvc'),
   ReferenceMovieDataRateAID              = FOUR_CHAR_CODE('rmdr'),
   ReferenceMovieComponentCheckAID        = FOUR_CHAR_CODE('rmcd'),
   ReferenceMovieQualityAID               = FOUR_CHAR_CODE('rmqu'),
   ReferenceMovieLanguageAID              = FOUR_CHAR_CODE('rmla'),
   ReferenceMovieCPURatingAID             = FOUR_CHAR_CODE('rmcs'),
   ReferenceMovieAlternateGroupAID        = FOUR_CHAR_CODE('rmag'),
   ReferenceMovieNetworkStatusAID         = FOUR_CHAR_CODE('rnet')
};

Specifying an Alternate Movie

A reference movie descriptor atom must contain a single data reference atom, which specifies an alternate movie. The atom data for the data reference atom is a reference movie data reference record, which has this structure:

struct ReferenceMovieDataRefRecord {
   long                       flags;
   OSType                     dataRefType;
   long                       dataRefSize;
   char                       dataRef[1];
};

Currently, this value is defined for the flags field:

enum {
   kDataRefIsSelfContained                  = (1 << 0)
};

If the flags field is kDataRefIsSelfContained, then the other fields are ignored and QuickTime searches for the movie data in the alternate reference movie itself (as shown in Figure 9). Otherwise, if the flags field is 0, QuickTime uses the data reference contained in the dataRef field, using the dataRefType and dataRefSize fields to determine the type and size of that data reference.

The dataRef field can contain any kind of data reference supported by QuickTime, but usually it's either a file data reference or a URL data reference. (See "Somewhere I'll Find You" in MacTech, October 2000 for more information about data references.) Listing 9 shows some code that builds a data reference atom, based on information stored in an application data structure associated with a pane.

Listing 9: Creating a data reference atom

MRM_SaveReferenceMovie
ReferenceMovieDataRefRecord               myDataRefRec;
AliasHandle                               myAlias = NULL;
Ptr                                       myData = NULL;
long                                      myFlags;

switch((**myAppData).fPaneType) {
   case kMRM_MoviePaneType:
      // create an alias record for the movie
      myErr = NewAlias(&gRefMovieSpec, 
            &(**myWindowObject).fFileFSSpec, &myAlias);
      if (myErr != noErr)
         goto bailLoop;
      if ((**myAppData).fCurrentFlat == kMRM_FlatCheckOn)
         myFlags = kDataRefIsSelfContained;
      else
         myFlags = 0;
      myDataRefRec.flags = EndianU32_NtoB(myFlags);
      myDataRefRec.dataRefType = 
            EndianU32_NtoB(kMRM_DataRefTypeAlias);
      myDataRefRec.dataRefSize = 
            EndianU32_NtoB(GetHandleSize((Handle)myAlias));
   // allocate a data block and copy the data reference record and alias record into it
      myData = NewPtrClear(sizeof(ReferenceMovieDataRefRecord) 
            + GetHandleSize((Handle)myAlias) - 1);
      if (myData == NULL)
         goto bailLoop;
      HLock((Handle)myAlias);
      BlockMove(&myDataRefRec, myData, 
            sizeof(ReferenceMovieDataRefRecord) - sizeof(char) - 
            1);
      BlockMove(*myAlias, (Ptr)(myData + 
            sizeof(ReferenceMovieDataRefRecord) - sizeof(char) - 
            1), GetHandleSize((Handle)myAlias));
      HUnlock((Handle)myAlias);
      break;
   case kMRM_URLPaneType:
      myDataRefRec.flags = EndianU32_NtoB(0L);
      myDataRefRec.dataRefType = 
            EndianU32_NtoB(kMRM_DataRefTypeURL);
      myDataRefRec.dataRefSize = 
            EndianU32_NtoB(strlen((**myAppData).fURL) + 1);
      // allocate a data block and copy the data reference record and URL into it
      myData = NewPtrClear(sizeof(ReferenceMovieDataRefRecord) 
            + strlen((**myAppData).fURL) - 1);
      if (myData == NULL)
         goto bailLoop;
      BlockMove(&myDataRefRec, myData, 
            sizeof(ReferenceMovieDataRefRecord) - sizeof(char) - 
            1);
      BlockMove((**myAppData).fURL, (Ptr)(myData + 
            sizeof(ReferenceMovieDataRefRecord) - sizeof(char) - 
            1), strlen((**myAppData).fURL) + 1);
      break;
}

Keep in mind that the ReferenceMovieDataRefRecord structure contains as its final field a single character, which is a placeholder for the actual data reference; this means that we generally need to subtract sizeof(char) from sizeof(ReferenceMovieDataRefRecord) to get the "real" size of the record.

Specifying Selection Criteria

A reference movie descriptor atom can contain one or more atoms that indicate the selection criteria for the movie specified in the data reference atom. To indicate that that movie should be selected based on the user's Internet connection speed, we add a data rate atom, of type ReferenceMovieDataRateAID. The atom data for the data rate atom is an alternate data rate record, which has this structure:

struct QTAltDataRateRecord {
   long                        flags;
   long                        dataRate;
};

The flags field is currently always 0, and the dataRate field should contain one of these constants:

enum {
   kDataRate144ModemRate               = 1400L,
   kDataRate288ModemRate               = 2800L,
   kDataRateISDNRate                   = 5600L,
   kDataRateDualISDNRate               = 11200L,
   kDataRate256kbpsRate                = 25600L,
   kDataRate384kbpsRate                = 38400L,
   kDataRate512kbpsRate                = 51200L,
   kDataRate768kbpsRate                = 76800L,
   kDataRate1MbpsRate                  = 100000L,
   kDataRateT1Rate                     = 150000L,
   kDataRateInfiniteRate               = 0x7FFFFFFF
};

Figure 10 shows the atom data of a reference movie descriptor atom for a movie that is appropriate for downloading across a connection whose speed is 56 Kb/sec.


Figure 10: A connection speed reference movie descriptor atom

When an alternate reference movie is opened, QuickTime looks for a reference movie descriptor atom whose data rate atom matches the connection speed specified in the Connection Speed pane of the QuickTime control panel. If no data rate atom exactly matches that speed preference, then the movie with the highest data rate that is less than that preference will be selected. If, further, no data rate atom specifies a data rate that is less than the user's preference, then the alternate movie with the lowest data rate will be selected.

To indicate that an alternate movie should be selected based on the operating system's language, we add a language atom (of type ReferenceMovieLanguageAID) to the reference movie descriptor atom. The atom data for the language atom is an alternate language record, which has this structure:

struct QTAltLanguageRecord {
   long                        flags;
   short                       language;
};

The flags field is once again always 0, and the language field should contain a language code. Listing 10 shows a chunk of code that creates a language atom.

Listing 10: Adding a language atom

MRM_SaveReferenceMovie
if ((**myAppData).fCurrLangIndex > 0) {
   QTAltLanguageRecord         myLanguageRec;
   long                              myAtomHeader[2];
   myLanguageRec.flags = 0L;
   myLanguageRec.language = EndianS16_NtoB(
            (**myAppData).fCurrLang);
   // concatenate the header for the language atom
   myAtomHeader[0] = EndianU32_NtoB(
            sizeof(myLanguageRec) + myAtomHeaderSize);
   myAtomHeader[1] = EndianU32_NtoB(
            ReferenceMovieLanguageAID);
   myErr = PtrAndHand(myAtomHeader, myRefMovieDescAtom, 
            myAtomHeaderSize);
   if (myErr != noErr)
      goto bail;
   // concatenate the language data onto the end of the reference movie descriptor atom
   myErr = PtrAndHand(&myLanguageRec, myRefMovieDescAtom, 
            sizeof(myLanguageRec));
   if (myErr != noErr)
      goto bail;
}

To indicate that an alternate movie should be selected based on the movie quality, we can add an atom of type ReferenceMovieQualityAID to the reference movie descriptor atom. In this case, the atom data is simply a signed long integer, as shown in Figure 11.


Figure 11: A quality reference movie descriptor atom

If two alternate movies meet all other listed selection criteria, then QuickTime uses the quality atom to break the tie; the alternate movie with the higher specified quality is selected.

Adding a Contained Movie

It's relatively straightforward to build an alternate reference movie file if there is no contained movie; as we've seen, we simply need to add the appropriate atoms to the movie file (and we've had plenty of practice doing that in earlier articles). Things get a bit more complicated when we want to build an alternate reference movie file that holds a contained movie, since we need to merge the contained movie with the alternate movie data to obtain the file shown in Figure 9. This actually isn't all that complicated, thanks to the fact that the FlattenMovieData function will happily append the flattened data to an existing movie file, if we tell it to do so. Let's see how to exploit that capability to create an alternate reference movie file with a contained movie.

Suppose we have created an alternate reference movie atom structure in memory, which has the structure shown in Figure 8. Suppose also that we have opened the movie that is to be flattened into the alternate reference movie file. We'll begin by creating a file that is the size of the reference movie record atom that's contained in the existing alternate reference movie atom:

myErr = FSpCreate(&gRefMovieSpec, FOUR_CHAR_CODE('TVOD'), 
            MovieFileType, 0);
myErr = FSpOpenDF(&gRefMovieSpec, fsRdWrPerm, &myRefNum);
myMovAtomSize = GetHandleSize(theMovieAtom);
myRefAtomSize = myMovAtomSize - myAtomHeaderSize;
*(long *)myData = EndianU32_NtoB(myRefAtomSize);
*(long *)(myData + sizeof(long)) = 
         EndianU32_NtoB(FreeAtomType);
SetFPos(myRefNum, fsFromStart, 0);
FSWrite(myRefNum, &myCount, myData);

As you can see, this new file contains all zeros, except for the 8-byte atom header; the atom type is set to FreeAtomType (that is, 'free'). Figure 12 shows the new file at this point.


Figure 12: Step 1: Space for the reference movie record atom

Now we'll call FlattenMovieData to append the data for the contained movie onto the end of this file:

myMovie = FlattenMovieData(theMovie, 
                        flattenAddMovieToDataFork | 
                        flattenForceMovieResourceBeforeMovieData,
                        &gRefMovieSpec,
                        FOUR_CHAR_CODE('TVOD'),
                        smSystemScript,
                        0L);

Notice that we pass the flag flattenAddMovieToDataFork to append the data to the existing file and flattenForceMovieResourceBeforeMovieData to ensure that the movie atom is written out at the beginning of the data. At this point, the file has the structure shown in Figure 13.


Figure 13: Step 2: The flattened data of the contained movie

We reopen the file and read the size of the 'moov' atom:

myErr = FSpOpenDF(&gRefMovieSpec, fsRdWrPerm, &myRefNum);
SetFPos(myRefNum, fsFromStart, myRefAtomSize);
myCount = 8;
myErr = FSRead(myRefNum, &myCount, &(myAtom[0]));
myAtom[0] = EndianU32_BtoN(myAtom[0]);

Now we know what the size of the entire movie file should be, and we allocate a block of memory large enough to hold the file data:

myData = NewPtrClear(myRefAtomSize + myAtom[0]);
We write a 'moov' atom header into this new block of data:
*(long *)myData = EndianU32_NtoB(myRefAtomSize + myAtom[0]);
*(long *)(myData + sizeof(long)) = EndianU32_NtoB(MovieAID);
Then we copy the reference movie record atom into place:
BlockMoveData(*theMovieAtom + myAtomHeaderSize, myData + 
            myAtomHeaderSize, myRefAtomSize);

And then we read the flattened movie data out of our existing movie file into the appropriate spot in our block of data:

myCount = myAtom[0] - myAtomHeaderSize;
myErr = FSRead(myRefNum, &myCount, myData + yAtomHeaderSize + 
            myRefAtomSize);

We're almost done; the block of memory now looks like Figure 14.


Figure 14: Step 3: The movie data in memory

All we need to do now is write this block of data back into the movie file, overwriting its existing data.

myCount = myRefAtomSize + myAtom[0];
SetFPos(myRefNum, fsFromStart, 0);
myErr = FSWrite(myRefNum, &myCount, myData);

And we're done! The complete definition of MRM_MakeReferenceMovie is shown in Listing 11.

Listing 11: Adding a contained movie to an alternate reference movie

MRM_MakeReferenceMovie
void MRM_MakeReferenceMovie 
            (Movie theMovie, Handle theMovieAtom)
{
   Movie              myMovie = NULL;
   short              myRefNum = -1;
   long               myAtom[2];
   Ptr                myData = NULL;
   long               myCount;               // the number of bytes to read or write
   long               myMovAtomSize;         // size of the entire movie atom
   long               myRefAtomSize;         // size of the reference movie atom
   unsigned long      myAtomHeaderSize = 2 * sizeof(long);
   OSErr              myErr = noErr;

   // create and open the output file
   myErr = FSpCreate(&gRefMovieSpec, FOUR_CHAR_CODE('TVOD'), 
            MovieFileType, 0);
   if (myErr != noErr) {
      // if the file already exists, we want to delete it and recreate it
      if (myErr == dupFNErr) {
         myErr = FSpDelete(&gRefMovieSpec);
         if (myErr == noErr)
           myErr = FSpCreate(&gRefMovieSpec, 
                  FOUR_CHAR_CODE('TVOD'), MovieFileType, 0);
      }
      if (myErr != noErr)
         goto bail;
   }
   myErr = FSpOpenDF(&gRefMovieSpec, fsRdWrPerm, &myRefNum);
      if (myErr != noErr)
         goto bail;
   myMovAtomSize = GetHandleSize(theMovieAtom);
   myRefAtomSize = myMovAtomSize - myAtomHeaderSize;
   HLock(theMovieAtom);
   // if there is no contained movie to flatten into the output file,
   // we can skip most of this code and just go ahead and write the data
   if (theMovie == NULL) {
      myData = *theMovieAtom;
      myCount = myMovAtomSize;
      goto writeData;
   }
   // write a free atom at the start of the file so that FlattenMovieData adds the new 
   // movie resource and media data far enough into the file to allow room for the 
   // reference movie atom
   myCount = myRefAtomSize;
   myData = NewPtrClear(myRefAtomSize);
   if (myData == NULL) {
      myErr = MemError();
      goto bail;
   }
   *(long *)myData = EndianU32_NtoB(myRefAtomSize);
   *(long *)(myData + sizeof(long)) = 
            EndianU32_NtoB(FreeAtomType);
   SetFPos(myRefNum, fsFromStart, 0);
   FSWrite(myRefNum, &myCount, myData);
   DisposePtr(myData);
   // close the file, so that FlattenMovieData can open it
   FSClose(myRefNum);
   // flatten the contained movie into the output file;
   // because the output file already exists and because we're not deleting it,
   // the flattened movie data is *appended* to the existing data
   myMovie = FlattenMovieData(theMovie, 
                        flattenAddMovieToDataFork | 
                           flattenForceMovieResourceBeforeMovieData,
                        &gRefMovieSpec,
                        FOUR_CHAR_CODE('TVOD'),
                        smSystemScript,
                        0L);
   myErr = GetMoviesError();
   if (myErr != noErr)
         goto bail;
   // open the output file again and read the movie atom
   myErr = FSpOpenDF(&gRefMovieSpec, fsRdWrPerm, &myRefNum);
   if (myErr != noErr)
      goto bail;
   SetFPos(myRefNum, fsFromStart, myRefAtomSize);
   // should put us at the 'moov' atom
   myCount = 8;
   myErr = FSRead(myRefNum, &myCount, &(myAtom[0]));
   if (myErr != noErr)
      goto bail;
   // swap the size and type data so that we can use it here
   myAtom[0] = EndianU32_BtoN(myAtom[0]);
   myAtom[1] = EndianU32_BtoN(myAtom[1]);
   if (myAtom[1] != MovieAID) {            // this should never happen
      myErr = paramErr;
      goto bail;
   }
   myData = NewPtrClear(myRefAtomSize + myAtom[0]);
   if (myData == NULL) {
      myErr = MemError();
      goto bail;
   }
// merge the movie atom that FlattenMovieData created with the reference movie atom
   *(long *)myData = EndianU32_NtoB(myRefAtomSize + 
            myAtom[0]);
   *(long *)(myData + sizeof(long)) = 
            EndianU32_NtoB(MovieAID);
   // insert the reference movie atom
   BlockMoveData(*theMovieAtom + myAtomHeaderSize, myData + 
            myAtomHeaderSize, myRefAtomSize);
   // read original movie atom
   myCount = myAtom[0] - myAtomHeaderSize;
   myErr = FSRead(myRefNum, &myCount, myData + 
            myAtomHeaderSize + myRefAtomSize);
   if (myErr != noErr)
         goto bail;
   myCount = myRefAtomSize + myAtom[0];
writeData:
   // write the final movie to disk
   SetFPos(myRefNum, fsFromStart, 0);
   myErr = FSWrite(myRefNum, &myCount, myData);
bail:
   if (myData != NULL)
      DisposePtr(myData);
   if (myRefNum != -1)
      FSClose(myRefNum);
   HUnlock(theMovieAtom);
}

Conclusion

Alternate track groups provide a simple but effective way to tailor a QuickTime movie for playback in different languages and on computers with different sound and video hardware. They do, however, have their limitations. For one thing, adding lots of alternate tracks can increase the size of the movie file to the point that it's too bulky for easy web deployment. More importantly, alternate tracks can be selected based only on the quality or language of the track. For these reasons, it's generally more useful to use alternate reference movies to select one from a set of alternate movies.

Credits and References


Tim Monroe in a member of the QuickTime engineering team. You can contact him at monroe@apple.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

OmniGraffle Pro 7.2.2 - Create diagrams,...
OmniGraffle Pro helps you draw beautiful diagrams, family trees, flow charts, org charts, layouts, and (mathematically speaking) any other directed or non-directed graphs. We've had people use... Read more
OmniGraffle 7.2.2 - Create diagrams, flo...
OmniGraffle helps you draw beautiful diagrams, family trees, flow charts, org charts, layouts, and (mathematically speaking) any other directed or non-directed graphs. We've had people use Graffle to... Read more
Spotify 1.0.44.100. - Stream music, crea...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
Microsoft OneNote 15.29 - Free digital n...
OneNote is your very own digital notebook. With OneNote, you can capture that flash of genius, that moment of inspiration, or that list of errands that's too important to forget. Whether you're at... Read more
WALTR 2 2.0.8 - $39.95
WALTR 2 helps you wirelessly drag-and-drop any music, ringtones, videos, PDF, and ePub files onto your iPhone, iPad, or iPod without iTunes. It is the second major version of Softorino's critically-... Read more
Dropbox 16.3.27 - Cloud backup and synch...
Dropbox is an application that creates a special Finder folder that automatically syncs online and between your computers. It allows you to both backup files and keep them up-to-date between systems... Read more
EtreCheck 3.1.5 - For troubleshooting yo...
EtreCheck is an app that displays the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support Communities to... Read more
Carbon Copy Cloner 4.1.12 - Easy-to-use...
Carbon Copy Cloner backups are better than ordinary backups. Suppose the unthinkable happens while you're under deadline to finish a project: your Mac is unresponsive and all you hear is an ominous,... Read more
VueScan 9.5.62 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
SpamSieve 2.9.27 - Robust spam filter fo...
SpamSieve is a robust spam filter for major email clients that uses powerful Bayesian spam filtering. SpamSieve understands what your spam looks like in order to block it all, but also learns what... Read more

Latest Forum Discussions

See All

Track Santa with these three festive app...
Christmas is fast approaching and that means it's time to prepare for Santa's yearly pilgrimage around the globe. Christmas Eve is an exciting time as parents help their kids get ready to welcome Santa. You've got the cookies and milk all planned... | Read more »
Galaxy on Fire 3 and four other fantasti...
Galaxy on Fire 3 - Manticore brings the series back for another round of daring space battles. It's familiar territory for folks who are familiar with the franchise. If you've beaten the game and are looking to broaden your horizons, might we... | Read more »
The best apps for your holiday gift exch...
What's that, you say? You still haven't started your holiday shopping? Don't beat yourself up over it -- a lot of people have been putting it off, too. It's become easier and easier to procrastinate gift shopping thanks to a number of apps that... | Read more »
Toca Hair Salon 3 (Education)
Toca Hair Salon 3 1.0 Device: iOS Universal Category: Education Price: $2.99, Version: 1.0 (iTunes) Description: | Read more »
Winter comes to Darkwood as Seekers Note...
MyTona, based in the chilly Siberian city of Yakutsk, has brought a little festive fun to its hidden object game Seekers Notes: Hidden Mystery. The Christmas update introduces some new inhabitants to players, and with them a chance to win plenty of... | Read more »
Bully: Anniversary Edition (Games)
Bully: Anniversary Edition 1.03.1 Device: iOS Universal Category: Games Price: $6.99, Version: 1.03.1 (iTunes) Description: *** PLEASE NOTE: This game is officially supported on the following devices: iPhone 5 and newer, iPod Touch... | Read more »
PINE GROVE (Games)
PINE GROVE 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: A pine grove where there are no footsteps of people due to continuous missing cases. The case is still unsolved and nothing has... | Read more »
Niantic teases new Pokémon announcement...
After rumors started swirling yesterday, it turns out there is an official Pokémon GO update on its way. We’ll find out what’s in store for us and our growing Pokémon collections tomorrow during the Starbucks event, but Niantic will be revealing... | Read more »
3 reasons why Nicki Minaj: The Empire is...
Nicki Minaj is as business-savvy as she is musically talented and she’s proved that by launching her own game. Designed by Glu, purveyors of other fine celebrity games like cult favorite Kim Kardashian: Hollywood, Nicki Minaj: The Empire launched... | Read more »
Clash of Clans is getting its own animat...
Riding on its unending wave of fame and success, Clash of Clans is getting an animated web series based on its Clash-A-Rama animated shorts.As opposed to the current shorts' 60 second run time, the new and improved Clash-A-Rama will be comprised of... | Read more »

Price Scanner via MacPrices.net

New 2016 13-inch Touch Bar MacBook Pros on sa...
B&H Photo the new 2016 Apple 13″ 2.9GHz/256GB Touch Bar MacBook Pros on sale for $50 off MSRP, each including free shipping plus NY sales tax only: - 13″ 2.9GHz/256GB Touch Bar MacBook Pro Space... Read more
12-inch 1.2GHz Space Gray Retina MacBook on s...
B&H Photo has dropped their price on the 2016 Apple 12″ 1.2GHz Space Gray Retina MacBook (MLH82LL/A) to $1399 including free shipping plus NY sales tax only. Their price is $200 off MSRP, and it’... Read more
Never Settle for Low Performing Wifi With iOS...
AppYogi Software has announced the release of WiFi Signal Strength Status App 1.0, the company’s new utility developed exclusively for macOS. WiFi Signal Strength Status App features a unique, single... Read more
New 2016 13-inch Touch Bar MacBook Pros in st...
B&H Photo has stock of new 2016 Apple 13″ Touch Bar MacBook Pro models, each including free shipping plus NY sales tax only: - 13″ 2.9GHz/512GB Touch Bar MacBook Pro Space Gray: $1999 - 13″ 2.... Read more
New 2016 15″ Touch Bar MacBook Pros in stock...
B&H Photo has new 2016 Apple 15″ Touch Bar MacBook Pro models in stock today including free shipping plus NY sales tax only: - 15″ 2.7GHz Touch Bar MacBook Pro Space Gray: $2799 - 15″ 2.7GHz... Read more
DietSensor App Targeting Diabetes and Obesity...
DietSensor, Inc., a developer of smart food and nutrition applications designed to fight diabetes and obesity and help improve overall fitness, has announced the launch of its DietSensor app for... Read more
Holiday 2016 13-inch 2.0GHz MacBook Pro sales...
B&H has the non-Touch Bar 13″ MacBook Pros in stock today for $50-$100 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.0GHz MacBook Pro Space Gray (MLL42LL/A): $1449 $... Read more
Holiday sale: Apple TVs for $51-$40 off MSRP,...
Best Buy has dropped their price on the 64GB Apple TV to $159.99 including free shipping. That’s $40 off MSRP. 32GB Apple TVs are on sale right now for $98 on Sams Club’s online store. That’s $51 off... Read more
12-inch Retina MacBooks, Apple refurbished, n...
Apple has restocked a full line of Certified Refurbished 2016 12″ Retina MacBooks, now available for $200-$260 off MSRP. Refurbished 2015 models are available starting at $929. Apple will include a... Read more
Holiday sale: 12-inch Retina MacBook for $100...
B&H has 12″ Retina MacBooks on sale for $100 off MSRP as part of their Holiday sale. Shipping is free, and B&H charges NY sales tax only: - 12″ 1.1GHz Space Gray Retina MacBook: $1199 $100... Read more

Jobs Board

Integration Technician, *Apple* - Zones, In...
…at Zones and for our customers each day. Position Overview The Apple Integration Technician will be responsible for performing customer specific configuration Read more
*Apple* Brand Ambassador (Macy's) - The...
…(T-ROC), is proud of its unprecedented relationship with our partner and client, APPLE ,in bringing amazing" APPLE ADVOCATES"to "non" Apple store locations. Read more
*Apple* Retail - Multiple Positions- Trumbul...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
US- *Apple* Store Leader Program - Apple (Un...
…Summary Learn and grow as you explore the art of leadership at the Apple Store. You'll master our retail business inside and out through training, hands-on Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.