Retired Document
Important: This sample code may not represent best practices for current development. The project may use deprecated symbols and illustrate technologies and techniques that are no longer recommended.
CaptureAndCompressIPBMovie.c
/* |
File: CaptureAndCompressIPBMovie.c |
Abstract: Shows how to capture video, recompress it on the fly with H.264 and |
store the output in a movie file, using QuickTime 7's Compression Session APIs. |
Shows how to capture audio using SGAudioMediaType SGChannel. |
Version: 1.1 |
© Copyright 2005 Apple Computer, Inc. All rights reserved. |
IMPORTANT: This Apple software is supplied to |
you by Apple Computer, Inc. ("Apple") in |
consideration of your agreement to the following |
terms, and your use, installation, modification |
or redistribution of this Apple software |
constitutes acceptance of these terms. If you do |
not agree with these terms, please do not use, |
install, modify or redistribute this Apple |
software. |
In consideration of your agreement to abide by |
the following terms, and subject to these terms, |
Apple grants you a personal, non-exclusive |
license, under Apple's copyrights in this |
original Apple software (the "Apple Software"), |
to use, reproduce, modify and redistribute the |
Apple Software, with or without modifications, in |
source and/or binary forms; provided that if you |
redistribute the Apple Software in its entirety |
and without modifications, you must retain this |
notice and the following text and disclaimers in |
all such redistributions of the Apple Software. |
Neither the name, trademarks, service marks or |
logos of Apple Computer, Inc. may be used to |
endorse or promote products derived from the |
Apple Software without specific prior written |
permission from Apple. Except as expressly |
stated in this notice, no other rights or |
licenses, express or implied, are granted by |
Apple herein, including but not limited to any |
patent rights that may be infringed by your |
derivative works or by other works in which the |
Apple Software may be incorporated. |
The Apple Software is provided by Apple on an "AS |
IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR |
IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED |
WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY |
AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING |
THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE |
OR IN COMBINATION WITH YOUR PRODUCTS. |
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY |
SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL |
DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS |
OF USE, DATA, OR PROFITS; OR BUSINESS |
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, |
REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF |
THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER |
UNDER THEORY OF CONTRACT, TORT (INCLUDING |
NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN |
IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF |
SUCH DAMAGE. |
*/ |
#include <QuickTime/QuickTime.h> |
#define ASK_SEQUENCE_GRABBER_TO_CONVERT_AUDIO_TO_AAC 0 |
// defines |
#define BailErr(x) {err = x; if(err != noErr) goto bail;} |
#define DisplayAndBail(x, y) {pMungData->err = x; if(x != noErr) { displayError(pMungData->pWindow, y, x); goto bail;}} |
// constants |
const EventTime kTimerInterval = kEventDurationSecond / 60; // idle timer interval |
// mung data struct |
typedef struct { |
WindowRef pWindow; // window |
int width; // dest width |
int height; // dest height |
CodecType codecType; // codec |
SInt32 averageDataRate; // data rate |
HIViewRef imageView; // for displaying decoded frames |
ICMDecompressionSessionRef decompressionSession; // decompresses and scales captured frames |
ICMCompressionSessionRef compressionSession; // compresses frames |
Movie outputMovie; // movie file for storing compressed frames |
Media outputVideoMedia; // media for our video track in the movie |
DataHandler outputMovieDataHandler; // storage for movie header |
CFURLRef outputMovieURL; // URL to the movie file (once we create it) |
Boolean didBeginVideoMediaEdits; |
Boolean verbose; |
SeqGrabComponent seqGrab; // sequence grabber |
TimeValue lastTime; |
TimeScale timeScale; |
int desiredFramesPerSecond; |
TimeValue minimumFrameDuration; |
long frameCount; |
Boolean isGrabbing; |
EventLoopTimerRef timerRef; |
OSErr err; |
SGChannel audioChan; |
Media soundMedia; |
Boolean didBeginSoundMediaEdits; |
TimeScale audioTimeScale; |
SoundDescriptionHandle audioDescH; |
AudioStreamBasicDescription asbd; |
} MungDataRecord, *MungDataPtr; |
static MungDataPtr initializeMungData(WindowRef inWindow, Rect inBounds, SeqGrabComponent inSeqGrab); |
static void disposeMungData(MungDataPtr pMungData); |
static OSStatus setUpOutputMovie( MungDataPtr pMungData ); |
static void finishOutputMovie( MungDataPtr pMungData ); |
static void openOutputMovie( MungDataPtr pMungData ); |
// -------------------- |
// initializeMungData |
// |
static MungDataPtr initializeMungData(WindowRef inWindow, Rect inBounds, SeqGrabComponent inSeqGrab) |
{ |
MungDataPtr pMungData = NULL; |
HIViewRef contentView = NULL; |
HIViewID kImageID = { 'IMAG', 0 }; |
OSErr err = noErr; |
// allocate memory for the data |
pMungData = (MungDataPtr)calloc(sizeof(MungDataRecord), 1); |
if (MemError() || NULL == pMungData ) return NULL; |
pMungData->pWindow = inWindow; |
pMungData->width = inBounds.right - inBounds.left; |
pMungData->height = inBounds.bottom - inBounds.top; |
pMungData->codecType = kH264CodecType; |
pMungData->averageDataRate = 100000; // 100 kbyte/sec == 800 kbit/sec |
pMungData->desiredFramesPerSecond = 15; |
pMungData->seqGrab = inSeqGrab; |
pMungData->isGrabbing = false; |
pMungData->verbose = false; |
GetRootControl( pMungData->pWindow, &contentView ); |
HIViewFindByID( contentView, kImageID, &pMungData->imageView ); |
err = setUpOutputMovie( pMungData ); |
if( err ) |
goto bail; |
return pMungData; |
bail: |
// clean up and get out |
if (pMungData) { |
disposeMungData(pMungData); |
} |
return NULL; |
} |
// -------------------- |
// disposeMungData |
// |
static void disposeMungData(MungDataPtr pMungData) |
{ |
// clean up the bits |
if(pMungData) { |
if (pMungData->seqGrab) |
CloseComponent(pMungData->seqGrab); |
ICMDecompressionSessionRelease( pMungData->decompressionSession ); |
// It is important to push out any remaining frames before we release the compression session. |
// If we knew the timestamp following the last source frame, you should pass it in here. |
ICMCompressionSessionCompleteFrames( pMungData->compressionSession, true, 0, 0 ); |
ICMCompressionSessionRelease( pMungData->compressionSession ); |
if( pMungData->outputMovie ) { |
finishOutputMovie( pMungData ); |
openOutputMovie( pMungData ); |
} |
if( pMungData->outputMovieURL ) |
CFRelease( pMungData->outputMovieURL ); |
DisposeHandle((Handle)pMungData->audioDescH); |
free((Ptr)pMungData); |
pMungData = NULL; |
} |
} |
static OSStatus promptForOutputMovieDataRef( Handle *dataRefOut, OSType *dataRefTypeOut ) |
{ |
OSStatus err = noErr; |
NavDialogCreationOptions navOptions = { kNavDialogCreationOptionsVersion, 0, }; |
NavDialogRef navDialog = NULL; |
NavReplyRecord navReply = { kNavReplyRecordVersion, }; |
AEDesc actualDesc = { 0, 0 }; |
FSRef parentFSRef; |
err = NavGetDefaultDialogCreationOptions( &navOptions ); |
if( err ) |
goto bail; |
navOptions.windowTitle = CFSTR("Save Captured Movie As..."); |
navOptions.message = CFSTR("Pick where to save the captured and compressed movie. (Make sure your camera is plugged in!)"); |
navOptions.saveFileName = CFSTR("captured.mov"); |
navOptions.modality = kWindowModalityAppModal; |
err = NavCreatePutFileDialog( &navOptions, MovieFileType, 'TVOD', NULL, NULL, &navDialog ); |
if( err ) |
goto bail; |
err = NavDialogRun( navDialog ); |
if( err ) |
goto bail; |
if( kNavUserActionSaveAs != NavDialogGetUserAction( navDialog ) ) { |
err = userCanceledErr; |
goto bail; |
} |
err = NavDialogGetReply( navDialog, &navReply ); |
if( err ) |
goto bail; |
err = AECoerceDesc( &navReply.selection, typeFSRef, &actualDesc ); |
if( err ) |
goto bail; |
err = AEGetDescData( &actualDesc, &parentFSRef, sizeof( FSRef ) ); |
if( err ) |
goto bail; |
err = QTNewDataReferenceFromFSRefCFString( &parentFSRef, navReply.saveFileName, 0, dataRefOut, dataRefTypeOut ); |
if( err ) { |
fprintf( stderr, "QTNewDataReferenceFromFSRefCFString() failed (%d)\n", err ); |
goto bail; |
} |
bail: |
NavDisposeReply( &navReply ); |
NavDialogDispose( navDialog ); |
AEDisposeDesc( &actualDesc ); |
return err; |
} |
static OSStatus setUpOutputMovie( MungDataPtr pMungData ) |
{ |
OSStatus err = noErr; |
Handle outputMovieDataRef = NULL; |
OSType outputMovieDataRefType = 0; |
CFStringRef outputMovieFullPathString = NULL; |
// Prompt the user for an output file. |
err = promptForOutputMovieDataRef( &outputMovieDataRef, &outputMovieDataRefType ); |
if( err ) |
goto bail; |
// Create a new movie file. |
// If you're using CreateMovieFile, consider switching to CreateMovieStorage, which is long-file-name aware. |
err = CreateMovieStorage( outputMovieDataRef, outputMovieDataRefType, 'TVOD', 0, |
createMovieFileDeleteCurFile, &pMungData->outputMovieDataHandler, &pMungData->outputMovie ); |
if( err ) { |
fprintf( stderr, "CreateMovieStorage failed (%d)\n", err ); |
goto bail; |
} |
// Remember a URL for the movie file so we can open it in QuickTime Player afterwards. |
err = QTGetDataReferenceFullPathCFString( outputMovieDataRef, outputMovieDataRefType, |
kQTPOSIXPathStyle, &outputMovieFullPathString ); |
if( err ) |
goto bail; |
pMungData->outputMovieURL = CFURLCreateWithFileSystemPath( NULL, outputMovieFullPathString, kCFURLPOSIXPathStyle, false ); |
bail: |
DisposeHandle( outputMovieDataRef ); |
if( outputMovieFullPathString ) |
CFRelease( outputMovieFullPathString ); |
return err; |
} |
static void finishOutputMovie(MungDataPtr pMungData) |
{ |
OSStatus err = noErr; |
Track videoTrack = NULL; |
if( pMungData->didBeginVideoMediaEdits ) { |
// End the media sample-adding session. |
err = EndMediaEdits( pMungData->outputVideoMedia ); |
if( err ) { |
fprintf( stderr, "EndMediaEdits() failed (%d)\n", err ); |
goto bail; |
} |
} |
// Make sure things are extra neat. |
ExtendMediaDecodeDurationToDisplayEndTime( pMungData->outputVideoMedia, NULL ); |
// Insert the stuff we added into the track, at the end. |
videoTrack = GetMediaTrack( pMungData->outputVideoMedia ); |
err = InsertMediaIntoTrack( videoTrack, |
GetTrackDuration(videoTrack), |
0, GetMediaDisplayDuration( pMungData->outputVideoMedia ), // NOTE: use this instead of GetMediaDuration |
fixed1 ); |
if( err ) { |
fprintf( stderr, "InsertMediaIntoTrack() failed (%d)\n", err ); |
goto bail; |
} |
if ( pMungData->didBeginSoundMediaEdits ) |
{ |
err = EndMediaEdits( pMungData->soundMedia ); |
if( err ) { |
fprintf( stderr, "EndMediaEdits(soundMedia) failed (%d)\n", err ); |
goto bail; |
} |
} |
{ |
Track soundTrack = GetMediaTrack( pMungData->soundMedia ); |
TimeValue soundTrackDur, videoTrackDur; |
err = InsertMediaIntoTrack( soundTrack, 0, 0, GetMediaDuration( pMungData->soundMedia ), fixed1 ); |
if ( err ) { |
fprintf( stderr, "InsertMediaIntoTrack(soundTrack) failed (%d)\n", err ); |
goto bail; |
} |
// trim the sound track to the duration of the video track |
// (no one likes white frames at the end of their movies) |
soundTrackDur = GetTrackDuration(soundTrack); |
videoTrackDur = GetTrackDuration(videoTrack); |
if (soundTrackDur > videoTrackDur) |
{ |
fprintf( stderr, "sound track ran long, soundTrackDur = %ld, videoTrackDur = %ld\n", soundTrackDur, videoTrackDur ); |
DeleteTrackSegment(soundTrack, videoTrackDur, soundTrackDur - videoTrackDur); |
} |
} |
// Write the movie header to the file. |
err = AddMovieToStorage( pMungData->outputMovie, pMungData->outputMovieDataHandler ); |
if( err ) { |
fprintf( stderr, "AddMovieToStorage() failed (%d)\n", err ); |
goto bail; |
} |
CloseMovieStorage( pMungData->outputMovieDataHandler ); |
pMungData->outputMovieDataHandler = 0; |
DisposeMovie( pMungData->outputMovie ); |
bail: |
return; |
} |
static void openOutputMovie( MungDataPtr pMungData ) |
{ |
OSStatus err = noErr; |
// Open the movie we've created in QuickTime Player. |
err = LSOpenCFURLRef( pMungData->outputMovieURL, NULL ); |
bail: |
return; |
} |
// -------------------- |
// makeSequenceGrabber |
// |
static SeqGrabComponent makeSequenceGrabber(WindowRef pWindow) |
{ |
SeqGrabComponent seqGrab = NULL; |
OSErr err = noErr; |
// open the default sequence grabber |
seqGrab = OpenDefaultComponent(SeqGrabComponentType, 0); |
if (seqGrab != NULL) { |
// initialize the default sequence grabber component |
err = SGInitialize(seqGrab); |
if (err == noErr) |
// set its graphics world to the specified window |
err = SGSetGWorld(seqGrab, GetWindowPort(pWindow), NULL); |
if (err == noErr) |
// specify the destination data reference for a record operation |
// tell it we're not making a movie |
// if the flag seqGrabDontMakeMovie is used, the sequence grabber still calls |
// your data function, but does not write any data to the movie file |
// writeType will always be set to seqGrabWriteAppend |
err = SGSetDataRef(seqGrab, |
0, |
0, |
seqGrabDontMakeMovie); |
} |
if (err && (seqGrab != NULL)) { // clean up on failure |
CloseComponent(seqGrab); |
seqGrab = NULL; |
} |
return seqGrab; |
} |
// -------------------- |
// makeSequenceGrabVideoChannel |
// |
static OSErr makeSequenceGrabVideoChannel(SeqGrabComponent seqGrab, SGChannel *sgchanVideo, Rect const *rect) |
{ |
long flags = 0; |
OSErr err = noErr; |
err = SGNewChannel(seqGrab, VideoMediaType, sgchanVideo); |
if (err == noErr) { |
err = SGSetChannelBounds(*sgchanVideo, rect); |
if (err == noErr) |
// set usage for new video channel to avoid playthrough |
// note we don't set seqGrabPlayDuringRecord |
err = SGSetChannelUsage(*sgchanVideo, flags | seqGrabRecord); |
if (err != noErr) { |
// clean up on failure |
SGDisposeChannel(seqGrab, *sgchanVideo); |
*sgchanVideo = NULL; |
} |
} |
return err; |
} |
// -------------------- |
// makeSequenceGrabAudioChannel |
// |
static OSErr makeSequenceGrabAudioChannel(SeqGrabComponent seqGrab, SGChannel *sgchanAudio) |
{ |
OSErr err = noErr; |
err = SGNewChannel(seqGrab, SGAudioMediaType, sgchanAudio); |
if (err == noErr) { |
// set usage for new audio channel. |
// We will use the SGAudioChannel's built in mechanism for previewing |
// by adding the seqGrabPlayDuringRecord flag |
err = SGSetChannelUsage(*sgchanAudio, seqGrabRecord | seqGrabPlayDuringRecord); |
if (err == noErr) |
{ |
// By default, the device from which the SGAudioChannel captures is |
// the audio portion of your selected video device. If your video device |
// has no audio recording capabilities, SGAudioChannel will capture from |
// the default device specified in your system-wide audio settings, found |
// in /Applications/Utilities/Audio Midi Setup.app |
// If you are recording live from a microphone (i.e. the built in microphone |
// on an iSight camera), chances are good that your microphone is |
// situated in close proximity to the speakers through which you will |
// be previewing the audio -- which may introduce a feedback loop. |
// To prevent this, we can set the master gain of the preview device |
// to be very low (NOTE - the gain is adjusted in software. The physical |
// volume of your playback device will not be altered). |
Float32 lowVolume = 0.05; // 1.0 is full, 0.0 is muted |
err = QTSetComponentProperty(*sgchanAudio, kQTPropertyClass_SGAudioPreviewDevice, |
kQTSGAudioPropertyID_MasterGain, |
sizeof(lowVolume), &lowVolume); |
} |
#if ASK_SEQUENCE_GRABBER_TO_CONVERT_AUDIO_TO_AAC |
if (err == noErr) |
{ |
AudioStreamBasicDescription outputFormat; |
// get the current output format, so we carry over the source |
// sample rate. we'll mix down to stereo if there are > 2 source |
// channels. |
err = QTGetComponentProperty(*sgchanAudio, kQTPropertyClass_SGAudio, |
kQTSGAudioPropertyID_StreamFormat, |
sizeof(outputFormat), &outputFormat, NULL); |
if (err == noErr) |
{ |
outputFormat.mFormatID = 'aac '; |
if (outputFormat.mChannelsPerFrame > 2) |
outputFormat.mChannelsPerFrame = 2; |
outputFormat.mBitsPerChannel = 0; |
outputFormat.mBytesPerFrame = 0; |
outputFormat.mBytesPerPacket = 0; |
outputFormat.mFormatFlags = 0; |
outputFormat.mFramesPerPacket = 0; // Sequence Grabber will fill in the fields we don't know. |
err = QTSetComponentProperty(*sgchanAudio, kQTPropertyClass_SGAudio, |
kQTSGAudioPropertyID_StreamFormat, |
sizeof(outputFormat), &outputFormat); |
} |
} |
#endif |
// See QuickTimeComponents.h for a full list of the SGAudioChannel's configurable |
// properties. Search for SGAudioMediaType |
if (err != noErr) { |
// clean up on failure |
SGDisposeChannel(seqGrab, *sgchanAudio); |
*sgchanAudio = NULL; |
} |
} |
return err; |
} |
static void displayError(WindowRef inWindow, char inStr[], OSErr inError) |
{ |
// set the window title to display the error |
char errMsg[64]; |
sprintf(errMsg, "%s: %d", inStr, inError); |
CopyCStringToPascal(errMsg, (unsigned char *)&errMsg); |
SetWTitle(inWindow, (unsigned char *)errMsg); |
} |
static OSStatus createDecompressionSession( |
ImageDescriptionHandle imageDesc, |
int width, |
int height, |
OSType pixelFormat, |
ICMDecompressionTrackingCallback trackingCallback, |
void *trackingRefCon, |
ICMDecompressionSessionRef *decompressionSessionOut ) |
{ |
OSStatus err = noErr; |
CFNumberRef number = NULL; |
CFMutableDictionaryRef pixelBufferAttributes = NULL; |
ICMDecompressionSessionOptionsRef sessionOptions = NULL; |
ICMDecompressionTrackingCallbackRecord trackingCallbackRecord; |
// Make a CFDictionary that describes the pixel buffers we want the decompression session to output. |
// We want them to be the right dimensions and pixel format; we want them to be compatible with |
// CGBitmapContext and CGImage. |
// Note: if we didn't need to draw on frames with CG or display the frames in an HIImageView, we |
// could improve performance by getting the compression session's preferred pixel buffer attributes |
// via the kICMCompressionSessionPropertyID_CompressorPixelBufferAttributes property |
// and passing that when creating the decompression session. |
pixelBufferAttributes = CFDictionaryCreateMutable( NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks ); |
number = CFNumberCreate( NULL, kCFNumberIntType, &width ); |
CFDictionaryAddValue( pixelBufferAttributes, kCVPixelBufferWidthKey, number ); |
CFRelease( number ); |
number = CFNumberCreate( NULL, kCFNumberIntType, &height ); |
CFDictionaryAddValue( pixelBufferAttributes, kCVPixelBufferHeightKey, number ); |
CFRelease( number ); |
number = CFNumberCreate( NULL, kCFNumberSInt32Type, &pixelFormat ); |
CFDictionaryAddValue( pixelBufferAttributes, kCVPixelBufferPixelFormatTypeKey, number ); |
CFRelease( number ); |
CFDictionaryAddValue( pixelBufferAttributes, kCVPixelBufferCGBitmapContextCompatibilityKey, kCFBooleanTrue ); |
CFDictionaryAddValue( pixelBufferAttributes, kCVPixelBufferCGImageCompatibilityKey, kCFBooleanTrue ); |
trackingCallbackRecord.decompressionTrackingCallback = trackingCallback; |
trackingCallbackRecord.decompressionTrackingRefCon = trackingRefCon; |
err = ICMDecompressionSessionCreate( NULL, imageDesc, sessionOptions, |
pixelBufferAttributes, &trackingCallbackRecord, decompressionSessionOut ); |
if( err ) { |
fprintf( stderr, "ICMDecompressionSessionCreate failed (%d)", err ); |
} |
CFRelease( pixelBufferAttributes ); |
ICMDecompressionSessionOptionsRelease( sessionOptions ); |
return err; |
} |
static OSStatus |
createCompressionSession( |
int width, int height, CodecType codecType, SInt32 averageDataRate, TimeScale timeScale, |
ICMEncodedFrameOutputCallback outputCallback, void *outputRefCon, |
ICMCompressionSessionRef *compressionSessionOut ) |
{ |
OSStatus err = noErr; |
ICMEncodedFrameOutputRecord encodedFrameOutputRecord = {0}; |
ICMCompressionSessionOptionsRef sessionOptions = NULL; |
err = ICMCompressionSessionOptionsCreate( NULL, &sessionOptions ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsCreate() failed (%d)\n", err ); |
goto bail; |
} |
// We must set this flag to enable P or B frames. |
err = ICMCompressionSessionOptionsSetAllowTemporalCompression( sessionOptions, true ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsSetAllowTemporalCompression() failed (%d)\n", err ); |
goto bail; |
} |
// We must set this flag to enable B frames. |
err = ICMCompressionSessionOptionsSetAllowFrameReordering( sessionOptions, true ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsSetAllowFrameReordering() failed (%d)\n", err ); |
goto bail; |
} |
// Set the maximum key frame interval, also known as the key frame rate. |
err = ICMCompressionSessionOptionsSetMaxKeyFrameInterval( sessionOptions, 30 ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsSetMaxKeyFrameInterval() failed (%d)\n", err ); |
goto bail; |
} |
// This allows the compressor more flexibility (ie, dropping and coalescing frames). |
err = ICMCompressionSessionOptionsSetAllowFrameTimeChanges( sessionOptions, true ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsSetAllowFrameTimeChanges() failed (%d)\n", err ); |
goto bail; |
} |
// We need durations when we store frames. |
err = ICMCompressionSessionOptionsSetDurationsNeeded( sessionOptions, true ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsSetDurationsNeeded() failed (%d)\n", err ); |
goto bail; |
} |
// Set the average data rate. |
err = ICMCompressionSessionOptionsSetProperty( sessionOptions, |
kQTPropertyClass_ICMCompressionSessionOptions, |
kICMCompressionSessionOptionsPropertyID_AverageDataRate, |
sizeof( averageDataRate ), |
&averageDataRate ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionOptionsSetProperty(AverageDataRate) failed (%d)\n", err ); |
goto bail; |
} |
encodedFrameOutputRecord.encodedFrameOutputCallback = outputCallback; |
encodedFrameOutputRecord.encodedFrameOutputRefCon = outputRefCon; |
encodedFrameOutputRecord.frameDataAllocator = NULL; |
err = ICMCompressionSessionCreate( NULL, width, height, codecType, timeScale, |
sessionOptions, NULL, &encodedFrameOutputRecord, compressionSessionOut ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionCreate() failed (%d)\n", err ); |
goto bail; |
} |
bail: |
ICMCompressionSessionOptionsRelease( sessionOptions ); |
return err; |
} |
// Utility to wrap a CGBitmapContext around a 32ARGB CVPixelBuffer. |
static OSStatus |
createCGBitmapContextFor32ARGBCVPixelBuffer( |
CVPixelBufferRef pixelBuffer, |
CGContextRef *contextOut ) |
{ |
size_t width, height, rowBytes; |
void *baseAddr; |
OSStatus err; |
CGColorSpaceRef colorspace = NULL; |
CGContextRef context = NULL; |
err = CVPixelBufferLockBaseAddress( pixelBuffer, 0 ); |
if( err ) { |
fprintf( stderr, "CVPixelBufferLockBaseAddress() failed (%d)\n", err ); |
goto bail; |
} |
rowBytes = CVPixelBufferGetBytesPerRow( pixelBuffer ); |
baseAddr = CVPixelBufferGetBaseAddress( pixelBuffer ); |
width = CVPixelBufferGetWidth( pixelBuffer ); |
height = CVPixelBufferGetHeight( pixelBuffer ); |
colorspace = CGColorSpaceCreateDeviceRGB(); |
context = CGBitmapContextCreate( baseAddr, width, height, |
8, rowBytes, colorspace, kCGImageAlphaPremultipliedFirst ); |
*contextOut = context; |
context = NULL; |
bail: |
if( colorspace ) CGColorSpaceRelease( colorspace ); |
if( context ) CGContextRelease( context ); |
return err; |
} |
static void |
unlockCVPixelBufferAndReleaseCGBitmapContext( |
CVPixelBufferRef pixelBuffer, |
CGContextRef context ) |
{ |
CGContextRelease( context ); |
CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 ); |
} |
// Utility to draw a string left- or right- justified with a stroked border in one color, filled with another color. |
enum Justify { |
kLeftJustify, kRightJustify |
}; |
static void showString( CGContextRef c, float x, float y, enum Justify justify, const char *str) |
{ |
CGPoint after; |
CGContextSaveGState( c ); |
if( kRightJustify == justify ) { |
CGContextSetTextPosition( c, 0, 0 ); |
CGContextSetTextDrawingMode( c, kCGTextInvisible ); |
CGContextShowText( c, str, strlen( str ) ); |
after = CGContextGetTextPosition( c ); |
x = x - after.x; |
y = y - after.y; |
} |
CGContextSetTextPosition( c, x, y ); |
CGContextSetTextDrawingMode( c, kCGTextFillStroke ); |
CGContextShowText( c, str, strlen( str ) ); |
CGContextRestoreGState( c ); |
} |
// Draw "LIVE" and the number of seconds into the capture on the pixel buffer. |
static void drawOnPixelBuffer( CVPixelBufferRef pixelBuffer, TimeValue64 displayTime, TimeScale timeScale ) |
{ |
CGContextRef context = NULL; |
if( noErr == createCGBitmapContextFor32ARGBCVPixelBuffer( pixelBuffer, &context ) ) { |
int width = CVPixelBufferGetWidth( pixelBuffer ); |
int timeInSeconds = displayTime / timeScale; |
char buf[100]; |
sprintf( buf, "%d:%02d", timeInSeconds / 60, timeInSeconds % 60 ); |
CGContextSelectFont( context, "Arial-Black", 32, kCGEncodingMacRoman ); |
CGContextSetLineWidth( context, 1.5 ); |
CGContextSetRGBStrokeColor( context, 1.0, 1.0, 1.0, 1.0 ); // white |
CGContextSetRGBFillColor( context, 0.2, 0.0, 0.0, 1.0 ); // dark red |
showString( context, 10, 10, kLeftJustify, "LIVE" ); |
CGContextSetRGBFillColor( context, 0.0, 0.0, 0.3, 1.0 ); // dark blue |
showString( context, width-10, 10, kRightJustify, buf ); |
unlockCVPixelBufferAndReleaseCGBitmapContext( pixelBuffer, context ); |
} |
} |
// Utility to wrap a CGImage around a 32ARGB CVPixelBuffer. |
static void releaseAndUnlockThis( void *info, const void *data, size_t size ) |
{ |
CVPixelBufferRef pixelBuffer = info; |
CVPixelBufferUnlockBaseAddress( pixelBuffer, 0 ); |
CVBufferRelease( pixelBuffer ); |
} |
static CGImageRef createCGImageFrom32XRGBCVPixelBuffer( CVPixelBufferRef pixelBuffer ) |
{ |
size_t width, height, rowBytes; |
void *baseAddr = NULL; |
CGColorSpaceRef colorspace = NULL; |
CGDataProviderRef provider = NULL; |
CGImageRef image = NULL; |
CVPixelBufferLockBaseAddress( pixelBuffer, 0 ); |
rowBytes = CVPixelBufferGetBytesPerRow( pixelBuffer ); |
baseAddr = CVPixelBufferGetBaseAddress( pixelBuffer ); |
width = CVPixelBufferGetWidth( pixelBuffer ); |
height = CVPixelBufferGetHeight( pixelBuffer ); |
colorspace = CGColorSpaceCreateDeviceRGB(); |
CVBufferRetain( pixelBuffer ); |
provider = CGDataProviderCreateWithData( pixelBuffer, baseAddr, rowBytes * height, releaseAndUnlockThis ); |
image = CGImageCreate( width, height, 8, 32, rowBytes, colorspace, |
kCGImageAlphaNoneSkipFirst, provider, NULL, true, kCGRenderingIntentDefault ); |
bail: |
if( provider ) CGDataProviderRelease( provider ); |
if( colorspace ) CGColorSpaceRelease( colorspace ); |
return image; |
} |
// Replace the image in an HIImageView and force it to be displayed immediately. |
static void putCGImageInHIImageView( CGImageRef image, HIViewRef imageView ) |
{ |
OSStatus err; |
if( imageView ) { |
// Pass our view the new image |
err = HIImageViewSetImage( imageView, image ); |
err = HIViewSetNeedsDisplay( imageView, true ); |
err = HIViewRender( imageView ); |
err = HIWindowFlush( HIViewGetWindow( imageView ) ); |
} |
} |
// The tracking callback function for the decompression session. |
// Draw on pixel buffers, display them in the window and feed them to the compression session. |
static void |
displayAndCompressFrame( |
void *decompressionTrackingRefCon, |
OSStatus result, |
ICMDecompressionTrackingFlags decompressionTrackingFlags, |
CVPixelBufferRef pixelBuffer, |
TimeValue64 displayTime, |
TimeValue64 displayDuration, |
ICMValidTimeFlags validTimeFlags, |
void *reserved, |
void *sourceFrameRefCon ) |
{ |
OSStatus err = noErr; |
MungDataPtr pMungData = (MungDataPtr)decompressionTrackingRefCon; |
if( kICMDecompressionTracking_ReleaseSourceData & decompressionTrackingFlags ) { |
// if we were responsible for managing source data buffers, we should release the source buffer here, |
// using sourceFrameRefCon to identify it. |
} |
if( ( kICMDecompressionTracking_EmittingFrame & decompressionTrackingFlags ) && pixelBuffer ) { |
ICMCompressionFrameOptionsRef frameOptions = NULL; |
CGImageRef image = NULL; |
// Draw in the corner of the pixelBuffer. |
drawOnPixelBuffer( pixelBuffer, displayTime, pMungData->timeScale ); |
// Display the frame in the imageView. |
image = createCGImageFrom32XRGBCVPixelBuffer( pixelBuffer ); |
putCGImageInHIImageView( image, pMungData->imageView ); |
if( image ) CFRelease( image ); |
// Feed the frame to the compression session. |
err = ICMCompressionSessionEncodeFrame( pMungData->compressionSession, pixelBuffer, |
displayTime, displayDuration, validTimeFlags, |
frameOptions, NULL, NULL ); |
if( err ) { |
fprintf( stderr, "ICMCompressionSessionEncodeFrame() failed (%d)\n", err ); |
} |
} |
} |
// Create a video track and media to hold encoded frames. |
// This is called the first time we get an encoded frame back from the compression session. |
static OSStatus |
createVideoMedia( |
MungDataPtr pMungData, |
ImageDescriptionHandle imageDesc, |
TimeScale timescale ) |
{ |
OSStatus err = noErr; |
Fixed trackWidth, trackHeight; |
Track outputTrack = NULL; |
err = ICMImageDescriptionGetProperty( |
imageDesc, |
kQTPropertyClass_ImageDescription, |
kICMImageDescriptionPropertyID_ClassicTrackWidth, |
sizeof( trackWidth ), |
&trackWidth, |
NULL ); |
if( err ) { |
fprintf( stderr, "ICMImageDescriptionGetProperty(kICMImageDescriptionPropertyID_DisplayWidth) failed (%d)\n", err ); |
goto bail; |
} |
err = ICMImageDescriptionGetProperty( |
imageDesc, |
kQTPropertyClass_ImageDescription, |
kICMImageDescriptionPropertyID_ClassicTrackHeight, |
sizeof( trackHeight ), |
&trackHeight, |
NULL ); |
if( err ) { |
fprintf( stderr, "ICMImageDescriptionGetProperty(kICMImageDescriptionPropertyID_DisplayHeight) failed (%d)\n", err ); |
goto bail; |
} |
if( pMungData->verbose ) { |
printf( "creating %g x %g track\n", Fix2X(trackWidth), Fix2X(trackHeight) ); |
} |
outputTrack = NewMovieTrack( pMungData->outputMovie, trackWidth, trackHeight, 0 ); |
err = GetMoviesError(); |
if( err ) { |
fprintf( stderr, "NewMovieTrack() failed (%d)\n", err ); |
goto bail; |
} |
pMungData->outputVideoMedia = NewTrackMedia( outputTrack, VideoMediaType, timescale, 0, 0 ); |
err = GetMoviesError(); |
if( err ) { |
fprintf( stderr, "NewTrackMedia() failed (%d)\n", err ); |
goto bail; |
} |
err = BeginMediaEdits( pMungData->outputVideoMedia ); |
if( err ) { |
fprintf( stderr, "BeginMediaEdits() failed (%d)\n", err ); |
goto bail; |
} |
pMungData->didBeginVideoMediaEdits = true; |
bail: |
return err; |
} |
// This is the tracking callback function for the compression session. |
// Write the encoded frame to the movie file. |
// Note that this function adds each sample separately; better chunking can be achieved |
// by flattening the movie after it is finished, or by grouping samples, writing them in |
// groups to the data reference manually, and using AddSampleTableToMedia. |
static OSStatus |
writeEncodedFrameToMovie( void *encodedFrameOutputRefCon, |
ICMCompressionSessionRef session, |
OSStatus err, |
ICMEncodedFrameRef encodedFrame, |
void *reserved ) |
{ |
MungDataPtr pMungData = encodedFrameOutputRefCon; |
ImageDescriptionHandle imageDesc = NULL; |
TimeValue64 decodeDuration; |
if( err ) { |
fprintf( stderr, "writeEncodedFrameToMovie received an error (%d)\n", err ); |
goto bail; |
} |
err = ICMEncodedFrameGetImageDescription( encodedFrame, &imageDesc ); |
if( err ) { |
fprintf( stderr, "ICMEncodedFrameGetImageDescription() failed (%d)\n", err ); |
goto bail; |
} |
if( ! pMungData->outputVideoMedia ) { |
err = createVideoMedia( pMungData, imageDesc, ICMEncodedFrameGetTimeScale( encodedFrame ) ); |
if( err ) |
goto bail; |
} |
decodeDuration = ICMEncodedFrameGetDecodeDuration( encodedFrame ); |
if( decodeDuration == 0 ) { |
// You can't add zero-duration samples to a media. If you try you'll just get invalidDuration back. |
// Because we don't tell the ICM what the source frame durations are, |
// the ICM calculates frame durations using the gaps between timestamps. |
// It can't do that for the final frame because it doesn't know the "next timestamp" |
// (because in this example we don't pass a "final timestamp" to ICMCompressionSessionCompleteFrames). |
// So we'll give the final frame our minimum frame duration. |
decodeDuration = pMungData->minimumFrameDuration * ICMEncodedFrameGetTimeScale( encodedFrame ) / pMungData->timeScale; |
} |
if( pMungData->verbose ) { |
printf( "adding %ld byte sample: decode duration %ld, display offset %ld, flags %#lx", |
(long)ICMEncodedFrameGetDataSize( encodedFrame ), |
(long)decodeDuration, |
(long)ICMEncodedFrameGetDisplayOffset( encodedFrame ), |
(long)ICMEncodedFrameGetMediaSampleFlags( encodedFrame ) ); |
if( true ) { |
ICMValidTimeFlags validTimeFlags = ICMEncodedFrameGetValidTimeFlags( encodedFrame ); |
if( kICMValidTime_DecodeTimeStampIsValid & validTimeFlags ) |
printf( ", decode time stamp %ld", (long)ICMEncodedFrameGetDecodeTimeStamp( encodedFrame ) ); |
if( kICMValidTime_DisplayTimeStampIsValid & validTimeFlags ) |
printf( ", display time stamp %ld", (long)ICMEncodedFrameGetDisplayTimeStamp( encodedFrame ) ); |
} |
printf( "\n" ); |
} |
err = AddMediaSample2( |
pMungData->outputVideoMedia, |
ICMEncodedFrameGetDataPtr( encodedFrame ), |
ICMEncodedFrameGetDataSize( encodedFrame ), |
decodeDuration, |
ICMEncodedFrameGetDisplayOffset( encodedFrame ), |
(SampleDescriptionHandle)imageDesc, |
1, |
ICMEncodedFrameGetMediaSampleFlags( encodedFrame ), |
NULL ); |
if( err ) { |
fprintf( stderr, "AddMediaSample2() failed (%d)\n", err ); |
goto bail; |
} |
// Note: if you don't need to intercept any values, you could equivalently call: |
// err = AddMediaSampleFromEncodedFrame( pMungData->outputVideoMedia, encodedFrame, NULL ); |
// if( err ) { |
// fprintf( stderr, "AddMediaSampleFromEncodedFrame() failed (%d)\n", err ); |
// goto bail; |
// } |
bail: |
return err; |
} |
static OSStatus |
writeAudioToMovie(MungDataPtr pMungData, UInt8 *p, long len, long *offset, TimeValue time, long chRefCon) |
{ |
OSStatus err = noErr; |
UInt32 numSamples = 0; |
if (!pMungData) |
return -1; |
if (pMungData->soundMedia == NULL) |
{ |
Track t; |
// Get the timescale |
err = SGGetChannelTimeScale(pMungData->audioChan, &pMungData->audioTimeScale); |
if ( err ) { |
fprintf( stderr, "SGGetChannelTimeScale(audioChan) failed (%d)\n", err ); |
goto bail; |
} |
// create the sound track |
t = NewMovieTrack(pMungData->outputMovie, 0, 0, kFullVolume); |
err = GetMoviesError(); |
if ( err ) { |
fprintf( stderr, "NewMovieTrack(SoundMediaType) failed (%d)\n", err ); |
goto bail; |
} |
pMungData->soundMedia = NewTrackMedia(t, SoundMediaType, pMungData->audioTimeScale, NULL, 0); |
err = GetMoviesError(); |
if ( err ) { |
fprintf(stderr, "NewTrackMedia(SoundMediaType) failed (%d)\n", err ); |
goto bail; |
} |
err = BeginMediaEdits( pMungData->soundMedia ); |
if( err ) { |
fprintf( stderr, "BeginMediaEdits(soundMedia) failed (%d)\n", err ); |
goto bail; |
} |
pMungData->didBeginSoundMediaEdits = true; |
// cache the sound sample description |
err = QTGetComponentProperty(pMungData->audioChan, kQTPropertyClass_SGAudio, |
kQTSGAudioPropertyID_SoundDescription, sizeof(pMungData->audioDescH), |
&pMungData->audioDescH, NULL); |
if ( err ) { |
fprintf( stderr, "QTGetComponentProperty(kQTSGAudioPropertyID_SoundDescription) failed (%d)\n", err ); |
goto bail; |
} |
// and get the AudioStreamBasicDescription equivalent (see CoreAudioTypes.h for more info on this struct) |
err = QTSoundDescriptionGetProperty(pMungData->audioDescH, kQTPropertyClass_SoundDescription, |
kQTSoundDescriptionPropertyID_AudioStreamBasicDescription, |
sizeof(pMungData->asbd), &pMungData->asbd, NULL); |
if ( err ) { |
fprintf( stderr, "QTSoundDescriptionGetProperty(ASBD) failed (%d)\n", err ); |
goto bail; |
} |
} |
// Handle CBR and VBR audio. For VBR formats (such as AAC), an array of |
// AudioStreamPacketDescriptions accompanies each blob of audio packets (you'll find them |
// wrapped in a CFDataRef in the sgdataproc's chRefCon parameter). If you are writing |
// VBR data, you must call AddMediaSample2 for each AudioStreamPacketDescription. |
if ( pMungData->asbd.mBytesPerPacket ) |
{ |
numSamples = (len / pMungData->asbd.mBytesPerPacket) * pMungData->asbd.mFramesPerPacket; |
err = AddMediaSample2( pMungData->soundMedia, // the Media |
p, // const UInt8 * dataIn |
len, // ByteCount size |
1, // TimeValue64 decodeDurationPerSample |
0, // TimeValue64 displayOffset |
(SampleDescriptionHandle)pMungData->audioDescH, // SampleDescriptionHandle sampleDescriptionH |
numSamples, // ItemCount numberOfSamples |
0, // MediaSampleFlags sampleFlags |
NULL ); // TimeValue64 * sampleDecodeTimeOut |
if( err ) { |
fprintf( stderr, "AddMediaSample2(soundMedia) failed (%d)\n", err ); |
goto bail; |
} |
} |
else { |
AudioStreamPacketDescription * aspd = (chRefCon ? (AudioStreamPacketDescription*)CFDataGetBytePtr((CFDataRef)chRefCon) : NULL); |
numSamples = CFDataGetLength((CFDataRef)chRefCon) / sizeof(AudioStreamPacketDescription); |
UInt32 i; |
for (i = 0; i < numSamples; i++) |
{ |
err = AddMediaSample2( pMungData->soundMedia, |
p + aspd[i].mStartOffset, |
aspd[i].mDataByteSize, |
pMungData->asbd.mFramesPerPacket, |
0, |
(SampleDescriptionHandle)pMungData->audioDescH, |
1, |
0, |
NULL ); |
if ( err ) { |
fprintf( stderr, "AddMediaSample2(soundMedia) #%lu of %lu failed (%d)\n", i, numSamples, err ); |
goto bail; |
} |
} |
if (chRefCon) |
CFRelease((CFDataRef)chRefCon); |
} |
bail: |
return err; |
} |
/* ---------------------------------------------------------------------- */ |
/* sequence grabber data procedure - this is where the work is done |
/* ---------------------------------------------------------------------- */ |
/* mungGrabDataProc - the sequence grabber calls the data function whenever |
any of the grabber’s channels write digitized data to the destination movie file. |
NOTE: We really mean any, if you have an audio and video channel then the DataProc will |
be called for either channel whenever data has been captured. Be sure to check which |
channel is being passed in. |
This data function makes sure its compression and decompression sessions are set up, |
and then feeds the frame to the decompression session. |
For more information refer to Inside Macintosh: QuickTime Components, page 5-120 |
c - the channel component that is writing the digitized data. |
p - a pointer to the digitized data. |
len - the number of bytes of digitized data. |
offset - a pointer to a field that may specify where you are to write the digitized data, |
and that is to receive a value indicating where you wrote the data. |
chRefCon - per channel reference constant specified using SGSetChannelRefCon. |
time - the starting time of the data, in the channel’s time scale. |
writeType - the type of write operation being performed. |
seqGrabWriteAppend - Append new data. |
seqGrabWriteReserve - Do not write data. Instead, reserve space for the amount of data |
specified in the len parameter. |
seqGrabWriteFill - Write data into the location specified by offset. Used to fill the space |
previously reserved with seqGrabWriteReserve. The Sequence Grabber may |
call the DataProc several times to fill a single reserved location. |
refCon - the reference constant you specified when you assigned your data function to the sequence grabber. |
*/ |
pascal OSErr mungGrabDataProc(SGChannel c, Ptr p, long len, long *offset, long chRefCon, TimeValue time, short writeType, long refCon); |
pascal OSErr mungGrabDataProc(SGChannel c, Ptr p, long len, long *offset, long chRefCon, TimeValue time, short writeType, long refCon) |
{ |
#pragma unused(offset,chRefCon,writeType) |
ICMFrameTimeRecord frameTime = {{0}}; |
ComponentResult err = noErr; |
int n; |
MungDataPtr pMungData = (MungDataPtr)refCon; |
if (NULL == pMungData) return -1; |
if (c == pMungData->audioChan) { |
// Handle audio in this other function: |
return (writeAudioToMovie(pMungData, (UInt8 *)p, len, offset, time, chRefCon)); |
} |
// reset frame and time counters after a stop/start |
if (pMungData->lastTime > time) { |
pMungData->lastTime = 0; |
pMungData->frameCount = 0; |
} |
if (pMungData->timeScale == 0) { |
// first time here so set the time scale |
err = SGGetChannelTimeScale(c, &pMungData->timeScale); |
DisplayAndBail(err, "SGGetChannelTimeScale"); |
// Work out how much to throttle frame times |
pMungData->minimumFrameDuration = pMungData->timeScale / pMungData->desiredFramesPerSecond; |
} |
// round times to a multiple of the frame rate |
n = floor( ( ((float)time) * pMungData->desiredFramesPerSecond / pMungData->timeScale ) + 0.5 ); |
time = n * pMungData->timeScale / pMungData->desiredFramesPerSecond; |
if( ( pMungData->lastTime > 0 ) && ( time < pMungData->lastTime + pMungData->minimumFrameDuration ) ) { |
// drop this frame. |
err = noErr; |
goto bail; |
} |
pMungData->frameCount++; |
if (pMungData->compressionSession == NULL) { |
// Set up a compression session that will compress each frame and call |
// writeEncodedFrameToMovie with each output frame. |
err = createCompressionSession( |
pMungData->width, pMungData->height, pMungData->codecType, |
pMungData->averageDataRate, pMungData->timeScale, |
writeEncodedFrameToMovie, pMungData, |
&pMungData->compressionSession ); |
DisplayAndBail(err, "createCompressionSession"); |
} |
if (pMungData->decompressionSession == NULL) { |
// Set up decompression session |
ImageDescriptionHandle imageDesc = (ImageDescriptionHandle)NewHandle(0); |
// retrieve a channel’s current sample description, the channel returns a sample description that is |
// appropriate to the type of data being captured |
err = SGGetChannelSampleDescription(c, (Handle)imageDesc); |
DisplayAndBail(err, "SGGetChannelSampleDescription"); |
// Set up a decompression session that will call displayAndCompressFrame with each decoded frame. |
// Note: if we didn't need to draw on frames with CG or display the frames in an HIImageView, we |
// could improve performance by getting the compression session's preferred pixel buffer attributes |
// via the kICMCompressionSessionPropertyID_CompressorPixelBufferAttributes property |
// and passing that when creating the decompression session. |
err = createDecompressionSession( imageDesc, |
pMungData->width, pMungData->height, k32ARGBPixelFormat, |
displayAndCompressFrame, pMungData, |
&pMungData->decompressionSession ); |
DisplayAndBail(err, "createDecompressionSession"); |
DisposeHandle((Handle)imageDesc); |
} |
frameTime.recordSize = sizeof(ICMFrameTimeRecord); |
*(TimeValue64 *)&frameTime.value = time; |
frameTime.scale = pMungData->timeScale; |
frameTime.rate = fixed1; |
frameTime.frameNumber = pMungData->frameCount; |
frameTime.flags = icmFrameTimeIsNonScheduledDisplayTime; |
// Push encoded frame in. |
err = ICMDecompressionSessionDecodeFrame( pMungData->decompressionSession, |
(UInt8 *)p, len, NULL, &frameTime, pMungData ); |
DisplayAndBail(err, "ICMDecompressionSessionDecodeFrame"); |
// Pull decoded frame out. |
ICMDecompressionSessionSetNonScheduledDisplayTime( pMungData->decompressionSession, time, pMungData->timeScale, 0 ); |
pMungData->lastTime = time; |
bail: |
return err; |
} |
// Munggrab idle timer to idle the sequence grabber, call this at least |
// as much as the desired frame rate - more is better |
static pascal void mgIdleTimer(EventLoopTimerRef inTimer, void *inUserData) |
{ |
#pragma unused(inTimer) |
MungDataPtr pMungData = (MungDataPtr)inUserData; |
if (NULL == pMungData) return; |
OSErr err = SGIdle(pMungData->seqGrab); |
if (err && err != pMungData->err) { |
// some error specific to SGIdle occurred - any errors returned from the |
// data proc will also show up here and we don't want to write over them |
// in QT 4 you would always encounter a cDepthErr error after a user drags |
// the window, this failure condition has been greatly relaxed in QT 5 |
// it may still occur but should only apply to vDigs that really control |
// the screen |
// you don't always know where these errors originate from, some may come |
// from the VDig... |
displayError(pMungData->pWindow, "SGIdle", err); |
// ...to fix this we simply call SGStop and SGStartRecord again |
// calling stop allows the SG to release and re-prepare for grabbing |
// hopefully fixing any problems, this is obviously a very relaxed |
// approach |
SGStop(pMungData->seqGrab); |
SGStartRecord(pMungData->seqGrab); |
} |
} |
static void disposeCaptureWindow( WindowRef window ) |
{ |
MungDataPtr pMungData = (MungDataPtr)GetWRefCon( window ); |
DisposeWindow(window); |
disposeMungData(pMungData); |
} |
static pascal OSStatus mgWindowEventHandler(EventHandlerCallRef nextHandler, EventRef theEvent, void *inUserData) |
{ |
#pragma unused(nextHandler) |
OSStatus status = eventNotHandledErr; |
UInt32 eventKind, eventClass; |
MungDataPtr pMungData = (MungDataPtr)inUserData; |
if (NULL == pMungData) goto done; |
eventKind = GetEventKind(theEvent); |
eventClass = GetEventClass(theEvent); |
switch (eventClass) { |
case kEventClassKeyboard: |
{ |
switch (eventKind) { |
case kEventRawKeyDown: |
case kEventRawKeyRepeat: |
{ |
// adjust the audio preview volume if it's the up or down arrow key |
char macKeyCode; |
OSErr err; |
Float32 volume; |
err = GetEventParameter(theEvent, kEventParamKeyMacCharCodes, typeChar, |
NULL, sizeof(macKeyCode), NULL, &macKeyCode); |
if (err) goto done; |
switch (macKeyCode) { |
case 31: // down arrow key |
if (noErr == QTGetComponentProperty( pMungData->audioChan, |
kQTPropertyClass_SGAudioPreviewDevice, |
kQTSGAudioPropertyID_MasterGain, |
sizeof(volume), &volume, NULL ) ) |
{ |
if (volume > 0.) |
{ |
volume -= .1; |
// pin it to zero |
if (volume < 0.) |
volume = 0.; |
(void)QTSetComponentProperty( pMungData->audioChan, |
kQTPropertyClass_SGAudioPreviewDevice, |
kQTSGAudioPropertyID_MasterGain, |
sizeof(volume), &volume ); |
} |
status = noErr; |
} |
break; |
case 30: // up arrow key |
if (noErr == QTGetComponentProperty( pMungData->audioChan, |
kQTPropertyClass_SGAudioPreviewDevice, |
kQTSGAudioPropertyID_MasterGain, |
sizeof(volume), &volume, NULL ) ) |
{ |
// don't pin the volume to an upper limit of 1.0. The volume |
// level is being set in software on a mixer, so it can be |
// set to levels greater than 1.0, in effect boosting low |
// signals. |
volume += .1; |
(void)QTSetComponentProperty( pMungData->audioChan, |
kQTPropertyClass_SGAudioPreviewDevice, |
kQTSGAudioPropertyID_MasterGain, |
sizeof(volume), &volume ); |
status = noErr; |
} |
break; |
} |
} |
break; |
} |
} |
break; |
case kEventClassWindow: |
{ |
switch (eventKind) { |
case kEventWindowClose: |
{ |
// we're done |
WindowRef theWindow; |
// we need the window ref or bail |
OSErr err; |
err = GetEventParameter(theEvent, kEventParamDirectObject, typeWindowRef, NULL, sizeof(theWindow), NULL, &theWindow); |
if (err) goto done; |
RemoveEventLoopTimer(pMungData->timerRef); |
disposeCaptureWindow( theWindow ); // this also finishes creating the output movie |
QuitApplicationEventLoop(); |
status = noErr; |
break; |
} |
break; |
} |
} |
break; |
default: |
break; |
} |
done: |
return status; |
} |
pascal OSErr handleQuitAE(const AppleEvent *theAppleEvent, AppleEvent *reply, long inRefcon); |
pascal OSErr handleQuitAE(const AppleEvent *theAppleEvent, AppleEvent *reply, long inRefcon) |
{ |
#pragma unused (theAppleEvent, reply) |
MungDataPtr pMungData = (MungDataPtr)inRefcon; |
if (pMungData) { |
RemoveEventLoopTimer(pMungData->timerRef); |
disposeCaptureWindow( pMungData->pWindow ); // this also finishes creating the output movie |
} |
QuitApplicationEventLoop(); |
return noErr; |
} |
static OSErr installEventHandlers(MungDataPtr inMungData) |
{ |
OSStatus err = eventInternalErr; |
const EventTypeSpec windowEventList[] = { { kEventClassWindow, kEventWindowClose }, |
{ kEventClassKeyboard, kEventRawKeyDown }, |
{ kEventClassKeyboard, kEventRawKeyRepeat } }; |
err = InstallEventLoopTimer(GetMainEventLoop(), kEventDurationNoWait, kTimerInterval, NewEventLoopTimerUPP(mgIdleTimer), inMungData, &inMungData->timerRef); |
BailErr(err); |
err = InstallWindowEventHandler(inMungData->pWindow, |
NewEventHandlerUPP(mgWindowEventHandler), |
GetEventTypeCount(windowEventList), |
windowEventList, inMungData, NULL); |
BailErr(err); |
err = AEInstallEventHandler(kCoreEventClass, kAEQuitApplication, NewAEEventHandlerUPP(handleQuitAE), (long)inMungData, false); |
bail: |
return err; |
} |
static OSStatus startCaptureUsingWindow( WindowRef window ) |
{ |
OSStatus err = noErr; |
Rect portRect = { 0, 0, 240, 320 }; |
MungDataPtr pMungData = NULL; |
SeqGrabComponent seqGrab = 0; |
SGChannel sgchanVideo = NULL, sgchanAudio = NULL; |
GetPortBounds(GetWindowPort(window), &portRect); |
// create and initialize the sequence grabber |
seqGrab = makeSequenceGrabber( window ); |
BailErr(NULL == seqGrab); |
// create the video channel |
err = makeSequenceGrabVideoChannel( seqGrab, &sgchanVideo, &portRect ); |
BailErr(err); |
// create the audio channel - disregarding the error. We'll |
// carry on whether or not there's an audio recording device present |
if (noErr != makeSequenceGrabAudioChannel( seqGrab, &sgchanAudio ) ) |
sgchanAudio = NULL; |
// initialize our data that's going to be passed around |
pMungData = initializeMungData( window, portRect, seqGrab ); |
BailErr(NULL == pMungData); |
SetWRefCon( window, (long)pMungData ); |
pMungData->audioChan = sgchanAudio; |
// specify a sequence grabber data function |
err = SGSetDataProc( seqGrab, NewSGDataUPP(mungGrabDataProc), (long)pMungData ); |
BailErr(err); |
// install carbon event handlers |
err = installEventHandlers( pMungData ); |
BailErr(err); |
// lights...camera... |
err = SGPrepare( seqGrab, false, true ); |
BailErr(err); |
// ...action |
err = SGStartRecord( seqGrab ); |
BailErr(err); |
pMungData->isGrabbing = true; |
bail: |
return err; |
} |
int main(int argc, char* argv[]) |
{ |
IBNibRef nibRef = NULL; |
WindowRef window = NULL; |
OSStatus err = noErr; |
EnterMovies(); |
// Create a Nib reference passing the name of the nib file (without the .nib extension) |
// CreateNibReference only searches into the application bundle. |
err = CreateNibReference( CFSTR("main"), &nibRef ); |
require_noerr( err, CantGetNibRef ); |
// Once the nib reference is created, set the menu bar. "MainMenu" is the name of the menu bar |
// object. This name is set in InterfaceBuilder when the nib is created. |
err = SetMenuBarFromNib( nibRef, CFSTR("MenuBar") ); |
require_noerr( err, CantSetMenuBar ); |
// Then create a window. "MainWindow" is the name of the window object. This name is set in |
// InterfaceBuilder when the nib is created. |
err = CreateWindowFromNib( nibRef, CFSTR("MainWindow"), &window ); |
require_noerr( err, CantCreateWindow ); |
// We don't need the nib reference anymore. |
DisposeNibReference( nibRef ); |
err = startCaptureUsingWindow( window ); |
if( err ) { |
DialogRef alert = NULL; |
DialogItemIndex ignored; |
CFStringRef explanation = CFStringCreateWithFormat( NULL, NULL, CFSTR("Error code %d."), (int)err ); |
CreateStandardAlert( kAlertStopAlert, |
CFSTR("Could not set up capture. Make sure you have a camera attached and turned on."), |
explanation, NULL, &alert ); |
RunStandardAlert( alert, NULL, &ignored ); |
CFRelease( explanation ); |
goto CantStartCapture; |
} |
// The window was created hidden so show it. |
ShowWindow( window ); |
// Call the event loop |
RunApplicationEventLoop(); |
CantStartCapture: |
CantCreateWindow: |
CantSetMenuBar: |
CantGetNibRef: |
exit(0); |
} |
Copyright © 2007 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2007-08-14