[mythtv] Mac OS X video: QuickTime implementation

Jeremiah Morris jm at whpress.com
Tue Sep 7 12:00:26 EDT 2004


With Nigel's aid, I discovered an error in some pointer arithmetic; a 
new version of videoout_quartz.cpp is attached.

- Jeremiah
-------------- next part --------------
/********************************************************************************
 * = NAME
 * videoout_quartz.cpp
 *
 * = DESCRIPTION
 * Basic video for Mac OS X, using an unholy amalgamation of QuickTime,
 * QuickDraw, and Quartz/Core Graphics.
 *
 * = PERFORMANCE
 * Seems to be better than version 1 -- numbers, Nigel?
 *
 * = KNOWN BUGS
 * - Changing video resolution is only controllable at compile time
 * - Video always scales to fullscreen or GUI size; to avoid scaling, set
 *   monitor resolution or GUI size (with "Use GUI size for TV playback")
 *   to an appropriate value
 * - Haven't tested "Live preview" function
 * - Doesn't use over/underscan or offset values for playback
 * 
 * = REVISION
 * $Id: videoout_quartz.cpp,v 1.1 2004/08/19 05:20:10 ijr Exp $
 *
 * = AUTHORS
 * Nigel Pearson, Jeremiah Morris
 *******************************************************************************/

// *****************************************************************************
// Configuration:

// Define this if you want to change the display's mode to one
// that more closely matches the resolution of the video stream.
// This may improve performance when scaling (less bytes to copy/scale to))
#define CHANGE_SCREEN_MODE

// Define this if we want to scale each frame to either the fullscreen,
// or the correct aspect ratio. The alternative is to just
// copy the image rectangle to the middle of the screen
#define SCALE_VIDEO


// Default numbers of buffers from some of the other videoout modules:
const int kNumBuffers      = 31;
const int kNeedFreeFrames  = 1;
const int kPrebufferFrames = 12;
const int kKeepPrebuffer   = 2;

// *****************************************************************************

#include <map>
#include <iostream>
using namespace std;

#include "mythcontext.h"
#include "filtermanager.h"
#include "videoout_quartz.h"

#import <CoreGraphics/CGBase.h>
#import <CoreGraphics/CGDisplayConfiguration.h>
#import <CoreGraphics/CGImage.h>
#import <Carbon/Carbon.h>
#import <QuickTime/QuickTime.h>


struct QuartzData
{
    // Stored information about the media stream:
    int                srcWidth,
                       srcHeight;
    float              srcAspect;


    // What size/position does the user want the stream to be displayed at?
    int                desiredWidth,
                       desiredHeight,
                       desiredXoff,
                       desiredYoff;


    // Information about the display and viewport:
    CGDirectDisplayID  theDisplay;
    bool               capturedDisplay;  // true if we captured the display
    CGrafPtr           thePort;
    
    bool               drawInWindow;
    bool               changeResolution;
    CFDictionaryRef    originalMode,
                       newMode;

    bool               capturedBeforeEmbed;  // true if capturedDisplay
                                             //  mode was used before
                                             //  embedding was called
    
    // Structures that we use for decompression:
    ImageSequence      seqID;         // codec sequence identifier
    PlanarPixmapInfoYUV420 *pixmap;   // frame header + data
    size_t             pixmapSize;    // pixmap size
    void *             pixelData;     // start of data section
    size_t             pixelSize;     // data size
    
};

VideoOutputQuartz::VideoOutputQuartz(void)
                 : VideoOutput()
{
    Started = 0; 

    pauseFrame.buf = NULL;

    data = new QuartzData();
    bzero(data, sizeof(QuartzData));
    
#ifdef CHANGE_SCREEN_MODE
    data->changeResolution = true;
#endif

    if (gContext->GetNumSetting("GuiSizeForTV", 0))
    {
        // If this setting is on, we refrain from full screen
        data->drawInWindow = true;
    }
}

VideoOutputQuartz::~VideoOutputQuartz()
{
    EndDisplay();
    
    if (pauseFrame.buf)
        delete [] pauseFrame.buf;

    Exit();
    delete data;
}

void VideoOutputQuartz::Exit(void)
{
    if (Started) 
    {
        Started = false;

        DeleteQuartzBuffers();
    }
}


/* Tear down vbuffers
 */
void VideoOutputQuartz::DeleteQuartzBuffers()
{
    for (int i = 0; i < numbuffers + 1; i++)
    {
        delete [] vbuffers[i].buf;
        vbuffers[i].buf = NULL;
    }
}


