/* =========================================================================== Copyright (C) 2005-2006 Tim Angus This file is part of Quake III Arena source code. Quake III Arena source code is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Quake III Arena source code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License 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 =========================================================================== */ #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; int sampleSize; unsigned int totalBytes; } audioFormat_t; typedef struct aviFileData_s { qboolean fileOpen; qboolean pipe; fileHandle_t f; char fileName[ MAX_QPATH ]; unsigned int fileSize; unsigned int moviOffset; unsigned int moviSize; fileHandle_t idxF; int numIndices; int frameRate; int framePeriod; int width, height; int numVideoFrames; int maxRecordSize; qboolean motionJpeg; qboolean audio; audioFormat_t a; int numAudioFrames; int audioFrameSize; int chunkStack[ MAX_RIFF_CHUNKS ]; int chunkStackTop; byte *cBuffer, *eBuffer; } aviFileData_t; static aviFileData_t afd; #define MAX_AVI_BUFFER 2048 static byte buffer[ MAX_AVI_BUFFER ]; static int bufIndex; /* =============== SafeFS_Write =============== */ static ID_INLINE void SafeFS_Write( const void *buf, int len, fileHandle_t f ) { if ( FS_Write( buf, len, f ) < len ) Com_Error( ERR_DROP, "Failed to write avi file" ); } /* =============== WRITE_STRING =============== */ static ID_INLINE void WRITE_STRING( const char *s ) { Com_Memcpy( &buffer[ bufIndex ], s, strlen( s ) ); bufIndex += strlen( s ); } /* =============== 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; } /* =============== WRITE_2BYTES =============== */ static ID_INLINE void WRITE_2BYTES( int x ) { buffer[ bufIndex + 0 ] = (byte)( ( x >> 0 ) & 0xFF ); buffer[ bufIndex + 1 ] = (byte)( ( x >> 8 ) & 0xFF ); bufIndex += 2; } /* =============== 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" ); } else { afd.chunkStack[ afd.chunkStackTop ] = bufIndex; afd.chunkStackTop++; WRITE_STRING( s ); WRITE_4BYTES( 0 ); } } /* =============== END_CHUNK =============== */ static ID_INLINE void END_CHUNK( void ) { int endIndex = bufIndex; if( afd.chunkStackTop <= 0 ) { Com_Error( ERR_DROP, "ERROR: Bottom of chunkstack breached" ); } else { afd.chunkStackTop--; bufIndex = afd.chunkStack[ afd.chunkStackTop ]; bufIndex += 4; WRITE_4BYTES( endIndex - bufIndex - 4 ); bufIndex = endIndex; bufIndex = PAD( bufIndex, 2 ); } } /* =============== CL_WriteAVIHeader =============== */ static void CL_WriteAVIHeader( void ) { bufIndex = 0; afd.chunkStackTop = 0; 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.pipe ) WRITE_4BYTES( 0x100 ); //dwFlags bits IS_INTERLEAVED=0x100 else WRITE_4BYTES( 0x110 ); //dwFlags bits HAS_INDEX=0x10 and IS_INTERLEAVED=0x100 WRITE_4BYTES( afd.numVideoFrames ); //dwTotalFrames WRITE_4BYTES( 0 ); //dwInitialFrame 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 ] START_CHUNK( "LIST" ); { WRITE_STRING( "strl" ); WRITE_STRING( "strh" ); WRITE_4BYTES( 56 ); //"strh" "chunk" size WRITE_STRING( "vids" ); if ( afd.motionJpeg && !afd.pipe ) WRITE_STRING( "MJPG" ); else WRITE_4BYTES( 0 ); // BI_RGB WRITE_4BYTES( 0 ); //dwFlags WRITE_4BYTES( 0 ); //dwPriority WRITE_4BYTES( 0 ); //dwInitialFrame WRITE_4BYTES( 1 ); //dwTimescale WRITE_4BYTES( afd.frameRate ); //dwDataRate WRITE_4BYTES( 0 ); //dwStartTime WRITE_4BYTES( afd.numVideoFrames ); //dwDataLength 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( 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 if( afd.motionJpeg && !afd.pipe ) //biCompression { WRITE_STRING( "MJPG" ); WRITE_4BYTES( afd.width * afd.height ); //biSizeImage } else { WRITE_4BYTES( 0 ); // BI_RGB WRITE_4BYTES( afd.width * afd.height * 3 ); //biSizeImage } WRITE_4BYTES( 0 ); //biXPelsPetMeter WRITE_4BYTES( 0 ); //biYPelsPetMeter WRITE_4BYTES( 0 ); //biClrUsed WRITE_4BYTES( 0 ); //biClrImportant } END_CHUNK( ); 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( 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( 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_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( ); afd.moviOffset = bufIndex; START_CHUNK( "LIST" ); { WRITE_STRING( "movi" ); } } } } static qboolean CL_ValidatePipeFormat( const char *s ) { while ( *s != '\0' ) { if ( *s == '.' && *(s+1) == '.' && ( *(s+2) == '/' || *(s+2) == '\\' ) ) return qfalse; if ( *s == ':' && *(s+1) == ':' ) return qfalse; if ( *s == '>' || *s == '|' || *s == '&' ) return qfalse; s++; } return qtrue; } /* =============== CL_OpenAVIForWriting Creates an AVI file and gets it into a state where writing the actual data can begin =============== */ qboolean CL_OpenAVIForWriting( const char *fileName, qboolean pipe, qboolean reopen ) { if ( afd.fileOpen ) return qfalse; if ( reopen ) { // keep currently allocated buffers byte *cBuffer = afd.cBuffer; byte *eBuffer = afd.eBuffer; Com_Memset( &afd, 0, sizeof( aviFileData_t ) ); afd.cBuffer = cBuffer; afd.eBuffer = eBuffer; } else { Com_Memset( &afd, 0, sizeof( aviFileData_t ) ); } if ( pipe ) { char cmd[MAX_OSPATH * 4]; const char *cmd_fmt = "ffmpeg -f avi -i - -threads 0 -y %s \"%s\" 2> \"%s-log.txt\""; const char *ospath; if ( !CL_ValidatePipeFormat( cl_aviPipeFormat->string ) ) { Com_Printf( S_COLOR_YELLOW "Invalid pipe format: %s\n", cl_aviPipeFormat->string ); return qfalse; } ospath = FS_BuildOSPath( Cvar_VariableString( "fs_homepath" ), "", fileName ); Com_sprintf( cmd, sizeof( cmd ), cmd_fmt, cl_aviPipeFormat->string, ospath, ospath ); if ( (afd.f = FS_PipeOpenWrite( cmd, fileName )) == FS_INVALID_HANDLE ) return qfalse; } else { if ( (afd.f = FS_FOpenFileWrite( fileName )) == FS_INVALID_HANDLE ) return qfalse; if ( (afd.idxF = FS_FOpenFileWrite( va( "%s" INDEX_FILE_EXTENSION, fileName ) )) == FS_INVALID_HANDLE ) { FS_FCloseFile( afd.f ); return qfalse; } } Q_strncpyz( afd.fileName, fileName, sizeof( afd.fileName ) ); afd.frameRate = cl_aviFrameRate->integer; afd.framePeriod = (int)(1000000.0 / afd.frameRate); afd.width = cls.captureWidth; afd.height = cls.captureHeight; if ( cl_aviMotionJpeg->integer && !pipe ) afd.motionJpeg = qtrue; else afd.motionJpeg = qfalse; if ( !reopen ) { // Buffers only need to store RGB pixels. // Allocate a bit more space for the capture buffer to account for possible // padding at the end of pixel lines, and padding for alignment #define MAX_PACK_LEN 16 //afd.cBuffer = Z_Malloc((afd.width * 3 + MAX_PACK_LEN - 1) * afd.height + MAX_PACK_LEN - 1); afd.cBuffer = Z_Malloc( (afd.width * afd.height * 4) + MAX_PACK_LEN - 1 ); // allocate for RGBA storage // raw avi files have pixel lines start on 4-byte boundaries afd.eBuffer = Z_Malloc( PAD( afd.width * 3, AVI_LINE_PADDING ) * afd.height ); } afd.a.rate = dma.speed; afd.a.format = dma.isfloat ? WAVE_FORMAT_IEEE_FLOAT : WAV_FORMAT_PCM; afd.a.channels = dma.channels; afd.a.bits = dma.samplebits; afd.a.sampleSize = (afd.a.bits * afd.a.channels) / 8; afd.audioFrameSize = ceil( (float)(afd.a.rate * afd.a.sampleSize) / (float)afd.frameRate ); if ( Cvar_VariableIntegerValue( "s_initsound" ) == 0 ) { afd.audio = qfalse; } else { afd.audio = qtrue; } // This doesn't write a real header, but allocates the // correct amount of space at the beginning of the file CL_WriteAVIHeader(); if ( pipe ) { afd.pipe = qtrue; SafeFS_Write( buffer, bufIndex, afd.f ); bufIndex = 0; } else { SafeFS_Write( buffer, bufIndex, afd.f ); afd.fileSize = bufIndex; bufIndex = 0; START_CHUNK( "idx1" ); SafeFS_Write( buffer, bufIndex, afd.idxF ); afd.moviSize = 4; // For the "movi" } afd.fileOpen = qtrue; return qtrue; } #define AVI_SEGMENT_SIZE UINT_MAX /* =============== CL_CheckFileSize =============== */ static qboolean CL_CheckFileSize( int bytesToAdd ) { unsigned int newFileSize; if ( afd.pipe ) { return qfalse; } 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 ) if ( newFileSize >= AVI_SEGMENT_SIZE || newFileSize < afd.fileSize ) { // Close the current file... CL_CloseAVI( qtrue ); // ...And open a new one CL_OpenAVIForWriting( va( "%s-%02d.avi", clc.videoName, ++clc.videoIndex ), qfalse, qtrue ); return qtrue; } return qfalse; } /* =============== CL_WriteAVIVideoFrame =============== */ void CL_WriteAVIVideoFrame( const byte *imageBuffer, int size ) { unsigned int chunkOffset; int chunkSize = 8 + size; int paddingSize = PADLEN( size, 2 ); byte padding[ 4 ] = { 0 }; if ( !afd.fileOpen ) return; // Chunk header + contents + padding CL_CheckFileSize( chunkSize + paddingSize ); chunkOffset = afd.fileSize - afd.moviOffset - 8; 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.numVideoFrames++; if ( size > afd.maxRecordSize ) afd.maxRecordSize = size; if ( afd.pipe ) return; afd.fileSize += ( chunkSize + paddingSize ); afd.moviSize += ( chunkSize + paddingSize ); // 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++; } #define PCM_BUFFER_SIZE 44100 static byte pcmCaptureBuffer[ PCM_BUFFER_SIZE ]; static int bytesInBuffer = 0; /* =============== CL_FlushAudioBuffer =============== */ static void CL_FlushCaptureBuffer( void ) { unsigned int chunkOffset = afd.fileSize - afd.moviOffset - 8; int chunkSize = 8 + bytesInBuffer; int paddingSize = PADLEN( bytesInBuffer, 2 ); byte padding[ 4 ] = { 0 }; if ( !bytesInBuffer ) return; 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.numAudioFrames++; if ( !afd.pipe ) { afd.fileSize += ( chunkSize + paddingSize ); 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; } /* =============== CL_WriteAVIAudioFrame =============== */ void CL_WriteAVIAudioFrame( const byte *pcmBuffer, int size ) { if ( !afd.audio ) return; if ( !afd.fileOpen ) return; // Chunk header + contents + padding CL_CheckFileSize( 8 + bytesInBuffer + size + 2 ); if ( bytesInBuffer + size > PCM_BUFFER_SIZE ) { Com_Printf( S_COLOR_YELLOW "WARNING: Audio capture buffer overflow -- truncating\n" ); size = PCM_BUFFER_SIZE - bytesInBuffer; } // Only write if we have a frame's worth of audio if ( bytesInBuffer >= afd.audioFrameSize ) { CL_FlushCaptureBuffer(); } if ( pcmBuffer ) { Com_Memcpy( &pcmCaptureBuffer[ bytesInBuffer ], pcmBuffer, size ); bytesInBuffer += size; } } /* =============== 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 ); } /* =============== CL_CloseAVI Closes the AVI file and writes an index chunk =============== */ qboolean CL_CloseAVI( qboolean reopen ) { int indexRemainder; int indexSize; const char *idxFileName; // AVI file isn't open if ( !afd.fileOpen ) { return qfalse; } CL_FlushCaptureBuffer(); if ( !reopen ) { Z_Free( afd.cBuffer ); Z_Free( afd.eBuffer ); } if ( afd.pipe ) { Com_Printf( "Wrote %d:%d frames to pipe:%s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName ); FS_FCloseFile( afd.f ); afd.f = FS_INVALID_HANDLE; afd.fileOpen = qfalse; afd.pipe = qfalse; return qtrue; } idxFileName = va( "%s" INDEX_FILE_EXTENSION, afd.fileName ); indexSize = afd.numIndices * 16; afd.fileOpen = qfalse; FS_Seek( afd.idxF, 4, FS_SEEK_SET ); bufIndex = 0; WRITE_4BYTES( indexSize ); SafeFS_Write( buffer, bufIndex, afd.idxF ); FS_FCloseFile( afd.idxF ); afd.idxF = FS_INVALID_HANDLE; // Write index // Open the temp index file if ( ( indexSize = FS_Home_FOpenFileRead( idxFileName, &afd.idxF ) ) <= 0 ) { if ( afd.idxF != FS_INVALID_HANDLE ) { FS_FCloseFile( afd.idxF ); } FS_FCloseFile( afd.f ); 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 ); 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 ); FS_FCloseFile( afd.f ); Com_DPrintf( "Wrote %d:%d frames to %s\n", afd.numVideoFrames, afd.numAudioFrames, afd.fileName ); return qtrue; } /* =============== CL_VideoRecording =============== */ qboolean CL_VideoRecording( void ) { return afd.fileOpen; }