[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