From 642d1546eba719fb55bbf56869845a26d01bf653 Mon Sep 17 00:00:00 2001 From: Craig Watson Date: Fri, 20 Nov 2015 14:49:52 +0100 Subject: [PATCH] Podcast functionality (partially) restored on OS X. Migrated all QuickTime-related code to modern AVFoundation / Core Video / Core Media equivalents. Audio support was temporarily removed; to be re-established ASAP. Beginnings of doxygen-style function documentation was added --- OpenBoard.pro | 4 +- src/podcast/UBPodcastController.cpp | 14 +- src/podcast/podcast.pri | 69 +- .../quicktime/UBAudioQueueRecorder.cpp | 4 +- src/podcast/quicktime/UBQuickTimeFile.cpp | 656 ------------------ src/podcast/quicktime/UBQuickTimeFile.h | 77 +- src/podcast/quicktime/UBQuickTimeFile.mm | 262 +++++++ .../quicktime/UBQuickTimeVideoEncoder.cpp | 10 +- 8 files changed, 348 insertions(+), 748 deletions(-) delete mode 100644 src/podcast/quicktime/UBQuickTimeFile.cpp create mode 100644 src/podcast/quicktime/UBQuickTimeFile.mm diff --git a/OpenBoard.pro b/OpenBoard.pro index 4ba4b151..b6802c50 100644 --- a/OpenBoard.pro +++ b/OpenBoard.pro @@ -130,13 +130,13 @@ macx { LIBS += -framework Foundation LIBS += -framework Cocoa LIBS += -framework Carbon + LIBS += -framework AVFoundation + LIBS += -framework CoreMedia LIBS += -lcrypto CONFIG(release, debug|release):CONFIG += x86_64 CONFIG(debug, debug|release):CONFIG += x86_64 - # TODO Craig: switch to 64bit - QMAKE_MAC_SDK = macosx QMAKE_MACOSX_DEPLOYMENT_TARGET = "10.10" diff --git a/src/podcast/UBPodcastController.cpp b/src/podcast/UBPodcastController.cpp index 45a5e435..12641434 100644 --- a/src/podcast/UBPodcastController.cpp +++ b/src/podcast/UBPodcastController.cpp @@ -59,9 +59,9 @@ #ifdef Q_OS_WIN #include "windowsmedia/UBWindowsMediaVideoEncoder.h" #include "windowsmedia/UBWaveRecorder.h" -//#elif defined(Q_OS_OSX) -// #include "quicktime/UBQuickTimeVideoEncoder.h" -// #include "quicktime/UBAudioQueueRecorder.h" +#elif defined(Q_OS_OSX) + #include "quicktime/UBQuickTimeVideoEncoder.h" + #include "quicktime/UBAudioQueueRecorder.h" #endif #include "core/memcheck.h" @@ -305,8 +305,8 @@ void UBPodcastController::start() #ifdef Q_OS_WIN mVideoEncoder = new UBWindowsMediaVideoEncoder(this); //deleted on stop -//#elif defined(Q_OS_OSX) -// mVideoEncoder = new UBQuickTimeVideoEncoder(this); //deleted on stop +#elif defined(Q_OS_OSX) + mVideoEncoder = new UBQuickTimeVideoEncoder(this); //deleted on stop #endif if (mVideoEncoder) @@ -795,8 +795,8 @@ QStringList UBPodcastController::audioRecordingDevices() #ifdef Q_OS_WIN devices = UBWaveRecorder::waveInDevices(); -//#elif defined(Q_OS_OSX) -// devices = UBAudioQueueRecorder::waveInDevices(); +#elif defined(Q_OS_OSX) + devices = UBAudioQueueRecorder::waveInDevices(); #endif return devices; diff --git a/src/podcast/podcast.pri b/src/podcast/podcast.pri index d6d5aa94..3dde6273 100644 --- a/src/podcast/podcast.pri +++ b/src/podcast/podcast.pri @@ -1,34 +1,35 @@ - -HEADERS += src/podcast/UBPodcastController.h \ - src/podcast/UBAbstractVideoEncoder.h \ - src/podcast/UBPodcastRecordingPalette.h \ - src/podcast/youtube/UBYouTubePublisher.h \ - src/podcast/intranet/UBIntranetPodcastPublisher.h - -SOURCES += src/podcast/UBPodcastController.cpp \ - src/podcast/UBAbstractVideoEncoder.cpp \ - src/podcast/UBPodcastRecordingPalette.cpp \ - src/podcast/youtube/UBYouTubePublisher.cpp \ - src/podcast/intranet/UBIntranetPodcastPublisher.cpp - -win32 { - - SOURCES += src/podcast/windowsmedia/UBWindowsMediaVideoEncoder.cpp \ - src/podcast/windowsmedia/UBWindowsMediaFile.cpp \ - src/podcast/windowsmedia/UBWaveRecorder.cpp - - HEADERS += src/podcast/windowsmedia/UBWindowsMediaVideoEncoder.h \ - src/podcast/windowsmedia/UBWindowsMediaFile.h \ - src/podcast/windowsmedia/UBWaveRecorder.h -} - -#macx { - -# SOURCES += src/podcast/quicktime/UBQuickTimeVideoEncoder.cpp \ -# src/podcast/quicktime/UBQuickTimeFile.cpp \ -# src/podcast/quicktime/UBAudioQueueRecorder.cpp - -# HEADERS += src/podcast/quicktime/UBQuickTimeVideoEncoder.h \ -# src/podcast/quicktime/UBQuickTimeFile.h \ -# src/podcast/quicktime/UBAudioQueueRecorder.h -#} + +HEADERS += src/podcast/UBPodcastController.h \ + src/podcast/UBAbstractVideoEncoder.h \ + src/podcast/UBPodcastRecordingPalette.h \ + src/podcast/youtube/UBYouTubePublisher.h \ + src/podcast/intranet/UBIntranetPodcastPublisher.h + +SOURCES += src/podcast/UBPodcastController.cpp \ + src/podcast/UBAbstractVideoEncoder.cpp \ + src/podcast/UBPodcastRecordingPalette.cpp \ + src/podcast/youtube/UBYouTubePublisher.cpp \ + src/podcast/intranet/UBIntranetPodcastPublisher.cpp + +win32 { + + SOURCES += src/podcast/windowsmedia/UBWindowsMediaVideoEncoder.cpp \ + src/podcast/windowsmedia/UBWindowsMediaFile.cpp \ + src/podcast/windowsmedia/UBWaveRecorder.cpp + + HEADERS += src/podcast/windowsmedia/UBWindowsMediaVideoEncoder.h \ + src/podcast/windowsmedia/UBWindowsMediaFile.h \ + src/podcast/windowsmedia/UBWaveRecorder.h +} + +macx { + + SOURCES += src/podcast/quicktime/UBQuickTimeVideoEncoder.cpp \ + src/podcast/quicktime/UBAudioQueueRecorder.cpp + + HEADERS += src/podcast/quicktime/UBQuickTimeVideoEncoder.h \ + src/podcast/quicktime/UBQuickTimeFile.h \ + src/podcast/quicktime/UBAudioQueueRecorder.h + + OBJECTIVE_SOURCES += src/podcast/quicktime/UBQuickTimeFile.mm +} diff --git a/src/podcast/quicktime/UBAudioQueueRecorder.cpp b/src/podcast/quicktime/UBAudioQueueRecorder.cpp index 1c529ab5..43c106f2 100644 --- a/src/podcast/quicktime/UBAudioQueueRecorder.cpp +++ b/src/podcast/quicktime/UBAudioQueueRecorder.cpp @@ -151,10 +151,10 @@ QString UBAudioQueueRecorder::deviceUIDFromDeviceID(AudioDeviceID id) { char *cname = new char[1024]; - CFStringGetCString (name, cname, 1024, kCFStringEncodingASCII); + CFStringGetCString (name, cname, 1024, kCFStringEncodingISOLatin1); int length = CFStringGetLength (name); - uid = QString::fromAscii(cname, length); + uid = QString::fromLatin1(cname, length); delete cname; diff --git a/src/podcast/quicktime/UBQuickTimeFile.cpp b/src/podcast/quicktime/UBQuickTimeFile.cpp deleted file mode 100644 index 97ebf048..00000000 --- a/src/podcast/quicktime/UBQuickTimeFile.cpp +++ /dev/null @@ -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 . - */ - - - - -#include "UBQuickTimeFile.h" - -#include - -#include "UBAudioQueueRecorder.h" -#include - -#include "core/memcheck.h" - -QQueue 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 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; -} - - diff --git a/src/podcast/quicktime/UBQuickTimeFile.h b/src/podcast/quicktime/UBQuickTimeFile.h index d6fe2ce6..1d1efd70 100644 --- a/src/podcast/quicktime/UBQuickTimeFile.h +++ b/src/podcast/quicktime/UBQuickTimeFile.h @@ -30,12 +30,29 @@ #include -#include -#include -#include +#include #include "UBAudioQueueRecorder.h" + + +// Trick to get around the fact that the C++ compiler doesn't +// like Objective C code. + +#ifdef __OBJC__ // defined by the Objective C compiler + @class AVAssetWriter; + @class AVAssetWriterInput; + @class AVAssetWriterInputPixelBufferAdaptor; + + typedef AVAssetWriter* AssetWriterPTR; + typedef AVAssetWriterInput* AssetWriterInputPTR; + typedef AVAssetWriterInputPixelBufferAdaptor* AssetWriterInputAdaptorPTR; +#else + typedef void* AssetWriterPTR; + typedef void* AssetWriterInputPTR; + typedef void* AssetWriterInputAdaptorPTR; +#endif + class UBQuickTimeFile : public QThread { Q_OBJECT; @@ -52,15 +69,11 @@ class UBQuickTimeFile : public QThread CVPixelBufferRef newPixelBuffer(); - bool isCompressionSessionRunning() - { - return mCompressionSessionRunning; - } + bool isCompressionSessionRunning() { return mCompressionSessionRunning; } - QString lastErrorMessage() const - { - return mLastErrorMessage; - } + QString lastErrorMessage() const { return mLastErrorMessage; } + + void endSession(); struct VideoFrame { @@ -79,47 +92,19 @@ class UBQuickTimeFile : public QThread protected: void run(); - private slots: - - void appendAudioBuffer(void* pBuffer, long pLength, int inNumberPacketDescriptions - , const AudioStreamPacketDescription* inPacketDescs); private: - static OSStatus addEncodedFrameToMovie(void *encodedFrameOutputRefCon, - ICMCompressionSessionRef session, - OSStatus err, - ICMEncodedFrameRef encodedFrame, - void *reserved); + bool beginSession(); void appendVideoFrame(CVPixelBufferRef pixelBuffer, long msTimeStamp); - void addEncodedFrame(ICMEncodedFrameRef encodedFrame, OSStatus err); - - bool createCompressionSession(); - bool closeCompressionSession(); - bool createMovie(); - - bool createVideoMedia(); - bool createAudioMedia(); - void setLastErrorMessage(const QString& error); bool flushPendingFrames(); - ICMCompressionSessionRef mVideoCompressionSession; - - Media mVideoMedia; - Media mSoundMedia; - Track mVideoOutputTrack; - Track mSoundOutputTrack; - volatile CVPixelBufferPoolRef mCVPixelBufferPool; - SoundDescriptionHandle mSoundDescription; - - Movie mOutputMovie; - DataHandler mOutputMovieDataHandler; int mFramesPerSecond; QSize mFrameSize; @@ -130,17 +115,17 @@ class UBQuickTimeFile : public QThread QString mLastErrorMessage; - AudioStreamBasicDescription mAudioDataFormat; - - QPointer mWaveRecorder; + QString mSpatialQuality; - CodecQ mSpatialQuality; - - volatile bool mSouldStopCompression; + volatile bool mShouldStopCompression; volatile bool mCompressionSessionRunning; QString mAudioRecordingDeviceName; volatile int mPendingFrames; + + AssetWriterPTR mVideoWriter; + AssetWriterInputPTR mVideoWriterInput; + AssetWriterInputAdaptorPTR mAdaptor; }; #endif /* UBQUICKTIMEFILE_H_ */ diff --git a/src/podcast/quicktime/UBQuickTimeFile.mm b/src/podcast/quicktime/UBQuickTimeFile.mm new file mode 100644 index 00000000..e6e18d72 --- /dev/null +++ b/src/podcast/quicktime/UBQuickTimeFile.mm @@ -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 . + */ + + + + +#include "UBQuickTimeFile.h" + +#include +#import +#import +#import + +#include "UBAudioQueueRecorder.h" +#include + +#include "core/memcheck.h" + +QQueue 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 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; +} + + + diff --git a/src/podcast/quicktime/UBQuickTimeVideoEncoder.cpp b/src/podcast/quicktime/UBQuickTimeVideoEncoder.cpp index ab0271be..e3497640 100644 --- a/src/podcast/quicktime/UBQuickTimeVideoEncoder.cpp +++ b/src/podcast/quicktime/UBQuickTimeVideoEncoder.cpp @@ -130,6 +130,14 @@ void UBQuickTimeVideoEncoder::newPixmap(const QImage& pImage, long timestamp) } } +/** + * \brief Encode QImage into a video frame and add it to the UBQuickTimeFile's queue. + * + * This method retrieves the raw image from the supplied QImage, and uses memcpy to + * dump it into a CVPixelBuffer, obtained through the UBQuickTimeFile member. The + * pixel buffer, along with the timestamp, constitute a video frame which is added + * to the member UBQuickTimeFile's queue. + */ void UBQuickTimeVideoEncoder::encodeFrame(const QImage& pImage, long timestamp) { Q_ASSERT(pImage.format() == QImage::QImage::Format_RGB32); // <=> CVPixelBuffers / k32BGRAPixelFormat @@ -157,7 +165,7 @@ void UBQuickTimeVideoEncoder::encodeFrame(const QImage& pImage, long timestamp) const uchar* imageBuffer = pImage.bits(); - memcpy((void*) pixelBufferAddress, imageBuffer, pImage.numBytes()); + memcpy((void*) pixelBufferAddress, imageBuffer, pImage.byteCount()); CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);