Oct 00 QTToolkit
Volume Number: 16 (2000)
Issue Number: 10
Column Tag: QuickTime Toolkit
Somewhere I'll Find You
by Tim Monroe
Working With Data References and Data Handlers
Introduction
In previous QuickTime Toolkit articles, we've learned that a movie is composed of tracks and that each track is associated with one specific kind of media data. During movie playback, QuickTime uses a media handler (a component of type MediaHandlerType) to interpret the media data and present it to the user in the appropriate manner. For example, if the media data for some particular track consists of compressed video data, the video media handler calls the decompressor specified in the image description and then draws the decompressed frames in the appropriate location. A media handler, however, does not typically concern itself with reading the media data from the movie file (or from wherever else the media data is stored). Instead, the media handler gets that data from a data handler (a component of type DataHandlerType), which is responsible for reading and writing a media's data. In other words, a data handler provides data input and output services for a media handler and for other parts of QuickTime.
We identify a source of media data to a data handler by providing a data reference. As we'll see shortly, QuickTime currently includes four standard data handlers. Each data handler works with one specific sort of data reference. For instance, the URL data handler expects data references that are handles to URLs. That is to say, a URL data reference is a handle to a block of data that contains a NULL-terminated string of characters. The other data handlers expect their references in other forms.
A typical movie file contains data references to its media data in a data information atom, which is nested deep inside each track atom in the movie atom. In the previous article ("The Atomic Café" in MacTech, August 2000), we saw that a shortcut movie file contains a data reference atom that identifies an entire movie file. Data references can in fact pick out any kind of data, not just media data. In general, if we want to tell QuickTime where to find some data or where to put some data, we'll use a data reference to do so.
In this article, we're going to see how to work with each of the four standard data handlers. At the very simplest level, this involves nothing more than creating an appropriate data reference and putting that reference into a file (as we did last time) or passing it to a Movie Toolbox function like NewMovieFromDataRef. So we'll begin by learning how to create data references and call a few of the "FromDataRef" functions. Then we'll take a look at some of the functions that we can use to work with data handlers directly. These are fairly low-level functions that we won't need to use very often. Here, for fun, we'll see how to use them to write a general-purpose asynchronous file-transfer utility that relies solely on QuickTime APIs. Finally, toward the end of this article, we'll take a brief look at data reference extensions, which are blocks of additional data that we can associate with a data reference to assist QuickTime in working with the data picked out by the data reference. As we'll see, data reference extensions can be especially useful to graphics importers, to help them avoid having to take the time to validate some block of image data.
Our sample application this month is called QTDataRef; it illustrates how to work with data references and data handlers. Figure 1 shows the Test menu from QTDataRef.
Figure 1. The Test menu of QTDataRef.
Data Handler Overview
When QuickTime was first released, it included only one data handler, the file data handler, which can read and write data stored in files on a local file system. QuickTime version 2.0 introduced the handle data handler, which allows the Movie Toolbox to work with data stored in memory rather than in a file. QuickTime version 2.5 added the resource data handler, which can read data stored in a file's resource fork. More recently, QuickTime 3.0 added the URL data handler to support reading data from locations specified using uniform resource locators (URLs).
All data handlers are components of type DataHandlerType. Data handlers are distinguished from one another by their component subtypes. The file Movies.h defines constants for three of the four data handler subtypes:
enum {
HandleDataHandlerSubType = FOUR_CHAR_CODE('hndl'),
ResourceDataHandlerSubType = FOUR_CHAR_CODE('rsrc'),
URLDataHandlerSubType = FOUR_CHAR_CODE('url ')
};
And the file Aliases.h defines the constant used for the file data handler:
enum {
rAliasType = FOUR_CHAR_CODE('alis')
};
QuickTime also includes several other data handlers that it uses for its own private purposes and for which there is currently no public API. We won't consider these private data handlers in this article, but you should at least know that they exist (so that, for example, you aren't surprised if you iterate through all components of type DataHandlerType and find more than four of them).
A data reference is a handle to a block of memory that uniquely identifies the location of some media data for a QuickTime movie or some other data that QuickTime can manage. QuickTime currently provides support for four types of data references, one for each of the available data handlers. To let the cat out of the bag:
- A file data reference is a handle to an alias record that specifies a file on a local storage volume (or on a remote storage volume mounted on the local machine). That is to say, a file data reference is an alias handle. Because the file data handler is of subtype rAliasType and uses alias handles as its data references, it is often called the alias data handler. In addition, because it originally handled files on the Macintosh hierarchical file system (HFS), it is also sometimes called the HFS data handler.
- A handle data reference is a handle to a 4-byte block of memory that holds a handle to some other block of data. That is to say, a handle data reference is a handle to a handle.
- A resource data reference is a handle to an alias record to which two pieces of information have been appended, a resource type and a resource ID. The target data is found in the resource fork of the file specified by that alias record, in the resource of the specified type and ID.
- A URL data reference is a handle to a NULL-terminated string of characters that comprise the URL. The string of characters should conform to the relevant IETF specifications; in particular, some non-alphanumeric characters may need to be encoded using the hexadecimal equivalents of their ASCII codes (for instance, the space character should be encoded as "%20").
In the following four sections, we'll investigate these four types of data references in more detail. Before we begin, however, let's introduce a bit of terminology that will be useful throughout this article. Let's call the block of memory to which a data reference is a handle the referring data. And let's call the data picked out by the data reference the target data (or just the target) of the data reference. So, for example, the referring data of a URL data reference is the string of characters, and the target data of a URL data reference is the data in the file picked out by that URL.
The File Data Handler
We can use the file data handler to open movies that are specified using a file data reference, which is an alias handle. Listing 1 shows how to create a file data reference for a given file.
Listing 1: Creating a file data reference
QTDR_MakeFileDataRef
Handle QTDR_MakeFileDataRef (FSSpecPtr theFile)
{
Handle myDataRef = NULL;
QTNewAlias(theFile, (AliasHandle *)&myDataRef, true);
return(myDataRef);
}
QTDR_MakeFileDataRef consists mainly of a call to the Movie Toolbox function QTNewAlias, which returns, in the location specified by the second parameter, an alias handle for the file specified by the first parameter. The third parameter is a Boolean value that indicates whether to create a minimal or a full alias record. (A minimal alias record contains only the minimum information needed to find a file; it's generally much faster to find the target of a minimal alias record, so that's what we'll use here.)
We could also have used the Alias Manager functions NewAlias (for a full alias record) or NewAliasMinimal (for a minimal alias record) to create the file data reference. The principal advantage of using QTNewAlias is that it returns an alias handle even if the file specified by theFile doesn't yet exist. Both NewAlias and NewAliasMinimal return an error (fnfErr) - and do not create an alias record - if the specified file does not exist. As we'll see below, we sometimes want to create data references for files that we haven't yet created. (The Alias Manager does provide the NewAliasMinimalFromFullPath function that can create alias records for files that don't exist, but we'd rather not have to deal with full pathnames just to do that.)
Opening a Movie File
Let's consider a few examples of using file data references. First, we can pass a file data reference to the NewMovieFromDataRef function, to achieve exactly the same effect as calling the OpenMovieFile and NewMovieFromFile functions. Listing 2 defines the function QTDR_GetMovieFromFile, which creates a file data reference and then retrieves the movie in the specified movie file.
Listing 2: Opening a file specified by a file data reference
QTDR_GetMovieFromFile
Movie QTDR_GetMovieFromFile (FSSpecPtr theFile)
{
Movie myMovie = NULL;
Handle myDataRef = NULL;
myDataRef = QTDR_MakeFileDataRef(theFile);
if (myDataRef != NULL) {
NewMovieFromDataRef(&myMovie, newMovieActive, NULL,
myDataRef, rAliasType);
DisposeHandle(myDataRef);
}
return(myMovie);
}
The last two parameters to the NewMovieFromDataRef function specify, respectively, the data reference and the data reference type. Since we are passing it a file data reference, we set the data reference type to rAliasType. Note that we call DisposeHandle to dispose of the data reference once we are finished using it. By the time NewMovieFromDataRef returns, the Movie Toolbox will have made a copy of the data reference, if necessary.
Creating a Reference Movie File
Now let's turn to a more interesting example of using file data references. We learned in an earlier article ("Making Movies" in MacTech, June 2000) that a movie file might not contain the media data referenced by the movie's tracks; instead, that media data might be contained in some other file. A file that contains some media data is a media file, and the file that contains the movie atom is the movie file. (For simplicity, let's assume that our movies have only one track, a video track.) When the movie file and the media file are different files, the movie file is a reference movie file (because it refers to its media data and does not contain it). When the movie file and the media file are the same file, the movie file is a self-contained movie file. While generally we prefer to create self-contained movie files, it's useful to know how to create a reference movie file. (Reference movie files can be useful, for instance, if we want to share some media data among several QuickTime movies.)
One way to do this is to pass a file data reference to the NewTrackMedia function. Recall that NewTrackMedia is declared essentially like this:
Media NewTrackMedia ( Track theTrack,
OSType mediaType,
TimeScale timeScale,
Handle dataRef,
OSType dataRefType);
The fourth and fifth parameters specify the data reference and the data reference type of the file (or other container) that is to hold the media data for the specified track. Previously, whenever we called NewTrackMedia, we passed NULL and 0 in those parameters, to indicate that the media file should be the same as the movie file. Now we'll create a reference movie file simply by specifying a media file that is different from the movie file.
We're going to define a function QTDR_CreateReferenceCopy that we can use to create a copy of an existing movie that contains the first video track in the original movie. The copy's movie atom will be contained in one file (theDstMovieFile) and its media data will be contained in some other file (theDstMediaFile). QTDR_CreateReferenceCopy has this prototype:
OSErr QTDR_CreateReferenceCopy (Movie theSrcMovie,
FSSpecPtr theDstMovieFile, FSSpecPtr theDstMediaFile);
The definition of QTDR_CreateReferenceCopy will look vaguely familiar, at least if you recall the sequence of movie-making functions that we encountered in the QTMM_CreateVideoMovie function (in the "Making Movies" article). It uses CreateMovieFile, NewMovieTrack, NewTrackMedia, BeginMediaEdits, EndMediaEdits, and AddMovieResource in the standard ways. There are only two important differences between QTDR_CreateReferenceCopy and QTMM_CreateVideoMovie. First, we're going to obtain the media data for the copy from the first video track in the existing movie; to do this, we'll call GetMovieIndTrackType and GetTrackMedia to get the media from the source movie:
mySrcTrack = GetMovieIndTrackType(theSrcMovie, 1,
VideoMediaType, movieTrackMediaType);
mySrcMedia = GetTrackMedia(mySrcTrack);
Then we'll call GetTrackDimensions and GetMediaHandlerDescription to obtain some information about the source track and its media:
GetTrackDimensions(mySrcTrack, &myWidth, &myHeight);
GetMediaHandlerDescription(mySrcMedia, &myType, 0, 0);
When we create the copy movie, we'll need this information to be able to specify the size of the new track and the type of the track's media.
The second main difference between QTDR_CreateReferenceCopy and QTMM_CreateVideoMovie concerns the way we add media data to the new movie. Previously, we called an application-defined function to create the media samples and add them to the track's media (using AddMediaSample). Now, however, we've already got the media data for the new file - it's just the media data contained in the first video track of the source movie file. So, instead, we can use the Movie Toolbox function InsertTrackSegment to copy the media data from the source track to the new track, like this:
myErr = InsertTrackSegment(mySrcTrack, myDstTrack, 0,
GetTrackDuration(mySrcTrack),0);
Here, InsertTrackSegment copies the entire source track (that is, starting at time 0 and extending for the duration GetTrackDuration(mySrcTrack)) and inserts it into the destination track starting at time 0. The result is that the new media file will contain a copy of the source track's media data.
There is one final detail we need to attend to. In order for the destination track to have the same visual characteristics as the source track, we also need to copy the source track matrix, clipping region, and graphics mode (among other things) into the destination track. Similarly, if the source track were a sound track, we'd need to copy its volume, sound balance, and other sound characteristics into the destination track. We could copy the visual settings by calling GetTrackMatrix and SetTrackMatrix, GetTrackClipRgn and SetTrackClipRgn, and so forth. Better yet, the Movie Toolbox provides the CopyTrackSettings function that copies all these settings in one fell swoop:
CopyTrackSettings(mySrcTrack, myDstTrack);
Listing 3 shows our complete definition of QTDR_CreateReferenceCopy.
Listing 3: Creating a reference movie file
QTDR_CreateReferenceCopy
OSErr QTDR_CreateReferenceCopy (Movie theSrcMovie,
FSSpecPtr theDstMovieFile, FSSpecPtr theDstMediaFile)
{
Track mySrcTrack = NULL;
Media mySrcMedia = NULL;
Movie myDstMovie = NULL;
Track myDstTrack = NULL;
Media myDstMedia = NULL;
Handle myMediaRef = NULL;
Fixed myWidth, myHeight;
OSType myType;
long myFlags = createMovieFileDeleteCurFile |
createMovieFileDontCreateResFile;
short myResRefNum = 0;
short myResID = movieInDataForkResID;
OSErr myErr = paramErr;
// get the first video track and media in the source movie
mySrcTrack = GetMovieIndTrackType(theSrcMovie, 1,
VideoMediaType, movieTrackMediaType);
if (mySrcTrack == NULL)
goto bail;
mySrcMedia = GetTrackMedia(mySrcTrack);
if (mySrcMedia == NULL)
goto bail;
// get some information about the source track and media
GetTrackDimensions(mySrcTrack, &myWidth, &myHeight);
GetMediaHandlerDescription(mySrcMedia, &myType, 0, 0);
// create a file data reference for the new media file
myMediaRef = QTDR_MakeFileDataRef(theDstMediaFile);
if (myMediaRef == NULL)
goto bail;
// create a file for the destination movie data and create an empty movie
myErr = FSpCreate(theDstMediaFile, sigMoviePlayer,
MovieFileType, 0);
if (myErr != noErr)
goto bail;
// create a file for the destination movie atom
myErr = CreateMovieFile(theDstMovieFile, sigMoviePlayer,
smCurrentScript, myFlags,
&myResRefNum, &myDstMovie);
if (myErr != noErr)
goto bail;
// assign the default progress proc to the destination movie
SetMovieProgressProc(myDstMovie, (MovieProgressUPP)-1, 0);
// create the destination movie track and media
myDstTrack = NewMovieTrack(myDstMovie, myWidth, myHeight,
kNoVolume);
myErr = GetMoviesError();
if (myErr != noErr)
goto bail;
myDstMedia = NewTrackMedia(myDstTrack, myType,
GetMediaTimeScale(mySrcMedia), myMediaRef, rAliasType);
myErr = GetMoviesError();
if (myErr != noErr)
goto bail;
// copy the entire source track into the destination track; this copies the track's media
// samples into the destination media file
myErr = BeginMediaEdits(myDstMedia);
if (myErr != noErr)
goto bail;
myErr = InsertTrackSegment(mySrcTrack, myDstTrack, 0,
GetTrackDuration(mySrcTrack), 0);
if (myErr != noErr)
goto bail;
CopyTrackSettings(mySrcTrack, myDstTrack);
myErr = EndMediaEdits(myDstMedia);
if (myErr != noErr)
goto bail;
// add the movie atom to the data fork of the movie file
myErr = AddMovieResource(myDstMovie, myResRefNum,
&myResID, NULL);
bail:
return(myErr);
}
We call the QTDR_MakeFileDataRef function to create a file data reference for the media file specified by the theDstMediaFile parameter and we pass that data reference to the NewTrackMedia function, as described earlier. By default, QTDataRef names the new movie file "untitled.mov" and the new media file "untitled.dat". When a user tries to open untitled.mov, the Movie Toolbox looks for untitled.dat to find the movie's media data. If it cannot find that file, the Movie Toolbox displays the dialog box shown in Figure 2.
Figure 2. The missing data dialog box.
One reason we prefer to create self-contained movie files is to ensure that the media data is always available, so the user won't ever have to see this dialog box.
Before we move on, it's worth mentioning that the Movie Toolbox provides the AddEmptyTrackToMovie function, which we could use in Listing 3 instead of NewMovieTrack, NewTrackMedia, and CopyTrackSettings, like this:
myErr = AddEmptyTrackToMovie(mySrcTrack, myDstMovie,
myMediaRef, rAliasType, &myDstTrack);
if (myErr != noErr)
goto bail;
myDstMedia = GetTrackMedia(myDstTrack);
AddEmptyTrackToMovie makes a copy of an existing track, either in the same movie or in a different movie; it doesn't actually copy the media data, but it does copy the track settings. The source code files accompanying this article provide an alternate version of the QTDR_CreateReferenceCopy function that uses AddEmptyTrackToMovie.
The Handle Data Handler
The handle data handler is used to read data from and write data to a location in memory. That location is specified using a Macintosh handle. A handle data reference is a handle to that handle. Listing 4 shows how to create a handle data reference.
Listing 4: Creating a handle data reference
QTDR_MakeHandleDataRef
Handle QTDR_MakeHandleDataRef (Handle theHandle)
{
Handle myDataRef = NULL;
myDataRef = NewHandleClear(sizeof(Handle));
if (myDataRef != NULL)
BlockMove(&theHandle, *myDataRef, sizeof(Handle));
return(myDataRef);
}
Here we allocate a relocatable block of memory that is the size of a handle (4 bytes) and then copy the handle passed to the function into that block of memory. The result is just what we want, a handle to the original handle. For an even simpler routine, we can replace the middle three lines of code by this one line:
PtrToHand(&theHandle, &myDataRef, sizeof(Handle));
The Memory Manager function PtrToHand allocates a new relocatable block of memory of the specified size and then copies the data specified by its first parameter into that block. In a little while, we'll encounter PtrAndHand, a cousin of PtrToHand that appends the data in a pointer to an existing handle.
The handle data handler is useful for many tasks. It's useful for playing movies that can fit entirely into RAM, and it's useful for handling images when the image data resides in memory and not in a file. In the latter case, we can just create a handle data reference from the handle that holds the image data and pass it to GetGraphicsImporterForDataRef to get a graphics importer than can manage the image data. Similarly, to play a movie stored in RAM, we can create a handle data reference and pass it to NewMovieFromDataRef. In this case, the block of memory referenced by the handle must contain the movie data and the movie atom.
To create a movie whose data is stored in RAM, we could use NewTrackMedia as illustrated in the previous section, passing it a handle data reference instead of a file data reference. Or, even more simply, we can exploit the ability of the FlattenMovieData function to flatten a movie into a location specified by a data reference instead of by a file specification record. The third parameter to FlattenMovieData is declared as a pointer to an FSSpec record, but we can set the flag flattenFSSpecPtrIsDataRefRecordPtr to instruct it to interpret that parameter as a pointer to a data reference record, defined by the DataReferenceRecord data type:
struct DataReferenceRecord {
OSType dataRefType;
Handle dataRef;
};
So all we need to do is create a handle data reference, fill in a data reference record with the appropriate information, and then pass the address of that record to FlattenMovieData in place of the FSSpecPtr. Listing 5 shows an excerpt from our QTApp_HandleMenu function that handles the "Create RAM Copy and Play" menu item.
Listing 5: Creating a movie in RAM
QTApp_HandleMenu
case IDM_CREATE_RAM_COPY_AND_PLAY:
if (myMovie != NULL) {
Movie myNewMovie = NULL;
Handle myDataRef = NULL;
Handle myHandle = NULL;
DataReferenceRecord myDataRefRecord;
myHandle = NewHandleClear(0);
if (myHandle == NULL)
goto bail;
myDataRef = QTDR_MakeHandleDataRef(myHandle);
if (myDataRef == NULL)
goto bail;
myDataRefRecord.dataRefType = HandleDataHandlerSubType;
myDataRefRecord.dataRef = myDataRef;
myNewMovie = FlattenMovieData( myMovie,
flattenFSSpecPtrIsDataRefRecordPtr,
(FSSpecPtr)&myDataRefRecord,
sigMoviePlayer,
smSystemScript,
0L);
if (myNewMovie != NULL) {
QTDR_PlayMovieFromRAM(myNewMovie);
DisposeMovie(myNewMovie);
}
bail:
if (myHandle != NULL)
DisposeHandle(myHandle);
if (myDataRef != NULL)
DisposeHandle(myDataRef);
}
In this case, once we've created the movie in RAM, we call the function QTDR_PlayMovieFromRAM to play the movie in a window on the screen. Then we dispose of the new movie and the handle that contains the movie data, along with the handle data reference.
The Resource Data Handler
The resource data handler was introduced in QuickTime version 2.5 to allow data to be read from a resource in a file's resource fork. Listing 6 shows how to create a resource data reference.
Listing 6: Creating a resource data reference
QTDR_MakeResourceDataRef
Handle QTDR_MakeResourceDataRef (FSSpecPtr theFile,
OSType theResType, SInt16 theResID)
{
Handle myDataRef = NULL;
OSType myResType;
SInt16 myResID;
OSErr myErr = noErr;
myDataRef = QTDR_MakeFileDataRef(theFile);
if (myDataRef == NULL)
goto bail;
// append the resource type and ID to the data reference
myResType = EndianU32_NtoB(theResType);
myResID = EndianS16_NtoB(theResID);
myErr = PtrAndHand(&myResType, myDataRef,
sizeof(myResType));
if (myErr == noErr)
myErr = PtrAndHand(&myResID, myDataRef, sizeof(myResID));
bail:
if (myErr != noErr) {
if (myDataRef != NULL)
DisposeHandle(myDataRef);
myDataRef = NULL;
}
return(myDataRef);
}
In QTDR_MakeResourceDataRef, we begin by creating a file data reference to the specified file. Then we call PtrAndHand to append the resource type to the referring data of that data reference, and then we call PtrAndHand once again to append the resource ID. Note that the resource type and ID must be converted to big-endian format before being appended to the referring data.
We won't spend too much time considering the resource data handler, largely because we prefer not to keep our movie or image data in resource files (so that it's easily transportable to non-Macintosh operating systems). To show you that it works on Macintosh resource files, I've added the menu item "Open Resource Movie..." to the QTDataRef sample application; I've also included the file ResBased.mov, which contains a movie stored in a resource.
There is another reason why the resource data handler is of limited interest to us: it can read data from resource files but cannot write data to them. To see this, let's suppose that myDataRef is a resource data reference. Then we can call the GetDataHandler function like this:
myComponent = GetDataHandler(myDataRef,
ResourceDataHandlerSubType, kDataHCanWrite)
GetDataHandler returns the best data handler for the specified data reference and data handler type that provides the services specified by the third parameter. In this case, we're asking for a resource data handler that can write data. After this line of code completes, however, the value returned in myComponent will be NULL, indicating that there is no such resource data handler.
The last reason we're going to ignore the resource data handler in the future is perhaps the most obvious: if we do want to access some movie or image data stored in a resource, we can simply load that data into memory (by calling GetResource) and then use the handle data handler. So the resource data handler is largely redundant.
The URL Data Handler
We can use QuickTime to open movies and images that are specified using URLs. A URL is the address of some resource on the Internet or on a local disk. Typically, a URL is a string of characters like "http://www.apple.com". The initial portion of the URL, which precedes the first colon (:), is the URL's scheme or protocol. In this case, the scheme is "http", for the hypertext transfer protocol. QuickTime provides data handlers that can work with URLs whose scheme is "http", "ftp" (file transfer protocol), "rtsp" (real-time streaming protocol), or "file" (picking out a file on the local file system). Note that there really isn't just one URL data handler; there are in fact several data handlers that support URLs. For instance, the file data handler will ultimately be used to handle URLs whose scheme is "file". Still, we access these data handlers using URL data references.
As we've already seen, a data reference for the URL data handler is a handle to the NULL-terminated string of characters that comprise a URL. So it's relatively easy to create a URL data reference: just allocate a relocatable block of the appropriate size and copy the URL into that block. Listing 7 defines the function QTDR_MakeURLDataRef, which creates a URL data reference for a given URL.
Listing 7: Creating a URL data reference
QTDR_MakeURLDataRef
Handle QTDR_MakeURLDataRef (char *theURL)
{
Handle myDataRef = NULL;
Size mySize = 0;
// get the size of the URL, plus the terminating null byte
mySize = (Size)strlen(theURL) + 1;
if (mySize == 1)
goto bail;
// allocate a new handle and copy the URL into the handle
myDataRef = NewHandleClear(mySize);
if (myDataRef != NULL)
BlockMove(theURL, *myDataRef, mySize);
bail:
return(myDataRef);
}
If a URL picks out a movie file, we can call QTDR_MakeURLDataRef to create a URL data reference for it and then pass that data reference to the NewMovieFromDataRef function to open the specified movie file. Listing 8 defines the function QTURL_NewMovieFromURL that takes a URL and returns a movie identifier for the movie addressed by that URL. QTDR_GetMovieFromURL is just like QTDR_GetMovieFromFile, except that it uses URL data references.
Listing 8: Opening a movie specified by a URL
QTDR_GetMovieFromURL
Movie QTDR_GetMovieFromURL (char *theURL)
{
Movie myMovie = NULL;
Handle myDataRef = NULL;
myDataRef = QTDR_MakeURLDataRef(theURL);
if (myDataRef != NULL) {
NewMovieFromDataRef(&myMovie, newMovieActive, NULL,
myDataRef, URLDataHandlerSubType);
DisposeHandle(myDataRef);
}
return(myMovie);
}
The QTDR_GetMovieFromURL function is a large part of what we need to handle the "Open URL..." menu item (supported both by QuickTime Player and by our sample application QTDataRef). But it isn't quite all of what we need. First, of course, when the user selects "Open URL...", we need to obtain a URL from him or her. QuickTime Player displays the dialog box shown in Figure 3, which contains space to type in a URL, as well as a pop-up menu containing a list of recently opened URLs.
Figure 3. The Open URL dialog box of QuickTime Player.
For the moment, we'll be content to display the dialog box shown in Figure 4, which does not provide the pop-up menu. (Providing a pop-up menu or some other kind of list for recently accessed URLs is a good idea, however, since URLs are often longer than 255 characters, which is the most our current code will support.)
Figure 4. The Open URL dialog box of QTDataRef.
The code to display and manage this dialog box is contained in the function QTDR_GetURLFromUser, which we won't consider in detail here; it's really quite similar to the function QTInfo_EditAnnotation, which we considered in a previous article (see "The Informant" in MacTech, July 2000). It's also useful to get the basename of the URL so that our movie window has a title (the basename is the portion of the URL following the rightmost URL separator). See the file QTDataRef.c for the function QTDR_GetURLBasename, which we use to find that basename.
File Transfer
We've learned that QuickTime supplies a data handler that can read data from remote locations specified by a URL and a data handler that can write data into files on the local file system. We can use both of these data handlers at the same time to read data stored remotely on the Internet and write it into a local file. This gives us, in effect, a network file-transfer capability that operates using only QuickTime APIs. Moreover, QuickTime supports calling these data handlers asynchronously, so that our application can perform other processing while the file transfer is taking place. In this section we'll see how to do all this.
Our ulterior motive here is to get a glimpse of the lower-level functions supported by data handlers. Hitherto, we've pretty much always used data handlers indirectly, by passing data references to Movie Toolbox or ICM functions, or simply by embedding data references in files. Now we want to see how to work with data handlers directly.
In overview, our file transfer will proceed like this: get an instance of the URL data handler and configure it to copy data from a remote file into a memory buffer. Then get an instance of the file data handler and configure it to copy data from that buffer into a local file. Then keep copying data into and out of the buffer until the entire remote file has been copied. To make it easy for this to work asynchronously, our file-transfer code is going to rely on a few global variables, declared like this:
Ptr gDataBuffer = NULL;
ComponentInstance gDataReader = NULL;
ComponentInstance gDataWriter = NULL;
DataHCompletionUPP gReadDataHCompletionUPP = NULL;
DataHCompletionUPP gWriteDataHCompletionUPP = NULL;
long gBytesToTransfer = 0L;
long gBytesTransferred = 0L;
Boolean gDoneTransferring = false;
The variables gDataReader and gDataWriter are the component instances of the URL data handler that reads data and the file data handler that writes data. gDataReader puts data into the buffer gDataBuffer, whence gDataWriter gets the data that it writes into the local file. The asynchronous nature of the file transfer is achieved largely by two completion functions, to which gReadDataHCompletionUPP and gWriteDataHCompletionUPP are universal procedure pointers. Finally, the global variables gBytesToTransfer, gBytesTransferred, and gDoneTransferring keep track of information while the transfer is underway.
Creating the Local File
The first thing we need to do, of course, is create the local file into which the remote file data is to be copied. Let's suppose that theFile is a file system specification for the local file (which we perhaps got from the user by calling our framework function QTFrame_PutFile). We should first delete that file, if it already exists, by calling FSpDelete:
FSpDelete(theFile);
If the specified file doesn't exist, FSpDelete will return the error code fnfErr, which we can safely ignore. Then we can call FSpCreate to create a new empty file, like this:
myErr = FSpCreate(theFile, kTransFileCreator, kTransFileType,
smSystemScript);
(Note that we are hard-coding the file's type and creator codes here; I'll leave it as an exercise for you to concoct a more intelligent way to set these values.)
Opening and Configuring Data Handlers
Before we can work with a data handler, we need to open an instance of the appropriate data handler component. We saw earlier that we can find a particular data handler by calling GetDataHandler, passing it a data reference, a type, and a set of flags that indicate the data-handling services we need it to provide. If theURL is a C string that specifies the remote file and theFile is a pointer to a file system specification for the local file into which the remote file data is to be copied, we can create the required data references like this:
Handle myReaderRef = QTDR_MakeURLDataRef(theURL);
Handle myWriterRef = QTDR_MakeFileDataRef(theFile);
And we can open the appropriate data handlers with this code:
gDataReader = OpenComponent(GetDataHandler(myReaderRef,
URLDataHandlerSubType, kDataHCanRead));
gDataWriter = OpenComponent(GetDataHandler(myWriterRef,
rAliasType, kDataHCanWrite));
As you can see, we're asking for a URL data handler that can read data and a file data handler that can write data. Once we've got our data handler instances, we need to configure them by telling them which data references they are going to work with. We also need to have them open a connection to the targets of those data references. We can set the data references by calling DataHSetDataRef, like this:
myErr = DataHSetDataRef(gDataReader, myReaderRef);
myErr = DataHSetDataRef(gDataWriter, myWriterRef);
Then we can open the appropriate connections by calling DataHOpenForRead and DataHOpenForWrite:
myErr = DataHOpenForRead(gDataReader);
myErr = DataHOpenForWrite(gDataWriter);
Transferring Data Synchronously
It might seem like we haven't done much work yet, but (believe it or not) we are ready to begin transferring data from the remote file into the local file. At this point, we have a choice: we can transfer the data synchronously (that is, waiting for all the data to arrive before continuing with any other work) or asynchronously. Ultimately we want to have our transfers proceed asynchronously, but let's take a moment to see how to do them synchronously.
To perform a synchronous transfer, we can call DataHGetData and DataHPutData. These functions require the intermediate buffer to be accessed by a handle (not by a pointer, as with asynchronous transfers). We can call DataHGetFileSize to see how many bytes we need to transfer and then allocate a handle of that size:
myErr = DataHGetFileSize(gDataReader, &gBytesToTransfer);
Handle gDataBuffer = NewHandleClear(gBytesToTransfer);
If gDataBuffer is successfully created, we can then transfer the remote file to the local file with these four lines of code:
DataHGetData(gDataReader, gDataBuffer, 0L, 0L,
gBytesToTransfer);
DataHCloseForRead(gDataReader);
DataHPutData(gDataWriter, gDataBuffer, 0L, NULL,
gBytesToTransfer);
DataHCloseForWrite(gDataWriter);
All we need to do is fetch the remote data into the intermediate buffer, close the connection to the remote file, write the data into the local file, and then close the connection to the local file. This code assumes, of course, that we can allocate a buffer large enough to hold the entire remote file. With very little work, you could generalize this code to work with files too large to fit entirely into the available RAM. (We'll do this below, when transferring asynchronously.)
So there you have it: with barely a dozen lines of code, we've managed to copy a file located somewhere out on the Internet into a local file. The downside here is that the transfer occurs synchronously and is limited to files that can fit entirely into RAM. Let's remove these two limitations.
Transferring Data Asynchronously
We can remove the limitation on file size rather simply, by allocating an intermediate buffer of a set size and then reading and writing chunks of data of that size. We'll allocate a buffer that is 10 kilobytes, like this:
#define kDataBufferSize 1024*10
gDataBuffer = NewPtrClear(kDataBufferSize);
So all we need to do is read a chunk of data of size kDataBufferSize, write that chunk of data, read another chunk of data, write that chunk, and so on until the entire remote file is transferred. Of course, eventually we'll probably end up with a chunk that's smaller than kDataBufferSize, so we'll need to keep track of how much of the file remains to be transferred and adjust our reads accordingly.
We can achieve an asynchronous transfer by using the functions DataHReadAsync and DataHWrite to read and write data. These functions queue up a read or write request to the data handler and then return immediately, without waiting for the request to be serviced. The request will be serviced at some later time, when we call DataHTask to task the data handler. Once the request is serviced, the data handler executes a data handler completion function that we specify when we call DataHReadAsync or DataHWrite.
A data handler completion function takes three parameters: a pointer to the buffer into which data was written or from which data was read, an application-specific reference constant, and an error code. We'll ignore the error code and use the reference constant to hold the number of bytes just written or read by the data handler.
We begin the file transfer by reading some data from the remote file. We could simply call DataHReadAsync directly, passing it the appropriate parameters. Instead, however, we'll be a bit clever here and call our write completion function QTDR_WriteDataCompletionProc, passing it parameters that indicate that we've just successfully finished writing 0 bytes:
QTDR_WriteDataCompletionProc(gDataBuffer, 0L, noErr);
The reason for this is simple: our QTDR_WriteDataCompletionProc function (defined in Listing 9) contains code for figuring out how many bytes to request and then for issuing that request.
Listing 9: Responding to a write operation
QTDR_WriteDataCompletionProc
PASCAL_RTN void QTDR_WriteDataCompletionProc
(Ptr theRequest, long theRefCon, OSErr theErr)
{
#pragma unused(theErr)
long myNumBytesToRead;
wide myWide;
// increment our tally of the number of bytes written so far
gBytesTransferred += theRefCon;
if (gBytesTransferred < gBytesToTransfer) {
// there is still data to read and write, so schedule a read operation
// determine how big a chunk to read
if (gBytesToTransfer - gBytesTransferred >
kDataBufferSize)
myNumBytesToRead = kDataBufferSize;
else
myNumBytesToRead = gBytesToTransfer -
gBytesTransferred;
myWide.lo = gBytesTransferred; // read from the current offset
myWide.hi = 0;
// schedule a read operation
DataHReadAsync(gDataReader,
theRequest, // the data buffer
myNumBytesToRead,
&myWide,
gReadDataHCompletionUPP,
myNumBytesToRead);
} else {
// we've transferred all the data; set a flag to tell us to close down the data handlers
gDoneTransferring = true;
}
}
As you can see, we first update the global variable gBytesTransferred that keeps track of the number of bytes already transferred. Then we figure out how many bytes remain to be read from the remote file; we read that number of bytes, if it's less than the size of our intermediate buffer, or else we read an entire buffer of data. Finally, we call DataHReadAsync to schedule a read operation. Notice that we specify the gReadDataHCompletionUPP as the read completion function.
Once some data is read into the local buffer, our read completion function will be executed. Listing 10 shows our read completion function. It's even simpler than the write completion function; all it does is schedule a write operation to copy the data from the buffer into the local file.
Listing 10: Responding to a read operation
QTDR_ReadDataCompletionProc
PASCAL_RTN void QTDR_ReadDataCompletionProc
(Ptr theRequest, long theRefCon, OSErr theErr)
{
#pragma unused(theErr)
// we just finished reading some data, so schedule a write operation
DataHWrite(gDataWriter,
theRequest, // the data buffer
gBytesTransferred, // write from the current offset
theRefCon, // the number of bytes to write
gWriteDataHCompletionUPP,
theRefCon);
}
In this case, theRefCon contains the number of bytes just read from the remote file, which is the number of bytes that should be written to the local file. Notice that now we specify gWriteDataHCompletionUPP as the write completion function. The read and write completion functions keep scheduling write and read requests, specifying each other as the completion function for those requests. So we keep successively reading and writing data, until the entire file is transferred.
Tasking the Data Handlers
There is one final step needed to make this all work. Namely, we need to give the data handlers some processor time to do their work. We do this by periodically calling DataHTask. On the Macintosh, we can insert calls to DataHTask into the application function QTApp_HandleEvent, which is called by our application framework every trip through the main event loop. Listing 11 shows the definition of QTApp_HandleEvent in QTDataRef.
Listing 11: Tasking the data handlers
QTApp_HandleEvent
Boolean QTApp_HandleEvent (EventRecord *theEvent)
{
#pragma unused(theEvent)
// if we're done, close down the data handlers
if (gDoneTransferring)
QTDR_CloseDownHandlers();
// give the data handlers some time, if they are still active
if (gDataReader != NULL)
DataHTask(gDataReader);
if (gDataWriter != NULL)
DataHTask(gDataWriter);
return(false);
}
If the file is done being transferred, then QTApp_HandleEvent calls the function QTDR_CloseDownHandlers (defined later) to close things down. Otherwise, it calls DataHTask on both the URL data handler and the file data handler. At most one of them will have some work to do, but it doesn't hurt to task both of them.
Our Windows framework does not however call QTApp_HandleEvent periodically, so we'll have to do that ourselves. Probably the easiest way is to install a timer task, like this:
gTimerID = SetTimer(NULL, 0, kQTDR_TimeOut,
(TIMERPROC)QTDR_TimerProc);
The timer callback function QTDR_TimerProc, defined in Listing 12, simply calls QTApp_HandleEvent.
Listing 12: Handling timer callbacks
QTDR_TimerProc
void CALLBACK QTDR_TimerProc (HWND theWnd, UINT theMessage,
UINT_PTR theID, DWORD theTime)
{
#pragma unused(theWnd, theMessage, theID, theTime)
QTApp_HandleEvent(NULL);
}
We remove the timer task once the data transfer is completed (see Listing 13 below).
It's important to know that a data handler may execute our requests to read and write data asynchronously or not, even if we specify completion functions. If the handler decides to operate synchronously, then it will not return immediately when we call DataHReadAsync or DataHWrite; instead, it will perform the requested operation and then return. We still need to call DataHTask, however, to give the data handlers an opportunity to execute their completion functions.
Finishing Up
When the write completion function QTDR_WriteDataCompletionProc determines that all the data has been read from the remote file and written into the local file, it sets the global variable gDoneTransferring to true. When QTApp_HandleEvent is called and gDoneTransferring is true, QTApp_HandleEvent calls QTDR_CloseDownHandlers, defined in Listing 13.
Listing 13: Closing down the data handlers
QTDR_CloseDownHandlers
void QTDR_CloseDownHandlers (void)
{
if (gDataReader != NULL) {
DataHCloseForRead(gDataReader);
CloseComponent(gDataReader);
gDataReader = NULL;
}
if (gDataWriter != NULL) {
DataHCloseForWrite(gDataWriter);
CloseComponent(gDataWriter);
gDataWriter = NULL;
}
// dispose of the data buffer
if (gDataBuffer != NULL)
DisposePtr(gDataBuffer);
// dispose of the routine descriptors
if (gReadDataHCompletionUPP != NULL)
DisposeDataHCompletionUPP(gReadDataHCompletionUPP);
if (gWriteDataHCompletionUPP != NULL)
DisposeDataHCompletionUPP(gWriteDataHCompletionUPP);
gDoneTransferring = false;
#if TARGET_OS_WIN32
// kill the timer that tasks the data handlers
KillTimer(NULL, gTimerID);
#endif
}
QTDR_CloseDownHandlers simply closes the connections to the local and remote files and then closes the component instances of the data handlers. On Windows, it also removes the timer task that was calling QTApp_HandleEvent periodically.
Listing 14 contains the complete definition of the QTDR_CopyRemoteFileToLocalFile function, which is called in response to the "Transfer Remote File..." menu item.
Listing 14: Copying a remote file into a local file
QTDR_CopyRemoteFileToLocalFile
OSErr QTDR_CopyRemoteFileToLocalFile
(char *theURL, FSSpecPtr theFile)
{
Handle myReaderRef = NULL; // data ref for the remote file
Handle myWriterRef = NULL; // data ref for the local file
ComponentResult myErr = badComponentType;
// delete the target local file, if it already exists;
// if it doesn't exist yet, we'll get an error (fnfErr), which we just ignore
FSpDelete(theFile);
// create the local file with the desired type and creator
myErr = FSpCreate(theFile, kTransFileCreator,
kTransFileType, smSystemScript);
if (myErr != noErr)
goto bail;
// create data references for the remote file and the local file
myReaderRef = QTDR_MakeURLDataRef(theURL);
if (myReaderRef == NULL)
goto bail;
myWriterRef = QTDR_MakeFileDataRef(theFile);
if (myWriterRef == NULL)
goto bail;
// find and open the URL and file data handlers
gDataReader = OpenComponent(GetDataHandler(myReaderRef,
URLDataHandlerSubType, kDataHCanRead));
if (gDataReader == NULL)
goto bail;
gDataWriter = OpenComponent(GetDataHandler(myWriterRef,
rAliasType, kDataHCanWrite));
if (gDataWriter == NULL)
goto bail;
// set the data reference for the URL data handler
myErr = DataHSetDataRef(gDataReader, myReaderRef);
if (myErr != noErr)
goto bail;
// set the data reference for the file data handler
myErr = DataHSetDataRef(gDataWriter, myWriterRef);
if (myErr != noErr)
goto bail;
// allocate a data buffer; the URL data handler copies data into this buffer,
// and the file data handler copies data out of it
gDataBuffer = NewPtrClear(kDataBufferSize);
myErr = MemError();
if (myErr != noErr)
goto bail;
// open a read-only path to the remote data reference
myErr = DataHOpenForRead(gDataReader);
if (myErr != noErr)
goto bail;
// get the size of the remote file
myErr = DataHGetFileSize(gDataReader, &gBytesToTransfer);
if (myErr != noErr)
goto bail;
// open a write-only path to the local data reference
myErr = DataHOpenForWrite(gDataWriter);
if (myErr != noErr)
goto bail;
// start reading and writing data
gDoneTransferring = false;
gBytesTransferred = 0L;
gReadDataHCompletionUPP =
NewDataHCompletionUPP(QTDR_ReadDataCompletionProc);
gWriteDataHCompletionUPP =
NewDataHCompletionUPP(QTDR_WriteDataCompletionProc);
// start retrieving the data; we do this by calling our own write completion routine,
// pretending that we've just successfully finished writing 0 bytes of data
QTDR_WriteDataCompletionProc(gDataBuffer, 0L, noErr);
bail:
// if we encountered any error, close the data handler components
if (myErr != noErr)
QTDR_CloseDownHandlers();
return((OSErr)myErr);
}
So we've managed to use QuickTime's data handlers to provide a general-purpose network file-transfer capability that operates asynchronously, allowing us to play movies or perform other operations while the transfer is underway. Of course, there are still some refinements we might add, such as alerting the user if he or she decides to quit the application while a file transfer is in progress. We'll leave these as exercises for the interested reader.
Notice that we call the function DataHGetFileSize to determine the size of the remote file (which is of course the number of bytes we need to transfer). DataHGetFileSize may need to read through the entire remote file to determine its size, which can sometimes slow things down (since we're calling it synchronously). Some data handlers (but not all) support the DataHGetFileSizeAsync function, which allows us to get this information asynchronously. You might try experimenting with DataHGetFileSizeAsync to see if it improves performance in your particular situation.
In that vein, you might be wondering: "sure, we can use QuickTime to transfer data across the net, but is it any good? What's the performance like?" My preliminary (and admittedly unscientific) tests show that our code is in fact very good. In a few sample FTP transfers, QTDataRef consistently transferred files at least as fast as the latest version of Anarchie, a popular shareware Internet file transfer application for the Mac. Moreover, with a movie playing continuously in the foreground, QTDataRef took only about 10% longer to transfer the file. (And don't forget that our code works on MacOS 8 and 9, Windows, and Mac OS X!)
Data Reference Extensions
Consider now this question: if we pass a handle data reference to the function GetGraphicsImporterForDataRef, how does it figure out which graphics importer to open and return to us? Recall (from "Quick on the Draw" in MacTech, April 2000) that when we pass a file specification record to GetGraphicsImporterForFile, it first inspects the Macintosh file type (on Mac OS) and then the filename extension of the specified file. If neither of these inspections reveals the type of image data in the file, GetGraphicsImporterForFile must then validate the file data (that is, look through the file data for clues to the image type). With a handle data reference, where there is no file type or filename extension, only the validation step is possible. Unfortunately, validation is time-consuming and, alas, not guaranteed to produce correct results.
QuickTime 3.0 provided a preliminary solution to this problem by allowing us to attach a filename to the referring data of a handle data reference. (Let's call this a filenaming extension.) That is to say, a handle data reference is a handle to a 4-byte handle that is optionally followed by a Pascal string containing a filename. Listing 15 defines the function QTDR_AddFilenamingExtension that attaches a filename to the referring data of a handle data reference.
Listing 15: Appending a filename to some referring data
QTDR_AddFilenamingExtension
OSErr QTDR_AddFilenamingExtension (Handle theDataRef,
StringPtr theFileName)
{
unsigned char myChar = 0;
OSErr myErr = noErr;
if (theFileName == NULL)
myErr = PtrAndHand(&myChar, theDataRef, sizeof(myChar));
else
myErr = PtrAndHand(theFileName, theDataRef,
theFileName[0] + 1);
return(myErr);
}
The filename can contain an extension that provides an indication of the kind of data in the data reference target. For instance, a filename of the form "myImage.bmp" indicates that the data consists of Windows bitmap data. For reasons that will become clear in a few moments, QTDR_AddFilenamingExtension looks to see whether theFileName parameter is NULL; if it is, QTDR_AddFilenamingExtension appends a single byte whose value is 0.
QuickTime 4.0 provides a more complete solution to this problem by allowing us to create data reference extensions for handle data references. A data reference extension is a block of data that is appended to the referring data, in pretty much the same way that we just appended a filename to that data. The main difference is that, unlike the filenaming extension, a data reference extension is packaged as an atom, with an explicit type. QuickTime currently supports four kinds of data reference extensions, defined by these constants:
enum {
kDataRefExtensionChokeSpeed = FOUR_CHAR_CODE('chok'),
kDataRefExtensionMIMEType = FOUR_CHAR_CODE('mime'),
kDataRefExtensionMacOSFileType = FOUR_CHAR_CODE('ftyp'),
kDataRefExtensionInitializationData
= FOUR_CHAR_CODE('data')
};
A data reference extension of type kDataRefExtensionChokeSpeed can be added to a URL data reference to specify a choke speed (which limits the data rate of a file streamed using HTTP streaming). The other three types can be added to a handle data reference to help identify the kind of data in the target of the data reference or to supply some initialization data to the data handler. If a data reference extension is present, then the filenaming extension must also be present. The filename can be 0-length, however, in which case the filenaming extension consists only of a single byte whose value is 0.
Listing 16 shows how to add a Macintosh file type as a data reference extension.
Listing 16: Appending a file type data reference extension
QTDR_AddMacOSFileTypeDataRefExtension
OSErr QTDR_AddMacOSFileTypeDataRefExtension
(Handle theDataRef, OSType theType)
{
unsigned long myAtomHeader[2];
OSType myType;
OSErr myErr = noErr;
myAtomHeader[0] = EndianU32_NtoB(
sizeof(myAtomHeader) + sizeof(theType));
myAtomHeader[1] = EndianU32_NtoB(
kDataRefExtensionMacOSFileType);
myType = EndianU32_NtoB(theType);
myErr = PtrAndHand(myAtomHeader, theDataRef,
sizeof(myAtomHeader));
if (myErr == noErr)
myErr = PtrAndHand(&myType, theDataRef,
sizeof(myType));
return(myErr);
}
This code simply calls PtrAndHand to append an atom header to the referring data and then calls PtrAndHand again to append the file type (suitably converted to big-endian format).
Another way to flag the type of data in a handle is by specifying a MIME type. MIME (for Multipurpose Internet Mail Extension) is a standard protocol for transmitting binary data across the Internet. A MIME type is a text string used in MIME transmissions to indicate the type of the data being transmitted. (For instance, the string "video/quicktime" is the MIME type of QuickTime movie files.) MIME types can also be used locally to indicate the type of a file or other collection of data. Movie importers and graphics importers will look for MIME type data reference extensions to help identify the type of data specified by a handle data reference. If you are building some movie or image data in memory, you can use the QTDR_AddMIMETypeDataRefExtension function, defined in Listing 17, to add a MIME type as a data reference extension. (Be sure to add a filenaming extension before adding a MIME type data reference extension.)
Listing 17: Appending a MIME type data reference extension
QTDR_AddMIMETypeDataRefExtension
OSErr QTDR_AddMIMETypeDataRefExtension
(Handle theDataRef, StringPtr theMIMEType)
{
unsigned long myAtomHeader[2];
OSErr myErr = noErr;
if (theMIMEType == NULL)
return(paramErr);
myAtomHeader[0] = EndianU32_NtoB(sizeof(myAtomHeader) +
theMIMEType[0] + 1);
myAtomHeader[1] = EndianU32_NtoB(
kDataRefExtensionMIMEType);
myErr = PtrAndHand(myAtomHeader, theDataRef, sizeof(myAtomHeader));
if (myErr == noErr)
myErr = PtrAndHand(theMIMEType, theDataRef,
theMIMEType[0] + 1);
return(myErr);
}
Conclusion
As we've seen, QuickTime uses data references to find the data that it's supposed to handle. Data references can be embedded in movie files or passed to Movie Toolbox and ICM functions. So whether we're building movies or operating on them, understanding data references is crucial to doing any real work with QuickTime. Here we've learned how to work with data references to create reference movie files, play movies from RAM, and open movies located remotely on the Internet. In the previous article, we also saw how to use data references to create shortcut movie files and embedded movies. In future articles, we'll work with data references as a normal part of our QuickTime programming. So it's good that we've taken time to learn how to create and work with them.
On the other hand, data handlers are normally transparent to applications. We can use them directly for certain special purposes, such as transferring remote files to the local machine. But normally the QuickTime APIs insulate us from having to work with them at all. Typically, we can accomplish what we need by handing the Movie Toolbox or ICM a data reference and letting it communicate with the appropriate data handler.
Credits and References
Thanks are due to Chris Flick and Scott Kuechle for reviewing this article and offering helpful comments, and to Peter Hoddie for providing some historical perspective.
The official Apple documentation on data handlers can be found at http://developer.apple.com/techpubs/quicktime/qtdevdocs/RM/rmDataHandlerComp.htm.,
See also http://developer.apple.com/technotes/tn/tn1195.html for another discussion of reference data extensions.
Tim Monroe's lizards have started laying eggs. Will they hatch? Will the parents eat the young? Stay tuned for further details. In the meantime, you can contact him at monroe@apple.com.