FFmpegFB.cpp revision e64031e20c39650a7bc902a3e1aba613b9415dee
/** @file
*
* Framebuffer implementation that interfaces with FFmpeg
* to create a video of the guest.
*/
/*
* Copyright (C) 2006-2007 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
#define LOG_GROUP LOG_GROUP_GUI
#include "FFmpegFB.h"
#include <png.h>
#define VBOX_SHOW_AVAILABLE_FORMATS
// external constructor for dynamic loading
/////////////////////////////////////////////////////////////////////////////
/**
* Callback function to register an ffmpeg framebuffer.
*
* @returns COM status code.
* @param width Framebuffer width.
* @param height Framebuffer height.
* @param bitrate Bitrate of mpeg file to be created.
* @param filename Name of mpeg file to be created
* @retval retVal The new framebuffer
*/
{
Log2(("VBoxRegisterFFmpegFB: called\n"));
("failed to initialise the FFmpeg framebuffer, rc = %d\n",
rc));
{
*retVal = pFramebuffer;
return S_OK;
}
delete pFramebuffer;
return rc;
}
// constructor / destructor
/////////////////////////////////////////////////////////////////////////////
/**
* Perform parts of initialisation which are guaranteed not to fail
* unless we run out of memory. In this case, we just set the guest
* buffer to 0 so that RequestResize() does not free it the first time
* it is called.
*/
mfUrlOpen(false),
mBitsPerPixel(0),
mBytesPerLine(0),
mOutOfMemory(false), mToggle(false)
{
LogFlow(("Creating FFmpegFB object %p, width=%lu, height=%lu\n",
/* For temporary RGB frame we allocate enough memory to deal with
RGB16 to RGB32 */
if (mTempRGBBuffer == 0)
goto nomem_temp_rgb_buffer;
if (mYUVBuffer == 0)
goto nomem_yuv_buffer;
if (mFrame == 0)
goto nomem_mframe;
if (mOutBuf == 0)
goto nomem_moutbuf;
return;
/* C-based memory allocation and how to deal with it in C++ :) */
Log(("Failed to allocate memory for mOutBuf\n"));
Log(("Failed to allocate memory for mFrame\n"));
Log(("Failed to allocate memory for mYUVBuffer\n"));
Log(("Failed to allocate memory for mTempRGBBuffer\n"));
mOutOfMemory = true;
}
/**
* Write the last frame to disk and free allocated memory
*/
{
LogFlow(("Destroying FFmpegFB object %p\n", this));
if (mpFormatContext != 0)
{
if (mfUrlOpen)
{
/* Dummy update to make sure we get all the frame (timing). */
NotifyUpdate(0, 0, 0, 0);
/* Write the last pending frame before exiting */
int rc = do_rgb_to_yuv_conversion();
#if 1
/* Add another 10 seconds. */
for (int i = 10*25; i > 0; i--)
#endif
/* write a png file of the last frame */
write_png();
/* free the streams */
for(unsigned i = 0; i < (unsigned)mpFormatContext->nb_streams; i++) {
}
/* Changed sometime between 50.5.0 and 52.7.0 */
#else /* older version */
#endif /* older version */
}
}
/* We have already freed the stream above */
mpStream = 0;
if (mTempRGBBuffer != 0)
if (mYUVBuffer != 0)
if (mFrame != 0)
if (mOutBuf != 0)
if (mRGBBuffer != 0)
}
// public methods only for internal purposes
/////////////////////////////////////////////////////////////////////////////
/**
* Perform any parts of the initialisation which could potentially fail
* for reasons other than "out of memory".
*
* @returns COM status code
* @param width width to be used for MPEG frame framebuffer and initially
* for the guest frame buffer - must be a multiple of two
* @param height height to be used for MPEG frame framebuffer and
* initially for the guest framebuffer - must be a multiple
* of two
* @param depth depth to be used initially for the guest framebuffer
*/
{
LogFlow(("Initialising FFmpegFB object %p\n", this));
if (mOutOfMemory == true)
return E_OUTOFMEMORY;
int rcSetupLibrary = setup_library();
int rcSetupFormat = setup_output_format();
int rcOpenCodec = open_codec();
int rcOpenFile = open_output_file();
/* Fill in the picture data for the AVFrame - not particularly
elegant, but that is the API. */
/* Set the initial framebuffer size to the mpeg frame dimensions */
/* Start counting time */
mLastTime = RTTimeMilliTS();
return rc;
}
// IFramebuffer properties
/////////////////////////////////////////////////////////////////////////////
/**
* Return the address of the frame buffer for the virtual VGA device to
* write to. If COMGETTER(UsesGuestVRAM) returns FLASE (or if this address
* is not the same as the guests VRAM buffer), the device will perform
* translation.
*
* @returns COM status code
* @retval address The address of the buffer
*/
{
if (!address)
return E_POINTER;
return S_OK;
}
/**
* Return the width of our frame buffer.
*
* @returns COM status code
* @retval width The width of the frame buffer
*/
{
if (!width)
return E_POINTER;
LogFlow(("FFmpeg::COMGETTER(Width): returning width %lu\n",
(unsigned long) mGuestWidth));
*width = mGuestWidth;
return S_OK;
}
/**
* Return the height of our frame buffer.
*
* @returns COM status code
* @retval height The height of the frame buffer
*/
{
if (!height)
return E_POINTER;
LogFlow(("FFmpeg::COMGETTER(Height): returning height %lu\n",
(unsigned long) mGuestHeight));
*height = mGuestHeight;
return S_OK;
}
/**
* Return the colour depth of our frame buffer. Note that we actually
* store the pixel format, not the colour depth internally, since
* when display sets FramebufferPixelFormat_Opaque, it
* wants to retreive FramebufferPixelFormat_Opaque and
* nothing else.
*
* @returns COM status code
* @retval bitsPerPixel The colour depth of the frame buffer
*/
{
if (!bitsPerPixel)
return E_POINTER;
LogFlow(("FFmpeg::COMGETTER(BitsPerPixel): returning depth %lu\n",
(unsigned long) *bitsPerPixel));
return S_OK;
}
/**
* Return the number of bytes per line in our frame buffer.
*
* @returns COM status code
* @retval bytesPerLine The number of bytes per line
*/
{
if (!bytesPerLine)
return E_POINTER;
LogFlow(("FFmpeg::COMGETTER(BytesPerLine): returning line size %lu\n",
(unsigned long) mBytesPerLine));
return S_OK;
}
/**
* Return the pixel layout of our frame buffer.
*
* @returns COM status code
* @retval pixelFormat The pixel layout
*/
{
if (!pixelFormat)
return E_POINTER;
LogFlow(("FFmpeg::COMGETTER(PixelFormat): returning pixel format: %lu\n",
(unsigned long) mPixelFormat));
return S_OK;
}
/**
* Return whether we use the guest VRAM directly.
*
* @returns COM status code
* @retval pixelFormat The pixel layout
*/
{
if (!usesGuestVRAM)
return E_POINTER;
LogFlow(("FFmpeg::COMGETTER(UsesGuestVRAM): uses guest VRAM? %d\n",
mRGBBuffer == NULL));
return S_OK;
}
/**
* Return the number of lines of our frame buffer which can not be used
* (e.g. for status lines etc?).
*
* @returns COM status code
* @retval heightReduction The number of unused lines
*/
{
if (!heightReduction)
return E_POINTER;
/* no reduction */
*heightReduction = 0;
LogFlow(("FFmpeg::COMGETTER(HeightReduction): returning 0\n"));
return S_OK;
}
/**
* Return a pointer to the alpha-blended overlay used to render status icons
* etc above the framebuffer.
*
* @returns COM status code
* @retval aOverlay The overlay framebuffer
*/
{
if (!aOverlay)
return E_POINTER;
/* not yet implemented */
*aOverlay = 0;
LogFlow(("FFmpeg::COMGETTER(Overlay): returning 0\n"));
return S_OK;
}
/**
* Return id of associated window
*
* @returns COM status code
* @retval winId Associated window id
*/
{
if (!winId)
return E_POINTER;
*winId = 0;
return S_OK;
}
// IFramebuffer methods
/////////////////////////////////////////////////////////////////////////////
{
LogFlow(("FFmpeg::Lock: called\n"));
if (rc == VINF_SUCCESS)
return S_OK;
return E_UNEXPECTED;
}
{
LogFlow(("FFmpeg::Unlock: called\n"));
return S_OK;
}
/**
* This method is used to notify us that an area of the guest framebuffer
* has been updated.
*
* @returns COM status code
* @param x X co-ordinate of the upper left-hand corner of the
* area which has been updated
* @param y Y co-ordinate of the upper left-hand corner of the
* area which has been updated
* @param w width of the area which has been updated
* @param h height of the area which has been updated
*/
{
int rc;
LogFlow(("FFmpeg::NotifyUpdate called: x=%lu, y=%lu, w=%lu, h=%lu\n",
(unsigned long) x, (unsigned long) y, (unsigned long) w,
(unsigned long) h));
/* We always leave at least one frame update pending, which we
process when the time until the next frame has elapsed. */
{
{
copy_to_intermediate_buffer(x, y, w, h);
return rc;
}
rc = do_encoding_and_write();
{
copy_to_intermediate_buffer(x, y, w, h);
return rc;
}
/* Write frames for the time in-between. Not a good way
to handle this. */
{
/* rc = do_rgb_to_yuv_conversion();
if (rc != S_OK)
{
copy_to_intermediate_buffer(x, y, w, h);
return rc;
}
{
copy_to_intermediate_buffer(x, y, w, h);
return rc;
}
}
}
/* Finally we copy the updated data to the intermediate buffer,
ready for the next update. */
copy_to_intermediate_buffer(x, y, w, h);
return S_OK;
}
/**
* Requests a resize of our "screen".
*
* @returns COM status code
* @param pixelFormat Layout of the guest video RAM (i.e. 16, 24,
* 32 bpp)
* @param vram host context pointer to the guest video RAM,
* in case we can cope with the format
* @param bitsPerPixel color depth of the guest video RAM
* @param bytesPerLine length of a screen line in the guest video RAM
* @param w video mode width in pixels
* @param h video mode height in pixels
* @retval finished set to true if the method is synchronous and
* to false otherwise
*
* This method is called when the guest attempts to resize the virtual
* screen. The pointer to the guest's video RAM is supplied in case
* the framebuffer can handle the pixel format. If it can't, it should
* allocate a memory buffer itself, and the virtual VGA device will copy
* the guest VRAM to that in a format we can handle. The
* COMGETTER(UsesGuestVRAM) method is used to tell the VGA device which method
* we have chosen, and the other COMGETTER methods tell the device about
* the layout of our buffer. We currently handle all VRAM layouts except
* FramebufferPixelFormat_Opaque (which cannot be handled by
* definition).
*/
{
if (!finished)
return E_POINTER;
LogFlow(("FFmpeg::RequestResize called: pixelFormat=%lu, vram=%lu, "
"bpp=%lu bpl=%lu, w=%lu, h=%lu\n",
(unsigned long) pixelFormat, (unsigned long) vram,
(unsigned long) bitsPerPixel, (unsigned long) bytesPerLine,
(unsigned long) w, (unsigned long) h));
/* For now, we are doing things synchronously */
*finished = true;
/* We always reallocate our buffer */
if (mRGBBuffer)
mGuestWidth = w;
mGuestHeight = h;
bool fallback = false;
/* See if there are conditions under which we can use the guest's VRAM,
* fallback to our own memory buffer otherwise */
{
switch (bitsPerPixel)
{
case 32:
Log2(("FFmpeg::RequestResize: setting ffmpeg pixel format to PIX_FMT_RGBA32\n"));
break;
case 24:
Log2(("FFmpeg::RequestResize: setting ffmpeg pixel format to PIX_FMT_RGB24\n"));
break;
case 16:
Log2(("FFmpeg::RequestResize: setting ffmpeg pixel format to PIX_FMT_RGB565\n"));
break;
default:
fallback = true;
break;
}
}
else
{
fallback = true;
}
if (!fallback)
{
mRGBBuffer = 0;
Log2(("FFmpeg::RequestResize: setting mBufferAddress to vram and mLineSize to %lu\n",
(unsigned long) mBytesPerLine));
}
else
{
/* we always fallback to 32bpp RGB */
Log2(("FFmpeg::RequestResize: setting ffmpeg pixel format to PIX_FMT_RGBA32\n"));
mBytesPerLine = w * 4;
mBitsPerPixel = 32;
Log2(("FFmpeg::RequestResize: alloc'ing mBufferAddress and mRGBBuffer to %p and mBytesPerLine to %lu\n",
mBufferAddress, (unsigned long) mBytesPerLine));
}
/* Blank out the intermediate frame framebuffer */
return S_OK;
}
/**
* Returns whether we like the given video mode.
*
* @returns COM status code
* @param width video mode width in pixels
* @param height video mode height in pixels
* @param bpp video mode bit depth in bits per pixel
* @param supported pointer to result variable
*
* As far as I know, the only restruction we have on video modes is that
* we have to have an even number of horizontal and vertical pixels.
* I sincerely doubt that anything else will be requested, and if it
* is anyway, we will just silently amputate one line when we write to
* the mpeg file.
*/
{
if (!supported)
return E_POINTER;
*supported = true;
return S_OK;
}
/** Stubbed */
STDMETHODIMP FFmpegFB::GetVisibleRegion(BYTE *rectangles, ULONG /* count */, ULONG * /* countCopied */)
{
if (!rectangles)
return E_POINTER;
*rectangles = 0;
return S_OK;
}
/** Stubbed */
{
if (!rectangles)
return E_POINTER;
return S_OK;
}
{
return E_NOTIMPL;
}
// Private Methods
//////////////////////////////////////////////////////////////////////////
//
{
/* Set up the avcodec library */
avcodec_init();
/* Register all codecs in the library. */
/* Register all formats in the format library */
sizeof(mpFormatContext->filename));
return S_OK;
}
/**
* Determine the correct output format and codec for our MPEG file.
*
* @returns COM status code
*
* @pre The format context (mpFormatContext) should have already been
* allocated.
*/
{
Assert(mpFormatContext != 0);
0);
#ifdef VBOX_SHOW_AVAILABLE_FORMATS
if (!pOutFormat)
{
RTPrintf("Could not guess an output format for that extension.\n"
"Available formats:\n");
list_formats();
}
#endif
AssertMsgReturn(pOutFormat != 0,
("Could not deduce output format from file name\n"),
("Can't handle output format for file\n"),
("pOutFormat->flags=%x, pOutFormat->name=%s\n",
("No video codec available - you have probably selected a non-video file format\n"), E_UNEXPECTED);
/* Set format specific parameters - requires the format to be set. */
#if 1 /* bird: This works for me on the mac, please review & test elsewhere. */
/* Fill in any uninitialized parameters like opt_output_file in ffpmeg.c does.
This fixes most of the buffer underflow warnings:
if (!mpFormatContext->preload)
if (!mpFormatContext->max_delay)
#endif
return S_OK;
}
{
{
{
{
}
}
}
return S_OK;
}
/**
* Open the FFmpeg codec and set it up (width, etc) for our MPEG file.
*
* @returns COM status code
*
* @pre The format context (mpFormatContext) and the stream (mpStream)
* should have already been allocated.
*/
{
Assert(mpFormatContext != 0);
#ifdef VBOX_SHOW_AVAILABLE_FORMATS
if (!pCodec)
{
RTPrintf("Could not find a suitable codec for the output format on your system\n"
"Available formats:\n");
list_formats();
}
#endif
/* taken from the ffmpeg output example */
// some formats want stream headers to be seperate
/* end output example section */
return S_OK;
}
/**
* Open our MPEG file and write the header.
*
* @returns COM status code
*
* @pre The format context (mpFormatContext) and the stream (mpStream)
* should have already been allocated and set up.
*/
{
char szFileName[RTPATH_MAX];
mfUrlOpen = true;
return S_OK;
}
/**
* Copy an area from the output buffer used by the virtual VGA (may
* just be the guest's VRAM) to our fixed size intermediate buffer.
* The picture in the intermediate buffer is centred if the guest
* screen dimensions are smaller and amputated if they are larger than
* our frame dimensions.
*
* @param x X co-ordinate of the upper left-hand corner of the
* area which has been updated
* @param y Y co-ordinate of the upper left-hand corner of the
* area which has been updated
* @param w width of the area which has been updated
* @param h height of the area which has been updated
*/
{
Log2(("FFmpegFB::copy_to_intermediate_buffer: x=%lu, y=%lu, w=%lu, h=%lu\n",
(unsigned long) x, (unsigned long) y, (unsigned long) w, (unsigned long) h));
/* Perform clipping and calculate the destination co-ordinates */
return;
{
x = -xDiff;
destX = 0;
}
else
return;
{
y = -yDiff;
destY = 0;
}
else
return; /* nothing visible */
if (destX + w > mFrameWidth)
w = mFrameWidth - destX;
if (destY + h > mFrameHeight)
h = mFrameHeight - destY;
/* Calculate bytes per pixel */
{
switch (mBitsPerPixel)
{
case 32:
case 24:
case 16:
break;
default:
bpp = 1;
break;
}
}
else
{
bpp = 1;
}
/* Calculate start offset in source and destination buffers */
/* do the copy */
for (unsigned int i = 0; i < h; i++)
{
/* Overflow check */
w * bpp);
}
}
/**
* Copy the RGB data in the intermediate framebuffer to YUV data in
* the YUV framebuffer.
*
* @returns COM status code
*/
{
switch (mFFMPEGPixelFormat)
{
case PIX_FMT_RGBA32:
return E_UNEXPECTED;
break;
case PIX_FMT_RGB24:
return E_UNEXPECTED;
break;
case PIX_FMT_RGB565:
return E_UNEXPECTED;
break;
default:
return E_UNEXPECTED;
}
return S_OK;
}
/**
* Encode the YUV framebuffer as an MPEG frame and write it to the file.
*
* @returns COM status code
*/
{
/* A hack: ffmpeg mpeg2 only writes a frame if something has
changed. So we flip the low luminance bit of the first
pixel every frame. */
if (mToggle)
mYUVBuffer[0] |= 1;
else
mYUVBuffer[0] &= 0xfe;
mFrame);
AssertMsgReturn(cSize >= 0,
("avcodec_encode_video() failed with rc=%d.\n"
"mFrameWidth=%u, mFrameHeight=%u\n", cSize,
if (cSize > 0)
{
/* write the compressed frame in the media file */
}
return S_OK;
}
/**
* Capture the current (i.e. the last) frame as a PNG file with the
* same basename as the captured video file.
*/
{
/* Work out the new file name - for some reason, we can't use
the com::Utf8Str() directly, but have to copy it */
if (baseLen == 0)
/* Open output file */
if (fp == 0)
{
goto fopen_failed;
}
/* Create libpng basic structures */
0 /* error function */, 0 /* warning function */);
if (png_ptr == 0)
if (info_ptr == 0)
{
}
/* Convert image to standard RGB24 to simplify life */
* mFrameHeight * 4));
if (PNGBuffer == 0)
goto av_malloc_buffer_failed;
* sizeof(png_bytep)));
if (row_pointers == 0)
switch (mFFMPEGPixelFormat)
{
case PIX_FMT_RGBA32:
goto setjmp_exception;
break;
case PIX_FMT_RGB24:
goto setjmp_exception;
break;
case PIX_FMT_RGB565:
goto setjmp_exception;
break;
default:
goto setjmp_exception;
}
/* libpng exception handling */
goto setjmp_exception;
/* pass libpng the file pointer */
/* set the image properties */
/* set up the information about the bitmap for libpng */
for (unsigned i = 1; i < mFrameHeight; i++)
/* and write the thing! */
/* drop through to cleanup */
Log(("FFmpegFB::write_png: Failed to write .png image of final frame\n"));
return errorCode;
}
#ifdef VBOX_WITH_XPCOM
#endif