Munggrab.c

/*
    File:       Munggrab.c
    
    Description: This example shows how to run the Sequence Grabber in record mode and use
                 a DataProc to get and modify the captured data. Munggrab calculates the
                 frame rate using the time value stamp passed to the data proc then draws this
                 rate onto the frame. This technique provides optimal performance, far better
                 than using preview mode or bottlenecks. This code will help a lot when
                 capturing from DV and should allow 30fps playthrough using DV capture on a G3.
 
    Author:     km, era
 
    Copyright:  © Copyright 2000 - 2002 Apple Computer, Inc. All rights reserved.
    
    Disclaimer: 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.
                
    Change History (most recent first): <3> 3/28/02 DV source rect bug fix in DataProc
                                        <2> 7/08/01 carbonized
                                        <1> 1/13/00 initial release
 
*/
 
// NOTE:
// this sample uses carbon accessesors and will not
// build if you have not specified a carbon target
 
// build for carbon
#define TARGET_API_MAC_CARBON 1
 
#if __APPLE_CC__
    #include <Carbon/Carbon.h>
    #include <QuickTime/QuickTime.h>
#else 
    #include <ConditionalMacros.h>
    #include <Carbon.h>
    #include <QuickTimeComponents.h>
    #include <stdio.h>
#endif
 
// defines
#define BailErr(x) {err = x; if(err != noErr) goto bail;}
 
// mung data struct
typedef struct {
    WindowRef           pWindow;    // window
    Rect                boundsRect; // bounds rect
    GWorldPtr           pGWorld;    // offscreen
    SeqGrabComponent    seqGrab;    // sequence grabber
    ImageSequence       decomSeq;   // unique identifier for our decompression sequence
    ImageSequence       drawSeq;    // unique identifier for our draw sequence
    long                drawSize;
    TimeValue           lastTime;
    TimeScale           timeScale;
    long                frameCount;
} MungDataRecord, *MungDataPtr;
 
// globals
static BitMap gScreenbits;
static MungDataPtr gMungData = NULL;
static Boolean gDone = false,
               gIsCollapsed = false,
               gIsGrabbing = false;
               
void Initialize(void);
OSErr InitializeMungData(Rect inBounds, WindowRef inWindow);
OSErr MakeAWindow(WindowRef *outWindow);
SeqGrabComponent MakeSequenceGrabber(WindowRef pWindow);
OSErr MakeSequenceGrabChannel(SeqGrabComponent seqGrab, SGChannel *sgchanVideo, Rect const *rect);
void DoUpdate(void);
 
OSErr MakeImageSequenceForGWorld(GWorldPtr pGWorld, GWorldPtr pDest, long *imageSize, ImageSequence *seq);
pascal OSErr MungGrabDataProc(SGChannel c, Ptr p, long len, long *offset, long chRefCon, TimeValue time, short writeType, long refCon);
 
// --------------------
// Initialize for Carbon & QuickTime
//
void Initialize(void)
{   
    InitCursor();   
    EnterMovies();
    
    GetQDGlobalsScreenBits(&gScreenbits);
} 
 
// --------------------
// InitializeMungData
//
OSErr InitializeMungData(Rect inBounds, WindowRef inWindow)
{
    CGrafPtr theOldPort;
    GDHandle theOldDevice;
    
    OSErr err = noErr;
    
    // allocate memory for the data
    gMungData = (MungDataPtr)NewPtrClear(sizeof(MungDataRecord));
    if (MemError() || NULL == gMungData ) return NULL;
    
    // create a GWorld
    err = QTNewGWorld(&(gMungData->pGWorld),    // returned GWorld
                        k32ARGBPixelFormat,     // pixel format
                        &inBounds,              // bounds
                        0,                      // color table
                        NULL,                   // GDHandle
                        0);                     // flags
    BailErr(err);
    
    // lock the pixmap and make sure it's locked because
    // we can't decompress into an unlocked pixmap
    if(!LockPixels(GetGWorldPixMap(gMungData->pGWorld)))
        goto bail;
    
    GetGWorld(&theOldPort, &theOldDevice);    
    SetGWorld(gMungData->pGWorld, NULL);
    BackColor(blackColor);
    ForeColor(whiteColor);
    EraseRect(&inBounds);    
    SetGWorld(theOldPort, theOldDevice);
 
    gMungData->boundsRect = inBounds;
    gMungData->pWindow = inWindow;
 
bail:
    return err;
}
 
