Guide to getting raw data in and out of the movie library.

Written by Eric Graves
Last Updated 12/20/97 by Scott Eikenberry

This document has the following goals:

Organization of a QuickTime file, and how to access it through the SGI movie library.

Movie Library API Organization

There are a number of different types of calls in the movielib, ranging from very high level (mvPlay, mvRender, etc), to very low level calls. Additionally, there are alternate forms of some calls. Note that many high on this list internally call lower level calls. This web page primarily focuses on the lower-level calls (especially those under 5, 6, 7, and 8 below).
  1. Basic calls (used by essentially all movie lib application): eg, mvOpenFile
  2. High level playback calls: eg, mvBind*, mvPlay*
  3. High level data access calls.
  4. High level Edit list calls. These change the edit list for you, although they don't let you find out what the edit list actually looks like. eg, mvDeleteFramesAtTime, mvCopyFramesAtTime
  5. Low level Edit list calls.
  6. Data description calls. These calls let you set or get information about a particular piece of data in a track, via its data index. eg mvGetTrackDataInfo (Line labeled #2 in picture above)
  7. Data access calls which do IO for you
  8. Data Access calls which do not perform IO

Background: Writing raw data into a QuickTime movie file

There are two different ways that the movie library allows you to insert raw data into a QuickTime track. You may either pass in one or more buffers of raw data (and have the movie library write the data to disk), or you may simply tell the movie library the file offset (in bytes) of data which you have already written into the file.

If you choose to do the latter, keep in mind that the movie library will require space for its first QuickTime atom at the beginning of the file as well as extending the file with the remainder of the QuickTime header. The minimum empty space required at the beginning of the file is 128 bytes (assuming that you will want to take advantage of the movie library's ability to generate QuickTime files with 64 bit file offsets). In practice, however, most often 1K or 4K (or some other convenient size, perhaps derived from the disk block size) is more commonly used (extra space is simply ignored).

Do not call mvInsertTrackDataAtOffset with an offset less than the minimum headersize of 128. If you do, when the movie header is written to disk(during mvClose for instance) the header will overwrite the first few bytes of the first block of data. This will appear to the user as a few bad pixels in the first field/frame. Also remember mvInsertTrackDataAtOffset does not move the file pointer for you.  This means to leave space for the header you must call a function like lseek with your initial offset before your first write.

If you do your own file I/O it is important that you return the file pointer to the position it was in when the movie library lasted used it for file I/O.  If you do all your own file I/O this often means returning it to position 0 (with a call to lseek) just before calling mvClose which flushes the movie header.

To insert raw buffers of data (eg, perhaps compressed JPEG data that you have received from the dmIC library, or uncompressed data you have received from the VL), you would normally do the following.

Note that this call is simple, since it takes a void* (or pair of void*'s) of data, and inserts it (them) into the movie file at a particular time, without requiring multiple API calls. However, if your application requires specific control over where on the disk a particular piece of data goes (for example, so that you can take advantage of directio), it is not the function for you.

To insert data which is already on disk into the movie's edit list (so that its location is stored in the file, and can be played back using standard QuickTime compatible tools), you would normally perform a multiple step process centered around mvInsertTrackDataAtOffset and friends. Note that mvInsertTrackDataAtOffset does not write anything to disk (nor does mvGetTrackDataIndex, nor mvSetTrackDataFieldInfo); it simply updates in memory movie data structures, which will be flushed to disk when mvWrite or mvClose is called.

Also, note that calling mvInsertTrackDataAtOffset need not be called between each write; you can also write all of your data to disk in one pass (and maintain your own data structure keeping track of where each frame/field is on disk), and then have a post-processing step that calls mvInsertTrackDataAtOffset. In some cases, this may be preferable, since mvInsertTrackDataAtOffset may call malloc; however, in other cases, it may be preferable to call it after each write, as this will provide some amount of CPU/IO overlap.

Creating a Movie from a file descriptor (eg, "creating the QuickTime header")

Creating the movie from a RDWR file descriptor: This piece of code will create a QuickTime movie with one image track, uncompressed, interlaced, CbYCrY. Note that the header will not be written to disk until the user calls the mvWrite or mvClose function.
DMstatus initMovie( int fd, int width, int height, MVid *movie, MVid 
                    *track)
{
     DMstatus s = DM_FAILURE;
     DMparams *movieP = NULL;
     DMparams *trackP = NULL;

     *movie = NULL;
     *track = NULL;

     if (dmParamsCreate(&movieP) != DM_SUCCESS)
          goto ERR;

     if (mvSetMovieDefaults(movieP, MV_FORMAT_QT) != DM_SUCCESS)
          goto ERR;

     if (mvCreateFD(fd, movieP, NULL, movie) != DM_SUCCESS)
          goto ERR;

     if (dmParamsCreate(&trackP) != DM_SUCCESS)
          goto ERR;

     if (mvSetImageDefaults(trackP, width, height, MV_FORMAT_QT) 
         != DM_SUCCESS)
          goto ERR;

     if (dmParamsSetEnum(trackP, DM_IMAGE_ORIENTATION, 
         DM_IMAGE_TOP_TO_BOTTOM) != DM_SUCCESS)
          goto ERR;

     if (dmParamsSetEnum(trackP, DM_IMAGE_INTERLACING, 
         DM_IMAGE_INTERLACED_ODD) != DM_SUCCESS)
          goto ERR;

     if (dmParamsSetEnum(trackP, DM_IMAGE_PACKING, 
         DM_IMAGE_PACKING_CbYCrY) != DM_SUCCESS)
          goto ERR;

     if (mvAddTrack(*movie, DM_IMAGE, trackP, NULL, track) != DM_SUCCESS)
          goto ERR;

     s = DM_SUCCESS;

ERR:
     if (movieP != NULL)
          dmParamsDestroy(movieP);
     if (trackP != NULL)
          dmParamsDestroy(trackP);

     return s;
}

Example: Inserting already-written field-based data into a tracks edit list.

This piece of code is a rough approximation of the loop that will take data you have already written to disk at offset1, size1, offset2, size2 (assuming a pair of fields), and add it to the movie's edit list.
MVtime frameDur = 1001;
MVtime TS = 30000;
MVtime curTime = 0;

// Note that before this loop is entered, you should have already
// seeked far enough to leave space for the first QT Atom.

while( !done )
{
    off64_t off1, off2;
    size64_t sz1, sz2;
    int index, dummy;
    void * data1, * data2;

    // this function writes data to disk, and fills in off1, sz1, 
    // off2, sz2.
    // it is the thing that actually calls the write system call.
    // 
    // In its most basic form it could be something along the lines
    // of the following:
    //
    //    off1 = tell64( fd );
    //    write( fd, data1, sz1 );
    //    off2 = tell64( fd );
    //    write( fd, data2, sz2 );
    //

    myWriterFunction( &off1, &sz1, &off2, &sz2 );

    // Now, tell the movie lib about the data just written to disk.
    // This assumes that off1 < off2, although that is not strictly
    // required.

    s = mvInsertTrackDataAtOffset( track, 1, curTime, frameDur, TS,
                                   off1, 0, 
                                   MV_FRAMETYPE_KEY, 0 );
    // Since we will be immediately calling mvSetTrackDataFieldInfo,
    // which overwrites the size set by mvInsertTrackDataAtOffset,
    // pass in a zero as the size above.

    if ( s != DM_SUCCESS ) 
        ERROR();

    s = mvGetTrackDataIndexAtTime( track, curTime, TS, &index, &dummy );

    if ( s != DM_SUCCESS )
        ERROR();

    s = mvSetTrackDataFieldInfo( track, index, sz1, off2-(off1+sz1),
                                 sz2 );

    curTime += frameDur;
}

Background: reading raw data from a QuickTime file (via the QuickTime edit list)

Since QuickTime files consist of media plus edit list, reading the raw data in edited order from a QuickTime file requires going through a time-mapping (the edit list), and requesting data in the media.

Note that when reading raw data from disk, you must pay attention to whether the chunk of data was written as a frame or as a field pair. Pay special attention the the mvTrackDataHasFieldInfo call below.

Also, it is important to note that the QuickTime file format allows the compression/size/etc parameters of chunks within a track to change at any point within the track. For this reason, you need to use mvGetTrackDataInfo (and possibly mvGetTrackDataParams) to make sure the data for the current chunk is in a format you are expecting. Although it is uncommon to encounter files with varying parameters for chunks within one track, it does happen, and therefore is something that any code reading raw data from a QuickTime file must be aware of. Depending on your application, you may wish to abort when this happens, change codecs/etc, or, simply fall back to the general purpose movielib function mvRenderMovieToImageBuffer or mvRenderMovieToAudioBuffer. The mvRender* functions in the movielib have full support for changing chunk parameters, so they make a good fallback case. The correct behavior will obviously depend upon your application.

Just as with the "writing raw data into a QuickTime file" (example above), you have a choice as to how to read the data: you may either pass the movielib one or two void* buffers (mvReadTrackData/mvReadTrackDataFields), or you may ask the movielib for the on-disk offset of a chunk of data (using mvGetTrackDataInfo/mvGetTrackDataFieldInfo) and perform the read operation yourself. Obviously, the former is simpler for non performance critical applications, but the latter allows you to do things like combine multiple reads into one system call (eg, readv), or use directio, allowing you to (potentially) avoid extra bcopy operations.

Since all but one step is the same for the void* method as for the read method, I've combined them into one description.

Example for reading data:

This piece of example code will examine a file, and tell you the file offsets of each chunk of data. It may be modified to do something like checking that every chunk of data is written at a directio-compatible file offset (based on your disk block size).