/* Tear down display changes
 */
void VideoOutputQuartz::EndDisplay(void)
{
    // make sure decompression stops first
    EndCodec();
    
    if (data->capturedDisplay)
    {
        DisposePort(data->thePort);
        data->thePort = NULL;
        if (data->originalMode)
        {
            CGDisplaySwitchToMode(data->theDisplay, data->originalMode);
            data->originalMode = NULL;
        }
        CGDisplayRelease(data->theDisplay);
        data->capturedDisplay = false;
    }
}

/* Tear down QuickTime decompressor and buffer storage
 */
void VideoOutputQuartz::EndCodec(void)
{
    if (data->seqID)
    {
        CDSequenceEnd(data->seqID);
        data->seqID = NULL;
    }
    if (data->pixmap)
    {
        delete [] data->pixmap;
        data->pixmap = NULL;
    }
}


/* Set the transformation matrix for moving and resizing
 * video into our viewport.
 */
void VideoOutputQuartz::Transform(void)
{
    if (!data->seqID)
        return;
        
    MatrixRecord matrix;
    SetIdentityMatrix(&matrix);
    
    int x, y, w, h, sw, sh;
    x = data->desiredXoff;
    y = data->desiredYoff;
    w = data->desiredWidth;
    h = data->desiredHeight;
    sw = data->srcWidth;
    sh = data->srcHeight;
    
    VERBOSE(VB_PLAYBACK, QString("Viewport is %1 x %2").arg(w).arg(h));
    VERBOSE(VB_PLAYBACK, QString("Image is %1 x %2").arg(sw).arg(sh));
    
    // constants for transformation operations
    Fixed one, zero;
    one  = Long2Fix(1);
    zero = Long2Fix(0);
    
#ifdef SCALE_VIDEO  
    // scale width for non-square pixels
    if (fabsf(data->srcAspect - (sw * 1.0 / sh)) > 0.01)
    {
        double aspectScale = data->srcAspect * sh / sw;
        VERBOSE(VB_PLAYBACK, QString("Scaling to %1 of width").arg(aspectScale));
        ScaleMatrix(&matrix,
            X2Fix(aspectScale),
            one,
            zero, zero);
        
        // reset sw to be apparent width
        sw = (int)lroundf(sh * data->srcAspect);
    }

    // scale to fill viewport
    if ((h != sh) || (w != sw))
    {
        double scale = fmin(h * 1.0 / sh, w * 1.0 / sw);
        VERBOSE(VB_PLAYBACK, QString("Scaling to %1 of original").arg(scale));
        Fixed scaleFix = X2Fix(scale);
        ScaleMatrix(&matrix,
            scaleFix, scaleFix,
            zero, zero);
        
        // reset sw, sh for new apparent width/height
        sw = (int)(sw * scale);
        sh = (int)(sh * scale);
    }
#endif

    // center image in viewport
    if ((h != sh) || (w != sw))
    {
        VERBOSE(VB_PLAYBACK, QString("Centering with %1, %2").arg((w - sw)/2.0).arg((h - sh)/2.0));
        TranslateMatrix(&matrix,
            X2Fix((w - sw) / 2.0),
            X2Fix((h - sh) / 2.0));
    }

#ifdef SCALE_VIDEO    
    // apply over/underscan
    int hscan = gContext->GetNumSetting("HorizScanPercentage", 5);
    int vscan = gContext->GetNumSetting("VertScanPercentage", 5);
    if (hscan || vscan)
    {
        QString HorizScanMode = gContext->GetSetting("HorizScanMode", "overscan");
        QString VertScanMode = gContext->GetSetting("VertScanMode", "overscan");
        if (VertScanMode == "underscan")
        {
              vscan = 0 - vscan;
        }
        if (HorizScanMode == "underscan")
        {
            hscan = 0 - hscan;
        }
        VERBOSE(VB_PLAYBACK, QString("Overscanning to %1, %2").arg(hscan).arg(vscan));
        ScaleMatrix(&matrix,
            X2Fix((double)(1.0 + (hscan / 100.0))),
            X2Fix((double)(1.0 + (vscan / 100.0))),
            X2Fix(sw / 2.0),
            X2Fix(sh / 2.0));
    }
#endif

    // apply TV mode offset
    int tv_xoff = gContext->GetNumSetting("xScanDisplacement", 0);
    int tv_yoff = gContext->GetNumSetting("yScanDisplacement", 0);
    if (!embedding && (tv_xoff || tv_yoff))
    {
        VERBOSE(VB_PLAYBACK, QString("TV offset by %1, %2").arg(tv_xoff).arg(tv_yoff));
        TranslateMatrix(&matrix,
            Long2Fix(tv_xoff),
            Long2Fix(tv_yoff));
    }
    
    // apply graphics port or embedding offset
    if (x || y)
    {
    VERBOSE(VB_PLAYBACK, QString("Translating to %1, %2").arg((w - sw)/2.0).arg((h - sh)/2.0));
        TranslateMatrix(&matrix,
            Long2Fix(x),
            Long2Fix(y));
    }
    
    // apply matrix to decompressor
    SetDSequenceMatrix(data->seqID, &matrix);
}


