1260 lines
37 KiB
C
1260 lines
37 KiB
C
/*
|
|
|
|
* AVISAVE.C
|
|
|
|
* routine for writing Standard AVI files
|
|
|
|
* AVISave()
|
|
|
|
* Copyright (c) 1992-1995 Microsoft Corporation. All Rights Reserved.
|
|
|
|
* You have a royalty-free right to use, modify, reproduce and
|
|
* distribute the Sample Files (and/or any modified version) in
|
|
* any way you find useful, provided that you agree that
|
|
* Microsoft has no warranty obligations or liability for any
|
|
* Sample Application Files which are modified.
|
|
|
|
*/
|
|
|
|
#include <win32.h>
|
|
|
|
#ifdef USE_ISVALIDINTERFACE
|
|
#include <valid.h>
|
|
#endif
|
|
|
|
#include <vfw.h>
|
|
#include "avicmprs.h"
|
|
#include "debug.h"
|
|
|
|
#include <stdlib.h>
|
|
#ifndef _WIN32 // DS!=SS nightmare
|
|
#ifndef WINDLL
|
|
#define WINDLL
|
|
#define _WINDLL
|
|
#define __WINDLL
|
|
#endif
|
|
#endif
|
|
#include <stdarg.h>
|
|
|
|
#ifdef UNICODE
|
|
#include <wchar.h>
|
|
#endif
|
|
|
|
//extern LONG FAR PASCAL muldiv32(LONG,LONG,LONG);
|
|
|
|
|
|
/* Auto-doc for the AVICOMPRESSOPTIONS structure. Make sure it matches */
|
|
/* the declarations in avifile.h !!! */
|
|
|
|
|
|
/*
|
|
* @doc EXTERNAL AVICOMPRESSOPTIONS
|
|
|
|
* @types AVICOMPRESSOPTIONS | This structure contains information
|
|
* about a stream and how it is to be compressed and saved.
|
|
* This structure passes data to <f AVIMakeCompressedStream>
|
|
* (or <f AVISave> which uses <f AVIMakeCompressedStream>).
|
|
|
|
* @field DWORD | fccType | Specifies a four-character code
|
|
* indicating the stream type. The following
|
|
* constants have been defined for the data commonly
|
|
* found in AVI streams:
|
|
|
|
* @flag streamtypeAUDIO | Indicates an audio stream.
|
|
* @flag streamtypeMIDI | Indicates a MIDI stream.
|
|
* @flag streamtypeTEXT | Indicates a text stream.
|
|
* @flag streamtypeVIDEO | Indicates a video stream.
|
|
|
|
* @field DWORD | fccHandler | For a video stream, specifies the
|
|
* four-character code for the compressor handler that
|
|
* will compress this stream when it is saved
|
|
* (For example, mmioFOURCC('M','S','V','C')).
|
|
* This member is not used for audio streams.
|
|
|
|
* @field DWORD | dwKeyFrameEvery | Specifies the maximum period
|
|
* between key frames. This member is used only
|
|
* if the AVICOMPRESSF_KEYFRAMES flag is set, otherwise
|
|
* every frame is a key frame.
|
|
|
|
* @field DWORD | dwQuality | Specifies the quality value passed
|
|
* to a video compressor. This member is not used for
|
|
* an audio compressor.
|
|
|
|
* @field DWORD | dwBytesPerSecond | Specifies the data rate a video
|
|
* compressor should use. This member is used only
|
|
* if the AVICOMPRESSF_DATARATE flag is set.
|
|
|
|
* @field DWORD | dwFlags | Specifies the flags used for compression:
|
|
|
|
* @flag AVICOMPRESSF_INTERLEAVE | Indicates this stream is to be interleaved
|
|
* every <e AVICOMPRESSOPTIONS.dwInterleaveEvery> frames
|
|
* with respect to the first stream.
|
|
|
|
* @flag AVICOMPRESSF_KEYFRAMES | Indicates this video stream
|
|
* is to be saved with key frames at least
|
|
* every <e AVICOMPRESSOPTIONS.dwKeyFrameEvery> frames.
|
|
* By default, every frame will be a key frame.
|
|
|
|
* @flag AVICOMPRESSF_DATARATE | Indicates this video stream
|
|
* is to be compressed with the data rate
|
|
* specified in <e AVICOMPRESSOPTIONS.dwBytesPerSecond>.
|
|
|
|
* @flag AVICOMPRESSF_VALID | Indicates this structure contains
|
|
* valid data. If this flag is set, AVIFile uses the structure
|
|
* data to set the default compression values for <f AVISaveOptions>.
|
|
* If an empty structure is passed and this flag is not set,
|
|
* some defaults will be chosen.
|
|
|
|
* @field LPVOID | lpFormat | Specifies a pointer to a structure
|
|
* defining the data format. For an audio stream,
|
|
* this is an <t LPWAVEFORMAT> structure.
|
|
|
|
* @field DWORD | cbFormat | Specifies the size of the data referenced by
|
|
* <e AVICOMPRESSOPTIONS.lpFormat>
|
|
|
|
* @field LPVOID | lpParms | Used internally to store compressor
|
|
* specific data.
|
|
|
|
* @field DWORD | cbParms | Specifies the size of the data referenced by
|
|
* <e AVICOMPRESSOPTIONS.lpParms>
|
|
|
|
* @field DWORD | dwInterleaveEvery | Specifies how often
|
|
* to interleave stream data with the data
|
|
* from the first stream. Used only if the
|
|
* AVICOMPRESSF_INTERLEAVE flag is set.
|
|
|
|
*/
|
|
|
|
/*
|
|
* @doc EXTERNAL AVISave
|
|
|
|
* @api LONG | AVISave | This function is used to save an AVI file.
|
|
|
|
* @parm LPCTSTR | szFile | Specifies a zero-terminated string
|
|
* containing the name of the file to save.
|
|
|
|
* @parm CLSID FAR * | pclsidHandler | Specifies a pointer to the
|
|
* file handler used to write the file. The file will
|
|
* be created by calling <f AVIFileOpen> using this handler. If
|
|
* a handler is not specified, a default one is selected based
|
|
* upon the file extension.
|
|
|
|
* @parm AVISAVECALLBACK | lpfnCallback | Specifies a far pointer to
|
|
* a callback function for the save operation.
|
|
|
|
* @parm int | nStreams | Specifies the number of streams saved in the
|
|
* the file.
|
|
|
|
* @parm PAVISTREAM | pavi | Specifies a pointer an AVI stream.
|
|
* This parameter is paired with <p lpOptions>. The parameter
|
|
* pair can be repeated as a variable number of arguments.
|
|
|
|
* @parm LPAVICOMPRESSOPTIONS | lpOptions | Specifies a pointer to an
|
|
* <t AVICOMPRESSOPTIONS> structure containing the compression
|
|
* options for the stream referenced by <p pavi>.
|
|
* This parameter is paired with <p pavi>. The parameter
|
|
* pair can be repeated as a variable number of arguments.
|
|
|
|
* @parm .| . . | Additional streams can be appened
|
|
* by including more <p pavi> and <p lpOptions> parameter pairs.
|
|
|
|
* @rdesc Returns AVIERR_OK if successful; otherwise it returns an error code.
|
|
|
|
* @comm This function saves an AVI sequence to the file
|
|
* specified by <p szFile>. The <p pavi> and <p lpOptions> parameters
|
|
* define the streams saved. If saving more than one stream,
|
|
* repeat the <p pavi> and <p lpOptions> parameter pair for
|
|
* each additional stream.
|
|
|
|
* A callback function can be supplied in <p lpfnCallback> to
|
|
* display status information and let the user cancel the
|
|
* save operation. The callback uses the following format:
|
|
|
|
* LONG FAR PASCAL SaveCallback(int nPercent)
|
|
|
|
* The <p nPercent> parameter specifies the percentage of the
|
|
* file saved.
|
|
|
|
* The callback function should return AVIERR_OK if the
|
|
* operation should continue and AVIERR_USERABORT if the
|
|
* user wishes to abort the save operation.
|
|
|
|
|
|
* @xref <f AVISaveV> <f AVISaveOptions>
|
|
|
|
*/
|
|
EXTERN_C HRESULT CDECL AVISave(LPCTSTR szFile,
|
|
CLSID FAR *pclsidHandler,
|
|
AVISAVECALLBACK lpfnCallback,
|
|
int nStreams,
|
|
PAVISTREAM pavi,
|
|
LPAVICOMPRESSOPTIONS lpOptions,
|
|
...
|
|
)
|
|
{
|
|
PAVISTREAM FAR *apavi;
|
|
LPAVICOMPRESSOPTIONS FAR *alpOptions;
|
|
int i;
|
|
HRESULT hr;
|
|
|
|
|
|
// We were passed arguments of the form PAVI, OPTIONS, PAVI, OPTIONS, etc.
|
|
// for AVISaveV, we need to separate these into an array of PAVI's and
|
|
// an array of LPAVICOMPRESSOPTIONS.
|
|
|
|
|
|
// !!!not only that, but we need to do it properly, using va_arg etc!!!
|
|
va_list va;
|
|
|
|
|
|
|
|
apavi = (PAVISTREAM FAR *)GlobalAllocPtr(GMEM_MOVEABLE,
|
|
nStreams * sizeof(PAVISTREAM));
|
|
alpOptions = (LPAVICOMPRESSOPTIONS FAR *)GlobalAllocPtr(GMEM_MOVEABLE,
|
|
nStreams * sizeof(LPAVICOMPRESSOPTIONS));
|
|
if (!apavi || !alpOptions)
|
|
return ResultFromScode(AVIERR_MEMORY);
|
|
|
|
// first two args are explicit
|
|
if (nStreams) {
|
|
apavi[0] = pavi;
|
|
alpOptions[0] = lpOptions;
|
|
}
|
|
|
|
// do the rest by *portable* varargs methods
|
|
va_start(va, lpOptions);
|
|
for (i = 1; i < nStreams; i++) {
|
|
apavi[i] = va_arg(va, PAVISTREAM);
|
|
alpOptions[i] = va_arg(va, LPAVICOMPRESSOPTIONS);
|
|
}
|
|
va_end(va);
|
|
|
|
hr = AVISaveV(szFile, pclsidHandler, lpfnCallback, nStreams, apavi,
|
|
alpOptions);
|
|
|
|
GlobalFreePtr(apavi);
|
|
GlobalFreePtr(alpOptions);
|
|
return hr;
|
|
}
|
|
|
|
|
|
#ifdef UNICODE
|
|
//ansi thunk for AVISave - same, but calls AVISaveVA instead
|
|
EXTERN_C HRESULT CDECL
|
|
AVISaveA(
|
|
LPCSTR szFile,
|
|
CLSID FAR *pclsidHandler,
|
|
AVISAVECALLBACK lpfnCallback,
|
|
int nStreams,
|
|
PAVISTREAM pavi,
|
|
LPAVICOMPRESSOPTIONS lpOptions,
|
|
...
|
|
)
|
|
{
|
|
PAVISTREAM FAR *apavi;
|
|
LPAVICOMPRESSOPTIONS FAR *alpOptions;
|
|
int i;
|
|
HRESULT hr;
|
|
|
|
|
|
// We were passed arguments of the form PAVI, OPTIONS, PAVI, OPTIONS, etc.
|
|
// for AVISaveV, we need to separate these into an array of PAVI's and
|
|
// an array of LPAVICOMPRESSOPTIONS.
|
|
|
|
|
|
// !!!not only that, but we need to do it properly, using va_arg etc!!!
|
|
va_list va;
|
|
|
|
|
|
apavi = (PAVISTREAM FAR *)GlobalAllocPtr(GMEM_MOVEABLE,
|
|
nStreams * sizeof(PAVISTREAM));
|
|
alpOptions = (LPAVICOMPRESSOPTIONS FAR *)GlobalAllocPtr(GMEM_MOVEABLE,
|
|
nStreams * sizeof(LPAVICOMPRESSOPTIONS));
|
|
if (!apavi || !alpOptions)
|
|
return ResultFromScode(AVIERR_MEMORY);
|
|
|
|
// first two args are explicit
|
|
if (nStreams) {
|
|
apavi[0] = pavi;
|
|
alpOptions[0] = lpOptions;
|
|
}
|
|
|
|
// do the rest by *portable* varargs methods
|
|
va_start(va, lpOptions);
|
|
for (i = 1; i < nStreams; i++) {
|
|
apavi[i] = va_arg(va, PAVISTREAM);
|
|
alpOptions[i] = va_arg(va, LPAVICOMPRESSOPTIONS);
|
|
}
|
|
va_end(va);
|
|
|
|
hr = AVISaveVA(szFile, pclsidHandler, lpfnCallback, nStreams, apavi,
|
|
alpOptions);
|
|
|
|
GlobalFreePtr(apavi);
|
|
GlobalFreePtr(alpOptions);
|
|
return hr;
|
|
}
|
|
#else
|
|
#ifdef _WIN32
|
|
EXTERN_C HRESULT CDECL
|
|
AVISaveW(
|
|
LPCWSTR szFile,
|
|
CLSID FAR *pclsidHandler,
|
|
AVISAVECALLBACK lpfnCallback,
|
|
int nStreams,
|
|
PAVISTREAM pavi,
|
|
LPAVICOMPRESSOPTIONS lpOptions,
|
|
...
|
|
)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
|
|
|
|
|
|
BOOL FAR PASCAL DummySaveCallback(int iProgress)
|
|
{
|
|
return FALSE; // do nothing, allow save to continue
|
|
}
|
|
|
|
|
|
|
|
#ifdef UNICODE
|
|
// Ansi thunk for AVISaveV
|
|
STDAPI AVISaveVA(LPCSTR szFile,
|
|
CLSID FAR *pclsidHandler,
|
|
AVISAVECALLBACK lpfnCallback,
|
|
int nStreams,
|
|
PAVISTREAM FAR * ppavi,
|
|
LPAVICOMPRESSOPTIONS FAR * plpOptions)
|
|
{
|
|
|
|
// convert the filename, and then call AVISaveVW
|
|
LPWSTR pW;
|
|
int sz;
|
|
HRESULT hr;
|
|
|
|
sz = lstrlenA(szFile)+1;
|
|
pW = (LPWSTR) (LocalAlloc(LPTR, sz * sizeof(WCHAR)));
|
|
if (pW == NULL) {
|
|
return ResultFromScode(AVIERR_MEMORY);
|
|
}
|
|
|
|
mbstowcs(pW, szFile, sz);
|
|
|
|
hr = AVISaveVW(pW, pclsidHandler, lpfnCallback, nStreams, ppavi, plpOptions);
|
|
|
|
LocalFree((HANDLE)pW);
|
|
|
|
return hr;
|
|
}
|
|
#else
|
|
#if _WIN32
|
|
STDAPI AVISaveVW(LPCWSTR szFile,
|
|
CLSID FAR *pclsidHandler,
|
|
AVISAVECALLBACK lpfnCallback,
|
|
int nStreams,
|
|
PAVISTREAM FAR * ppavi,
|
|
LPAVICOMPRESSOPTIONS FAR * plpOptions)
|
|
{
|
|
return E_FAIL;
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
|
|
/*
|
|
* @doc EXTERNAL AVIStreamTimeToSampleNoClip
|
|
|
|
* @api LONG | AVIStreamTimeToSampleNoClip | Converts from milliseconds to
|
|
* samples. It is different from the regular API in that 1) it doesn't
|
|
* clip to the size of the stream, and will return "theoretical" positions
|
|
* past the start or end, and 2) it will always round UP because that's
|
|
* what we want.
|
|
|
|
* @parm PAVISTREAM | pavi | Specifies a handle to an open stream.
|
|
|
|
* @parm LONG | lTime | Specifies the time in milliseconds.
|
|
|
|
* @devnote Currently, this doesn't call a handler function at all.
|
|
|
|
* @comm Samples typically correspond to audio samples or video frames.
|
|
* Other stream types might support different formats than these.
|
|
|
|
* @rdesc Returns the converted time, or -1 on error.
|
|
|
|
* @xref AVIStreamSampleToTime
|
|
|
|
*/
|
|
STDAPI_(LONG) AVIStreamTimeToSampleNoClip (PAVISTREAM pavi, LONG lTime)
|
|
{
|
|
AVISTREAMINFOW avistream;
|
|
HRESULT hr;
|
|
LONG lSample;
|
|
|
|
// Invalid time
|
|
if (lTime < 0)
|
|
return -1;
|
|
|
|
hr = pavi->lpVtbl->Info(pavi, &avistream, sizeof(avistream));
|
|
|
|
if (hr != NOERROR || avistream.dwScale == 0) {
|
|
DPF("Error in AVIStreamTimeToSample!\n");
|
|
return lTime;
|
|
}
|
|
|
|
// This is likely to overflow if we're not careful for long AVIs
|
|
// so keep the 1000 inside the brackets.
|
|
lSample = muldivru32(lTime, avistream.dwRate, avistream.dwScale * 1000);
|
|
|
|
return lSample;
|
|
}
|
|
|
|
// Converts from samples to milliseconds.
|
|
// It is different from the regular API in that 1) it doesn't
|
|
// clip to the size of the stream, and will return "theoretical" positions
|
|
// past the start or end, and 2) it will always round DOWN because that's
|
|
// what we want.
|
|
STDAPI_(LONG) AVIStreamSampleToTimeNoClip (PAVISTREAM pavi, LONG lSample)
|
|
{
|
|
AVISTREAMINFOW avistream;
|
|
HRESULT hr;
|
|
|
|
hr = pavi->lpVtbl->Info(pavi, &avistream, sizeof(avistream));
|
|
|
|
if (hr != NOERROR || avistream.dwRate == 0) {
|
|
DPF("Error in AVIStreamSampleToTime!\n");
|
|
return lSample;
|
|
}
|
|
|
|
// lSample * 1000 would overflow too easily
|
|
return muldivrd32(lSample, avistream.dwScale * 1000, avistream.dwRate);
|
|
}
|
|
|
|
|
|
#define AVIStreamSampleToSampleNoClip(pavi1, pavi2, l) \
|
|
AVIStreamTimeToSampleNoClip(pavi1,AVIStreamSampleToTimeNoClip(pavi2, l))
|
|
|
|
|
|
|
|
/*
|
|
* @doc EXTERNAL AVISaveV
|
|
|
|
* @api LONG | AVISaveV | This function is used to save an AVI file.
|
|
|
|
* @parm LPCTSTR | szFile | Specifies a zero-terminated string
|
|
* containing the name of the file to save.
|
|
|
|
* @parm CLSID FAR * | pclsidHandler | Specifies a pointer to the
|
|
* file handler used to write the file. The file will
|
|
* be created by calling <f AVIFileOpen> using this handler. If
|
|
* a handler is not specified, a default one is selected based upon
|
|
* the file extension.
|
|
|
|
* @parm AVISAVECALLBACK | lpfnCallback | Specifies a pointer to a callback
|
|
* function used to display status information and let the use
|
|
* cancel the save operation.
|
|
|
|
* @parm int | nStreams | Specifies the number of streams to save.
|
|
|
|
* @parm PAVISTREAM FAR * | ppavi | Specifies a pointer to an
|
|
* array of <t PAVISTREAM> pointers. The array uses one pointer
|
|
* for each stream.
|
|
|
|
* @parm LPAVICOMPRESSOPTIONS FAR * | plpOptions | Specifies a pointer
|
|
* to an array of <t LPAVICOMPRESSOPTIONS> pointers. The
|
|
* uses one pointer for each stream.
|
|
|
|
* @rdesc Returns AVIERR_OK on success, an error code otherwise.
|
|
|
|
* @comm This function is equivalent to <f AVISave> except
|
|
* the streams are passed in an array instead of as a
|
|
* variable number of arguments. (<f AVISaveV> is to <f AVISave>
|
|
* as <f wvsprintf> is to <f wsprintf>.)
|
|
|
|
* @xref <f AVISave> <f AVISaveOptions>
|
|
|
|
*/
|
|
STDAPI AVISaveV(LPCTSTR szFile,
|
|
CLSID FAR *pclsidHandler,
|
|
AVISAVECALLBACK lpfnCallback,
|
|
int nStreams,
|
|
PAVISTREAM FAR * ppavi,
|
|
LPAVICOMPRESSOPTIONS FAR * plpOptions)
|
|
{
|
|
int stream;
|
|
MainAVIHeader hdrNew;
|
|
PAVIFILE pfilesave = 0;
|
|
HRESULT hr;
|
|
AVISTREAMINFOW strhdr;
|
|
AVIFILEINFOW finfo;
|
|
LONG cbFormat;
|
|
DWORD dwSamplesRead;
|
|
LPVOID lpBuffer = 0;
|
|
DWORD dwBufferSize;
|
|
LONG l;
|
|
DWORD dwSize;
|
|
DWORD dwFlags;
|
|
WORD cktype;
|
|
LPBITMAPINFOHEADER lpbi;
|
|
DWORD dwInterleaveEvery = 0;
|
|
|
|
#define MAXSTREAMS 64
|
|
|
|
int iVideoStream = -1;
|
|
PAVISTREAM apavi[MAXSTREAMS];
|
|
PAVISTREAM apaviNew[MAXSTREAMS];
|
|
LONG lDone[MAXSTREAMS];
|
|
LONG lInterval;
|
|
|
|
|
|
if (nStreams > MAXSTREAMS)
|
|
return ResultFromScode(AVIERR_INTERNAL);
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
apavi[stream] = NULL;
|
|
apaviNew[stream] = NULL;
|
|
}
|
|
|
|
|
|
// Open file and write out the main header
|
|
|
|
DPF("Creating new file\n");
|
|
|
|
hr = AVIFileOpen(&pfilesave, szFile, OF_CREATE | OF_WRITE | OF_SHARE_EXCLUSIVE, pclsidHandler);
|
|
if (hr != 0)
|
|
goto Error;
|
|
|
|
AVIFileInfoW(pfilesave, &finfo, sizeof(finfo));
|
|
|
|
DPF("Creating compressed streams\n");
|
|
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
|
|
#ifdef USE_ISVALIDINTERFACE
|
|
if (!IsValidInterface(ppavi[stream])) {
|
|
hr = ResultFromScode(AVIERR_INTERNAL);
|
|
goto Error;
|
|
}
|
|
#endif
|
|
|
|
hr = AVIStreamInfoW(ppavi[stream], &strhdr, sizeof(strhdr));
|
|
|
|
if (hr != AVIERR_OK) {
|
|
DPF("Error from AVIStreamInfo!\n");
|
|
goto Error;
|
|
}
|
|
|
|
// Find the video stream....
|
|
if (strhdr.fccType == streamtypeVIDEO) {
|
|
if (iVideoStream < 0) {
|
|
iVideoStream = stream;
|
|
}
|
|
// Allow interleaving for any other type of stream
|
|
} else {
|
|
if (dwInterleaveEvery == 0) {
|
|
// Should the interleave factor be in the options at all?
|
|
if (plpOptions && plpOptions[stream] &&
|
|
plpOptions[stream]->dwFlags & AVICOMPRESSF_INTERLEAVE)
|
|
dwInterleaveEvery = plpOptions[stream]->dwInterleaveEvery;
|
|
}
|
|
}
|
|
|
|
apavi[stream] = NULL;
|
|
|
|
if (plpOptions && plpOptions[stream] &&
|
|
(plpOptions[stream]->fccHandler ||
|
|
plpOptions[stream]->lpFormat)) {
|
|
DWORD dwKeyFrameEvery = plpOptions[stream]->dwKeyFrameEvery;
|
|
|
|
if (finfo.dwCaps & AVIFILECAPS_ALLKEYFRAMES)
|
|
plpOptions[stream]->dwKeyFrameEvery = 1;
|
|
|
|
// If they've given compression options for this stream,
|
|
// use them....
|
|
hr = AVIMakeCompressedStream(&apavi[stream],
|
|
ppavi[stream],
|
|
plpOptions[stream],
|
|
NULL);
|
|
|
|
plpOptions[stream]->dwKeyFrameEvery = dwKeyFrameEvery;
|
|
|
|
if (hr != 0) {
|
|
DPF("AVISave: Failed to create compressed stream!\n");
|
|
apavi[stream] = NULL;
|
|
goto Error; // !!!
|
|
} else {
|
|
hr = AVIStreamInfoW(apavi[stream], &strhdr, sizeof(strhdr));
|
|
if (hr != 0) {
|
|
DPF("AVISave: Failed to create compressed stream!\n");
|
|
AVIStreamClose(apavi[stream]);
|
|
apavi[stream] = NULL;
|
|
goto Error; // !!!
|
|
}
|
|
}
|
|
}
|
|
|
|
if (apavi[stream] == NULL) {
|
|
// otherwise just copy the stream over....
|
|
apavi[stream] = ppavi[stream];
|
|
AVIStreamAddRef(apavi[stream]);
|
|
}
|
|
|
|
lDone[stream] = AVIStreamStart(apavi[stream]);
|
|
}
|
|
|
|
// Put the video stream first, so interleaving will work.
|
|
// !!!
|
|
if (iVideoStream > 0) {
|
|
PAVISTREAM p;
|
|
|
|
p = apavi[iVideoStream];
|
|
apavi[iVideoStream] = apavi[0];
|
|
apavi[0] = p;
|
|
iVideoStream = 0;
|
|
}
|
|
|
|
if (lpfnCallback == NULL)
|
|
lpfnCallback = DummySaveCallback;
|
|
|
|
/* pick a good buffer size and go for it.... */
|
|
dwBufferSize = 32768L;
|
|
|
|
lpBuffer = GlobalAllocPtr(GMEM_MOVEABLE, dwBufferSize);
|
|
if (!lpBuffer) {
|
|
hr = ResultFromScode(AVIERR_MEMORY);
|
|
goto Error;
|
|
}
|
|
|
|
|
|
// Construct AVI file header
|
|
|
|
AVIStreamInfoW(apavi[0], &strhdr, sizeof(strhdr));
|
|
hdrNew.dwMicroSecPerFrame = muldiv32(1000000L, strhdr.dwScale, strhdr.dwRate);
|
|
hdrNew.dwMaxBytesPerSec = 0;
|
|
hdrNew.dwPaddingGranularity = 0;
|
|
|
|
hdrNew.dwFlags = AVIF_HASINDEX;
|
|
hdrNew.dwFlags &= ~(AVIF_ISINTERLEAVED | AVIF_WASCAPTUREFILE |
|
|
AVIF_MUSTUSEINDEX);
|
|
|
|
hdrNew.dwTotalFrames = strhdr.dwLength;
|
|
hdrNew.dwInitialFrames = 0;
|
|
|
|
hdrNew.dwStreams = nStreams;
|
|
hdrNew.dwSuggestedBufferSize = 32768;
|
|
|
|
if (iVideoStream >= 0) {
|
|
cbFormat = dwBufferSize;
|
|
hr = AVIStreamReadFormat(apavi[iVideoStream],
|
|
AVIStreamStart(apavi[iVideoStream]),
|
|
lpBuffer,
|
|
&cbFormat);
|
|
|
|
if (cbFormat < sizeof(BITMAPINFOHEADER)) {
|
|
hr = ResultFromScode(AVIERR_INTERNAL);
|
|
}
|
|
|
|
if (hr != 0) {
|
|
DPF("AVISave: Error from initial ReadFormat!\n");
|
|
goto Error;
|
|
}
|
|
|
|
lpbi = (LPBITMAPINFOHEADER) lpBuffer;
|
|
|
|
hdrNew.dwWidth = lpbi->biWidth;
|
|
hdrNew.dwHeight = lpbi->biHeight;
|
|
lInterval = 1;
|
|
} else {
|
|
hdrNew.dwWidth = 0;
|
|
hdrNew.dwHeight = 0;
|
|
lInterval = AVIStreamTimeToSample(apavi[0], 500);
|
|
}
|
|
|
|
|
|
// Loop through streams and write out stream header
|
|
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
// DPF2("Making stream %d header LIST\n", stream);
|
|
|
|
AVIStreamInfoW(apavi[stream], &strhdr, sizeof(strhdr));
|
|
strhdr.dwInitialFrames = 0;
|
|
|
|
// If we're interleaving, skew all streams but video by 3/4 of a second
|
|
// so their renderers won't starve
|
|
if (dwInterleaveEvery > 0 && stream > 0) {
|
|
if (strhdr.fccType != streamtypeVIDEO) {
|
|
strhdr.dwInitialFrames = AVIStreamTimeToSample(apavi[0], 750);
|
|
DPF("Stream %d has %lu initial frames\n", stream, strhdr.dwInitialFrames);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Get stream format and write it out
|
|
|
|
cbFormat = dwBufferSize;
|
|
hr = AVIStreamReadFormat(apavi[stream], AVIStreamStart(apavi[stream]),
|
|
lpBuffer, &cbFormat);
|
|
if (hr != AVIERR_OK)
|
|
goto Error;
|
|
|
|
// !!! Overflow?
|
|
if (!cbFormat) {
|
|
// !!!
|
|
}
|
|
|
|
hr = AVIFileCreateStreamW(pfilesave, &apaviNew[stream], &strhdr);
|
|
|
|
#if 0
|
|
if (hr != AVIERR_OK || apaviNew[stream] == NULL)
|
|
goto Error;
|
|
#else
|
|
// If we can't make a stream, continue with the other streams....
|
|
if (hr != AVIERR_OK || apaviNew[stream] == NULL) {
|
|
int i;
|
|
|
|
DPF("AVISave: Couldn't create stream in new file!\n");
|
|
AVIStreamClose(apavi[stream]);
|
|
|
|
for (i = stream + 1; i < nStreams; i++) {
|
|
apavi[stream] = apavi[stream + 1];
|
|
}
|
|
--nStreams;
|
|
--stream;
|
|
continue;
|
|
}
|
|
#endif
|
|
|
|
hr = AVIStreamSetFormat(apaviNew[stream], 0, lpBuffer, cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
DPF("Initial set format failed!\n");
|
|
goto Error;
|
|
}
|
|
|
|
cbFormat = dwBufferSize;
|
|
hr = AVIStreamReadData(apavi[stream], ckidSTREAMHANDLERDATA,
|
|
lpBuffer, &cbFormat);
|
|
// !!! overflow?
|
|
|
|
if (hr == AVIERR_OK && cbFormat) {
|
|
/*
|
|
** Make the stream Data data chunk
|
|
*/
|
|
// DPF2("Making stream %ld Data data chunk\n", stream);
|
|
hr = AVIStreamWriteData(apaviNew[stream], ckidSTREAMHANDLERDATA,
|
|
lpBuffer, cbFormat);
|
|
if (hr != AVIERR_OK)
|
|
goto Error;
|
|
}
|
|
|
|
if (strhdr.dwInitialFrames > hdrNew.dwInitialFrames)
|
|
hdrNew.dwInitialFrames = strhdr.dwInitialFrames;
|
|
|
|
// This will round UP to the nearest video sample, which is what we want
|
|
dwSize = AVIStreamSampleToSampleNoClip(apavi[0],
|
|
apavi[stream],
|
|
AVIStreamLength(apavi[stream]));
|
|
|
|
if (dwSize > hdrNew.dwTotalFrames)
|
|
hdrNew.dwTotalFrames = dwSize;
|
|
|
|
// !!! Should call ReadExtra and WriteExtra to move over information!
|
|
}
|
|
|
|
if (nStreams <= 0) {
|
|
DPF("No streams at all accepted by the file!\n");
|
|
goto Error;
|
|
}
|
|
|
|
|
|
// We've written the header. Now, there are two possibilities:
|
|
|
|
// 1.) File is interleaved. We loop in time from beginning to end,
|
|
// then loop through the streams and write out any data for the
|
|
// current time.
|
|
|
|
// 2.) File is not interleaved. We loop through the streams and
|
|
// write each one out separately.
|
|
|
|
|
|
if (dwInterleaveEvery > 0) {
|
|
DPF("Saving interleaved: factor = %lu, intial = %lu, total = %lu\n", dwInterleaveEvery, hdrNew.dwInitialFrames, hdrNew.dwTotalFrames);
|
|
|
|
if (dwInterleaveEvery == 1) {
|
|
hdrNew.dwFlags |= AVIF_ISINTERLEAVED;
|
|
AVIFileEndRecord(pfilesave); // Make first record....
|
|
}
|
|
|
|
|
|
// Interleaved case: loop from start to end...
|
|
|
|
for (l = - (LONG) hdrNew.dwInitialFrames;
|
|
l < (LONG) hdrNew.dwTotalFrames;
|
|
l += lInterval) {
|
|
|
|
//DPF2("Writing data for frame #%ld/%lu\n", l, hdrNew.dwTotalFrames);
|
|
|
|
|
|
// Loop through all of the streams to see what needs to be
|
|
// done at this time...
|
|
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
LONG lPos;
|
|
LONG lPosNext;
|
|
|
|
LONG lStart;
|
|
LONG lEnd;
|
|
|
|
hr = AVIStreamInfoW(apaviNew[stream], &strhdr, sizeof(strhdr));
|
|
|
|
if (hr != AVIERR_OK)
|
|
goto Error;
|
|
|
|
if (l < - (LONG) strhdr.dwInitialFrames)
|
|
continue;
|
|
|
|
// !!! Better use of TWOCCs...
|
|
if (strhdr.fccType == streamtypeAUDIO)
|
|
cktype = cktypeWAVEbytes;
|
|
else if (strhdr.fccType == streamtypeVIDEO) {
|
|
if (strhdr.fccHandler == comptypeDIB)
|
|
cktype = cktypeDIBbits;
|
|
else
|
|
cktype = cktypeDIBcompressed;
|
|
} else
|
|
cktype = aviTWOCC('x', 'x');
|
|
|
|
|
|
// Time is based on the first stream:
|
|
// Right now, we want to write out any data in the current
|
|
// stream that lines up between time <l> and <l+1> in the
|
|
// first stream.
|
|
|
|
lPos = l + strhdr.dwInitialFrames;
|
|
lPosNext = lPos + lInterval;
|
|
|
|
lStart = lDone[stream];
|
|
|
|
if (l >= (LONG) hdrNew.dwTotalFrames - lInterval) {
|
|
// If this is going to be the last time through the
|
|
// interleave loop, make sure everything gets written.
|
|
lEnd = AVIStreamEnd(apavi[stream]);
|
|
} else {
|
|
|
|
// Complication: to make other data come in bigger chunks,
|
|
// we only write it out every once in a while.
|
|
// We will interleave any non-video stream, not just audio.
|
|
if (strhdr.fccType != streamtypeVIDEO && stream != 0) {
|
|
if ((lPos % dwInterleaveEvery) != 0)
|
|
continue;
|
|
|
|
lPosNext = lPos + dwInterleaveEvery;
|
|
}
|
|
|
|
if (stream != 0) {
|
|
|
|
// Figure out the data for this stream that needs to be
|
|
// written this time. Round UP so that the data goes
|
|
// early in the file so the stream won't starve.
|
|
|
|
lEnd = AVIStreamSampleToSampleNoClip(apavi[stream],
|
|
apavi[0], lPosNext);
|
|
} else {
|
|
lEnd = lPosNext;
|
|
}
|
|
|
|
lEnd = min(lEnd, AVIStreamEnd(apavi[stream]));
|
|
}
|
|
|
|
lDone[stream] = lEnd;
|
|
|
|
//DPF2(" Stream %d: (%ld - %ld)\n", stream, lStart, lEnd);
|
|
|
|
|
|
// Loop until we've read all we want.
|
|
|
|
while (lEnd > lStart) {
|
|
// !!! Right here, we should call AVIStreamGetFormat
|
|
// and then call AVIStreamSetFormat on the new
|
|
// streams.
|
|
// !!! Whose job is it to tell if the format has really
|
|
// changed?
|
|
cbFormat = dwBufferSize;
|
|
hr = AVIStreamReadFormat(apavi[stream],
|
|
lStart,
|
|
lpBuffer,
|
|
&cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
DPF("AVIStreamReadFormat failed!\n");
|
|
goto Error;
|
|
}
|
|
|
|
hr = AVIStreamSetFormat(apaviNew[stream],
|
|
lStart,
|
|
lpBuffer,
|
|
cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
// !!! Oh, well: we couldn't write the palette change...
|
|
DPF("AVIStreamSetFormat failed!\n");
|
|
}
|
|
|
|
ReadAgain0:
|
|
cbFormat = dwBufferSize;
|
|
dwSamplesRead = 0;
|
|
|
|
hr = AVIStreamRead(apavi[stream], lStart,
|
|
lEnd - lStart,
|
|
lpBuffer, dwBufferSize,
|
|
&dwSize, &dwSamplesRead);
|
|
|
|
if (// dwSamplesRead == 0 &&
|
|
(GetScode(hr) == AVIERR_BUFFERTOOSMALL)) {
|
|
|
|
// The frame didn't fit in our buffer.
|
|
// Make a bigger buffer.
|
|
|
|
dwBufferSize *= 2;
|
|
DPF("Resizing buffer to be %lx bytes\n", dwBufferSize);
|
|
lpBuffer = GlobalReAllocPtr(lpBuffer, dwBufferSize, GMEM_MOVEABLE);
|
|
if (lpBuffer)
|
|
goto ReadAgain0;
|
|
|
|
hr = ResultFromScode(AVIERR_MEMORY);
|
|
}
|
|
|
|
if (hr != 0) {
|
|
DPF("AVISave: Error %08lx reading stream %d, position %ld!\n", (DWORD) hr, stream, lStart);
|
|
goto Error;
|
|
}
|
|
|
|
dwFlags = 0;
|
|
|
|
if (AVIStreamFindSample(apavi[stream], lStart,
|
|
FIND_KEY | FIND_PREV) == lStart)
|
|
dwFlags |= AVIIF_KEYFRAME;
|
|
|
|
hr = AVIStreamWrite(apaviNew[stream],
|
|
-1, dwSamplesRead,
|
|
lpBuffer, dwSize,
|
|
// cktype, // !!!
|
|
dwFlags, 0L, 0L);
|
|
|
|
if (hr != AVIERR_OK)
|
|
goto Error;
|
|
|
|
lStart += dwSamplesRead;
|
|
|
|
if ((LONG) dwSamplesRead != lEnd - lStart) {
|
|
// DPF2(" %lu of %lu actually read....\n", dwSamplesRead, lEnd - lStart);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
|
|
// Mark the end of the frame, in case we're writing out
|
|
// the "strict" interleaved format with LIST 'rec' chunks...
|
|
|
|
if (dwInterleaveEvery == 1) {
|
|
hr = AVIFileEndRecord(pfilesave);
|
|
if (hr != AVIERR_OK) {
|
|
DPF("AVISave: Error from EndRecord!\n");
|
|
goto Error;
|
|
}
|
|
}
|
|
|
|
// Give the application a chance to update status and the user
|
|
// a chance to abort...
|
|
if (lpfnCallback((int)
|
|
muldiv32(l + hdrNew.dwInitialFrames, 100,
|
|
hdrNew.dwInitialFrames +
|
|
hdrNew.dwTotalFrames))) {
|
|
hr = ResultFromScode(AVIERR_USERABORT);
|
|
DPF("AVISave: Aborted!\n");
|
|
goto Error;
|
|
}
|
|
}
|
|
} else {
|
|
|
|
// Non-interleaved case: loop through the streams and write
|
|
// each one out by itself.
|
|
|
|
DPF("Saving non-interleaved.\n");
|
|
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
if (lpfnCallback(MulDiv(stream, 100, nStreams))) {
|
|
hr = ResultFromScode(AVIERR_USERABORT);
|
|
goto Error;
|
|
}
|
|
|
|
AVIStreamInfoW(apavi[stream], &strhdr, sizeof(strhdr));
|
|
|
|
DPF("Saving stream %d: start=%lx, len=%lx\n", stream, strhdr.dwStart, strhdr.dwLength);
|
|
|
|
// !!! Need better cktype handling....
|
|
if (strhdr.fccType == streamtypeAUDIO)
|
|
cktype = cktypeWAVEbytes;
|
|
else if (strhdr.fccType == streamtypeVIDEO) {
|
|
if (strhdr.fccHandler == comptypeDIB)
|
|
cktype = cktypeDIBbits;
|
|
else
|
|
cktype = cktypeDIBcompressed;
|
|
} else
|
|
cktype = aviTWOCC('x', 'x');
|
|
|
|
|
|
// As usual, there are two possibilities:
|
|
|
|
// 1.) "wave-like" data, where lots of samples can be in
|
|
// a single chunk. In this case, we write out big chunks
|
|
// with many samples at a time.
|
|
|
|
// 2.) "video-like" data, where each sample is a different
|
|
// size, and thus each must be written individually.
|
|
|
|
if (strhdr.dwSampleSize != 0) {
|
|
/* It's wave-like data: lots of samples per chunk */
|
|
|
|
l = strhdr.dwStart;
|
|
while (l < (LONG) strhdr.dwLength) {
|
|
DWORD dwRead;
|
|
|
|
// Make the format of the new stream
|
|
// match the old one at every point....
|
|
|
|
// !!! Whose job is it to tell if the format has really
|
|
// changed?
|
|
cbFormat = dwBufferSize;
|
|
hr = AVIStreamReadFormat(apavi[stream],
|
|
l,
|
|
lpBuffer,
|
|
&cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
DPF("AVIStreamReadFormat failed!\n");
|
|
goto Error;
|
|
}
|
|
|
|
hr = AVIStreamSetFormat(apaviNew[stream],
|
|
l,
|
|
lpBuffer,
|
|
cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
DPF("AVIStreamSetFormat failed!\n");
|
|
// !!! Oh, well: we couldn't write the palette change...
|
|
}
|
|
|
|
|
|
|
|
// Read some data...
|
|
|
|
ReadAgain1:
|
|
dwSize = dwBufferSize;
|
|
dwSamplesRead = 0;
|
|
dwRead = min(dwBufferSize / strhdr.dwSampleSize,
|
|
strhdr.dwLength - (DWORD) l);
|
|
|
|
hr = AVIStreamRead(apavi[stream], l, dwRead,
|
|
lpBuffer, dwBufferSize,
|
|
&dwSize, &dwSamplesRead);
|
|
|
|
if (// dwSamplesRead == 0 &&
|
|
(GetScode(hr) == AVIERR_BUFFERTOOSMALL)) {
|
|
|
|
// The frame didn't fit in our buffer.
|
|
// Make a bigger buffer.
|
|
|
|
dwBufferSize *= 2;
|
|
lpBuffer = GlobalReAllocPtr(lpBuffer, dwBufferSize, GMEM_MOVEABLE);
|
|
if (lpBuffer)
|
|
goto ReadAgain1;
|
|
}
|
|
|
|
// !!! Check if format has changed
|
|
|
|
dwFlags = 0; // !!! KEYFRAME?
|
|
|
|
DPF("Save: Read %lx/%lx samples at %lx\n", dwSamplesRead, dwRead, l);
|
|
|
|
if (hr != AVIERR_OK) {
|
|
DPF("Save: Read failed! (%08lx) pos=%lx, len=%lx\n", (DWORD) hr, l, dwRead);
|
|
|
|
goto Error;
|
|
}
|
|
|
|
if (dwSamplesRead == 0) {
|
|
DPF("Ack: Read zero samples!");
|
|
|
|
if (l + 1 == (LONG) strhdr.dwLength) {
|
|
DPF("Pretending it's OK, since this was the last one....");
|
|
break;
|
|
}
|
|
|
|
hr = ResultFromScode(AVIERR_FILEREAD);
|
|
goto Error;
|
|
}
|
|
|
|
l += dwSamplesRead;
|
|
|
|
|
|
// Write the data out...
|
|
|
|
hr = AVIStreamWrite(apaviNew[stream],
|
|
-1, dwSamplesRead,
|
|
lpBuffer, dwSize,
|
|
// !!! cktype, // !!!TWOCCFromFOURCC(ckid),
|
|
dwFlags, 0L, 0L);
|
|
|
|
if (hr != AVIERR_OK) {
|
|
DPF("AVIStreamWrite failed! (%08lx)\n", (DWORD) hr);
|
|
|
|
goto Error;
|
|
}
|
|
|
|
if (lpfnCallback(MulDiv(stream, 100, nStreams) +
|
|
(int) muldiv32(l, 100,
|
|
nStreams * strhdr.dwLength))) {
|
|
hr = ResultFromScode(AVIERR_USERABORT);
|
|
goto Error;
|
|
}
|
|
}
|
|
} else {
|
|
/* It's video-like data: one sample (frame) per chunk */
|
|
|
|
for (l = strhdr.dwStart;
|
|
l < (LONG) strhdr.dwLength;
|
|
l++) {
|
|
// !!! Right here, we should call AVIStreamGetFormat
|
|
// and then call AVIStreamSetFormat on the new
|
|
// streams.
|
|
// !!! Whose job is it to tell if the format has really
|
|
// changed?
|
|
|
|
cbFormat = dwBufferSize;
|
|
hr = AVIStreamReadFormat(apavi[stream],
|
|
l,
|
|
lpBuffer,
|
|
&cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
DPF("AVIStreamReadFormat failed!\n");
|
|
goto Error;
|
|
}
|
|
|
|
hr = AVIStreamSetFormat(apaviNew[stream],
|
|
l,
|
|
lpBuffer,
|
|
cbFormat);
|
|
if (hr != AVIERR_OK) {
|
|
// !!! Oh, well: we couldn't write the palette change...
|
|
DPF("AVIStreamSetFormat failed!\n");
|
|
}
|
|
|
|
|
|
ReadAgain:
|
|
dwSize = dwBufferSize;
|
|
/* Write out a single frame.... */
|
|
dwSamplesRead = 0;
|
|
hr = AVIStreamRead(apavi[stream], l, 1,
|
|
lpBuffer, dwBufferSize,
|
|
&dwSize, &dwSamplesRead);
|
|
|
|
// !!! Check if format has changed (palette change)
|
|
|
|
if (// dwSamplesRead == 0 &&
|
|
(GetScode(hr) == AVIERR_BUFFERTOOSMALL)) {
|
|
|
|
// The frame didn't fit in our buffer.
|
|
// Make a bigger buffer.
|
|
|
|
dwBufferSize *= 2;
|
|
lpBuffer = GlobalReAllocPtr(lpBuffer, dwBufferSize, GMEM_MOVEABLE);
|
|
if (lpBuffer)
|
|
goto ReadAgain;
|
|
}
|
|
|
|
if (dwSamplesRead != 1 || hr != 0) {
|
|
hr = ResultFromScode(AVIERR_FILEREAD);
|
|
goto Error;
|
|
}
|
|
|
|
dwFlags = 0; // !!!!
|
|
|
|
|
|
// Check whether this should be marked a key frame.
|
|
|
|
// !!! shouldn't this be returned from AVIStreamRead()?
|
|
|
|
if (AVIStreamFindSample(apavi[stream], l,
|
|
FIND_KEY | FIND_PREV) == l)
|
|
dwFlags |= AVIIF_KEYFRAME;
|
|
|
|
|
|
// Write the chunk out.
|
|
|
|
hr = AVIStreamWrite(apaviNew[stream],
|
|
-1, dwSamplesRead,
|
|
lpBuffer, dwSize,
|
|
// !!! cktype, // !!!TWOCCFromFOURCC(ckid),
|
|
dwFlags, 0L, 0L);
|
|
|
|
if (hr != AVIERR_OK)
|
|
goto Error;
|
|
|
|
|
|
// Video frames can be big, so call back every time.
|
|
|
|
if (lpfnCallback(MulDiv(stream, 100, nStreams) +
|
|
(int) muldiv32(l, 100, nStreams * strhdr.dwLength))) {
|
|
hr = ResultFromScode(AVIERR_USERABORT);
|
|
goto Error;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Error:
|
|
|
|
// We're done, one way or another.
|
|
|
|
|
|
/* Free buffer */
|
|
if (lpBuffer) {
|
|
GlobalFreePtr(lpBuffer);
|
|
}
|
|
|
|
// If everything's OK so far, finish writing the file.
|
|
// Close the file, free resources associated with writing it.
|
|
if (pfilesave) {
|
|
// Release all of our new streams
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
if (apaviNew[stream])
|
|
AVIStreamClose(apaviNew[stream]);
|
|
}
|
|
|
|
if (hr != AVIERR_OK)
|
|
AVIFileClose(pfilesave);
|
|
else {
|
|
// !!! ACK: AVIFileClose doesn't return an error! How do I tell
|
|
// if it worked?
|
|
// !!! does this mean I need a Flush() call?
|
|
/* hr = */ AVIFileClose(pfilesave);
|
|
}
|
|
|
|
}
|
|
|
|
// Release all of our streams
|
|
for (stream = 0; stream < nStreams; stream++) {
|
|
if (apavi[stream])
|
|
AVIStreamClose(apavi[stream]);
|
|
}
|
|
|
|
if (hr != 0) {
|
|
DPF("AVISave: Returning error %08lx\n", (DWORD) hr);
|
|
}
|
|
|
|
return hr;
|
|
}
|