Creating a Capture Application


DirectShow provides the capability to capture and preview both video and audio data from an application, when combined with the appropriate capture hardware. The data source might include a VCR, camera, TV tuner, microphone, or other source. An application can display the captured data immediately (preview) or save it to a file for later viewing either inside or outside of the application.

DirectShow takes advantage of new capture drivers that are written as DirectShow filters, and also uses existing Video for Windows-style drivers.

Note: This article relies heavily on the DirectShow Capture sample application. See the AMCap sample code (Amcap.cpp) in the Samples\DS\Capture directory of the DirectShow SDK for complete sample code, because this article does not present DirectShow Capture in its entirety.

The DirectShow Capture sample application performs video and audio capture, similar to the VidCap sample from Video for Windows®. It uses the ICaptureGraphBuilder interface to handle the majority of the capture work. In your own capture application, you'll use the same methods and interfaces that AMCap uses. This article focuses on AMCap's use of ICaptureGraphBuilder to perform video and audio capture and presents relevant code excerpts from AMCap.

This article assumes you are familiar with the DirectShow filter graph architecture and the general layout of a capture filter graph. See Filter Graph Manager and Filter Graphs and About Capture Filter Graphs for more information.

Contents of this article:

Introduction to ICaptureGraphBuilder

The ICaptureGraphBuilder interface provides a filter graph builder object that applications use to handle some of the more tedious tasks involved in building a capture filter graph, which frees the application to focus on capture. You access the graph builder object by calling methods on ICaptureGraphBuilder. The variety of methods satisfies the basic requirements for capture and preview functionality.

The FindInterface method searches for a particular capture-related interface in the filter graph. The method handles the complexities of filter graph traversal for you, which enables you to access the functionality of a particular interface without having to enumerate pins and filters in the filter graph looking for the interface. The RenderStream method connects source filters to rendering filters, optionally adding other needed intermediate filters. The ControlStream method independently control sections of the graph for frame-accurate start and stop.

Additional methods deal with allocating space for the capture file (AllocCapFile), specifying a name for it and building up the file writer section of the graph, which consists of the multiplexer and file writer filters (SetOutputFileName), and saving the captured data to another file (CopyCaptureFile). Finally, SetFiltergraph and GetFiltergraph enable the application to provide a filter graph for the graph builder to use or retrieve the filter graph already in use.

Device Enumeration and Capture Interfaces

AMCap's InitCapFilters function enumerates the capture devices on the system by using the ICreateDevEnum::CreateClassEnumerator method. After enumerating a capture device and instantiating a DirectShow filter to use that device, the sample calls the ICaptureGraphBuilder::FindInterface method several times to obtain interface pointers for the IAMDroppedFrames, IAMVideoCompression, IAMStreamConfig, and IAMVfwCaptureDialogs capture-related interfaces. The AMCap code saves all of these interface pointers for later use in the gcap global structure and uses gcap structure members throughout the code.

Note: IAMVfwCaptureDialogs is designed for you to use only with the Microsoft-supplied video capture filter and only when using a former Video for Windows device.

For your convenience, the declaration of the gcap structure follows:


struct _capstuff {
    char szCaptureFile[_MAX_PATH];
    WORD wCapFileSize;  // size in Meg
    ICaptureGraphBuilder *pBuilder;
    IVideoWindow *pVW;
    IMediaEventEx *pME;
    IAMDroppedFrames *pDF;
    IAMVideoCompression *pVC;
    IAMVfwCaptureDialogs *pDlg;
    IAMStreamConfig *pASC;      // for audio cap
    IAMStreamConfig *pVSC;      // for video cap
    IBaseFilter *pRender;
    IBaseFilter *pVCap, *pACap;
    IGraphBuilder *pFg;
    IFileSinkFilter *pSink;
    IConfigAviMux *pConfigAviMux;
    int  iMasterStream;
    BOOL fCaptureGraphBuilt;
    BOOL fPreviewGraphBuilt;
    BOOL fCapturing;
    BOOL fPreviewing;
    BOOL fCapAudio;
    int  iVideoDevice;
    int  iAudioDevice;
    double FrameRate;
    BOOL fWantPreview;
    long lCapStartTime;
    long lCapStopTime;
    char achFriendlyName[120];
    BOOL fUseTimeLimit;
    DWORD dwTimeLimit;
} gcap;