/* Set the clipping region for only drawing into
 * part of the graphics port.  This is used for
 * the video preview, for instance.
 */
void VideoOutputQuartz::Mask(int x, int y, int w, int h)
{
    if (!data->thePort)
        return;
    if (!data->seqID)
        return;
        
    RgnHandle clipRgn = NULL;
    Rect portRect;
    GetPortBounds(data->thePort, &portRect);
    
    if (!x && !y && !w && !h)
    {
        // set up desired size based on port
        data->desiredXoff   = portRect.left;
        data->desiredYoff   = portRect.top;
        data->desiredWidth  = (portRect.right - portRect.left);
        data->desiredHeight = (portRect.bottom - portRect.top);
    }
    else
    {
        // correct offset based on any port coordinate transforms
        data->desiredXoff   = x + portRect.left;
        data->desiredYoff   = y + portRect.top;
        data->desiredWidth  = w;
        data->desiredHeight = h;
        
        if ((data->desiredXoff   !=  portRect.left) ||
            (data->desiredYoff   !=  portRect.top)  ||
            (data->desiredWidth  != (portRect.right - portRect.left)) ||
            (data->desiredHeight != (portRect.bottom - portRect.top)))
        {
            clipRgn = NewRgn();
            OpenRgn();
            InsetRgn(clipRgn, data->desiredWidth, data->desiredHeight);
            OffsetRgn(clipRgn, data->desiredXoff, data->desiredYoff);
            CloseRgn(clipRgn);
        }
    }
        
    SetDSequenceMask(data->seqID, clipRgn);
    if (clipRgn)
        DisposeRgn(clipRgn);
}


void VideoOutputQuartz::AspectChanged(float aspect)
{
    VideoOutput::AspectChanged(aspect);
    MoveResize();
    
    // update transformation matrix with new aspect ratio
    data->srcAspect = aspect;
    Transform();
    
}

void VideoOutputQuartz::Zoom(int direction)
{
    VideoOutput::Zoom(direction);
    MoveResize();
}

void VideoOutputQuartz::InputChanged(int width, int height, float aspect)
{
    VideoOutput::InputChanged(width, height, aspect);
    
    DeleteQuartzBuffers();
    CreateQuartzBuffers();
    
    MoveResize();

    scratchFrame = &(vbuffers[kNumBuffers]);

    if (pauseFrame.buf)
        delete [] pauseFrame.buf;

    pauseFrame.height = scratchFrame->height;
    pauseFrame.width  = scratchFrame->width;
    pauseFrame.bpp    = scratchFrame->bpp;
    pauseFrame.size   = scratchFrame->size;
    pauseFrame.buf    = new unsigned char[pauseFrame.size];
    
    // rebuild QuickTime decompressor
    data->srcWidth = width;
    data->srcHeight = height;
    data->srcAspect = aspect;
    BeginCodec(data->desiredXoff, data->desiredYoff,
               data->desiredWidth, data->desiredHeight);
}


/* Return refresh rate of our display
 */
int VideoOutputQuartz::GetRefreshRate(void)
{
    int refresh = 0;
    
    CFDictionaryRef mode = CGDisplayCurrentMode(data->theDisplay);
    if (mode)
    {
        CFNumberRef value = (CFNumberRef)
            CFDictionaryGetValue(mode, kCGDisplayRefreshRate);
        if (value)
        {
            CFNumberGetValue(value, kCFNumberIntType, &refresh);
        }
    }
    
    return refresh;
}


