This chapter describes how to build sequence grabber channel components, also known simply as channel components. These components are used by higher-level sequence grabber components, and act to isolate the sequence grabber from the details of working with actual data types. Channel components may, in turn, depend on the services of still lower-level components, such as video digitizer components.
For example, a sequence grabber component may provide both audio and video to an application. It may receive the audio and video data from two channel components: an audio channel component and a video channel component. The video channel component may receive its data from a video digitizer component that is specific to a particular manufacturer’s video capture board.
You should read this chapter if you are developing a sequence grabber component, a channel component, and/or a video digitizer component. Application programmers should use the services of a sequence grabber component, and should not need to read this material.
Creating Sequence Grabber Channel Components
A Sample Sequence Grabber Channel Component
Support for Sound Capture at Any Sample Rate
Channel Source Names
Capturing to Multiple Files
Sequence grabber channel components are the most convenient mechanism for extending the ability of the sequence grabber component to accommodate new types of source data. For example, if you are developing special-purpose hardware that generates a new kind of data, you should create a channel component for that kind of data.
This section discusses issues you should consider when creating a sequence grabber channel component. It also provides a sample program for the implementation of a sequence grabber channel component.
Apple has defined a component type value for sequence grabber channel components; that type value is 'sgch'
. You can use the following constant to specify this type value:
#define SeqGrabChannelType 'sgch'; |
Sequence grabber channel components use their component subtype value to indicate the media type created by the component. For example, a channel component that works with video data would have a subtype of 'vide'
(this value is defined by the Movie Toolbox’s VideoMediaType
constant).
At a minimum, your channel component should support the following functions:
In addition, if your channel component supports visual data, it should support at least the following functions:
If your channel component supports audio data, it should support the following functions as well:
Other functions mentioned in this chapter are optional. However, your channel component should support as many of these functions as possible, so that your component is more useful to applications and users.
As with all components, your channel component receives its requests from the Component Manager in the form of request codes. Apple strongly recommends that you fully support all of the Component Manager’s request codes in your channel component, especially the target request. Developers will want to extend the capabilities of the sequence grabber channel components. The Component Manager’s CaptureComponent
function, which uses the target request, is the most convenient mechanism for obtaining the services of a component and then extending those services. If your channel component does not support the target request, then it cannot be used by applications or other components in this manner. You can use the following constants to refer to the request codes for each of the functions that your channel component must support.
/* basic sequence grabber channel component selectors */ |
kSGSetGWorldSelect = 0x4; |
kSGStartPreviewSelect = 0x10; |
kSGStartRecordSelect = 0x11; |
kSGIdleSelect = 0x12; |
kSGStopSelect = 0x13; |
kSGPauseSelect = 0x14; |
kSGPrepareSelect = 0x15; |
kSGReleaseSelect = 0x16; |
kSGUpdateSelect = 0x27; |
/* selectors for common channel configuration functions */ |
kSGCSetChannelUsageSelect = 0x80; |
kSGCGetChannelUsageSelect = 0x81; |
kSGCSetChannelBoundsSelect = 0x82; |
kSGCGetChannelBoundsSelect = 0x83; |
kSGCSetChannelVolumeSelect = 0x84; |
kSGCGetChannelVolumeSelect = 0x85; |
kSGCGetChannelInfoSelect = 0x86; |
kSGCSetChannelPlayFlagsSelect = 0x87; |
kSGCGetChannelPlayFlagsSelect = 0x88; |
kSGCSetChannelMaxFramesSelect = 0x89; |
kSGCGetChannelMaxFramesSelect = 0x8a; |
kSGCSetChannelRefConSelect = 0x8b; |
kSGCSetChannelClipSelect = 0x8C; |
kSGCGetChannelClipSelect = 0x8D; |
kSGCGetChannelSampleDescriptionSelect = 0x8E; |
kSGCGetChannelDeviceListSelect = 0x8F; |
kSGCSetChannelDeviceSelect = 0x90; |
kSGCSetChannelMatrixSelect = 0x91; |
kSGCGetChannelMatrixSelect = 0x92; |
kSGCGetChannelTimeScaleSelect = 0x93; |
/* selectors for video channel configuration functions */ |
kSGCGetSrcVideoBoundsSelect = 0x100; |
kSGCSetVideoRectSelect = 0x101; |
kSGCGetVideoRectSelect = 0x102; |
kSGCGetVideoCompressorTypeSelect = 0x103; |
kSGCSetVideoCompressorTypeSelect = 0x104; |
kSGCSetVideoCompressorSelect = 0x105; |
kSGCGetVideoCompressorSelect = 0x106; |
kSGCGetVideoDigitizerComponentSelect = 0x107; |
kSGCSetVideoDigitizerComponentSelect = 0x108; |
kSGCVideoDigitizerChangedSelect = 0x109; |
kSGCSetVideoBottlenecksSelect = 0x10a; |
kSGCGetVideoBottlenecksSelect = 0x10b; |
kSGCGrabFrameSelect = 0x10c; |
kSGCGrabFrameCompleteSelect = 0x10d; |
kSGCDisplayFrameSelect = 0x10e; |
kSGCCompressFrameSelect = 0x10f; |
kSGCCompressFrameCompleteSelect = 0x110; |
kSGCAddFrameSelect = 0x111; |
kSGCTransferFrameForCompressSelect = 0x112; |
kSGCSetCompressBufferSelect = 0x113; |
kSGCGetCompressBufferSelect = 0x114; |
kSGCGetBufferInfoSelect = 0x115; |
kSGCSetUseScreenBufferSelect = 0x116; |
kSGCGetUseScreenBufferSelect = 0x117; |
kSGCGrabCompressCompleteSelect = 0x118; |
kSGCDisplayCompressSelect = 0x119; |
kSGCSetFrameRateSelect = 0x11A; |
kSGCGetFrameRateSelect = 0x11B; |
/* selectors for sound channel configuration functions */ |
kSGCSetSoundInputDriverSelect = 0x100; |
kSGCGetSoundInputDriverSelect = 0x101; |
kSGCSoundInputDriverChangedSelect = 0x102; |
kSGCSetSoundRecordChunkSizeSelect = 0x103; |
kSGCGetSoundRecordChunkSizeSelect = 0x104; |
kSGCSetSoundInputRateSelect = 0x105; |
kSGCGetSoundInputRateSelect = 0x106; |
kSGCSetSoundInputParametersSelect = 0x107; |
kSGCGetSoundInputParametersSelect = 0x108; |
/* selectors for channel control functions */ |
kSGCInitChannelSelect = 0x180; |
kSGCWriteSamplesSelect = 0x181; |
kSGCGetDataRateSelect = 0x182; |
kSGCAlignChannelRectSelect = 0x183; |
}; |
This section describes a sample sequence grabber channel component for PICT image data.
Listing 5-1 supplies the component dispatchers for the sequence grabber channel component together with the required functions.
Listing 5-1 Setting up global variables and implementing required functions
#define kMediaTimeScale 600 |
typedef struct { |
ComponentInstance self; |
SeqGrabComponent grabber; |
long usage; |
Boolean paused; |
CGrafPtr destPort; |
GDHandle destGD; |
CGrafPort tempPort; |
MatrixRecord displayMatrix; |
Rect destRect; |
Rect srcRect; |
RgnHandle clip; |
Boolean inPreview; |
Boolean inRecord; |
TimeBase base; |
long bytesWritten; |
Boolean showTickCount; |
long saveUsage; |
} SGPictGlobalsRecord, *SGPictGlobals; |
pascal ComponentResult SGPICTDispatcher |
(ComponentParameters *params, Handle storage) |
{ |
OSErr err = badComponentSelector; |
ComponentFunction componentProc = 0; |
switch (params->what) { |
case kComponentOpenSelect: |
componentProc = SGPictOpen; break; |
case kComponentCloseSelect: |
componentProc = SGPictClose; break; |
case kComponentCanDoSelect: |
componentProc = SGPictCanDo; break; |
case kComponentVersionSelect: |
componentProc = SGPictVersion; break; |
case kSGSetGWorldSelect: |
componentProc = SGPictSetGWorld; break; |
case kSGStartPreviewSelect: |
componentProc = SGPictStartPreview; break; |
case kSGStartRecordSelect: |
componentProc = SGPictStartRecord; break; |
case kSGIdleSelect: |
componentProc = SGPictIdle; break; |
case kSGStopSelect: |
componentProc = SGPictStop; break; |
case kSGPauseSelect: |
componentProc = SGPictPause; break; |
case kSGPrepareSelect: |
componentProc = SGPictPrepare; break; |
case kSGReleaseSelect: |
componentProc = SGPictRelease; break; |
case kSGCSetChannelUsageSelect: |
componentProc = SGPictSetChannelUsage; break; |
case kSGCGetChannelUsageSelect: |
componentProc = SGPictGetChannelUsage; break; |
case kSGCSetChannelBoundsSelect: |
componentProc = SGPictSetChannelBounds; break; |
case kSGCGetChannelBoundsSelect: |
componentProc = SGPictGetChannelBounds; break; |
case kSGCGetChannelInfoSelect: |
componentProc = SGPictGetChannelInfo; break; |
case kSGCSetChannelMatrixSelect: |
componentProc = SGPictSetChannelMatrix; break; |
case kSGCGetChannelMatrixSelect: |
componentProc = SGPictGetChannelMatrix; break; |
case kSGCSetChannelClipSelect: |
componentProc = SGPictSetChannelClip; break; |
case kSGCGetChannelClipSelect: |
componentProc = SGPictGetChannelClip; break; |
case kSGCGetChannelSampleDescriptionSelect: |
componentProc = SGPictGetChannelSampleDescription; |
break; |
case kSGCGetChannelDeviceListSelect: |
componentProc = SGPictGetChannelDeviceList; break; |
case kSGCSetChannelDeviceSelect: |
componentProc = SGPictSetChannelDevice; break; |
case kSGCGetChannelTimeScaleSelect: |
componentProc = SGPictGetChannelTimeScale; break; |
case kSGCInitChannelSelect: |
componentProc = SGPictInitChannel; break; |
case kSGCWriteSamplesSelect: |
componentProc = SGPictWriteSamples; break; |
case kSGCGetDataRateSelect: |
componentProc = SGPictGetDataRate; break; |
case kSGCPanelGetDitlSelect: |
componentProc = SGPictPanelGetDitl; break; |
case kSGCPanelInstallSelect: |
componentProc = SGPictPanelInstall; break; |
case kSGCPanelEventSelect: |
componentProc = SGPictPanelEvent; break; |
case kSGCPanelRemoveSelect: |
componentProc = SGPictPanelRemove; break; |
case kSGCPanelGetSettingsSelect: |
componentProc = SGPictPanelGetSettings; break; |
case kSGCPanelSetSettingsSelect: |
componentProc = SGPictPanelSetSettings; break; |
case 0x0100: |
componentProc = SGPictSetShowTickCount; break; |
case 0x0101: |
componentProc = SGPictGetShowTickCount; break; |
} |
if (componentProc) |
err = CallComponentFunctionWithStorage (storage, params, |
componentProc); |
return err; |
} |
pascal ComponentResult SGPictCanDo (SGPictGlobals store, |
short ftnNumber) |
{ |
switch (ftnNumber) { |
case kComponentOpenSelect: |
case kComponentCloseSelect: |
case kComponentCanDoSelect: |
case kComponentVersionSelect: |
case kSGSetGWorldSelect: |
case kSGStartPreviewSelect: |
case kSGStartRecordSelect: |
case kSGIdleSelect: |
case kSGStopSelect: |
case kSGPauseSelect: |
case kSGPrepareSelect: |
case kSGReleaseSelect: |
case kSGCSetChannelUsageSelect: |
case kSGCGetChannelUsageSelect: |
case kSGCSetChannelBoundsSelect: |
case kSGCGetChannelBoundsSelect: |
case kSGCGetChannelInfoSelect: |
case kSGCSetChannelMatrixSelect: |
case kSGCGetChannelMatrixSelect: |
case kSGCSetChannelClipSelect: |
case kSGCGetChannelClipSelect: |
case kSGCGetChannelSampleDescriptionSelect: |
case kSGCGetChannelDeviceListSelect: |
case kSGCSetChannelDeviceSelect: |
case kSGCGetChannelTimeScaleSelect: |
case kSGCInitChannelSelect: |
case kSGCWriteSamplesSelect: |
case kSGCGetDataRateSelect: |
case kSGCPanelGetDitlSelect: |
case kSGCPanelInstallSelect: |
case kSGCPanelEventSelect: |
case kSGCPanelRemoveSelect: |
case kSGCPanelGetSettingsSelect: |
case kSGCPanelSetSettingsSelect: |
/* private component functions */ |
case 0x0100: |
case 0x0101: |
return true; |
default: |
return false; |
} |
} |
pascal ComponentResult SGPictVersion (SGPictGlobals store) |
{ |
return 0x00020001; |
} |
pascal ComponentResult SGPictOpen (SGPictGlobals store, |
ComponentInstance self) |
{ |
OSErr err; |
GrafPtr savePort; |
/* allocate global variables */ |
store = |
(SGPictGlobals)NewPtrClear(sizeof(SGPictGlobalsRecord)); |
if (err = MemError()) goto bail; |
/* create a temporary port for drawing during the idle |
function */ |
GetPort (&savePort); |
OpenCPort (&store->tempPort); |
SetPort ((GrafPtr)&store->tempPort); |
PortSize (4096, 4096); |
SetRectRgn (store->tempPort.visRgn, 0, 0, 4096, 4096); |
ClipRgn (store->tempPort.visRgn); |
SetPort (savePort); |
store->self = self; |
store->showTickCount = false; |
SetComponentInstanceStorage (self, (Handle)store); |
bail: |
return err; |
} |
pascal ComponentResult SGPictClose (SGPictGlobals store, |
ComponentInstance self) |
{ |
/* disposal operations */ |
if (store) { |
if (store->clip) DisposeRgn(store->clip); |
CloseCPort(&store->tempPort); |
DisposPtr((Ptr)store); |
} |
return noErr; |
} |
To initialize the channel component, the sequence grabber component calls the SGInitChannel
function.
The code in Listing 5-2 initializes channel variables. The grabber component calls the SGPictInitChannel
function to initialize a sequence grabber channel component. The SGPictInitChannel
function calls QuickDraw’s SetRect
routine and QuickTime’s SetIdentityMatrix
function to specify the size of the area (around a mouse-down event) in which the sequence grabber component will capture PICT images.
Listing 5-2 Initializing the sequence grabber channel component
pascal ComponentResult SGPictInitChannel (SGPictGlobals store, |
SeqGrabComponent owner) |
{ |
/* initialize any variables here */ |
SetRect(&store->srcRect, 0, 0, 160, 120);/* rectangle in which |
capture occurs */ |
SetIdentityMatrix (&store->displayMatrix); |
store->grabber = owner; |
SGGetTimeBase (owner, &store->base); |
return noErr; |
} |
Listing 5-3 supplies configuration functions that set the usage parameters and storage for the channel component. (See the descriptions of the SGSetChannelUsage
and SGGetChannelUsage
functions for details.)
The sample code illustrates how to retrieve usage information. In this case, you indicate that the sequence grabber component has spatial boundaries by using the seqGrabHasBounds
constant in the channelInfo
parameter.
Listing 5-3 Determining usage parameters and getting usage data
pascal ComponentResult SGPictSetChannelUsage(SGPictGlobals store, long usage) |
{ |
/* remember usage */ |
store->usage = usage; |
return noErr; |
} |
pascal ComponentResult SGPictGetChannelUsage(SGPictGlobals store, long *usage) |
{ |
/* return usage */ |
*usage = store->usage; |
return noErr; |
} |
pascal ComponentResult SGPictGetChannelInfo (SGPictGlobals store, |
long *channelInfo) |
{ |
/* indicate that you have spatial boundaries */ |
*channelInfo = seqGrabHasBounds; |
return noErr; |
} |
To set up an area in which the channel component displays image data, the sequence grabber should perform these tasks:
Assign the destination graphics world and graphics device for the display of the captured image with the SGSetGWorld
function.
Specify a display transformation matrix for a video channel using the SGSetChannelMatrix
function. Your function determines the matrix that is being set, validates it, and updates the matrix and destination rectangle. Your channel uses this matrix to transform its video image into the destination window.
Obtain the channel’s display transformation matrix by calling the SGGetChannelMatrix
function.
Specify the channel’s display boundary rectangle with the SGSetChannelBounds
function. The display boundary rectangle defines the destination for data from this channel and adjusts the channel matrix.
Determine the channel’s display boundary rectangle with the SGGetChannelBounds
function.
Dispose of the old clipping region and apply a new clipping region to the channel’s display region using the SGSetChannelClip
function.
Retrieve the new clipping region by calling the SGGetChannelClip
function.
The code in Listing 5-4 provides an example of how to manage the spatial characteristics of the area in which the channel component displays PICT image data.
Listing 5-4 Managing spatial characteristics
pascal ComponentResult SGPictSetGWorld (SGPictGlobals store, |
CGrafPtr gp, GDHandle gd) |
{ |
/* remember the destination graphics world */ |
store->destPort = gp; |
store->destGD = gd; |
return noErr; |
} |
pascal ComponentResult SGPictSetChannelMatrix |
(SGPictGlobals store, const MatrixRecord *m) |
{ |
OSErr err = noErr; |
MatrixRecord mat; |
short matType; |
/* determine the matrix being set */ |
if (m) |
mat = *m; |
else |
SetIdentityMatrix (&mat); |
/* validate it */ |
matType = GetMatrixType (&mat); |
if ((mat.matrix[0][0] < 0) || (mat.matrix[1][1] < 0) || |
(matType >= linearMatrixType)) |
return paramErr; |
/* update the matrix and destination rectangle */ |
store->displayMatrix = mat; |
store->destRect = store->srcRect; |
TransformRect (&mat, &store->destRect, nil); |
return err; |
} |
pascal ComponentResult SGPictGetChannelMatrix |
(SGPictGlobals store, MatrixRecord *m) |
{ |
/* return current matrix */ |
*m = store->displayMatrix; |
return noErr; |
} |
pascal ComponentResult SGPictSetChannelBounds |
(SGPictGlobals store, const Rect *bounds) |
{ |
/* remember destination rect */ |
store->destRect = *bounds; |
/* recalculate display matrix from it */ |
RectMatrix (&store->displayMatrix, &store->srcRect, |
&store->destRect); |
return noErr; |
} |
pascal ComponentResult SGPictGetChannelBounds |
(SGPictGlobals store, Rect *bounds) |
{ |
/* return current boundaries */ |
*bounds = store->destRect; |
return noErr; |
} |
pascal ComponentResult SGPictSetChannelClip (SGPictGlobals store, |
RgnHandle theClip) |
{ |
OSErr err = noErr; |
/* toss the old channel clipping */ |
if (store->clip) { |
DisposeRgn (store->clip); |
store->clip = nil; |
} |
/* and remember the new one */ |
if (theClip) { |
err = HandToHand ((Handle *)&theClip); |
store->clip = theClip; |
} |
return err; |
} |
pascal ComponentResult SGPictGetChannelClip |
(SGPictGlobals store, RgnHandle *theClip) |
{ |
OSErr err = noErr; |
/* return clip, if there is one */ |
if (*theClip = store->clip) |
err = HandToHand ((Handle *)theClip); |
return err; |
} |
To preview and record image data in the channel component, the code in Listing 5-5 implements these tasks:
The SGStartPreview
function instructs the channel to commence processing any source data. In preview mode, the component does not save any of the data it gathers from its source. Your channel component should immediately present the data to the user in the appropriate format for the channel’s configuration and display video data in the destination display region.
The SGStartRecord
function instructs the channel to begin recording data from its source. The sequence grabber component stores the collected data. The channel component should immediately begin recording data.
The SGIdle
function allows the sequence grabber component to grant processing time to the channel component. The SGIdle
function permits the processing time for the previewing and recording operations to take place. In the example shown in Listing 5-5, the work for the channel consists of getting the current time, adding data to the movie if recording, and showing the preview image if necessary.
The SGStop
function stops the channel’s preview and recording operations.
The SGPause
function suspends or restarts the channel’s preview and recording operations.
The SGPrepare
function has the sequence grabber component prepare the channel for subsequent preview or record operations.
The SGRelease
function releases any system resources that were allocated during preview or recording operations and that remain thereafter.
The code in Listing 5-5 illustrates a channel component’s control of the previewing and recording of a PICT image.
Listing 5-5 Controlling previewing and recording operations
pascal ComponentResult SGPictStartPreview (SGPictGlobals store) |
{ |
/* into preview mode */ |
store->inPreview = (store->usage & seqGrabPreview) != 0; |
return noErr; |
} |
pascal ComponentResult SGPictStartRecord (SGPictGlobals store) |
{ |
/* into record mode (also preview, if PlayDuringRecord) */ |
store->inRecord = (store->usage & seqGrabRecord) != 0; |
store->inPreview = (store->usage & seqGrabPlayDuringRecord) != |
0; |
return noErr; |
} |
pascal ComponentResult SGPictIdle (SGPictGlobals store) |
{ |
OSErr err = noErr; |
/* this is where the work for preview and record happens */ |
if (!store->paused && (store->inRecord || store->inPreview)) { |
Point mouseLoc; |
Rect r; |
PicHandle tempPict = nil; |
TimeRecord tr; |
CGrafPtr savePort; |
GDHandle saveGD; |
Rect maxR; |
GetGWorld (&savePort, &saveGD); |
/* get the current time */ |
GetTimeBaseTime (store->base, kMediaTimeScale, &tr); |
/* figure the current area around the mouse |
(only on main screen) */ |
SetGWorld (&store->tempPort, GetMainDevice()); |
GetMouse (&mouseLoc); |
LocalToGlobal (&mouseLoc); |
r.top = r.bottom = mouseLoc.v; |
r.left = r.right = mouseLoc.h; |
InsetRect(&r, -(store->srcRect.right >> 1), |
-(store->srcRect.bottom >> 1)); |
maxR = (**GetMainDevice()).gdRect; |
if (r.left < maxR.left) |
OffsetRect (&r, -r.left + maxR.left, 0); |
if (r.top < maxR.top) |
OffsetRect (&r, 0, -r.top + maxR.top); |
if (r.right > maxR.right) |
OffsetRect(&r, maxR.right - r.right, 0); |
if (r.bottom > maxR.bottom) |
OffsetRect (&r, 0, maxR.bottom - r.bottom); |
/* copy the screen into a picture */ |
tempPict = OpenPicture(&r); |
CopyBits ((BitMap *)&store->tempPort.portPixMap, |
(BitMap *)&store->tempPort.portPixMap, &r, &r, |
srcCopy, nil); |
if (store->showTickCount) { |
/* if users want to see ticks, draw them */ |
Str63 str; |
NumToString ( TickCount(), str); |
/* do some magic positioning */ |
r.right = r.left + StringWidth(str) + 4; |
r.bottom = r.top + 14; |
EraseRect (&r); |
MoveTo(r.left + 2, r.bottom - 3); |
TextSize (12); |
DrawString (str); |
} |
ClosePicture(); |
/* if recording, add data to movie */ |
if (store->inRecord) { |
long offset; |
long pictSize = GetHandleSize ((Handle)tempPict); |
HLock ((Handle)tempPict); |
err = SGAddMovieData (store->grabber, store->self, |
(Ptr)*tempPict, pictSize, &offset, 0, |
tr.value.lo, seqGrabWriteAppend); |
store->bytesWritten += pictSize; |
} |
/* if you need to show the preview image, do that */ |
if (store->inPreview) { |
RgnHandle saveClip; |
SetGWorld (store->destPort, store->destGD); |
if (store->clip) { |
saveClip = NewRgn(); |
GetClip (saveClip); |
SetClip (store->clip); |
} |
DrawPicture (tempPict, &store->destRect); |
if (store->clip) { |
SetClip (saveClip); |
DisposeRgn (saveClip); |
} |
} |
KillPicture (tempPict); |
SetGWorld (savePort, saveGD); |
} |
return err; |
} |
pascal ComponentResult SGPictStop (SGPictGlobals store) |
{ |
/* stop all previewing and recording */ |
store->inRecord = store->inPreview = false; |
return noErr; |
} |
pascal ComponentResult SGPictPause (SGPictGlobals store, |
Byte pause) |
{ |
/* pause */ |
store->paused = pause; |
return noErr; |
} |
pascal ComponentResult SGPictPrepare (SGPictGlobals store, |
Boolean prepareForPreview, |
Boolean prepareForRecord) |
{ |
/* prepare for previewing and recording operations -- |
all you do here is initialize a variable */ |
store->bytesWritten = 0; |
return noErr; |
} |
pascal ComponentResult SGPictRelease (SGPictGlobals store) |
{ |
/* no resources to release after previewing or recording */ |
return noErr; |
} |
To manage channel devices such as video digitizers or sound input drivers, you should
Let the sequence grabber retrieve a list of devices that are valid for the channel, using the SGGetChannelDeviceList
function.
Assign an appropriate channel device with the SGSetChannelDevice
function.
Listing 5-6 provides examples of these required functions for channel device management. The SGPictGetChannelDeviceList
function obtains a list of devices associated with the channel component. The SGPictSetChannelDevice
function allows the sequence grabber to specify a channel device. In this code sample, there are no devices associated with the channel component.
Listing 5-6 Coordinating devices for the channel component
pascal ComponentResult SGPictGetChannelDeviceList |
(SGPictGlobals store, |
long selectionFlags, |
SGDeviceList *list) |
{ |
*list = (SGDeviceList) NewHandleClear |
(sizeof (SGDeviceListRecord)); /* no devices */ |
return MemError(); |
} |
pascal ComponentResult SGPictSetChannelDevice |
(SGPictGlobals store, StringPtr name) |
{ |
/* you have no devices, so no problem */ |
return noErr; |
} |
To record image data, the channel component must allow the sequence grabber to do the following:
Obtain an appropriate time scale with the SGGetChannelTimeScale
function.
Retrieve the sample description of the image that is to be recorded with the SGGetChannelSampleDescription
function.
Create a track and media in which to record the sample image by calling the SGWriteSamples
function. SGWriteSamples
writes the captured data to a movie file after a record operation.
Obtain references from the sequence grabber and add them to the newly created media using the SGGetNextFrameReference
function so that the channel component can retrieve the sample references it stored.
Determine how many bytes of captured data the channel is collecting each second using the SGGetDataRate
function.
The code in Listing 5-7 shows how the channel component uses these utility functions to record PICT image data.
Listing 5-7 Recording image data
pascal ComponentResult SGPictGetChannelTimeScale |
(SGPictGlobals store, TimeScale *scale) |
{ |
*scale = kMediaTimeScale; /* a reasonable default time scale */ |
return noErr; |
} |
pascal ComponentResult SGPictGetChannelSampleDescription |
(SGPictGlobals store, Handle sampleDesc) |
{ |
OSErr err; |
SampleDescriptionPtr sdp; |
SetHandleSize (sampleDesc, sizeof(SampleDescription)); |
if (err = MemError()) goto bail; |
/* make up a minimal sample description */ |
sdp = (SampleDescriptionPtr)*sampleDesc; |
sdp->descSize = sizeof(SampleDescription); |
sdp->dataFormat = 'PICT'; |
sdp->resvd1 = 0; |
sdp->resvd2 = 0; |
sdp->dataRefIndex = 0; |
bail: |
return err; |
} |
pascal ComponentResult SGPictWriteSamples (SGPictGlobals store, |
Movie m, AliasHandle theFile) |
{ |
OSErr err = 0; |
Track pictT; |
Media pictM; |
long i; |
MatrixRecord aMatrix; |
Rect from, to; |
seqGrabFrameInfo fi; |
TimeRecord tr; |
TimeValue mediaDuration; |
SampleDescriptionHandle sampleDesc = 0; |
/* after SGStop, this function creates the track and media */ |
if (!(store->usage & seqGrabRecord)) |
return err; |
/* get the sample description */ |
sampleDesc = (SampleDescriptionHandle)NewHandle(4); |
if (err = MemError()) goto bail; |
if (err = SGGetChannelSampleDescription (store->self, |
(Handle)sampleDesc)) goto bail; |
/* figure out the track matrix */ |
SetRect (&from, 0, 0, store->srcRect.right, |
store->srcRect.bottom); |
to = from; |
TransformRect (&store->displayMatrix, &to, nil); |
/* create the track and media */ |
pictT = NewMovieTrack (m, (long)from.right << 16, |
(long)from.bottom << 16, 0); |
pictM = NewTrackMedia (pictT, 'PICT', kMediaTimeScale, |
(Handle)theFile, rAliasType); |
/* spin in a loop getting sample references from the |
sequence grabber and adding them to the media */ |
fi.frameChannel = store->self; |
i = -1; |
do { |
TimeValue frameDuration; |
err = SGGetNextFrameReference (store->grabber, |
&fi, &frameDuration, &i); |
if (err) { |
if (err == paramErr) |
err = 0; |
break; |
} |
err = AddMediaSampleReference (pictM, |
fi.frameOffset, fi.frameSize, |
frameDuration, |
sampleDesc, 1, |
0, 0); |
if (err == invalidDuration) { |
err = noErr; |
break; |
} |
} while (!err); |
done: |
if (err) goto bail; |
GetTimeBaseTime (store->base, 0, &tr); |
ConvertTimeScale (&tr, kMediaTimeScale); |
/* trim media inserted to not extend beyond end time */ |
mediaDuration = GetMediaDuration(pictM); |
/* add media to track */ |
err = InsertMediaIntoTrack (pictT, 0, 0, tr.value.lo, kFix1); |
/* set track matrix */ |
RectMatrix (&aMatrix, &from, &to); |
SetTrackMatrix (pictT, &aMatrix); |
/* set track clipping region */ |
SetTrackClipRgn (pictT, store->clip); |
bail: |
if (sampleDesc) DisposHandle ((Handle)sampleDesc); |
return err; |
} |
pascal ComponentResult SGPictGetDataRate (SGPictGlobals store, |
long *bytesPerSecond) |
{ |
/* take a guess at the data rate */ |
*bytesPerSecond = 24 * 1024; |
if (store->bytesWritten) { |
TimeValue timeNow = GetTimeBaseTime (store->base, 8, nil); |
/* one-eighth second resolution */ |
if (!timeNow) |
return seqGrabInfoNotAvailable; |
*bytesPerSecond = (store->bytesWritten / timeNow) * 8; |
/* convert back to seconds */ |
} |
return noErr; |
} |
The channel can provide media-specific functions for a particular channel type. These functions are analogous to the SGSetVideoCompressorType
and SGGetVideoCompressorType
functions. These functions allow the sequence grabber to specify and determine the type of image compression the channel component is to apply to the captured video images.
The code in Listing 5-8 provides two specialized channel component functions, SGPictSetShowTickCount
and SGPictGetShowTickCount
, which set and retrieve the tick count, respectively. Note that both the functions refer to the showTickCount
field in the SGPictGlobals
structure.
Listing 5-8 Showing the tick count
pascal ComponentResult SGPictSetShowTickCount |
(SGPictGlobals store, Boolean show) |
{ |
store->showTickCount = show; |
return noErr; |
} |
pascal ComponentResult SGPictGetShowTickCount |
(SGPictGlobals store, Boolean *show) |
{ |
*show = store->showTickCount; |
return noErr; |
} |
The channel allows the sequence grabber to manage the placement of your channel data in the sequence grabber’s settings dialog box. This is how it works:
To prepare to add the channel component’s items to the settings dialog box, the sequence grabber obtains your item list by calling the sequence grabber panel component’s SGPanelGetDITL
function. It retrieves and detaches the dialog box template from the sequence grabber panel component.
Once it has installed the items, the sequence grabber uses the SGPanelInstall
function so initial values can be set. This function resets the channel to use the dialog window and preview mode. It also updates the boundaries to match the size of the user item list.
To provide idle time in which to draw the channel’s information in the settings dialog box, the sequence grabber uses the SGPanelEvent
function. It allows the sequence grabber component to receive and process dialog events in a manner similar to a modal-dialog filter function. In this example, the information is the tick count.
Prior to the removal of items from the settings dialog box, the sequence grabber component calls the SGPanelRemove
function. The sequence grabber supplies information that specifies the channel that the panel is to configure, the dialog box, and the offset of the panel’s items into the dialog box.
The code in Listing 5-9 calls the sequence grabber panel component and indicates that the channel component will display a tick count checkbox in the panel settings.
Listing 5-9 Including a tick count checkbox in a dialog box in the panel component
pascal ComponentResult SGPictPanelGetDitl (SGPictGlobals store, |
Handle *ditl) |
{ |
/* get and detach your dialog template */ |
*ditl = GetResource('DITL', 7000); |
if (!*ditl) return resNotFound; |
DetachResource(*ditl); |
return noErr; |
} |
pascal ComponentResult SGPictPanelInstall (SGPictGlobals store, |
SGChannel c, |
DialogPtr d, |
short itemOffset) |
{ |
Rect newBounds; |
short kind; |
Handle h; |
/* reset this channel to use the dialog window and be in |
preview mode with no clip */ |
SGSetGWorld (store->self, (CGrafPtr)d, GetMainDevice()); |
SGGetChannelUsage (store->self, &store->saveUsage); |
SGSetChannelUsage (store->self, seqGrabPreview); |
SGSetChannelClip (c, nil); |
/* update boundaries to match size of user item */ |
GetDItem (d, 1 + itemOffset, &kind, &h, &newBounds); |
SGSetChannelBounds (c, &newBounds); |
SGStartPreview (store->self); |
return noErr; |
} |
pascal ComponentResult SGPictPanelEvent (SGPictGlobals store, |
SGChannel c, DialogPtr d, |
short itemOffset, |
EventRecord *theEvent, |
short *itemHit, |
Boolean *handled) |
{ |
/* use idle time to draw */ |
if (theEvent->what == nullEvent) |
return SGIdle (store->self); |
return noErr; |
} |
pascal ComponentResult SGPictPanelRemove (SGPictGlobals store, |
SGChannel c, DialogPtr d, |
short itemOffset) |
{ |
/* stop playing */ |
SGStop (store->self); |
SGRelease (store->self); |
/* note that the clip and bounds are automatically restored |
for you because you stored them using the SGGetSettings |
function */ |
/* restore usage */ |
SGSetChannelUsage(store->self, store->saveUsage); |
return noErr; |
} |
The final step in the implementation of a sequence grabber channel component is the display of the channel preview in the settings dialog box. Two sequence grabber functions, SGSettingsDialog
and SGGetSettingsDialog
, facilitate this process.
The channel component instructs the sequence grabber to display its settings dialog box to the user by calling the sequence grabber component’s SGSettingsDialog
function. The user can specify the configuration of a sequence grabber channel in this dialog box.
To retrieve the current settings of all channels used by the sequence grabber, call the SGGetSettings
function. The sequence grabber places all of this configuration information into a Movie Toolbox user data list.
Listing 5-10 illustrates code that creates a user data list to contain the tick count information for the sequence grabber’s settings dialog box, adds a matrix to the list, and stores clipping information (if any exists). The sample code then restores the clipping and the matrix.
Listing 5-10 Displaying channel settings
pascal ComponentResult SGPictPanelGetSettings |
(SGPictGlobals store, SGChannel c, |
UserData *result, long flags) |
{ |
OSErr err = noErr; |
UserData ud = 0; |
MatrixRecord matrix; |
RgnHandle clip; |
/* create a user data list to hold your state */ |
if (err = NewUserData (&ud)) goto bail; |
/* add matrix to user data */ |
if (SGGetChannelMatrix (c, &matrix) == noErr) { |
if (err = SetUserDataItem (ud, &matrix, sizeof(matrix), |
sgMatrixType, 1)) |
goto bail; |
} |
/* store clip, if there is one */ |
if (SGGetChannelClip (c, &clip) == noErr) { |
if (clip) |
err = AddUserData (ud, (Handle)clip, sgClipType); |
else |
err = SetUserDataItem (ud, nil, 0, sgClipType, 1); |
/* add a dummy to indicate none */ |
DisposeRgn(clip); |
if (err) goto bail; |
} |
bail: |
if (err) { |
DisposeUserData (ud); |
ud = 0; |
} |
*result = ud; |
return err; |
} |
pascal ComponentResult SGPictPanelSetSettings |
(SGPictGlobals store, |
SGChannel c, UserData ud, long flags) |
{ |
OSErr err; |
RgnHandle clip = NewRgn(); |
MatrixRecord matrix; |
/* restore clip, if one was stored */ |
if (GetUserData (ud, (Handle)clip, sgClipType, 1) == noErr) { |
if (err = SGSetChannelClip |
(c, GetHandleSize ((Handle)clip) ? clip : 0)) |
goto bail; |
} |
/* restore matrix */ |
if (err = GetUserDataItem (ud, &matrix, sizeof(matrix), |
sgMatrixType, 1)) goto bail; |
if (err = SGSetChannelMatrix (c, &matrix)) |
goto bail; |
bail: |
DisposeRgn (clip); |
return err; |
} |
The sequence grabber sound channel allows sound to be captured at any sample rate. The sample rate is specified by using SGSetSoundInputRate
. If the requested rate is not one of the hardware rates, the sound will be captured using the closest available hardware sample rate and will be rate-converted in software to the requested rate.
In most cases, sound capture hardware does not run at the same clock rate as the motherboard crystal used to generate time stamps. Sound capture hardware also rarely runs on the same clock as video capture hardware. Over time, drift between these clocks can result in the loss of synchronization between sound and video.
QuickTime measures the drift over the duration of the capture and applies an adjustment to the sample rate of the audio to keep things synchronized. In nearly all cases, this is the right thing to do. If your hardware really knows that it always captures at the correct sample rate, it can tell QuickTime not to adjust the sample rate.
To prohibit adjustment of the sample rate, implement the 'qtrt'
resource in your sound input device’s GetInfo
routine. The argument passed is a pointer to a short
. Set the short
to true
to indicate you don’t want sample rate adjustment to be applied.
The sequence grabber supports two functions, SGChannelSetDataSourceName
and SGChannelGetDataSourceName
, that allow you to specify the source identification information associated with a sequence grabber channel.
In QuickTime, sequence grabber channel components can capture data into multiple files. Capturing to multiple files can improve the performance and flexibility of captures and enable larger total captures.
You can create a sequence grabber component that can capture to multiple files by doing the following in your sequence grabber component:
Use SGAddExtendedMovieData
rather than SGAddMovieData
to write data.
In the SGWriteSamples
routine, instead of using SGGetNextFrameReference
, use SGGetNextExtendedFrameReference
.
An example of how to do this is shown in Listing 5-11. This example also shows how to use the SGAddOutputDataRefToMedia
helper routine to easily manage the multiple files in which the captured data is stored.
Listing 5-11 Channel capture and managing multiple output files
Track aTrack = NewMovieTrack(theMovie, width, height, 0); |
Media aMedia = NewTrackMedia(aTrack, TextMediaType, |
kMediaTimeScale, nil, 0); |
SeqGrabExtendedFrameInfo fi; |
SGOutput lastOutput = nil; |
long i; |
OSErr err; |
fi.frameChannel = store->self; |
i = -1; |
do { |
TimeValue frameDuration; |
err = SGGetNextExtendedFrameReference(store->grabber, &fi, |
&frameDuration, &i); |
if (err) { |
if (err == paramErr) |
err = noErr; |
break; |
} |
// switch to the next data reference |
if (lastOutput != fi.frameOutput) { |
err = SGAddOutputDataRefToMedia(store->grabber, |
fi.frameOutput, aMedia, sampleDescription); |
if (err) goto exit; |
lastOutput = fi.frameOutput; |
} |
//note that only the low 32 bits of the file offset are used here |
err = AddMediaSampleReference(aMedia, |
fi.frameOffset.lo, fi.frameSize, |
frameDuration, |
sampleDescription, 1, |
0, 0); |
} while (err == noErr); |
exit: |
if (alias) DisposeHandle((Handle)alias); |
return err; |
In this example, the default data reference is not defined when NewTrackMedia
is called. Instead, the default data reference is defined by the first call to SGAddOutputDataRefToMedia
. This approach provides added flexibility by allowing movies to be captured to data handlers other than the standard file system data handler.
© 2005, 2007 Apple Inc. All Rights Reserved. (Last updated: 2007-01-08)