AMCap's InitCapFilters function stores several interface pointers in the gcap structure. Be sure to properly release all interface pointers when they are no longer needed as shown in the following example.


    if (gcap.pBuilder)
	gcap.pBuilder->Release();
    gcap.pBuilder = NULL;
    if (gcap.pSink)
	gcap.pSink->Release();
    gcap.pSink = NULL;
    if (gcap.pConfigAviMux)
	gcap.pConfigAviMux->Release();
    gcap.pConfigAviMux = NULL;
    if (gcap.pRender)
	gcap.pRender->Release();
    gcap.pRender = NULL;
    if (gcap.pVW)
	gcap.pVW->Release();
    gcap.pVW = NULL;
    if (gcap.pME)
	gcap.pME->Release();
    gcap.pME = NULL;
    if (gcap.pFg)
	gcap.pFg->Release();
    gcap.pFg = NULL;

See Enumerate and Access Hardware Devices in DirectShow Applications for more information about device enumeration.

Building the Capture and Preview Filter Graph

AMCap includes a BuildCaptureGraph function that builds up a capture graph with both capture and preview components. Most applications will perform the same tasks as follows:

These tasks are explained in greater detail later in this section.

AMCap also includes a BuildPreviewGraph function that is essentially a version of BuildCaptureGraph that deals only with preview. Another difference between BuildCaptureGraph and BuildPreviewGraph is that the latter uses ICaptureGraphBuilder::SetFiltergraph to provide a filter graph object (IGraphBuilder interface) for the capture graph builder object (ICaptureGraphBuilder interface) to use. You probably won't need to call SetFiltergraph as the graph builder object creates a filter graph to use by default. Use this method only if you have already created your own filter graph and want the graph builder to use your filter graph. If you call this method after the graph builder has created a filter graph, this method will fail. BuildPreviewGraph calls CoCreateInstance to create a new filter graph object, if necessary, as shown in the following example.


	hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC,
                      IID_IGraphBuilder, (LPVOID *)&gcap.pFg);


hr = gcap.pBuilder->SetFiltergraph(gcap.pFg);
    if (hr != NOERROR) {
	ErrMsg("Cannot give graph to builder");
	goto SetupPreviewFail;
    }

The details of each important task performed by BuildCaptureGraph follow.

Set the Capture File Name

AMCap's SetCaptureFile function displays the common Open File dialog box to enable the user to select a capture file. If the specified file is a new file, it calls the application-defined AllocCaptureFile function that prompts the user to allocate space for the capture file. This "preallocation" of file space is important, because it reserves a large block of space on disk. This speeds up the capture operation when it occurs, because the file space doesn't have to be allocated while capture takes place (it has been preallocated). The ICaptureGraphBuilder::AllocCapFile method performs the actual file allocation. IFileSinkFilter::SetFileName instructs the file writer filter to use the file name that the user chose. The code assumes you've called ICaptureGraphBuilder::SetOutputFileName to add the file writer to the filter graph. See Create the Rendering Section of the Graph and Tell it to Write to the Capture File Specified Previously for more information.

The AMCap-defined SetCaptureFile and AllocCaptureFile functions follow:


/*
 * Put up a dialog to allow the user to select a capture file.
 */
BOOL SetCaptureFile(HWND hWnd)
{
    if (OpenFileDialog(hWnd, gcap.szCaptureFile, _MAX_PATH)) {
        OFSTRUCT os;

	// We have a capture file name

        /*
         * if this is a new file, then invite the user to
         * allocate some space
         */
        if (OpenFile(gcap.szCaptureFile, &os, OF_EXIST) == HFILE_ERROR) {

	    // Bring up dialog, and set new file size
	    BOOL f = AllocCaptureFile(hWnd);
	    if (!f)
		return FALSE;
        }
    } else {
	return FALSE;
    }

    SetAppCaption();	// need a new app caption

    // Tell the file writer to use the new file name
    if (gcap.pSink) {
        WCHAR wach[_MAX_PATH];
        MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, gcap.szCaptureFile, -1,
							wach, _MAX_PATH);
        gcap.pSink->SetFileName(wach, NULL);
    }

    return TRUE;
}