// --------------------
// MakeImageSequenceForGWorld
//
OSErr MakeImageSequenceForGWorld(GWorldPtr pGWorld, GWorldPtr pDest, long *imageSize, ImageSequence *seq)
{
    ImageDescriptionHandle desc = NULL;
    PixMapHandle hPixMap = GetGWorldPixMap(pGWorld);
    Rect bounds;
    
    OSErr err = noErr;
    
    GetPixBounds(hPixMap, &bounds);
 
    *seq = NULL;
    
    // returns an image description for the GWorlds PixMap
    // on entry the imageDesc is NULL, on return it is correctly filled out
    // you are responsible for disposing it
    err = MakeImageDescriptionForPixMap(hPixMap, &desc);
    BailErr(err);
    
    *imageSize = (GetPixRowBytes(hPixMap) * (*desc)->height); // ((**hPixMap).rowBytes & 0x3fff) * (*desc)->height;
 
    // begin the process of decompressing a sequence of frames
    // the destination is the onscreen window
    err = DecompressSequenceBegin(seq,                  // pointer to field to receive unique ID for sequence
                                  desc,                 // handle to image description structure
                                  pDest,                // port for the DESTINATION image
                                  NULL,                 // graphics device handle, if port is set, set to NULL
                                  &bounds,              // source rectangle defining the portion of the image to decompress
                                  NULL,                 // transformation matrix
                                  ditherCopy,           // transfer mode specifier
                                  (RgnHandle)NULL,      // clipping region in dest. coordinate system to use as a mask  
                                  0,                    // flags
                                  codecNormalQuality,   // accuracy in decompression
                                  anyCodec);            // compressor identifier or special identifiers ie. bestSpeedCodec
bail:
    if (desc)
        DisposeHandle((Handle)desc);
    
    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. In this example we never create an audio channel so we know
         we're always dealing with video.
   
   This data function does two things, it first decompresses captured video
   data into an offscreen GWorld, draws some status information onto the frame then
   transfers the frame to an onscreen window.
   
   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)
{
#pragma unused(offset,chRefCon,writeType,refCon)
 
    CGrafPtr    theSavedPort;
    GDHandle    theSavedDevice;
    CodecFlags  ignore;
    float       fps = 0,
                averagefps = 0;
    char        status[64];
    Str255      theString; 
    
    ComponentResult err = noErr;
    
    // reset frame and time counters after a stop/start
    if (gMungData->lastTime > time) {
        gMungData->lastTime = 0;
        gMungData->frameCount = 0;
    }
    
    gMungData->frameCount++;
        
    if (gMungData->timeScale == 0) {
        // first time here so set the time scale
        err = SGGetChannelTimeScale(c, &gMungData->timeScale);
        BailErr(err);
    }
    
    if (gMungData->pGWorld) {
        if (gMungData->decomSeq == 0) {
            // Set up getting grabbed data into the GWorld
            
            Rect                   sourceRect = { 0, 0 };
            MatrixRecord           scaleMatrix;
            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);
            BailErr(err);
            
            /***** IMPORTANT NOTE *****
            
             Previous versions of this sample code made an incorrect decompression
             request.  Intending to draw the DV frame at quarter-size into a quarter-size
             offscreen GWorld, it made the call
 
                err = DecompressSequenceBegin(..., &rect, nil, ...);
 
             passing a quarter-size rectangle as the source rectangle.  The correct
             interpretation of this request is to draw the top-left corner of the DV
             frame cropped at normal size.  Unfortunately, a DV-specific bug in QuickTime
             5 caused it to misinterpret this request and scale the frame to fit.
 
             This bug will be fixed in QuickTime 6.  If your code behaves as intended
             because of the bug, you should fix your code to pass a matrix scaling the
             frame to fit the offscreen gworld:
 
                RectMatrix( & scaleMatrix, &dvFrameRect, &gworldBounds );
                err = DecompressSequenceBegin(..., nil, &scaleMatrix, ...);
          
             This approach will work in all versions of QuickTime.
                        
            **************************/
            
            // make a scaling matrix for the sequence
            sourceRect.right = (**imageDesc).width;
            sourceRect.bottom = (**imageDesc).height;
            RectMatrix(&scaleMatrix, &sourceRect, &gMungData->boundsRect);
            
            // begin the process of decompressing a sequence of frames
            // this is a set-up call and is only called once for the sequence - the ICM will interrogate different codecs
            // and construct a suitable decompression chain, as this is a time consuming process we don't want to do this
            // once per frame (eg. by using DecompressImage)
            // for more information see Ice Floe #8 http://developer.apple.com/quicktime/icefloe/dispatch008.html
            // the destination is specified as the GWorld
            err = DecompressSequenceBegin(&gMungData->decomSeq, // pointer to field to receive unique ID for sequence
                                          imageDesc,            // handle to image description structure
                                          gMungData->pGWorld,   // port for the DESTINATION image
                                          NULL,                 // graphics device handle, if port is set, set to NULL
                                          NULL,                 // source rectangle defining the portion of the image to decompress 
                                          &scaleMatrix,         // transformation matrix
                                          srcCopy,              // transfer mode specifier
                                          (RgnHandle)NULL,      // clipping region in dest. coordinate system to use as a mask
                                          NULL,                 // flags
                                          codecNormalQuality,   // accuracy in decompression
                                          bestSpeedCodec);      // compressor identifier or special identifiers ie. bestSpeedCodec
            BailErr(err);
            
            DisposeHandle((Handle)imageDesc);         
            
            // Set up getting grabbed data into the Window
            
            // create the image sequence for the offscreen
            err = MakeImageSequenceForGWorld(gMungData->pGWorld,
                                             GetWindowPort(gMungData->pWindow),
                                             &gMungData->drawSize,
                                             &gMungData->drawSeq);
            BailErr(err);
        }
        
        // decompress a frame into the GWorld - can queue a frame for async decompression when passed in a completion proc
        err = DecompressSequenceFrameS(gMungData->decomSeq, // sequence ID returned by DecompressSequenceBegin
                                       p,                   // pointer to compressed image data
                                       len,                 // size of the buffer
                                       0,                   // in flags
                                       &ignore,             // out flags
                                       NULL);               // async completion proc
 
        if (err) {
            // show the error
            TextSize(10);
            TextMode(srcXor);
            MoveTo(gMungData->boundsRect.left + 10, gMungData->boundsRect.top + 80);
            sprintf(status,"DecompressSequenceFrameS gave error %ld (%lx)",err,err);
            CopyCStringToPascal(status, theString);
            DrawString(theString);
            err = noErr;
        } else {    
            // write status information onto the frame          
            GetGWorld(&theSavedPort, &theSavedDevice);
            SetGWorld(gMungData->pGWorld, NULL);
           
            TextSize(12);
            TextMode(srcCopy);
            MoveTo(gMungData->boundsRect.left +10, gMungData->boundsRect.bottom - 14);
            fps = (float)gMungData->timeScale / (float)(time - gMungData->lastTime);
            averagefps = ((float)gMungData->frameCount * (float)gMungData->timeScale) / (float)time;
            sprintf(status, "time stamp: %ld, fps:%5.1f average fps:%5.1f", time, fps, averagefps);
            CopyCStringToPascal(status, theString);
            DrawString(theString);
            SetGWorld(theSavedPort, theSavedDevice);
           
            // draw the frame to the destination, in this case the onscreen window      
            err = DecompressSequenceFrameS(gMungData->drawSeq,                                  // sequence ID
                                           GetPixBaseAddr(GetGWorldPixMap(gMungData->pGWorld)), // pointer image data
                                           gMungData->drawSize,                                 // size of the buffer
                                           0,                                                   // in flags
                                           &ignore,                                             // out flags
                                           NULL);                                               // can async help us?
        }
    }
            
