mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-07-14 08:04:40 +00:00
Engine directory for ticket #1
This commit is contained in:
parent
352279af7a
commit
7dbfe6994d
3795 changed files with 1363358 additions and 0 deletions
697
Engine/source/gfx/video/theoraTexture.cpp
Normal file
697
Engine/source/gfx/video/theoraTexture.cpp
Normal file
|
|
@ -0,0 +1,697 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) 2012 GarageGames, LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifdef TORQUE_OGGTHEORA
|
||||
|
||||
/// If defined, uses a separate file stream to read Vorbis sound data
|
||||
/// from the Ogg stream. This removes both the contention caused by
|
||||
/// concurrently streaming from a single master stream as well as the
|
||||
/// coupling between Theora reads and Vorbis reads that arises from
|
||||
/// multiplexing.
|
||||
#define SPLIT_VORBIS
|
||||
|
||||
|
||||
#include "theoraTexture.h"
|
||||
|
||||
#include "sfx/sfxDescription.h"
|
||||
#include "sfx/sfxSystem.h"
|
||||
#include "sfx/sfxCommon.h"
|
||||
#include "sfx/sfxMemoryStream.h"
|
||||
#include "sfx/sfxSound.h"
|
||||
#ifdef SPLIT_VORBIS
|
||||
#include "sfx/media/sfxVorbisStream.h"
|
||||
#endif
|
||||
|
||||
#include "core/stream/fileStream.h"
|
||||
#include "core/ogg/oggInputStream.h"
|
||||
#include "core/ogg/oggTheoraDecoder.h"
|
||||
#include "core/ogg/oggVorbisDecoder.h"
|
||||
#include "core/util/safeDelete.h"
|
||||
#include "core/util/rawData.h"
|
||||
#include "console/console.h"
|
||||
#include "math/mMath.h"
|
||||
#include "gfx/bitmap/gBitmap.h"
|
||||
#include "gfx/gfxDevice.h"
|
||||
#include "gfx/gfxFormatUtils.h"
|
||||
#include "gfx/gfxTextureManager.h"
|
||||
|
||||
|
||||
|
||||
/// Profile for the video texture.
|
||||
GFX_ImplementTextureProfile( GFXTheoraTextureProfile,
|
||||
GFXTextureProfile::DiffuseMap,
|
||||
GFXTextureProfile::NoMipmap | GFXTextureProfile::Dynamic,
|
||||
GFXTextureProfile::None );
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
static const char* GetPixelFormatName( OggTheoraDecoder::EPixelFormat format )
|
||||
{
|
||||
switch( format )
|
||||
{
|
||||
case OggTheoraDecoder::PIXEL_FORMAT_420:
|
||||
return "4:2:0";
|
||||
case OggTheoraDecoder::PIXEL_FORMAT_422:
|
||||
return "4:2:2";
|
||||
case OggTheoraDecoder::PIXEL_FORMAT_444:
|
||||
return "4:4:4";
|
||||
|
||||
case OggTheoraDecoder::PIXEL_FORMAT_Unknown: ;
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TheoraTexture::FrameReadItem implementation.
|
||||
//=============================================================================
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTexture::FrameReadItem::FrameReadItem( AsyncBufferedInputStream< TheoraTextureFrame*, IInputStream< OggTheoraFrame* >* >* stream, ThreadContext* context )
|
||||
: Parent( context ),
|
||||
mFrameStream( dynamic_cast< FrameStream* >( stream ) )
|
||||
{
|
||||
AssertFatal( mFrameStream != NULL, "TheoraTexture::FrameReadItem::FrameReadItem() - expecting stream of type 'FrameStream'" );
|
||||
|
||||
mAsyncState = mFrameStream->mAsyncState;
|
||||
|
||||
// Assign a TheoraTextureFrame record to us. The nature of
|
||||
// AsyncBufferedInputStream ensures that we are always serial
|
||||
// here so this is thread-safe.
|
||||
|
||||
mFrame = &mFrameStream->mFrames[ mFrameStream->mFrameIndex ];
|
||||
mFrameStream->mFrameIndex = ( mFrameStream->mFrameIndex + 1 ) % FrameStream::NUM_FRAME_RECORDS;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::FrameReadItem::execute()
|
||||
{
|
||||
// Read Theora frame data.
|
||||
|
||||
OggTheoraFrame* frame;
|
||||
if( mFrameStream->getSourceStream()->read( &frame, 1 ) != 1 )
|
||||
return;
|
||||
|
||||
// Copy the data into the texture.
|
||||
|
||||
OggTheoraDecoder* decoder = mAsyncState->getTheora();
|
||||
const U32 height = decoder->getFrameHeight();
|
||||
const U32 framePitch = decoder->getFrameWidth() * 4;
|
||||
|
||||
GFXLockedRect* rect = mFrame->mLockedRect;
|
||||
if( rect )
|
||||
{
|
||||
const U32 usePitch = getMin(framePitch, mFrame->mTexture->getWidth() * 4);
|
||||
const U32 maxHeight = getMin(height, mFrame->mTexture->getHeight());
|
||||
if( (framePitch == rect->pitch) && (height == maxHeight) )
|
||||
dMemcpy( rect->bits, frame->data, rect->pitch * height );
|
||||
else
|
||||
{
|
||||
// Scanline length does not match. Copy line by line.
|
||||
|
||||
U8* dst = rect->bits;
|
||||
U8* src = ( U8* ) frame->data;
|
||||
|
||||
// Center the video if it is too big for the texture mode
|
||||
if ( height > maxHeight )
|
||||
src += framePitch * ((height - maxHeight) / 2);
|
||||
if ( framePitch > usePitch )
|
||||
src += (framePitch - usePitch) / 2;
|
||||
for( U32 i = 0; i < maxHeight; ++ i )
|
||||
{
|
||||
dMemcpy( dst, src, usePitch );
|
||||
dst += rect->pitch;
|
||||
src += framePitch;
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef TORQUE_DEBUG
|
||||
else
|
||||
Platform::outputDebugString( "[TheoraTexture] texture not locked on frame %i", frame->mFrameNumber );
|
||||
#endif
|
||||
|
||||
// Copy frame metrics.
|
||||
|
||||
mFrame->mFrameNumber = frame->mFrameNumber;
|
||||
mFrame->mFrameTime = frame->mFrameTime;
|
||||
mFrame->mFrameDuration = frame->mFrameDuration;
|
||||
|
||||
// Yield the frame packet back to the Theora decoder.
|
||||
|
||||
decoder->reusePacket( frame );
|
||||
|
||||
// Buffer the frame.
|
||||
|
||||
mFrameStream->_onArrival( mFrame );
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TheoraTexture::FrameStream implementation.
|
||||
//=============================================================================
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTexture::FrameStream::FrameStream( AsyncState* asyncState, bool looping )
|
||||
: Parent( asyncState->getTheora(), 0, FRAME_READ_AHEAD, looping ),
|
||||
mAsyncState( asyncState ),
|
||||
mFrameIndex( 0 )
|
||||
{
|
||||
// Create the textures.
|
||||
|
||||
OggTheoraDecoder* theora = asyncState->getTheora();
|
||||
const U32 width = theora->getFrameWidth();
|
||||
const U32 height = theora->getFrameHeight();
|
||||
|
||||
for( U32 i = 0; i < NUM_FRAME_RECORDS; ++ i )
|
||||
{
|
||||
mFrames[ i ].mTexture.set(
|
||||
width,
|
||||
height,
|
||||
GFXFormatR8G8B8A8,
|
||||
&GFXTheoraTextureProfile,
|
||||
String::ToString( "Theora texture frame buffer %i (%s:%i)", i, __FILE__, __LINE__ )
|
||||
);
|
||||
}
|
||||
|
||||
acquireTextureLocks();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::FrameStream::acquireTextureLocks()
|
||||
{
|
||||
for( U32 i = 0; i < NUM_FRAME_RECORDS; ++ i )
|
||||
if( !mFrames[ i ].mLockedRect )
|
||||
mFrames[ i ].mLockedRect = mFrames[ i ].mTexture.lock();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::FrameStream::releaseTextureLocks()
|
||||
{
|
||||
for( U32 i = 0; i < NUM_FRAME_RECORDS; ++ i )
|
||||
if( mFrames[ i ].mLockedRect )
|
||||
{
|
||||
mFrames[ i ].mTexture.unlock();
|
||||
mFrames[ i ].mLockedRect = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TheoraTexture::AsyncState implementation.
|
||||
//=============================================================================
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTexture::AsyncState::AsyncState( const ThreadSafeRef< OggInputStream >& oggStream, bool looping )
|
||||
: mOggStream( oggStream ),
|
||||
mTheoraDecoder( dynamic_cast< OggTheoraDecoder* >( oggStream->getDecoder( "Theora" ) ) ),
|
||||
mVorbisDecoder( dynamic_cast< OggVorbisDecoder* >( oggStream->getDecoder( "Vorbis" ) ) ),
|
||||
mCurrentTime( 0 )
|
||||
{
|
||||
if( mTheoraDecoder )
|
||||
{
|
||||
mTheoraDecoder->setTimeSource( this );
|
||||
mFrameStream = new FrameStream( this, looping );
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTextureFrame* TheoraTexture::AsyncState::readNextFrame()
|
||||
{
|
||||
TheoraTextureFrame* frame;
|
||||
if( mFrameStream->read( &frame, 1 ) )
|
||||
return frame;
|
||||
else
|
||||
return NULL;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::AsyncState::start()
|
||||
{
|
||||
mFrameStream->start();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::AsyncState::stop()
|
||||
{
|
||||
mFrameStream->stop();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool TheoraTexture::AsyncState::isAtEnd()
|
||||
{
|
||||
return mOggStream->isAtEnd();
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
// TheoraTexture implementation.
|
||||
//=============================================================================
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTexture::TheoraTexture()
|
||||
: mPlaybackQueue( NULL ),
|
||||
mCurrentFrame( NULL ),
|
||||
mIsPaused( true )
|
||||
{
|
||||
GFXTextureManager::addEventDelegate( this, &TheoraTexture::_onTextureEvent );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTexture::~TheoraTexture()
|
||||
{
|
||||
GFXTextureManager::removeEventDelegate( this, &TheoraTexture::_onTextureEvent );
|
||||
_reset();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::_reset()
|
||||
{
|
||||
// Stop the async streams.
|
||||
|
||||
if( mAsyncState != NULL )
|
||||
mAsyncState->stop();
|
||||
|
||||
// Delete the playback queue.
|
||||
|
||||
if( mPlaybackQueue )
|
||||
SAFE_DELETE( mPlaybackQueue );
|
||||
|
||||
// Kill the sound source.
|
||||
|
||||
if( mSFXSource != NULL )
|
||||
{
|
||||
mSFXSource->stop();
|
||||
SFX_DELETE( mSFXSource );
|
||||
mSFXSource = NULL;
|
||||
}
|
||||
|
||||
mLastFrameNumber = 0;
|
||||
mNumDroppedFrames = 0;
|
||||
mCurrentFrame = NULL;
|
||||
mAsyncState = NULL;
|
||||
mIsPaused = false;
|
||||
mPlaybackTimer.reset();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::_onTextureEvent( GFXTexCallbackCode code )
|
||||
{
|
||||
switch( code )
|
||||
{
|
||||
case GFXZombify:
|
||||
mCurrentFrame = NULL;
|
||||
if( mAsyncState )
|
||||
{
|
||||
// Blast out work items and then release all texture locks.
|
||||
|
||||
ThreadPool::GLOBAL().flushWorkItems();
|
||||
mAsyncState->getFrameStream()->releaseTextureLocks();
|
||||
|
||||
// The Theora decoder does not implement seeking at the moment,
|
||||
// so we absolutely want to make sure we don't fall behind too far or
|
||||
// we may end up having the decoder go crazy trying to skip through
|
||||
// Ogg packets (even just reading these undecoded packets takes a
|
||||
// lot of time). So, for the time being, just pause playback when
|
||||
// we go zombie.
|
||||
|
||||
if( mSFXSource )
|
||||
mSFXSource->pause();
|
||||
else
|
||||
mPlaybackTimer.pause();
|
||||
}
|
||||
break;
|
||||
|
||||
case GFXResurrect:
|
||||
if( mAsyncState )
|
||||
{
|
||||
// Reacquire texture locks.
|
||||
|
||||
mAsyncState->getFrameStream()->acquireTextureLocks();
|
||||
|
||||
// Resume playback if we have paused it.
|
||||
|
||||
if( !mIsPaused )
|
||||
{
|
||||
if( mSFXSource )
|
||||
mSFXSource->play();
|
||||
else
|
||||
mPlaybackTimer.start();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::_initVideo()
|
||||
{
|
||||
OggTheoraDecoder* theora = _getTheora();
|
||||
|
||||
// Set the decoder's pixel output format to match
|
||||
// the texture format.
|
||||
|
||||
OggTheoraDecoder::PacketFormat format;
|
||||
format.mFormat = GFXFormatR8G8B8A8;
|
||||
format.mPitch = GFXFormatInfo( format.mFormat ).getBytesPerPixel() * theora->getFrameWidth();
|
||||
|
||||
theora->setPacketFormat( format );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::_initAudio( const ThreadSafeRef< SFXStream >& stream )
|
||||
{
|
||||
// Create an SFXDescription if we don't have one.
|
||||
|
||||
if( !mSFXDescription )
|
||||
{
|
||||
SFXDescription* description = new SFXDescription;
|
||||
description->mIsStreaming = true;
|
||||
description->registerObject();
|
||||
description->setAutoDelete( true );
|
||||
|
||||
mSFXDescription = description;
|
||||
}
|
||||
|
||||
// Create an SFX memory stream that consumes the output
|
||||
// of the Vorbis decoder.
|
||||
|
||||
ThreadSafeRef< SFXStream > sfxStream = stream;
|
||||
if( !sfxStream )
|
||||
{
|
||||
OggVorbisDecoder* vorbis = _getVorbis();
|
||||
SFXFormat sfxFormat( vorbis->getNumChannels(),
|
||||
vorbis->getNumChannels() * 16,
|
||||
vorbis->getSamplesPerSecond() );
|
||||
|
||||
sfxStream = new SFXMemoryStream( sfxFormat, vorbis );
|
||||
}
|
||||
|
||||
// Create the SFXSource.
|
||||
|
||||
mSFXSource = SFX->createSourceFromStream( sfxStream, mSFXDescription );
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
TheoraTexture::TimeSourceType* TheoraTexture::_getTimeSource() const
|
||||
{
|
||||
if( mSFXSource != NULL )
|
||||
return mSFXSource;
|
||||
else
|
||||
return ( TimeSourceType* ) &mPlaybackTimer;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
U32 TheoraTexture::getWidth() const
|
||||
{
|
||||
return _getTheora()->getFrameWidth();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
U32 TheoraTexture::getHeight() const
|
||||
{
|
||||
return _getTheora()->getFrameHeight();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool TheoraTexture::setFile( const String& filename, SFXDescription* desc )
|
||||
{
|
||||
_reset();
|
||||
|
||||
if( filename.isEmpty() )
|
||||
return true;
|
||||
|
||||
// Check SFX profile.
|
||||
|
||||
if( desc && !desc->mIsStreaming )
|
||||
{
|
||||
Con::errorf( "TheoraTexture::setFile - Not a streaming SFXDescription" );
|
||||
return false;
|
||||
}
|
||||
mSFXDescription = desc;
|
||||
|
||||
// Open the Theora file.
|
||||
|
||||
Stream* stream = FileStream::createAndOpen( filename, Torque::FS::File::Read );
|
||||
if( !stream )
|
||||
{
|
||||
Con::errorf( "TheoraTexture::setFile - Theora file '%s' not found.", filename.c_str() );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create the OGG stream.
|
||||
|
||||
Con::printf( "TheoraTexture - Loading file '%s'", filename.c_str() );
|
||||
|
||||
ThreadSafeRef< OggInputStream > oggStream = new OggInputStream( stream );
|
||||
oggStream->addDecoder< OggTheoraDecoder >();
|
||||
#ifndef SPLIT_VORBIS
|
||||
oggStream->addDecoder< OggVorbisDecoder >();
|
||||
#endif
|
||||
|
||||
if( !oggStream->init() )
|
||||
{
|
||||
Con::errorf( "TheoraTexture - Failed to initialize OGG stream" );
|
||||
return false;
|
||||
}
|
||||
|
||||
mFilename = filename;
|
||||
mAsyncState = new AsyncState( oggStream, desc ? desc->mIsLooping : false );
|
||||
|
||||
// Set up video.
|
||||
|
||||
OggTheoraDecoder* theoraDecoder = _getTheora();
|
||||
if( !theoraDecoder )
|
||||
{
|
||||
Con::errorf( "TheoraTexture - '%s' is not a Theora file", filename.c_str() );
|
||||
mAsyncState = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
Con::printf( " - Theora: %ix%i pixels, %.02f fps, %s format",
|
||||
theoraDecoder->getFrameWidth(),
|
||||
theoraDecoder->getFrameHeight(),
|
||||
theoraDecoder->getFramesPerSecond(),
|
||||
GetPixelFormatName( theoraDecoder->getDecoderPixelFormat() ) );
|
||||
|
||||
_initVideo();
|
||||
|
||||
// Set up sound if we have it. For performance reasons, create
|
||||
// a separate physical stream for the Vorbis sound data rather than
|
||||
// using the bitstream from the multiplexed OGG master stream. The
|
||||
// contention caused by the OGG page/packet feeding will otherwise
|
||||
// slow us down significantly.
|
||||
|
||||
#ifdef SPLIT_VORBIS
|
||||
|
||||
stream = FileStream::createAndOpen( filename, Torque::FS::File::Read );
|
||||
if( stream )
|
||||
{
|
||||
ThreadSafeRef< SFXStream > vorbisStream = SFXVorbisStream::create( stream );
|
||||
if( !vorbisStream )
|
||||
{
|
||||
Con::errorf( "TheoraTexture - could not create Vorbis stream for '%s'", filename.c_str() );
|
||||
|
||||
// Stream is deleted by SFXVorbisStream.
|
||||
}
|
||||
else
|
||||
{
|
||||
Con::printf( " - Vorbis: %i channels, %i kHz",
|
||||
vorbisStream->getFormat().getChannels(),
|
||||
vorbisStream->getFormat().getSamplesPerSecond() / 1000 );
|
||||
|
||||
_initAudio( vorbisStream );
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
OggVorbisDecoder* vorbisDecoder = _getVorbis();
|
||||
if( vorbisDecoder )
|
||||
{
|
||||
Con::printf( " - Vorbis: %i bits, %i channels, %i kHz",
|
||||
vorbisDecoder->getNumChannels(),
|
||||
vorbisDecoder->getSamplesPerSecond() / 1000 );
|
||||
|
||||
_initAudio();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Initiate the background request chain.
|
||||
|
||||
mAsyncState->start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
bool TheoraTexture::isPlaying() const
|
||||
{
|
||||
if( !mAsyncState || !mCurrentFrame )
|
||||
return false;
|
||||
|
||||
if( mSFXSource )
|
||||
return mSFXSource->isPlaying();
|
||||
else
|
||||
return mPlaybackTimer.isStarted();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::play()
|
||||
{
|
||||
if( isPlaying() )
|
||||
return;
|
||||
|
||||
if( !mAsyncState )
|
||||
setFile( mFilename, mSFXDescription );
|
||||
|
||||
// Construct playback queue that sync's to our time source,
|
||||
// writes to us, and drops outdated packets.
|
||||
|
||||
if( !mPlaybackQueue )
|
||||
mPlaybackQueue = new PlaybackQueueType( 1, _getTimeSource(), this, 0, true );
|
||||
|
||||
// Start playback.
|
||||
|
||||
if( mSFXSource )
|
||||
mSFXSource->play();
|
||||
else
|
||||
mPlaybackTimer.start();
|
||||
|
||||
mIsPaused = false;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::pause()
|
||||
{
|
||||
if( mSFXSource )
|
||||
mSFXSource->pause();
|
||||
else
|
||||
mPlaybackTimer.pause();
|
||||
|
||||
mIsPaused = true;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::stop()
|
||||
{
|
||||
_reset();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::refresh()
|
||||
{
|
||||
PROFILE_SCOPE( TheoraTexture_refresh );
|
||||
|
||||
if( !mAsyncState || !mPlaybackQueue )
|
||||
return;
|
||||
|
||||
// Synchronize the async state to our current time.
|
||||
// Unfortunately, we cannot set the Theora decoder to
|
||||
// synchronize directly with us as our lifetime and the
|
||||
// lifetime of our time sources isn't bound to the
|
||||
// threaded state.
|
||||
|
||||
mAsyncState->syncTime( _getTimeSource()->getPosition() );
|
||||
|
||||
// Update the texture, if necessary.
|
||||
|
||||
bool haveFrame = false;
|
||||
while( mPlaybackQueue->needPacket() )
|
||||
{
|
||||
// Lock the current frame.
|
||||
|
||||
if( mCurrentFrame && !mCurrentFrame->mLockedRect )
|
||||
mCurrentFrame->mLockedRect = mCurrentFrame->mTexture.lock();
|
||||
|
||||
// Try to read a new frame.
|
||||
|
||||
TheoraTextureFrame* frame = mAsyncState->readNextFrame();
|
||||
if( !frame )
|
||||
break;
|
||||
|
||||
// Submit frame to queue.
|
||||
|
||||
mPlaybackQueue->submitPacket(
|
||||
frame,
|
||||
frame->mFrameDuration * 1000.f,
|
||||
false,
|
||||
frame->mFrameTime * 1000.f
|
||||
);
|
||||
|
||||
// See if we have dropped frames.
|
||||
|
||||
if( frame->mFrameNumber != mLastFrameNumber + 1 )
|
||||
mNumDroppedFrames += frame->mFrameNumber - mLastFrameNumber - 1;
|
||||
mLastFrameNumber = frame->mFrameNumber;
|
||||
|
||||
haveFrame = true;
|
||||
}
|
||||
|
||||
// Unlock current frame.
|
||||
|
||||
if( mCurrentFrame && mCurrentFrame->mLockedRect )
|
||||
{
|
||||
mCurrentFrame->mTexture.unlock();
|
||||
mCurrentFrame->mLockedRect = NULL;
|
||||
}
|
||||
|
||||
// Release async state if we have reached the
|
||||
// end of the Ogg stream.
|
||||
|
||||
if( mAsyncState->isAtEnd() && !haveFrame )
|
||||
_reset();
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
void TheoraTexture::write( TheoraTextureFrame* const* frames, U32 num )
|
||||
{
|
||||
if( !num )
|
||||
return;
|
||||
|
||||
mCurrentFrame = frames[ num - 1 ]; // Only used last.
|
||||
}
|
||||
|
||||
#endif // TORQUE_OGGTHEORA
|
||||
418
Engine/source/gfx/video/theoraTexture.h
Normal file
418
Engine/source/gfx/video/theoraTexture.h
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) 2012 GarageGames, LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef _THEORATEXTURE_H_
|
||||
#define _THEORATEXTURE_H_
|
||||
|
||||
#ifdef TORQUE_OGGTHEORA
|
||||
|
||||
#ifndef _PLATFORM_H_
|
||||
#include "platform/platform.h"
|
||||
#endif
|
||||
#ifndef _GFXTEXTUREHANDLE_H_
|
||||
#include "gfx/gfxTextureHandle.h"
|
||||
#endif
|
||||
#ifndef _ASYNCPACKETQUEUE_H_
|
||||
#include "platform/async/asyncPacketQueue.h"
|
||||
#endif
|
||||
#ifndef _ASYNCBUFFEREDSTREAM_H_
|
||||
#include "platform/async/asyncBufferedStream.h"
|
||||
#endif
|
||||
#ifndef _TIMESOURCE_H_
|
||||
#include "core/util/timeSource.h"
|
||||
#endif
|
||||
#ifndef _THREADSAFEREFCOUNT_H_
|
||||
#include "platform/threads/threadSafeRefCount.h"
|
||||
#endif
|
||||
#ifndef _RAWDATA_H_
|
||||
#include "core/util/rawData.h"
|
||||
#endif
|
||||
#ifndef _SIMOBJECT_H_
|
||||
#include "console/simObject.h"
|
||||
#endif
|
||||
#ifndef _SFXSTREAM_H_
|
||||
#include "sfx/sfxStream.h"
|
||||
#endif
|
||||
#ifndef _OGGTHEORADECODER_H_
|
||||
#include "core/ogg/oggTheoraDecoder.h"
|
||||
#endif
|
||||
#ifndef _TYPETRAITS_H_
|
||||
#include "platform/typetraits.h"
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
class SFXDescription;
|
||||
class SFXSound;
|
||||
class SFXVorbisStream;
|
||||
|
||||
class OggInputStream;
|
||||
class OggVorbisDecoder;
|
||||
|
||||
class Stream;
|
||||
|
||||
|
||||
/// A single frame in the video frame stream.
|
||||
///
|
||||
/// Frames are uploaded directly into textures by the asynchronous
|
||||
/// streaming system. This offloads as much work as possible to the worker
|
||||
/// threads and guarantees the smoothest possible playback.
|
||||
///
|
||||
/// Frame records are re-used and are managed directly by the video frame
|
||||
/// stream. The number of textures concurrently used by a Theora stream
|
||||
/// is determined by its stream read-ahead.
|
||||
class TheoraTextureFrame
|
||||
{
|
||||
public:
|
||||
|
||||
typedef void Parent;
|
||||
|
||||
/// The texture containing the video frame.
|
||||
GFXTexHandle mTexture;
|
||||
|
||||
/// The locked rectangle, if the texture is currently locked.
|
||||
/// Frames will remain in locked state except if currently displayed.
|
||||
GFXLockedRect* mLockedRect;
|
||||
|
||||
///
|
||||
U32 mFrameNumber;
|
||||
|
||||
/// The play time in seconds at which to display this frame.
|
||||
F32 mFrameTime;
|
||||
|
||||
/// The duration in seconds to display this frame.
|
||||
F32 mFrameDuration;
|
||||
|
||||
TheoraTextureFrame()
|
||||
: mLockedRect( NULL )
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
inline void destructSingle( TheoraTextureFrame* frame )
|
||||
{
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
|
||||
/// TheoraTexture decodes Ogg Theora files, and their audio.
|
||||
///
|
||||
/// TheoraTexture objects can be used similarly to TextureObjects. Just
|
||||
/// set the video, call play(), and then refresh every frame to get the
|
||||
/// latest video. Audio happens automagically.
|
||||
///
|
||||
/// @note Uses Theora and ogg libraries which are Copyright (C) Xiph.org Foundation
|
||||
class TheoraTexture : private IOutputStream< TheoraTextureFrame* >,
|
||||
public IPositionable< U32 >
|
||||
{
|
||||
public:
|
||||
|
||||
typedef void Parent;
|
||||
|
||||
protected:
|
||||
|
||||
typedef IPositionable< U32 > TimeSourceType;
|
||||
typedef GenericTimeSource<> TimerType;
|
||||
typedef AsyncPacketQueue< TheoraTextureFrame*, TimeSourceType*, IOutputStream< TheoraTextureFrame* >*, F32 > PlaybackQueueType;
|
||||
|
||||
class FrameStream;
|
||||
class AsyncState;
|
||||
friend class GuiTheoraCtrl; // accesses OggTheoraDecoder to set transcoder
|
||||
|
||||
/// Parameters for tuning streaming behavior.
|
||||
enum
|
||||
{
|
||||
/// Number of textures to load in background.
|
||||
FRAME_READ_AHEAD = 6,
|
||||
};
|
||||
|
||||
/// WorkItem that reads a frame from a Theora decoder and uploads it into a TheoraTextureFrame.
|
||||
///
|
||||
/// Loading directly into textures moves the costly uploads out of the main thread into worker
|
||||
/// threads. The downside to this is that since we cannot do GFX work on the worker threads,
|
||||
/// we need to expect textures to get to us in locked state.
|
||||
class FrameReadItem : public ThreadWorkItem
|
||||
{
|
||||
public:
|
||||
|
||||
typedef ThreadWorkItem Parent;
|
||||
|
||||
protected:
|
||||
|
||||
/// The asynchronous state we belong to. This reference
|
||||
/// ensures that all our streaming state stays live for as long as our
|
||||
/// work item is in the pipeline.
|
||||
ThreadSafeRef< AsyncState > mAsyncState;
|
||||
|
||||
///
|
||||
FrameStream* mFrameStream;
|
||||
|
||||
/// The frame texture we are loading.
|
||||
TheoraTextureFrame* mFrame;
|
||||
|
||||
// WorkItem.
|
||||
virtual void execute();
|
||||
|
||||
public:
|
||||
|
||||
///
|
||||
FrameReadItem( AsyncBufferedInputStream< TheoraTextureFrame*, IInputStream< OggTheoraFrame* >* >* stream,
|
||||
ThreadPool::Context* context );
|
||||
};
|
||||
|
||||
/// Stream filter that turns a stream of OggTheoraFrames into a buffered background stream of TheoraTextureFrame
|
||||
/// records.
|
||||
///
|
||||
/// This streams allocates a fixed amount 'M' of TheoraTextureFrames. Reading the n-th frame from the stream, will
|
||||
/// automatically invalidate the (n-M)-th frame.
|
||||
class FrameStream : public AsyncSingleBufferedInputStream< TheoraTextureFrame*, IInputStream< OggTheoraFrame* >*, FrameReadItem >
|
||||
{
|
||||
public:
|
||||
|
||||
typedef AsyncSingleBufferedInputStream< TheoraTextureFrame*, IInputStream< OggTheoraFrame* >*, FrameReadItem > Parent;
|
||||
|
||||
protected:
|
||||
|
||||
friend class FrameReadItem;
|
||||
|
||||
enum
|
||||
{
|
||||
/// Number of TheoraTextureFrame records to allocate.
|
||||
///
|
||||
/// We need to pre-allocate TheoraTextureFrames as we cannot do GFX operations
|
||||
/// on the fly on worker threads. This number corresponds to the length of the
|
||||
/// buffering queue plus one record that will be returned to the user as the
|
||||
/// current frame record.
|
||||
NUM_FRAME_RECORDS = FRAME_READ_AHEAD + 1
|
||||
};
|
||||
|
||||
/// Asynchronous state of the texture object.
|
||||
/// This is *NOT* a ThreadSafeRef to not create a cycle.
|
||||
AsyncState* mAsyncState;
|
||||
|
||||
/// Wrap-around index into "mFrames."
|
||||
U32 mFrameIndex;
|
||||
|
||||
/// The pre-allocated TheoraTextureFrames.
|
||||
TheoraTextureFrame mFrames[ NUM_FRAME_RECORDS ];
|
||||
|
||||
public:
|
||||
|
||||
///
|
||||
FrameStream( AsyncState* asyncState, bool looping = false );
|
||||
|
||||
///
|
||||
void acquireTextureLocks();
|
||||
|
||||
///
|
||||
void releaseTextureLocks();
|
||||
};
|
||||
|
||||
/// Encapsulation of compound asynchronous state. Allows releasing
|
||||
/// the entire state in one go.
|
||||
class AsyncState : public ThreadSafeRefCount< AsyncState >,
|
||||
private IPositionable< U32 >
|
||||
{
|
||||
public:
|
||||
|
||||
typedef void Parent;
|
||||
friend class FrameStream; // mDecoderBufferStream
|
||||
|
||||
protected:
|
||||
|
||||
typedef AsyncSingleBufferedInputStream< OggTheoraFrame* > DecoderBufferStream;
|
||||
|
||||
/// Last synchronization position in the video stream. This is what the
|
||||
/// Theora decoder gets passed to see if frames are outdated.
|
||||
U32 mCurrentTime;
|
||||
|
||||
/// The Ogg master stream.
|
||||
ThreadSafeRef< OggInputStream > mOggStream;
|
||||
|
||||
/// The raw video decoding stream.
|
||||
OggTheoraDecoder* mTheoraDecoder;
|
||||
|
||||
/// The raw sound decoding stream; NULL if no Vorbis in video or if
|
||||
/// Vorbis is streamed separately.
|
||||
OggVorbisDecoder* mVorbisDecoder;
|
||||
|
||||
/// The background-buffered frame stream.
|
||||
ThreadSafeRef< FrameStream > mFrameStream;
|
||||
|
||||
public:
|
||||
|
||||
///
|
||||
AsyncState( const ThreadSafeRef< OggInputStream >& oggStream, bool looping = false );
|
||||
|
||||
/// Return the Theora decoder substream.
|
||||
OggTheoraDecoder* getTheora() const { return mTheoraDecoder; }
|
||||
|
||||
/// Return the Vorbis decoder substream.
|
||||
/// @note If Vorbis streaming is split out into a separate physical substream,
|
||||
/// this method will always return NULL even if Vorbis sound is being used.
|
||||
OggVorbisDecoder* getVorbis() const { return mVorbisDecoder; }
|
||||
|
||||
///
|
||||
const ThreadSafeRef< FrameStream >& getFrameStream() const { return mFrameStream; }
|
||||
|
||||
///
|
||||
TheoraTextureFrame* readNextFrame();
|
||||
|
||||
///
|
||||
void start();
|
||||
|
||||
///
|
||||
void stop();
|
||||
|
||||
///
|
||||
bool isAtEnd();
|
||||
|
||||
///
|
||||
void syncTime( U32 ms ) { mCurrentTime = ms; }
|
||||
|
||||
private:
|
||||
|
||||
// IPositionable.
|
||||
virtual U32 getPosition() const { return mCurrentTime; }
|
||||
virtual void setPosition( U32 pos ) {}
|
||||
};
|
||||
|
||||
/// The Theora video file.
|
||||
String mFilename;
|
||||
|
||||
/// The SFXDescription used for sound playback. Synthesized if not provided.
|
||||
SimObjectPtr< SFXDescription > mSFXDescription;
|
||||
|
||||
/// If there's a Vorbis stream, this is the sound source used for playback.
|
||||
/// Playback will synchronize to this source then.
|
||||
SimObjectPtr< SFXSound > mSFXSource;
|
||||
|
||||
/// The current frame.
|
||||
TheoraTextureFrame* mCurrentFrame;
|
||||
|
||||
/// The queue that synchronizes the writing of frames to the TheoraTexture.
|
||||
PlaybackQueueType* mPlaybackQueue;
|
||||
|
||||
/// The timer for synchronizing playback when there is no audio stream
|
||||
/// to synchronize to.
|
||||
TimerType mPlaybackTimer;
|
||||
|
||||
/// Our threaded state.
|
||||
ThreadSafeRef< AsyncState > mAsyncState;
|
||||
|
||||
///
|
||||
bool mIsPaused;
|
||||
|
||||
///
|
||||
U32 mLastFrameNumber;
|
||||
|
||||
/// Number of frames we have dropped so far.
|
||||
U32 mNumDroppedFrames;
|
||||
|
||||
/// Release all dynamic playback state.
|
||||
void _reset();
|
||||
|
||||
/// Initialize video playback.
|
||||
void _initVideo();
|
||||
|
||||
/// Initialize audio playback.
|
||||
void _initAudio( const ThreadSafeRef< SFXStream >& stream = NULL );
|
||||
|
||||
/// Return the Theora decoder stream or NULL.
|
||||
OggTheoraDecoder* _getTheora() const { return ( mAsyncState != NULL ? mAsyncState->getTheora() : NULL ); }
|
||||
|
||||
/// Return the Vorbis decoder stream or NULL.
|
||||
OggVorbisDecoder* _getVorbis() const { return ( mAsyncState != NULL ? mAsyncState->getVorbis() : NULL ); }
|
||||
|
||||
/// Return the object that is acting as our time source.
|
||||
TimeSourceType* _getTimeSource() const;
|
||||
|
||||
///
|
||||
void _onTextureEvent( GFXTexCallbackCode code );
|
||||
|
||||
// IOutputStream.
|
||||
virtual void write( TheoraTextureFrame* const* frames, U32 num );
|
||||
|
||||
public:
|
||||
|
||||
///
|
||||
TheoraTexture();
|
||||
|
||||
~TheoraTexture();
|
||||
|
||||
/// Return the width of a single video frame in pixels.
|
||||
U32 getWidth() const;
|
||||
|
||||
/// Return the height of a single video frame in pixels.
|
||||
U32 getHeight() const;
|
||||
|
||||
/// Return the filename of the Theora video file loaded by the player.
|
||||
const String& getFilename() const { return mFilename; }
|
||||
|
||||
/// Load the given Theora video file. Set up audio using the given SFXDescription (must have
|
||||
/// streaming enabled). If no SFXDescription is provided, a default description is used.
|
||||
bool setFile( const String& filename, SFXDescription* desc = NULL );
|
||||
|
||||
/// Start video playback.
|
||||
void play();
|
||||
|
||||
/// Pause video playback.
|
||||
void pause();
|
||||
|
||||
/// Stop video playback.
|
||||
void stop();
|
||||
|
||||
/// Refresh the current frame. This should be called before getTexture() to ensure that
|
||||
/// the texture returned by getTexture() contains up-to-date contents.
|
||||
void refresh();
|
||||
|
||||
/// Return true if a video file has been loaded and is ready for playback.
|
||||
bool isReady() const { return ( mAsyncState != NULL ); }
|
||||
|
||||
/// Return true if the video is currently playing.
|
||||
bool isPlaying() const;
|
||||
|
||||
/// Return true if the video is currently paused.
|
||||
bool isPaused() const { return mIsPaused; }
|
||||
|
||||
/// Return the sequence number of the current frame. Starts at 0.
|
||||
U32 getFrameNumber() const { return mCurrentFrame->mFrameNumber; }
|
||||
|
||||
/// Return the playback position of the current frame.
|
||||
F32 getFrameTime() const { return mCurrentFrame->mFrameTime; }
|
||||
|
||||
/// Return the number of frames that have been dropped so far.
|
||||
U32 getNumDroppedFrames() const { return mNumDroppedFrames; }
|
||||
|
||||
/// Return the texture containing the current frame. Call refresh() first
|
||||
/// to ensure the texture contents are up-to-date.
|
||||
const GFXTexHandle& getTexture() const { return mCurrentFrame->mTexture; }
|
||||
GFXTexHandle& getTexture() { return mCurrentFrame->mTexture; }
|
||||
|
||||
// IPositionable.
|
||||
virtual U32 getPosition() const { return _getTimeSource()->getPosition(); }
|
||||
virtual void setPosition( U32 pos ) {} // Not (yet?) implemented.
|
||||
};
|
||||
|
||||
#endif // TORQUE_OGGTHEORA
|
||||
#endif // !_THEORATEXTURE_H_
|
||||
359
Engine/source/gfx/video/videoCapture.cpp
Normal file
359
Engine/source/gfx/video/videoCapture.cpp
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) 2012 GarageGames, LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include "platform/platform.h"
|
||||
#include "gfx/video/videoCapture.h"
|
||||
|
||||
#include "console/console.h"
|
||||
#include "core/strings/stringFunctions.h"
|
||||
#include "core/util/journal/journal.h"
|
||||
#include "core/module.h"
|
||||
#include "gui/core/guiCanvas.h"
|
||||
#include "gfx/gfxTextureManager.h"
|
||||
#include "console/engineAPI.h"
|
||||
|
||||
|
||||
Vector<VideoCapture::EncoderFactory> VideoCapture::mEncoderFactoryFnList;
|
||||
|
||||
MODULE_BEGIN( VideoCapture )
|
||||
|
||||
MODULE_INIT_BEFORE( GFX )
|
||||
MODULE_SHUTDOWN_BEFORE( GFX )
|
||||
|
||||
MODULE_INIT
|
||||
{
|
||||
ManagedSingleton< VideoCapture >::createSingleton();
|
||||
}
|
||||
|
||||
MODULE_SHUTDOWN
|
||||
{
|
||||
VIDCAP->end();
|
||||
ManagedSingleton< VideoCapture >::deleteSingleton();
|
||||
}
|
||||
|
||||
MODULE_END;
|
||||
|
||||
VideoCapture::VideoCapture() :
|
||||
mEncoder(NULL),
|
||||
mIsRecording(false),
|
||||
mCanvas(NULL),
|
||||
mFrameGrabber(NULL),
|
||||
mWaitingForCanvas(false),
|
||||
mResolution(0,0),
|
||||
mFrameRate(30.0f),
|
||||
mEncoderName("THEORA"),
|
||||
mFileName(""),
|
||||
mMsPerFrameError(0)
|
||||
{
|
||||
}
|
||||
|
||||
S32 VideoCapture::getMsPerFrame()
|
||||
{
|
||||
//Add accumulated error to ms per frame before rounding
|
||||
F32 roundTime = mFloor(mMsPerFrame + mMsPerFrameError + 0.5f);
|
||||
|
||||
//Accumulate the rounding errors
|
||||
mMsPerFrameError += mMsPerFrame - roundTime;
|
||||
|
||||
return (S32)roundTime;
|
||||
}
|
||||
|
||||
void VideoCapture::begin( GuiCanvas* canvas )
|
||||
{
|
||||
// No longer waiting for a canvas
|
||||
mWaitingForCanvas = false;
|
||||
|
||||
// No specified file
|
||||
if (mFileName.isEmpty())
|
||||
{
|
||||
Con::errorf("VideoCapture: no file specified!");
|
||||
return;
|
||||
}
|
||||
|
||||
// No framegrabber, cannot capture
|
||||
if (mFrameGrabber == NULL)
|
||||
{
|
||||
Con::errorf("VideoCapture: cannot capture without a VideoFrameGrabber! One should be created in the GFXDevice initialization!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the active encoder
|
||||
if (!initEncoder(mEncoderName))
|
||||
return;
|
||||
|
||||
// Store the canvas, so we know which one to capture from
|
||||
mCanvas = canvas;
|
||||
|
||||
// If the resolution is zero, get the current video mode
|
||||
if (mResolution.isZero())
|
||||
mResolution = mCanvas->getPlatformWindow()->getVideoMode().resolution;
|
||||
|
||||
// Set the encoder file, framerate and resolution
|
||||
mEncoder->setFile(mFileName);
|
||||
mEncoder->setFramerate( &mFrameRate );
|
||||
mEncoder->setResolution( &mResolution );
|
||||
|
||||
// The frame grabber must know about the resolution as well, since it'll do the resizing for us
|
||||
mFrameGrabber->setOutResolution( mResolution );
|
||||
|
||||
// Calculate the ms per frame
|
||||
mMsPerFrame = 1000.0f / mFrameRate;
|
||||
|
||||
// Start the encoder
|
||||
if (!mEncoder->begin())
|
||||
return;
|
||||
|
||||
// We're now recording
|
||||
mIsRecording = true;
|
||||
mNextFramePosition = 0.0f;
|
||||
}
|
||||
|
||||
void VideoCapture::end()
|
||||
{
|
||||
if (!mIsRecording)
|
||||
return;
|
||||
|
||||
if (mEncoder && !mEncoder->end())
|
||||
Con::errorf("VideoCapture: an error has ocurred while closing the video stream");
|
||||
|
||||
// Garbage collect the processed bitmaps
|
||||
deleteProcessedBitmaps();
|
||||
|
||||
delete mEncoder;
|
||||
mEncoder = NULL;
|
||||
|
||||
mIsRecording = false;
|
||||
}
|
||||
|
||||
void VideoCapture::capture()
|
||||
{
|
||||
// If this is the first frame, capture and encode it right away
|
||||
if (mNextFramePosition == 0.0f)
|
||||
{
|
||||
mVideoCaptureStartTime = Platform::getVirtualMilliseconds();
|
||||
mCapturedFramePos = -1.0f;
|
||||
}
|
||||
|
||||
// Calculate the frame position for this captured frame
|
||||
U32 frameTimeMs = Platform::getVirtualMilliseconds() - mVideoCaptureStartTime;
|
||||
F32 framePosition = (F32)frameTimeMs / mMsPerFrame;
|
||||
|
||||
// Repeat until the current frame is captured
|
||||
while (framePosition > mCapturedFramePos)
|
||||
{
|
||||
// If the frame position is closer to the next frame position
|
||||
// than the previous one capture it
|
||||
if ( mFabs(framePosition - mNextFramePosition) < mFabs(mCapturedFramePos - mNextFramePosition) )
|
||||
{
|
||||
mFrameGrabber->captureBackBuffer();
|
||||
mCapturedFramePos = framePosition;
|
||||
}
|
||||
|
||||
// If the new frame position is greater or equal than the next frame time
|
||||
// tell the framegrabber to make bitmaps out from the last captured backbuffer until the video catches up
|
||||
while ( framePosition >= mNextFramePosition )
|
||||
{
|
||||
mFrameGrabber->makeBitmap();
|
||||
mNextFramePosition++;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch bitmaps from the framegrabber and encode them
|
||||
GBitmap *bitmap = NULL;
|
||||
while ( (bitmap = mFrameGrabber->fetchBitmap()) != NULL )
|
||||
{
|
||||
//mEncoder->pushProcessedBitmap(bitmap);
|
||||
if (!mEncoder->pushFrame(bitmap))
|
||||
{
|
||||
Con::errorf("VideoCapture: an error occurred while encoding a frame. Recording aborted.");
|
||||
end();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Garbage collect the processed bitmaps
|
||||
deleteProcessedBitmaps();
|
||||
}
|
||||
|
||||
|
||||
void VideoCapture::registerEncoder( const char* name, VideoEncoderFactoryFn factoryFn )
|
||||
{
|
||||
mEncoderFactoryFnList.increment();
|
||||
mEncoderFactoryFnList.last().name = name;
|
||||
mEncoderFactoryFnList.last().factory = factoryFn;
|
||||
}
|
||||
|
||||
|
||||
bool VideoCapture::initEncoder( const char* name )
|
||||
{
|
||||
if ( mEncoder )
|
||||
{
|
||||
Con::errorf("VideoCapture:: cannot change video encoder while capturing! Stop the capture first!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try creating an encoder based on the name
|
||||
for (U32 i=0; i<mEncoderFactoryFnList.size(); i++)
|
||||
{
|
||||
if (dStricmp(name, mEncoderFactoryFnList[i].name) == 0)
|
||||
{
|
||||
mEncoder = mEncoderFactoryFnList[i].factory();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//If we got here there's no encoder matching the speficied name
|
||||
Con::errorf("\"%s\" isn't a valid encoder!", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
void VideoCapture::deleteProcessedBitmaps()
|
||||
{
|
||||
if (mEncoder == NULL)
|
||||
return;
|
||||
|
||||
//Grab bitmaps processed by our encoder
|
||||
GBitmap* bitmap = NULL;
|
||||
while ( (bitmap = mEncoder->getProcessedBitmap()) != NULL )
|
||||
mBitmapDeleteList.push_back(bitmap);
|
||||
|
||||
//Now delete them (or not... se below)
|
||||
while ( mBitmapDeleteList.size() )
|
||||
{
|
||||
bitmap = mBitmapDeleteList[0];
|
||||
mBitmapDeleteList.pop_front();
|
||||
|
||||
// Delete the bitmap only if it's the different than the next one (or it's the last one).
|
||||
// This is done because repeated frames re-use the same GBitmap object
|
||||
// and thus their pointers will appearl multiple times in the list
|
||||
if (mBitmapDeleteList.size() == 0 || bitmap != mBitmapDeleteList[0])
|
||||
delete bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
///----------------------------------------------------------------------
|
||||
|
||||
///----------------------------------------------------------------------
|
||||
|
||||
void VideoEncoder::setFile( const char* path )
|
||||
{
|
||||
mPath = path;
|
||||
}
|
||||
|
||||
GBitmap* VideoEncoder::getProcessedBitmap()
|
||||
{
|
||||
GBitmap* bitmap = NULL;
|
||||
if (mProcessedBitmaps.tryPopFront(bitmap))
|
||||
return bitmap;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void VideoEncoder::pushProcessedBitmap( GBitmap* bitmap )
|
||||
{
|
||||
mProcessedBitmaps.pushBack(bitmap);
|
||||
}
|
||||
|
||||
///----------------------------------------------------------------------
|
||||
|
||||
///----------------------------------------------------------------------
|
||||
|
||||
GBitmap* VideoFrameGrabber::fetchBitmap()
|
||||
{
|
||||
if (mBitmapList.size() == 0)
|
||||
return NULL;
|
||||
|
||||
GBitmap *bitmap = mBitmapList.first();
|
||||
mBitmapList.pop_front();
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
VideoFrameGrabber::VideoFrameGrabber()
|
||||
{
|
||||
GFXTextureManager::addEventDelegate( this, &VideoFrameGrabber::_onTextureEvent );
|
||||
}
|
||||
|
||||
VideoFrameGrabber::~VideoFrameGrabber()
|
||||
{
|
||||
GFXTextureManager::removeEventDelegate( this, &VideoFrameGrabber::_onTextureEvent );
|
||||
}
|
||||
|
||||
void VideoFrameGrabber::_onTextureEvent(GFXTexCallbackCode code)
|
||||
{
|
||||
if ( code == GFXZombify )
|
||||
releaseTextures();
|
||||
}
|
||||
|
||||
///----------------------------------------------------------------------
|
||||
|
||||
///----------------------------------------------------------------------
|
||||
|
||||
DefineEngineFunction( startVideoCapture, void,
|
||||
( GuiCanvas *canvas, const char *filename, const char *encoder, F32 framerate, Point2I resolution ),
|
||||
( "THEORA", 30.0f, Point2I( 0, 0 ) ),
|
||||
"Begins a video capture session.\n"
|
||||
"@see stopVideoCapture\n"
|
||||
"@ingroup Rendering\n" )
|
||||
{
|
||||
if ( !canvas )
|
||||
{
|
||||
Con::errorf("startVideoCapture -Please specify a GuiCanvas object to record from!");
|
||||
return;
|
||||
}
|
||||
|
||||
VIDCAP->setFilename( filename );
|
||||
VIDCAP->setEncoderName( encoder );
|
||||
VIDCAP->setFramerate( framerate );
|
||||
|
||||
if ( !resolution.isZero() )
|
||||
VIDCAP->setResolution(resolution);
|
||||
|
||||
VIDCAP->begin(canvas);
|
||||
}
|
||||
|
||||
DefineEngineFunction( stopVideoCapture, void, (),,
|
||||
"Stops the video capture session.\n"
|
||||
"@see startVideoCapture\n"
|
||||
"@ingroup Rendering\n" )
|
||||
{
|
||||
VIDCAP->end();
|
||||
}
|
||||
|
||||
DefineEngineFunction( playJournalToVideo, void,
|
||||
( const char *journalFile, const char *videoFile, const char *encoder, F32 framerate, Point2I resolution ),
|
||||
( NULL, "THEORA", 30.0f, Point2I( 0, 0 ) ),
|
||||
"Load a journal file and capture it video.\n"
|
||||
"@ingroup Rendering\n" )
|
||||
{
|
||||
if ( !videoFile )
|
||||
videoFile = journalFile;
|
||||
|
||||
VIDCAP->setFilename( Torque::Path( videoFile ).getFileName() );
|
||||
VIDCAP->setEncoderName( encoder );
|
||||
VIDCAP->setFramerate( framerate );
|
||||
|
||||
if ( !resolution.isZero() )
|
||||
VIDCAP->setResolution(resolution);
|
||||
|
||||
VIDCAP->waitForCanvas();
|
||||
|
||||
Journal::Play( journalFile );
|
||||
}
|
||||
260
Engine/source/gfx/video/videoCapture.h
Normal file
260
Engine/source/gfx/video/videoCapture.h
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) 2012 GarageGames, LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef _VIDEOCAPTURE_H_
|
||||
#define _VIDEOCAPTURE_H_
|
||||
|
||||
#ifndef _TSINGLETON_H_
|
||||
#include "core/util/tSingleton.h"
|
||||
#endif
|
||||
|
||||
#ifndef _TVECTOR_H_
|
||||
#include "core/util/tVector.h"
|
||||
#endif
|
||||
|
||||
#ifndef _TORQUE_STRING_H_
|
||||
#include "core/util/str.h"
|
||||
#endif
|
||||
|
||||
#ifndef _GFXTEXTUREHANDLE_H_
|
||||
#include "gfx/gfxTextureHandle.h"
|
||||
#endif
|
||||
|
||||
#ifndef _MPOINT2_H_
|
||||
#include "math/mPoint2.h"
|
||||
#endif
|
||||
|
||||
#ifndef _THREADSAFEDEQUE_H_
|
||||
#include "platform/threads/threadSafeDeque.h"
|
||||
#endif
|
||||
|
||||
|
||||
class GuiCanvas;
|
||||
class VideoFrameGrabber;
|
||||
class VideoEncoder;
|
||||
class GBitmap;
|
||||
|
||||
typedef VideoEncoder* (*VideoEncoderFactoryFn)();
|
||||
|
||||
|
||||
/// Abstract frame grabber class
|
||||
/// implementation and initalization depends on video device
|
||||
class VideoFrameGrabber
|
||||
{
|
||||
friend class VideoCapture;
|
||||
protected:
|
||||
Point2I mResolution; // The resolution used to capture the back buffer (scaling will be used)
|
||||
Vector<GBitmap*> mBitmapList; //List of bitmaps created from backbuffer captures
|
||||
|
||||
/// Sets the output frame resolution
|
||||
void setOutResolution( const Point2I& res ) { mResolution = res; }
|
||||
|
||||
/// Pushes a fresh bitmap into our list
|
||||
void pushNewBitmap( GBitmap* bitmap ) { mBitmapList.push_back(bitmap); }
|
||||
|
||||
/// Returns one captured bitmap. Returns NULL if there are no more bitmaps.
|
||||
GBitmap* fetchBitmap();
|
||||
|
||||
/// Texture event callback
|
||||
void _onTextureEvent(GFXTexCallbackCode code);
|
||||
|
||||
/// Captures the current backbuffer. If the last capture wasn't made into a bitmap, it will be overriden.
|
||||
virtual void captureBackBuffer() = 0;
|
||||
|
||||
/// Starts converting the last backbuffer capture to a bitmap
|
||||
/// Depending on the VideoFrameGrabber implementation, this may not produce a bitmap right away.
|
||||
virtual void makeBitmap() = 0;
|
||||
|
||||
/// Releases internal textures
|
||||
virtual void releaseTextures() {};
|
||||
|
||||
public:
|
||||
VideoFrameGrabber();
|
||||
virtual ~VideoFrameGrabber();
|
||||
};
|
||||
|
||||
|
||||
/// Video capture interface class
|
||||
class VideoCapture
|
||||
{
|
||||
private:
|
||||
struct EncoderFactory {
|
||||
const char* name;
|
||||
VideoEncoderFactoryFn factory;
|
||||
};
|
||||
|
||||
/// List of encoder factory functions
|
||||
static Vector<EncoderFactory> mEncoderFactoryFnList;
|
||||
|
||||
// The frame position of the latest backbuffer capture
|
||||
F32 mCapturedFramePos;
|
||||
|
||||
/// Our current video encoder
|
||||
VideoEncoder* mEncoder;
|
||||
|
||||
/// Our video frame grabber
|
||||
VideoFrameGrabber* mFrameGrabber;
|
||||
|
||||
/// The canvas we're recording from
|
||||
GuiCanvas* mCanvas;
|
||||
|
||||
/// True if we're recording
|
||||
bool mIsRecording;
|
||||
|
||||
/// Time when we captured the previous frame
|
||||
U32 mVideoCaptureStartTime;
|
||||
|
||||
/// Frame to be captured next
|
||||
F32 mNextFramePosition;
|
||||
|
||||
/// The per-frame time (in milliseconds)
|
||||
F32 mMsPerFrame;
|
||||
|
||||
/// The framerate we'll use to record
|
||||
F32 mFrameRate;
|
||||
|
||||
/// Accumulated error when converting the per-frame-time to integer
|
||||
/// this is used to dither the value and keep overall time advancing
|
||||
/// correct
|
||||
F32 mMsPerFrameError;
|
||||
|
||||
/// Name of the encoder we'll be using
|
||||
String mEncoderName;
|
||||
|
||||
/// The video output resolution
|
||||
Point2I mResolution;
|
||||
|
||||
/// Output filename
|
||||
String mFileName;
|
||||
|
||||
/// Tur if we're waiting for a canvas to bre created before capturing
|
||||
bool mWaitingForCanvas;
|
||||
|
||||
/// Vector with bitmaps to delete
|
||||
Vector< GBitmap* > mBitmapDeleteList;
|
||||
|
||||
/// Initializes our encoder
|
||||
bool initEncoder( const char* name );
|
||||
|
||||
/// Deletes processed bitmaps
|
||||
void deleteProcessedBitmaps();
|
||||
|
||||
public:
|
||||
VideoCapture();
|
||||
|
||||
/// Start a video capture session
|
||||
void begin( GuiCanvas* canvas );
|
||||
|
||||
/// Captures a new frame
|
||||
void capture();
|
||||
|
||||
/// Ends a video capture
|
||||
void end();
|
||||
|
||||
/// Sets the output filename
|
||||
void setFilename( const char* filename ) { mFileName = filename; }
|
||||
|
||||
/// Sets the encoder we'll use
|
||||
void setEncoderName( const char* encoder ) { mEncoderName = encoder; }
|
||||
|
||||
/// Sets the framerate
|
||||
void setFramerate( F32 fps ) { mFrameRate = fps; }
|
||||
|
||||
/// Sets the video output resolution
|
||||
void setResolution(const Point2I& res) { mResolution = res; }
|
||||
|
||||
/// Returns true if we're capturing
|
||||
bool isRecording() { return mIsRecording; }
|
||||
|
||||
/// Returns the number of milliseconds per frame
|
||||
S32 getMsPerFrame();
|
||||
|
||||
/// Sets the video farme grabber (cannot record without one).
|
||||
void setFrameGrabber( VideoFrameGrabber* grabber ) { mFrameGrabber = grabber; }
|
||||
|
||||
/// This will make the video capture begin capturing
|
||||
/// as soon as a GuiCanvas is created
|
||||
void waitForCanvas() { mWaitingForCanvas = true; }
|
||||
bool isWaitingForCanvas() { return mWaitingForCanvas; }
|
||||
|
||||
/// Registers an encoder
|
||||
static void registerEncoder( const char* name, VideoEncoderFactoryFn factoryFn );
|
||||
|
||||
// For ManagedSingleton.
|
||||
static const char* getSingletonName() { return "VideoCapture"; }
|
||||
};
|
||||
|
||||
|
||||
|
||||
/// Abstract video encoder class
|
||||
class VideoEncoder
|
||||
{
|
||||
protected:
|
||||
// Video output file path
|
||||
String mPath;
|
||||
|
||||
// Video framerate
|
||||
F32 mFramerate;
|
||||
|
||||
// Video resolution
|
||||
Point2I mResolution;
|
||||
|
||||
// List with bitmaps which are done encoding
|
||||
ThreadSafeDeque< GBitmap* > mProcessedBitmaps;
|
||||
public:
|
||||
// Stores an encoded bitmap to be dealt with later
|
||||
void pushProcessedBitmap( GBitmap* bitmap );
|
||||
|
||||
public:
|
||||
/// Sets the file the encoder will write to
|
||||
void setFile( const char* path );
|
||||
|
||||
/// Sets the framerate (and fixes it if its invalid)
|
||||
virtual void setFramerate( F32* framerate ) { mFramerate = *framerate; }
|
||||
|
||||
/// Sets the output resolution (and fixes it if its invalid)
|
||||
virtual void setResolution( Point2I* resolution ) { mResolution = *resolution; }
|
||||
|
||||
/// Begins accepting frames for encoding
|
||||
virtual bool begin() = 0;
|
||||
|
||||
/// Pushes a new frame into the video stream
|
||||
virtual bool pushFrame( GBitmap * bitmap ) = 0;
|
||||
|
||||
/// Finishes the encoding and closes the video
|
||||
virtual bool end() = 0;
|
||||
|
||||
/// Returns an already encoded bitmap. Video capture will get these and manage their deletion
|
||||
GBitmap* getProcessedBitmap();
|
||||
};
|
||||
|
||||
/// Returns the VideoCapture singleton.
|
||||
#define VIDCAP ManagedSingleton<VideoCapture>::instance()
|
||||
|
||||
//-----------------------------------------
|
||||
/// VIDEO ENCODER REGISTRATION MACRO
|
||||
#define REGISTER_VIDEO_ENCODER(ClassName, EncoderName) \
|
||||
VideoEncoder* EncoderFactory##EncoderName() { return new ClassName(); } \
|
||||
struct __VidEncReg##EncoderName { __VidEncReg##EncoderName() { VideoCapture::registerEncoder( #EncoderName, &EncoderFactory##EncoderName ); } }; \
|
||||
static __VidEncReg##EncoderName _gEncoderRegistration;
|
||||
|
||||
#endif // !_VIDEOCAPTURE_H_
|
||||
76
Engine/source/gfx/video/videoEncoderPNG.cpp
Normal file
76
Engine/source/gfx/video/videoEncoderPNG.cpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) 2012 GarageGames, LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include "videoCapture.h"
|
||||
#include "core/stream/fileStream.h"
|
||||
#include "console/console.h"
|
||||
#include "gfx/bitmap/gBitmap.h"
|
||||
|
||||
/// This is a very basic "encoder" that records the video as a series of numbered PNGs
|
||||
/// Good if you're having problems with container-based formats and need extra flexibility.
|
||||
class VideoEncoderPNG : public VideoEncoder
|
||||
{
|
||||
U32 mCurrentFrame;
|
||||
|
||||
public:
|
||||
/// Begins accepting frames for encoding
|
||||
bool begin()
|
||||
{
|
||||
mPath += "\\";
|
||||
mCurrentFrame = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Pushes a new frame into the video stream
|
||||
bool pushFrame( GBitmap * bitmap )
|
||||
{
|
||||
FileStream fs;
|
||||
String framePath = mPath + String::ToString("%.6u.png", mCurrentFrame);
|
||||
if ( !fs.open( framePath, Torque::FS::File::Write ) )
|
||||
{
|
||||
Con::errorf( "VideoEncoderPNG::pushFrame() - Failed to open output file '%s'!", framePath.c_str() );
|
||||
return false;
|
||||
}
|
||||
|
||||
//Increment
|
||||
mCurrentFrame++;
|
||||
|
||||
bool result = bitmap->writeBitmap("png", fs, 0);
|
||||
pushProcessedBitmap(bitmap);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Finishes the encoding and closes the video
|
||||
bool end()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void setResolution( Point2I* resolution )
|
||||
{
|
||||
mResolution = *resolution;
|
||||
}
|
||||
};
|
||||
|
||||
REGISTER_VIDEO_ENCODER(VideoEncoderPNG, PNG)
|
||||
461
Engine/source/gfx/video/videoEncoderTheora.cpp
Normal file
461
Engine/source/gfx/video/videoEncoderTheora.cpp
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) 2012 GarageGames, LLC
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including without limitation the
|
||||
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||
// sell copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||
// IN THE SOFTWARE.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifdef TORQUE_OGGTHEORA
|
||||
|
||||
#include "videoCapture.h"
|
||||
#include "core/stream/fileStream.h"
|
||||
#include "console/console.h"
|
||||
#include "gfx/bitmap/gBitmap.h"
|
||||
#include "gfx/bitmap/bitmapUtils.h"
|
||||
|
||||
#include "platform/threads/thread.h"
|
||||
#include "platform/threads/threadSafeDeque.h"
|
||||
#include "platform/threads/semaphore.h"
|
||||
|
||||
#include "theora/theoraenc.h"
|
||||
#include "vorbis/codec.h"
|
||||
#include "vorbis/vorbisenc.h"
|
||||
|
||||
#include "platform/profiler.h"
|
||||
|
||||
//#define THEORA_ENCODER_SINGLE_THREAD
|
||||
|
||||
// These are the coefficient lookup tables, so we don't need to multiply
|
||||
dALIGN( static S32 sYRGB[ 256 ][ 4 ] );
|
||||
dALIGN( static S32 sURGB[ 256 ][ 4 ] );
|
||||
dALIGN( static S32 sVRGB[ 256 ][ 4 ] );
|
||||
|
||||
/// Initialize the lookup tables used in the RGB-YUV transcoding
|
||||
static void initLookupTables()
|
||||
{
|
||||
static bool sGenerated = false;
|
||||
if( !sGenerated )
|
||||
{
|
||||
for( S32 i = 0; i < 256; ++ i )
|
||||
{
|
||||
//Y = ( ( 66 * R + 129 * G + 25 * B + 128) >> 8) + 16
|
||||
//U = ( ( -38 * R - 74 * G + 112 * B + 128) >> 8) + 128
|
||||
//V = ( ( 112 * R - 94 * G - 18 * B + 128) >> 8) + 128
|
||||
|
||||
// Y coefficients
|
||||
sYRGB[ i ][ 0 ] = 66 * i;
|
||||
sYRGB[ i ][ 1 ] = 129 * i;
|
||||
sYRGB[ i ][ 2 ] = 25 * i + 128 + (16 << 8);
|
||||
|
||||
// U coefficients
|
||||
sURGB[ i ][ 0 ] = -38 * i;
|
||||
sURGB[ i ][ 1 ] = -74 * i;
|
||||
sURGB[ i ][ 2 ] = 112 * i + 128 + (128 << 8);
|
||||
|
||||
// V coefficients
|
||||
sVRGB[ i ][ 0 ] = 112 * i;
|
||||
sVRGB[ i ][ 1 ] = -94 * i;
|
||||
sVRGB[ i ][ 2 ] = -18 * i + 128 + (128 << 8);
|
||||
}
|
||||
sGenerated = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Theora video capture encoder
|
||||
class VideoEncoderTheora : public VideoEncoder, public Thread
|
||||
{
|
||||
U32 mCurrentFrame;
|
||||
|
||||
ogg_stream_state to; // take physical pages, weld into a logical stream of packets
|
||||
th_enc_ctx *td; // Theora encoder context
|
||||
th_info ti; // Theora info structure
|
||||
th_comment tc; // Theora comment structure
|
||||
|
||||
FileStream mFile; // Output file
|
||||
|
||||
th_ycbcr_buffer mBuffer; // YCbCr buffer
|
||||
|
||||
GBitmap* mLastFrame;
|
||||
|
||||
ThreadSafeDeque< GBitmap* > mFrameBitmapList; // List with unprocessed frame bitmaps
|
||||
Semaphore mSemaphore; //Semaphore for preventing the encoder from being lagged behind the game
|
||||
|
||||
bool mErrorStatus; //Status flag, true if OK, false if an error ocurred
|
||||
|
||||
/// Sets our error status
|
||||
bool setStatus(bool status)
|
||||
{
|
||||
mErrorStatus = status;
|
||||
return mErrorStatus;
|
||||
}
|
||||
bool getStatus() { return mErrorStatus; }
|
||||
|
||||
/// Encodes one frame
|
||||
void encodeFrame( GBitmap* bitmap, bool isLast=false )
|
||||
{
|
||||
PROFILE_SCOPE(Theora_encodeFrame);
|
||||
|
||||
//Copy bitmap to YUV buffer
|
||||
copyBitmapToYUV420(bitmap);
|
||||
|
||||
PROFILE_START(th_encode_ycbcr_in);
|
||||
//Submit frame for encoding
|
||||
if (th_encode_ycbcr_in(td, mBuffer))
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::encodeFrame() - The buffer size does not match the frame size the encoder was initialized with, or encoding has already completed. .");
|
||||
setStatus(false);
|
||||
PROFILE_END();
|
||||
return;
|
||||
}
|
||||
PROFILE_END();
|
||||
|
||||
//Fetch the encoded packets
|
||||
ogg_packet packet;
|
||||
if (!th_encode_packetout(td, isLast, &packet))
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::encodeFrame() - Internal Theora library error.");
|
||||
setStatus(false);
|
||||
return;
|
||||
}
|
||||
|
||||
//Insert packet into vorbis stream page
|
||||
ogg_stream_packetin(&to,&packet);
|
||||
|
||||
//Increment
|
||||
mCurrentFrame++;
|
||||
|
||||
//Is there a video page flushed?
|
||||
ogg_page videopage;
|
||||
while (ogg_stream_pageout(&to,&videopage))
|
||||
{
|
||||
//Write the video page to disk
|
||||
mFile.write(videopage.header_len, videopage.header);
|
||||
mFile.write(videopage.body_len, videopage.body);
|
||||
|
||||
F64 videotime = th_granule_time(td,ogg_page_granulepos(&videopage));
|
||||
if (videotime > 0)
|
||||
{
|
||||
int hundredths=(int)(videotime*100-(long)videotime*100);
|
||||
int seconds=(long)videotime%60;
|
||||
Platform::outputDebugString("Encoding time %g %02i.%02i", videotime, seconds, hundredths);
|
||||
}
|
||||
}
|
||||
|
||||
mSemaphore.release();
|
||||
}
|
||||
|
||||
bool process(bool ending)
|
||||
{
|
||||
if (!getStatus())
|
||||
return false;
|
||||
|
||||
//Try getting a bitmap for encoding
|
||||
GBitmap* bitmap = NULL;
|
||||
if (mFrameBitmapList.tryPopFront(bitmap))
|
||||
{
|
||||
encodeFrame(bitmap, false);
|
||||
}
|
||||
|
||||
//Delete previous bitmap
|
||||
if (!ending && bitmap)
|
||||
{
|
||||
if (mLastFrame)
|
||||
pushProcessedBitmap(mLastFrame);
|
||||
mLastFrame = bitmap;
|
||||
}
|
||||
|
||||
//If we're stopping encoding, but didn't have a frame, re-encode the last frame
|
||||
if (ending && !bitmap && mLastFrame)
|
||||
{
|
||||
encodeFrame(mLastFrame, true);
|
||||
pushProcessedBitmap(mLastFrame);
|
||||
mLastFrame = NULL;
|
||||
}
|
||||
|
||||
// We'll live while we have a last frame
|
||||
return (mLastFrame != NULL);
|
||||
}
|
||||
|
||||
public:
|
||||
VideoEncoderTheora() :
|
||||
mLastFrame(NULL)
|
||||
{
|
||||
setStatus(false);
|
||||
}
|
||||
|
||||
virtual void run( void* arg )
|
||||
{
|
||||
_setName( "TheoraEncoderThread" );
|
||||
while (!checkForStop())
|
||||
process(false);
|
||||
|
||||
// Encode all pending frames and close the last one
|
||||
while (process(true));
|
||||
}
|
||||
|
||||
/// Begins accepting frames for encoding
|
||||
bool begin()
|
||||
{
|
||||
mPath += ".ogv";
|
||||
mCurrentFrame = 0;
|
||||
|
||||
//Try opening the file for writing
|
||||
if ( !mFile.open( mPath, Torque::FS::File::Write ) )
|
||||
{
|
||||
Platform::outputDebugString( "VideoEncoderTheora::begin() - Failed to open output file '%s'!", mPath.c_str() );
|
||||
return setStatus(false);
|
||||
}
|
||||
|
||||
ogg_stream_init(&to, Platform::getRandom() * S32_MAX);
|
||||
|
||||
ogg_packet op;
|
||||
|
||||
th_info_init(&ti);
|
||||
ti.frame_width = mResolution.x;
|
||||
ti.frame_height = mResolution.y;
|
||||
ti.pic_width = mResolution.x;
|
||||
ti.pic_height = mResolution.y;
|
||||
ti.pic_x = 0;
|
||||
ti.pic_y = 0;
|
||||
|
||||
ti.fps_numerator = (int)(mFramerate * 1000.0f);
|
||||
ti.fps_denominator = 1000;
|
||||
|
||||
ti.aspect_numerator = 0;
|
||||
ti.aspect_denominator = 0;
|
||||
ti.colorspace = TH_CS_UNSPECIFIED;
|
||||
|
||||
ti.target_bitrate = 0;
|
||||
ti.quality = 63;
|
||||
ti.pixel_fmt = TH_PF_420;
|
||||
|
||||
td = th_encode_alloc(&ti);
|
||||
if (td == NULL)
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::begin() - Theora initialization error.");
|
||||
return setStatus(false);
|
||||
}
|
||||
|
||||
th_info_clear(&ti);
|
||||
|
||||
// This is needed for youtube compatibility
|
||||
int vp3_compatible = 1;
|
||||
th_encode_ctl(td, TH_ENCCTL_SET_VP3_COMPATIBLE, &vp3_compatible, sizeof(vp3_compatible));
|
||||
|
||||
// Set the encoder to max speed
|
||||
int speed_max;
|
||||
int ret;
|
||||
ret = th_encode_ctl(td, TH_ENCCTL_GET_SPLEVEL_MAX, &speed_max, sizeof(speed_max));
|
||||
if(ret<0){
|
||||
Platform::outputDebugString("VideoEncoderTheora::begin() - could not determine maximum speed level.");
|
||||
speed_max = 0;
|
||||
}
|
||||
ret = th_encode_ctl(td, TH_ENCCTL_SET_SPLEVEL, &speed_max, sizeof(speed_max));
|
||||
|
||||
// write the bitstream header packets with proper page interleave
|
||||
th_comment_init(&tc);
|
||||
|
||||
// first packet will get its own page automatically
|
||||
if(th_encode_flushheader(td,&tc,&op) <= 0)
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::begin() - Internal Theora library error.");
|
||||
return setStatus(false);
|
||||
}
|
||||
|
||||
ogg_page og;
|
||||
ogg_stream_packetin(&to,&op);
|
||||
if(ogg_stream_pageout(&to,&og) != 1)
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::begin() - Internal Ogg library error.");
|
||||
return setStatus(false);
|
||||
}
|
||||
mFile.write(og.header_len, og.header);
|
||||
mFile.write(og.body_len, og.body);
|
||||
|
||||
// create the remaining theora headers
|
||||
while((ret = th_encode_flushheader(td,&tc,&op)) != 0)
|
||||
{
|
||||
if(ret < 0)
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::begin() - Internal Theora library error.");
|
||||
return setStatus(false);
|
||||
}
|
||||
ogg_stream_packetin(&to,&op);
|
||||
}
|
||||
|
||||
// Flush the rest of our headers. This ensures
|
||||
// the actual data in each stream will start
|
||||
// on a new page, as per spec.
|
||||
while((ret = ogg_stream_flush(&to,&og)) != 0)
|
||||
{
|
||||
if(ret < 0)
|
||||
{
|
||||
Platform::outputDebugString("VideoEncoderTheora::begin() - Internal Ogg library error.");
|
||||
return setStatus(false);
|
||||
}
|
||||
mFile.write(og.header_len, og.header);
|
||||
mFile.write(og.body_len, og.body);
|
||||
}
|
||||
|
||||
//Initialize the YUV buffer
|
||||
S32 decimation[] = {0,1,1};
|
||||
for (U32 i=0; i<3; i++)
|
||||
{
|
||||
mBuffer[i].width = mResolution.x >> decimation[i];
|
||||
mBuffer[i].height = mResolution.y >> decimation[i];
|
||||
mBuffer[i].stride = mBuffer[i].width * sizeof(U8);
|
||||
mBuffer[i].data = new U8[mBuffer[i].width*mBuffer[i].height*sizeof(U8)];
|
||||
}
|
||||
|
||||
//Initialize the YUV coefficient lookup tables
|
||||
initLookupTables();
|
||||
|
||||
setStatus(true);
|
||||
|
||||
#ifndef THEORA_ENCODER_SINGLE_THREAD
|
||||
start();
|
||||
#endif
|
||||
|
||||
return getStatus();
|
||||
}
|
||||
|
||||
/// Pushes a new frame into the video stream
|
||||
bool pushFrame( GBitmap * bitmap )
|
||||
{
|
||||
|
||||
// Push the bitmap into the frame list
|
||||
mFrameBitmapList.pushBack( bitmap );
|
||||
|
||||
mSemaphore.acquire();
|
||||
|
||||
#ifdef THEORA_ENCODER_SINGLE_THREAD
|
||||
process(false);
|
||||
#endif
|
||||
|
||||
return getStatus();
|
||||
}
|
||||
|
||||
/// Finishes the encoding and closes the video
|
||||
bool end()
|
||||
{
|
||||
//Let's wait the thread stop doing whatever it needs to do
|
||||
stop();
|
||||
join();
|
||||
|
||||
#ifdef THEORA_ENCODER_SINGLE_THREAD
|
||||
while (process(true));
|
||||
#endif
|
||||
|
||||
th_encode_free(td);
|
||||
ogg_stream_clear(&to);
|
||||
th_comment_clear(&tc);
|
||||
|
||||
mFile.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void setResolution( Point2I* resolution )
|
||||
{
|
||||
/* Theora has a divisible-by-sixteen restriction for the encoded frame size */
|
||||
/* scale the picture size up to the nearest /16 and calculate offsets */
|
||||
resolution->x = (resolution->x) + 15 & ~0xF;
|
||||
resolution->y = (resolution->y) + 15 & ~0xF;
|
||||
|
||||
mResolution = *resolution;
|
||||
}
|
||||
|
||||
/// Converts the bitmap to YUV420 and copies it into our internal buffer
|
||||
void copyBitmapToYUV420( GBitmap* bitmap )
|
||||
{
|
||||
PROFILE_SCOPE(copyBitmapToYUV420);
|
||||
|
||||
// Convert luma
|
||||
const U8* rgb = bitmap->getBits();
|
||||
|
||||
// Chroma planes are half width and half height
|
||||
U32 w = mResolution.x / 2;
|
||||
U32 h = mResolution.y / 2;
|
||||
|
||||
// We'll update two luminance rows at once
|
||||
U8* yuv_y0 = mBuffer[0].data;
|
||||
U8* yuv_y1 = mBuffer[0].data + mBuffer[0].stride;
|
||||
|
||||
// Get pointers to chroma planes
|
||||
U8* yuv_u = mBuffer[1].data;
|
||||
U8* yuv_v = mBuffer[2].data;
|
||||
|
||||
// We'll also need to read two RGB rows at once
|
||||
U32 rgbStride = mResolution.x * bitmap->getBytesPerPixel();
|
||||
const U8* row0 = rgb;
|
||||
const U8* row1 = row0 + rgbStride;
|
||||
|
||||
for(U32 y = 0; y < h; y++)
|
||||
{
|
||||
for(U32 x = 0; x < w; x++)
|
||||
{
|
||||
// Fetch two RGB samples from each RGB row (for downsampling the chroma)
|
||||
U8 r0 = *row0++;
|
||||
U8 g0 = *row0++;
|
||||
U8 b0 = *row0++;
|
||||
U8 r1 = *row0++;
|
||||
U8 g1 = *row0++;
|
||||
U8 b1 = *row0++;
|
||||
U8 r2 = *row1++;
|
||||
U8 g2 = *row1++;
|
||||
U8 b2 = *row1++;
|
||||
U8 r3 = *row1++;
|
||||
U8 g3 = *row1++;
|
||||
U8 b3 = *row1++;
|
||||
|
||||
// Convert the four RGB samples into four luminance samples
|
||||
*yuv_y0 = ( (sYRGB[r0][0] + sYRGB[g0][1] + sYRGB[b0][2]) >> 8);
|
||||
yuv_y0++;
|
||||
*yuv_y0 = ( (sYRGB[r1][0] + sYRGB[g1][1] + sYRGB[b1][2]) >> 8);
|
||||
yuv_y0++;
|
||||
*yuv_y1 = ( (sYRGB[r2][0] + sYRGB[g2][1] + sYRGB[b2][2]) >> 8);
|
||||
yuv_y1++;
|
||||
*yuv_y1 = ( (sYRGB[r3][0] + sYRGB[g3][1] + sYRGB[b3][2]) >> 8);
|
||||
yuv_y1++;
|
||||
|
||||
// Downsample the four RGB samples
|
||||
U8 r = (r0 + r1 + r2 + r3) >> 2;
|
||||
U8 g = (g0 + g1 + g2 + g3) >> 2;
|
||||
U8 b = (b0 + b1 + b2 + b3) >> 2;
|
||||
|
||||
// Convert downsampled RGB into chroma
|
||||
*yuv_u = ( (sURGB[r][0] + sURGB[g][1] + sURGB[b][2]) >> 8);
|
||||
*yuv_v = ( (sVRGB[r][0] + sVRGB[g][1] + sVRGB[b][2]) >> 8);
|
||||
yuv_u++;
|
||||
yuv_v++;
|
||||
}
|
||||
|
||||
//Next RGB rows
|
||||
row0 += rgbStride;
|
||||
row1 += rgbStride;
|
||||
|
||||
//Next luminance rows
|
||||
yuv_y0 += mBuffer[0].stride;
|
||||
yuv_y1 += mBuffer[0].stride;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
REGISTER_VIDEO_ENCODER(VideoEncoderTheora, THEORA)
|
||||
|
||||
#endif
|
||||
Loading…
Add table
Add a link
Reference in a new issue