// Preallocate the capture file
// 
BOOL AllocCaptureFile(HWND hWnd)
{
// We'll get into an infinite loop in the dlg proc setting a value
    if (gcap.szCaptureFile[0] == 0)
	return FALSE;

    /*
     * show the allocate file space dialog to encourage
     * the user to pre-allocate space
     */
    if (DoDialog(hWnd, IDD_AllocCapFileSpace, AllocCapFileProc, 0)) {

	// Ensure repaint after dismissing dialog before
	// possibly lengthy operation
	UpdateWindow(ghwndApp);

        // User has hit OK. Alloc requested capture file space
	BOOL f = MakeBuilder();
	if (!f)
	    return FALSE;
        WCHAR wach[_MAX_PATH];
        MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, gcap.szCaptureFile, -1,
							wach, _MAX_PATH);
        if (gcap.pBuilder->AllocCapFile(wach,
			gcap.wCapFileSize * 1024L * 1024L) != NOERROR) {
            MessageBoxA(ghwndApp, "Error",
				"Failed to pre-allocate capture file space",
                          	MB_OK | MB_ICONEXCLAMATION);
	    return FALSE;
        }
	return TRUE;
    } else {
	return FALSE;
    }
}

Create a Graph Builder Object

AMCap's MakeBuilder function creates a capture graph builder object and obtains an ICaptureGraphBuilder interface pointer by calling CoCreateInstance. If you already have a capture graph builder object, you can call QueryInterface to obtain an interface pointer. AMCap stores the object pointer in the pBuilder member of the global gcap structure.


// Make a graph builder object we can use for capture graph building
//
BOOL MakeBuilder()
{
    // We have one already
    if (gcap.pBuilder)
	return TRUE;

    HRESULT hr = CoCreateInstance((REFCLSID)CLSID_CaptureGraphBuilder,
			NULL, CLSCTX_INPROC, (REFIID)IID_ICaptureGraphBuilder,
			(void **)&gcap.pBuilder);
    return (hr == NOERROR) ? TRUE : FALSE;
}

Create the Rendering Section of the Graph and Tell it to Write to the Capture File Specified Previously

The rendering section consists of a multiplexer filter and the File Writer. DirectShow provides an AVI MUX (multiplexer) filter. See About Capture Filter Graphs for more information.

ICaptureGraphBuilder::SetOutputFileName is the key method here. It signals to add the multiplexer and file writer to the filter graph, connects them, and sets the file name. The value of the first parameter is MEDIASUBTYPE_Avi, indicating that the capture graph builder object will insert an AVI multiplexer filter, and, consequently, the file writer will write the data to the capture file in AVI file format. The second parameter (wach) is the file name. The last two parameters are pointers to the multiplexer filter (gcap.pRender) and the file writer filter (gcap.pSink), respectively, and are initialized for you by the capture graph builder object upon return from SetOutputFileName. AMCap stores these pointers in its gcap structure. Internally, the capture graph builder object creates a filter graph object which exposes the IGraphBuilder interface and inserts these two filters into that filter graph. It tells the file writer to use the specified file when writing to disk. The following example demonstrates a call to SetOutputFileName.


//
// We need a rendering section that will write the capture file out in AVI
// file format
//

    WCHAR wach[_MAX_PATH];
    MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, gcap.szCaptureFile, -1, wach,
								_MAX_PATH);
    GUID guid = MEDIASUBTYPE_Avi;
    hr = gcap.pBuilder->SetOutputFileName(&guid, wach, &gcap.pRender,
								&gcap.pSink);
    if (hr != NOERROR) {
	ErrMsg("Error %x: Cannot set output file", hr);
	goto SetupCaptureFail;
    }

Alternatively, if you want filters besides the multiplexer and file writer in the rendering section of your filter graph, call IFilterGraph::AddFilter to explicitly add the necessary filters. You might need to remember the pointer to the IBaseFilter interface of the first filter in your custom rendering chain so you can use it in calls such as RenderStream.

Retrieve the Current Filter Graph

Because the capture graph builder object created a filter graph in response to SetOutputFileName and you must put the necessary filters in the same filter graph, call the ICaptureGraphBuilder::GetFiltergraph method to retrieve the newly created filter graph. The pointer to the filter graph's IGraphBuilder interface is returned in the parameter (gcap.pFg).


