diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 03:01:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 03:01:46 +0000 |
commit | f8fe689a81f906d1b91bb3220acde2a4ecb14c5b (patch) | |
tree | 26484e9d7e2c67806c2d1760196ff01aaa858e8c /src/VBox/Main/src-client/Recording.cpp | |
parent | Initial commit. (diff) | |
download | virtualbox-upstream.tar.xz virtualbox-upstream.zip |
Adding upstream version 6.0.4-dfsg.upstream/6.0.4-dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/VBox/Main/src-client/Recording.cpp')
-rw-r--r-- | src/VBox/Main/src-client/Recording.cpp | 718 |
1 files changed, 718 insertions, 0 deletions
diff --git a/src/VBox/Main/src-client/Recording.cpp b/src/VBox/Main/src-client/Recording.cpp new file mode 100644 index 00000000..522ad627 --- /dev/null +++ b/src/VBox/Main/src-client/Recording.cpp @@ -0,0 +1,718 @@ +/* $Id: Recording.cpp $ */ +/** @file + * Recording context code. + * + * This code employs a separate encoding thread per recording context + * to keep time spent in EMT as short as possible. Each configured VM display + * is represented by an own recording stream, which in turn has its own rendering + * queue. Common recording data across all recording streams is kept in a + * separate queue in the recording context to minimize data duplication and + * multiplexing overhead in EMT. + */ + +/* + * Copyright (C) 2012-2019 Oracle Corporation + * + * This file is part of VirtualBox Open Source Edition (OSE), as + * available from http://www.virtualbox.org. This file is free software; + * you can redistribute it and/or modify it under the terms of the GNU + * 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. + */ + +#ifdef LOG_GROUP +# undef LOG_GROUP +#endif +#define LOG_GROUP LOG_GROUP_MAIN_DISPLAY +#include "LoggingNew.h" + +#include <stdexcept> +#include <vector> + +#include <iprt/asm.h> +#include <iprt/assert.h> +#include <iprt/critsect.h> +#include <iprt/path.h> +#include <iprt/semaphore.h> +#include <iprt/thread.h> +#include <iprt/time.h> + +#include <VBox/err.h> +#include <VBox/com/VirtualBox.h> + +#include "ConsoleImpl.h" +#include "Recording.h" +#include "RecordingInternals.h" +#include "RecordingStream.h" +#include "RecordingUtils.h" +#include "WebMWriter.h" + +using namespace com; + +#ifdef DEBUG_andy +/** Enables dumping audio / video data for debugging reasons. */ +//# define VBOX_RECORDING_DUMP +#endif + +#ifdef VBOX_RECORDING_DUMP +#pragma pack(push) +#pragma pack(1) +typedef struct +{ + uint16_t u16Magic; + uint32_t u32Size; + uint16_t u16Reserved1; + uint16_t u16Reserved2; + uint32_t u32OffBits; +} RECORDINGBMPHDR, *PRECORDINGBMPHDR; +AssertCompileSize(RECORDINGBMPHDR, 14); + +typedef struct +{ + uint32_t u32Size; + uint32_t u32Width; + uint32_t u32Height; + uint16_t u16Planes; + uint16_t u16BitCount; + uint32_t u32Compression; + uint32_t u32SizeImage; + uint32_t u32XPelsPerMeter; + uint32_t u32YPelsPerMeter; + uint32_t u32ClrUsed; + uint32_t u32ClrImportant; +} RECORDINGBMPDIBHDR, *PRECORDINGBMPDIBHDR; +AssertCompileSize(RECORDINGBMPDIBHDR, 40); + +#pragma pack(pop) +#endif /* VBOX_RECORDING_DUMP */ + + +RecordingContext::RecordingContext(Console *a_pConsole, const settings::RecordingSettings &a_Settings) + : pConsole(a_pConsole) + , enmState(RECORDINGSTS_UNINITIALIZED) + , cStreamsEnabled(0) +{ + int rc = RecordingContext::createInternal(a_Settings); + if (RT_FAILURE(rc)) + throw rc; +} + +RecordingContext::~RecordingContext(void) +{ + destroyInternal(); +} + +/** + * Worker thread for all streams of a recording context. + * + * For video frames, this also does the RGB/YUV conversion and encoding. + */ +DECLCALLBACK(int) RecordingContext::threadMain(RTTHREAD hThreadSelf, void *pvUser) +{ + RecordingContext *pThis = (RecordingContext *)pvUser; + + /* Signal that we're up and rockin'. */ + RTThreadUserSignal(hThreadSelf); + + LogFunc(("Thread started\n")); + + for (;;) + { + int rc = RTSemEventWait(pThis->WaitEvent, RT_INDEFINITE_WAIT); + AssertRCBreak(rc); + + Log2Func(("Processing %zu streams\n", pThis->vecStreams.size())); + + /** @todo r=andy This is inefficient -- as we already wake up this thread + * for every screen from Main, we here go again (on every wake up) through + * all screens. */ + RecordingStreams::iterator itStream = pThis->vecStreams.begin(); + while (itStream != pThis->vecStreams.end()) + { + RecordingStream *pStream = (*itStream); + + rc = pStream->Process(pThis->mapBlocksCommon); + if (RT_FAILURE(rc)) + { + LogRel(("Recording: Processing stream #%RU16 failed (%Rrc)\n", pStream->GetID(), rc)); + break; + } + + ++itStream; + } + + if (RT_FAILURE(rc)) + LogRel(("Recording: Encoding thread failed (%Rrc)\n", rc)); + + /* Keep going in case of errors. */ + + if (ASMAtomicReadBool(&pThis->fShutdown)) + { + LogFunc(("Thread is shutting down ...\n")); + break; + } + + } /* for */ + + LogFunc(("Thread ended\n")); + return VINF_SUCCESS; +} + +/** + * Notifies a recording context's encoding thread. + * + * @returns IPRT status code. + */ +int RecordingContext::threadNotify(void) +{ + return RTSemEventSignal(this->WaitEvent); +} + +/** + * Creates a recording context. + * + * @returns IPRT status code. + * @param a_Settings Recording settings to use for context creation. + */ +int RecordingContext::createInternal(const settings::RecordingSettings &a_Settings) +{ + int rc = RTCritSectInit(&this->CritSect); + if (RT_FAILURE(rc)) + return rc; + + settings::RecordingScreenMap::const_iterator itScreen = a_Settings.mapScreens.begin(); + while (itScreen != a_Settings.mapScreens.end()) + { + RecordingStream *pStream = NULL; + try + { + pStream = new RecordingStream(this, itScreen->first /* Screen ID */, itScreen->second); + this->vecStreams.push_back(pStream); + if (itScreen->second.fEnabled) + this->cStreamsEnabled++; + } + catch (std::bad_alloc &) + { + rc = VERR_NO_MEMORY; + break; + } + + ++itScreen; + } + + if (RT_SUCCESS(rc)) + { + this->tsStartMs = RTTimeMilliTS(); + this->enmState = RECORDINGSTS_CREATED; + this->fShutdown = false; + + /* Copy the settings to our context. */ + this->Settings = a_Settings; + + rc = RTSemEventCreate(&this->WaitEvent); + AssertRCReturn(rc, rc); + } + + if (RT_FAILURE(rc)) + destroyInternal(); + + return rc; +} + +/** + * Starts a recording context by creating its worker thread. + * + * @returns IPRT status code. + */ +int RecordingContext::startInternal(void) +{ + if (this->enmState == RECORDINGSTS_STARTED) + return VINF_SUCCESS; + + Assert(this->enmState == RECORDINGSTS_CREATED); + + int rc = RTThreadCreate(&this->Thread, RecordingContext::threadMain, (void *)this, 0, + RTTHREADTYPE_MAIN_WORKER, RTTHREADFLAGS_WAITABLE, "Record"); + + if (RT_SUCCESS(rc)) /* Wait for the thread to start. */ + rc = RTThreadUserWait(this->Thread, 30 * RT_MS_1SEC /* 30s timeout */); + + if (RT_SUCCESS(rc)) + { + LogRel(("Recording: Started\n")); + this->enmState = RECORDINGSTS_STARTED; + } + else + Log(("Recording: Failed to start (%Rrc)\n", rc)); + + return rc; +} + +/** + * Stops a recording context by telling the worker thread to stop and finalizing its operation. + * + * @returns IPRT status code. + */ +int RecordingContext::stopInternal(void) +{ + if (this->enmState != RECORDINGSTS_STARTED) + return VINF_SUCCESS; + + LogThisFunc(("Shutting down thread ...\n")); + + /* Set shutdown indicator. */ + ASMAtomicWriteBool(&this->fShutdown, true); + + /* Signal the thread and wait for it to shut down. */ + int rc = threadNotify(); + if (RT_SUCCESS(rc)) + rc = RTThreadWait(this->Thread, 30 * 1000 /* 10s timeout */, NULL); + + lock(); + + if (RT_SUCCESS(rc)) + { + LogRel(("Recording: Stopped\n")); + this->enmState = RECORDINGSTS_CREATED; + } + else + Log(("Recording: Failed to stop (%Rrc)\n", rc)); + + unlock(); + + LogFlowThisFunc(("%Rrc\n", rc)); + return rc; +} + +/** + * Destroys a recording context, internal version. + */ +void RecordingContext::destroyInternal(void) +{ + if (this->enmState == RECORDINGSTS_UNINITIALIZED) + return; + + int rc = stopInternal(); + AssertRCReturnVoid(rc); + + lock(); + + rc = RTSemEventDestroy(this->WaitEvent); + AssertRCReturnVoid(rc); + + this->WaitEvent = NIL_RTSEMEVENT; + + RecordingStreams::iterator it = this->vecStreams.begin(); + while (it != this->vecStreams.end()) + { + RecordingStream *pStream = (*it); + + rc = pStream->Uninit(); + AssertRC(rc); + + delete pStream; + pStream = NULL; + + this->vecStreams.erase(it); + it = this->vecStreams.begin(); + } + + /* Sanity. */ + Assert(this->vecStreams.empty()); + Assert(this->mapBlocksCommon.size() == 0); + + unlock(); + + if (RTCritSectIsInitialized(&this->CritSect)) + { + Assert(RTCritSectGetWaiters(&this->CritSect) == -1); + RTCritSectDelete(&this->CritSect); + } + + this->enmState = RECORDINGSTS_UNINITIALIZED; +} + +/** + * Returns a recording context's current settings. + * + * @returns The recording context's current settings. + */ +const settings::RecordingSettings &RecordingContext::GetConfig(void) const +{ + return this->Settings; +} + +/** + * Returns the recording stream for a specific screen. + * + * @returns Recording stream for a specific screen, or NULL if not found. + * @param uScreen Screen ID to retrieve recording stream for. + */ +RecordingStream *RecordingContext::getStreamInternal(unsigned uScreen) const +{ + RecordingStream *pStream; + + try + { + pStream = this->vecStreams.at(uScreen); + } + catch (std::out_of_range &) + { + pStream = NULL; + } + + return pStream; +} + +int RecordingContext::lock(void) +{ + int rc = RTCritSectEnter(&this->CritSect); + AssertRC(rc); + return rc; +} + +int RecordingContext::unlock(void) +{ + int rc = RTCritSectLeave(&this->CritSect); + AssertRC(rc); + return rc; +} + +/** + * Retrieves a specific recording stream of a recording context. + * + * @returns Pointer to recording stream if found, or NULL if not found. + * @param uScreen Screen number of recording stream to look up. + */ +RecordingStream *RecordingContext::GetStream(unsigned uScreen) const +{ + return getStreamInternal(uScreen); +} + +/** + * Returns the number of configured recording streams for a recording context. + * + * @returns Number of configured recording streams. + */ +size_t RecordingContext::GetStreamCount(void) const +{ + return this->vecStreams.size(); +} + +/** + * Creates a new recording context. + * + * @returns IPRT status code. + * @param a_Settings Recording settings to use for creation. + * + */ +int RecordingContext::Create(const settings::RecordingSettings &a_Settings) +{ + return createInternal(a_Settings); +} + +/** + * Destroys a recording context. + */ +void RecordingContext::Destroy(void) +{ + destroyInternal(); +} + +/** + * Starts a recording context. + * + * @returns IPRT status code. + */ +int RecordingContext::Start(void) +{ + return startInternal(); +} + +/** + * Stops a recording context. + */ +int RecordingContext::Stop(void) +{ + return stopInternal(); +} + +/** + * Returns if a specific recoding feature is enabled for at least one of the attached + * recording streams or not. + * + * @returns \c true if at least one recording stream has this feature enabled, or \c false if + * no recording stream has this feature enabled. + * @param enmFeature Recording feature to check for. + */ +bool RecordingContext::IsFeatureEnabled(RecordingFeature_T enmFeature) +{ + lock(); + + RecordingStreams::const_iterator itStream = this->vecStreams.begin(); + while (itStream != this->vecStreams.end()) + { + if ((*itStream)->GetConfig().isFeatureEnabled(enmFeature)) + { + unlock(); + return true; + } + ++itStream; + } + + unlock(); + + return false; +} + +/** + * Returns if this recording context is ready to start recording. + * + * @returns @c true if recording context is ready, @c false if not. + */ +bool RecordingContext::IsReady(void) +{ + lock(); + + const bool fIsReady = this->enmState >= RECORDINGSTS_CREATED; + + unlock(); + + return fIsReady; +} + +/** + * Returns if this recording context is ready to accept new recording data for a given screen. + * + * @returns @c true if the specified screen is ready, @c false if not. + * @param uScreen Screen ID. + * @param msTimestamp Current timestamp (in ms). Currently not being used. + */ +bool RecordingContext::IsReady(uint32_t uScreen, uint64_t msTimestamp) +{ + RT_NOREF(msTimestamp); + + lock(); + + bool fIsReady = false; + + if (this->enmState != RECORDINGSTS_STARTED) + { + const RecordingStream *pStream = GetStream(uScreen); + if (pStream) + fIsReady = pStream->IsReady(); + + /* Note: Do not check for other constraints like the video FPS rate here, + * as this check then also would affect other (non-FPS related) stuff + * like audio data. */ + } + + unlock(); + + return fIsReady; +} + +/** + * Returns whether a given recording context has been started or not. + * + * @returns true if active, false if not. + */ +bool RecordingContext::IsStarted(void) +{ + lock(); + + const bool fIsStarted = this->enmState == RECORDINGSTS_STARTED; + + unlock(); + + return fIsStarted; +} + +/** + * Checks if a specified limit for recording has been reached. + * + * @returns true if any limit has been reached. + */ +bool RecordingContext::IsLimitReached(void) +{ + lock(); + + LogFlowThisFunc(("cStreamsEnabled=%RU16\n", this->cStreamsEnabled)); + + const bool fLimitReached = this->cStreamsEnabled == 0; + + unlock(); + + return fLimitReached; +} + +/** + * Checks if a specified limit for recording has been reached. + * + * @returns true if any limit has been reached. + * @param uScreen Screen ID. + * @param msTimestamp Timestamp (in ms) to check for. + */ +bool RecordingContext::IsLimitReached(uint32_t uScreen, uint64_t msTimestamp) +{ + lock(); + + bool fLimitReached = false; + + const RecordingStream *pStream = getStreamInternal(uScreen); + if ( !pStream + || pStream->IsLimitReached(msTimestamp)) + { + fLimitReached = true; + } + + unlock(); + + return fLimitReached; +} + +DECLCALLBACK(int) RecordingContext::OnLimitReached(uint32_t uScreen, int rc) +{ + RT_NOREF(uScreen, rc); + LogFlowThisFunc(("Stream %RU32 has reached its limit (%Rrc)\n", uScreen, rc)); + + lock(); + + Assert(this->cStreamsEnabled); + this->cStreamsEnabled--; + + LogFlowThisFunc(("cStreamsEnabled=%RU16\n", cStreamsEnabled)); + + unlock(); + + return VINF_SUCCESS; +} + +/** + * Sends an audio frame to the video encoding thread. + * + * @thread EMT + * + * @returns IPRT status code. + * @param pvData Audio frame data to send. + * @param cbData Size (in bytes) of (encoded) audio frame data. + * @param msTimestamp Timestamp (in ms) of audio playback. + */ +int RecordingContext::SendAudioFrame(const void *pvData, size_t cbData, uint64_t msTimestamp) +{ +#ifdef VBOX_WITH_AUDIO_RECORDING + AssertPtrReturn(pvData, VERR_INVALID_POINTER); + AssertReturn(cbData, VERR_INVALID_PARAMETER); + + /* To save time spent in EMT, do the required audio multiplexing in the encoding thread. + * + * The multiplexing is needed to supply all recorded (enabled) screens with the same + * audio data at the same given point in time. + */ + RecordingBlock *pBlock = new RecordingBlock(); + pBlock->enmType = RECORDINGBLOCKTYPE_AUDIO; + + PRECORDINGAUDIOFRAME pFrame = (PRECORDINGAUDIOFRAME)RTMemAlloc(sizeof(RECORDINGAUDIOFRAME)); + AssertPtrReturn(pFrame, VERR_NO_MEMORY); + + pFrame->pvBuf = (uint8_t *)RTMemAlloc(cbData); + AssertPtrReturn(pFrame->pvBuf, VERR_NO_MEMORY); + pFrame->cbBuf = cbData; + + memcpy(pFrame->pvBuf, pvData, cbData); + + pBlock->pvData = pFrame; + pBlock->cbData = sizeof(RECORDINGAUDIOFRAME) + cbData; + pBlock->cRefs = this->cStreamsEnabled; + pBlock->msTimestamp = msTimestamp; + + lock(); + + int rc; + + try + { + RecordingBlockMap::iterator itBlocks = this->mapBlocksCommon.find(msTimestamp); + if (itBlocks == this->mapBlocksCommon.end()) + { + RecordingBlocks *pRecordingBlocks = new RecordingBlocks(); + pRecordingBlocks->List.push_back(pBlock); + + this->mapBlocksCommon.insert(std::make_pair(msTimestamp, pRecordingBlocks)); + } + else + itBlocks->second->List.push_back(pBlock); + + rc = VINF_SUCCESS; + } + catch (const std::exception &ex) + { + RT_NOREF(ex); + rc = VERR_NO_MEMORY; + } + + unlock(); + + if (RT_SUCCESS(rc)) + rc = threadNotify(); + + return rc; +#else + RT_NOREF(pvData, cbData, msTimestamp); + return VINF_SUCCESS; +#endif +} + +/** + * Copies a source video frame to the intermediate RGB buffer. + * This function is executed only once per time. + * + * @thread EMT + * + * @returns IPRT status code. + * @param uScreen Screen number to send video frame to. + * @param x Starting x coordinate of the video frame. + * @param y Starting y coordinate of the video frame. + * @param uPixelFormat Pixel format. + * @param uBPP Bits Per Pixel (BPP). + * @param uBytesPerLine Bytes per scanline. + * @param uSrcWidth Width of the video frame. + * @param uSrcHeight Height of the video frame. + * @param puSrcData Pointer to video frame data. + * @param msTimestamp Timestamp (in ms). + */ +int RecordingContext::SendVideoFrame(uint32_t uScreen, uint32_t x, uint32_t y, + uint32_t uPixelFormat, uint32_t uBPP, uint32_t uBytesPerLine, + uint32_t uSrcWidth, uint32_t uSrcHeight, uint8_t *puSrcData, + uint64_t msTimestamp) +{ + AssertReturn(uSrcWidth, VERR_INVALID_PARAMETER); + AssertReturn(uSrcHeight, VERR_INVALID_PARAMETER); + AssertReturn(puSrcData, VERR_INVALID_POINTER); + + lock(); + + RecordingStream *pStream = GetStream(uScreen); + if (!pStream) + { + unlock(); + + AssertFailed(); + return VERR_NOT_FOUND; + } + + int rc = pStream->SendVideoFrame(x, y, uPixelFormat, uBPP, uBytesPerLine, uSrcWidth, uSrcHeight, puSrcData, msTimestamp); + + unlock(); + + if ( RT_SUCCESS(rc) + && rc != VINF_RECORDING_THROTTLED) /* Only signal the thread if operation was successful. */ + { + threadNotify(); + } + + return rc; +} + |