Merge branch 'dev' of https://github.com/DIP-SEM/OpenBoard into dev
commit
ab4ae9435a
@ -1,656 +0,0 @@ |
||||
/*
|
||||
* Copyright (C) 2013 Open Education Foundation |
||||
* |
||||
* Copyright (C) 2010-2013 Groupement d'Intérêt Public pour |
||||
* l'Education Numérique en Afrique (GIP ENA) |
||||
* |
||||
* This file is part of OpenBoard. |
||||
* |
||||
* OpenBoard is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, |
||||
* with a specific linking exception for the OpenSSL project's |
||||
* "OpenSSL" library (or with modified versions of it that use the |
||||
* same license as the "OpenSSL" library). |
||||
* |
||||
* OpenBoard is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with OpenBoard. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
|
||||
|
||||
|
||||
|
||||
#include "UBQuickTimeFile.h" |
||||
|
||||
#include <AudioToolbox/AudioToolbox.h> |
||||
|
||||
#include "UBAudioQueueRecorder.h" |
||||
#include <QtGui> |
||||
|
||||
#include "core/memcheck.h" |
||||
|
||||
QQueue<UBQuickTimeFile::VideoFrame> UBQuickTimeFile::frameQueue; |
||||
QMutex UBQuickTimeFile::frameQueueMutex; |
||||
QWaitCondition UBQuickTimeFile::frameBufferNotEmpty; |
||||
|
||||
|
||||
UBQuickTimeFile::UBQuickTimeFile(QObject * pParent) |
||||
: QThread(pParent) |
||||
, mVideoCompressionSession(0) |
||||
, mVideoMedia(0) |
||||
, mSoundMedia(0) |
||||
, mVideoOutputTrack(0) |
||||
, mSoundOutputTrack(0) |
||||
, mCVPixelBufferPool(0) |
||||
, mOutputMovie(0) |
||||
, mFramesPerSecond(-1) |
||||
, mTimeScale(100) |
||||
, mRecordAudio(true) |
||||
, mWaveRecorder(0) |
||||
, mSouldStopCompression(false) |
||||
, mCompressionSessionRunning(false) |
||||
, mPendingFrames(0) |
||||
{ |
||||
// NOOP
|
||||
} |
||||
|
||||
|
||||
bool UBQuickTimeFile::init(const QString& pVideoFileName, const QString& pProfileData, int pFramesPerSecond |
||||
, const QSize& pFrameSize, bool pRecordAudio, const QString& audioRecordingDevice) |
||||
{ |
||||
mFrameSize = pFrameSize; |
||||
mFramesPerSecond = pFramesPerSecond; |
||||
mVideoFileName = pVideoFileName; |
||||
mRecordAudio = pRecordAudio && QSysInfo::MacintoshVersion >= QSysInfo::MV_10_5; // Audio Queue are available in 10.5 +;
|
||||
|
||||
if (mRecordAudio) |
||||
mAudioRecordingDeviceName = audioRecordingDevice; |
||||
else |
||||
mAudioRecordingDeviceName = ""; |
||||
|
||||
if (pProfileData.toLower() == "lossless") |
||||
mSpatialQuality = codecLosslessQuality; |
||||
if (pProfileData.toLower() == "high") |
||||
mSpatialQuality = codecHighQuality; |
||||
else if (pProfileData.toLower() == "normal") |
||||
mSpatialQuality = codecNormalQuality; |
||||
else if (pProfileData.toLower() == "low") |
||||
mSpatialQuality = codecLowQuality; |
||||
else |
||||
mSpatialQuality = codecHighQuality; |
||||
|
||||
qDebug() << "Quality " << pProfileData << mSpatialQuality; |
||||
|
||||
return true; |
||||
|
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::run() |
||||
{ |
||||
EnterMoviesOnThread(kCSAcceptThreadSafeComponentsOnlyMode); |
||||
|
||||
mSouldStopCompression = false; |
||||
mPendingFrames = 0; |
||||
|
||||
createCompressionSession(); |
||||
|
||||
mCompressionSessionRunning = true; |
||||
emit compressionSessionStarted(); |
||||
|
||||
while(!mSouldStopCompression) |
||||
{ |
||||
frameQueueMutex.lock(); |
||||
//qDebug() << "run .... wait" << QTime::currentTime();
|
||||
|
||||
frameBufferNotEmpty.wait(&UBQuickTimeFile::frameQueueMutex); |
||||
|
||||
//qDebug() << "awakend ..." << QTime::currentTime();
|
||||
if (!frameQueue.isEmpty()) |
||||
{ |
||||
QQueue<VideoFrame> localQueue = frameQueue; |
||||
frameQueue.clear(); |
||||
|
||||
frameQueueMutex.unlock(); |
||||
|
||||
while (!localQueue.isEmpty()) |
||||
{ |
||||
VideoFrame frame = localQueue.dequeue(); |
||||
appendVideoFrame(frame.buffer, frame.timestamp); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
frameQueueMutex.unlock(); |
||||
} |
||||
} |
||||
|
||||
flushPendingFrames(); |
||||
} |
||||
|
||||
|
||||
bool UBQuickTimeFile::createCompressionSession() |
||||
{ |
||||
CodecType codecType = kH264CodecType; |
||||
|
||||
CFStringRef keys[] = {kCVPixelBufferPixelFormatTypeKey, kCVPixelBufferWidthKey, kCVPixelBufferHeightKey}; |
||||
|
||||
int width = mFrameSize.width(); |
||||
int height = mFrameSize.height(); |
||||
int pixelFormat = k32BGRAPixelFormat; |
||||
|
||||
CFTypeRef values[] = |
||||
{ |
||||
(CFTypeRef)CFNumberCreate(0, kCFNumberIntType, (void*)&pixelFormat), |
||||
(CFTypeRef)CFNumberCreate(0, kCFNumberIntType, (void*)&width), |
||||
(CFTypeRef)CFNumberCreate(0, kCFNumberIntType, (void*)&height) |
||||
}; |
||||
|
||||
CFDictionaryRef pixelBufferAttributes = CFDictionaryCreate(kCFAllocatorDefault |
||||
, (const void **)keys, (const void **)values, 3, 0, 0); |
||||
|
||||
if(!pixelBufferAttributes) |
||||
{ |
||||
setLastErrorMessage("Could not create CV buffer pool pixel buffer attributes"); |
||||
return false; |
||||
} |
||||
|
||||
OSStatus err = noErr; |
||||
ICMEncodedFrameOutputRecord encodedFrameOutputRecord = {NULL, NULL, NULL}; |
||||
ICMCompressionSessionOptionsRef sessionOptions = 0; |
||||
|
||||
err = ICMCompressionSessionOptionsCreate(0, &sessionOptions); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionOptionsCreate() failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
// We must set this flag to enable P or B frames.
|
||||
err = ICMCompressionSessionOptionsSetAllowTemporalCompression(sessionOptions, true); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionOptionsSetAllowTemporalCompression() failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
// We must set this flag to enable B frames.
|
||||
err = ICMCompressionSessionOptionsSetAllowFrameReordering(sessionOptions, true); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionOptionsSetAllowFrameReordering() failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
// Set the maximum key frame interval, also known as the key frame rate.
|
||||
err = ICMCompressionSessionOptionsSetMaxKeyFrameInterval(sessionOptions, mFramesPerSecond); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionOptionsSetMaxKeyFrameInterval() failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
// This allows the compressor more flexibility (ie, dropping and coalescing frames).
|
||||
err = ICMCompressionSessionOptionsSetAllowFrameTimeChanges(sessionOptions, true); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionOptionsSetAllowFrameTimeChanges() failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
// Set the average quality.
|
||||
err = ICMCompressionSessionOptionsSetProperty(sessionOptions, |
||||
kQTPropertyClass_ICMCompressionSessionOptions, |
||||
kICMCompressionSessionOptionsPropertyID_Quality, |
||||
sizeof(mSpatialQuality), |
||||
&mSpatialQuality); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionOptionsSetProperty(Quality) failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
//qDebug() << "available quality" << mSpatialQuality;
|
||||
|
||||
encodedFrameOutputRecord.encodedFrameOutputCallback = addEncodedFrameToMovie; |
||||
encodedFrameOutputRecord.encodedFrameOutputRefCon = this; |
||||
encodedFrameOutputRecord.frameDataAllocator = 0; |
||||
|
||||
err = ICMCompressionSessionCreate(0, mFrameSize.width(), mFrameSize.height(), codecType, mTimeScale, |
||||
sessionOptions, pixelBufferAttributes, &encodedFrameOutputRecord, &mVideoCompressionSession); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionCreate() failed %1").arg(err)); |
||||
goto bail; |
||||
} |
||||
|
||||
mCVPixelBufferPool = ICMCompressionSessionGetPixelBufferPool(mVideoCompressionSession); |
||||
|
||||
if(!mCVPixelBufferPool) |
||||
{ |
||||
setLastErrorMessage("ICMCompressionSessionGetPixelBufferPool() failed."); |
||||
err = !noErr; |
||||
goto bail; |
||||
} |
||||
|
||||
if(mRecordAudio) |
||||
{ |
||||
mWaveRecorder = new UBAudioQueueRecorder(); |
||||
|
||||
if(mWaveRecorder->init(mAudioRecordingDeviceName)) |
||||
{ |
||||
connect(mWaveRecorder, SIGNAL(newWaveBuffer(void*, long, int , const AudioStreamPacketDescription*)) |
||||
, this, SLOT(appendAudioBuffer(void*, long, int, const AudioStreamPacketDescription*))); |
||||
|
||||
connect(mWaveRecorder, SIGNAL(audioLevelChanged(quint8)), this, SIGNAL(audioLevelChanged(quint8))); |
||||
} |
||||
else |
||||
{ |
||||
setLastErrorMessage(mWaveRecorder->lastErrorMessage()); |
||||
mWaveRecorder->deleteLater(); |
||||
} |
||||
} |
||||
|
||||
createMovie(); |
||||
|
||||
bail: |
||||
ICMCompressionSessionOptionsRelease(sessionOptions); |
||||
sessionOptions = 0; |
||||
|
||||
CFRelease(pixelBufferAttributes); |
||||
|
||||
return err == noErr; |
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::stop() |
||||
{ |
||||
mSouldStopCompression = true; |
||||
} |
||||
|
||||
bool UBQuickTimeFile::flushPendingFrames() |
||||
{ |
||||
mCompressionSessionRunning = false; |
||||
|
||||
if (mWaveRecorder) |
||||
{ |
||||
mWaveRecorder->close(); |
||||
mWaveRecorder->deleteLater(); |
||||
} |
||||
|
||||
//Flush pending frames in compression session
|
||||
OSStatus err = ICMCompressionSessionCompleteFrames(mVideoCompressionSession, true, 0, 0); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("ICMCompressionSessionCompleteFrames() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
|
||||
bool UBQuickTimeFile::closeCompressionSession() |
||||
{ |
||||
OSStatus err = noErr; |
||||
|
||||
if (mVideoMedia) |
||||
{ |
||||
// End the media sample-adding session.
|
||||
err = EndMediaEdits(mVideoMedia); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("EndMediaEdits(mVideoMedia) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
// Make sure things are extra neat.
|
||||
ExtendMediaDecodeDurationToDisplayEndTime(mVideoMedia, 0); |
||||
|
||||
// Insert the stuff we added into the track, at the end.
|
||||
Track videoTrack = GetMediaTrack(mVideoMedia); |
||||
|
||||
err = InsertMediaIntoTrack(videoTrack, |
||||
GetTrackDuration(videoTrack), |
||||
0, GetMediaDisplayDuration(mVideoMedia), |
||||
fixed1); |
||||
mVideoMedia = 0; |
||||
|
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("InsertMediaIntoTrack() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
if (mSoundMedia) |
||||
{ |
||||
err = EndMediaEdits(mSoundMedia); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("EndMediaEdits(mAudioMedia) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
Track soundTrack = GetMediaTrack(mSoundMedia); |
||||
|
||||
err = InsertMediaIntoTrack(soundTrack, |
||||
GetTrackDuration(soundTrack), |
||||
0, GetMediaDisplayDuration(mSoundMedia), |
||||
fixed1); |
||||
|
||||
mSoundMedia = 0; |
||||
|
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("InsertMediaIntoTrack(mAudioMedia) failed %1").arg(err)); |
||||
} |
||||
|
||||
TimeValue soundTrackDuration = GetTrackDuration(soundTrack); |
||||
TimeValue videoTrackDuration = GetTrackDuration(videoTrack); |
||||
|
||||
if (soundTrackDuration > videoTrackDuration) |
||||
{ |
||||
qDebug() << "Sound track is longer then video track" << soundTrackDuration << ">" << videoTrackDuration; |
||||
DeleteTrackSegment(soundTrack, videoTrackDuration, soundTrackDuration - videoTrackDuration); |
||||
} |
||||
|
||||
DisposeHandle((Handle)mSoundDescription); |
||||
} |
||||
} |
||||
|
||||
// Write the movie header to the file.
|
||||
err = AddMovieToStorage(mOutputMovie, mOutputMovieDataHandler); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("AddMovieToStorage() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
err = UpdateMovieInStorage(mOutputMovie, mOutputMovieDataHandler); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("UpdateMovieInStorage() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
err = CloseMovieStorage(mOutputMovieDataHandler); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("CloseMovieStorage() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
CVPixelBufferPoolRelease(mCVPixelBufferPool); |
||||
mCVPixelBufferPool = 0; |
||||
|
||||
mOutputMovie = 0; |
||||
mOutputMovieDataHandler = 0; |
||||
mVideoCompressionSession = 0; |
||||
|
||||
ExitMoviesOnThread(); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
|
||||
OSStatus UBQuickTimeFile::addEncodedFrameToMovie(void *encodedFrameOutputRefCon, |
||||
ICMCompressionSessionRef session, |
||||
OSStatus err, |
||||
ICMEncodedFrameRef encodedFrame, |
||||
void *reserved) |
||||
{ |
||||
Q_UNUSED(session); |
||||
Q_UNUSED(reserved); |
||||
|
||||
UBQuickTimeFile *quickTimeFile = (UBQuickTimeFile *)encodedFrameOutputRefCon; |
||||
|
||||
if(quickTimeFile) |
||||
quickTimeFile->addEncodedFrame(encodedFrame, err); |
||||
|
||||
return noErr; |
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::addEncodedFrame(ICMEncodedFrameRef encodedFrame, OSStatus frameErr) |
||||
{ |
||||
mPendingFrames--; |
||||
|
||||
//qDebug() << "addEncodedFrame" << mSouldStopCompression << mPendingFrames;
|
||||
|
||||
if(frameErr == noErr) |
||||
{ |
||||
if (mVideoMedia) |
||||
{ |
||||
OSStatus err = AddMediaSampleFromEncodedFrame(mVideoMedia, encodedFrame, 0); |
||||
|
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("AddMediaSampleFromEncodedFrame() failed %1").arg(err)); |
||||
} |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
setLastErrorMessage(QString("addEncodedFrame received an error %1").arg(frameErr)); |
||||
} |
||||
|
||||
if (mSouldStopCompression && mPendingFrames == 0) |
||||
{ |
||||
closeCompressionSession(); |
||||
} |
||||
} |
||||
|
||||
|
||||
bool UBQuickTimeFile::createMovie() |
||||
{ |
||||
if(!mOutputMovie) |
||||
{ |
||||
OSStatus err = noErr; |
||||
|
||||
Handle dataRef; |
||||
OSType dataRefType; |
||||
|
||||
CFStringRef filePath = CFStringCreateWithCString(0, mVideoFileName.toUtf8().constData(), kCFStringEncodingUTF8); |
||||
|
||||
QTNewDataReferenceFromFullPathCFString(filePath, kQTPOSIXPathStyle, 0, &dataRef, &dataRefType); |
||||
|
||||
err = CreateMovieStorage(dataRef, dataRefType, 'TVOD', 0, createMovieFileDeleteCurFile, &mOutputMovieDataHandler, &mOutputMovie); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("CreateMovieStorage() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
mVideoOutputTrack = NewMovieTrack(mOutputMovie, X2Fix(mFrameSize.width()), X2Fix(mFrameSize.height()), 0); |
||||
err = GetMoviesError(); |
||||
|
||||
if( err ) |
||||
{ |
||||
setLastErrorMessage(QString("NewMovieTrack(Video) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
if(!createVideoMedia()) |
||||
return false; |
||||
|
||||
if(mRecordAudio) |
||||
{ |
||||
mSoundOutputTrack = NewMovieTrack(mOutputMovie, 0, 0, kFullVolume); |
||||
err = GetMoviesError(); |
||||
|
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("NewMovieTrack(Sound) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
if(!createAudioMedia()) |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
|
||||
bool UBQuickTimeFile::createVideoMedia() |
||||
{ |
||||
mVideoMedia = NewTrackMedia(mVideoOutputTrack, VideoMediaType, mTimeScale, 0, 0); |
||||
OSStatus err = GetMoviesError(); |
||||
|
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("NewTrackMedia(VideoMediaType) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
err = BeginMediaEdits(mVideoMedia); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("BeginMediaEdits(VideoMediaType) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
|
||||
bool UBQuickTimeFile::createAudioMedia() |
||||
{ |
||||
if(mRecordAudio) |
||||
{ |
||||
mAudioDataFormat = UBAudioQueueRecorder::audioFormat(); |
||||
|
||||
mSoundMedia = NewTrackMedia(mSoundOutputTrack, SoundMediaType, mAudioDataFormat.mSampleRate, 0, 0); |
||||
OSStatus err = GetMoviesError(); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("NewTrackMedia(AudioMediaType) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
err = BeginMediaEdits(mSoundMedia); |
||||
if(err) |
||||
{ |
||||
setLastErrorMessage(QString("BeginMediaEdits(AudioMediaType) failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
err = QTSoundDescriptionCreate(&mAudioDataFormat, 0, 0, 0, 0, |
||||
kQTSoundDescriptionKind_Movie_LowestPossibleVersion, |
||||
&mSoundDescription); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("QTSoundDescriptionCreate() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
|
||||
err = QTSoundDescriptionGetProperty(mSoundDescription, kQTPropertyClass_SoundDescription, |
||||
kQTSoundDescriptionPropertyID_AudioStreamBasicDescription, |
||||
sizeof(mAudioDataFormat), &mAudioDataFormat, 0); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("QTSoundDescriptionGetProperty() failed %1").arg(err)); |
||||
return false; |
||||
} |
||||
|
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
|
||||
UBQuickTimeFile::~UBQuickTimeFile() |
||||
{ |
||||
// NOOP
|
||||
} |
||||
|
||||
|
||||
CVPixelBufferRef UBQuickTimeFile::newPixelBuffer() |
||||
{ |
||||
CVPixelBufferRef pixelBuffer = 0; |
||||
|
||||
if(CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, mCVPixelBufferPool, &pixelBuffer) != kCVReturnSuccess) |
||||
{ |
||||
setLastErrorMessage("Could not retreive CV buffer from pool"); |
||||
return 0; |
||||
} |
||||
|
||||
return pixelBuffer; |
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::appendVideoFrame(CVPixelBufferRef pixelBuffer, long msTimeStamp) |
||||
{ |
||||
TimeValue64 msTimeStampScaled = msTimeStamp * mTimeScale / 1000; |
||||
|
||||
/*
|
||||
{ |
||||
CVPixelBufferLockBaseAddress(pixelBuffer, 0) ; |
||||
void *pixelBufferAddress = CVPixelBufferGetBaseAddress(pixelBuffer); |
||||
qDebug() << "will comp newVideoFrame - PixelBuffer @" << pixelBufferAddress |
||||
<< QTime::currentTime().toString("ss:zzz") << QThread::currentThread(); |
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); |
||||
} |
||||
*/ |
||||
|
||||
OSStatus err = ICMCompressionSessionEncodeFrame(mVideoCompressionSession, pixelBuffer, |
||||
msTimeStampScaled, 0, kICMValidTime_DisplayTimeStampIsValid, |
||||
0, 0, 0); |
||||
|
||||
if (err == noErr) |
||||
{ |
||||
mPendingFrames++; |
||||
} |
||||
else |
||||
{ |
||||
setLastErrorMessage(QString("Could not encode frame %1").arg(err)); |
||||
} |
||||
|
||||
CVPixelBufferRelease(pixelBuffer); |
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::appendAudioBuffer(void* pBuffer, long pLength, int inNumberPacketDescriptions, const AudioStreamPacketDescription* inPacketDescs) |
||||
{ |
||||
Q_UNUSED(pLength); |
||||
//qDebug() << "appendAudioBuffer" << QThread::currentThread();
|
||||
|
||||
if(mRecordAudio) |
||||
{ |
||||
for (int i = 0; i < inNumberPacketDescriptions; i++) |
||||
{ |
||||
OSStatus err = AddMediaSample2(mSoundMedia, |
||||
(UInt8*)pBuffer + inPacketDescs[i].mStartOffset, |
||||
inPacketDescs[i].mDataByteSize, |
||||
mAudioDataFormat.mFramesPerPacket, |
||||
0, |
||||
(SampleDescriptionHandle)mSoundDescription, |
||||
1, |
||||
0, |
||||
0); |
||||
if (err) |
||||
{ |
||||
setLastErrorMessage(QString("AddMediaSample2(soundMedia) failed %1").arg(err)); |
||||
} |
||||
} |
||||
} |
||||
#ifdef Q_OS_OSX |
||||
free((void*)inPacketDescs); |
||||
#endif |
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::setLastErrorMessage(const QString& error) |
||||
{ |
||||
mLastErrorMessage = error; |
||||
qWarning() << "UBQuickTimeFile error" << error; |
||||
} |
||||
|
||||
|
@ -0,0 +1,262 @@ |
||||
/* |
||||
* Copyright (C) 2013 Open Education Foundation |
||||
* |
||||
* Copyright (C) 2010-2013 Groupement d'Intérêt Public pour |
||||
* l'Education Numérique en Afrique (GIP ENA) |
||||
* |
||||
* This file is part of OpenBoard. |
||||
* |
||||
* OpenBoard is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, version 3 of the License, |
||||
* with a specific linking exception for the OpenSSL project's |
||||
* "OpenSSL" library (or with modified versions of it that use the |
||||
* same license as the "OpenSSL" library). |
||||
* |
||||
* OpenBoard is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with OpenBoard. If not, see <http://www.gnu.org/licenses/>. |
||||
*/ |
||||
|
||||
|
||||
|
||||
|
||||
#include "UBQuickTimeFile.h" |
||||
|
||||
#include <AudioToolbox/AudioToolbox.h> |
||||
#import <AVFoundation/AVFoundation.h> |
||||
#import <Foundation/Foundation.h> |
||||
#import <CoreMedia/CoreMedia.h> |
||||
|
||||
#include "UBAudioQueueRecorder.h" |
||||
#include <QtGui> |
||||
|
||||
#include "core/memcheck.h" |
||||
|
||||
QQueue<UBQuickTimeFile::VideoFrame> UBQuickTimeFile::frameQueue; |
||||
QMutex UBQuickTimeFile::frameQueueMutex; |
||||
QWaitCondition UBQuickTimeFile::frameBufferNotEmpty; |
||||
|
||||
|
||||
UBQuickTimeFile::UBQuickTimeFile(QObject * pParent) |
||||
: QThread(pParent) |
||||
, mVideoWriter(0) |
||||
, mVideoWriterInput(0) |
||||
, mAdaptor(0) |
||||
, mCVPixelBufferPool(0) |
||||
, mFramesPerSecond(-1) |
||||
, mTimeScale(100) |
||||
, mRecordAudio(true) |
||||
, mShouldStopCompression(false) |
||||
, mCompressionSessionRunning(false) |
||||
, mPendingFrames(0) |
||||
{ |
||||
// NOOP |
||||
} |
||||
|
||||
|
||||
UBQuickTimeFile::~UBQuickTimeFile() |
||||
{ |
||||
// NOOP |
||||
} |
||||
|
||||
bool UBQuickTimeFile::init(const QString& pVideoFileName, const QString& pProfileData, int pFramesPerSecond |
||||
, const QSize& pFrameSize, bool pRecordAudio, const QString& audioRecordingDevice) |
||||
{ |
||||
mFrameSize = pFrameSize; |
||||
mFramesPerSecond = pFramesPerSecond; |
||||
mVideoFileName = pVideoFileName; |
||||
mRecordAudio = pRecordAudio; |
||||
mSpatialQuality = pProfileData; |
||||
|
||||
if (mRecordAudio) |
||||
mAudioRecordingDeviceName = audioRecordingDevice; |
||||
else |
||||
mAudioRecordingDeviceName = ""; |
||||
|
||||
|
||||
qDebug() << "UBQuickTimeFile created; video size: " << pFrameSize.width() << " x " << pFrameSize.height(); |
||||
|
||||
return true; |
||||
|
||||
} |
||||
|
||||
|
||||
void UBQuickTimeFile::run() |
||||
{ |
||||
mShouldStopCompression = false; |
||||
mPendingFrames = 0; |
||||
|
||||
if (!beginSession()) |
||||
return; |
||||
|
||||
mCompressionSessionRunning = true; |
||||
emit compressionSessionStarted(); |
||||
|
||||
do { |
||||
frameQueueMutex.lock(); |
||||
|
||||
frameBufferNotEmpty.wait(&UBQuickTimeFile::frameQueueMutex); |
||||
|
||||
if (!frameQueue.isEmpty()) { |
||||
QQueue<VideoFrame> localQueue = frameQueue; |
||||
frameQueue.clear(); |
||||
|
||||
frameQueueMutex.unlock(); |
||||
|
||||
while (!localQueue.isEmpty()) { |
||||
if ([mVideoWriterInput isReadyForMoreMediaData]) { |
||||
VideoFrame frame = localQueue.dequeue(); |
||||
appendVideoFrame(frame.buffer, frame.timestamp); |
||||
} |
||||
else |
||||
usleep(10000); |
||||
} |
||||
} |
||||
else |
||||
frameQueueMutex.unlock(); |
||||
} while(!mShouldStopCompression); |
||||
|
||||
endSession(); |
||||
|
||||
} |
||||
|
||||
/** |
||||
* \brief Initialize the AVAssetWriter, which handles writing the media to file |
||||
*/ |
||||
bool UBQuickTimeFile::beginSession() |
||||
{ |
||||
NSError *outError; |
||||
NSString * outputPath = [[NSString alloc] initWithUTF8String: mVideoFileName.toUtf8().data()]; |
||||
NSURL * outputUrl = [[NSURL alloc] initFileURLWithPath: outputPath]; |
||||
|
||||
if (!outputUrl) { |
||||
qDebug() << "Podcast video URL invalid; not recording"; |
||||
return false; |
||||
} |
||||
|
||||
// Create and check the assetWriter |
||||
mVideoWriter = [[AVAssetWriter assetWriterWithURL:outputUrl |
||||
fileType:AVFileTypeQuickTimeMovie |
||||
error:&outError] retain]; |
||||
NSCParameterAssert(mVideoWriter); |
||||
|
||||
mVideoWriter.movieTimeScale = mTimeScale; |
||||
|
||||
int frameWidth = mFrameSize.width(); |
||||
int frameHeight = mFrameSize.height(); |
||||
|
||||
// Create the input and check it |
||||
NSDictionary * videoSettings = [NSDictionary dictionaryWithObjectsAndKeys: |
||||
AVVideoCodecH264, AVVideoCodecKey, |
||||
[NSNumber numberWithInt:frameWidth], AVVideoWidthKey, |
||||
[NSNumber numberWithInt:frameHeight], AVVideoHeightKey, |
||||
nil]; |
||||
|
||||
|
||||
mVideoWriterInput = [[AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo |
||||
outputSettings:videoSettings] retain]; |
||||
NSCParameterAssert(mVideoWriterInput); |
||||
|
||||
|
||||
|
||||
// Pixel Buffer Adaptor. This makes it possible to pass CVPixelBuffers to the WriterInput |
||||
NSDictionary* pixelBufSettings = [NSDictionary dictionaryWithObjectsAndKeys: |
||||
[NSNumber numberWithInt:kCVPixelFormatType_32BGRA], kCVPixelBufferPixelFormatTypeKey, |
||||
[NSNumber numberWithInt: frameWidth], kCVPixelBufferWidthKey, |
||||
[NSNumber numberWithInt: frameHeight], kCVPixelBufferHeightKey, |
||||
nil]; |
||||
|
||||
mAdaptor = [[AVAssetWriterInputPixelBufferAdaptor |
||||
assetWriterInputPixelBufferAdaptorWithAssetWriterInput:mVideoWriterInput |
||||
sourcePixelBufferAttributes:pixelBufSettings] retain]; |
||||
|
||||
|
||||
|
||||
// Add the input(s) to the assetWriter |
||||
NSCParameterAssert([mVideoWriter canAddInput:mVideoWriterInput]); |
||||
[mVideoWriter addInput:mVideoWriterInput]; |
||||
|
||||
|
||||
// begin the writing session |
||||
bool canStartWriting = [mVideoWriter startWriting]; |
||||
[mVideoWriter startSessionAtSourceTime:CMTimeMake(0, mTimeScale)]; |
||||
|
||||
// return true if everything was created and started successfully |
||||
return (mVideoWriter != nil) && (mVideoWriterInput != nil) && canStartWriting; |
||||
} |
||||
|
||||
/** |
||||
* \brief Close the recording sesion and finish writing the video file |
||||
*/ |
||||
void UBQuickTimeFile::endSession() |
||||
{ |
||||
[mVideoWriterInput markAsFinished]; |
||||
bool success = [mVideoWriter finishWriting]; |
||||
|
||||
[mAdaptor release]; |
||||
[mVideoWriterInput release]; |
||||
[mVideoWriter release]; |
||||
|
||||
mAdaptor = nil; |
||||
mVideoWriterInput = nil; |
||||
mVideoWriter = nil; |
||||
} |
||||
|
||||
/** |
||||
* \brief Request the recording to stop |
||||
*/ |
||||
void UBQuickTimeFile::stop() |
||||
{ |
||||
mShouldStopCompression = true; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* \brief Create a CVPixelBufferRef from the input adaptor's CVPixelBufferPool |
||||
*/ |
||||
CVPixelBufferRef UBQuickTimeFile::newPixelBuffer() |
||||
{ |
||||
CVPixelBufferRef pixelBuffer = 0; |
||||
|
||||
if(CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, mAdaptor.pixelBufferPool, &pixelBuffer) != kCVReturnSuccess) |
||||
{ |
||||
setLastErrorMessage("Could not retrieve CV buffer from pool"); |
||||
return 0; |
||||
} |
||||
|
||||
return pixelBuffer; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* \brief Add a frame to the pixel buffer adaptor |
||||
*/ |
||||
void UBQuickTimeFile::appendVideoFrame(CVPixelBufferRef pixelBuffer, long msTimeStamp) |
||||
{ |
||||
//qDebug() << "adding video frame at time: " << msTimeStamp; |
||||
|
||||
CMTime t = CMTimeMake((msTimeStamp * mTimeScale / 1000.0), mTimeScale); |
||||
|
||||
bool added = [mAdaptor appendPixelBuffer: pixelBuffer |
||||
withPresentationTime: t]; |
||||
|
||||
if (!added) |
||||
setLastErrorMessage(QString("Could not encode frame at time %1").arg(msTimeStamp)); |
||||
|
||||
|
||||
CVPixelBufferRelease(pixelBuffer); |
||||
} |
||||
|
||||
void UBQuickTimeFile::setLastErrorMessage(const QString& error) |
||||
{ |
||||
mLastErrorMessage = error; |
||||
qWarning() << "UBQuickTimeFile error" << error; |
||||
} |
||||
|
||||
|
||||
|
Loading…
Reference in new issue