diff options
Diffstat (limited to 'video/quicktime_recorder.cpp')
| -rw-r--r-- | video/quicktime_recorder.cpp | 2146 |
1 files changed, 2146 insertions, 0 deletions
diff --git a/video/quicktime_recorder.cpp b/video/quicktime_recorder.cpp new file mode 100644 index 0000000..9ed4766 --- /dev/null +++ b/video/quicktime_recorder.cpp @@ -0,0 +1,2146 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +//---------------------------------------------------------------------------------------- + +#define WIN32_LEAN_AND_MEAN + +#include "quicktime_recorder.h" +#include "filesystem.h" + +#ifdef _WIN32 + #include "windows.h" +#endif + +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- + +#define SAFE_DISPOSE_HANDLE( _handle ) if ( _handle != nullptr ) { DisposeHandle( (Handle) _handle ); _handle = nullptr; } +#define SAFE_DISPOSE_GWORLD( _gworld ) if ( _gworld != nullptr ) { DisposeGWorld( _gworld ); _gworld = nullptr; } +#define SAFE_DISPOSE_MOVIE( _movie ) if ( _movie != nullptr ) { DisposeMovie( _movie ); ThreadSleep(10); Assert( GetMoviesError() == noErr ); _movie = nullptr; } + + +// Platform check +#if defined ( OSX ) || defined ( WIN32 ) + // platform is supported +#else + #error "Unsupported Platform for QuickTime" +#endif + + + +//----------------------------------------------------------------------------- +// Helper functions for copying and converting bitmaps +//----------------------------------------------------------------------------- +enum PixelComponent_t +{ + RED = 0, + GREEN, + BLUE, + ALPHA +}; + + +int GetBytesPerPixel( OSType pixelFormat ) +{ + int bpp = ( pixelFormat == k24BGRPixelFormat || pixelFormat == k24RGBPixelFormat ) ? 3 : + ( pixelFormat == k32BGRAPixelFormat || pixelFormat == k32RGBAPixelFormat ) ? 4 : 0; + + Assert( bpp > 0 ); + return bpp; +} + + +int GetPixelCompnentByteOffset( OSType format, PixelComponent_t component ) +{ + if ( component == RED ) + { + return ( format == k24RGBPixelFormat || format == k32RGBAPixelFormat ) ? 0 : + ( format == k24BGRPixelFormat || format == k32BGRAPixelFormat ) ? 2 : -1; + } + if ( component == GREEN ) + { + return ( format == k24RGBPixelFormat || format == k32RGBAPixelFormat ) ? 1 : + ( format == k24BGRPixelFormat || format == k32BGRAPixelFormat ) ? 1 : -1; + } + if ( component == BLUE ) + { + return ( format == k24RGBPixelFormat || format == k32RGBAPixelFormat ) ? 2 : + ( format == k24BGRPixelFormat || format == k32BGRAPixelFormat ) ? 0 : -1; + } + if ( component == ALPHA ) + { + return ( format == k32BGRAPixelFormat || format == k32RGBAPixelFormat ) ? 3 : -1; + } + + Assert( false ); + return -1; +} + + +bool CopyBitMapPixels( int width, int height, OSType srcFmt, byte *srcBase, int srcStride, OSType dstFmt, byte *dstBase, int dstStride ) +{ + AssertExitF( width > 0 && height > 0 && srcBase != nullptr && srcStride > 0 && dstBase != nullptr && dstStride > 0 ); + + // copy the bitmap pixels into our GWorld + if ( srcFmt == dstFmt ) // identical formats, memcopy each line + { + int srcLineSize = width * GetBytesPerPixel( srcFmt ); + AssertExitF( srcLineSize <= dstStride && srcLineSize <= srcStride ); + + for ( int y = 0; y < height; y++ ) + { + byte *src = srcBase + srcStride * y; + byte *dst = dstBase + dstStride * y; + memcpy( dst, src, srcLineSize ); + } + + return true; + } + + // ok, we got some byte swizzling to do.. get the info we need + int srcBPP = GetBytesPerPixel( srcFmt ); + int dstBPP = GetBytesPerPixel( dstFmt ); + + int rSrcIndex = GetPixelCompnentByteOffset( srcFmt, RED ); + int gSrcIndex = GetPixelCompnentByteOffset( srcFmt, GREEN ); + int bSrcIndex = GetPixelCompnentByteOffset( srcFmt, BLUE ); + int aSrcIndex = GetPixelCompnentByteOffset( srcFmt, ALPHA ); + + int rDstIndex = GetPixelCompnentByteOffset( dstFmt, RED ); + int gDstIndex = GetPixelCompnentByteOffset( dstFmt, GREEN ); + int bDstIndex = GetPixelCompnentByteOffset( dstFmt, BLUE ); + int aDstIndex = GetPixelCompnentByteOffset( dstFmt, ALPHA ); + + Assert( rSrcIndex >= 0 && gSrcIndex >= 0 && bSrcIndex >= 0 ); + + // 3 byte format to 3 byte format or a 4 byte format to a 3 byte format? + if ( dstBPP == 3 ) + { + for ( int y = 0; y < height; y++ ) + { + byte *src = srcBase + srcStride * y; + byte *dst = dstBase + dstStride * y; + + for ( int x = 0; x < width; x++, dst+=dstBPP, src+=srcBPP ) + { + dst[rDstIndex] = src[rSrcIndex]; + dst[gDstIndex] = src[gSrcIndex]; + dst[bDstIndex] = src[bSrcIndex]; + } + } + return true; + } + + AssertExitF( aDstIndex >= 0 ); + + // 3 byte format to 4 byte format? + if ( srcBPP == 3 && dstBPP == 4 ) + { + for ( int y = 0; y < height; y++ ) + { + byte *src = srcBase + srcStride * y; + byte *dst = dstBase + dstStride * y; + + for ( int x = 0; x < width; x++, dst+=dstBPP, src+=srcBPP ) + { + dst[rDstIndex] = src[rSrcIndex]; + dst[gDstIndex] = src[gSrcIndex]; + dst[bDstIndex] = src[bSrcIndex]; + dst[aDstIndex] = 0xFF; + } + } + return true; + } + + // 4 byte format to 4 byte format? + if ( srcBPP == 4 && dstBPP == 4 ) + { + for ( int y = 0; y < height; y++ ) + { + byte *src = srcBase + srcStride * y; + byte *dst = dstBase + dstStride * y; + + for ( int x = 0; x < width; x++, dst+=dstBPP, src+=srcBPP ) + { + dst[rDstIndex] = src[rSrcIndex]; + dst[gDstIndex] = src[gSrcIndex]; + dst[bDstIndex] = src[bSrcIndex]; + dst[aDstIndex] = src[aSrcIndex]; + } + } + return true; + } + + // didn't find the format? + Assert( false ); + return false; +} + + + +//----------------------------------------------------------------------------- +// Utility functions to save targa images +//----------------------------------------------------------------------------- +#pragma pack( push, 1 ) +struct TGA_Header +{ + public: + byte identsize; // size of ID field that follows 18 byte header (0 usually) + byte colourmaptype; // type of colour map 0=none, 1=has palette + byte imagetype; // type of image 0=none,1=indexed,2=rgb,3=grey,+8=rle packed + + short colourmapstart; // first colour map entry in palette + short colourmaplength; // number of colours in palette + byte colourmapbits; // number of bits per palette entry 15,16,24,32 + + short xstart; // image x origin + short ystart; // image y origin + short width; // image width in pixels + short height; // image height in pixels + byte bits; // image bits per pixel 8,16,24,32 + byte descriptor; // image descriptor bits (vh flip bits) + + // pixel data follows header +}; +#pragma pack(pop) + + + +void SaveToTargaFile( int frameNum, const char* pBaseFileName, int width, int height, void *pPixels, OSType PixelFormat, int strideAdjust ) +{ + if ( pBaseFileName == nullptr || pPixels== nullptr ) return; + + Assert( sizeof( TGA_Header ) == 18 ); + + TGA_Header theHeader; + ZeroVar( theHeader ); + + int BytesPerPixel = GetBytesPerPixel( PixelFormat ); + Assert( BytesPerPixel > 0 ); + + theHeader.imagetype = 2; + theHeader.width = (short) width; + theHeader.height = (short) height; + theHeader.colourmapbits = BytesPerPixel * 8; + theHeader.bits = BytesPerPixel * 8; + theHeader.descriptor = ( BytesPerPixel == 4) ? ( 8 | 32 ) : 32; // Targa32, Upper Left Origin, attribute (alpha) bits in bits 0-3 + + char TGAFileName[MAX_PATH]; + + V_snprintf( TGAFileName, MAX_PATH, "%s%.4d.tga", pBaseFileName, frameNum ); + + FileHandle_t TGAFile = g_pFullFileSystem->Open( TGAFileName, "wb" ); + + g_pFullFileSystem->Write( &theHeader, sizeof( theHeader ), TGAFile ); + + // is the buffer in BGR format? + if ( PixelFormat == k24BGRPixelFormat || PixelFormat == k32BGRAPixelFormat ) + { + if ( strideAdjust == 0 ) + { + g_pFullFileSystem->Write( pPixels, width * height * BytesPerPixel, TGAFile ); + } + else + { + int lineWidth = width * BytesPerPixel; + int lineOffset = lineWidth + strideAdjust; + for ( int y = 0; y < height; y++ ) + { + byte *pData = (byte*) pPixels + ( y * lineOffset ); + g_pFullFileSystem->Write( pData, lineWidth, TGAFile ); + } + } + + } + else // we need to convert the bits from RGB to BGR + { + byte *pData = new byte[width * height * BytesPerPixel]; + + OSType tgaFormat = ( PixelFormat == k24RGBPixelFormat ) ? k24BGRPixelFormat : + ( PixelFormat == k32RGBAPixelFormat ) ? k32BGRAPixelFormat : 0; + + CopyBitMapPixels( width, height, PixelFormat, (byte*) pPixels, width * BytesPerPixel + strideAdjust, tgaFormat, pData, width * BytesPerPixel ); + + g_pFullFileSystem->Write( pData, width * height * BytesPerPixel, TGAFile ); + + delete [] pData; + } + + g_pFullFileSystem->Close( TGAFile ); + +} + + + + +// =========================================================================== +// Data tables used to estimate file size +// =========================================================================== +enum EstVideoEncodeQuality_t +{ + cVEQuality_Min = 0, + cVEQuality_Low = 25, + cVEQuality_Normal = 50, + cVEQuality_High = 75, + cVEQuality_Max = 100 +}; + + +struct EncodingDataRateInfo_t +{ + EstVideoEncodeQuality_t m_QualitySetting; + int m_XResolution; + int m_YResolution; + float m_DataRate; // in MBits / second +}; + + +struct VideoRes_t +{ + int X, Y; +}; + + +static EstVideoEncodeQuality_t s_QualityPresets[] = +{ + cVEQuality_Min, + cVEQuality_Low, + cVEQuality_Normal, + cVEQuality_High, + cVEQuality_Max +}; + + +static VideoRes_t s_ResolutionPresets[] = +{ + { 16, 16 }, + { 720, 480 }, + { 640, 960 }, + { 960, 640 }, + { 1280, 720 }, + { 1920, 1080 }, + { 2048, 2048 }, +}; + + +static EncodingDataRateInfo_t s_H264EncodeRates[] = +{ + { cVEQuality_Min, 16, 160, 2.00f }, + { cVEQuality_Min, 720, 480, 2.26f }, + { cVEQuality_Min, 640, 960, 2.73f }, + { cVEQuality_Min, 960, 640, 2.91f }, + { cVEQuality_Min, 1280, 720, 3.56f }, + { cVEQuality_Min, 1920, 1080, 5.6f }, + { cVEQuality_Min, 2048, 2048, 6.6f }, + + { cVEQuality_Low, 16, 160, 3.00f }, + { cVEQuality_Low, 720, 480, 3.65f }, + { cVEQuality_Low, 640, 960, 4.57f }, + { cVEQuality_Low, 960, 640, 5.03f }, + { cVEQuality_Low, 1280, 720, 6.41f }, + { cVEQuality_Low, 1920, 1080, 10.57f }, + { cVEQuality_Low, 2048, 2048, 13.0f }, + + { cVEQuality_Normal, 16, 160, 5.00f }, + { cVEQuality_Normal, 720, 480, 6.4f }, + { cVEQuality_Normal, 640, 960, 8.25f }, + { cVEQuality_Normal, 960, 640, 9.24f }, + { cVEQuality_Normal, 1280, 720, 12.1f }, + { cVEQuality_Normal, 1920, 1080, 20.64f }, + { cVEQuality_Normal, 2048, 2048, 25.0f }, + + { cVEQuality_High, 16, 160, 9.50f }, + { cVEQuality_High, 720, 480, 11.3f }, + { cVEQuality_High, 640, 960, 15.06f }, + { cVEQuality_High, 960, 640, 16.9f }, + { cVEQuality_High, 1280, 720, 22.72 }, + { cVEQuality_High, 1920, 1080, 40.06f }, + { cVEQuality_High, 2048, 2048, 52.5f }, + + { cVEQuality_Max, 16, 160, 15.50f }, + { cVEQuality_Max, 720, 480, 19.33f }, + { cVEQuality_Max, 640, 960, 29.89f }, + { cVEQuality_Max, 960, 640, 26.82f }, + { cVEQuality_Max, 1280, 720, 41.08f }, + { cVEQuality_Max, 1920, 1080, 75.14f }, + { cVEQuality_Max, 2048, 2048, 90.0f }, + +}; + + + + +// =========================================================================== +// CQuickTimeVideoRecorder class - implements IVideoRecorder interface for +// QuickTime, and buffers commands to the actual encoder object +// =========================================================================== +CQuickTimeVideoRecorder::CQuickTimeVideoRecorder() : + m_pEncoder( nullptr ), + m_LastResult( VideoResult::SUCCESS ), + m_bHasAudio( false ), + m_bMovieFinished( false ) +{ + +} + + +CQuickTimeVideoRecorder::~CQuickTimeVideoRecorder() +{ + if ( m_pEncoder != nullptr ) + { + // Abort any encoding in progress + if ( !m_bMovieFinished ) + { + AbortMovie(); + } + SAFE_DELETE( m_pEncoder ); + } +} + + +bool CQuickTimeVideoRecorder::CreateNewMovieFile( const char *pFilename, bool hasAudio ) +{ + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_NOT_EMPTY( pFilename ) ); + + SetResult( VideoResult::OPERATION_ALREADY_PERFORMED ); + AssertExitF( m_pEncoder == nullptr && !m_bMovieFinished ); + + // Create new video recorder + m_pEncoder = new CQTVideoFileComposer(); + if ( !m_pEncoder->CreateNewMovie( pFilename, hasAudio ) ) + { + SetResult( m_pEncoder->GetResult() ); // save the error result for after the encoder goes poof + SAFE_DELETE( m_pEncoder ); + return false; + } + + m_bHasAudio = hasAudio; + + SetResult( VideoResult::SUCCESS ); + return true; +} + + +bool CQuickTimeVideoRecorder::SetMovieVideoParameters( VideoEncodeCodec_t theCodec, int videoQuality, int movieFrameWidth, int movieFrameHeight, VideoFrameRate_t movieFPS, VideoEncodeGamma_t gamma ) +{ + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_IN_RANGECOUNT( theCodec, VideoEncodeCodec::DEFAULT_CODEC, VideoEncodeCodec::CODEC_COUNT ) ); + AssertExitF( IS_IN_RANGE( videoQuality, VideoEncodeQuality::MIN_QUALITY, VideoEncodeQuality::MAX_QUALITY ) ); + AssertExitF( IS_IN_RANGE( movieFrameWidth, cMinVideoFrameWidth, cMaxVideoFrameWidth ) && IS_IN_RANGE( movieFrameHeight, cMinVideoFrameHeight, cMaxVideoFrameHeight ) ); + AssertExitF( IS_IN_RANGE( movieFPS.GetFPS(), cMinFPS, cMaxFPS ) ); + AssertExitF( IS_IN_RANGECOUNT( gamma, VideoEncodeGamma::NO_GAMMA_ADJUST, VideoEncodeGamma::GAMMA_COUNT ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_pEncoder != nullptr && !m_bMovieFinished ); + + return m_pEncoder->SetMovieVideoParameters( movieFrameWidth, movieFrameHeight, movieFPS, theCodec, videoQuality, gamma ); +} + + +bool CQuickTimeVideoRecorder::SetMovieSourceImageParameters( VideoEncodeSourceFormat_t srcImageFormat, int imgWidth, int imgHeight ) +{ + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_IN_RANGECOUNT( srcImageFormat, VideoEncodeSourceFormat::VIDEO_FORMAT_FIRST, VideoEncodeSourceFormat::VIDEO_FORMAT_COUNT ) ); + AssertExitF( IS_IN_RANGE( imgWidth, cMinVideoFrameWidth, cMaxVideoFrameWidth ) && IS_IN_RANGE( imgHeight, cMinVideoFrameHeight, cMaxVideoFrameHeight ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_pEncoder != nullptr && !m_bMovieFinished ); + + return m_pEncoder->SetMovieSourceImageParameters( imgWidth, imgHeight, srcImageFormat ); +} + + +bool CQuickTimeVideoRecorder::SetMovieSourceAudioParameters( AudioEncodeSourceFormat_t srcAudioFormat, int audioSampleRate, AudioEncodeOptions_t audioOptions, int audioSampleGroupSize ) +{ + SetResult( VideoResult::ILLEGAL_OPERATION ); + AssertExitF( m_bHasAudio ); + + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_IN_RANGECOUNT( srcAudioFormat, AudioEncodeSourceFormat::AUDIO_NONE, AudioEncodeSourceFormat::AUDIO_FORMAT_COUNT ) ); + AssertExitF( audioSampleRate == 0 || IS_IN_RANGE( audioSampleRate, cMinSampleRate, cMaxSampleRate ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_pEncoder != nullptr && !m_bMovieFinished ); + + bool result = m_pEncoder->SetMovieSourceAudioParameters( srcAudioFormat, audioSampleRate, audioOptions, audioSampleGroupSize ); + + m_bHasAudio = m_pEncoder->HasAudio(); // Audio can be turned off after specifying, so reload status + return result; +} + + +bool CQuickTimeVideoRecorder::IsReadyToRecord() +{ + return ( m_pEncoder == nullptr || m_bMovieFinished ) ? false : m_pEncoder->IsReadyToRecord(); +} + + +VideoResult_t CQuickTimeVideoRecorder::GetLastResult() +{ + return ( m_pEncoder == nullptr ) ? m_LastResult : m_pEncoder->GetResult(); +} + + +void CQuickTimeVideoRecorder::SetResult( VideoResult_t resultCode ) +{ + m_LastResult = resultCode; + if ( m_pEncoder != nullptr ) + { + m_pEncoder->SetResult( resultCode ); + } +} + + +bool CQuickTimeVideoRecorder::AppendVideoFrame( void *pFrameBuffer, int nStrideAdjustBytes ) +{ + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( pFrameBuffer != nullptr ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( IsReadyToRecord() ); + + return m_pEncoder->AppendVideoFrameToMedia( pFrameBuffer, nStrideAdjustBytes ); +} + + +bool CQuickTimeVideoRecorder::AppendAudioSamples( void *pSampleBuffer, size_t sampleSize ) +{ + SetResult( VideoResult::ILLEGAL_OPERATION ); + AssertExitF( m_bHasAudio ); + + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( pSampleBuffer != nullptr ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( IsReadyToRecord() ); + + return m_pEncoder->AppendAudioSamplesToMedia( pSampleBuffer, sampleSize ); +} + + +int CQuickTimeVideoRecorder::GetFrameCount() +{ + return ( m_pEncoder == nullptr ) ? 0 : m_pEncoder->GetFrameCount(); +} + + +int CQuickTimeVideoRecorder::GetSampleCount() +{ + return ( m_pEncoder == nullptr ) ? 0 : m_pEncoder->GetSampleCount(); +} + + +VideoFrameRate_t CQuickTimeVideoRecorder::GetFPS() +{ + return ( m_pEncoder == nullptr ) ? VideoFrameRate_t( 0 ) : m_pEncoder->GetFPS(); +} + + +int CQuickTimeVideoRecorder::GetSampleRate() +{ + return ( m_pEncoder == nullptr ) ? 0 : m_pEncoder->GetSampleRate(); +} + + +bool CQuickTimeVideoRecorder::AbortMovie() +{ + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_pEncoder != nullptr && !m_bMovieFinished ); + + m_bMovieFinished = true; + return m_pEncoder->AbortMovie(); +} + + +bool CQuickTimeVideoRecorder::FinishMovie( bool SaveMovieToDisk ) +{ + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_pEncoder != nullptr && !m_bMovieFinished ); + + m_bMovieFinished = true; + return m_pEncoder->FinishMovie( SaveMovieToDisk ); +} + + +#ifdef ENABLE_EXTERNAL_ENCODER_LOGGING +bool CQuickTimeVideoRecorder::LogMessage( const char *pMsg ) +{ + if ( m_pEncoder != nullptr ) + { + m_pEncoder->LogMessage( pMsg ); + } + + return true; +} +#endif + +bool CQuickTimeVideoRecorder::EstimateMovieFileSize( size_t *pEstSize, int movieWidth, int movieHeight, VideoFrameRate_t movieFps, float movieDuration, VideoEncodeCodec_t theCodec, int videoQuality, AudioEncodeSourceFormat_t srcAudioFormat, int audioSampleRate ) +{ + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertPtrExitF( pEstSize ); + *pEstSize = 0; + + AssertExitF( IS_IN_RANGE( movieWidth, cMinVideoFrameWidth, cMaxVideoFrameWidth ) && IS_IN_RANGE( movieHeight, cMinVideoFrameHeight, cMaxVideoFrameHeight ) ); + AssertExitF( IS_IN_RANGE( movieFps.GetFPS(), cMinFPS, cMaxFPS ) && movieDuration > 0.0f ); + AssertExitF( IS_IN_RANGECOUNT( theCodec, VideoEncodeCodec::DEFAULT_CODEC, VideoEncodeCodec::CODEC_COUNT ) ); + AssertExitF( IS_IN_RANGE( videoQuality, VideoEncodeQuality::MIN_QUALITY, VideoEncodeQuality::MAX_QUALITY ) ); + AssertExitF( IS_IN_RANGECOUNT( srcAudioFormat, AudioEncodeSourceFormat::AUDIO_NONE, AudioEncodeSourceFormat::AUDIO_FORMAT_COUNT ) ); + AssertExitF( audioSampleRate == 0 || IS_IN_RANGE( audioSampleRate, cMinSampleRate, cMaxSampleRate ) ); + + // Determine the Quality LERP + int Q1 = VideoEncodeQuality::MIN_QUALITY, Q2 = VideoEncodeQuality::MAX_QUALITY; + float Qlerp = 0.0f; + bool bQLerp = true; + + for ( int i = 0; i < ARRAYSIZE( s_QualityPresets ); i++ ) + { + if ( s_QualityPresets[i] == videoQuality ) + { + Q1 = videoQuality; + Q2 = videoQuality; + Qlerp = 0.0f; + bQLerp = false; + break; + } + else if ( s_QualityPresets[i] < videoQuality && s_QualityPresets[i] > Q1 ) + { + Q1 = s_QualityPresets[i]; + } + else if ( s_QualityPresets[i] > videoQuality && s_QualityPresets[i] < Q2 ) + { + Q2 = s_QualityPresets[i]; + } + } + + if ( bQLerp ) + { + Qlerp = ( (float) videoQuality - (float) Q1 ) / ( (float) Q2 - (float) Q1 ) ; + } + + // determine the resolution lerp + + VideoRes_t RES1 = { cMinVideoFrameWidth, cMinVideoFrameHeight }, RES2 = { cMaxVideoFrameWidth, cMaxVideoFrameHeight }; + float RLerp = 0.0f; + bool bRLerp = true; + int nPixels = movieHeight * movieWidth; + int R1pixels = RES1.X * RES1.Y; + int R2pixels = RES2.X * RES2.Y; + + for ( int i = 0; i < ARRAYSIZE( s_ResolutionPresets ); i++ ) + { + if ( s_ResolutionPresets[i].X == movieWidth && s_ResolutionPresets[i].Y == movieHeight ) + { + RES1 = s_ResolutionPresets[i]; + RES2 = s_ResolutionPresets[i]; + RLerp = 0.0f; + bRLerp = false; + break; + } + + int rPixels = s_ResolutionPresets[i].X * s_ResolutionPresets[i].Y; + + if ( rPixels <= nPixels && rPixels > R1pixels ) + { + RES1 = s_ResolutionPresets[i]; + R1pixels = rPixels; + } + else if ( rPixels > nPixels && rPixels < R2pixels ) + { + RES2 = s_ResolutionPresets[i]; + R2pixels = rPixels; + } + } + + if ( bRLerp ) + { + RLerp = (float) (nPixels - R1pixels) / (float) ( R2pixels - R1pixels ); + } + + + // Now we see what we need to do + // We determine the estimated Data Rate + + float DR = 0.0f; + + if ( bQLerp == false && bRLerp == false ) + { + DR = GetDataRate( videoQuality, movieWidth, movieHeight ); + } + else if ( bQLerp == true && bRLerp == false ) + { + float D1 = GetDataRate( Q1, movieWidth, movieHeight ); + float D2 = GetDataRate( Q2, movieWidth, movieHeight ); + + DR = D1 + Qlerp * ( D2 - D1 ); + } + else if ( bQLerp == false && bRLerp == true ) + { + float D1 = GetDataRate( videoQuality, RES1.X, RES1.Y ); + float D2 = GetDataRate( videoQuality, RES2.X, RES2.Y ); + + DR = D1 + RLerp * ( D2 - D1 ); + + } + else // need the full filter + { + float D1 = GetDataRate( Q1, RES1.X, RES1.Y ); + float D2 = GetDataRate( Q1, RES2.X, RES2.Y ); + float D3 = GetDataRate( Q2, RES1.X, RES1.Y ); + float D4 = GetDataRate( Q2, RES2.X, RES2.Y ); + + float I1 = D1 + Qlerp * ( D3 - D1 ); + float I2 = D2 + Qlerp * ( D4 - D2 ); + + DR = I1 + RLerp * ( I2 - I1 ); + } + + // Now do the big computation + // should this be 1024 * 1024? + + double VideoData = DR * 1000000 / 8 * movieDuration ; + + // Quick hack to guess at audio data size + double audioData = 0; + + if ( srcAudioFormat == AudioEncodeSourceFormat::AUDIO_16BIT_PCMStereo ) + { + audioData = ( audioSampleRate * 2 ) * ( 0.05 * DR ); + } + + *pEstSize = (size_t) VideoData + (size_t) audioData; + + SetResult( VideoResult::SUCCESS ); + return true; +} + + +float CQuickTimeVideoRecorder::GetDataRate( int quality, int width, int height ) +{ + for (int i = 0; i < ARRAYSIZE( s_H264EncodeRates ); i++ ) + { + if ( s_H264EncodeRates[i].m_QualitySetting == quality && s_H264EncodeRates[i].m_XResolution == width && s_H264EncodeRates[i].m_YResolution == height ) + { + return s_H264EncodeRates[i].m_DataRate; + } + } + + Assert( false ); + return 0.0f; +} + + + + + +// ------------------------------------------------------------------------ +// CQTVideoFileComposer - Class to encapsulate the creation of a QuickTime +// Movie from a sequence of uncompressed images and (future) audio samples +// ------------------------------------------------------------------------ +CQTVideoFileComposer::CQTVideoFileComposer() : + m_LastResult( VideoResult::SUCCESS ), + + m_bMovieCreated( false ), + m_bHasAudioTrack( false ), + + m_bMovieConfigured( false ), + m_bSourceImagesConfigured( false ), + m_bSourceAudioConfigured( false ), + + m_bComposingMovie( false ), + m_bMovieCompleted( false ), + + m_nFramesAdded( 0 ), + m_nAudioFramesAdded( 0 ), + m_nSamplesAdded( 0 ), + m_nSamplesAddedToMedia( 0 ), + + m_MovieFrameWidth( 0 ), + m_MovieFrameHeight( 0 ), + + m_MovieTimeScale( 0 ), + m_DurationPerFrame( 0 ), + + m_AudioOptions( AudioEncodeOptions::NO_AUDIO_OPTIONS ), + m_SampleGrouping( AG_NONE ), + m_nAudioSampleGroupSize( 0 ), + + m_AudioSourceFrequency( 0 ), + m_AudioBytesPerSample( 0 ), + + m_bBufferSourceAudio( false ), + m_bLimitAudioDurationToVideo( false ), + + m_srcAudioBuffer( nullptr ), + m_srcAudioBufferSize( 0 ), + m_srcAudioBufferCurrentSize( 0 ), + + m_AudioSampleFrameCounter( 0 ), + + m_FileName( nullptr ), + + m_SrcImageWidth( 0 ), + m_SrcImageHeight( 0 ), + m_ScrImageMaxCompressedSize( 0 ), + m_SrcImageBuffer( nullptr ), + m_SrcImageCompressedBuffer( nullptr ), + m_SrcPixelFormat( 0 ), + m_SrcBytesPerPixel( 0 ), + + m_GWorldPixelFormat( 0 ), + m_GWorldBytesPerPixel( 0 ), + m_GWorldImageWidth( 0 ), + m_GWorldImageHeight( 0 ), + + m_srcSoundDescription( nullptr ), + + m_EncodeQuality( CQTVideoFileComposer::DEFAULT_ENCODE_QUALITY ), + m_VideoCodecToUse( CQTVideoFileComposer::DEFAULT_CODEC ), + m_EncodeGamma( CQTVideoFileComposer::DEFAULT_GAMMA ), + + m_theSrcGWorld( nullptr ), + + m_MovieFileDataRef( nullptr ), + m_MovieFileDataRefType( 0 ), + m_MovieFileDataHandler( nullptr ), + + m_theMovie( nullptr ), + m_theVideoTrack( nullptr), + m_theAudioTrack( nullptr ), + m_theVideoMedia( nullptr ), + m_theAudioMedia( nullptr ) + +#ifdef LOG_ENCODER_OPERATIONS + ,m_LogFile( FILESYSTEM_INVALID_HANDLE ) +#endif +{ + m_MovieRecordFPS.SetFPS( 0, false ); + m_GWorldRect.top = m_GWorldRect.left = m_GWorldRect.bottom = m_GWorldRect.right = 0; + +#ifdef LOG_FRAMES_TO_TGA + ZeroVar( m_TGAFileBase ); +#endif + +} + + +CQTVideoFileComposer::~CQTVideoFileComposer() +{ + if ( m_bComposingMovie ) + { + AbortMovie(); + } + +#ifdef LOG_ENCODER_OPERATIONS + if ( m_LogFile != FILESYSTEM_INVALID_HANDLE ) + { + g_pFullFileSystem->Close( m_LogFile ); + m_LogFile = FILESYSTEM_INVALID_HANDLE; + } + +#endif + + + SAFE_DELETE_ARRAY( m_FileName ); + SAFE_DELETE_ARRAY( m_SrcImageBuffer ); + SAFE_DELETE_ARRAY( m_srcAudioBuffer ); + + SAFE_DISPOSE_HANDLE( m_MovieFileDataRef ); + SAFE_DISPOSE_HANDLE( m_srcSoundDescription ); + SAFE_DISPOSE_HANDLE( m_SrcImageCompressedBuffer ); + SAFE_DISPOSE_GWORLD( m_theSrcGWorld ); + +} + + +#ifdef LOG_ENCODER_OPERATIONS +void CQTVideoFileComposer::LogMsg( const char* pMsg, ... ) +{ + const int MAX_TEXT = 8192; + static char messageBuf[MAX_TEXT]; + + if ( m_LogFile == FILESYSTEM_INVALID_HANDLE || pMsg == nullptr ) + { + return; + } + + va_list marker; + + va_start( marker, pMsg ); + +#ifdef _WIN32 + int len = _vsnprintf( messageBuf, MAX_TEXT, pMsg, marker ); +#elif POSIX + int len = vsnprintf( messageBuf, MAX_TEXT, pMsg, marker ); +#else + #error "define vsnprintf type." +#endif + + // Len < 0 represents an overflow + if( len < 0 ) + { + ((char*) pMsg)[MAX_TEXT-1] = nullchar; + } + va_end( marker ); + + g_pFullFileSystem->Write( messageBuf, V_strlen( messageBuf ), m_LogFile ); + +} +#endif + + +bool CQTVideoFileComposer::CreateNewMovie( const char *fileName, bool hasAudio ) +{ + // Validate input and state + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_NOT_EMPTY( fileName ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( !m_bMovieCreated && !m_bMovieCompleted ); + +#ifdef LOG_ENCODER_OPERATIONS + char logFileName[MAX_PATH]; + V_strncpy( logFileName, fileName, MAX_PATH ); + V_SetExtension( logFileName, ".log", MAX_PATH ); + m_LogFile = g_pFullFileSystem->Open( logFileName, "wb" ); + const char* aMsg = (hasAudio) ? "HAS" : "DOES NOT HAVE"; + LogMsg( "Creating Video File: '%s' - %s AUDIO TRACK\n", fileName, aMsg ); +#endif + + // now create the movie file + + OSErr status = noErr; + OSType movieType = FOUR_CHAR_CODE('TVOD'); // todo - change movie type?? + + m_MovieFileDataRef = nullptr; + m_MovieFileDataRefType = 0; + m_MovieFileDataHandler = nullptr; + + CFStringRef imageStrRef = CFStringCreateWithCString ( NULL, fileName, 0 ); + + status = QTNewDataReferenceFromFullPathCFString( imageStrRef, (QTPathStyle) kQTNativeDefaultPathStyle, 0, &m_MovieFileDataRef, &m_MovieFileDataRefType ); + AssertExitF( status == noErr ); + + + status = CreateMovieStorage( m_MovieFileDataRef, m_MovieFileDataRefType, movieType, smCurrentScript, createMovieFileDeleteCurFile | createMovieFileDontCreateResFile, &m_MovieFileDataHandler, &m_theMovie ); + AssertExitF( status == noErr ); + + CFRelease( imageStrRef ); + m_FileName = COPY_STRING( fileName ); + +#ifdef LOG_FRAMES_TO_TGA + V_strncpy( m_TGAFileBase, m_FileName, sizeof( m_TGAFileBase ) ); + V_StripExtension( m_TGAFileBase, m_TGAFileBase, sizeof( m_TGAFileBase ) ); +#endif + + // we did it! party on... + SetResult( VideoResult::SUCCESS ); + m_bMovieCreated = true; + m_bHasAudioTrack = hasAudio; + + return m_bMovieCreated; +} + + +bool CQTVideoFileComposer::SetMovieVideoParameters( int width, int height, VideoFrameRate_t movieFPS, VideoEncodeCodec_t desiredCodec, int encodeQuality, VideoEncodeGamma_t gamma ) +{ + // Validate input and state + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_IN_RANGE( width, cMinVideoFrameWidth, cMaxVideoFrameWidth ) && IS_IN_RANGE( height, cMinVideoFrameHeight, cMaxVideoFrameHeight ) ); + AssertExitF( IS_IN_RANGE( movieFPS.GetFPS(), cMinFPS, cMaxFPS ) ); + AssertExitF( IS_IN_RANGECOUNT( desiredCodec, VideoEncodeCodec::DEFAULT_CODEC, VideoEncodeCodec::CODEC_COUNT ) ); + AssertExitF( IS_IN_RANGE( encodeQuality, VideoEncodeQuality::MIN_QUALITY, VideoEncodeQuality::MAX_QUALITY ) ); + AssertExitF( IS_IN_RANGECOUNT( gamma, VideoEncodeGamma::NO_GAMMA_ADJUST, VideoEncodeGamma::GAMMA_COUNT ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bMovieCreated && !m_bMovieConfigured ); + + // Configure video parameters + m_MovieFrameWidth = width; + m_MovieFrameHeight = height; + + // map the requested codec in + switch( desiredCodec ) + { + case VideoEncodeCodec::MPEG2_CODEC: + { + m_VideoCodecToUse = kMpegYUV420CodecType; + break; + } + case VideoEncodeCodec::MPEG4_CODEC: + { + m_VideoCodecToUse = kMPEG4VisualCodecType; + break; + } + case VideoEncodeCodec::H261_CODEC: + { + m_VideoCodecToUse = kH261CodecType; + break; + } + case VideoEncodeCodec::H263_CODEC: + { + m_VideoCodecToUse = kH263CodecType; + break; + } + case VideoEncodeCodec::H264_CODEC: + { + m_VideoCodecToUse = kH264CodecType; + break; + } + case VideoEncodeCodec::MJPEG_A_CODEC: + { + m_VideoCodecToUse = kMotionJPEGACodecType; + break; + } + case VideoEncodeCodec::MJPEG_B_CODEC: + { + m_VideoCodecToUse = kMotionJPEGBCodecType; + break; + } + case VideoEncodeCodec::SORENSON3_CODEC: + { + m_VideoCodecToUse = kSorenson3CodecType; + break; + } + case VideoEncodeCodec::CINEPACK_CODEC: + { + m_VideoCodecToUse = kCinepakCodecType; + break; + } + default: // should never hit this because we are already range checked + { + m_VideoCodecToUse = CQTVideoFileComposer::DEFAULT_CODEC; + break; + } + } + + // Determine if codec is available... + CodecInfo theInfo; + + OSErr status = GetCodecInfo( &theInfo, m_VideoCodecToUse, 0 ); + if ( status == noCodecErr ) + { + SetResult( VideoResult::CODEC_NOT_AVAILABLE ); + return false; + } + AssertExitF( status == noErr ); + +#ifdef LOG_ENCODER_OPERATIONS + char codecName[64]; + ZeroVar( codecName ); + V_memcpy( codecName, &theInfo.typeName[1], (int) theInfo.typeName[0] ); + + LogMsg( "Video Image Size is (%d x %d)\n", m_MovieFrameWidth, m_MovieFrameHeight ); + LogMsg( "Codec selected is %s\n", codecName ); + LogMsg( "Encoding Quality = %d\n", (int) encodeQuality ); + LogMsg( "Encode Gamma = %d\n", (int) gamma ); +#endif + + // convert encoding quality into quicktime specific value + int Q = (int) encodeQuality; - (int) VideoEncodeQuality::MIN_QUALITY; + int MaxQ = (int) VideoEncodeQuality::MAX_QUALITY - (int) VideoEncodeQuality::MIN_QUALITY; + + m_EncodeQuality = codecLosslessQuality * ( (float) Q / (float) MaxQ ) ; + clamp( m_EncodeQuality, codecMinQuality, codecMaxQuality ); + + // convert the gamma correction value into quicktime specific values + switch( gamma ) + { + case VideoEncodeGamma::NO_GAMMA_ADJUST: + { + m_EncodeGamma = kQTUseSourceGammaLevel; + break; + } + case VideoEncodeGamma::PLATFORM_STANDARD_GAMMA: + { + m_EncodeGamma = kQTUsePlatformDefaultGammaLevel; + break; + } + case VideoEncodeGamma::GAMMA_1_8: + { + m_EncodeGamma = 0x0001CCCC; // (Fixed) Gamma 1.8 + break; + } + case VideoEncodeGamma::GAMMA_2_2: + { + m_EncodeGamma = kQTCCIR601VideoGammaLevel; + break; + } + case VideoEncodeGamma::GAMMA_2_5: + { + m_EncodeGamma = 0x00028000; // (Fixed) Gamma 2.5 + break; + } + default: + { + m_EncodeGamma = CQTVideoFileComposer::DEFAULT_GAMMA; + break; + } + } + + // Process the framerate into usable values + + m_MovieRecordFPS = movieFPS; + + m_DurationPerFrame = m_MovieRecordFPS.GetUnitsPerFrame(); + m_MovieTimeScale = m_MovieRecordFPS.GetUnitsPerSecond(); + + AssertExitF( m_DurationPerFrame > 0 && m_MovieTimeScale > 0 ); + +/* if ( movieFPS.IsNTSCRate() ) + { + m_MovieTimeScale = movieFPS.GetIntFPS() * 1000; + m_DurationPerFrame = 1001; + } + else if ( movieFPS.GetUnitsPerSecond() % movieFPS.GetUnitsPerFrame() == 0 ) // integer frame rate? + { + m_MovieTimeScale = movieFPS.GetIntFPS() * 1000; + m_DurationPerFrame = 1000; + } + else // round to nearest .001 second + { + m_MovieTimeScale = (int) ( movieFPS.GetFPS() * 1000 ); + m_DurationPerFrame = 1000; + } +*/ + +#ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Video Frame Rate = %f FPS\n %d time units per second\n %d time units per frame\n", m_MovieRecordFPS.GetFPS(), m_MovieRecordFPS.GetUnitsPerSecond(), m_MovieRecordFPS.GetUnitsPerFrame() ); + if ( m_MovieRecordFPS.IsNTSCRate() ) + LogMsg( " IS CONSIDERED NTSC RATE\n"); + LogMsg( "MovieTimeScale is being set to %d\nDuration Per Frame is %d\n\n", m_MovieTimeScale, m_DurationPerFrame ); +#endif + + // Create the video track and media + SetResult( VideoResult::VIDEO_ERROR_OCCURED ); + + m_theVideoTrack = NewMovieTrack( m_theMovie, FixRatio( width, 1 ), FixRatio( height, 1 ), kNoVolume ); + AssertExitF( GetMoviesError() == noErr ); + + m_theVideoMedia = NewTrackMedia( m_theVideoTrack, VideoMediaType, m_MovieTimeScale, NULL, 0 ); + AssertExitF( GetMoviesError() == noErr ); + + // we have successfully configured the output movie + SetResult( VideoResult::SUCCESS ); + m_bMovieConfigured = true; + + return true; +} + + +bool CQTVideoFileComposer::SetMovieSourceImageParameters( int srcWidth, int srcHeight, VideoEncodeSourceFormat_t srcImageFormat ) +{ + // Validate input and state + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_IN_RANGE( srcWidth, cMinVideoFrameWidth, cMaxVideoFrameWidth ) && IS_IN_RANGE( srcHeight, cMinVideoFrameHeight, cMaxVideoFrameHeight ) ); + AssertExitF( IS_IN_RANGECOUNT( srcImageFormat, VideoEncodeSourceFormat::VIDEO_FORMAT_FIRST, VideoEncodeSourceFormat::VIDEO_FORMAT_COUNT ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bMovieCreated && !m_bMovieCompleted && m_bMovieConfigured && !m_bSourceImagesConfigured ); + + // Setup source image format related stuff + m_SrcPixelFormat = ( srcImageFormat == VideoEncodeSourceFormat::BGRA_32BIT ) ? k32BGRAPixelFormat : + ( srcImageFormat == VideoEncodeSourceFormat::BGR_24BIT ) ? k24BGRPixelFormat : + ( srcImageFormat == VideoEncodeSourceFormat::RGB_24BIT ) ? k24RGBPixelFormat : + ( srcImageFormat == VideoEncodeSourceFormat::RGBA_32BIT ) ? k32RGBAPixelFormat : 0; + + m_SrcBytesPerPixel = GetBytesPerPixel( m_SrcPixelFormat ); + + // Setup source image size related stuff + m_SrcImageWidth = srcWidth; + m_SrcImageHeight = srcHeight; + m_SrcImageSize = srcWidth * srcHeight * m_SrcBytesPerPixel; + + // Setup the GWorld to hold the frame to video compress + + m_GWorldPixelFormat = k32BGRAPixelFormat; // can use k24BGRPixelFormat on Win32.. but it compresses wrong on OSX? + m_GWorldBytesPerPixel = 4; + m_GWorldImageWidth = CONTAINING_MULTIPLE_OF( srcWidth, 4 ); // make sure the encoded surface is a multiple of 4 in each dimensions + m_GWorldImageHeight = CONTAINING_MULTIPLE_OF( srcHeight, 4 ); + + m_GWorldRect.top = m_GWorldRect.left = 0; + m_GWorldRect.bottom = m_GWorldImageHeight; + m_GWorldRect.right = m_GWorldImageWidth; + + // Setup the QuiuckTime Graphics World for incoming frames of video + // Always use a 32-bit GWORD to avoid encoding bugs + SetResult( VideoResult::VIDEO_ERROR_OCCURED ); + + OSErr status = QTNewGWorld( &m_theSrcGWorld, m_GWorldPixelFormat, &m_GWorldRect, nil, nil, 0 ); + AssertExitF( status == noErr ); + + PixMapHandle thePixMap = GetGWorldPixMap( m_theSrcGWorld ); + AssertPtrExitF( thePixMap ); + + status = QTSetPixMapHandleRequestedGammaLevel( thePixMap, m_EncodeGamma ); + AssertExitF( status == noErr ); + + // Set encoding buffer to max size at max quality + // Should we try it with the actual quality setting? + + status = GetMaxCompressionSize( thePixMap, &m_GWorldRect, 0, m_EncodeQuality, m_VideoCodecToUse, + (CompressorComponent)anyCodec, (long*) &m_ScrImageMaxCompressedSize ); + + AssertExitF( status == noErr && m_ScrImageMaxCompressedSize > 0 ); + + // allocated buffers for the uncompressed and compressed images + m_SrcImageBuffer = new byte[ m_SrcImageSize ]; + m_SrcImageCompressedBuffer = NewHandle( m_ScrImageMaxCompressedSize ); + + // we have successfully configured the video input images + SetResult( VideoResult::SUCCESS ); + m_bSourceImagesConfigured = true; + + return CheckForReadyness(); // We are ready to go if audio is... +} + + +bool CQTVideoFileComposer::SetMovieSourceAudioParameters( AudioEncodeSourceFormat_t srcAudioFormat, int audioSampleRate, AudioEncodeOptions_t audioOptions, int audioSampleGroupSize ) +{ + SetResult( VideoResult::ILLEGAL_OPERATION ); + AssertExitF( m_bHasAudioTrack ); + + // Validate input and state + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( IS_IN_RANGECOUNT( srcAudioFormat, AudioEncodeSourceFormat::AUDIO_NONE, AudioEncodeSourceFormat::AUDIO_FORMAT_COUNT ) ); + AssertExitF( audioSampleRate == 0 || IS_IN_RANGE( audioSampleRate, cMinSampleRate, cMaxSampleRate ) ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bMovieCreated && !m_bMovieCompleted && m_bMovieConfigured && !m_bSourceAudioConfigured ); + + // it is possible to disable audio here by passing in AudioEncodeSourceFormat::AUDIO_NONE even + // if the movie was created with the hasHadio flag set to true, or by setting the sample rate to 0 + + if ( srcAudioFormat == AudioEncodeSourceFormat::AUDIO_NONE || audioSampleRate == 0 ) + { + m_bHasAudioTrack = false; + } + else + { + m_AudioOptions = audioOptions; + + // Setup the audio frequency + m_AudioSourceFrequency = audioSampleRate; + + // Create the audio track and media + SetResult( VideoResult::AUDIO_ERROR_OCCURED ); + + m_theAudioTrack = NewMovieTrack( m_theMovie, 0, 0, kFullVolume ); + AssertExitF( GetMoviesError() == noErr ); + + m_theAudioMedia = NewTrackMedia( m_theAudioTrack, SoundMediaType, (TimeScale) audioSampleRate, NULL, 0 ); + AssertExitF( GetMoviesError() == noErr ); + + // Setup the Audio Sound description + AudioStreamBasicDescription inASBD; + + switch( srcAudioFormat ) + { + case AudioEncodeSourceFormat::AUDIO_16BIT_PCMStereo: + { + inASBD.mSampleRate = Float64( audioSampleRate ); + inASBD.mFormatID = kAudioFormatLinearPCM; + inASBD.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + inASBD.mBytesPerPacket = 4; + inASBD.mFramesPerPacket = 1; + inASBD.mBytesPerPacket = 4; + inASBD.mChannelsPerFrame = 2; + inASBD.mBitsPerChannel = 16; + inASBD.mReserved = 0; + break; + } + default: + { + Assert( false ); // Impossible.. we hope + return false; + } + } + + m_AudioBytesPerSample = inASBD.mBytesPerPacket; + + OSStatus result = QTSoundDescriptionCreate( &inASBD, NULL, 0, NULL, 0, kQTSoundDescriptionKind_Movie_LowestPossibleVersion, (SoundDescriptionHandle*) &m_srcSoundDescription ); + AssertExitF( result == noErr ); + + // Setup audio sample buffering if needed + + m_bLimitAudioDurationToVideo = BITFLAGS_SET( audioOptions, AudioEncodeOptions::LIMIT_AUDIO_TRACK_TO_VIDEO_DURATION ); + + m_SampleGrouping = ( BITFLAGS_SET( audioOptions, AudioEncodeOptions::GROUP_SIZE_IS_VIDEO_FRAME ) ) ? CQTVideoFileComposer::AG_PER_FRAME : + ( BITFLAGS_SET( audioOptions, AudioEncodeOptions::USE_AUDIO_ENCODE_GROUP_SIZE ) ) ? CQTVideoFileComposer::AG_FIXED_SIZE : AG_NONE; + + // check for invalid sample grouping duration + if ( m_SampleGrouping == AG_FIXED_SIZE && ( audioSampleGroupSize < MIN_AUDIO_SAMPLE_GROUP_SIZE || audioSampleGroupSize > MAX_AUDIO_GROUP_SIZE_IN_SEC * m_AudioSourceFrequency ) ) + { + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + Assert( false ); + return false; + } + + m_bBufferSourceAudio = ( m_SampleGrouping != AG_NONE ) || m_bLimitAudioDurationToVideo; + + // Set up an audio buffer than can hold the maxium specified duration + if ( m_bBufferSourceAudio ) + { + m_srcAudioBufferSize = m_AudioSourceFrequency * m_AudioBytesPerSample * MAX_AUDIO_GROUP_SIZE_IN_SEC; + m_srcAudioBuffer = new byte[m_srcAudioBufferSize]; + m_srcAudioBufferCurrentSize = 0; + } + + if ( m_SampleGrouping == AG_FIXED_SIZE ) + { + // Set up to emit audio after fixed number of samples + m_nAudioSampleGroupSize = audioSampleGroupSize; + + } + + if ( m_SampleGrouping == AG_PER_FRAME ) + { + m_AudioSampleFrameCounter = 0; + } + + m_bSourceAudioConfigured = true; + } + +#ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Audio Sample Grouping Mode = %d\nSample Group Size = %d \n", (int) m_SampleGrouping, m_nAudioSampleGroupSize ); + LogMsg( "Audio Track Sample Rate is %d samples per second\n", m_AudioSourceFrequency ); + LogMsg( "Estimated Samples per frame = %d\n\n", (int) ( m_AudioSourceFrequency / m_MovieRecordFPS.GetFPS() ) ); +#endif + + // finish up + SetResult( VideoResult::SUCCESS ); + + return CheckForReadyness(); // We are ready to go if video is... +} + + +// Returns true if we are not ready, or if we began movie creation successfully +// This ONLY returns false if it tried to begin the movie creation process and failed +bool CQTVideoFileComposer::CheckForReadyness() +{ + return ( m_bMovieCreated && !m_bMovieCompleted && !m_bComposingMovie && m_bMovieConfigured && m_bSourceImagesConfigured && + m_bSourceAudioConfigured == m_bHasAudioTrack ) ? BeginMovieCreation() : true; +} + + +bool CQTVideoFileComposer::BeginMovieCreation() +{ + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bMovieCreated && !m_bMovieCompleted && !m_bComposingMovie && + m_bMovieConfigured && m_bSourceImagesConfigured && m_bSourceAudioConfigured == m_bHasAudioTrack ); + + // Open the tracks up for editing + SetResult( VideoResult::VIDEO_ERROR_OCCURED ); + OSErr status = BeginMediaEdits( m_theVideoMedia ); + AssertExitF( status == noErr ); + + if ( m_bHasAudioTrack ) + { + OSErr status = BeginMediaEdits( m_theAudioMedia ); + AssertExitF( status == noErr ); + } + + +#ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Media Tracks opened for editing\n\n" ); +#endif + + + // We are now ready to take in data to make a movie with + SetResult( VideoResult::SUCCESS ); + m_bComposingMovie = true; + + return true; +} + + +bool CQTVideoFileComposer::AppendVideoFrameToMedia( void *ImageBuffer, int strideAdjustBytes ) +{ +#ifdef LOG_ENCODER_OPERATIONS + LogMsg( "AppendVideoFrameToMedia( %8.8x ) called for %d --- ", ImageBuffer, m_nFramesAdded+1 ); +#endif + + // Validate input and state + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( ImageBuffer != nullptr ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bComposingMovie && !m_bMovieCompleted ); + + SetResult( VideoResult::VIDEO_ERROR_OCCURED ); + + // Get the pixmap + PixMapHandle thePixMap = GetGWorldPixMap( m_theSrcGWorld ); + AssertPtrExitF( thePixMap ); + + // copy the raw image into our bitmap + AssertExitF( LockPixels( thePixMap ) ); + + byte *srcBase = (byte*) ImageBuffer; + int srcStride = m_SrcImageWidth * m_SrcBytesPerPixel + strideAdjustBytes; + + byte *dstBase = nullptr; + int dstStride = 0; + +#if defined ( WIN32 ) + // Get the HBITMAP of our GWorld + HBITMAP theHBITMAP = (HBITMAP) GetPortHBITMAP( (GrafPtr) m_theSrcGWorld ); + + // retrieve the bitmap info header information + BITMAP bmp; + AssertExitF( GetObject( theHBITMAP, sizeof(BITMAP), (LPSTR) &bmp) ); + + // validate the BMP we just got + AssertExitF( bmp.bmWidth == m_SrcImageWidth && bmp.bmHeight == m_SrcImageHeight && bmp.bmBitsPixel == 8 * m_GWorldBytesPerPixel ); + + // setup the pixel copy info + dstBase = (byte*)bmp.bmBits; + dstStride = bmp.bmWidthBytes; + +#elif defined ( OSX ) + // setup the pixel copy info + dstBase = (byte*) GetPixBaseAddr( thePixMap ); + dstStride = GetPixRowBytes( thePixMap ); + +#endif + + AssertExitF( dstBase != nullptr && dstStride > 0 ); + + // save a TGA if we are running diagnostics +#if defined ( LOG_FRAMES_TO_TGA ) + + if ( ( m_nFramesAdded % LOG_FRAMES_TO_TGA_INTERVAL ) == 0 ) + { + SaveToTargaFile( m_nFramesAdded, m_TGAFileBase, m_SrcImageWidth, m_SrcImageHeight, srcBase, m_SrcPixelFormat, strideAdjustBytes ); + } + +#endif + + // copy the supplied pixel buffer into our GWORLD data + if ( !CopyBitMapPixels( m_SrcImageWidth, m_SrcImageHeight, + m_SrcPixelFormat, srcBase, srcStride, + m_GWorldPixelFormat, dstBase, dstStride ) ) + { + Assert( false ); + return false; + } + + // You are now free to move about the cabin... + UnlockPixels( thePixMap ); + + // allocate a handle which CompressImage will resize... + ImageDescriptionHandle theImageDescHandle = (ImageDescriptionHandle) NewHandle( sizeof(ImageDescriptionHandle) ); + AssertExitF( theImageDescHandle != nullptr ); + + // compress the single image + OSErr status = CompressImage( thePixMap, &m_GWorldRect, m_EncodeQuality, + m_VideoCodecToUse, theImageDescHandle, *m_SrcImageCompressedBuffer ); + if ( status != noErr ) + { + Assert( false ); // tell the user + SAFE_DISPOSE_HANDLE( theImageDescHandle ); + return false; + } + + TimeValue addedTime = 0; + + // Lets add gamma info the image description + if ( m_EncodeGamma != kQTUseSourceGammaLevel ) + { + Fixed newGamma = m_EncodeGamma; + status = ICMImageDescriptionSetProperty( theImageDescHandle, kQTPropertyClass_ImageDescription, kICMImageDescriptionPropertyID_GammaLevel, sizeof( newGamma ), &newGamma ); + AssertExitF( status == noErr ); + } + + // add the compressed image to the movie stream + status = AddMediaSample( m_theVideoMedia, m_SrcImageCompressedBuffer, 0, (**theImageDescHandle).dataSize, m_DurationPerFrame, + (SampleDescriptionHandle) theImageDescHandle, 1, 0, &addedTime ); + + if ( status != noErr ) + { + Assert( false ); // tell the user + SAFE_DISPOSE_HANDLE( theImageDescHandle ); + return false; + } + + +#ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Video Frame %d added to Video Media: Duration = %d, Inserted at Time %d\n", m_nFramesAdded+1, m_DurationPerFrame, addedTime ); +#endif + + // free up dynamic resources + SAFE_DISPOSE_HANDLE( theImageDescHandle ); + + // Report success + SetResult( VideoResult::SUCCESS ); + m_nFramesAdded++; + + return true; +} + + + +int CQTVideoFileComposer::GetAudioSampleCountThruFrame( int frameNo ) +{ + if ( frameNo < 1 ) + { + return 0; + } + + double secondsSoFar = (double) ( frameNo * m_DurationPerFrame ) / (double) m_MovieTimeScale; + int nAudioSamples = (int) floor( secondsSoFar * (double) m_AudioSourceFrequency ); + + return nAudioSamples; +} + + + +bool CQTVideoFileComposer::AppendAudioSamplesToMedia( void *soundBuffer, size_t bufferSize ) +{ + SetResult( VideoResult::ILLEGAL_OPERATION ); + AssertExitF( m_bHasAudioTrack ); + + // Validate input and state + SetResult( VideoResult::BAD_INPUT_PARAMETERS ); + AssertExitF( soundBuffer != nullptr && bufferSize % m_AudioBytesPerSample == 0 ); + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bComposingMovie && !m_bMovieCompleted ); + + int nSamples = bufferSize / m_AudioBytesPerSample; + Assert( bufferSize % m_AudioBytesPerSample == 0 ); + + TimeValue64 insertTime64 = 0; + OSErr status = noErr; + + bool retest; + int samplesToEmit, MaxCanAdd, nSamplesAvailable; + + m_nSamplesAdded+= nSamples; // track samples given to encoder + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "%d Audio Samples Submitted (%d total) -- ", nSamples, m_nSamplesAdded ); + #endif + + // We can pass in 0 bytes to trigger a flush... + if ( nSamples == 0 ) + { + if ( !m_bBufferSourceAudio || m_srcAudioBufferCurrentSize == 0 ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "NO SAMPLES TO PROCESS. EXIT\n" ); + #endif + goto finish_up; + } + } + + // Are we not buffering audio? + if ( !m_bBufferSourceAudio && nSamples > 0 ) + { + SetResult( VideoResult::AUDIO_ERROR_OCCURED ); + status = AddMediaSample2( m_theAudioMedia, (const UInt8 *) soundBuffer, (ByteCount) bufferSize, + (TimeValue64) 1, (TimeValue64) 0, (SampleDescriptionHandle) m_srcSoundDescription, + (ItemCount) nSamples, 0, &insertTime64 ); + + AssertExitF( status == noErr ); + + m_nSamplesAddedToMedia+= nSamples; + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "%d samples (%d total) inserted into Video at time %ld\n", nSamples, m_nSamplesAddedToMedia, insertTime64 ); + #endif + + goto finish_up; + } + + // Buffering audio .... + if ( nSamples > 0 ) + { + memaddr_t pSrc = (memaddr_t) soundBuffer; + size_t bytesToCopy = nSamples * m_AudioBytesPerSample; + + // Is buffer big enough to hold it all? + if ( m_srcAudioBufferCurrentSize + bytesToCopy > m_srcAudioBufferSize ) + { + // get a bigger buffer + size_t newBufferSize = m_srcAudioBufferSize * 2 + bytesToCopy; + byte *newBuffer = new byte[newBufferSize]; + + // copy buffered sound and swap out buffers + V_memcpy( newBuffer, m_srcAudioBuffer, m_srcAudioBufferCurrentSize ); + + delete[] m_srcAudioBuffer; + m_srcAudioBuffer = newBuffer; + m_srcAudioBufferSize = newBufferSize; + } + + // Append samples to buffer + V_memcpy( m_srcAudioBuffer + m_srcAudioBufferCurrentSize, pSrc, bytesToCopy ); + m_srcAudioBufferCurrentSize += bytesToCopy; + } + +#ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "%d Samples buffered. Buffer=%d Samples -- ", nSamples, ( m_srcAudioBufferCurrentSize / m_AudioBytesPerSample ) ); +#endif + +retest_here: + nSamplesAvailable = m_srcAudioBufferCurrentSize / m_AudioBytesPerSample; + samplesToEmit = 0; + retest = false; + MaxCanAdd = ( m_bLimitAudioDurationToVideo ) ? ( GetAudioSampleCountThruFrame( m_nFramesAdded ) - m_nSamplesAddedToMedia ) : INT32_MAX; + + if ( MaxCanAdd <= 0 ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Can't Add Audio Now.\n" ); + #endif + goto finish_up; + } + + // Now.. we determine if we are ready to insert the audio samples into the media..and if so, how much... + + if ( m_SampleGrouping == AG_NONE ) + { + // are we keeping audio from getting ahead of video? + Assert( m_bLimitAudioDurationToVideo ); + + samplesToEmit = MIN( MaxCanAdd, nSamplesAvailable ); + } + else if ( m_SampleGrouping == AG_FIXED_SIZE ) + { + // do we have enough to emit a sample? + if ( ( nSamplesAvailable < m_nAudioSampleGroupSize ) || ( m_bLimitAudioDurationToVideo && ( MaxCanAdd < m_nAudioSampleGroupSize ) ) ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + if ( nSamplesAvailable < m_nAudioSampleGroupSize ) + LogMsg( "Need %d Samples to emit sample group\n", m_nAudioSampleGroupSize ); + else + LogMsg( "Audio is caught up to Video (can add %d) \n", MaxCanAdd ); + #endif + goto finish_up; + } + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "emitting 1 group of audio (%d samples)\n", m_nAudioSampleGroupSize ); + #endif + + samplesToEmit = m_nAudioSampleGroupSize; + retest = true; + } + else if ( m_SampleGrouping == AG_PER_FRAME ) + { + // is the audio already caught up with the current video frame? + if ( m_bLimitAudioDurationToVideo && m_AudioSampleFrameCounter >= m_nFramesAdded ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Audio is caught up to Video\n" ); + #endif + goto finish_up; + } + + int curSampleCount = GetAudioSampleCountThruFrame( m_AudioSampleFrameCounter ); + int nextSampleCount = GetAudioSampleCountThruFrame( m_AudioSampleFrameCounter+1 ); + int thisGroupSize = nextSampleCount - curSampleCount; + + Assert( m_nSamplesAddedToMedia == curSampleCount ); + + if ( nSamplesAvailable < thisGroupSize ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Not enough samples to fill video frame (need %d)\n", thisGroupSize ); + #endif + goto finish_up; + } + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "emitting 1 video frame of audio (%d samples)\n", thisGroupSize ); + #endif + + samplesToEmit = thisGroupSize; + m_AudioSampleFrameCounter++; + retest = true; + } + else + { + Assert( false ); + } + + if ( samplesToEmit > 0 ) + { + SetResult( VideoResult::AUDIO_ERROR_OCCURED ); + + status = AddMediaSample2( m_theAudioMedia, (const UInt8 *) m_srcAudioBuffer, (ByteCount) samplesToEmit * m_AudioBytesPerSample, + (TimeValue64) 1, (TimeValue64) 0, (SampleDescriptionHandle) m_srcSoundDescription, + (ItemCount) samplesToEmit, 0, &insertTime64 ); + + AssertExitF( status == noErr ); + + m_nSamplesAddedToMedia+= samplesToEmit; + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "%d samples inserted into Video (%d total) at time %ld -- ", samplesToEmit, m_nSamplesAddedToMedia, insertTime64 ); + #endif + + // remove added samples from sound buffer + // (this really should be a circular buffer.. but that has its own problems) + + m_srcAudioBufferCurrentSize -= samplesToEmit * m_AudioBytesPerSample; + nSamplesAvailable -= samplesToEmit; + + if ( nSamplesAvailable > 0 ) + { + V_memcpy( m_srcAudioBuffer, m_srcAudioBuffer + (samplesToEmit * m_AudioBytesPerSample), nSamplesAvailable * m_AudioBytesPerSample ); + } + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Buffer now =%d samples", nSamplesAvailable ); + #endif + + if ( retest ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( " -- rechecking -- " ); + #endif + goto retest_here; + } + } + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "\n" ); + #endif + + +finish_up: + // Report success + SetResult( VideoResult::SUCCESS ); + + return true; +} + + + +bool CQTVideoFileComposer::SyncAndFlushAudio() +{ + if ( !m_bHasAudioTrack ) + { + return false; + } + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Resolving Audio Track...\n" ); + #endif + +restart_sync: + bool bPadWithSilence = BITFLAGS_SET( m_AudioOptions, AudioEncodeOptions::PAD_AUDIO_WITH_SILENCE ); + int VideoDurationInSamples = GetAudioSampleCountThruFrame( m_nFramesAdded ); + int CurShortfall = VideoDurationInSamples - m_nSamplesAddedToMedia; + + int SilenceToEmit = 0; + bool forceFlush = false; + bool forcePartialGroupFlush = false; + int nSamplesInBuffer = ( m_bBufferSourceAudio ) ? m_srcAudioBufferCurrentSize / m_AudioBytesPerSample : 0; + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Video duration is %d frames, which is %d Audio Samples\n", m_nFramesAdded, VideoDurationInSamples ); + LogMsg( "%d Samples emitted to Audio track so far. %d samples remain in audio buffer\n", m_nSamplesAddedToMedia, nSamplesInBuffer ); + LogMsg( "Delta to sync end of audio to end of video is %d Samples\n", CurShortfall ); + LogMsg( "Pad With Silence Mode = %d, Align End of Audio With Video Mode = %d\n", (int) bPadWithSilence, (int) m_bLimitAudioDurationToVideo ); + #endif + + // not grouping samples mode + if ( m_SampleGrouping == AG_NONE ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "No sample grouping Mode\n" ); + #endif + + Assert( m_bLimitAudioDurationToVideo == m_bBufferSourceAudio ); // if we're not limiting, we're not buffering + + if ( m_bLimitAudioDurationToVideo && CurShortfall > 0 && bPadWithSilence ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Padding with %d samples to match video duration", CurShortfall ); + #endif + + SilenceToEmit = CurShortfall; // pad with silence + } + + if ( nSamplesInBuffer > 0 || SilenceToEmit > 0 ) // force if we have something to add + { + forceFlush = true; + } + } + + // Fixed sized grouping (and buffering) + if ( m_SampleGrouping == AG_FIXED_SIZE ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Fixed sample grouping mode. Group size of %d\n", m_nAudioSampleGroupSize ); + #endif + + // No matter what, if we have a partially filled buffer, we add silence to it to make a complete group + // do we have a partially full buffer? if so pad with silence to make a full group + if ( nSamplesInBuffer > 0 && nSamplesInBuffer % m_nAudioSampleGroupSize != 0 ) + { + SilenceToEmit = m_nAudioSampleGroupSize - ( nSamplesInBuffer % m_nAudioSampleGroupSize ); + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Adding %d silence samples to complete group\n", SilenceToEmit ); + #endif + + } + + int bufferedSamples = nSamplesInBuffer + SilenceToEmit; + int newShortFall = VideoDurationInSamples - m_nSamplesAddedToMedia - bufferedSamples; + + if ( bPadWithSilence && newShortFall > 0 ) + { + SilenceToEmit += newShortFall; // pad with silence until audio matches video duration + forceFlush = true; + forcePartialGroupFlush = true; + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Adding %d silence samples to pad to match audio to video duration\n", newShortFall ); + #endif + } + } + + if ( m_SampleGrouping == AG_PER_FRAME ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Video Frame duraiton Audio grouping mode\n" ); + #endif + + // Have we already enough audio to match the video + if ( m_bLimitAudioDurationToVideo && m_AudioSampleFrameCounter >= m_nFramesAdded ) + { + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Audio is caught up to Video\n" ); + #endif + goto audio_complete; + } + + // if we have anything in the buffer... pad it out with zeros + if ( nSamplesInBuffer > 0 ) + { + // get the group size for the video frame the audio is currently on + int thisGroupSize = GetAudioSampleCountThruFrame( m_AudioSampleFrameCounter+1 ) - GetAudioSampleCountThruFrame( m_AudioSampleFrameCounter ); + Assert( m_nSamplesAddedToMedia == GetAudioSampleCountThruFrame( m_AudioSampleFrameCounter ) ); + + // if we already have 1 (or more) groups in the buffer.. emit them, and restart + if ( nSamplesInBuffer >= thisGroupSize ) + { + char n = nullchar; + AppendAudioSamplesToMedia( &n, 0 ); + goto restart_sync; + } + + SilenceToEmit = thisGroupSize - nSamplesInBuffer; + forceFlush = true; + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Adding %d silence samples to pad current group to match video frame duration\n", SilenceToEmit ); + #endif + + } + + // with the output being aligned to a video frame, do we need to add more to pad to end of the video + + int bufferedSamples = nSamplesInBuffer + SilenceToEmit; + int newShortFall = VideoDurationInSamples - m_nSamplesAddedToMedia - bufferedSamples; + if ( bPadWithSilence && newShortFall > 0 ) + { + SilenceToEmit += newShortFall; // pad with silence until audio matches video duration + forceFlush = true; + forcePartialGroupFlush = true; + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Adding %d silence samples to pad audio to match video duration", newShortFall ); + #endif + } + + } + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "\n" ); + #endif + + // now we append any needed silence to the audio stream... + if ( SilenceToEmit > 0 ) + { + int bufferSize = SilenceToEmit * m_AudioBytesPerSample; + byte *pSilenceBuf = new byte[ bufferSize ]; + V_memset( pSilenceBuf, nullchar, bufferSize ); + + #ifdef LOG_ENCODER_AUDIO_OPERATIONS + LogMsg( "Appending %d Silence samples\n", SilenceToEmit ); + #endif + + AppendAudioSamplesToMedia( pSilenceBuf, bufferSize ); + } + else + { + if ( forceFlush ) + { + char n = nullchar; + AppendAudioSamplesToMedia( &n, 0 ); + } + } + + if ( forcePartialGroupFlush && m_srcAudioBufferCurrentSize >0 ) + { + int nSamplesThisAdd = m_srcAudioBufferCurrentSize / m_AudioBytesPerSample; + TimeValue64 insertTime64 = 0; + + OSErr status = AddMediaSample2( m_theAudioMedia, (const UInt8 *) m_srcAudioBuffer, (ByteCount) m_srcAudioBufferCurrentSize, + (TimeValue64) 1, (TimeValue64) 0, (SampleDescriptionHandle) m_srcSoundDescription, + (ItemCount) nSamplesThisAdd, 0, &insertTime64 ); + + AssertExitF( status == noErr ); + + m_srcAudioBufferCurrentSize = 0; + m_nSamplesAddedToMedia+= nSamplesThisAdd; + + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "FORCED FLUSH - Audio Samples added to media. %d added, %d total samples, inserted at time %ld\n", nSamplesThisAdd, m_nSamplesAddedToMedia, insertTime64 ); + #endif + } + + +audio_complete: + + return true; +} + + +bool CQTVideoFileComposer::EndMovieCreation( bool saveMovieData ) +{ + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nEndMovieCreation Called Composing=%d Completed=%d\n\n", (int) m_bComposingMovie, (int) m_bMovieCompleted ); + #endif + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bComposingMovie && !m_bMovieCompleted ); + + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nEndMovieCreation Called\n\n" ); + #endif + + // Stop adding to and (optionally) save the video media into the file + SetResult( VideoResult::VIDEO_ERROR_OCCURED ); + if ( m_nFramesAdded > 0 ) + { + TimeValue VideoDuration = GetMediaDuration( m_theVideoMedia ); + AssertExitF( VideoDuration == m_nFramesAdded * m_DurationPerFrame ); + + OSErr status = EndMediaEdits( m_theVideoMedia ); + AssertExitF( status == noErr ); + + if ( saveMovieData ) + { + status = InsertMediaIntoTrack( m_theVideoTrack, 0, 0, VideoDuration, fixed1 ); + Assert( status == noErr ); + + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nVideo Media inserted into Track\n" ); + #endif + + } + } + + // Stop adding to and (optionally) save the audio media into the file + SetResult( VideoResult::AUDIO_ERROR_OCCURED ); + if ( m_bHasAudioTrack && m_nSamplesAdded > 0 ) + { + // flush any remaining samples in the buffer to the media + + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Calling SyncAndFlushAudio()\n" ); + #endif + + SyncAndFlushAudio(); + + TimeValue AudioDuration = GetMediaDuration( m_theAudioMedia ); + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Audio Duration = %d nSamples Added = %d\n", AudioDuration, m_nSamplesAdded ); + #endif + // AssertExitF( AudioDuration == m_nSamplesAdded ); + + OSErr status = EndMediaEdits( m_theAudioMedia ); + AssertExitF( status == noErr ); + + if ( saveMovieData ) + { + status = InsertMediaIntoTrack( m_theAudioTrack, 0, 0, AudioDuration, fixed1 ); + AssertExitF( status == noErr ); + + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nAudio Media inserted into Track\n" ); + #endif + } + } + + if ( saveMovieData ) + { + +#ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Saving Movie Data...\n" ); +#endif + + SetResult( VideoResult::FILE_ERROR_OCCURED ); + OSErr status = AddMovieToStorage( m_theMovie, m_MovieFileDataHandler ); + AssertExitF( status == noErr ); + if ( status != noErr ) + { + DataHDeleteFile( m_MovieFileDataHandler ); + } + + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nMovie Resource added to file. Returned Status = %d\n", (int) status ); + #endif + } + + // free our resources + if ( m_MovieFileDataHandler != nullptr ) + { + OSErr status = CloseMovieStorage( m_MovieFileDataHandler ); + m_MovieFileDataHandler = nullptr; + AssertExitF( status == noErr ); + } + + SAFE_DISPOSE_HANDLE( m_MovieFileDataRef ); + SAFE_DISPOSE_HANDLE( m_SrcImageCompressedBuffer ); + SAFE_DISPOSE_GWORLD( m_theSrcGWorld ); + SAFE_DISPOSE_HANDLE( m_srcSoundDescription ); + + SetResult( VideoResult::SUCCESS ); + m_bComposingMovie = false; + return true; +} + + +// The movie can be aborted at any time before completion +bool CQTVideoFileComposer::AbortMovie() +{ + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( !m_bMovieCompleted ); + + // Shut down the movie if we are recording + if ( m_bComposingMovie ) + { + if ( !EndMovieCreation( false ) ) + { + return false; + } + } + + if ( m_bMovieCreated ) + { + if ( m_MovieFileDataHandler != nullptr ) + { + SetResult( VideoResult::FILE_ERROR_OCCURED ); + OSErr status = CloseMovieStorage( m_MovieFileDataHandler ); + AssertExitF( status == noErr ); + m_MovieFileDataHandler = nullptr; + } + + SAFE_DISPOSE_HANDLE( m_MovieFileDataRef ); + SAFE_DISPOSE_MOVIE( m_theMovie ); + } + + if ( m_FileName ) + { + g_pFullFileSystem->RemoveFile( m_FileName ); + } + + SetResult( VideoResult::SUCCESS ); + m_bMovieCompleted = true; + + return true; +} + + + +bool CQTVideoFileComposer::FinishMovie( bool SaveMovieToDisk ) +{ + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nFinish Movie Called\n" ); + #endif + + SetResult( VideoResult::OPERATION_OUT_OF_SEQUENCE ); + AssertExitF( m_bComposingMovie && !m_bMovieCompleted ); + + // Shutdown movie creation + if ( !EndMovieCreation( SaveMovieToDisk ) ) + { + #ifdef LOG_ENCODER_OPERATIONS + LogMsg( "\nEndMovieCreation Aborted\n" ); + #endif + return false; + } + + // todo: check on Disposing of theMovie and theMedia + if ( m_MovieFileDataHandler != nullptr ) + { + SetResult( VideoResult::FILE_ERROR_OCCURED ); + OSErr status = CloseMovieStorage( m_MovieFileDataHandler ); + AssertExitF( status == noErr ); + m_MovieFileDataHandler = nullptr; + +#ifdef LOG_ENCODER_OPERATIONS + LogMsg( "Movie File Closed\n" ); +#endif + + } + + SAFE_DISPOSE_HANDLE( m_MovieFileDataRef ); + SAFE_DISPOSE_MOVIE( m_theMovie ); + + // if no frames have been added.. delete files + if ( SaveMovieToDisk == false || ( m_nFramesAdded <= 0 && m_nSamplesAdded <= 0) ) + { + g_pFullFileSystem->RemoveFile( m_FileName ); + } + + SetResult( VideoResult::SUCCESS ); + m_bMovieCompleted = true; + +#ifdef LOG_ENCODER_OPERATIONS + g_pFullFileSystem->Close( m_LogFile ); + m_LogFile = FILESYSTEM_INVALID_HANDLE; +#endif + + + return true; +} + + + +void CQTVideoFileComposer::SetResult( VideoResult_t status ) +{ + m_LastResult = status; +} + +VideoResult_t CQTVideoFileComposer::GetResult() +{ + return m_LastResult; +} + +bool CQTVideoFileComposer::IsReadyToRecord() +{ + return ( m_bComposingMovie && !m_bMovieCompleted ); +} + + +#ifdef ENABLE_EXTERNAL_ENCODER_LOGGING +bool CQTVideoFileComposer::LogMessage( const char *msg ) +{ + +#ifdef LOG_ENCODER_OPERATIONS + if ( IS_NOT_EMPTY(msg) && m_LogFile != FILESYSTEM_INVALID_HANDLE ) + { + g_pFullFileSystem->Write( msg, V_strlen( msg ), m_LogFile ); + } +#endif + return true; +} +#endif |