bool VideoOutputQuartz::Init(int width, int height, float aspect,
                             WId winid, int winx, int winy,
                             int winw, int winh, WId embedid)
{
    VERBOSE(VB_PLAYBACK, QString("VideoOutputQuartz::Init(width=%1, height=%2, aspect=%3, winid=%4\n winx=%5, winy=%6, winw=%7, winh=%8, WId embedid=%9)")
        .arg(width)
        .arg(height)
        .arg(aspect)
        .arg(winid)
        .arg(winx)
        .arg(winy)
        .arg(winw)
        .arg(winh)
        .arg(embedid));

    VideoOutput::InitBuffers(kNumBuffers, true, kNeedFreeFrames, 
                             kPrebufferFrames, kKeepPrebuffer);
    VideoOutput::Init(width, height, aspect, winid,
                      winx, winy, winw, winh, embedid);

    if (!CreateQuartzBuffers())
        return false;
    
    scratchFrame = &(vbuffers[kNumBuffers]);

    pauseFrame.height = scratchFrame->height;
    pauseFrame.width  = scratchFrame->width;
    pauseFrame.bpp    = scratchFrame->bpp;
    pauseFrame.size   = scratchFrame->size;
    pauseFrame.buf    = new unsigned char[pauseFrame.size];

    data->srcWidth  = width;
    data->srcHeight = height;
    data->srcAspect = aspect;
    
    // Initialize QuickTime
    if (EnterMovies())
    {
        puts("EnterMovies failed");
        return false;
    }
    
    // Set up display, which also sets up codec
    if (embedid)
    {
        embedding = true;
        BeginDisplay(true, winx, winy, winw, winh);
    }
    else
    {
        BeginDisplay(data->drawInWindow, 0, 0, 0, 0);
    }
    
    MoveResize();
    Started = true;

    return true;
}

bool VideoOutputQuartz::BeginDisplay(bool windowed, int x, int y,
                                     int w, int h)
{
    data->theDisplay = CGMainDisplayID();
    if (windowed)
    {
        // we reuse the GUI window
        data->thePort = GetWindowPort(FrontNonFloatingWindow());
        data->capturedDisplay = false;
    }
    else
    {
        // capture the main display
        if (CGDisplayCapture(data->theDisplay))
        {
            puts("CGDisplayCapture failed");
            return false;
        }
        
        if (data->changeResolution)
        {
            data->originalMode = CGDisplayCurrentMode(data->theDisplay);
            data->newMode =
                CGDisplayBestModeForParameters(data->theDisplay, 32,
                    data->srcWidth, data->srcHeight, NULL);
            CGDisplaySwitchToMode(data->theDisplay, data->newMode);
        }
        
        CGDisplayHideCursor(data->theDisplay);
        data->thePort = CreateNewPortForCGDisplayID((UInt32)data->theDisplay);
        data->capturedDisplay = true;
    }
    
    if (!data->thePort)
    {
        puts("Failed to capture display port");
        return false;
    }
    
    // set up everything else
    return BeginCodec(x, y, w, h);
}

bool VideoOutputQuartz::BeginCodec(int x, int y, int w, int h)
{
    int width, height;
    width = data->srcWidth;
    height = data->srcHeight;
    
    // Set up decompressor to display YUV data
    ImageDescriptionHandle yuvDesc =
        (ImageDescriptionHandle) NewHandleClear(sizeof(ImageDescription));
    HLock((Handle)yuvDesc);
    
    (**yuvDesc).idSize = sizeof(ImageDescription);
    (**yuvDesc).cType = kYUV420CodecType;
    (**yuvDesc).version = 1;
    (**yuvDesc).revisionLevel = 0;
    (**yuvDesc).spatialQuality = codecLosslessQuality;
    (**yuvDesc).width = width;
    (**yuvDesc).height = height;
    (**yuvDesc).hRes = Long2Fix(72);
    (**yuvDesc).vRes = Long2Fix(72);
    (**yuvDesc).depth = 24;
    (**yuvDesc).frameCount = 0;
    (**yuvDesc).dataSize = 0;
    (**yuvDesc).clutID = -1;
    
    HUnlock((Handle)yuvDesc);
    
    if (DecompressSequenceBeginS(&data->seqID,
                                 yuvDesc,
                                 NULL,
                                 0,
                                 data->thePort,
                                 NULL,
                                 NULL,
                                 NULL,
                                 srcCopy,
                                 NULL,
                                 0, //codecFlagUseImageBuffer,
                                 codecLosslessQuality,
                                 bestSpeedCodec))
    {
        puts("DecompressSequenceBeginS failed");
        return false;
    }
    SetDSequenceFlags(data->seqID,
                       codecDSequenceFlushInsteadOfDirtying,
                       codecDSequenceFlushInsteadOfDirtying);
    
    // Set up storage area for one YUV frame (header + data)
    data->pixelSize = (width * height * 3) / 2;
    data->pixmapSize = sizeof(PlanarPixmapInfoYUV420) + data->pixelSize;
    data->pixmap = (PlanarPixmapInfoYUV420 *) new char[data->pixmapSize];
    
    long offset = sizeof(PlanarPixmapInfoYUV420);
    data->pixelData = (char *)(data->pixmap) + offset;
    
    
    data->pixmap->componentInfoY.offset = offset;
    data->pixmap->componentInfoY.rowBytes = width;
    
    offset += width * height;
    data->pixmap->componentInfoCb.offset = offset;
    data->pixmap->componentInfoCb.rowBytes = width / 2;
    
    offset += (width * height) / 4;
    data->pixmap->componentInfoCr.offset = offset;
    data->pixmap->componentInfoCr.rowBytes = width / 2;
    
    // Things won't work until the mask and transform are set properly
    Mask(x, y, w, h);
    Transform();
    
    return true;
}