bail:
    gMungData->lastTime = time;
    
    return err;
}
 
// --------------------
// DoUpdate
//
void DoUpdate(void)
{
    CodecFlags  ignore;
 
    // draw the last frame captured   
    DecompressSequenceFrameS(gMungData->drawSeq,
                             GetPixBaseAddr(GetGWorldPixMap(gMungData->pGWorld)),
                                            gMungData->drawSize,
                                            0,
                                            &ignore,
                                            NULL);      
}
 
// --------------------
// MakeSequenceGrabber
//
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;
}
 
// --------------------
// MakeSequenceGrabChannel
//
OSErr MakeSequenceGrabChannel(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;
}
 
// --------------------
// MakeAWindow
//
OSErr MakeAWindow(WindowRef *outWindow)
{
    Rect        windowRect = {0, 0, 240, 320};
    Rect        bestRect;
    
    OSErr   err = noErr;
 
    // figure out the best monitor for the window
    GetBestDeviceRect(NULL, &bestRect);
 
    // put the window in the top left corner of that monitor
    OffsetRect(&windowRect, bestRect.left + 10, bestRect.top + 50);
    
    err = CreateNewWindow(kDocumentWindowClass, kWindowCloseBoxAttribute, &windowRect, outWindow);
    BailErr(err);
    
    SetWTitle(*outWindow, "\pMunggrab");
 
    // set the port to the new window
    SetPortWindowPort(*outWindow);
    ShowWindow(*outWindow);
 
bail:  
    return err;
}
 
