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