/* $Id: Recording.cpp 93115 2022-01-01 11:31:46Z vboxsync $ */ /** @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-2022 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 #include #include #include #include #include #include #include #include #include #include #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 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; }