//
// The graph builder created a filter graph to do that.  Find out what it is,
// and put the video capture filter in the graph too.
//

    hr = gcap.pBuilder->GetFiltergraph(&gcap.pFg);
    if (hr != NOERROR) {
	ErrMsg("Error %x: Cannot get filtergraph", hr);
	goto SetupCaptureFail;
    }

Add the Video and Audio Capture Filters to the Current Filter Graph

Call IFilterGraph::AddFilter to add the capture filters to the filter graph as shown in the following example.


    hr = gcap.pFg->AddFilter(gcap.pVCap, NULL);
    if (hr != NOERROR) {
	ErrMsg("Error %x: Cannot add vidcap to filtergraph", hr);
	goto SetupPreviewFail;
    }

    hr = gcap.pFg->AddFilter(gcap.pACap, NULL);
	if (hr != NOERROR) {
	    ErrMsg("Error %x: Cannot add audcap to filtergraph", hr);
	    goto SetupCaptureFail;
	}

Render the Video Capture Filter's Capture Pin and the Audio Capture Filter's Capture Pin

The ICaptureGraphBuilder::RenderStream method connects the source filter's pin to the rendering filter. It connects intermediate filters if necessary. The pin category is optional, but typically specifies either a capture pin (PIN_CATEGORY_CAPTURE) or a preview pin (PIN_CATEGORY_PREVIEW). The following example connects the capture pin on the video capture filter (gcap.pVCap) to the renderer (gcap.pRender).


//
// Render the video capture and preview pins - we may not have preview, so
// don't worry if it doesn't work
//

    hr = gcap.pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, NULL, gcap.pVCap,
						NULL, gcap.pRender);
    // Error checking

Call ICaptureGraphBuilder::RenderStream again to connect the audio capture filter (gcap.pACap) to the audio renderer as in the following example.


