Engine directory for ticket #1

This commit is contained in:
DavidWyand-GG 2012-09-19 11:15:01 -04:00
parent 352279af7a
commit 7dbfe6994d
3795 changed files with 1363358 additions and 0 deletions

View 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

View 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_

View 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 );
}

View 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_

View 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)

View 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