/* $Id: Recording.cpp 107496 2025-01-08 10:57:35Z 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-2024 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * SPDX-License-Identifier: GPL-3.0-only */ #ifdef LOG_GROUP # undef LOG_GROUP #endif #define LOG_GROUP LOG_GROUP_RECORDING #include "LoggingNew.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "ConsoleImpl.h" #include "ProgressImpl.h" #include "Recording.h" #include "RecordingInternals.h" #include "RecordingStream.h" #include "RecordingUtils.h" #include "WebMWriter.h" #include "VirtualBoxErrorInfoImpl.h" using namespace com; #ifdef DEBUG_andy /** Enables dumping audio / video data for debugging reasons. */ //# define VBOX_RECORDING_DUMP #endif RecordingCursorState::RecordingCursorState() : m_fFlags(VBOX_RECORDING_CURSOR_F_NONE) { m_Shape.Pos.x = UINT16_MAX; m_Shape.Pos.y = UINT16_MAX; RT_ZERO(m_Shape); } RecordingCursorState::~RecordingCursorState() { Destroy(); } /** * Destroys a cursor state. */ void RecordingCursorState::Destroy(void) { RecordingVideoFrameDestroy(&m_Shape); } /** * Creates or updates the cursor shape. * * @returns VBox status code. * @param fAlpha Whether the pixel data contains alpha channel information or not. * @param uWidth Width (in pixel) of new cursor shape. * @param uHeight Height (in pixel) of new cursor shape. * @param pu8Shape Pixel data of new cursor shape. * @param cbShape Bytes of \a pu8Shape. */ int RecordingCursorState::CreateOrUpdate(bool fAlpha, uint32_t uWidth, uint32_t uHeight, const uint8_t *pu8Shape, size_t cbShape) { int vrc; uint32_t fFlags = RECORDINGVIDEOFRAME_F_VISIBLE; const uint8_t uBPP = 32; /* Seems to be fixed. */ uint32_t offShape; if (fAlpha) { /* Calculate the offset to the actual pixel data. */ offShape = (uWidth + 7) / 8 * uHeight; /* size of the AND mask */ offShape = (offShape + 3) & ~3; AssertReturn(offShape <= cbShape, VERR_INVALID_PARAMETER); fFlags |= RECORDINGVIDEOFRAME_F_BLIT_ALPHA; } else offShape = 0; /* Cursor shape size has become bigger? Reallocate. */ if (cbShape > m_Shape.cbBuf) { RecordingVideoFrameDestroy(&m_Shape); vrc = RecordingVideoFrameInit(&m_Shape, fFlags, uWidth, uHeight, 0 /* posX */, 0 /* posY */, uBPP, RECORDINGPIXELFMT_BRGA32); } else /* Otherwise just zero out first. */ { RecordingVideoFrameClear(&m_Shape); vrc = VINF_SUCCESS; } if (RT_SUCCESS(vrc)) vrc = RecordingVideoFrameBlitRaw(&m_Shape, 0, 0, &pu8Shape[offShape], cbShape - offShape, 0, 0, uWidth, uHeight, uWidth * 4 /* BPP */, uBPP, m_Shape.Info.enmPixelFmt); #if 0 RecordingUtilsDbgDumpVideoFrameEx(&m_Shape, "/tmp/recording", "cursor-update"); #endif return vrc; } /** * Moves (sets) the cursor to a new position. * * @returns VBox status code. * @retval VERR_NO_CHANGE if the cursor wasn't moved (set). * @param iX New X position to set. * @param iY New Y position to set. */ int RecordingCursorState::Move(int32_t iX, int32_t iY) { /* No relative coordinates here. */ if ( iX < 0 || iY < 0) return VERR_NO_CHANGE; if ( m_Shape.Pos.x == (uint32_t)iX && m_Shape.Pos.y == (uint32_t)iY) return VERR_NO_CHANGE; m_Shape.Pos.x = (uint16_t)iX; m_Shape.Pos.y = (uint16_t)iY; return VINF_SUCCESS; } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Recording context constructor. * * @note Will throw vrc when unable to create. */ RecordingContext::RecordingContext(void) : m_pConsole(NULL) , m_enmState(RECORDINGSTS_UNINITIALIZED) , m_WaitEvent(NIL_RTSEMEVENT) , m_ulCurOp(0) , m_cOps(0) , m_fShutdown(false) , m_Thread(NIL_RTTHREAD) , m_cStreamsEnabled(0) , m_tsStartMs(0) { int vrc = RTCritSectInit(&m_CritSect); if (RT_FAILURE(vrc)) throw vrc; } RecordingContext::~RecordingContext(void) { destroyInternal(); if (RTCritSectIsInitialized(&m_CritSect)) RTCritSectDelete(&m_CritSect); } /** * Returns whether the recording progress object has been canceled or not. * * @returns \c true if canceled, or \c false if not. */ bool RecordingContext::progressIsCanceled(void) const { if (m_pProgress.isNull()) return true; BOOL fCanceled; HRESULT const hrc = m_pProgress->COMGETTER(Canceled(&fCanceled)); AssertComRC(hrc); return RT_BOOL(fCanceled); } /** * Returns whether the recording progress object has been completed or not. * * @returns \c true if completed, or \c false if not. */ bool RecordingContext::progressIsCompleted(void) const { if (m_pProgress.isNull()) return true; BOOL fCompleted; HRESULT const hrc = m_pProgress->COMGETTER(Completed(&fCompleted)); AssertComRC(hrc); return RT_BOOL(fCompleted); } /** * Creates a progress object based on the given recording settings. * * @returns VBox status code. * @param Settings Recording settings to use for creation. * @param pProgress Where to return the created progress object on success. */ int RecordingContext::progressCreate(const settings::Recording &Settings, ComObjPtr &pProgress) { /* Determine the number of operations the recording progress has. * We use the maximum time (in s) of each screen as the overall progress indicator. * If one screen is configured to be recorded indefinitely (until manually stopped), * the operation count gets reset to 1. */ ULONG cOperations = 1; /* Always start at 1. */ settings::RecordingScreenSettingsMap::const_iterator itScreen = Settings.mapScreens.begin(); while (itScreen != Settings.mapScreens.end()) { settings::RecordingScreen const &screenSettings = itScreen->second; if (screenSettings.ulMaxTimeS == 0) { cOperations = 1; /* Screen will be recorded indefinitely, reset operation count and bail out. */ break; } else cOperations = RT_MAX(cOperations, screenSettings.ulMaxTimeS); ++itScreen; } HRESULT hrc = pProgress.createObject(); if (SUCCEEDED(hrc)) { hrc = pProgress->init(static_cast(m_pConsole), Utf8Str("Recording"), TRUE /* aCancelable */, cOperations, cOperations /* ulTotalOperationsWeight */, Utf8Str("Starting"), 1 /* ulFirstOperationWeight */); if (SUCCEEDED(hrc)) pProgress->i_setCancelCallback(RecordingContext::s_progressCancelCallback, this /* pvUser */); } return SUCCEEDED(hrc) ? VINF_SUCCESS : VERR_COM_UNEXPECTED; } /** * Sets the current progress based on the operation. * * @returns VBox status code. * @param uOp Operation index to set (zero-based). * @param strDesc Description of the operation. */ int RecordingContext::progressSet(uint32_t uOp, const Bstr &strDesc) { if (m_pProgress.isNull()) return VINF_SUCCESS; if ( uOp == m_ulCurOp /* No change? */ || uOp + 1 > m_cOps /* Done? */ || m_cOps == 1) /* Indefinitely recording until canceled? Skip. */ return VINF_SUCCESS; Assert(uOp > m_ulCurOp); ComPtr pProgressControl(m_pProgress); AssertReturn(!!pProgressControl, VERR_COM_UNEXPECTED); /* hrc ignored */ pProgressControl->SetNextOperation(strDesc.raw(), 1 /* Weight */); /* Might be E_FAIL if already canceled. */ m_ulCurOp = uOp; return VINF_SUCCESS; } /** * Sets the current progress based on a timestamp (PTS). * * @returns VBox status code. * @param msTimestamp Timestamp to use (absolute, PTS). */ int RecordingContext::progressSet(uint64_t msTimestamp) { /* Run until stopped / canceled? */ if (m_cOps == 1) return VINF_SUCCESS; ULONG const nextOp = (ULONG)msTimestamp / RT_MS_1SEC; /* Each operation equals 1s (same weight). */ if (nextOp <= m_ulCurOp) /* If next operation still is the current operation, bail out early. */ return VINF_SUCCESS; /* Format the recording time as a human-readable time (HH:MM:SS) and set it as current progress operation text. */ char szDesc[32]; szDesc[0] = '\0'; char *psz = szDesc; RTTIMESPEC TimeSpec; RTTIME Time; RTTimeExplode(&Time, RTTimeSpecSetMilli(&TimeSpec, msTimestamp)); psz += RTStrFormatNumber(psz, Time.u8Hour, 10, 2, 0, RTSTR_F_ZEROPAD); *psz++ = ':'; psz += RTStrFormatNumber(psz, Time.u8Minute, 10, 2, 0, RTSTR_F_ZEROPAD); *psz++ = ':'; psz += RTStrFormatNumber(psz, Time.u8Second, 10, 2, 0, RTSTR_F_ZEROPAD); /* All operations have the same weight. */ uint8_t const uPercent = (100 * nextOp + m_cOps / 2) / m_cOps; LogRel2(("Recording: Progress %s (%RU32 / %RU32) -- %RU8%%\n", szDesc, nextOp, m_cOps, uPercent)); psz += RTStrPrintf2(psz, psz - szDesc, " (%RU8%%)", uPercent); return progressSet(nextOp, Bstr(szDesc)); } /** * Notifies the progress object about completion. * * @returns VBox status code. * @param hrc Completion result to set. * @param pErrorInfo Error info to set in case \a hrc indicates an error. Optional and can be NULL. */ int RecordingContext::progressNotifyComplete(HRESULT hrc /* = S_OK */, IVirtualBoxErrorInfo *pErrorInfo /* = NULL */) { if (m_pProgress.isNull()) return VINF_SUCCESS; BOOL fCompleted; HRESULT hrc2 = m_pProgress->COMGETTER(Completed)(&fCompleted); AssertComRC(hrc2); if (!fCompleted) { ComPtr pProgressControl(m_pProgress); AssertReturn(!!pProgressControl, VERR_COM_UNEXPECTED); pProgressControl->NotifyComplete(hrc, pErrorInfo); } return VINF_SUCCESS; } /** * Reports an error condition to the recording context. * * @returns VBox status code. * @param rc Error code to set. * @param strText Error description to set. */ int RecordingContext::SetError(int rc, const com::Utf8Str &strText) { lock(); if ( m_pProgress.isNull() || !m_pConsole) { unlock(); return VINF_SUCCESS; } ComObjPtr pErrorInfo; HRESULT hrc = pErrorInfo.createObject(); AssertComRC(hrc); hrc = pErrorInfo->initEx(VBOX_E_RECORDING_ERROR, (LONG)rc, m_pConsole->getStaticClassIID(), m_pConsole->getStaticComponentName(), strText); AssertComRC(hrc); unlock(); LogRel(("Recording: An error occurred: %s (%Rrc)\n", strText.c_str(), rc)); hrc = m_pProgress->NotifyComplete(VBOX_E_RECORDING_ERROR, pErrorInfo); AssertComRC(hrc); return VINF_SUCCESS; } /** * Worker thread for all streams of a recording context. */ DECLCALLBACK(int) RecordingContext::threadMain(RTTHREAD hThreadSelf, void *pvUser) { RecordingContext *pThis = (RecordingContext *)pvUser; /* Signal that we're up and rockin'. */ RTThreadUserSignal(hThreadSelf); LogRel2(("Recording: Thread started\n")); for (;;) { int vrcWait = RTSemEventWait(pThis->m_WaitEvent, RT_MS_1SEC); if (ASMAtomicReadBool(&pThis->m_fShutdown)) { LogRel2(("Recording: Thread is shutting down ...\n")); break; } Log2Func(("Processing %zu streams (wait = %Rrc)\n", pThis->m_vecStreams.size(), vrcWait)); uint64_t const msTimestamp = pThis->GetCurrentPTS(); /* Set the overall progress. */ int vrc = pThis->progressSet(msTimestamp); AssertRC(vrc); /* Process common raw blocks (data which not has been encoded yet). */ vrc = pThis->processCommonData(pThis->m_mapBlocksRaw, 100 /* ms timeout */); /** @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->m_vecStreams.begin(); while (itStream != pThis->m_vecStreams.end()) { RecordingStream *pStream = (*itStream); /* Hand-in common encoded blocks. */ vrc = pStream->ThreadMain(vrcWait, msTimestamp, pThis->m_mapBlocksEncoded); if (RT_FAILURE(vrc)) { LogRel(("Recording: Processing stream #%RU16 failed (%Rrc)\n", pStream->GetID(), vrc)); break; } ++itStream; } if (RT_FAILURE(vrc)) LogRel(("Recording: Encoding thread failed (%Rrc)\n", vrc)); /* Keep going in case of errors. */ } /* for */ LogRel2(("Recording: Thread ended\n")); return VINF_SUCCESS; } /** * Notifies a recording context's encoding thread. * * @returns VBox status code. */ int RecordingContext::threadNotify(void) { return RTSemEventSignal(m_WaitEvent); } /** * Worker function for processing common block data. * * @returns VBox status code. * @param mapCommon Common block map to handle. * @param msTimeout Timeout to use for maximum time spending to process data. * Use RT_INDEFINITE_WAIT for processing all data. * * @note Runs in recording thread. */ int RecordingContext::processCommonData(RecordingBlockMap &mapCommon, RTMSINTERVAL msTimeout) { Log2Func(("Processing %zu common blocks (%RU32ms timeout)\n", mapCommon.size(), msTimeout)); int vrc = VINF_SUCCESS; uint64_t const msStart = RTTimeMilliTS(); RecordingBlockMap::iterator itCommonBlocks = mapCommon.begin(); while (itCommonBlocks != mapCommon.end()) { RecordingBlockList::iterator itBlock = itCommonBlocks->second->List.begin(); while (itBlock != itCommonBlocks->second->List.end()) { RecordingBlock *pBlockCommon = (RecordingBlock *)(*itBlock); PRECORDINGFRAME pFrame = (PRECORDINGFRAME)pBlockCommon->pvData; AssertPtr(pFrame); switch (pFrame->enmType) { #ifdef VBOX_WITH_AUDIO_RECORDING case RECORDINGFRAME_TYPE_AUDIO: { vrc = recordingCodecEncodeFrame(&m_CodecAudio, pFrame, pFrame->msTimestamp, NULL /* pvUser */); break; } #endif /* VBOX_WITH_AUDIO_RECORDING */ default: /* Skip unknown stuff. */ break; } itCommonBlocks->second->List.erase(itBlock); delete pBlockCommon; itBlock = itCommonBlocks->second->List.begin(); if (RT_FAILURE(vrc) || RTTimeMilliTS() > msStart + msTimeout) break; } /* If no entries are left over in the block map, remove it altogether. */ if (itCommonBlocks->second->List.empty()) { delete itCommonBlocks->second; mapCommon.erase(itCommonBlocks); itCommonBlocks = mapCommon.begin(); } else ++itCommonBlocks; if (RT_FAILURE(vrc)) break; } return vrc; } /** * Writes common block data (i.e. shared / the same) in all streams. * * The multiplexing is needed to supply all recorded (enabled) screens with the same * data at the same given point in time. * * Currently this only is being used for audio data. * * @returns VBox status code. * @param mapCommon Common block map to write data to. * @param pCodec Pointer to codec instance which has written the data. * @param pvData Pointer to written data (encoded). * @param cbData Size (in bytes) of \a pvData. * @param msTimestamp Absolute PTS (in ms) of the written data. * @param uFlags Encoding flags of type RECORDINGCODEC_ENC_F_XXX. */ int RecordingContext::writeCommonData(RecordingBlockMap &mapCommon, PRECORDINGCODEC pCodec, const void *pvData, size_t cbData, uint64_t msTimestamp, uint32_t uFlags) { AssertPtrReturn(pvData, VERR_INVALID_POINTER); AssertReturn(cbData, VERR_INVALID_PARAMETER); LogFlowFunc(("pCodec=%p, cbData=%zu, msTimestamp=%zu, uFlags=%#x\n", pCodec, cbData, msTimestamp, uFlags)); RECORDINGFRAME_TYPE const enmType = pCodec->Parms.enmType == RECORDINGCODECTYPE_AUDIO ? RECORDINGFRAME_TYPE_AUDIO : RECORDINGFRAME_TYPE_INVALID; AssertReturn(enmType != RECORDINGFRAME_TYPE_INVALID, VERR_NOT_SUPPORTED); PRECORDINGFRAME pFrame = NULL; switch (enmType) { #ifdef VBOX_WITH_AUDIO_RECORDING case RECORDINGFRAME_TYPE_AUDIO: { pFrame = (PRECORDINGFRAME)RTMemAlloc(sizeof(RECORDINGFRAME)); AssertPtrReturn(pFrame, VERR_NO_MEMORY); pFrame->enmType = RECORDINGFRAME_TYPE_AUDIO; pFrame->msTimestamp = msTimestamp; PRECORDINGAUDIOFRAME pAudioFrame = &pFrame->u.Audio; pAudioFrame->pvBuf = (uint8_t *)RTMemDup(pvData, cbData); AssertPtrReturn(pAudioFrame->pvBuf, VERR_NO_MEMORY); pAudioFrame->cbBuf = cbData; break; } #endif default: AssertFailed(); break; } if (!pFrame) return VINF_SUCCESS; lock(); int vrc; RecordingBlock *pBlock = NULL; try { pBlock = new RecordingBlock(); pBlock->pvData = pFrame; pBlock->cbData = sizeof(RECORDINGFRAME); pBlock->cRefs = m_cStreamsEnabled; pBlock->msTimestamp = msTimestamp; pBlock->uFlags = uFlags; RecordingBlockMap::iterator itBlocks = mapCommon.find(msTimestamp); if (itBlocks == mapCommon.end()) { RecordingBlocks *pRecordingBlocks = new RecordingBlocks(); pRecordingBlocks->List.push_back(pBlock); mapCommon.insert(std::make_pair(msTimestamp, pRecordingBlocks)); } else itBlocks->second->List.push_back(pBlock); vrc = VINF_SUCCESS; } catch (const std::exception &) { vrc = VERR_NO_MEMORY; } unlock(); if (RT_SUCCESS(vrc)) { vrc = threadNotify(); } else { if (pBlock) delete pBlock; RecordingFrameFree(pFrame); } return vrc; } #ifdef VBOX_WITH_AUDIO_RECORDING /** * Callback function for writing encoded audio data into the common encoded block map. * * This is called by the audio codec when finishing encoding audio data. * * @copydoc RECORDINGCODECCALLBACKS::pfnWriteData */ /* static */ DECLCALLBACK(int) RecordingContext::s_audioCodecWriteDataCallback(PRECORDINGCODEC pCodec, const void *pvData, size_t cbData, uint64_t msAbsPTS, uint32_t uFlags, void *pvUser) { RecordingContext *pThis = (RecordingContext *)pvUser; return pThis->writeCommonData(pThis->m_mapBlocksEncoded, pCodec, pvData, cbData, msAbsPTS, uFlags); } /** * Initializes the audio codec for a (multiplexing) recording context. * * @returns VBox status code. * @param screenSettings Reference to recording screen settings to use for initialization. */ int RecordingContext::audioInit(const settings::RecordingScreen &screenSettings) { RecordingAudioCodec_T const enmCodec = screenSettings.Audio.enmCodec; if (enmCodec == RecordingAudioCodec_None) { LogRel2(("Recording: No audio codec configured, skipping audio init\n")); return VINF_SUCCESS; } RECORDINGCODECCALLBACKS Callbacks; Callbacks.pvUser = this; Callbacks.pfnWriteData = RecordingContext::s_audioCodecWriteDataCallback; int vrc = recordingCodecCreateAudio(&m_CodecAudio, enmCodec); if (RT_SUCCESS(vrc)) vrc = recordingCodecInit(&m_CodecAudio, &Callbacks, screenSettings); return vrc; } #endif /* VBOX_WITH_AUDIO_RECORDING */ /** * Progress canceled callback. * * @param pvUser User-supplied pointer. Points to the RecordingContext instance. */ /* static */ void RecordingContext::s_progressCancelCallback(void *pvUser) { RecordingContext *pThis = (RecordingContext *)pvUser; LogRel(("Recording: Canceled\n")); if (pThis->m_pConsole) { ComPtr pProgressIgnored; pThis->m_pConsole->i_onRecordingStateChange(FALSE /* Disable */, pProgressIgnored); } } /** @copydoc RecordingContext::CALLBACKS::pfnStateChanged */ DECLCALLBACK(void) RecordingContext::s_recordingStateChangedCallback(RecordingContext *pCtx, RECORDINGSTS enmSts, uint32_t uScreen, int vrc, void *pvUser) { RT_NOREF(vrc, pvUser); Log2Func(("enmSts=%0x, uScreen=%RU32, vrc=%Rrc\n", enmSts, uScreen, vrc)); switch (enmSts) { case RECORDINGSTS_LIMIT_REACHED: { if (uScreen == UINT32_MAX) /* Limit for all screens reached? Disable recording. */ { ComPtr pProgressIgnored; pCtx->m_pConsole->i_onRecordingStateChange(FALSE /* Disable */, pProgressIgnored); pCtx->lock(); /* Make sure to complete the progress object (if not already done so). */ pCtx->progressNotifyComplete(S_OK); pCtx->unlock(); } else if (pCtx->m_pConsole) pCtx->m_pConsole->i_onRecordingScreenStateChange(FALSE /* Disable */, uScreen); break; } default: break; } } /** * Creates a recording context. * * @returns VBox status code. * @param ptrConsole Pointer to console object this context is bound to (weak pointer). * @param Settings Reference to recording settings to use for creation. * @param pProgress Progress object returned on success. */ int RecordingContext::createInternal(Console *ptrConsole, const settings::Recording &Settings, ComPtr &pProgress) { int vrc = VINF_SUCCESS; /* Copy the settings to our context. */ m_Settings = Settings; #ifdef VBOX_WITH_AUDIO_RECORDING settings::RecordingScreenSettingsMap::const_iterator itScreen0 = m_Settings.mapScreens.begin(); AssertReturn(itScreen0 != m_Settings.mapScreens.end(), VERR_WRONG_ORDER); /* We always use the audio settings from screen 0, as we multiplex the audio data anyway. */ settings::RecordingScreen const &screen0Settings = itScreen0->second; vrc = this->audioInit(screen0Settings); if (RT_FAILURE(vrc)) return vrc; #endif m_pConsole = ptrConsole; RT_ZERO(m_Callbacks); settings::RecordingScreenSettingsMap::const_iterator itScreen = m_Settings.mapScreens.begin(); while (itScreen != m_Settings.mapScreens.end()) { RecordingStream *pStream = NULL; try { if (itScreen->second.fEnabled) { pStream = new RecordingStream(this, itScreen->first /* Screen ID */, itScreen->second); m_vecStreams.push_back(pStream); m_cStreamsEnabled++; LogFlowFunc(("pStream=%p\n", pStream)); } } catch (std::bad_alloc &) { vrc = VERR_NO_MEMORY; break; } catch (int vrc_thrown) /* Catch vrc thrown by constructor. */ { vrc = vrc_thrown; break; } ++itScreen; } ComObjPtr pThisProgress; vrc = progressCreate(m_Settings, pThisProgress); if (RT_SUCCESS(vrc)) { vrc = RTSemEventCreate(&m_WaitEvent); AssertRCReturn(vrc, vrc); RecordingContext::CALLBACKS Callbacks; RT_ZERO(Callbacks); Callbacks.pfnStateChanged = RecordingContext::s_recordingStateChangedCallback; SetCallbacks(&Callbacks, this /* pvUser */); reset(); unconst(m_pProgress) = pThisProgress; pThisProgress.queryInterfaceTo(pProgress.asOutParam()); } if (RT_FAILURE(vrc)) destroyInternal(); return vrc; } /** * Sets the callback table for a recording context. * * @param pCallbacks Callback table to set. * @param pvUser User-supplied pointer. */ void RecordingContext::SetCallbacks(RecordingContext::CALLBACKS *pCallbacks, void *pvUser) { lock(); memcpy(&m_Callbacks, pCallbacks, sizeof(RecordingContext::CALLBACKS)); m_Callbacks.pvUser = pvUser; unlock(); } /** * Resets a recording context. */ void RecordingContext::reset(void) { m_tsStartMs = 0; m_enmState = RECORDINGSTS_CREATED; m_fShutdown = false; m_cStreamsEnabled = 0; unconst(m_pProgress).setNull(); } /** * Starts a recording context by creating its worker thread. * * @returns VBox status code. */ int RecordingContext::startInternal(void) { if (m_enmState == RECORDINGSTS_STARTED) return VINF_SUCCESS; Assert(m_enmState == RECORDINGSTS_CREATED); LogRel2(("Recording: Starting ...\n")); m_tsStartMs = RTTimeMilliTS(); m_ulCurOp = 0; if (m_pProgress.isNotNull()) { HRESULT hrc = m_pProgress->COMGETTER(OperationCount)(&m_cOps); AssertComRCReturn(hrc, VERR_COM_UNEXPECTED); } int vrc = RTThreadCreate(&m_Thread, RecordingContext::threadMain, (void *)this, 0, RTTHREADTYPE_MAIN_WORKER, RTTHREADFLAGS_WAITABLE, "Record"); if (RT_SUCCESS(vrc)) /* Wait for the thread to start. */ vrc = RTThreadUserWait(m_Thread, RT_MS_30SEC /* 30s timeout */); if (RT_SUCCESS(vrc)) { LogRel(("Recording: Started\n")); m_enmState = RECORDINGSTS_STARTED; } else LogRel(("Recording: Failed to start (%Rrc)\n", vrc)); return vrc; } /** * Stops a recording context by telling the worker thread to stop and finalizing its operation. * * @returns VBox status code. */ int RecordingContext::stopInternal(void) { if (m_enmState != RECORDINGSTS_STARTED) return VINF_SUCCESS; LogRel2(("Recording: Stopping ...\n")); /* Set shutdown indicator. */ ASMAtomicWriteBool(&m_fShutdown, true); /* Signal the thread and wait for it to shut down. */ int vrc = threadNotify(); if (RT_SUCCESS(vrc)) vrc = RTThreadWait(m_Thread, RT_MS_30SEC /* 30s timeout */, NULL); lock(); if (RT_SUCCESS(vrc)) { if (m_pProgress.isNotNull()) progressNotifyComplete(); LogRel(("Recording: Stopped\n")); reset(); } else LogRel(("Recording: Failed to stop (%Rrc)\n", vrc)); unlock(); LogFlowThisFunc(("%Rrc\n", vrc)); return vrc; } /** * Destroys a recording context, internal version. */ void RecordingContext::destroyInternal(void) { lock(); if (m_enmState == RECORDINGSTS_UNINITIALIZED) { unlock(); return; } int vrc = stopInternal(); AssertRCReturnVoid(vrc); vrc = RTSemEventDestroy(m_WaitEvent); AssertRCReturnVoid(vrc); m_WaitEvent = NIL_RTSEMEVENT; RecordingStreams::iterator it = m_vecStreams.begin(); while (it != m_vecStreams.end()) { RecordingStream *pStream = (*it); vrc = pStream->Uninit(); AssertRC(vrc); delete pStream; pStream = NULL; m_vecStreams.erase(it); it = m_vecStreams.begin(); } /* Sanity. */ Assert(m_vecStreams.empty()); Assert(m_mapBlocksRaw.size() == 0); Assert(m_mapBlocksEncoded.size() == 0); m_enmState = RECORDINGSTS_UNINITIALIZED; unconst(m_pProgress).setNull(); unlock(); } /** * Returns a recording context's current settings. * * @returns The recording context's current settings. */ const settings::Recording &RecordingContext::GetConfig(void) const { return m_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 = m_vecStreams.at(uScreen); } catch (std::out_of_range &) { pStream = NULL; } return pStream; } /** * Locks the recording context for serializing access. * * @returns VBox status code. */ int RecordingContext::lock(void) { int vrc = RTCritSectEnter(&m_CritSect); AssertRC(vrc); return vrc; } /** * Unlocks the recording context for serializing access. * * @returns VBox status code. */ int RecordingContext::unlock(void) { int vrc = RTCritSectLeave(&m_CritSect); AssertRC(vrc); return vrc; } /** * 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 m_vecStreams.size(); } /** * Creates a new recording context. * * @returns VBox status code. * @param ptrConsole Pointer to console object this context is bound to (weak pointer). * @param Settings Reference to recording settings to use for creation. * @param pProgress Progress object returned on success. */ int RecordingContext::Create(Console *ptrConsole, const settings::Recording &Settings, ComPtr &pProgress) { return createInternal(ptrConsole, Settings, pProgress); } /** * Destroys a recording context. */ void RecordingContext::Destroy(void) { destroyInternal(); } /** * Starts a recording context. * * @returns VBox status code. */ int RecordingContext::Start(void) { return startInternal(); } /** * Stops a recording context. */ int RecordingContext::Stop(void) { return stopInternal(); } /** * Returns the current PTS (presentation time stamp) for a recording context. * * @returns Current PTS. */ uint64_t RecordingContext::GetCurrentPTS(void) const { return RTTimeMilliTS() - m_tsStartMs; } /** * 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 = m_vecStreams.begin(); while (itStream != m_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 = m_enmState >= RECORDINGSTS_CREATED; unlock(); return fIsReady; } /** * Returns if a feature for a given stream is enabled or not. * * @returns @c true if the specified feature is enabled (running), @c false if not. * @param uScreen Screen ID. * @param enmFeature Feature of stream to check for. * * @note Implies that the stream is enabled (i.e. active). */ bool RecordingContext::IsFeatureEnabled(uint32_t uScreen, RecordingFeature_T enmFeature) { lock(); bool fIsReady = false; if (m_enmState == RECORDINGSTS_STARTED) { const RecordingStream *pStream = getStreamInternal(uScreen); if (pStream) fIsReady = pStream->IsFeatureEnabled(enmFeature); /* 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 @c true if started, @c false if not. */ bool RecordingContext::IsStarted(void) { lock(); const bool fIsStarted = m_enmState == RECORDINGSTS_STARTED; unlock(); return fIsStarted; } /** * Checks if a specified limit for recording has been reached. * * @returns @c true if any limit has been reached, @c false if not. */ bool RecordingContext::IsLimitReached(void) { lock(); LogFlowThisFunc(("cStreamsEnabled=%RU16\n", m_cStreamsEnabled)); const bool fLimitReached = m_cStreamsEnabled == 0; unlock(); return fLimitReached; } /** * Checks if a specified limit for recording has been reached. * * @returns @c true if any limit has been reached, @c false if not. * @param uScreen Screen ID. * @param msTimestamp Timestamp (PTS, 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; } /** * Returns if a specific screen needs to be fed with an update or not. * * @returns @c true if an update is needed, @c false if not. * @param uScreen Screen ID to retrieve update stats for. * @param msTimestamp Timestamp (PTS, in ms). */ bool RecordingContext::NeedsUpdate(uint32_t uScreen, uint64_t msTimestamp) { lock(); bool fNeedsUpdate = false; if (m_enmState == RECORDINGSTS_STARTED) { #ifdef VBOX_WITH_AUDIO_RECORDING if ( recordingCodecIsInitialized(&m_CodecAudio) && recordingCodecGetWritable(&m_CodecAudio, msTimestamp) > 0) { fNeedsUpdate = true; } #endif /* VBOX_WITH_AUDIO_RECORDING */ if (!fNeedsUpdate) { const RecordingStream *pStream = getStreamInternal(uScreen); if (pStream) fNeedsUpdate = pStream->NeedsUpdate(msTimestamp); } } unlock(); return fNeedsUpdate; } /** * Gets called by a stream if its limit has been reached. * * @returns VBox status code. * @param uScreen The stream's ID (Screen ID). * @param vrc Result code of the limit operation. */ int RecordingContext::onLimitReached(uint32_t uScreen, int vrc) { lock(); LogRel2(("Recording: Active streams: %RU16\n", m_cStreamsEnabled)); if (m_cStreamsEnabled) m_cStreamsEnabled--; bool const fAllDisabled = m_cStreamsEnabled == 0; if (fAllDisabled) LogRel(("Recording: All set limits have been reached\n")); else LogRel(("Recording: Set limit for screen #%RU32 has been reached\n", uScreen)); unlock(); /* Leave the lock before invoking callbacks. */ if (m_Callbacks.pfnStateChanged) m_Callbacks.pfnStateChanged(this, RECORDINGSTS_LIMIT_REACHED, fAllDisabled ? UINT32_MAX : uScreen, vrc, m_Callbacks.pvUser); return VINF_SUCCESS; } /** * Sends an audio frame to the recording thread. * * @returns VBox status code. * @param pvData Audio frame data to send. * @param cbData Size (in bytes) of (encoded) audio frame data. * @param msTimestamp Timestamp (PTS, in ms) of audio playback. */ int RecordingContext::SendAudioFrame(const void *pvData, size_t cbData, uint64_t msTimestamp) { #ifdef VBOX_WITH_AUDIO_RECORDING return writeCommonData(m_mapBlocksRaw, &m_CodecAudio, pvData, cbData, msTimestamp, RECORDINGCODEC_ENC_F_BLOCK_IS_KEY); #else RT_NOREF(pvData, cbData, msTimestamp); return VERR_NOT_SUPPORTED; #endif } /** * Sends a video frame to the recording thread. * * @thread EMT * * @returns VBox status code. * @param uScreen Screen number to send video frame to. * @param pFrame Video frame to send. * @param msTimestamp Timestamp (PTS, in ms). */ int RecordingContext::SendVideoFrame(uint32_t uScreen, PRECORDINGVIDEOFRAME pFrame, uint64_t msTimestamp) { AssertPtrReturn(pFrame, VERR_INVALID_POINTER); LogFlowFunc(("uScreen=%RU32, offX=%RU32, offY=%RU32, w=%RU32, h=%RU32 (%zu bytes), msTimestamp=%RU64\n", uScreen, pFrame->Pos.x, pFrame->Pos.y, pFrame->Info.uWidth, pFrame->Info.uHeight, pFrame->Info.uHeight * pFrame->Info.uWidth * (pFrame->Info.uBPP / 8), msTimestamp)); if (!pFrame->pau8Buf) /* Empty / invalid frame, skip. */ return VINF_SUCCESS; /* Sanity. */ AssertReturn(pFrame->Info.uBPP, VERR_INVALID_PARAMETER); AssertReturn(pFrame->cbBuf, VERR_INVALID_PARAMETER); AssertReturn(pFrame->Info.uWidth * pFrame->Info.uHeight * (pFrame->Info.uBPP / 8) <= pFrame->cbBuf, VERR_INVALID_PARAMETER); lock(); RecordingStream *pStream = getStreamInternal(uScreen); if (!pStream) { unlock(); return VINF_SUCCESS; } unlock(); int vrc = pStream->SendVideoFrame(pFrame, msTimestamp); if (vrc == VINF_SUCCESS) /* Might be VINF_RECORDING_THROTTLED or VINF_RECORDING_LIMIT_REACHED. */ threadNotify(); return vrc; } /** * Sends a cursor position change to the recording context. * * @returns VBox status code. * @param uScreen Screen number. * @param x X location within the guest. * @param y Y location within the guest. * @param msTimestamp Timestamp (PTS, in ms). */ int RecordingContext::SendCursorPositionChange(uint32_t uScreen, int32_t x, int32_t y, uint64_t msTimestamp) { LogFlowFunc(("uScreen=%RU32, x=%RU32, y=%RU32\n", uScreen, x, y)); /* If no cursor shape is set yet, skip any cursor position changes. */ if (!m_Cursor.m_Shape.pau8Buf) return VINF_SUCCESS; int vrc = m_Cursor.Move(x, y); if (RT_SUCCESS(vrc)) { lock(); RecordingStream *pStream = getStreamInternal(uScreen); if (!pStream) { unlock(); return VINF_SUCCESS; } unlock(); vrc = pStream->SendCursorPos(0 /* idCursor */, &m_Cursor.m_Shape.Pos, msTimestamp); if (vrc == VINF_SUCCESS) /* Might be VINF_RECORDING_THROTTLED or VINF_RECORDING_LIMIT_REACHED. */ threadNotify(); } return vrc; } /** * Sends a cursor shape change to the recording context. * * @returns VBox status code. * @param fVisible Whether the mouse cursor actually is visible or not. * @param fAlpha Whether the pixel data contains alpha channel information or not. * @param xHot X hot position (in pixel) of the new cursor. * @param yHot Y hot position (in pixel) of the new cursor. * @param uWidth Width (in pixel) of the new cursor. * @param uHeight Height (in pixel) of the new cursor. * @param pu8Shape Pixel data of the new cursor. Must be 32 BPP RGBA for now. * @param cbShape Size of \a pu8Shape (in bytes). * @param msTimestamp Timestamp (PTS, in ms). */ int RecordingContext::SendCursorShapeChange(bool fVisible, bool fAlpha, uint32_t xHot, uint32_t yHot, uint32_t uWidth, uint32_t uHeight, const uint8_t *pu8Shape, size_t cbShape, uint64_t msTimestamp) { RT_NOREF(fAlpha, xHot, yHot); LogFlowFunc(("fVisible=%RTbool, fAlpha=%RTbool, uWidth=%RU32, uHeight=%RU32\n", fVisible, fAlpha, uWidth, uHeight)); if ( !pu8Shape /* Might be NULL on saved state load. */ || !fVisible) return VINF_SUCCESS; AssertReturn(cbShape, VERR_INVALID_PARAMETER); lock(); int vrc = m_Cursor.CreateOrUpdate(fAlpha, uWidth, uHeight, pu8Shape, cbShape); RecordingStreams::iterator it = m_vecStreams.begin(); while (it != m_vecStreams.end()) { RecordingStream *pStream = (*it); int vrc2 = pStream->SendCursorShape(0 /* idCursor */, &m_Cursor.m_Shape, msTimestamp); if (RT_SUCCESS(vrc)) vrc = vrc2; /* Bail out as soon as possible when the shutdown flag is set. */ if (ASMAtomicReadBool(&m_fShutdown)) break; ++it; } unlock(); if (vrc == VINF_SUCCESS) /* Might be VINF_RECORDING_THROTTLED or VINF_RECORDING_LIMIT_REACHED. */ threadNotify(); return vrc; } /** * Sends a screen change to a recording stream. * * @returns VBox status code. * @param uScreen Screen number. * @param pInfo Recording screen info to use. * @param msTimestamp Timestamp (PTS, in ms). */ int RecordingContext::SendScreenChange(uint32_t uScreen, PRECORDINGSURFACEINFO pInfo, uint64_t msTimestamp) { lock(); RecordingStream *pStream = getStreamInternal(uScreen); if (!pStream) { unlock(); return VINF_SUCCESS; } unlock(); int const vrc = pStream->SendScreenChange(pInfo, msTimestamp); return vrc; }