bool VideoOutputQuartz::CreateQuartzBuffers(void)
{
    for (int i = 0; i < numbuffers + 1; i++)
    {
        vbuffers[i].height = XJ_height;
        vbuffers[i].width = XJ_width;
        vbuffers[i].bpp = 12;
        vbuffers[i].size = XJ_height * XJ_width * 3 / 2;
        vbuffers[i].codec = FMT_YV12;
        vbuffers[i].buf = new unsigned char[vbuffers[i].size + 64];
        memset(vbuffers[i].buf, 0, XJ_height * XJ_width);
        memset(vbuffers[i].buf + XJ_height * XJ_width, 127, 
               XJ_height * XJ_width / 2);
    }

    return true;
}

void VideoOutputQuartz::EmbedInWidget(WId wid, int x, int y, int w, int h)
{
    VERBOSE(VB_PLAYBACK, "Calling EmbedInWidget");
    
    if (embedding)
        return;

    VideoOutput::EmbedInWidget(wid, x, y, w, h);
    
    if (data->capturedDisplay)
    {
        // If we've been running full screen, we need to
        // switch to the window port.
        VERBOSE(VB_PLAYBACK, "Changing display for embedding");
        data->capturedBeforeEmbed = true;
        BeginDisplay(true, x, y, w, h);
    }
    else
    {
        // We're already on the window port, we just need
        // to clip properly.
        
        VERBOSE(VB_PLAYBACK, "Changing mask/transform");
        data->capturedBeforeEmbed = false;
        Mask(x, y, w, h);
        Transform();
    }
}
 
void VideoOutputQuartz::StopEmbedding(void)
{
    if (!embedding)
        return;

    VideoOutput::StopEmbedding();
    
    if (data->capturedBeforeEmbed)
    {
        // Recapture display
        BeginDisplay(false, 0, 0, 0, 0);
    }
    else
    {
        // Reset clipping region
        Mask(0, 0, 0, 0);
        Transform();
    }
}

void VideoOutputQuartz::PrepareFrame(VideoFrame *buffer, FrameScanType t)
{
    (void)buffer;
    (void)t;
}

void VideoOutputQuartz::Show(FrameScanType t)
{
    (void)t;
    
    
    // feed our buffered data to QuickTime
    OSErr err;
    err = DecompressSequenceFrameWhen(data->seqID,
                                      (Ptr)data->pixmap,
                                      data->pixmapSize,
                                      0,
                                      NULL,
                                      NULL,
                                      NULL);
    if (err)
    {
        VERBOSE(VB_PLAYBACK, "DecompressSequenceFrameWhen failed");
    }
}

void VideoOutputQuartz::DrawUnusedRects(void)
{
}

void VideoOutputQuartz::UpdatePauseFrame(void)
{
    VideoFrame *pauseb = scratchFrame;
    if (usedVideoBuffers.count() > 0)
        pauseb = usedVideoBuffers.head();
    memcpy(pauseFrame.buf, pauseb->buf, pauseb->size);
}

void VideoOutputQuartz::ProcessFrame(VideoFrame *frame, OSD *osd,
                                     FilterChain *filterList,
                                     NuppelVideoPlayer *pipPlayer)
{
    if (!frame)
    {
        frame = scratchFrame;
        CopyFrame(scratchFrame, &pauseFrame);
    }

    if (filterList)
        filterList->ProcessFrame(frame);

    ShowPip(frame, pipPlayer);
    DisplayOSD(frame, osd);

    // copy data to our buffer
    memcpy(data->pixelData,
           frame->buf, frame->size);
}


More information about the mythtv-dev mailing list