//
// Render the audio capture pin?
//

    if (gcap.fCapAudio) {
	hr = gcap.pBuilder->RenderStream(&PIN_CATEGORY_CAPTURE, NULL,
					gcap.pACap, NULL, gcap.pRender);
    // Error checking

Render the Video Capture Filter's Preview Pin

Call ICaptureGraphBuilder::RenderStream again to render the graph from the capture filter's preview pin to a video renderer as in the following example.


    hr = gcap.pBuilder->RenderStream(&PIN_CATEGORY_PREVIEW, NULL, gcap.pVCap,
						NULL, NULL);

Obtain a Pointer to the Interface that Provides Access to the Video Preview Window

By default, the video preview window will be a separate window from your application window. If you want to change the default behavior, call ICaptureGraphBuilder::FindInterface to obtain a pointer to the IVideoWindow interface. The first parameter, specified by gcap.pVCap, represents the video capture filter, the second is the desired interface (IVideoWindow), and the last will be filled upon return from this function to give you the IVideoWindow interface. After you have the IVideoWindow interface, you can call IVideoWindow methods such as put_Owner, put_WindowStyle, or SetWindowPosition to take ownership of the video preview window, make it a child of your application, or to position it as desired.


// This will go through a possible decoder, find the video renderer it's
// connected to, and get the IVideoWindow interface on it
   hr = gcap.pBuilder->FindInterface(&PIN_CATEGORY_PREVIEW, gcap.pVCap,
					IID_IVideoWindow, (void **)&gcap.pVW);
    if (hr != NOERROR) {
	ErrMsg("This graph cannot preview");
    } else {
	RECT rc;
	gcap.pVW->put_Owner((long)ghwndApp);    // We own the window now
	gcap.pVW->put_WindowStyle(WS_CHILD);    // you are now a child
	// give the preview window all our space but where the status bar is
	GetClientRect(ghwndApp, &rc);
	cyBorder = GetSystemMetrics(SM_CYBORDER);
	cy = statusGetHeight() + cyBorder;
	rc.bottom -= cy;
	gcap.pVW->SetWindowPosition(0, 0, rc.right, rc.bottom); // be this big
	gcap.pVW->put_Visible(OATRUE);
    }

Now that you've built the entire capture filter graph, you can preview video, audio, or actually capture data.

Controlling the Capture Filter Graph

Because a capture filter graph constructed by the ICaptureGraphBuilder interface is simply a specialized filter graph, controlling a capture filter graph is much like controlling any other kind of filter graph: you use the IMediaControl interface's Run, Pause, and Stop methods. You can use the CBaseFilter::Pause method to cue things up, but remember that capture and recompression only happen when the graph is running. In addition, ICaptureGraphBuilder provides the ControlStream method to control the start and stop times of the capture filter graph's streams. Internally, ControlStream calls the IAMStreamControl::StartAt and IAMStreamControl::StopAt methods to start and stop the capture and preview portions of the filter graph for frame-accurate control.

Note: This method might not work on every capture filter because not every capture filter supports IAMStreamControl on its pins.

The ICaptureGraphBuilder::ControlStream method's first parameter (pCategory) is a pointer to a GUID that specifies the output pin category. This value is normally either PIN_CATEGORY_CAPTURE or PIN_CATEGORY_PREVIEW. See AMPROPERTY_PIN for a complete list of categories. Specify NULL to control all capture filters in the graph.

The second parameter (pFilter) in ICaptureGraphBuilder::ControlStream indicates which filter to control. Specify NULL to control the whole filter graph as AMCap does.

To run only the preview portion of the capture filter graph, prevent capture by calling ICaptureGraphBuilder::ControlStream with the capture pin category and the value MAX_TIME as the start time (third parameter, pstart). Call the method again with preview as the pin category, and a NULL start value to start preview immediately. The fourth parameter indicates the desired stop time (pstop, as with start time, NULL means immediately). MAX_TIME is defined in the DirectShow base classes as the maximum reference time, and in this case means to ignore or cancel the specified operation.

The last two parameters, wStartCookie and wStopCookie are start and stop cookies respectively. These cookies are arbitrary values set by the application so that it can differentiate between start and stop times and tell when specific actions have been completed. AMCap doesn't use a specific time in ICaptureGraphBuilder::ControlStream, so it doesn't need any cookies. If you use a cookie, use IMediaEvent to get event notifications. See IAMStreamControl for more information.

The following code fragment sets preview to start immediately, but ignores capture.


// Let the preview section run, but not the capture section
    // (There might not be a capture section)
    REFERENCE_TIME start = MAX_TIME, stop = MAX_TIME;
    // show us a preview first? but don't capture quite yet...
    hr = gcap.pBuilder->ControlStream(&PIN_CATEGORY_PREVIEW, NULL,
				gcap.fWantPreview ? NULL : &start,
				gcap.fWantPreview ? &stop : NULL, 0, 0);
    if (SUCCEEDED(hr))
        hr = gcap.pBuilder->ControlStream(&PIN_CATEGORY_CAPTURE, NULL, &start,
								NULL, 0, 0);

The same concept applies if you want only to capture and not preview. Set the capture start time to NULL to capture immediately and set the capture stop time to MAX_TIME. Set the preview start time to MAX_TIME, with an immediate (NULL) stop time.

The following example tells the filter graph to start the preview stream now (the start (third) parameter is NULL). Specifying MAX_TIME for the stop time means disregard the stop time.


    gcap.pBuilder->ControlStream(&PIN_CATEGORY_PREVIEW, NULL, NULL, MAX_TIME, 0, 0); 

Calling IMediaControl::Run runs the graph.


// Run the graph
    IMediaControl *pMC = NULL;
    HRESULT hr = gcap.pFg->QueryInterface(IID_IMediaControl, (void **)&pMC);
    if (SUCCEEDED(hr)) {
	hr = pMC->Run();
	if (FAILED(hr)) {
	    // Stop parts that ran
	    pMC->Stop();
	}
	pMC->Release();
    }
    if (FAILED(hr)) {
	ErrMsg("Error %x: Cannot run preview graph", hr);
	return FALSE;

If the graph is already running, start capture immediately with another call to ICaptureGraphBuilder::ControlStream. For example, the following call controls the whole filter graph (NULL second parameter), starts now (NULL third parameter), and never stops (fourth parameter initialized to MAX_TIME).


    // NOW!
    gcap.pBuilder->ControlStream(&PIN_CATEGORY_CAPTURE, NULL, NULL, &stop, 0, 0);

AMCap uses this approach to start capture in response to the user clicking a button.

To stop the capture or preview operation, call IMediaControl::Stop, much as you called IMediaControl::Run to run the filter graph.


// Stop the graph
    IMediaControl *pMC = NULL;
    HRESULT hr = gcap.pFg->QueryInterface(IID_IMediaControl, (void **)&pMC);
    if (SUCCEEDED(hr)) {
	hr = pMC->Stop();
	pMC->Release();
    }

Obtaining Capture Statistics

AMCap calls methods on the IAMDroppedFrames interface to obtain capture statistics. It determines the number of frames dropped (IAMDroppedFrames::GetNumDropped) and captured (IAMDroppedFrames::GetNumNotDropped), and uses the Win32 timeGetTime function at the beginning and end of capture to determine the capture operation's duration. The IAMDroppedFrames::GetAverageFrameSize method provides the average size of captured frames in bytes. Use the information from IAMDroppedFrames::GetNumNotDropped, timeGetTime, and IAMDroppedFrames::GetAverageFrameSize to obtain the total bytes captured and calculate the sustained frames per second for the capture operation.

Saving the Captured File

The original preallocated capture file temporarily holds capture data so you can capture as quickly as possible. When you want to save the data you captured to a more permanent location, call ICaptureGraphBuilder::CopyCaptureFile. This method transfers the captured data out of the previously allocated capture file to another file you choose. The resulting new file size matches the size of the actual captured data rather than the preallocated file size, which is usually very large.

The ICaptureGraphBuilder::CopyCaptureFile method's first parameter is the file you're copying from (typically the very large, preallocated file you always use for capture). The second parameter is the file to which you want to save your captured data. Setting the third parameter to TRUE indicates that the user is allowed to abort the copy operation by pressing ESC. The last parameter is optional and enables you to supply a progress indicator, if desired, by implementing the IAMCopyCaptureFileProgress interface. The following example demonstrates a call to CopyCaptureFile.


    hr = pBuilder->CopyCaptureFile(wachSrcFile, wachDstFile,TRUE,NULL);

The SaveCaptureFile function defined by AMCap prompts the to enter a new file name in the Open File common dialog box, uses the Win32 MultiByteToWideChar function to convert the file name to a wide string, and saves the captured data to the specified file using ICaptureGraphBuilder::CopyCaptureFile.


/*
 * Put up a dialog to allow the user to save the contents of the capture file
 * elsewhere
 */
BOOL SaveCaptureFile(HWND hWnd)
{
    HRESULT hr;
    char achDstFile[_MAX_PATH];
    WCHAR wachDstFile[_MAX_PATH];
    WCHAR wachSrcFile[_MAX_PATH];

    if (gcap.pBuilder == NULL)
	return FALSE;

    if (OpenFileDialog(hWnd, achDstFile, _MAX_PATH)) {

	// We have a capture file name
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, gcap.szCaptureFile, -1,
						wachSrcFile, _MAX_PATH);
	MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, achDstFile, -1,
						wachDstFile, _MAX_PATH);
	statusUpdateStatus(ghwndStatus, "Saving capture file - please wait...");

	// We need our own graph builder because the main one might not exist
	ICaptureGraphBuilder *pBuilder;
	hr = CoCreateInstance((REFCLSID)CLSID_CaptureGraphBuilder,
			NULL, CLSCTX_INPROC, (REFIID)IID_ICaptureGraphBuilder,
			(void **)&pBuilder);
	if (hr == NOERROR) {
	    // Allow the user to press ESC to abort... don't ask for progress
	    hr = pBuilder->CopyCaptureFile(wachSrcFile, wachDstFile,TRUE,NULL);
	    pBuilder->Release();
	}
	if (hr == S_OK)
	    statusUpdateStatus(ghwndStatus, "Capture file saved");
	else if (hr == S_FALSE)
	    statusUpdateStatus(ghwndStatus, "Capture file save aborted");
	else
	    statusUpdateStatus(ghwndStatus, "Capture file save ERROR");
	return (hr == NOERROR ? TRUE : FALSE); 

    } else {
return TRUE;    // They canceled or something
    }
}

See Amcap.cpp and Status.cpp from the AMCap sample for more details about capturing media files and obtaining capture statistics.

© 1997 Microsoft Corporation. All rights reserved. Terms of Use.