int main(void)
{
    WindowRef           pMainWindow = NULL;
    SeqGrabComponent    seqGrab;
    SGChannel           sgchanVideo;
    Rect                portRect;
    
    OSErr               err = noErr;
    
    Initialize();
    
    // create the window
    err = MakeAWindow(&pMainWindow);
    BailErr(err);
    
    GetPortBounds(GetWindowPort(pMainWindow), &portRect);
      
    // initialize our data
    err = InitializeMungData(portRect, pMainWindow);
    BailErr(err);
    
    // create and initialize the sequence grabber
    seqGrab = MakeSequenceGrabber(pMainWindow);
    BailErr(NULL == seqGrab);
    
    // create the channel
    err = MakeSequenceGrabChannel(seqGrab, &sgchanVideo, &portRect);
    BailErr(err);
    
    // specify a data function
    err = SGSetDataProc(seqGrab, NewSGDataUPP(MungGrabDataProc), NULL);
    BailErr(err);
    
    // lights...camera...
    err = SGPrepare(seqGrab, false, true);
    BailErr(err); 
    
    // ...action
    err = SGStartRecord(seqGrab);
    BailErr(err);
    gIsGrabbing = true;
    
    while (!gDone) {
        EventRecord theEvent;
        WindowRef theWindow;
        
        GetNextEvent(everyEvent, &theEvent);
        
        if (IsWindowCollapsed(pMainWindow)) {
            // checking this here avoids codecNothingToBlitErr later
            SGStop(seqGrab);
            gIsGrabbing = false;
            gIsCollapsed = true;            
        }
 
        switch (theEvent.what) {
        case nullEvent:         
            // give the sequence grabber time to do it's thing
            if (gIsGrabbing) {
                err = SGIdle(seqGrab);
                if (err) {
                    char errMsg[32];
                    
                    // if there is an error, display the result in the window title
                    // if it's a cDepthErr we don't pause; the sequence grabber
                    // would return cDepthErr if the window was moved or depth changed on
                    // QT 4.1.2, it does it less on QT 5 because Kevin made it smarter
                    // all other errors cause a pause - errors set in the DataProc show up
                    // here as well as others generated by the vDig - different vDigs can
                    // generate different errors in different situations
                    if (err == cDepthErr) {
                    
                        sprintf(errMsg, "cDepthErr ", err);
                        c2pstrcpy((unsigned char *)&errMsg, errMsg);
                        SetWTitle(pMainWindow, (unsigned char *)errMsg);
 
                        SGStop(seqGrab);
                        SGStartRecord(seqGrab);
                        break;
                    } else {
                        KeyMap  theKeys;
                        #define ISESCKEYDOWN() ((theKeys[1] & 0x00002000) == 0x00002000)
                        
                        SGStop(seqGrab);
                        
                        sprintf(errMsg, "Stopped, esc to continue %d", err);
                        c2pstrcpy((unsigned char *)&errMsg, errMsg);
                        SetWTitle(pMainWindow, (unsigned char *)errMsg);
 
                        // wait for esc 
                        do {
                            GetKeys(theKeys);
                        } while  (!ISESCKEYDOWN());
                        
                        SetWTitle(pMainWindow, "\pMunggrab");
                        SGStartRecord(seqGrab);
                    }
                }
            }
            break;
 
        case updateEvt:
            theWindow = (WindowRef)theEvent.message;
            if (theWindow == pMainWindow) {
                if (gIsGrabbing) {
                    // inform the sequence grabber of the update
                    RgnHandle theUpdateRgn = NewRgn();
                    
                    GetWindowRegion(theWindow, kWindowUpdateRgn, theUpdateRgn);
                    SGUpdate(seqGrab, theUpdateRgn);
                    DisposeRgn(theUpdateRgn);
                } else {
                    if (!IsWindowCollapsed(pMainWindow) && gIsCollapsed) {
                        // window was just un-collapsed, start grabbing again
                        SGStartRecord(seqGrab);
                        gIsGrabbing = true;
                        gIsCollapsed = false;
                    } else {
                        // update the still image
                        DoUpdate();
                    }
                }
                
                // swallow the update event
                BeginUpdate(theWindow);
                EndUpdate(theWindow);
            }
            break;
 
        case mouseDown:
            short nPart;
            nPart = FindWindow(theEvent.where, &theWindow);
            
            if (pMainWindow != theWindow)
                break;
 
            switch (nPart) {
            case inGoAway:
                gDone = TrackGoAway(theWindow, theEvent.where);
                break;
 
            case inDrag:
                ICMAlignmentProcRecord apr;
                
                SGGetAlignmentProc(seqGrab, &apr);
                
                DragAlignedWindow(theWindow, theEvent.where, &gScreenbits.bounds, NULL, &apr);              
                break;
            }
            break;
            
        case osEvt:
            if ((theEvent.message & (suspendResumeMessage << 24)) != 0 ) {
                if ((theEvent.message & resumeFlag) != 0 ) {
                    if (!gIsGrabbing) {
                        // switched in, start grabbin'
                        SGStartRecord(seqGrab);
                        gIsGrabbing = true;
                    }
                } else {
                    if (gIsGrabbing) {
                        // switched out, stop grabbin'
                        SGStop(seqGrab);
                        gIsGrabbing = false;
                    }
                }
            }
            break;
            
        default:
            break;
        } // switch
    }
    
bail:
    // clean up
    if (seqGrab) {
        SGStop(seqGrab);
        CloseComponent(seqGrab);
    }  
    if (pMainWindow)
        DisposeWindow(pMainWindow);
    
    return 0;
}