From 00851c9c7a49cb52dbefe8ad03876b953b73da4e Mon Sep 17 00:00:00 2001 From: irving ou Date: Sun, 14 Jun 2026 18:45:13 -0400 Subject: [PATCH 1/6] Add frame rate and resolution controls to video recording (DVLS-14562) The native recorder previously honored only VideoRecordingQuality, so it recorded at the full desktop resolution and the native paint cadence, ignoring the FPS/resolution configured by the client. - RdpSettings: parse and expose new VideoRecordingFrameRate / VideoRecordingWidth / VideoRecordingHeight extended properties (and .rdp file entries), mirroring VideoRecordingQuality. - ApiHooks: forward the new values to the OutputMirror. - OutputMirror: downscale the captured frame to the requested resolution via StretchBlt before encoding, drive the encoder frame rate, and throttle the paint-driven encode cadence to the configured FPS. No cadeau/xmf change required (the encoder already exposes SetFrameRate / SetFrameSize). Co-Authored-By: Claude Opus 4.8 (1M context) --- dll/ApiHooks.cpp | 8 +++ dll/OutputMirror.c | 98 ++++++++++++++++++++++++++++++++-- dll/RdpSettings.cpp | 72 +++++++++++++++++++++++++ include/MsRdpEx/OutputMirror.h | 2 + include/MsRdpEx/RdpSettings.h | 6 +++ 5 files changed, 182 insertions(+), 4 deletions(-) diff --git a/dll/ApiHooks.cpp b/dll/ApiHooks.cpp index 96c3df1..2a61845 100644 --- a/dll/ApiHooks.cpp +++ b/dll/ApiHooks.cpp @@ -431,6 +431,9 @@ bool WINAPI MsRdpEx_CaptureBlt( bool outputMirrorEnabled = false; bool videoRecordingEnabled = false; uint32_t videoRecordingQuality = 5; + uint32_t videoRecordingFrameRate = 0; + uint32_t videoRecordingWidth = 0; + uint32_t videoRecordingHeight = 0; bool dumpBitmapUpdates = false; IMsRdpExInstance* instance = NULL; MsRdpEx_OutputMirror* outputMirror = NULL; @@ -457,6 +460,9 @@ bool WINAPI MsRdpEx_CaptureBlt( outputMirrorEnabled = pExtendedSettings->GetOutputMirrorEnabled(); videoRecordingEnabled = pExtendedSettings->GetVideoRecordingEnabled(); videoRecordingQuality = pExtendedSettings->GetVideoRecordingQuality(); + videoRecordingFrameRate = pExtendedSettings->GetVideoRecordingFrameRate(); + videoRecordingWidth = pExtendedSettings->GetVideoRecordingWidth(); + videoRecordingHeight = pExtendedSettings->GetVideoRecordingHeight(); dumpBitmapUpdates = pExtendedSettings->GetDumpBitmapUpdates(); if (!outputMirrorEnabled) @@ -476,6 +482,8 @@ bool WINAPI MsRdpEx_CaptureBlt( MsRdpEx_OutputMirror_SetDumpBitmapUpdates(outputMirror, dumpBitmapUpdates); MsRdpEx_OutputMirror_SetVideoRecordingEnabled(outputMirror, videoRecordingEnabled); MsRdpEx_OutputMirror_SetVideoQualityLevel(outputMirror, videoRecordingQuality); + MsRdpEx_OutputMirror_SetVideoFrameRate(outputMirror, videoRecordingFrameRate); + MsRdpEx_OutputMirror_SetRecordingResolution(outputMirror, videoRecordingWidth, videoRecordingHeight); char* recordingPath = pExtendedSettings->GetRecordingPath(); if (recordingPath) { diff --git a/dll/OutputMirror.c b/dll/OutputMirror.c index ae20ad4..60e40cd 100644 --- a/dll/OutputMirror.c +++ b/dll/OutputMirror.c @@ -34,6 +34,20 @@ struct _MsRdpEx_OutputMirror MsRdpEx_RecordingManifest* manifest; FILE* frameMetadataFile; + uint32_t videoFrameRate; + uint32_t recordingWidth; + uint32_t recordingHeight; + uint64_t lastEncodeTime; + + bool scalingEnabled; + HDC hScaledDC; + HBITMAP hScaledBitmap; + HGDIOBJ hScaledObject; + uint8_t* scaledBitmapData; + uint32_t scaledBitmapWidth; + uint32_t scaledBitmapHeight; + uint32_t scaledBitmapStep; + CRITICAL_SECTION lock; }; @@ -83,9 +97,36 @@ bool MsRdpEx_OutputMirror_DumpFrame(MsRdpEx_OutputMirror* ctx) captureTime = GetTickCount64() - ctx->captureBaseTime; if (ctx->videoRecordingEnabled && ctx->videoRecorder) { - MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, - 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); - MsRdpEx_VideoRecorder_Timeout(ctx->videoRecorder); + // The native path is paint-driven (one DumpFrame per RDP update), so cap the encode cadence to the + // configured frame rate here -- the encoder itself does not drop frames. Mirrors the internal timer path. + bool encodeThisFrame = true; + + if (ctx->videoFrameRate > 0) { + uint64_t now = GetTickCount64(); + uint32_t intervalMs = 1000 / ctx->videoFrameRate; + + if ((ctx->lastEncodeTime != 0) && ((now - ctx->lastEncodeTime) < intervalMs)) { + encodeThisFrame = false; + } + else { + ctx->lastEncodeTime = now; + } + } + + if (encodeThisFrame) { + if (ctx->scalingEnabled) { + StretchBlt(ctx->hScaledDC, 0, 0, ctx->scaledBitmapWidth, ctx->scaledBitmapHeight, + ctx->hShadowDC, 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, SRCCOPY); + GdiFlush(); + MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->scaledBitmapData, + 0, 0, ctx->scaledBitmapWidth, ctx->scaledBitmapHeight, ctx->scaledBitmapStep); + } + else { + MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, + 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); + } + MsRdpEx_VideoRecorder_Timeout(ctx->videoRecorder); + } } if (ctx->dumpBitmapUpdates) { @@ -121,6 +162,17 @@ void MsRdpEx_OutputMirror_SetVideoQualityLevel(MsRdpEx_OutputMirror* ctx, uint32 ctx->videoQualityLevel = videoQualityLevel; } +void MsRdpEx_OutputMirror_SetVideoFrameRate(MsRdpEx_OutputMirror* ctx, uint32_t videoFrameRate) +{ + ctx->videoFrameRate = videoFrameRate; +} + +void MsRdpEx_OutputMirror_SetRecordingResolution(MsRdpEx_OutputMirror* ctx, uint32_t recordingWidth, uint32_t recordingHeight) +{ + ctx->recordingWidth = recordingWidth; + ctx->recordingHeight = recordingHeight; +} + void MsRdpEx_OutputMirror_SetRecordingPath(MsRdpEx_OutputMirror* ctx, const char* recordingPath) { strcpy_s(ctx->recordingPath, MSRDPEX_MAX_PATH, recordingPath); @@ -202,6 +254,28 @@ bool MsRdpEx_OutputMirror_Init(MsRdpEx_OutputMirror* ctx) sprintf_s(ctx->outputPath, MSRDPEX_MAX_PATH, "%s\\%s", ctx->recordingPath, ctx->sessionId); MsRdpEx_MakePath(ctx->outputPath, NULL); + uint32_t encodeWidth = ctx->bitmapWidth; + uint32_t encodeHeight = ctx->bitmapHeight; + + // Downscale the captured frame to the requested recording resolution before encoding. The xmf encoder + // does not scale, so without this it records at the native desktop size regardless of the configured value. + ctx->scalingEnabled = (ctx->recordingWidth > 0) && (ctx->recordingHeight > 0) + && ((ctx->recordingWidth != ctx->bitmapWidth) || (ctx->recordingHeight != ctx->bitmapHeight)); + + if (ctx->scalingEnabled) { + encodeWidth = ctx->recordingWidth; + encodeHeight = ctx->recordingHeight; + ctx->scaledBitmapWidth = encodeWidth; + ctx->scaledBitmapHeight = encodeHeight; + ctx->scaledBitmapStep = encodeWidth * 4; + ctx->hScaledDC = CreateCompatibleDC(ctx->hSourceDC); + ctx->hScaledBitmap = MsRdpEx_CreateDIBSection(ctx->hSourceDC, + encodeWidth, encodeHeight, ctx->bitsPerPixel, &ctx->scaledBitmapData); + ctx->hScaledObject = SelectObject(ctx->hScaledDC, ctx->hScaledBitmap); + SetStretchBltMode(ctx->hScaledDC, HALFTONE); + SetBrushOrgEx(ctx->hScaledDC, 0, 0, NULL); + } + ctx->videoRecorder = MsRdpEx_VideoRecorder_New(); if (ctx->videoRecorder) { @@ -210,10 +284,14 @@ bool MsRdpEx_OutputMirror_Init(MsRdpEx_OutputMirror* ctx) MsRdpEx_RecordingManifest_FinalizeFile(ctx->manifest, 0); MsRdpEx_RecordingManifest_AddFile(ctx->manifest, MsRdpEx_FileBase(filename), startTime, 0); ctx->videoRecordingCount++; - MsRdpEx_VideoRecorder_SetFrameSize(ctx->videoRecorder, ctx->bitmapWidth, ctx->bitmapHeight); + MsRdpEx_VideoRecorder_SetFrameSize(ctx->videoRecorder, encodeWidth, encodeHeight); MsRdpEx_VideoRecorder_SetFileName(ctx->videoRecorder, filename); MsRdpEx_VideoRecorder_SetVideoQuality(ctx->videoRecorder, ctx->videoQualityLevel); + if (ctx->videoFrameRate > 0) { + MsRdpEx_VideoRecorder_SetFrameRate(ctx->videoRecorder, ctx->videoFrameRate); + } + if (!MsRdpEx_StringIsNullOrEmpty(ctx->recordingPipeName)) { MsRdpEx_VideoRecorder_SetPipeName(ctx->videoRecorder, ctx->recordingPipeName); } @@ -249,6 +327,18 @@ bool MsRdpEx_OutputMirror_Uninit(MsRdpEx_OutputMirror* ctx) ctx->hShadowDC = NULL; } + if (ctx->hScaledDC) + { + SelectObject(ctx->hScaledDC, ctx->hScaledObject); + DeleteObject(ctx->hScaledBitmap); + DeleteDC(ctx->hScaledDC); + ctx->hScaledObject = NULL; + ctx->hScaledBitmap = NULL; + ctx->hScaledDC = NULL; + ctx->scaledBitmapData = NULL; + ctx->scalingEnabled = false; + } + if (ctx->videoRecorder) { MsRdpEx_VideoRecorder_Uninit(ctx->videoRecorder); MsRdpEx_VideoRecorder_Remux(ctx->videoRecorder, NULL); diff --git a/dll/RdpSettings.cpp b/dll/RdpSettings.cpp index 2ff1d20..ce6c1c6 100644 --- a/dll/RdpSettings.cpp +++ b/dll/RdpSettings.cpp @@ -826,6 +826,30 @@ HRESULT __stdcall CMsRdpExtendedSettings::put_Property(BSTR bstrPropertyName, VA hr = S_OK; } + else if (MsRdpEx_StringEquals(propName, "VideoRecordingFrameRate")) + { + if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) + goto end; + + m_VideoRecordingFrameRate = (uint32_t)pValue->uintVal; + hr = S_OK; + } + else if (MsRdpEx_StringEquals(propName, "VideoRecordingWidth")) + { + if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) + goto end; + + m_VideoRecordingWidth = (uint32_t)pValue->uintVal; + hr = S_OK; + } + else if (MsRdpEx_StringEquals(propName, "VideoRecordingHeight")) + { + if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) + goto end; + + m_VideoRecordingHeight = (uint32_t)pValue->uintVal; + hr = S_OK; + } else if (MsRdpEx_StringEquals(propName, "RecordingPath")) { if (pValue->vt != VT_BSTR) @@ -971,6 +995,21 @@ HRESULT __stdcall CMsRdpExtendedSettings::get_Property(BSTR bstrPropertyName, VA pValue->intVal = (INT)m_VideoRecordingQuality; hr = S_OK; } + else if (MsRdpEx_StringEquals(propName, "VideoRecordingFrameRate")) { + pValue->vt = VT_I4; + pValue->intVal = (INT)m_VideoRecordingFrameRate; + hr = S_OK; + } + else if (MsRdpEx_StringEquals(propName, "VideoRecordingWidth")) { + pValue->vt = VT_I4; + pValue->intVal = (INT)m_VideoRecordingWidth; + hr = S_OK; + } + else if (MsRdpEx_StringEquals(propName, "VideoRecordingHeight")) { + pValue->vt = VT_I4; + pValue->intVal = (INT)m_VideoRecordingHeight; + hr = S_OK; + } else if (MsRdpEx_StringEquals(propName, "RecordingPath")) { pValue->vt = VT_BSTR; const char* recordingPath = m_RecordingPath ? m_RecordingPath : ""; @@ -1384,6 +1423,24 @@ HRESULT CMsRdpExtendedSettings::ApplyRdpFile(void* rdpFilePtr) pMsRdpExtendedSettings->put_Property(propName, &value); } } + else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "VideoRecordingFrameRate")) { + if (MsRdpEx_RdpFileEntry_GetIntValue(entry, &value)) { + bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); + pMsRdpExtendedSettings->put_Property(propName, &value); + } + } + else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "VideoRecordingWidth")) { + if (MsRdpEx_RdpFileEntry_GetIntValue(entry, &value)) { + bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); + pMsRdpExtendedSettings->put_Property(propName, &value); + } + } + else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "VideoRecordingHeight")) { + if (MsRdpEx_RdpFileEntry_GetIntValue(entry, &value)) { + bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); + pMsRdpExtendedSettings->put_Property(propName, &value); + } + } else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 's', "RecordingPath")) { bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); bstr_t propValue = _com_util::ConvertStringToBSTR(entry->value); @@ -1635,6 +1692,21 @@ uint32_t CMsRdpExtendedSettings::GetVideoRecordingQuality() return m_VideoRecordingQuality; } +uint32_t CMsRdpExtendedSettings::GetVideoRecordingFrameRate() +{ + return m_VideoRecordingFrameRate; +} + +uint32_t CMsRdpExtendedSettings::GetVideoRecordingWidth() +{ + return m_VideoRecordingWidth; +} + +uint32_t CMsRdpExtendedSettings::GetVideoRecordingHeight() +{ + return m_VideoRecordingHeight; +} + char* CMsRdpExtendedSettings::GetRecordingPath() { if (m_RecordingPath) diff --git a/include/MsRdpEx/OutputMirror.h b/include/MsRdpEx/OutputMirror.h index 121131c..030db07 100644 --- a/include/MsRdpEx/OutputMirror.h +++ b/include/MsRdpEx/OutputMirror.h @@ -22,6 +22,8 @@ bool MsRdpEx_OutputMirror_DumpFrame(MsRdpEx_OutputMirror* ctx); void MsRdpEx_OutputMirror_SetDumpBitmapUpdates(MsRdpEx_OutputMirror* ctx, bool dumpBitmapUpdates); void MsRdpEx_OutputMirror_SetVideoRecordingEnabled(MsRdpEx_OutputMirror* ctx, bool videoRecordingEnabled); void MsRdpEx_OutputMirror_SetVideoQualityLevel(MsRdpEx_OutputMirror* ctx, uint32_t videoQualityLevel); +void MsRdpEx_OutputMirror_SetVideoFrameRate(MsRdpEx_OutputMirror* ctx, uint32_t videoFrameRate); +void MsRdpEx_OutputMirror_SetRecordingResolution(MsRdpEx_OutputMirror* ctx, uint32_t recordingWidth, uint32_t recordingHeight); void MsRdpEx_OutputMirror_SetRecordingPath(MsRdpEx_OutputMirror* ctx, const char* recordingPath); void MsRdpEx_OutputMirror_SetRecordingPipeName(MsRdpEx_OutputMirror* ctx, const char* recordingPipeName); void MsRdpEx_OutputMirror_SetSessionId(MsRdpEx_OutputMirror* ctx, const char* sessionId); diff --git a/include/MsRdpEx/RdpSettings.h b/include/MsRdpEx/RdpSettings.h index 3e75f68..7979e46 100644 --- a/include/MsRdpEx/RdpSettings.h +++ b/include/MsRdpEx/RdpSettings.h @@ -62,6 +62,9 @@ class CMsRdpExtendedSettings : public IMsRdpExtendedSettings bool GetOutputMirrorEnabled(); bool GetVideoRecordingEnabled(); uint32_t GetVideoRecordingQuality(); + uint32_t GetVideoRecordingFrameRate(); + uint32_t GetVideoRecordingWidth(); + uint32_t GetVideoRecordingHeight(); char* GetRecordingPath(); char* GetRecordingSessionId(); char* GetRecordingPipeName(); @@ -88,6 +91,9 @@ class CMsRdpExtendedSettings : public IMsRdpExtendedSettings bool m_OutputMirrorEnabled = false; bool m_VideoRecordingEnabled = false; uint32_t m_VideoRecordingQuality = 5; + uint32_t m_VideoRecordingFrameRate = 0; + uint32_t m_VideoRecordingWidth = 0; + uint32_t m_VideoRecordingHeight = 0; char* m_RecordingPath = NULL; char* m_RecordingSessionId = NULL; char* m_RecordingPipeName = NULL; From 8d73e190c2b7507914cf336fa129753e773af832 Mon Sep 17 00:00:00 2001 From: irving ou Date: Mon, 15 Jun 2026 14:32:59 -0400 Subject: [PATCH 2/6] Reduce recording controls to frame rate only (DVLS-14562) Dropped the resolution downscaling work (StretchBlt scaled DIB and the VideoRecordingWidth / VideoRecordingHeight extended properties) to keep the change focused. Only the VideoRecordingFrameRate extended property and the paint-cadence throttle remain, plus frame-rate input clamping/validation. Co-Authored-By: Claude Opus 4.8 (1M context) --- PowerShell/packages.lock.json | 14 ------- dll/ApiHooks.cpp | 5 --- dll/OutputMirror.c | 72 +++++----------------------------- dll/RdpSettings.cpp | 63 ++++++++--------------------- include/MsRdpEx/OutputMirror.h | 1 - include/MsRdpEx/RdpSettings.h | 4 -- 6 files changed, 25 insertions(+), 134 deletions(-) diff --git a/PowerShell/packages.lock.json b/PowerShell/packages.lock.json index 9ff0ffd..ed663db 100644 --- a/PowerShell/packages.lock.json +++ b/PowerShell/packages.lock.json @@ -8,25 +8,11 @@ "resolved": "2022.1.24", "contentHash": "zUhuNeCJOlgO0iho9JbyFeoagGE9belZKKKSJakwtvewTCk+G1L3T3bTr3J/xLO1tE3kSBlakJQD8ol11+ExKQ==" }, - "Microsoft.NETFramework.ReferenceAssemblies": { - "type": "Direct", - "requested": "[1.0.3, )", - "resolved": "1.0.3", - "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", - "dependencies": { - "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" - } - }, "PowerShellStandard.Library": { "type": "Direct", "requested": "[5.1.0, )", "resolved": "5.1.0", "contentHash": "iYaRvQsM1fow9h3uEmio+2m2VXfulgI16AYHaTZ8Sf7erGe27Qc8w/h6QL5UPuwv1aXR40QfzMEwcCeiYJp2cw==" - }, - "Microsoft.NETFramework.ReferenceAssemblies.net48": { - "type": "Transitive", - "resolved": "1.0.3", - "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" } } } diff --git a/dll/ApiHooks.cpp b/dll/ApiHooks.cpp index 2a61845..cdc4f7b 100644 --- a/dll/ApiHooks.cpp +++ b/dll/ApiHooks.cpp @@ -432,8 +432,6 @@ bool WINAPI MsRdpEx_CaptureBlt( bool videoRecordingEnabled = false; uint32_t videoRecordingQuality = 5; uint32_t videoRecordingFrameRate = 0; - uint32_t videoRecordingWidth = 0; - uint32_t videoRecordingHeight = 0; bool dumpBitmapUpdates = false; IMsRdpExInstance* instance = NULL; MsRdpEx_OutputMirror* outputMirror = NULL; @@ -461,8 +459,6 @@ bool WINAPI MsRdpEx_CaptureBlt( videoRecordingEnabled = pExtendedSettings->GetVideoRecordingEnabled(); videoRecordingQuality = pExtendedSettings->GetVideoRecordingQuality(); videoRecordingFrameRate = pExtendedSettings->GetVideoRecordingFrameRate(); - videoRecordingWidth = pExtendedSettings->GetVideoRecordingWidth(); - videoRecordingHeight = pExtendedSettings->GetVideoRecordingHeight(); dumpBitmapUpdates = pExtendedSettings->GetDumpBitmapUpdates(); if (!outputMirrorEnabled) @@ -483,7 +479,6 @@ bool WINAPI MsRdpEx_CaptureBlt( MsRdpEx_OutputMirror_SetVideoRecordingEnabled(outputMirror, videoRecordingEnabled); MsRdpEx_OutputMirror_SetVideoQualityLevel(outputMirror, videoRecordingQuality); MsRdpEx_OutputMirror_SetVideoFrameRate(outputMirror, videoRecordingFrameRate); - MsRdpEx_OutputMirror_SetRecordingResolution(outputMirror, videoRecordingWidth, videoRecordingHeight); char* recordingPath = pExtendedSettings->GetRecordingPath(); if (recordingPath) { diff --git a/dll/OutputMirror.c b/dll/OutputMirror.c index 60e40cd..7e92caf 100644 --- a/dll/OutputMirror.c +++ b/dll/OutputMirror.c @@ -7,6 +7,8 @@ #include #include +#define MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE 60 + struct _MsRdpEx_OutputMirror { uint8_t* bitmapData; @@ -35,19 +37,8 @@ struct _MsRdpEx_OutputMirror FILE* frameMetadataFile; uint32_t videoFrameRate; - uint32_t recordingWidth; - uint32_t recordingHeight; uint64_t lastEncodeTime; - bool scalingEnabled; - HDC hScaledDC; - HBITMAP hScaledBitmap; - HGDIOBJ hScaledObject; - uint8_t* scaledBitmapData; - uint32_t scaledBitmapWidth; - uint32_t scaledBitmapHeight; - uint32_t scaledBitmapStep; - CRITICAL_SECTION lock; }; @@ -114,17 +105,8 @@ bool MsRdpEx_OutputMirror_DumpFrame(MsRdpEx_OutputMirror* ctx) } if (encodeThisFrame) { - if (ctx->scalingEnabled) { - StretchBlt(ctx->hScaledDC, 0, 0, ctx->scaledBitmapWidth, ctx->scaledBitmapHeight, - ctx->hShadowDC, 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, SRCCOPY); - GdiFlush(); - MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->scaledBitmapData, - 0, 0, ctx->scaledBitmapWidth, ctx->scaledBitmapHeight, ctx->scaledBitmapStep); - } - else { - MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, - 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); - } + MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, + 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); MsRdpEx_VideoRecorder_Timeout(ctx->videoRecorder); } } @@ -164,13 +146,11 @@ void MsRdpEx_OutputMirror_SetVideoQualityLevel(MsRdpEx_OutputMirror* ctx, uint32 void MsRdpEx_OutputMirror_SetVideoFrameRate(MsRdpEx_OutputMirror* ctx, uint32_t videoFrameRate) { - ctx->videoFrameRate = videoFrameRate; -} + if (videoFrameRate > MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE) { + videoFrameRate = MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE; + } -void MsRdpEx_OutputMirror_SetRecordingResolution(MsRdpEx_OutputMirror* ctx, uint32_t recordingWidth, uint32_t recordingHeight) -{ - ctx->recordingWidth = recordingWidth; - ctx->recordingHeight = recordingHeight; + ctx->videoFrameRate = videoFrameRate; } void MsRdpEx_OutputMirror_SetRecordingPath(MsRdpEx_OutputMirror* ctx, const char* recordingPath) @@ -254,28 +234,6 @@ bool MsRdpEx_OutputMirror_Init(MsRdpEx_OutputMirror* ctx) sprintf_s(ctx->outputPath, MSRDPEX_MAX_PATH, "%s\\%s", ctx->recordingPath, ctx->sessionId); MsRdpEx_MakePath(ctx->outputPath, NULL); - uint32_t encodeWidth = ctx->bitmapWidth; - uint32_t encodeHeight = ctx->bitmapHeight; - - // Downscale the captured frame to the requested recording resolution before encoding. The xmf encoder - // does not scale, so without this it records at the native desktop size regardless of the configured value. - ctx->scalingEnabled = (ctx->recordingWidth > 0) && (ctx->recordingHeight > 0) - && ((ctx->recordingWidth != ctx->bitmapWidth) || (ctx->recordingHeight != ctx->bitmapHeight)); - - if (ctx->scalingEnabled) { - encodeWidth = ctx->recordingWidth; - encodeHeight = ctx->recordingHeight; - ctx->scaledBitmapWidth = encodeWidth; - ctx->scaledBitmapHeight = encodeHeight; - ctx->scaledBitmapStep = encodeWidth * 4; - ctx->hScaledDC = CreateCompatibleDC(ctx->hSourceDC); - ctx->hScaledBitmap = MsRdpEx_CreateDIBSection(ctx->hSourceDC, - encodeWidth, encodeHeight, ctx->bitsPerPixel, &ctx->scaledBitmapData); - ctx->hScaledObject = SelectObject(ctx->hScaledDC, ctx->hScaledBitmap); - SetStretchBltMode(ctx->hScaledDC, HALFTONE); - SetBrushOrgEx(ctx->hScaledDC, 0, 0, NULL); - } - ctx->videoRecorder = MsRdpEx_VideoRecorder_New(); if (ctx->videoRecorder) { @@ -284,7 +242,7 @@ bool MsRdpEx_OutputMirror_Init(MsRdpEx_OutputMirror* ctx) MsRdpEx_RecordingManifest_FinalizeFile(ctx->manifest, 0); MsRdpEx_RecordingManifest_AddFile(ctx->manifest, MsRdpEx_FileBase(filename), startTime, 0); ctx->videoRecordingCount++; - MsRdpEx_VideoRecorder_SetFrameSize(ctx->videoRecorder, encodeWidth, encodeHeight); + MsRdpEx_VideoRecorder_SetFrameSize(ctx->videoRecorder, ctx->bitmapWidth, ctx->bitmapHeight); MsRdpEx_VideoRecorder_SetFileName(ctx->videoRecorder, filename); MsRdpEx_VideoRecorder_SetVideoQuality(ctx->videoRecorder, ctx->videoQualityLevel); @@ -327,18 +285,6 @@ bool MsRdpEx_OutputMirror_Uninit(MsRdpEx_OutputMirror* ctx) ctx->hShadowDC = NULL; } - if (ctx->hScaledDC) - { - SelectObject(ctx->hScaledDC, ctx->hScaledObject); - DeleteObject(ctx->hScaledBitmap); - DeleteDC(ctx->hScaledDC); - ctx->hScaledObject = NULL; - ctx->hScaledBitmap = NULL; - ctx->hScaledDC = NULL; - ctx->scaledBitmapData = NULL; - ctx->scalingEnabled = false; - } - if (ctx->videoRecorder) { MsRdpEx_VideoRecorder_Uninit(ctx->videoRecorder); MsRdpEx_VideoRecorder_Remux(ctx->videoRecorder, NULL); diff --git a/dll/RdpSettings.cpp b/dll/RdpSettings.cpp index ce6c1c6..1f95d90 100644 --- a/dll/RdpSettings.cpp +++ b/dll/RdpSettings.cpp @@ -19,6 +19,19 @@ extern "C" const GUID IID_ITSPropertySet; extern MsRdpEx_mstscax g_mstscax; extern MsRdpEx_rdclientax g_rdclientax; +#define MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE 60 + +static uint32_t MsRdpEx_VariantToNonNegativeUInt32(VARIANT* pValue) +{ + if (pValue->vt == VT_UI4) + return pValue->uintVal; + + if (pValue->vt == VT_I4) + return pValue->intVal > 0 ? (uint32_t)pValue->intVal : 0; + + return 0; +} + static bool g_TSPropertySet_Hooked = false; static ITSPropertySet_SetBoolProperty Real_ITSPropertySet_SetBoolProperty = NULL; @@ -831,23 +844,11 @@ HRESULT __stdcall CMsRdpExtendedSettings::put_Property(BSTR bstrPropertyName, VA if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) goto end; - m_VideoRecordingFrameRate = (uint32_t)pValue->uintVal; - hr = S_OK; - } - else if (MsRdpEx_StringEquals(propName, "VideoRecordingWidth")) - { - if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) - goto end; + m_VideoRecordingFrameRate = MsRdpEx_VariantToNonNegativeUInt32(pValue); - m_VideoRecordingWidth = (uint32_t)pValue->uintVal; - hr = S_OK; - } - else if (MsRdpEx_StringEquals(propName, "VideoRecordingHeight")) - { - if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) - goto end; + if (m_VideoRecordingFrameRate > MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE) + m_VideoRecordingFrameRate = MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE; - m_VideoRecordingHeight = (uint32_t)pValue->uintVal; hr = S_OK; } else if (MsRdpEx_StringEquals(propName, "RecordingPath")) @@ -1000,16 +1001,6 @@ HRESULT __stdcall CMsRdpExtendedSettings::get_Property(BSTR bstrPropertyName, VA pValue->intVal = (INT)m_VideoRecordingFrameRate; hr = S_OK; } - else if (MsRdpEx_StringEquals(propName, "VideoRecordingWidth")) { - pValue->vt = VT_I4; - pValue->intVal = (INT)m_VideoRecordingWidth; - hr = S_OK; - } - else if (MsRdpEx_StringEquals(propName, "VideoRecordingHeight")) { - pValue->vt = VT_I4; - pValue->intVal = (INT)m_VideoRecordingHeight; - hr = S_OK; - } else if (MsRdpEx_StringEquals(propName, "RecordingPath")) { pValue->vt = VT_BSTR; const char* recordingPath = m_RecordingPath ? m_RecordingPath : ""; @@ -1429,18 +1420,6 @@ HRESULT CMsRdpExtendedSettings::ApplyRdpFile(void* rdpFilePtr) pMsRdpExtendedSettings->put_Property(propName, &value); } } - else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "VideoRecordingWidth")) { - if (MsRdpEx_RdpFileEntry_GetIntValue(entry, &value)) { - bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); - pMsRdpExtendedSettings->put_Property(propName, &value); - } - } - else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 'i', "VideoRecordingHeight")) { - if (MsRdpEx_RdpFileEntry_GetIntValue(entry, &value)) { - bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); - pMsRdpExtendedSettings->put_Property(propName, &value); - } - } else if (MsRdpEx_RdpFileEntry_IsMatch(entry, 's', "RecordingPath")) { bstr_t propName = _com_util::ConvertStringToBSTR(entry->name); bstr_t propValue = _com_util::ConvertStringToBSTR(entry->value); @@ -1697,16 +1676,6 @@ uint32_t CMsRdpExtendedSettings::GetVideoRecordingFrameRate() return m_VideoRecordingFrameRate; } -uint32_t CMsRdpExtendedSettings::GetVideoRecordingWidth() -{ - return m_VideoRecordingWidth; -} - -uint32_t CMsRdpExtendedSettings::GetVideoRecordingHeight() -{ - return m_VideoRecordingHeight; -} - char* CMsRdpExtendedSettings::GetRecordingPath() { if (m_RecordingPath) diff --git a/include/MsRdpEx/OutputMirror.h b/include/MsRdpEx/OutputMirror.h index 030db07..16d6d21 100644 --- a/include/MsRdpEx/OutputMirror.h +++ b/include/MsRdpEx/OutputMirror.h @@ -23,7 +23,6 @@ void MsRdpEx_OutputMirror_SetDumpBitmapUpdates(MsRdpEx_OutputMirror* ctx, bool d void MsRdpEx_OutputMirror_SetVideoRecordingEnabled(MsRdpEx_OutputMirror* ctx, bool videoRecordingEnabled); void MsRdpEx_OutputMirror_SetVideoQualityLevel(MsRdpEx_OutputMirror* ctx, uint32_t videoQualityLevel); void MsRdpEx_OutputMirror_SetVideoFrameRate(MsRdpEx_OutputMirror* ctx, uint32_t videoFrameRate); -void MsRdpEx_OutputMirror_SetRecordingResolution(MsRdpEx_OutputMirror* ctx, uint32_t recordingWidth, uint32_t recordingHeight); void MsRdpEx_OutputMirror_SetRecordingPath(MsRdpEx_OutputMirror* ctx, const char* recordingPath); void MsRdpEx_OutputMirror_SetRecordingPipeName(MsRdpEx_OutputMirror* ctx, const char* recordingPipeName); void MsRdpEx_OutputMirror_SetSessionId(MsRdpEx_OutputMirror* ctx, const char* sessionId); diff --git a/include/MsRdpEx/RdpSettings.h b/include/MsRdpEx/RdpSettings.h index 7979e46..3e99258 100644 --- a/include/MsRdpEx/RdpSettings.h +++ b/include/MsRdpEx/RdpSettings.h @@ -63,8 +63,6 @@ class CMsRdpExtendedSettings : public IMsRdpExtendedSettings bool GetVideoRecordingEnabled(); uint32_t GetVideoRecordingQuality(); uint32_t GetVideoRecordingFrameRate(); - uint32_t GetVideoRecordingWidth(); - uint32_t GetVideoRecordingHeight(); char* GetRecordingPath(); char* GetRecordingSessionId(); char* GetRecordingPipeName(); @@ -92,8 +90,6 @@ class CMsRdpExtendedSettings : public IMsRdpExtendedSettings bool m_VideoRecordingEnabled = false; uint32_t m_VideoRecordingQuality = 5; uint32_t m_VideoRecordingFrameRate = 0; - uint32_t m_VideoRecordingWidth = 0; - uint32_t m_VideoRecordingHeight = 0; char* m_RecordingPath = NULL; char* m_RecordingSessionId = NULL; char* m_RecordingPipeName = NULL; From fcd409fe5cf2d1d9b8d9357513c308bb4a4a4268 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 16 Jun 2026 16:06:50 -0400 Subject: [PATCH 3/6] Let cadeau drive the encode cadence in the native path (DVLS-14562) Submit every captured paint and rely on cadeau's frame-rate cap (ms_per_frame) instead of a per-paint Timeout force-encode, which bypassed the cap and produced ~24fps oversized recordings. The frame rate is conveyed once via SetFrameRate in Init. --- dll/OutputMirror.c | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/dll/OutputMirror.c b/dll/OutputMirror.c index 7e92caf..91d8290 100644 --- a/dll/OutputMirror.c +++ b/dll/OutputMirror.c @@ -37,7 +37,6 @@ struct _MsRdpEx_OutputMirror FILE* frameMetadataFile; uint32_t videoFrameRate; - uint64_t lastEncodeTime; CRITICAL_SECTION lock; }; @@ -88,27 +87,11 @@ bool MsRdpEx_OutputMirror_DumpFrame(MsRdpEx_OutputMirror* ctx) captureTime = GetTickCount64() - ctx->captureBaseTime; if (ctx->videoRecordingEnabled && ctx->videoRecorder) { - // The native path is paint-driven (one DumpFrame per RDP update), so cap the encode cadence to the - // configured frame rate here -- the encoder itself does not drop frames. Mirrors the internal timer path. - bool encodeThisFrame = true; - - if (ctx->videoFrameRate > 0) { - uint64_t now = GetTickCount64(); - uint32_t intervalMs = 1000 / ctx->videoFrameRate; - - if ((ctx->lastEncodeTime != 0) && ((now - ctx->lastEncodeTime) < intervalMs)) { - encodeThisFrame = false; - } - else { - ctx->lastEncodeTime = now; - } - } - - if (encodeThisFrame) { - MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, - 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); - MsRdpEx_VideoRecorder_Timeout(ctx->videoRecorder); - } + // [DVLS-14562] Submit every captured paint and let cadeau cap the encode rate to the configured + // frame rate (ms_per_frame). No manual throttle and no per-paint Timeout -- Timeout force-encodes + // and would bypass cadeau's cap. The frame rate is conveyed once via SetFrameRate in Init. + MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, + 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); } if (ctx->dumpBitmapUpdates) { From a34de8a4b96cd9975f0a05725588ce37fa6a97ea Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 16 Jun 2026 16:14:10 -0400 Subject: [PATCH 4/6] Shorten cadence comment, drop ticket reference --- dll/OutputMirror.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dll/OutputMirror.c b/dll/OutputMirror.c index 91d8290..c52cc4b 100644 --- a/dll/OutputMirror.c +++ b/dll/OutputMirror.c @@ -87,9 +87,7 @@ bool MsRdpEx_OutputMirror_DumpFrame(MsRdpEx_OutputMirror* ctx) captureTime = GetTickCount64() - ctx->captureBaseTime; if (ctx->videoRecordingEnabled && ctx->videoRecorder) { - // [DVLS-14562] Submit every captured paint and let cadeau cap the encode rate to the configured - // frame rate (ms_per_frame). No manual throttle and no per-paint Timeout -- Timeout force-encodes - // and would bypass cadeau's cap. The frame rate is conveyed once via SetFrameRate in Init. + // Submit every paint; cadeau caps the encode rate. A per-paint Timeout would force-encode and bypass that cap. MsRdpEx_VideoRecorder_UpdateFrame(ctx->videoRecorder, ctx->bitmapData, 0, 0, ctx->bitmapWidth, ctx->bitmapHeight, ctx->bitmapStep); } From d824e4b4ba7c65b63dc086521c7f6dd475f6d619 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 16 Jun 2026 16:23:18 -0400 Subject: [PATCH 5/6] Simplify frame-rate plumbing and drop unrelated lock-file churn Match the existing VideoRecordingQuality idiom: remove the redundant VariantToNonNegativeUInt32 helper, clamp the frame rate once at the input boundary, and stop re-clamping in the output mirror. Also revert an unrelated PowerShell packages.lock.json change. --- PowerShell/packages.lock.json | 14 ++++++++++++++ dll/OutputMirror.c | 6 ------ dll/RdpSettings.cpp | 13 +------------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/PowerShell/packages.lock.json b/PowerShell/packages.lock.json index ed663db..9ff0ffd 100644 --- a/PowerShell/packages.lock.json +++ b/PowerShell/packages.lock.json @@ -8,11 +8,25 @@ "resolved": "2022.1.24", "contentHash": "zUhuNeCJOlgO0iho9JbyFeoagGE9belZKKKSJakwtvewTCk+G1L3T3bTr3J/xLO1tE3kSBlakJQD8ol11+ExKQ==" }, + "Microsoft.NETFramework.ReferenceAssemblies": { + "type": "Direct", + "requested": "[1.0.3, )", + "resolved": "1.0.3", + "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==", + "dependencies": { + "Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3" + } + }, "PowerShellStandard.Library": { "type": "Direct", "requested": "[5.1.0, )", "resolved": "5.1.0", "contentHash": "iYaRvQsM1fow9h3uEmio+2m2VXfulgI16AYHaTZ8Sf7erGe27Qc8w/h6QL5UPuwv1aXR40QfzMEwcCeiYJp2cw==" + }, + "Microsoft.NETFramework.ReferenceAssemblies.net48": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==" } } } diff --git a/dll/OutputMirror.c b/dll/OutputMirror.c index c52cc4b..3212cef 100644 --- a/dll/OutputMirror.c +++ b/dll/OutputMirror.c @@ -7,8 +7,6 @@ #include #include -#define MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE 60 - struct _MsRdpEx_OutputMirror { uint8_t* bitmapData; @@ -127,10 +125,6 @@ void MsRdpEx_OutputMirror_SetVideoQualityLevel(MsRdpEx_OutputMirror* ctx, uint32 void MsRdpEx_OutputMirror_SetVideoFrameRate(MsRdpEx_OutputMirror* ctx, uint32_t videoFrameRate) { - if (videoFrameRate > MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE) { - videoFrameRate = MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE; - } - ctx->videoFrameRate = videoFrameRate; } diff --git a/dll/RdpSettings.cpp b/dll/RdpSettings.cpp index 1f95d90..def5538 100644 --- a/dll/RdpSettings.cpp +++ b/dll/RdpSettings.cpp @@ -21,17 +21,6 @@ extern MsRdpEx_rdclientax g_rdclientax; #define MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE 60 -static uint32_t MsRdpEx_VariantToNonNegativeUInt32(VARIANT* pValue) -{ - if (pValue->vt == VT_UI4) - return pValue->uintVal; - - if (pValue->vt == VT_I4) - return pValue->intVal > 0 ? (uint32_t)pValue->intVal : 0; - - return 0; -} - static bool g_TSPropertySet_Hooked = false; static ITSPropertySet_SetBoolProperty Real_ITSPropertySet_SetBoolProperty = NULL; @@ -844,7 +833,7 @@ HRESULT __stdcall CMsRdpExtendedSettings::put_Property(BSTR bstrPropertyName, VA if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) goto end; - m_VideoRecordingFrameRate = MsRdpEx_VariantToNonNegativeUInt32(pValue); + m_VideoRecordingFrameRate = (uint32_t)pValue->uintVal; if (m_VideoRecordingFrameRate > MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE) m_VideoRecordingFrameRate = MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE; From eef34fdb37092f3e67ceb5a8bd15117152573d43 Mon Sep 17 00:00:00 2001 From: irving ou Date: Tue, 16 Jun 2026 16:46:34 -0400 Subject: [PATCH 6/6] Align frame-rate cap with cadeau and handle signed input Clamp VideoRecordingFrameRate to 30 to match cadeau's recorder max (was 60, which the encoder silently capped). Read intVal for VT_I4 and treat negatives as 0 so .rdp-parsed values are not reinterpreted as large unsigned numbers. --- dll/RdpSettings.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dll/RdpSettings.cpp b/dll/RdpSettings.cpp index def5538..f0f3c1b 100644 --- a/dll/RdpSettings.cpp +++ b/dll/RdpSettings.cpp @@ -19,7 +19,7 @@ extern "C" const GUID IID_ITSPropertySet; extern MsRdpEx_mstscax g_mstscax; extern MsRdpEx_rdclientax g_rdclientax; -#define MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE 60 +#define MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE 30 static bool g_TSPropertySet_Hooked = false; @@ -833,7 +833,10 @@ HRESULT __stdcall CMsRdpExtendedSettings::put_Property(BSTR bstrPropertyName, VA if ((pValue->vt != VT_UI4) && (pValue->vt != VT_I4)) goto end; - m_VideoRecordingFrameRate = (uint32_t)pValue->uintVal; + if (pValue->vt == VT_I4) + m_VideoRecordingFrameRate = (pValue->intVal > 0) ? (uint32_t)pValue->intVal : 0; + else + m_VideoRecordingFrameRate = pValue->uintVal; if (m_VideoRecordingFrameRate > MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE) m_VideoRecordingFrameRate = MSRDPEX_VIDEO_RECORDING_MAX_FRAME_RATE;