added FFmpeg support for A/V capture compression

This commit is contained in:
myT 2023-11-27 02:53:15 +01:00
parent 9a6e253dc3
commit 3cea7e0c3d
8 changed files with 631 additions and 530 deletions

View File

@ -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 <string> are the FFmpeg command-line options for the output file
cl_ffmpegExePath <string> (default: "ffmpeg") is the path of the FFmpeg executable to use
note that the path cannot contain spaces on Windows
cl_ffmpegOutPath <string> (default: "") is the video output directory
leave empty to write to cpma/videos as with .avi files
cl_ffmpegOutExt <string> (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

View File

@ -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,16 +20,26 @@ 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
{
#define PCM_BUFFER_SIZE 44100
#define MAX_AVI_BUFFER 4096
// flags for the "AVIH" RIFF chunk
#define AVIH_HASINDEX_BIT 0x010
#define AVIH_ISINTERLEAVED_BIT 0x100
struct audioFormat_t {
int rate;
int format;
int channels;
@ -36,159 +47,149 @@ typedef struct audioFormat_s
int sampleSize;
int totalBytes;
} audioFormat_t;
};
typedef struct aviFileData_s
{
qbool fileOpen;
fileHandle_t f;
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;
fileHandle_t idxF;
fileHandle_t indexFile;
int numIndices;
int frameRate;
int framePeriod;
int width, height;
int width;
int height;
int numVideoFrames;
int maxRecordSize;
qbool motionJpeg;
qbool useMotionJpeg;
qbool audio;
audioFormat_t a;
qbool hasAudio;
audioFormat_t audioFormat;
int numAudioFrames;
int chunkStack[MAX_RIFF_CHUNKS];
int chunkStackTop;
byte *cBuffer, *eBuffer; // capture and encoding buffers
} aviFileData_t;
byte* captureBuffer;
byte* encodeBuffer;
};
typedef enum closeMode_s
{
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
} closeMode_t;
};
static aviFileData_t afd;
#define MAX_AVI_BUFFER 2048
static byte buffer[MAX_AVI_BUFFER];
static int bufIndex;
static int bufferWriteIndex;
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;
}
/*
===============
WRITE_4BYTES
===============
*/
static ID_INLINE void WRITE_4BYTES( int x )
{
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 (FS_Write(buff, len, f) < len) {
AbortCapture("Failed to write to video file/pipe");
}
}
/*
===============
WRITE_2BYTES
===============
*/
static ID_INLINE void WRITE_2BYTES( int x )
static void Safe_FS_Close( fileHandle_t* file )
{
buffer[ bufIndex + 0 ] = (byte)( ( x >> 0 ) & 0xFF );
buffer[ bufIndex + 1 ] = (byte)( ( x >> 8 ) & 0xFF );
bufIndex += 2;
if (*file > 0) {
FS_FCloseFile(*file);
*file = 0;
}
}
/*
===============
WRITE_1BYTES
===============
*/
static ID_INLINE void WRITE_1BYTES( int x )
static void Safe_Z_Free( byte** memory )
{
buffer[ bufIndex ] = x;
bufIndex += 1;
if (*memory != NULL) {
Z_Free(*memory);
*memory = NULL;
}
}
/*
===============
START_CHUNK
===============
*/
static ID_INLINE void START_CHUNK( const char *s )
static void WRITE_STRING( const char* s )
{
if( afd.chunkStackTop == MAX_RIFF_CHUNKS )
{
Com_Error( ERR_DROP, "ERROR: Top of chunkstack breached\n" );
Com_Memcpy(&buffer[bufferWriteIndex], s, strlen(s));
bufferWriteIndex += strlen(s);
}
afd.chunkStack[ afd.chunkStackTop ] = bufIndex;
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;
}
static void WRITE_2BYTES( int x )
{
buffer[bufferWriteIndex + 0] = (byte)((x >> 0) & 0xFF);
buffer[bufferWriteIndex + 1] = (byte)((x >> 8) & 0xFF);
bufferWriteIndex += 2;
}
static void START_CHUNK( const char* s )
{
if (afd.chunkStackTop == MAX_RIFF_CHUNKS) {
AbortCapture("Top of chunkstack breached");
return;
}
afd.chunkStack[afd.chunkStackTop] = bufferWriteIndex;
afd.chunkStackTop++;
WRITE_STRING(s);
WRITE_4BYTES(0);
}
/*
===============
END_CHUNK
===============
*/
static ID_INLINE void END_CHUNK( void )
{
int endIndex = bufIndex;
if( afd.chunkStackTop <= 0 )
static void END_CHUNK()
{
Com_Error( ERR_DROP, "ERROR: Bottom of chunkstack breached\n" );
if (afd.chunkStackTop <= 0) {
AbortCapture("Bottom of chunkstack breached");
return;
}
const int endIndex = bufferWriteIndex;
afd.chunkStackTop--;
bufIndex = afd.chunkStack[ afd.chunkStackTop ];
bufIndex += 4;
WRITE_4BYTES( endIndex - bufIndex - 4 );
bufIndex = endIndex;
bufIndex = PAD( bufIndex, 2 );
bufferWriteIndex = afd.chunkStack[afd.chunkStackTop];
bufferWriteIndex += 4;
WRITE_4BYTES(endIndex - bufferWriteIndex - 4);
bufferWriteIndex = endIndex;
bufferWriteIndex = PAD(bufferWriteIndex, 2);
}
/*
===============
CL_WriteAVIHeader
===============
*/
void CL_WriteAVIHeader( void )
void CL_WriteAVIHeader()
{
bufIndex = 0;
bufferWriteIndex = 0;
afd.chunkStackTop = 0;
START_CHUNK("RIFF");
@ -201,14 +202,16 @@ void CL_WriteAVIHeader( void )
WRITE_STRING("avih");
WRITE_4BYTES(56); //"avih" "chunk" size
WRITE_4BYTES(afd.framePeriod); //dwMicroSecPerFrame
WRITE_4BYTES( afd.maxRecordSize *
afd.frameRate ); //dwMaxBytesPerSec
WRITE_4BYTES(afd.maxRecordSize * afd.frameRate); //dwMaxBytesPerSec
WRITE_4BYTES(0); //dwReserved1
WRITE_4BYTES( 0x110 ); //dwFlags bits HAS_INDEX and IS_INTERLEAVED
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
if( afd.audio ) //dwStreams
if (afd.hasAudio) //dwStreams
WRITE_4BYTES(2);
else
WRITE_4BYTES(1);
@ -228,7 +231,8 @@ void CL_WriteAVIHeader( void )
WRITE_4BYTES(56); //"strh" "chunk" size
WRITE_STRING("vids"); //fccType
if( afd.motionJpeg ) //fccHandler
//fccHandler
if (!afd.isPiped && afd.useMotionJpeg)
WRITE_STRING("MJPG");
else
WRITE_4BYTES(0); //raw BGR
@ -258,16 +262,13 @@ void CL_WriteAVIHeader( void )
WRITE_2BYTES(1); //biPlanes
WRITE_2BYTES(24); //biBitCount
if( afd.motionJpeg ) //biCompression and biSizeImage
{
// we specify the pixel count
//biCompression and biSizeImage
if (!afd.isPiped && afd.useMotionJpeg) {
// 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...
} 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);
}
@ -279,8 +280,7 @@ void CL_WriteAVIHeader( void )
}
END_CHUNK();
if( afd.audio )
{
if (afd.hasAudio) {
START_CHUNK("LIST");
{
WRITE_STRING("strl");
@ -292,16 +292,14 @@ void CL_WriteAVIHeader( void )
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(afd.audioFormat.sampleSize); //dwTimescale
WRITE_4BYTES(afd.audioFormat.sampleSize * afd.audioFormat.rate); //dwDataRate
WRITE_4BYTES(0); //dwStartTime
WRITE_4BYTES( afd.a.totalBytes /
afd.a.sampleSize ); //dwDataLength
WRITE_4BYTES(afd.audioFormat.totalBytes / afd.audioFormat.sampleSize); //dwDataLength
WRITE_4BYTES(0); //dwSuggestedBufferSize
WRITE_4BYTES(-1); //dwQuality
WRITE_4BYTES( afd.a.sampleSize ); //dwSampleSize
WRITE_4BYTES(afd.audioFormat.sampleSize); //dwSampleSize
WRITE_2BYTES(0); //rcFrame
WRITE_2BYTES(0); //rcFrame
WRITE_2BYTES(0); //rcFrame
@ -309,13 +307,12 @@ void CL_WriteAVIHeader( void )
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(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();
@ -323,7 +320,7 @@ void CL_WriteAVIHeader( void )
}
END_CHUNK();
afd.moviOffset = bufIndex;
afd.moviOffset = bufferWriteIndex;
START_CHUNK("LIST");
{
@ -335,89 +332,121 @@ void CL_WriteAVIHeader( void )
// 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;
if ( reOpen )
if (!afd.isPiped && reOpen)
CloseAVI(CM_SEQUENCE_INCOMPLETE);
if ( afd.fileOpen )
if (afd.isFileOpen)
return qfalse;
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" );
Com_Printf("^1ERROR: cl_aviFrameRate must be >= 1\n");
return qfalse;
}
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 {
if (reOpen) {
avi_fileNameIndex++;
} 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 );
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 );
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;
}
}
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);
afd.useMotionJpeg = !afd.isPiped && 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.captureBuffer = (byte*)Z_Malloc(maxByteCount);
afd.encodeBuffer = (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;
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.a.rate % afd.frameRate )
{
if (afd.audioFormat.rate % afd.frameRate) {
int suggestRate = afd.frameRate;
while ((afd.a.rate % suggestRate) && suggestRate)
while ((afd.audioFormat.rate % suggestRate) && suggestRate)
--suggestRate;
Com_Printf( S_COLOR_YELLOW "WARNING: cl_aviFrameRate is not a divisor "
"of the audio rate, suggest %d\n", suggestRate );
Com_Printf("^3WARNING: cl_aviFrameRate is not a divisor of the audio rate, suggesting %d\n", suggestRate);
}
afd.audio = qfalse;
afd.hasAudio = 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.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
// This doesn't write a real header, but allocates the
// correct amount of space at the beginning of the file
CL_WriteAVIHeader();
SafeFS_Write( buffer, bufIndex, afd.f );
afd.fileSize = bufIndex;
Safe_FS_Write(buffer, bufferWriteIndex, afd.mainFile);
afd.fileSize = bufferWriteIndex;
bufIndex = 0;
if (!afd.isPiped) {
bufferWriteIndex = 0;
START_CHUNK("idx1");
SafeFS_Write( buffer, bufIndex, afd.idxF );
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);
@ -425,58 +454,51 @@ qbool CL_OpenAVIForWriting( const char* fileNameNoExt, qbool reOpen )
}
/*
===============
CL_CheckFileSize
===============
*/
static qbool CL_CheckFileSize( int bytesToAdd )
qbool CL_OpenAVIForWriting( const char* fileNameNoExt )
{
unsigned int newFileSize;
return OpenAVI(fileNameNoExt, qfalse);
}
newFileSize =
static qbool CheckFileSize( int bytesToAdd )
{
const unsigned int 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 );
// "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;
}
return qfalse;
}
/*
===============
CL_WriteAVIVideoFrame
===============
*/
void CL_WriteAVIVideoFrame( const byte* imageBuffer, int size )
{
int chunkOffset = afd.fileSize - afd.moviOffset - 8;
int chunkSize = 8 + size;
int paddingSize = PAD( size, 2 ) - size;
byte padding[ 4 ] = { 0 };
if( !afd.fileOpen )
if (!afd.isFileOpen)
return;
// Chunk header + contents + padding
if( CL_CheckFileSize( 8 + size + 2 ) )
if (CheckFileSize(8 + size + 2))
return;
bufIndex = 0;
bufferWriteIndex = 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 );
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);
afd.numVideoFrames++;
@ -485,43 +507,37 @@ void CL_WriteAVIVideoFrame( const byte *imageBuffer, int size )
if (size > afd.maxRecordSize)
afd.maxRecordSize = size;
// Index
bufIndex = 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
SafeFS_Write( buffer, 16, afd.idxF );
Safe_FS_Write(buffer, 16, afd.indexFile);
}
afd.numIndices++;
}
#define PCM_BUFFER_SIZE 44100
/*
===============
CL_WriteAVIAudioFrame
===============
*/
void CL_WriteAVIAudioFrame( const byte* pcmBuffer, int size )
{
static byte pcmCaptureBuffer[PCM_BUFFER_SIZE] = { 0 };
static int bytesInBuffer = 0;
if( !afd.audio )
if (!afd.hasAudio)
return;
if( !afd.fileOpen )
if (!afd.isFileOpen)
return;
// Chunk header + contents + padding
if( CL_CheckFileSize( 8 + bytesInBuffer + size + 2 ) )
if (CheckFileSize(8 + bytesInBuffer + size + 2))
return;
if( bytesInBuffer + size > PCM_BUFFER_SIZE )
{
Com_Printf( S_COLOR_YELLOW
"WARNING: Audio capture buffer overflow -- truncating\n" );
if (bytesInBuffer + size > PCM_BUFFER_SIZE) {
Com_Printf("^3WARNING: Audio capture buffer overflow -- truncating\n");
size = PCM_BUFFER_SIZE - bytesInBuffer;
}
@ -529,34 +545,34 @@ void CL_WriteAVIAudioFrame( const byte *pcmBuffer, int size )
bytesInBuffer += 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 };
bufIndex = 0;
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);
SafeFS_Write( buffer, 8, afd.f );
SafeFS_Write( pcmCaptureBuffer, bytesInBuffer, afd.f );
SafeFS_Write( padding, paddingSize, afd.f );
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.a.totalBytes += bytesInBuffer;
afd.audioFormat.totalBytes += bytesInBuffer;
// Index
bufIndex = 0;
// Write index data
if (!afd.isPiped) {
bufferWriteIndex = 0;
WRITE_STRING("01wb"); //dwIdentifier
WRITE_4BYTES(0); //dwFlags
WRITE_4BYTES(chunkOffset); //dwOffset
WRITE_4BYTES(bytesInBuffer); //dwLength
SafeFS_Write( buffer, 16, afd.idxF );
Safe_FS_Write(buffer, 16, afd.indexFile);
}
afd.numIndices++;
@ -564,109 +580,98 @@ void CL_WriteAVIAudioFrame( const byte *pcmBuffer, int size )
}
}
/*
===============
CL_TakeVideoFrame
===============
*/
void CL_TakeVideoFrame( void )
void CL_TakeVideoFrame()
{
// AVI file isn't open
if( !afd.fileOpen )
if (!afd.isFileOpen)
return;
re.TakeVideoFrame( afd.width, afd.height,
afd.cBuffer, afd.eBuffer, afd.motionJpeg );
re.TakeVideoFrame(afd.width, afd.height, afd.captureBuffer, afd.encodeBuffer, afd.useMotionJpeg);
}
/*
===============
CL_CloseAVI
Closes the AVI file and writes an index chunk
===============
*/
static qbool CloseAVI( closeMode_t closeMode )
static qbool FixIndices()
{
int indexRemainder;
int indexSize = afd.numIndices * 16;
const char *idxFileName = va( "%s" INDEX_FILE_EXTENSION, afd.fileName );
// AVI file isn't open
if( !afd.fileOpen )
return qfalse;
afd.fileOpen = qfalse;
FS_Seek( afd.idxF, 4, FS_SEEK_SET );
bufIndex = 0;
FS_Seek(afd.indexFile, 4, FS_SEEK_SET);
bufferWriteIndex = 0;
WRITE_4BYTES(indexSize);
SafeFS_Write( buffer, bufIndex, afd.idxF );
FS_FCloseFile( afd.idxF );
// Write index
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
if( ( indexSize = FS_FOpenFileRead( idxFileName,
&afd.idxF, qtrue ) ) <= 0 )
{
FS_FCloseFile( afd.f );
const char* const idxFileName = va("%s" INDEX_FILE_EXTENSION, afd.fileName);
if ((indexSize = FS_FOpenFileRead(idxFileName, &afd.indexFile, qtrue)) <= 0) {
return qfalse;
}
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 );
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.idxF );
SafeFS_Write( buffer, indexRemainder, afd.f );
FS_Read(buffer, indexRemainder, afd.indexFile);
Safe_FS_Write(buffer, indexRemainder, afd.mainFile);
afd.fileSize += indexRemainder;
FS_FCloseFile( afd.idxF );
// Remove temp index file
// Close and remove temp index file
Safe_FS_Close(&afd.indexFile);
FS_HomeRemove(idxFileName);
// Write the real header
FS_Seek( afd.f, 0, FS_SEEK_SET );
FS_Seek(afd.mainFile, 0, FS_SEEK_SET);
CL_WriteAVIHeader();
bufIndex = 4;
bufferWriteIndex = 4;
WRITE_4BYTES(afd.fileSize - 8); // "RIFF" size
bufIndex = afd.moviOffset + 4; // Skip "LIST"
bufferWriteIndex = 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();
Safe_FS_Write(buffer, bufferWriteIndex, afd.mainFile);
return qtrue;
}
qbool CL_CloseAVI( void )
// Closes the AVI file and optionally writes an index chunk
static qbool CloseAVI( closeMode_t closeMode )
{
if (!afd.isFileOpen)
return qfalse;
afd.isFileOpen = qfalse;
qbool result = qtrue;
if (!afd.isPiped) {
result = FixIndices();
}
Safe_Z_Free(&afd.captureBuffer);
Safe_Z_Free(&afd.encodeBuffer);
Safe_FS_Close(&afd.mainFile);
Safe_FS_Close(&afd.indexFile);
Com_Printf("Processed %d:%d V:A frames for %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName);
if (closeMode == CM_SEQUENCE_COMPLETE) {
S_StopAllSounds();
}
return result;
}
qbool CL_CloseAVI()
{
return CloseAVI(CM_SEQUENCE_COMPLETE);
}
/*
===============
CL_VideoRecording
===============
*/
qbool CL_VideoRecording( void )
qbool CL_VideoRecording()
{
return afd.fileOpen;
return afd.isFileOpen;
}

View File

@ -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();

View File

@ -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",

View File

@ -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

View File

@ -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,8 +721,12 @@ 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) {
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

View File

@ -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 );

View File

@ -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();