From 3cea7e0c3d1ad8964377d3ac216d6ac536a03bf7 Mon Sep 17 00:00:00 2001 From: myT <> Date: Mon, 27 Nov 2023 02:53:15 +0100 Subject: [PATCH] added FFmpeg support for A/V capture compression --- changelog.txt | 10 + code/client/cl_avi.cpp | 1049 +++++++++++++++++++------------------- code/client/cl_cgame.cpp | 2 +- code/client/cl_main.cpp | 43 +- code/client/client.h | 14 +- code/qcommon/files.cpp | 33 +- code/qcommon/qcommon.h | 4 + code/win32/win_main.cpp | 6 + 8 files changed, 631 insertions(+), 530 deletions(-) diff --git a/changelog.txt b/changelog.txt index 682c8df..a0d1483 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,16 @@ See the end of this file for known issues. DD Mmm YY - 1.54 +add: FFmpeg piping support for compression of audio/video captures + cl_ffmpeg <0|1> (default: 0) uses FFmpeg instead of writing raw .avi files + cl_ffmpegCommand are the FFmpeg command-line options for the output file + cl_ffmpegExePath (default: "ffmpeg") is the path of the FFmpeg executable to use + note that the path cannot contain spaces on Windows + cl_ffmpegOutPath (default: "") is the video output directory + leave empty to write to cpma/videos as with .avi files + cl_ffmpegOutExt (default: "mp4") is the output file extension + cl_ffmpegLog <0|1> (default: 0) enables the creation of 1 log file per capture + add: togglegui and toggleguiinput to toggle (the mouse input of) the built-in GUI system add: key binds starting with "keycatchgui" always take precedence over everything else diff --git a/code/client/cl_avi.cpp b/code/client/cl_avi.cpp index 70f7b02..2d53239 100644 --- a/code/client/cl_avi.cpp +++ b/code/client/cl_avi.cpp @@ -1,6 +1,7 @@ /* =========================================================================== Copyright (C) 2005-2006 Tim Angus +Copyright (C) 2018-2023 Gian 'myT' Schellenbaum This file is part of Quake III Arena source code. @@ -19,654 +20,658 @@ along with Quake III Arena source code; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA =========================================================================== */ +// captures audio/video to an AVI (raw BGR or MJPEG, PCM) file or pipes raw data to FFmpeg #include "client.h" #include "snd_local.h" + #define INDEX_FILE_EXTENSION ".index.dat" #define MAX_RIFF_CHUNKS 16 -typedef struct audioFormat_s -{ - int rate; - int format; - int channels; - int bits; +#define PCM_BUFFER_SIZE 44100 - int sampleSize; - int totalBytes; -} audioFormat_t; +#define MAX_AVI_BUFFER 4096 -typedef struct aviFileData_s -{ - qbool fileOpen; - fileHandle_t f; - char fileName[ 256 + 64 ]; // extra room for the "videos/" prefix and name suffix - int fileSize; - int moviOffset; - int moviSize; +// flags for the "AVIH" RIFF chunk +#define AVIH_HASINDEX_BIT 0x010 +#define AVIH_ISINTERLEAVED_BIT 0x100 - fileHandle_t idxF; - int numIndices; - int frameRate; - int framePeriod; - int width, height; - int numVideoFrames; - int maxRecordSize; - qbool motionJpeg; +struct audioFormat_t { + int rate; + int format; + int channels; + int bits; - qbool audio; - audioFormat_t a; - int numAudioFrames; + int sampleSize; + int totalBytes; +}; - int chunkStack[ MAX_RIFF_CHUNKS ]; - int chunkStackTop; +struct aviFileData_t { + qbool isPiped; + qbool isFileOpen; + fileHandle_t mainFile; + char fileName[256 + 64]; // extra room for the "videos/" prefix and name suffix + int fileSize; + int moviOffset; + int moviSize; - byte *cBuffer, *eBuffer; // capture and encoding buffers -} aviFileData_t; + fileHandle_t indexFile; + int numIndices; + + int frameRate; + int framePeriod; + int width; + int height; + int numVideoFrames; + int maxRecordSize; + qbool useMotionJpeg; + + qbool hasAudio; + audioFormat_t audioFormat; + int numAudioFrames; + + int chunkStack[MAX_RIFF_CHUNKS]; + int chunkStackTop; + + byte* captureBuffer; + byte* encodeBuffer; +}; + +enum closeMode_t { + CM_SEQUENCE_COMPLETE, // last in the sequence -> safe to clear sounds etc. + CM_SEQUENCE_INCOMPLETE // NOT last in the sequence -> we'll open a new one +}; -typedef enum closeMode_s -{ - CM_SEQUENCE_COMPLETE, // last in the sequence -> safe to clear sounds etc. - CM_SEQUENCE_INCOMPLETE // NOT last in the sequence -> we'll open a new one -} closeMode_t; static aviFileData_t afd; +static byte buffer[MAX_AVI_BUFFER]; +static int bufferWriteIndex; -#define MAX_AVI_BUFFER 2048 - -static byte buffer[ MAX_AVI_BUFFER ]; -static int bufIndex; static qbool CloseAVI( closeMode_t closeMode ); -/* -=============== -SafeFS_Write -=============== -*/ -static ID_INLINE void SafeFS_Write( const void *buff, int len, fileHandle_t f ) + +static void AbortCapture( const char* message ) { - if( FS_Write( buff, len, f ) < len ) - Com_Error( ERR_DROP, "Failed to write avi file\n" ); + Com_Printf("^1ERROR: %s\n", message); + Cbuf_AddText(clc.newDemoPlayer ? "demo_stopvideo\n" : "stopvideo\n"); } -/* -=============== -WRITE_STRING -=============== -*/ -static ID_INLINE void WRITE_STRING( const char *s ) + +static void Safe_FS_Write( const void* buff, int len, fileHandle_t f ) { - Com_Memcpy( &buffer[ bufIndex ], s, strlen( s ) ); - bufIndex += strlen( s ); + if (f <= 0) { + return; + } + + if (FS_Write(buff, len, f) < len) { + AbortCapture("Failed to write to video file/pipe"); + } } -/* -=============== -WRITE_4BYTES -=============== -*/ -static ID_INLINE void WRITE_4BYTES( int x ) + +static void Safe_FS_Close( fileHandle_t* file ) { - buffer[ bufIndex + 0 ] = (byte)( ( x >> 0 ) & 0xFF ); - buffer[ bufIndex + 1 ] = (byte)( ( x >> 8 ) & 0xFF ); - buffer[ bufIndex + 2 ] = (byte)( ( x >> 16 ) & 0xFF ); - buffer[ bufIndex + 3 ] = (byte)( ( x >> 24 ) & 0xFF ); - bufIndex += 4; + if (*file > 0) { + FS_FCloseFile(*file); + *file = 0; + } } -/* -=============== -WRITE_2BYTES -=============== -*/ -static ID_INLINE void WRITE_2BYTES( int x ) + +static void Safe_Z_Free( byte** memory ) { - buffer[ bufIndex + 0 ] = (byte)( ( x >> 0 ) & 0xFF ); - buffer[ bufIndex + 1 ] = (byte)( ( x >> 8 ) & 0xFF ); - bufIndex += 2; + if (*memory != NULL) { + Z_Free(*memory); + *memory = NULL; + } } -/* -=============== -WRITE_1BYTES -=============== -*/ -static ID_INLINE void WRITE_1BYTES( int x ) + +static void WRITE_STRING( const char* s ) { - buffer[ bufIndex ] = x; - bufIndex += 1; + Com_Memcpy(&buffer[bufferWriteIndex], s, strlen(s)); + bufferWriteIndex += strlen(s); } -/* -=============== -START_CHUNK -=============== -*/ -static ID_INLINE void START_CHUNK( const char *s ) -{ - if( afd.chunkStackTop == MAX_RIFF_CHUNKS ) - { - Com_Error( ERR_DROP, "ERROR: Top of chunkstack breached\n" ); - } - afd.chunkStack[ afd.chunkStackTop ] = bufIndex; - afd.chunkStackTop++; - WRITE_STRING( s ); - WRITE_4BYTES( 0 ); +static void WRITE_4BYTES( int x ) +{ + buffer[bufferWriteIndex + 0] = (byte)((x >> 0) & 0xFF); + buffer[bufferWriteIndex + 1] = (byte)((x >> 8) & 0xFF); + buffer[bufferWriteIndex + 2] = (byte)((x >> 16) & 0xFF); + buffer[bufferWriteIndex + 3] = (byte)((x >> 24) & 0xFF); + bufferWriteIndex += 4; } -/* -=============== -END_CHUNK -=============== -*/ -static ID_INLINE void END_CHUNK( void ) + +static void WRITE_2BYTES( int x ) { - int endIndex = bufIndex; - - if( afd.chunkStackTop <= 0 ) - { - Com_Error( ERR_DROP, "ERROR: Bottom of chunkstack breached\n" ); - } - - afd.chunkStackTop--; - bufIndex = afd.chunkStack[ afd.chunkStackTop ]; - bufIndex += 4; - WRITE_4BYTES( endIndex - bufIndex - 4 ); - bufIndex = endIndex; - bufIndex = PAD( bufIndex, 2 ); + buffer[bufferWriteIndex + 0] = (byte)((x >> 0) & 0xFF); + buffer[bufferWriteIndex + 1] = (byte)((x >> 8) & 0xFF); + bufferWriteIndex += 2; } -/* -=============== -CL_WriteAVIHeader -=============== -*/ -void CL_WriteAVIHeader( void ) + +static void START_CHUNK( const char* s ) { - bufIndex = 0; - afd.chunkStackTop = 0; + if (afd.chunkStackTop == MAX_RIFF_CHUNKS) { + AbortCapture("Top of chunkstack breached"); + return; + } - START_CHUNK( "RIFF" ); - { - WRITE_STRING( "AVI " ); - { - START_CHUNK( "LIST" ); - { - WRITE_STRING( "hdrl" ); - WRITE_STRING( "avih" ); - WRITE_4BYTES( 56 ); //"avih" "chunk" size - WRITE_4BYTES( afd.framePeriod ); //dwMicroSecPerFrame - WRITE_4BYTES( afd.maxRecordSize * - afd.frameRate ); //dwMaxBytesPerSec - WRITE_4BYTES( 0 ); //dwReserved1 - WRITE_4BYTES( 0x110 ); //dwFlags bits HAS_INDEX and IS_INTERLEAVED - WRITE_4BYTES( afd.numVideoFrames ); //dwTotalFrames - WRITE_4BYTES( 0 ); //dwInitialFrame + afd.chunkStack[afd.chunkStackTop] = bufferWriteIndex; + afd.chunkStackTop++; + WRITE_STRING(s); + WRITE_4BYTES(0); +} - if( afd.audio ) //dwStreams - WRITE_4BYTES( 2 ); - else - WRITE_4BYTES( 1 ); - WRITE_4BYTES( afd.maxRecordSize ); //dwSuggestedBufferSize - WRITE_4BYTES( afd.width ); //dwWidth - WRITE_4BYTES( afd.height ); //dwHeight - WRITE_4BYTES( 0 ); //dwReserved[ 0 ] - WRITE_4BYTES( 0 ); //dwReserved[ 1 ] - WRITE_4BYTES( 0 ); //dwReserved[ 2 ] - WRITE_4BYTES( 0 ); //dwReserved[ 3 ] +static void END_CHUNK() +{ + if (afd.chunkStackTop <= 0) { + AbortCapture("Bottom of chunkstack breached"); + return; + } - START_CHUNK( "LIST" ); - { - WRITE_STRING( "strl" ); - WRITE_STRING( "strh" ); - WRITE_4BYTES( 56 ); //"strh" "chunk" size - WRITE_STRING( "vids" ); //fccType + const int endIndex = bufferWriteIndex; + afd.chunkStackTop--; + bufferWriteIndex = afd.chunkStack[afd.chunkStackTop]; + bufferWriteIndex += 4; + WRITE_4BYTES(endIndex - bufferWriteIndex - 4); + bufferWriteIndex = endIndex; + bufferWriteIndex = PAD(bufferWriteIndex, 2); +} - if( afd.motionJpeg ) //fccHandler - WRITE_STRING( "MJPG" ); - else - WRITE_4BYTES( 0 ); //raw BGR - WRITE_4BYTES( 0 ); //dwFlags - WRITE_4BYTES( 0 ); //dwPriority - WRITE_4BYTES( 0 ); //dwInitialFrame +void CL_WriteAVIHeader() +{ + bufferWriteIndex = 0; + afd.chunkStackTop = 0; - WRITE_4BYTES( 1 ); //dwTimescale - WRITE_4BYTES( afd.frameRate ); //dwDataRate - WRITE_4BYTES( 0 ); //dwStartTime - WRITE_4BYTES( afd.numVideoFrames ); //dwDataLength + START_CHUNK("RIFF"); + { + WRITE_STRING("AVI "); + { + START_CHUNK("LIST"); + { + WRITE_STRING("hdrl"); + WRITE_STRING("avih"); + WRITE_4BYTES(56); //"avih" "chunk" size + WRITE_4BYTES(afd.framePeriod); //dwMicroSecPerFrame + WRITE_4BYTES(afd.maxRecordSize * afd.frameRate); //dwMaxBytesPerSec + WRITE_4BYTES(0); //dwReserved1 + if (afd.isPiped) + WRITE_4BYTES(AVIH_ISINTERLEAVED_BIT); + else + WRITE_4BYTES(AVIH_ISINTERLEAVED_BIT | AVIH_HASINDEX_BIT); + WRITE_4BYTES(afd.numVideoFrames); //dwTotalFrames + WRITE_4BYTES(0); //dwInitialFrame - WRITE_4BYTES( afd.maxRecordSize ); //dwSuggestedBufferSize - WRITE_4BYTES( -1 ); //dwQuality - WRITE_4BYTES( 0 ); //dwSampleSize - WRITE_2BYTES( 0 ); //rcFrame - WRITE_2BYTES( 0 ); //rcFrame - WRITE_2BYTES( afd.width ); //rcFrame - WRITE_2BYTES( afd.height ); //rcFrame + if (afd.hasAudio) //dwStreams + WRITE_4BYTES(2); + else + WRITE_4BYTES(1); - WRITE_STRING( "strf" ); - WRITE_4BYTES( 40 ); //"strf" "chunk" size - WRITE_4BYTES( 40 ); //biSize - WRITE_4BYTES( afd.width ); //biWidth - WRITE_4BYTES( afd.height ); //biHeight - WRITE_2BYTES( 1 ); //biPlanes - WRITE_2BYTES( 24 ); //biBitCount + WRITE_4BYTES(afd.maxRecordSize); //dwSuggestedBufferSize + WRITE_4BYTES(afd.width); //dwWidth + WRITE_4BYTES(afd.height); //dwHeight + WRITE_4BYTES(0); //dwReserved[ 0 ] + WRITE_4BYTES(0); //dwReserved[ 1 ] + WRITE_4BYTES(0); //dwReserved[ 2 ] + WRITE_4BYTES(0); //dwReserved[ 3 ] - if( afd.motionJpeg ) //biCompression and biSizeImage - { - // we specify the pixel count - WRITE_STRING( "MJPG" ); - WRITE_4BYTES( afd.width * afd.height ); - } - else - { - // must either specify 0 or the total byte count per image - // but there is no reason to trust most software... - WRITE_4BYTES( 0 ); // raw BGR - WRITE_4BYTES( afd.width * afd.height * 3 ); - } + START_CHUNK("LIST"); + { + WRITE_STRING("strl"); + WRITE_STRING("strh"); + WRITE_4BYTES(56); //"strh" "chunk" size + WRITE_STRING("vids"); //fccType - WRITE_4BYTES( 0 ); //biXPelsPetMeter - WRITE_4BYTES( 0 ); //biYPelsPetMeter - WRITE_4BYTES( 0 ); //biClrUsed - WRITE_4BYTES( 0 ); //biClrImportant - } - END_CHUNK( ); + //fccHandler + if (!afd.isPiped && afd.useMotionJpeg) + WRITE_STRING("MJPG"); + else + WRITE_4BYTES(0); //raw BGR - if( afd.audio ) - { - START_CHUNK( "LIST" ); - { - WRITE_STRING( "strl" ); - WRITE_STRING( "strh" ); - WRITE_4BYTES( 56 ); //"strh" "chunk" size - WRITE_STRING( "auds" ); - WRITE_4BYTES( 0 ); //FCC - WRITE_4BYTES( 0 ); //dwFlags - WRITE_4BYTES( 0 ); //dwPriority - WRITE_4BYTES( 0 ); //dwInitialFrame + WRITE_4BYTES(0); //dwFlags + WRITE_4BYTES(0); //dwPriority + WRITE_4BYTES(0); //dwInitialFrame - WRITE_4BYTES( afd.a.sampleSize ); //dwTimescale - WRITE_4BYTES( afd.a.sampleSize * - afd.a.rate ); //dwDataRate - WRITE_4BYTES( 0 ); //dwStartTime - WRITE_4BYTES( afd.a.totalBytes / - afd.a.sampleSize ); //dwDataLength + WRITE_4BYTES(1); //dwTimescale + WRITE_4BYTES(afd.frameRate); //dwDataRate + WRITE_4BYTES(0); //dwStartTime + WRITE_4BYTES(afd.numVideoFrames); //dwDataLength - WRITE_4BYTES( 0 ); //dwSuggestedBufferSize - WRITE_4BYTES( -1 ); //dwQuality - WRITE_4BYTES( afd.a.sampleSize ); //dwSampleSize - WRITE_2BYTES( 0 ); //rcFrame - WRITE_2BYTES( 0 ); //rcFrame - WRITE_2BYTES( 0 ); //rcFrame - WRITE_2BYTES( 0 ); //rcFrame + WRITE_4BYTES(afd.maxRecordSize); //dwSuggestedBufferSize + WRITE_4BYTES(-1); //dwQuality + WRITE_4BYTES(0); //dwSampleSize + WRITE_2BYTES(0); //rcFrame + WRITE_2BYTES(0); //rcFrame + WRITE_2BYTES(afd.width); //rcFrame + WRITE_2BYTES(afd.height); //rcFrame - WRITE_STRING( "strf" ); - WRITE_4BYTES( 18 ); //"strf" "chunk" size - WRITE_2BYTES( afd.a.format ); //wFormatTag - WRITE_2BYTES( afd.a.channels ); //nChannels - WRITE_4BYTES( afd.a.rate ); //nSamplesPerSec - WRITE_4BYTES( afd.a.sampleSize * - afd.a.rate ); //nAvgBytesPerSec - WRITE_2BYTES( afd.a.sampleSize ); //nBlockAlign - WRITE_2BYTES( afd.a.bits ); //wBitsPerSample - WRITE_2BYTES( 0 ); //cbSize - } - END_CHUNK( ); - } - } - END_CHUNK( ); + WRITE_STRING("strf"); + WRITE_4BYTES(40); //"strf" "chunk" size + WRITE_4BYTES(40); //biSize + WRITE_4BYTES(afd.width); //biWidth + WRITE_4BYTES(afd.height); //biHeight + WRITE_2BYTES(1); //biPlanes + WRITE_2BYTES(24); //biBitCount - afd.moviOffset = bufIndex; + //biCompression and biSizeImage + if (!afd.isPiped && afd.useMotionJpeg) { + // We specify the pixel count + WRITE_STRING("MJPG"); + WRITE_4BYTES(afd.width * afd.height); + } else { + // We must either specify 0 or the total byte count per image + WRITE_4BYTES(0); // raw BGR + WRITE_4BYTES(afd.width * afd.height * 3); + } - START_CHUNK( "LIST" ); - { - WRITE_STRING( "movi" ); - } - } - } + WRITE_4BYTES(0); //biXPelsPetMeter + WRITE_4BYTES(0); //biYPelsPetMeter + WRITE_4BYTES(0); //biClrUsed + WRITE_4BYTES(0); //biClrImportant + } + END_CHUNK(); + + if (afd.hasAudio) { + START_CHUNK("LIST"); + { + WRITE_STRING("strl"); + WRITE_STRING("strh"); + WRITE_4BYTES(56); //"strh" "chunk" size + WRITE_STRING("auds"); + WRITE_4BYTES(0); //FCC + WRITE_4BYTES(0); //dwFlags + WRITE_4BYTES(0); //dwPriority + WRITE_4BYTES(0); //dwInitialFrame + + WRITE_4BYTES(afd.audioFormat.sampleSize); //dwTimescale + WRITE_4BYTES(afd.audioFormat.sampleSize * afd.audioFormat.rate); //dwDataRate + WRITE_4BYTES(0); //dwStartTime + WRITE_4BYTES(afd.audioFormat.totalBytes / afd.audioFormat.sampleSize); //dwDataLength + + WRITE_4BYTES(0); //dwSuggestedBufferSize + WRITE_4BYTES(-1); //dwQuality + WRITE_4BYTES(afd.audioFormat.sampleSize); //dwSampleSize + WRITE_2BYTES(0); //rcFrame + WRITE_2BYTES(0); //rcFrame + WRITE_2BYTES(0); //rcFrame + WRITE_2BYTES(0); //rcFrame + + WRITE_STRING("strf"); + WRITE_4BYTES(18); //"strf" "chunk" size + WRITE_2BYTES(afd.audioFormat.format); //wFormatTag + WRITE_2BYTES(afd.audioFormat.channels); //nChannels + WRITE_4BYTES(afd.audioFormat.rate); //nSamplesPerSec + WRITE_4BYTES(afd.audioFormat.sampleSize * afd.audioFormat.rate); //nAvgBytesPerSec + WRITE_2BYTES(afd.audioFormat.sampleSize); //nBlockAlign + WRITE_2BYTES(afd.audioFormat.bits); //wBitsPerSample + WRITE_2BYTES(0); //cbSize + } + END_CHUNK(); + } + } + END_CHUNK(); + + afd.moviOffset = bufferWriteIndex; + + START_CHUNK("LIST"); + { + WRITE_STRING("movi"); + } + } + } } // creates an AVI file and gets it into a state where writing the actual data can begin - -qbool CL_OpenAVIForWriting( const char* fileNameNoExt, qbool reOpen ) +static qbool OpenAVI( const char* fileNameNoExt, qbool reOpen ) { static char avi_fileNameNoExt[MAX_QPATH]; - static int avi_fileNameIndex; + static int avi_fileNameIndex; - if ( reOpen ) - CloseAVI( CM_SEQUENCE_INCOMPLETE ); + if (!afd.isPiped && reOpen) + CloseAVI(CM_SEQUENCE_INCOMPLETE); - if ( afd.fileOpen ) + if (afd.isFileOpen) return qfalse; - Com_Memset( &afd, 0, sizeof( aviFileData_t ) ); + Com_Memset(&afd, 0, sizeof(aviFileData_t)); + afd.isPiped = cl_ffmpeg->integer != 0; // don't start if a framerate has not been chosen - if ( cl_aviFrameRate->integer <= 0 ) { - Com_Printf( S_COLOR_RED "cl_aviFrameRate must be >= 1\n" ); + if (cl_aviFrameRate->integer <= 0) { + Com_Printf("^1ERROR: cl_aviFrameRate must be >= 1\n"); return qfalse; } - if ( reOpen ) { - avi_fileNameIndex++; + if (afd.isPiped) { + const char* fileExtension = cl_ffmpegOutExt->string; + while (*fileExtension == '.') { + fileExtension++; + } + + if (cl_ffmpegOutPath->string[0] == '\0') { + const char* homePath = Cvar_VariableString("fs_homepath"); + const char* final = FS_BuildOSPath(homePath, NULL, va("videos/%s.%s", fileNameNoExt, fileExtension)); + Com_sprintf(afd.fileName, sizeof(afd.fileName), final); + } else { + Com_sprintf(afd.fileName, sizeof(afd.fileName), "%s/%s.%s", cl_ffmpegOutPath->string, fileNameNoExt, fileExtension); + FS_ReplaceSeparators(afd.fileName); + } + + // @NOTE: can't double quote the executable's path with _popen + const char* pipeCommand; + if (cl_ffmpegLog->integer != 0) { + pipeCommand = va("%s -f avi -i - %s -y \"%s\" > \"%s.log.txt\" 2>&1", + cl_ffmpegExePath->string, cl_ffmpegCommand->string, afd.fileName, afd.fileName); + } else { + pipeCommand = va("%s -f avi -i - %s -y \"%s\"", + cl_ffmpegExePath->string, cl_ffmpegCommand->string, afd.fileName); + } + + afd.mainFile = FS_OpenPipeWrite(pipeCommand); + if (afd.mainFile <= 0) { + Com_Printf("^1ERROR: failed to open video process for writing\n"); + return qfalse; + } } else { - Q_strncpyz( avi_fileNameNoExt, fileNameNoExt, sizeof( avi_fileNameNoExt ) ); - avi_fileNameIndex = 0; - } - Com_sprintf( afd.fileName, sizeof( afd.fileName ), "%s_%03d.avi", avi_fileNameNoExt, avi_fileNameIndex ); + if (reOpen) { + avi_fileNameIndex++; + } else { + Q_strncpyz(avi_fileNameNoExt, fileNameNoExt, sizeof(avi_fileNameNoExt)); + avi_fileNameIndex = 0; + } + Com_sprintf(afd.fileName, sizeof(afd.fileName), "videos/%s_%03d.avi", avi_fileNameNoExt, avi_fileNameIndex); - if ( ( afd.f = FS_FOpenFileWrite( afd.fileName ) ) <= 0 ) - return qfalse; - - if ( ( afd.idxF = FS_FOpenFileWrite( va( "%s" INDEX_FILE_EXTENSION, afd.fileName ) ) ) <= 0 ) { - FS_FCloseFile( afd.f ); - return qfalse; - } - - afd.frameRate = cl_aviFrameRate->integer; - afd.framePeriod = (int)( 1000000.0f / afd.frameRate ); - afd.width = cls.glconfig.vidWidth; - afd.height = cls.glconfig.vidHeight; - - afd.motionJpeg = (cl_aviMotionJpeg->integer != 0); - - const int maxByteCount = PAD( afd.width, 4 ) * afd.height * 4; - afd.cBuffer = (byte*)Z_Malloc( maxByteCount ); - afd.eBuffer = (byte*)Z_Malloc( maxByteCount ); - - afd.a.rate = dma.speed; - afd.a.format = WAV_FORMAT_PCM; - afd.a.channels = dma.channels; - afd.a.bits = dma.samplebits; - afd.a.sampleSize = ( afd.a.bits / 8 ) * afd.a.channels; - - if ( afd.a.rate % afd.frameRate ) - { - int suggestRate = afd.frameRate; - while ((afd.a.rate % suggestRate) && suggestRate) - --suggestRate; - Com_Printf( S_COLOR_YELLOW "WARNING: cl_aviFrameRate is not a divisor " - "of the audio rate, suggest %d\n", suggestRate ); - } - - afd.audio = qfalse; - if ( Cvar_VariableIntegerValue( "s_initsound" ) ) { - afd.audio = ( afd.a.bits == 16 && afd.a.channels == 2 ); - if (!afd.audio) { - Com_Printf( S_COLOR_YELLOW "WARNING: Audio capture needs 16-bit stereo" ); + afd.mainFile = FS_FOpenFileWrite(afd.fileName); + afd.indexFile = FS_FOpenFileWrite(va("%s" INDEX_FILE_EXTENSION, afd.fileName)); + if (afd.mainFile <= 0 || afd.indexFile <= 0) { + Safe_FS_Close(&afd.mainFile); + Safe_FS_Close(&afd.indexFile); + Com_Printf("^1ERROR: failed to open video file for writing\n"); + return qfalse; } } - // this doesn't write a real header, but allocates the + afd.frameRate = cl_aviFrameRate->integer; + afd.framePeriod = (int)(1000000.0f / afd.frameRate); + afd.width = cls.glconfig.vidWidth; + afd.height = cls.glconfig.vidHeight; + afd.useMotionJpeg = !afd.isPiped && cl_aviMotionJpeg->integer != 0; + + const int maxByteCount = PAD(afd.width, 4) * afd.height * 4; + afd.captureBuffer = (byte*)Z_Malloc(maxByteCount); + afd.encodeBuffer = (byte*)Z_Malloc(maxByteCount); + + afd.audioFormat.rate = dma.speed; + afd.audioFormat.format = WAV_FORMAT_PCM; + afd.audioFormat.channels = dma.channels; + afd.audioFormat.bits = dma.samplebits; + afd.audioFormat.sampleSize = (afd.audioFormat.bits / 8) * afd.audioFormat.channels; + + if (afd.audioFormat.rate % afd.frameRate) { + int suggestRate = afd.frameRate; + while ((afd.audioFormat.rate % suggestRate) && suggestRate) + --suggestRate; + Com_Printf("^3WARNING: cl_aviFrameRate is not a divisor of the audio rate, suggesting %d\n", suggestRate); + } + + afd.hasAudio = qfalse; + if (Cvar_VariableIntegerValue("s_initsound")) { + afd.hasAudio = (afd.audioFormat.bits == 16 && afd.audioFormat.channels == 2); + if (!afd.hasAudio) { + Com_Printf("^3WARNING: Audio capture needs 16-bit stereo"); + } + } + + // This doesn't write a real header, but allocates the // correct amount of space at the beginning of the file - CL_WriteAVIHeader( ); + CL_WriteAVIHeader(); - SafeFS_Write( buffer, bufIndex, afd.f ); - afd.fileSize = bufIndex; + Safe_FS_Write(buffer, bufferWriteIndex, afd.mainFile); + afd.fileSize = bufferWriteIndex; - bufIndex = 0; - START_CHUNK( "idx1" ); - SafeFS_Write( buffer, bufIndex, afd.idxF ); + if (!afd.isPiped) { + bufferWriteIndex = 0; + START_CHUNK("idx1"); + Safe_FS_Write(buffer, bufferWriteIndex, afd.indexFile); + } - afd.moviSize = 4; // for the "movi" header signature - afd.fileOpen = qtrue; + afd.moviSize = 4; // For the "movi" header signature + afd.isFileOpen = qtrue; - Com_Printf( "Recording to %s\n", afd.fileName ); + Com_Printf("Recording to %s\n", afd.fileName); return qtrue; } -/* -=============== -CL_CheckFileSize -=============== -*/ -static qbool CL_CheckFileSize( int bytesToAdd ) +qbool CL_OpenAVIForWriting( const char* fileNameNoExt ) { - unsigned int newFileSize; - - newFileSize = - afd.fileSize + // Current file size - bytesToAdd + // What we want to add - ( afd.numIndices * 16 ) + // The index - 4; // The index size - - // I assume all the operating systems - // we target can handle a 2Gb file - if( newFileSize > INT_MAX ) - { - CL_OpenAVIForWriting( NULL, qtrue ); - return qtrue; - } - - return qfalse; + return OpenAVI(fileNameNoExt, qfalse); } -/* -=============== -CL_WriteAVIVideoFrame -=============== -*/ -void CL_WriteAVIVideoFrame( const byte *imageBuffer, int size ) + +static qbool CheckFileSize( int bytesToAdd ) { - int chunkOffset = afd.fileSize - afd.moviOffset - 8; - int chunkSize = 8 + size; - int paddingSize = PAD( size, 2 ) - size; - byte padding[ 4 ] = { 0 }; + const unsigned int newFileSize = + afd.fileSize + // Current file size + bytesToAdd + // What we want to add + (afd.numIndices * 16) + // The index + 4; // The index size - if( !afd.fileOpen ) - return; + // "AVI " limit is 1 GB (AVI 1.0) + // "AVIX" limit is 2 GB (AVI 2.0 / OpenDML) + if (!afd.isPiped && newFileSize > (1 << 30)) { + OpenAVI(NULL, qtrue); + return qtrue; + } - // Chunk header + contents + padding - if( CL_CheckFileSize( 8 + size + 2 ) ) - return; - - bufIndex = 0; - WRITE_STRING( "00dc" ); - WRITE_4BYTES( size ); - - SafeFS_Write( buffer, 8, afd.f ); - SafeFS_Write( imageBuffer, size, afd.f ); - SafeFS_Write( padding, paddingSize, afd.f ); - afd.fileSize += ( chunkSize + paddingSize ); - - afd.numVideoFrames++; - afd.moviSize += ( chunkSize + paddingSize ); - - if( size > afd.maxRecordSize ) - afd.maxRecordSize = size; - - // Index - bufIndex = 0; - WRITE_STRING( "00dc" ); //dwIdentifier - WRITE_4BYTES( 0x00000010 ); //dwFlags (all frames are KeyFrames) - WRITE_4BYTES( chunkOffset ); //dwOffset - WRITE_4BYTES( size ); //dwLength - SafeFS_Write( buffer, 16, afd.idxF ); - - afd.numIndices++; + return qfalse; } -#define PCM_BUFFER_SIZE 44100 -/* -=============== -CL_WriteAVIAudioFrame -=============== -*/ -void CL_WriteAVIAudioFrame( const byte *pcmBuffer, int size ) +void CL_WriteAVIVideoFrame( const byte* imageBuffer, int size ) { - static byte pcmCaptureBuffer[ PCM_BUFFER_SIZE ] = { 0 }; - static int bytesInBuffer = 0; + if (!afd.isFileOpen) + return; - if( !afd.audio ) - return; + // Chunk header + contents + padding + if (CheckFileSize(8 + size + 2)) + return; - if( !afd.fileOpen ) - return; + bufferWriteIndex = 0; + WRITE_STRING("00dc"); + WRITE_4BYTES(size); - // Chunk header + contents + padding - if( CL_CheckFileSize( 8 + bytesInBuffer + size + 2 ) ) - return; + const int chunkOffset = afd.fileSize - afd.moviOffset - 8; + const int chunkSize = 8 + size; + const int paddingSize = PAD(size, 2) - size; + const byte padding[4] = { 0 }; + Safe_FS_Write(buffer, 8, afd.mainFile); + Safe_FS_Write(imageBuffer, size, afd.mainFile); + Safe_FS_Write(padding, paddingSize, afd.mainFile); + afd.fileSize += (chunkSize + paddingSize); - if( bytesInBuffer + size > PCM_BUFFER_SIZE ) - { - Com_Printf( S_COLOR_YELLOW - "WARNING: Audio capture buffer overflow -- truncating\n" ); - size = PCM_BUFFER_SIZE - bytesInBuffer; - } + afd.numVideoFrames++; + afd.moviSize += (chunkSize + paddingSize); - Com_Memcpy( &pcmCaptureBuffer[ bytesInBuffer ], pcmBuffer, size ); - bytesInBuffer += size; + if (size > afd.maxRecordSize) + afd.maxRecordSize = size; - // Only write if we have a frame's worth of audio - if( bytesInBuffer >= (int)ceil( (float)afd.a.rate / (float)afd.frameRate ) * - afd.a.sampleSize ) - { - int chunkOffset = afd.fileSize - afd.moviOffset - 8; - int chunkSize = 8 + bytesInBuffer; - int paddingSize = PAD( bytesInBuffer, 2 ) - bytesInBuffer; - byte padding[ 4 ] = { 0 }; + // Write index data + if (!afd.isPiped) { + bufferWriteIndex = 0; + WRITE_STRING("00dc"); //dwIdentifier + WRITE_4BYTES(0x00000010); //dwFlags (all frames are KeyFrames) + WRITE_4BYTES(chunkOffset); //dwOffset + WRITE_4BYTES(size); //dwLength + Safe_FS_Write(buffer, 16, afd.indexFile); + } - bufIndex = 0; - WRITE_STRING( "01wb" ); - WRITE_4BYTES( bytesInBuffer ); - - SafeFS_Write( buffer, 8, afd.f ); - SafeFS_Write( pcmCaptureBuffer, bytesInBuffer, afd.f ); - SafeFS_Write( padding, paddingSize, afd.f ); - afd.fileSize += ( chunkSize + paddingSize ); - - afd.numAudioFrames++; - afd.moviSize += ( chunkSize + paddingSize ); - afd.a.totalBytes += bytesInBuffer; - - // Index - bufIndex = 0; - WRITE_STRING( "01wb" ); //dwIdentifier - WRITE_4BYTES( 0 ); //dwFlags - WRITE_4BYTES( chunkOffset ); //dwOffset - WRITE_4BYTES( bytesInBuffer ); //dwLength - SafeFS_Write( buffer, 16, afd.idxF ); - - afd.numIndices++; - - bytesInBuffer = 0; - } + afd.numIndices++; } -/* -=============== -CL_TakeVideoFrame -=============== -*/ -void CL_TakeVideoFrame( void ) -{ - // AVI file isn't open - if( !afd.fileOpen ) - return; - re.TakeVideoFrame( afd.width, afd.height, - afd.cBuffer, afd.eBuffer, afd.motionJpeg ); +void CL_WriteAVIAudioFrame( const byte* pcmBuffer, int size ) +{ + static byte pcmCaptureBuffer[PCM_BUFFER_SIZE] = { 0 }; + static int bytesInBuffer = 0; + + if (!afd.hasAudio) + return; + + if (!afd.isFileOpen) + return; + + // Chunk header + contents + padding + if (CheckFileSize(8 + bytesInBuffer + size + 2)) + return; + + if (bytesInBuffer + size > PCM_BUFFER_SIZE) { + Com_Printf("^3WARNING: Audio capture buffer overflow -- truncating\n"); + size = PCM_BUFFER_SIZE - bytesInBuffer; + } + + Com_Memcpy(&pcmCaptureBuffer[bytesInBuffer], pcmBuffer, size); + bytesInBuffer += size; + + // Only write if we have a frame's worth of audio + const int bytesToWrite = (int)ceil((float)afd.audioFormat.rate / (float)afd.frameRate) * afd.audioFormat.sampleSize; + if (bytesInBuffer >= bytesToWrite) { + bufferWriteIndex = 0; + WRITE_STRING("01wb"); + WRITE_4BYTES(bytesInBuffer); + + const int chunkOffset = afd.fileSize - afd.moviOffset - 8; + const int chunkSize = 8 + bytesInBuffer; + const int paddingSize = PAD(bytesInBuffer, 2) - bytesInBuffer; + const byte padding[4] = { 0 }; + Safe_FS_Write(buffer, 8, afd.mainFile); + Safe_FS_Write(pcmCaptureBuffer, bytesInBuffer, afd.mainFile); + Safe_FS_Write(padding, paddingSize, afd.mainFile); + afd.fileSize += (chunkSize + paddingSize); + + afd.numAudioFrames++; + afd.moviSize += (chunkSize + paddingSize); + afd.audioFormat.totalBytes += bytesInBuffer; + + // Write index data + if (!afd.isPiped) { + bufferWriteIndex = 0; + WRITE_STRING("01wb"); //dwIdentifier + WRITE_4BYTES(0); //dwFlags + WRITE_4BYTES(chunkOffset); //dwOffset + WRITE_4BYTES(bytesInBuffer); //dwLength + Safe_FS_Write(buffer, 16, afd.indexFile); + } + + afd.numIndices++; + + bytesInBuffer = 0; + } } -/* -=============== -CL_CloseAVI -Closes the AVI file and writes an index chunk -=============== -*/ +void CL_TakeVideoFrame() +{ + if (!afd.isFileOpen) + return; + + re.TakeVideoFrame(afd.width, afd.height, afd.captureBuffer, afd.encodeBuffer, afd.useMotionJpeg); +} + + +static qbool FixIndices() +{ + int indexSize = afd.numIndices * 16; + FS_Seek(afd.indexFile, 4, FS_SEEK_SET); + bufferWriteIndex = 0; + WRITE_4BYTES(indexSize); + Safe_FS_Write(buffer, bufferWriteIndex, afd.indexFile); + Safe_FS_Close(&afd.indexFile); // needed in case the follow-up open fails + + // Open the temp index file + const char* const idxFileName = va("%s" INDEX_FILE_EXTENSION, afd.fileName); + if ((indexSize = FS_FOpenFileRead(idxFileName, &afd.indexFile, qtrue)) <= 0) { + return qfalse; + } + + // Append index to end of avi file + int indexRemainder = indexSize; + while (indexRemainder > MAX_AVI_BUFFER) { + FS_Read(buffer, MAX_AVI_BUFFER, afd.indexFile); + Safe_FS_Write(buffer, MAX_AVI_BUFFER, afd.mainFile); + afd.fileSize += MAX_AVI_BUFFER; + indexRemainder -= MAX_AVI_BUFFER; + } + FS_Read(buffer, indexRemainder, afd.indexFile); + Safe_FS_Write(buffer, indexRemainder, afd.mainFile); + afd.fileSize += indexRemainder; + + // Close and remove temp index file + Safe_FS_Close(&afd.indexFile); + FS_HomeRemove(idxFileName); + + // Write the real header + FS_Seek(afd.mainFile, 0, FS_SEEK_SET); + CL_WriteAVIHeader(); + + bufferWriteIndex = 4; + WRITE_4BYTES(afd.fileSize - 8); // "RIFF" size + + bufferWriteIndex = afd.moviOffset + 4; // Skip "LIST" + WRITE_4BYTES(afd.moviSize); + + Safe_FS_Write(buffer, bufferWriteIndex, afd.mainFile); + + return qtrue; +} + + +// Closes the AVI file and optionally writes an index chunk static qbool CloseAVI( closeMode_t closeMode ) { - int indexRemainder; - int indexSize = afd.numIndices * 16; - const char *idxFileName = va( "%s" INDEX_FILE_EXTENSION, afd.fileName ); + if (!afd.isFileOpen) + return qfalse; - // AVI file isn't open - if( !afd.fileOpen ) - return qfalse; + afd.isFileOpen = qfalse; - afd.fileOpen = qfalse; + qbool result = qtrue; + if (!afd.isPiped) { + result = FixIndices(); + } - FS_Seek( afd.idxF, 4, FS_SEEK_SET ); - bufIndex = 0; - WRITE_4BYTES( indexSize ); - SafeFS_Write( buffer, bufIndex, afd.idxF ); - FS_FCloseFile( afd.idxF ); + Safe_Z_Free(&afd.captureBuffer); + Safe_Z_Free(&afd.encodeBuffer); + Safe_FS_Close(&afd.mainFile); + Safe_FS_Close(&afd.indexFile); - // Write index + Com_Printf("Processed %d:%d V:A frames for %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName); - // Open the temp index file - if( ( indexSize = FS_FOpenFileRead( idxFileName, - &afd.idxF, qtrue ) ) <= 0 ) - { - FS_FCloseFile( afd.f ); - return qfalse; - } + if (closeMode == CM_SEQUENCE_COMPLETE) { + S_StopAllSounds(); + } - indexRemainder = indexSize; - - // Append index to end of avi file - while( indexRemainder > MAX_AVI_BUFFER ) - { - FS_Read( buffer, MAX_AVI_BUFFER, afd.idxF ); - SafeFS_Write( buffer, MAX_AVI_BUFFER, afd.f ); - afd.fileSize += MAX_AVI_BUFFER; - indexRemainder -= MAX_AVI_BUFFER; - } - FS_Read( buffer, indexRemainder, afd.idxF ); - SafeFS_Write( buffer, indexRemainder, afd.f ); - afd.fileSize += indexRemainder; - FS_FCloseFile( afd.idxF ); - - // Remove temp index file - FS_HomeRemove( idxFileName ); - - // Write the real header - FS_Seek( afd.f, 0, FS_SEEK_SET ); - CL_WriteAVIHeader( ); - - bufIndex = 4; - WRITE_4BYTES( afd.fileSize - 8 ); // "RIFF" size - - bufIndex = afd.moviOffset + 4; // Skip "LIST" - WRITE_4BYTES( afd.moviSize ); - - SafeFS_Write( buffer, bufIndex, afd.f ); - - Z_Free( afd.cBuffer ); - Z_Free( afd.eBuffer ); - FS_FCloseFile( afd.f ); - - Com_Printf( "Wrote %d:%d V:A frames to %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName ); - - if ( closeMode == CM_SEQUENCE_COMPLETE ) - S_StopAllSounds(); - - return qtrue; + return result; } -qbool CL_CloseAVI( void ) + +qbool CL_CloseAVI() { - return CloseAVI( CM_SEQUENCE_COMPLETE ); + return CloseAVI(CM_SEQUENCE_COMPLETE); } -/* -=============== -CL_VideoRecording -=============== -*/ -qbool CL_VideoRecording( void ) + +qbool CL_VideoRecording() { - return afd.fileOpen; + return afd.isFileOpen; } diff --git a/code/client/cl_cgame.cpp b/code/client/cl_cgame.cpp index bd8c5c3..ebd7200 100644 --- a/code/client/cl_cgame.cpp +++ b/code/client/cl_cgame.cpp @@ -698,7 +698,7 @@ static intptr_t CL_CgameSystemCalls( intptr_t *args ) case CG_EXT_NDP_STARTVIDEO: Cvar_Set( cl_aviFrameRate->name, va( "%d", (int)args[2] ) ); - return CL_OpenAVIForWriting( va( "videos/%s", (const char*)VMA(1) ), qfalse ); + return CL_OpenAVIForWriting( VMA(1) ); case CG_EXT_NDP_STOPVIDEO: CL_CloseAVI(); diff --git a/code/client/cl_main.cpp b/code/client/cl_main.cpp index 06896fa..1fc2f05 100644 --- a/code/client/cl_main.cpp +++ b/code/client/cl_main.cpp @@ -41,6 +41,12 @@ cvar_t* cl_showSend; cvar_t *cl_timedemo; cvar_t *cl_aviFrameRate; cvar_t *cl_aviMotionJpeg; +cvar_t *cl_ffmpeg; +cvar_t *cl_ffmpegCommand; +cvar_t *cl_ffmpegExePath; +cvar_t *cl_ffmpegOutPath; +cvar_t *cl_ffmpegOutExt; +cvar_t *cl_ffmpegLog; cvar_t *cl_allowDownload; cvar_t *cl_inGameVideo; @@ -1951,7 +1957,7 @@ static void CL_Video_f() 1900+t.tm_year, 1+t.tm_mon, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec ); } - CL_OpenAVIForWriting( va( "videos/%s", s ), qfalse ); + CL_OpenAVIForWriting( s ); } @@ -2148,6 +2154,41 @@ static const cvarTableItem_t cl_cvars[] = &cl_aviMotionJpeg, "cl_aviMotionJpeg", "1", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, help_cl_aviMotionJpeg, "AVI motion JPEG", CVARCAT_DEMO, "", "" }, + { + &cl_ffmpeg, "cl_ffmpeg", "0", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, + "use FFmpeg for video export\n" + "Pipes video through FFmpeg instead of writing raw .avi files.", + "Use FFmpeg", CVARCAT_DEMO, "Uses FFmpeg instead of writing raw .avi files", "" + }, + { + &cl_ffmpegCommand, "cl_ffmpegCommand", "-movflags faststart -bf 2 -c:v libx264 -preset medium -crf 23 -vf format=yuv420p -c:a aac -b:a 320k", + CVAR_ARCHIVE, CVART_STRING, NULL, NULL, + "FFmpeg encode settings\n" + "The full command-line options for the output file.", + "FFmpeg encode settings", CVARCAT_DEMO, "Command-line options for the output file", "" + }, + { + &cl_ffmpegExePath, "cl_ffmpegExePath", "ffmpeg", CVAR_ARCHIVE, CVART_STRING, NULL, NULL, + "FFmpeg executable path\n" + "The path cannot contain spaces.", + "FFmpeg executable path", CVARCAT_DEMO, "The path cannot contain spaces", "" + }, + { + &cl_ffmpegOutPath, "cl_ffmpegOutPath", "", CVAR_ARCHIVE, CVART_STRING, NULL, NULL, + "FFmpeg output directory\n" + "Leave empty to write to cpma/videos as with .avi files.", + "FFmpeg output directory", CVARCAT_DEMO, "Leave empty to write to cpma/videos as with .avi files", "" + }, + { + &cl_ffmpegOutExt, "cl_ffmpegOutExt", "mp4", CVAR_ARCHIVE, CVART_STRING, NULL, NULL, "FFmpeg output file extension", + "FFmpeg output file extension", CVARCAT_DEMO, "", "" + }, + { + &cl_ffmpegLog, "cl_ffmpegLog", "0", CVAR_ARCHIVE, CVART_BOOL, NULL, NULL, + "FFmpeg log file creation\n" + "Creates 1 log file per capture.", + "FFmpeg log file creation", CVARCAT_DEMO, "Creates 1 log file per capture", "" + }, { &rconAddress, "rconAddress", "", 0, CVART_STRING, NULL, NULL, help_rconAddress }, { &cl_maxpackets, "cl_maxpackets", "125", CVAR_ARCHIVE, CVART_INTEGER, "15", "125", "max. packet upload rate", diff --git a/code/client/client.h b/code/client/client.h index abe6a78..807b22e 100644 --- a/code/client/client.h +++ b/code/client/client.h @@ -371,6 +371,12 @@ extern cvar_t *cl_serverStatusResendTime; extern cvar_t *cl_timedemo; extern cvar_t *cl_aviFrameRate; extern cvar_t *cl_aviMotionJpeg; +extern cvar_t *cl_ffmpeg; +extern cvar_t *cl_ffmpegCommand; +extern cvar_t *cl_ffmpegExePath; +extern cvar_t *cl_ffmpegOutPath; +extern cvar_t *cl_ffmpegOutExt; +extern cvar_t *cl_ffmpegLog; extern cvar_t *cl_allowDownload; // 0=off, 1=CNQ3, -1=id extern cvar_t *cl_inGameVideo; @@ -547,12 +553,12 @@ qbool CL_Netchan_Process( netchan_t *chan, msg_t *msg ); // // cl_avi.c // -qbool CL_OpenAVIForWriting( const char *fileNameNoExt, qbool reOpen ); -void CL_TakeVideoFrame( void ); +qbool CL_OpenAVIForWriting( const char *fileNameNoExt ); +void CL_TakeVideoFrame(); void CL_WriteAVIVideoFrame( const byte *imageBuffer, int size ); void CL_WriteAVIAudioFrame( const byte *pcmBuffer, int size ); -qbool CL_CloseAVI( void ); -qbool CL_VideoRecording( void ); +qbool CL_CloseAVI(); +qbool CL_VideoRecording(); // // cl_download.cpp diff --git a/code/qcommon/files.cpp b/code/qcommon/files.cpp index 637817e..953e59f 100644 --- a/code/qcommon/files.cpp +++ b/code/qcommon/files.cpp @@ -174,6 +174,14 @@ or configs will never get loaded from disk! #define MAX_SEARCH_PATHS 4096 #define MAX_FILEHASH_SIZE 1024 +#if defined( _WIN32 ) +#define Sys_OpenPipeWrite(Command) _popen(Command, "wb") +#define Sys_ClosePipe(Pipe) _pclose(Pipe) +#else +#define Sys_OpenPipeWrite(Command) popen(Command, "w") +#define Sys_ClosePipe(Pipe) pclose(Pipe) +#endif + typedef struct fileInPack_s { char *name; // name of the file unsigned long pos; // file info position in zip @@ -228,6 +236,7 @@ typedef union { typedef struct { qfile_gut file; qbool unique; + qbool isPipe; } qfile_ut; typedef struct { @@ -316,6 +325,7 @@ static fileHandle_t FS_HandleForFile() { for ( int i = 1; i < MAX_FILE_HANDLES; ++i ) { if ( fsh[i].handleFiles.file.o == NULL ) { + Com_Memset( &fsh[i], 0, sizeof( fsh[i] ) ); return i; } } @@ -378,7 +388,7 @@ FS_ReplaceSeparators Fix things up differently for win/unix/mac ==================== */ -static void FS_ReplaceSeparators( char *path ) { +void FS_ReplaceSeparators( char *path ) { char *s; for ( s = path ; *s ; s++ ) { @@ -711,7 +721,11 @@ void FS_FCloseFile( fileHandle_t f ) { // we didn't find it as a pak, so close it as a unique file if (fsh[f].handleFiles.file.o) { - fclose (fsh[f].handleFiles.file.o); + if (fsh[f].handleFiles.isPipe) { + Sys_ClosePipe( fsh[f].handleFiles.file.o ); + } else { + fclose( fsh[f].handleFiles.file.o ); + } } Com_Memset( &fsh[f], 0, sizeof( fsh[f] ) ); } @@ -757,6 +771,21 @@ fileHandle_t FS_FOpenFileWrite( const char *filename ) { return f; } +fileHandle_t FS_OpenPipeWrite( const char* command ) { + fileHandle_t f = FS_HandleForFile(); + fsh[f].zipFile = qfalse; + + fsh[f].handleFiles.file.o = Sys_OpenPipeWrite( command ); + fsh[f].handleFiles.isPipe = qtrue; + Q_strncpyz( fsh[f].name, "$pipe", sizeof(fsh[f].name)); + fsh[f].handleSync = qfalse; + if ( !fsh[f].handleFiles.file.o ) { + f = 0; + } + + return f; +} + /* =========== FS_FOpenFileAppend diff --git a/code/qcommon/qcommon.h b/code/qcommon/qcommon.h index 173b0d2..63cb521 100644 --- a/code/qcommon/qcommon.h +++ b/code/qcommon/qcommon.h @@ -677,6 +677,8 @@ void FS_FreeFileList( char **list ); qbool FS_FileExists( const char *file ); // checks in current game dir qbool FS_FileExistsEx( const char *file, qbool curGameDir ); // if curGameDir is qfalse, checks in "baseq3" +void FS_ReplaceSeparators( char *path ); + char* FS_BuildOSPath( const char *base, const char *game, const char *qpath ); int FS_LoadStack(); @@ -687,6 +689,8 @@ int FS_GetModList( char *listbuf, int bufsize ); fileHandle_t FS_FOpenFileWrite( const char *qpath ); // will properly create any needed paths and deal with seperater character issues +fileHandle_t FS_OpenPipeWrite( const char* command ); + fileHandle_t FS_SV_FOpenFileWrite( const char *filename ); int FS_SV_FOpenFileRead( const char *filename, fileHandle_t *fp ); void FS_SV_Rename( const char *from, const char *to ); diff --git a/code/win32/win_main.cpp b/code/win32/win_main.cpp index e88a168..d83e161 100644 --- a/code/win32/win_main.cpp +++ b/code/win32/win_main.cpp @@ -1059,6 +1059,12 @@ int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLin return 0; } + // prevent child processes from spawning a command prompt window +#ifndef DEDICATED + AllocConsole(); + ShowWindow( GetConsoleWindow(), SW_HIDE ); +#endif + // done here so the early console can be shown on the primary monitor WIN_InitMonitorList();