From 7682e7279a1034d8f6740500114274605543b140 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 4 Aug 2026 17:05:30 -0700 Subject: [PATCH 1/5] Keep prepared audio mutable during capture --- Directory.Version.props | 2 +- .../VideoCaptureOptions.cs | 3 +- .../CaptureSessionConfig.h | 30 ++++++++++++++----- .../WindowsGraphicsCaptureSession.cpp | 5 +++- .../ScreenRecorder.cpp | 9 ++++-- .../ScreenRecorder.h | 1 + src/CaptureKit.Windows/NativeInterop.cs | 2 ++ ...indowsGraphicsCaptureSessionAudioTests.cpp | 28 ++++++++++++++++- .../CaptureRecorderBoundaryTests.cs | 15 ++++++++++ 9 files changed, 80 insertions(+), 15 deletions(-) diff --git a/Directory.Version.props b/Directory.Version.props index 84bd89c..c2ba0cb 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,6 +1,6 @@ - 1.0.3 + 1.0.4 $(CaptureKitVersion) diff --git a/src/CaptureKit.Abstractions/VideoCaptureOptions.cs b/src/CaptureKit.Abstractions/VideoCaptureOptions.cs index 3cbeb8c..9548374 100644 --- a/src/CaptureKit.Abstractions/VideoCaptureOptions.cs +++ b/src/CaptureKit.Abstractions/VideoCaptureOptions.cs @@ -8,4 +8,5 @@ public sealed record VideoCaptureOptions( uint VideoBitrate = 5_000_000, uint AudioBitrate = 128_000, string? AudioInputSourceId = null, - int AudioInputVolumePercentage = 100); + int AudioInputVolumePercentage = 100, + bool PrepareAudioPipeline = false); diff --git a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h index b379506..15cd29e 100644 --- a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h +++ b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h @@ -96,6 +96,12 @@ struct CaptureSessionConfig /// bool audioEnabled; + /// + /// Whether to initialize an initially muted audio pipeline so live audio + /// controls can enable a source later in the same recording. + /// + bool prepareAudioPipeline; + /// /// Optional Windows audio input endpoint id. Empty uses the existing default audio source. /// @@ -132,7 +138,8 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + bool prepareAudio = false) : targetType(CaptureTargetType::Monitor) , hMonitor(monitor) , hwnd(nullptr) @@ -142,6 +149,7 @@ struct CaptureSessionConfig , sourceHeight(0) , outputPath(path ? path : L"") , audioEnabled(audio) + , prepareAudioPipeline(prepareAudio) , frameRate(fps) , videoBitrate(vidBitrate) , audioBitrate(audBitrate) @@ -161,7 +169,8 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + bool prepareAudio = false) : targetType(CaptureTargetType::Monitor) , hMonitor(monitor) , hwnd(nullptr) @@ -171,6 +180,7 @@ struct CaptureSessionConfig , sourceHeight(0) , outputPath(std::move(path)) , audioEnabled(audio) + , prepareAudioPipeline(prepareAudio) , frameRate(fps) , videoBitrate(vidBitrate) , audioBitrate(audBitrate) @@ -192,6 +202,7 @@ struct CaptureSessionConfig , sourceHeight(0) , outputPath(L"") , audioEnabled(false) + , prepareAudioPipeline(false) , frameRate(30) , videoBitrate(5'000'000) , audioBitrate(128'000) @@ -208,9 +219,10 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + bool prepareAudio = false) { - CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); + CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, prepareAudio); config.targetType = CaptureTargetType::Monitor; return config; } @@ -223,9 +235,10 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + bool prepareAudio = false) { - CaptureSessionConfig config(nullptr, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); + CaptureSessionConfig config(nullptr, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, prepareAudio); config.targetType = CaptureTargetType::Window; config.hwnd = window; return config; @@ -243,9 +256,10 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + bool prepareAudio = false) { - CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); + CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, prepareAudio); config.targetType = CaptureTargetType::Rectangle; config.sourceLeft = left; config.sourceTop = top; diff --git a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp index 8491adf..39a4960 100644 --- a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp +++ b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp @@ -98,7 +98,10 @@ WindowsGraphicsCaptureSession::~WindowsGraphicsCaptureSession() bool WindowsGraphicsCaptureSession::Initialize(HRESULT* outHr) { HRESULT hr = S_OK; - const bool audioRequested = m_config.audioEnabled || !m_config.audioInputSourceId.empty(); + const bool audioRequested = + m_config.prepareAudioPipeline || + m_config.audioEnabled || + !m_config.audioInputSourceId.empty(); // Validate state - must be in Created state to initialize if (m_stateMachine.GetState() != CaptureSessionState::Created) diff --git a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp index f3cf1fe..4c211e7 100644 --- a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp +++ b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp @@ -63,7 +63,8 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage); + options->audioInputVolumePercentage, + options->prepareAudioPipeline != 0); break; case CaptureRecordingTargetKind::Window: @@ -79,7 +80,8 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage); + options->audioInputVolumePercentage, + options->prepareAudioPipeline != 0); break; case CaptureRecordingTargetKind::Rectangle: @@ -99,7 +101,8 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage); + options->audioInputVolumePercentage, + options->prepareAudioPipeline != 0); break; default: diff --git a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h index 6507df7..fa7f302 100644 --- a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h +++ b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h @@ -41,6 +41,7 @@ struct CaptureRecordingOptions uint32_t audioBitrate; const wchar_t* audioInputSourceId; uint32_t audioInputVolumePercentage; + uint32_t prepareAudioPipeline; }; extern "C" diff --git a/src/CaptureKit.Windows/NativeInterop.cs b/src/CaptureKit.Windows/NativeInterop.cs index 90dfc3f..7f9b6aa 100644 --- a/src/CaptureKit.Windows/NativeInterop.cs +++ b/src/CaptureKit.Windows/NativeInterop.cs @@ -132,6 +132,7 @@ public NativeVideoCaptureOptions(VideoCaptureOptions options) AudioBitrate = options.AudioBitrate; AudioInputSourceId = options.AudioInputSourceId; AudioInputVolumePercentage = (uint)Math.Clamp(options.AudioInputVolumePercentage, 0, 100); + PrepareAudioPipeline = options.PrepareAudioPipeline ? 1u : 0u; } public readonly CaptureTargetKind TargetKind; @@ -154,6 +155,7 @@ public NativeVideoCaptureOptions(VideoCaptureOptions options) public readonly string? AudioInputSourceId; public readonly uint AudioInputVolumePercentage; + public readonly uint PrepareAudioPipeline; } [StructLayout(LayoutKind.Sequential)] diff --git a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp index 7a5fef0..ade66ad 100644 --- a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp +++ b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp @@ -258,13 +258,17 @@ namespace CaptureInteropTests std::shared_ptr m_state; }; - static CaptureSessionConfig CreateConfig(bool desktopAudio, std::wstring inputSourceId = L"") + static CaptureSessionConfig CreateConfig( + bool desktopAudio, + std::wstring inputSourceId = L"", + bool prepareAudioPipeline = false) { CaptureSessionConfig config( reinterpret_cast(static_cast(1)), L"C:\\capturekit-optional-audio-test.mp4", desktopAudio); config.audioInputSourceId = std::move(inputSourceId); + config.prepareAudioPipeline = prepareAudioPipeline; return config; } @@ -344,6 +348,28 @@ namespace CaptureInteropTests session->Stop(); } + TEST_METHOD(PreparedMutedAudio_CanEnableDuringActiveRecording) + { + auto audio = std::make_shared(); + auto video = std::make_shared(); + auto sink = std::make_shared(); + auto session = CreateSession(CreateConfig(false, L"", true), audio, video, sink); + + HRESULT hr = S_OK; + Assert::IsTrue(session->Initialize(&hr)); + Assert::IsTrue(session->Start(&hr)); + + Assert::AreEqual(1, audio->initializeCalls); + Assert::AreEqual(1, audio->startCalls); + Assert::IsFalse(audio->enabled, L"Prepared desktop audio must start muted"); + Assert::AreEqual(1, sink->initializeAudioCalls); + + session->ToggleAudioCapture(true); + + Assert::IsTrue(audio->enabled, L"Prepared desktop audio must support false-to-true transitions"); + session->Stop(); + } + TEST_METHOD(AudioInitializationFailure_ContinuesWithVideoOnlySink) { auto audio = std::make_shared(); diff --git a/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs b/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs index d1e234e..e8a7494 100644 --- a/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs +++ b/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs @@ -31,6 +31,21 @@ public void EnsureSuccess_Success_DoesNotThrow() act.Should().NotThrow(); } + [TestMethod] + public void NativeVideoOptions_PreservePreparedAudioPipelineRequest() + { + var options = new VideoCaptureOptions( + CaptureTarget.Monitor((nint)1), + "capture.mp4", + CaptureAudio: false, + PrepareAudioPipeline: true); + + var nativeOptions = new NativeVideoCaptureOptions(options); + + nativeOptions.CaptureAudio.Should().Be(0u); + nativeOptions.PrepareAudioPipeline.Should().Be(1u); + } + [TestMethod] public void AudioSample_DoesNotReportRecordingStarted() { From d9467ffc9d2e6045f7a3c3db5014e13399060edc Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 4 Aug 2026 17:23:13 -0700 Subject: [PATCH 2/5] Always prepare video audio controls --- .../VideoCaptureOptions.cs | 3 +- .../CaptureSessionConfig.h | 30 +++------- .../WindowsGraphicsCaptureSession.cpp | 37 +++++-------- .../ScreenRecorder.cpp | 9 +-- .../ScreenRecorder.h | 1 - src/CaptureKit.Windows/NativeInterop.cs | 2 - ...indowsGraphicsCaptureSessionAudioTests.cpp | 55 +++++++------------ .../CaptureRecorderBoundaryTests.cs | 15 ----- 8 files changed, 47 insertions(+), 105 deletions(-) diff --git a/src/CaptureKit.Abstractions/VideoCaptureOptions.cs b/src/CaptureKit.Abstractions/VideoCaptureOptions.cs index 9548374..3cbeb8c 100644 --- a/src/CaptureKit.Abstractions/VideoCaptureOptions.cs +++ b/src/CaptureKit.Abstractions/VideoCaptureOptions.cs @@ -8,5 +8,4 @@ public sealed record VideoCaptureOptions( uint VideoBitrate = 5_000_000, uint AudioBitrate = 128_000, string? AudioInputSourceId = null, - int AudioInputVolumePercentage = 100, - bool PrepareAudioPipeline = false); + int AudioInputVolumePercentage = 100); diff --git a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h index 15cd29e..b379506 100644 --- a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h +++ b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h @@ -96,12 +96,6 @@ struct CaptureSessionConfig /// bool audioEnabled; - /// - /// Whether to initialize an initially muted audio pipeline so live audio - /// controls can enable a source later in the same recording. - /// - bool prepareAudioPipeline; - /// /// Optional Windows audio input endpoint id. Empty uses the existing default audio source. /// @@ -138,8 +132,7 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100, - bool prepareAudio = false) + uint32_t audioVolumePercentage = 100) : targetType(CaptureTargetType::Monitor) , hMonitor(monitor) , hwnd(nullptr) @@ -149,7 +142,6 @@ struct CaptureSessionConfig , sourceHeight(0) , outputPath(path ? path : L"") , audioEnabled(audio) - , prepareAudioPipeline(prepareAudio) , frameRate(fps) , videoBitrate(vidBitrate) , audioBitrate(audBitrate) @@ -169,8 +161,7 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100, - bool prepareAudio = false) + uint32_t audioVolumePercentage = 100) : targetType(CaptureTargetType::Monitor) , hMonitor(monitor) , hwnd(nullptr) @@ -180,7 +171,6 @@ struct CaptureSessionConfig , sourceHeight(0) , outputPath(std::move(path)) , audioEnabled(audio) - , prepareAudioPipeline(prepareAudio) , frameRate(fps) , videoBitrate(vidBitrate) , audioBitrate(audBitrate) @@ -202,7 +192,6 @@ struct CaptureSessionConfig , sourceHeight(0) , outputPath(L"") , audioEnabled(false) - , prepareAudioPipeline(false) , frameRate(30) , videoBitrate(5'000'000) , audioBitrate(128'000) @@ -219,10 +208,9 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100, - bool prepareAudio = false) + uint32_t audioVolumePercentage = 100) { - CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, prepareAudio); + CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); config.targetType = CaptureTargetType::Monitor; return config; } @@ -235,10 +223,9 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100, - bool prepareAudio = false) + uint32_t audioVolumePercentage = 100) { - CaptureSessionConfig config(nullptr, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, prepareAudio); + CaptureSessionConfig config(nullptr, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); config.targetType = CaptureTargetType::Window; config.hwnd = window; return config; @@ -256,10 +243,9 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100, - bool prepareAudio = false) + uint32_t audioVolumePercentage = 100) { - CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, prepareAudio); + CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); config.targetType = CaptureTargetType::Rectangle; config.sourceLeft = left; config.sourceTop = top; diff --git a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp index 39a4960..531cb08 100644 --- a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp +++ b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp @@ -98,10 +98,6 @@ WindowsGraphicsCaptureSession::~WindowsGraphicsCaptureSession() bool WindowsGraphicsCaptureSession::Initialize(HRESULT* outHr) { HRESULT hr = S_OK; - const bool audioRequested = - m_config.prepareAudioPipeline || - m_config.audioEnabled || - !m_config.audioInputSourceId.empty(); // Validate state - must be in Created state to initialize if (m_stateMachine.GetState() != CaptureSessionState::Created) @@ -117,10 +113,8 @@ bool WindowsGraphicsCaptureSession::Initialize(HRESULT* outHr) } // Validate dependencies - if (!m_mediaClock || !m_videoCaptureSource || !m_sinkWriter || (audioRequested && !m_audioCaptureSource)) + if (!m_mediaClock || !m_audioCaptureSource || !m_videoCaptureSource || !m_sinkWriter) { - // Audio is intentionally optional for a video-only session. All other dependencies, - // plus audio when requested, must be supplied by the factory. [[maybe_unused]] bool transitioned = m_stateMachine.TryTransitionTo(CaptureSessionState::Failed); // Transition should always succeed from Created to Failed assert(transitioned && "Transition to Failed should always succeed from Created state"); @@ -138,23 +132,20 @@ bool WindowsGraphicsCaptureSession::Initialize(HRESULT* outHr) } m_audioAvailable = false; - if (audioRequested) - { - // Audio is the preferred media-clock advancer when it was explicitly requested. - // A video-only session must not touch WASAPI or add an AAC stream. - m_mediaClock->SetClockAdvancer(m_audioCaptureSource.get()); + // Every video session prepares audio so mute, unmute, source, and volume controls + // remain live for the entire recording. audioEnabled is only the initial mute state. + m_mediaClock->SetClockAdvancer(m_audioCaptureSource.get()); - if (m_audioCaptureSource->Initialize(&hr) && m_audioCaptureSource->GetFormat()) - { - m_audioAvailable = true; - } - else - { - OutputDebugStringW(L"[CaptureInterop V1] Audio source initialization failed; continuing with video-only timing fallback.\r\n"); - m_audioCaptureSource->SetAudioSampleReadyCallback(nullptr); - m_audioCaptureSource->Stop(); - m_audioCaptureSource.reset(); - } + if (m_audioCaptureSource->Initialize(&hr) && m_audioCaptureSource->GetFormat()) + { + m_audioAvailable = true; + } + else + { + OutputDebugStringW(L"[CaptureInterop V1] Audio source initialization failed; continuing with video-only timing fallback.\r\n"); + m_audioCaptureSource->SetAudioSampleReadyCallback(nullptr); + m_audioCaptureSource->Stop(); + m_audioCaptureSource.reset(); } // Initialize sink writer diff --git a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp index 4c211e7..f3cf1fe 100644 --- a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp +++ b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp @@ -63,8 +63,7 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage, - options->prepareAudioPipeline != 0); + options->audioInputVolumePercentage); break; case CaptureRecordingTargetKind::Window: @@ -80,8 +79,7 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage, - options->prepareAudioPipeline != 0); + options->audioInputVolumePercentage); break; case CaptureRecordingTargetKind::Rectangle: @@ -101,8 +99,7 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage, - options->prepareAudioPipeline != 0); + options->audioInputVolumePercentage); break; default: diff --git a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h index fa7f302..6507df7 100644 --- a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h +++ b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h @@ -41,7 +41,6 @@ struct CaptureRecordingOptions uint32_t audioBitrate; const wchar_t* audioInputSourceId; uint32_t audioInputVolumePercentage; - uint32_t prepareAudioPipeline; }; extern "C" diff --git a/src/CaptureKit.Windows/NativeInterop.cs b/src/CaptureKit.Windows/NativeInterop.cs index 7f9b6aa..90dfc3f 100644 --- a/src/CaptureKit.Windows/NativeInterop.cs +++ b/src/CaptureKit.Windows/NativeInterop.cs @@ -132,7 +132,6 @@ public NativeVideoCaptureOptions(VideoCaptureOptions options) AudioBitrate = options.AudioBitrate; AudioInputSourceId = options.AudioInputSourceId; AudioInputVolumePercentage = (uint)Math.Clamp(options.AudioInputVolumePercentage, 0, 100); - PrepareAudioPipeline = options.PrepareAudioPipeline ? 1u : 0u; } public readonly CaptureTargetKind TargetKind; @@ -155,7 +154,6 @@ public NativeVideoCaptureOptions(VideoCaptureOptions options) public readonly string? AudioInputSourceId; public readonly uint AudioInputVolumePercentage; - public readonly uint PrepareAudioPipeline; } [StructLayout(LayoutKind.Sequential)] diff --git a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp index ade66ad..1c438f6 100644 --- a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp +++ b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp @@ -27,6 +27,7 @@ namespace CaptureInteropTests int stopCalls = 0; int setClockWriterCalls = 0; int setCallbackCalls = 0; + uint32_t volumePercentage = 100; bool enabled = true; bool running = false; }; @@ -87,7 +88,10 @@ namespace CaptureInteropTests return m_state->enabled; } - void SetVolume(uint32_t) override {} + void SetVolume(uint32_t volumePercentage) override + { + m_state->volumePercentage = volumePercentage; + } bool IsRunning() const override { @@ -258,17 +262,13 @@ namespace CaptureInteropTests std::shared_ptr m_state; }; - static CaptureSessionConfig CreateConfig( - bool desktopAudio, - std::wstring inputSourceId = L"", - bool prepareAudioPipeline = false) + static CaptureSessionConfig CreateConfig(bool desktopAudio, std::wstring inputSourceId = L"") { CaptureSessionConfig config( reinterpret_cast(static_cast(1)), L"C:\\capturekit-optional-audio-test.mp4", desktopAudio); config.audioInputSourceId = std::move(inputSourceId); - config.prepareAudioPipeline = prepareAudioPipeline; return config; } @@ -287,25 +287,34 @@ namespace CaptureInteropTests } public: - TEST_METHOD(VideoOnly_DoesNotInitializeOrStartAudioOrAddAac) + TEST_METHOD(InitiallyMutedAudio_InitializesPipelineAndCanEnableDuringCapture) { auto audio = std::make_shared(); auto video = std::make_shared(); auto sink = std::make_shared(); - auto session = CreateSession(CreateConfig(false), audio, video, sink); + auto config = CreateConfig(false); + config.audioInputVolumePercentage = 37; + auto session = CreateSession(config, audio, video, sink); HRESULT hr = S_OK; Assert::IsTrue(session->Initialize(&hr)); Assert::IsTrue(session->Start(&hr)); - Assert::AreEqual(0, audio->setClockWriterCalls); - Assert::AreEqual(0, audio->initializeCalls); - Assert::AreEqual(0, audio->startCalls); - Assert::AreEqual(0, sink->initializeAudioCalls); + Assert::AreEqual(1, audio->setClockWriterCalls); + Assert::AreEqual(1, audio->initializeCalls); + Assert::AreEqual(1, audio->startCalls); + Assert::IsFalse(audio->enabled, L"Desktop audio must honor its initial muted state"); + Assert::AreEqual(37u, audio->volumePercentage); + Assert::AreEqual(1, sink->initializeAudioCalls); Assert::AreEqual(1, sink->initializeCalls); Assert::AreEqual(1, sink->beginWritingCalls); Assert::AreEqual(1, sink->writeFrameCalls); + session->ToggleAudioCapture(true); + session->SetAudioInputVolume(64); + + Assert::IsTrue(audio->enabled, L"Desktop audio must support false-to-true transitions"); + Assert::AreEqual(64u, audio->volumePercentage, L"Desktop audio volume must remain mutable"); session->Stop(); } @@ -348,28 +357,6 @@ namespace CaptureInteropTests session->Stop(); } - TEST_METHOD(PreparedMutedAudio_CanEnableDuringActiveRecording) - { - auto audio = std::make_shared(); - auto video = std::make_shared(); - auto sink = std::make_shared(); - auto session = CreateSession(CreateConfig(false, L"", true), audio, video, sink); - - HRESULT hr = S_OK; - Assert::IsTrue(session->Initialize(&hr)); - Assert::IsTrue(session->Start(&hr)); - - Assert::AreEqual(1, audio->initializeCalls); - Assert::AreEqual(1, audio->startCalls); - Assert::IsFalse(audio->enabled, L"Prepared desktop audio must start muted"); - Assert::AreEqual(1, sink->initializeAudioCalls); - - session->ToggleAudioCapture(true); - - Assert::IsTrue(audio->enabled, L"Prepared desktop audio must support false-to-true transitions"); - session->Stop(); - } - TEST_METHOD(AudioInitializationFailure_ContinuesWithVideoOnlySink) { auto audio = std::make_shared(); diff --git a/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs b/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs index e8a7494..d1e234e 100644 --- a/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs +++ b/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs @@ -31,21 +31,6 @@ public void EnsureSuccess_Success_DoesNotThrow() act.Should().NotThrow(); } - [TestMethod] - public void NativeVideoOptions_PreservePreparedAudioPipelineRequest() - { - var options = new VideoCaptureOptions( - CaptureTarget.Monitor((nint)1), - "capture.mp4", - CaptureAudio: false, - PrepareAudioPipeline: true); - - var nativeOptions = new NativeVideoCaptureOptions(options); - - nativeOptions.CaptureAudio.Should().Be(0u); - nativeOptions.PrepareAudioPipeline.Should().Be(1u); - } - [TestMethod] public void AudioSample_DoesNotReportRecordingStarted() { From ed200863208bae30940963483666da144ade38dd Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 4 Aug 2026 17:54:03 -0700 Subject: [PATCH 3/5] Add independent system audio volume --- .../IVideoCaptureService.cs | 1 + .../VideoCaptureOptions.cs | 3 +- .../CaptureSessionConfig.h | 46 +++++++++++++------ .../CompositeAudioCaptureSource.cpp | 11 +++++ .../CompositeAudioCaptureSource.h | 1 + .../IAudioCaptureSource.h | 9 ++++ .../ICaptureSession.h | 5 ++ .../ScreenRecorderImpl.cpp | 11 +++++ .../ScreenRecorderImpl.h | 5 ++ .../WindowsGraphicsCaptureSession.cpp | 9 ++++ .../WindowsGraphicsCaptureSession.h | 1 + .../ScreenRecorder.cpp | 17 +++++-- .../ScreenRecorder.h | 2 + src/CaptureKit.Windows/NativeInterop.cs | 5 ++ src/CaptureKit.Windows/VideoCaptureService.cs | 11 +++++ .../CaptureRecorderBoundaryTests.cpp | 1 + ...indowsGraphicsCaptureSessionAudioTests.cpp | 20 ++++++-- .../CaptureRecorderBoundaryTests.cs | 35 ++++++++++++++ 18 files changed, 171 insertions(+), 22 deletions(-) diff --git a/src/CaptureKit.Abstractions/IVideoCaptureService.cs b/src/CaptureKit.Abstractions/IVideoCaptureService.cs index f84f27a..6499618 100644 --- a/src/CaptureKit.Abstractions/IVideoCaptureService.cs +++ b/src/CaptureKit.Abstractions/IVideoCaptureService.cs @@ -21,6 +21,7 @@ public interface IVideoCaptureSession : IDisposable void Pause(); void Resume(); void SetAudioCaptureEnabled(bool enabled); + void SetSystemAudioVolume(int volumePercentage); void SetAudioInputSource(string? sourceId); void SetAudioInputVolume(int volumePercentage); } diff --git a/src/CaptureKit.Abstractions/VideoCaptureOptions.cs b/src/CaptureKit.Abstractions/VideoCaptureOptions.cs index 3cbeb8c..95b06cd 100644 --- a/src/CaptureKit.Abstractions/VideoCaptureOptions.cs +++ b/src/CaptureKit.Abstractions/VideoCaptureOptions.cs @@ -8,4 +8,5 @@ public sealed record VideoCaptureOptions( uint VideoBitrate = 5_000_000, uint AudioBitrate = 128_000, string? AudioInputSourceId = null, - int AudioInputVolumePercentage = 100); + int AudioInputVolumePercentage = 100, + int SystemAudioVolumePercentage = 100); diff --git a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h index b379506..042b2d6 100644 --- a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h +++ b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h @@ -106,6 +106,11 @@ struct CaptureSessionConfig /// uint32_t audioInputVolumePercentage; + /// + /// System/desktop audio volume as a percentage. 100 preserves the captured signal. + /// + uint32_t systemAudioVolumePercentage; + /// /// Target video frame rate (FPS). Default is 30. /// @@ -132,7 +137,8 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + uint32_t systemVolumePercentage = 100) : targetType(CaptureTargetType::Monitor) , hMonitor(monitor) , hwnd(nullptr) @@ -147,6 +153,7 @@ struct CaptureSessionConfig , audioBitrate(audBitrate) , audioInputSourceId(std::move(audioSourceId)) , audioInputVolumePercentage(audioVolumePercentage) + , systemAudioVolumePercentage(systemVolumePercentage) { } @@ -161,7 +168,8 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + uint32_t systemVolumePercentage = 100) : targetType(CaptureTargetType::Monitor) , hMonitor(monitor) , hwnd(nullptr) @@ -176,6 +184,7 @@ struct CaptureSessionConfig , audioBitrate(audBitrate) , audioInputSourceId(std::move(audioSourceId)) , audioInputVolumePercentage(audioVolumePercentage) + , systemAudioVolumePercentage(systemVolumePercentage) { } @@ -197,6 +206,7 @@ struct CaptureSessionConfig , audioBitrate(128'000) , audioInputSourceId(L"") , audioInputVolumePercentage(100) + , systemAudioVolumePercentage(100) { } @@ -208,9 +218,10 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + uint32_t systemVolumePercentage = 100) { - CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); + CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, systemVolumePercentage); config.targetType = CaptureTargetType::Monitor; return config; } @@ -223,9 +234,10 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + uint32_t systemVolumePercentage = 100) { - CaptureSessionConfig config(nullptr, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); + CaptureSessionConfig config(nullptr, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, systemVolumePercentage); config.targetType = CaptureTargetType::Window; config.hwnd = window; return config; @@ -243,9 +255,10 @@ struct CaptureSessionConfig uint32_t vidBitrate = 5'000'000, uint32_t audBitrate = 128'000, std::wstring audioSourceId = L"", - uint32_t audioVolumePercentage = 100) + uint32_t audioVolumePercentage = 100, + uint32_t systemVolumePercentage = 100) { - CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage); + CaptureSessionConfig config(monitor, std::move(path), audio, fps, vidBitrate, audBitrate, std::move(audioSourceId), audioVolumePercentage, systemVolumePercentage); config.targetType = CaptureTargetType::Rectangle; config.sourceLeft = left; config.sourceTop = top; @@ -345,11 +358,18 @@ struct CaptureSessionConfig std::to_string(audioBitrate) + ")"); } - if (audioInputVolumePercentage > 100) - { - result.AddError("audioInputVolumePercentage must be between 0 and 100 (got " + - std::to_string(audioInputVolumePercentage) + ")"); - } + } + + if (audioInputVolumePercentage > 100) + { + result.AddError("audioInputVolumePercentage must be between 0 and 100 (got " + + std::to_string(audioInputVolumePercentage) + ")"); + } + + if (systemAudioVolumePercentage > 100) + { + result.AddError("systemAudioVolumePercentage must be between 0 and 100 (got " + + std::to_string(systemAudioVolumePercentage) + ")"); } return result; diff --git a/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.cpp b/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.cpp index 34990ea..bf940f4 100644 --- a/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.cpp +++ b/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.cpp @@ -434,6 +434,17 @@ void CompositeAudioCaptureSource::SetVolume(uint32_t volumePercentage) } } +void CompositeAudioCaptureSource::SetSystemVolume(uint32_t volumePercentage) +{ + uint32_t clampedVolume = std::min(volumePercentage, 100); + + std::lock_guard lock(m_sourceMutex); + if (m_loopbackSource) + { + m_loopbackSource->SetVolume(clampedVolume); + } +} + bool CompositeAudioCaptureSource::IsRunning() const { return m_isRunning.load(); diff --git a/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.h b/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.h index a072c0f..55da732 100644 --- a/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.h +++ b/src/CaptureKit.Windows.Native.Core/CompositeAudioCaptureSource.h @@ -27,6 +27,7 @@ class CompositeAudioCaptureSource final : public IAudioCaptureSource void SetEnabled(bool enabled) override; bool IsEnabled() const override; void SetVolume(uint32_t volumePercentage) override; + void SetSystemVolume(uint32_t volumePercentage) override; bool IsRunning() const override; bool SetInputDeviceId(const wchar_t* sourceId, HRESULT* outHr = nullptr) override; void SetClockWriter(IMediaClockWriter* clockWriter) override; diff --git a/src/CaptureKit.Windows.Native.Core/IAudioCaptureSource.h b/src/CaptureKit.Windows.Native.Core/IAudioCaptureSource.h index 17b6a96..202530d 100644 --- a/src/CaptureKit.Windows.Native.Core/IAudioCaptureSource.h +++ b/src/CaptureKit.Windows.Native.Core/IAudioCaptureSource.h @@ -84,6 +84,15 @@ class IAudioCaptureSource : public IMediaClockAdvancer /// virtual void SetVolume(uint32_t volumePercentage) = 0; + /// + /// Set the system/desktop volume independently from microphone/input volume. + /// Standalone sources treat this as their normal volume control. + /// + virtual void SetSystemVolume(uint32_t volumePercentage) + { + SetVolume(volumePercentage); + } + /// /// Check if the audio input source is currently running. /// diff --git a/src/CaptureKit.Windows.Native.Core/ICaptureSession.h b/src/CaptureKit.Windows.Native.Core/ICaptureSession.h index 0115a9c..029c987 100644 --- a/src/CaptureKit.Windows.Native.Core/ICaptureSession.h +++ b/src/CaptureKit.Windows.Native.Core/ICaptureSession.h @@ -50,6 +50,11 @@ class ICaptureSession /// True to enable audio, false to mute. virtual void ToggleAudioCapture(bool enabled) = 0; + /// + /// Set the system/desktop audio volume percentage for the active recording. + /// + virtual void SetSystemAudioVolume(uint32_t volumePercentage) = 0; + /// /// Change the audio input source for the active recording. /// diff --git a/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.cpp b/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.cpp index 08df0a3..d579a2b 100644 --- a/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.cpp +++ b/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.cpp @@ -190,6 +190,17 @@ bool ScreenRecorderImpl::SetAudioCaptureEnabled(bool enabled) return true; } +bool ScreenRecorderImpl::SetSystemAudioVolume(uint32_t volumePercentage) +{ + if (!HasActiveSession() || !m_captureSession->IsActive()) + { + return false; + } + + m_captureSession->SetSystemAudioVolume(volumePercentage); + return true; +} + bool ScreenRecorderImpl::SetAudioInputSource(const wchar_t* sourceId) { if (!HasActiveSession() || !m_captureSession->IsActive()) diff --git a/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.h b/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.h index e129d10..7d65d8c 100644 --- a/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.h +++ b/src/CaptureKit.Windows.Native.Core/ScreenRecorderImpl.h @@ -70,6 +70,11 @@ class ScreenRecorderImpl /// True to enable audio, false to mute. bool SetAudioCaptureEnabled(bool enabled); + /// + /// Set system/desktop audio volume during recording. + /// + bool SetSystemAudioVolume(uint32_t volumePercentage); + /// /// Change the audio input source during recording. /// diff --git a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp index 531cb08..7fb5ed1 100644 --- a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp +++ b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp @@ -420,6 +420,7 @@ bool WindowsGraphicsCaptureSession::StartAudioCapture(HRESULT* outHr) // Apply audio enabled setting m_audioCaptureSource->SetEnabled(m_config.audioEnabled); + m_audioCaptureSource->SetSystemVolume(m_config.systemAudioVolumePercentage); m_audioCaptureSource->SetVolume(m_config.audioInputVolumePercentage); // Start audio @@ -557,6 +558,14 @@ void WindowsGraphicsCaptureSession::ToggleAudioCapture(bool enabled) } } +void WindowsGraphicsCaptureSession::SetSystemAudioVolume(uint32_t volumePercentage) +{ + if (m_audioCaptureSource) + { + m_audioCaptureSource->SetSystemVolume(volumePercentage); + } +} + bool WindowsGraphicsCaptureSession::SetAudioInputSource(const wchar_t* sourceId) { if (!m_audioCaptureSource) diff --git a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.h b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.h index e1a37fa..e58d63c 100644 --- a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.h +++ b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.h @@ -49,6 +49,7 @@ class WindowsGraphicsCaptureSession : public ICaptureSession void Pause() override; void Resume() override; void ToggleAudioCapture(bool enabled) override; + void SetSystemAudioVolume(uint32_t volumePercentage) override; bool SetAudioInputSource(const wchar_t* sourceId) override; void SetAudioInputVolume(uint32_t volumePercentage) override; bool IsActive() const override { return m_stateMachine.IsActive(); } diff --git a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp index f3cf1fe..9bb8363 100644 --- a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp +++ b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.cpp @@ -63,7 +63,8 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage); + options->audioInputVolumePercentage, + options->systemAudioVolumePercentage); break; case CaptureRecordingTargetKind::Window: @@ -79,7 +80,8 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage); + options->audioInputVolumePercentage, + options->systemAudioVolumePercentage); break; case CaptureRecordingTargetKind::Rectangle: @@ -99,7 +101,8 @@ extern "C" options->videoBitrate, options->audioBitrate, options->audioInputSourceId ? options->audioInputSourceId : L"", - options->audioInputVolumePercentage); + options->audioInputVolumePercentage, + options->systemAudioVolumePercentage); break; default: @@ -154,6 +157,14 @@ extern "C" }); } + __declspec(dllexport) CaptureRecorderResult SetScreenRecordingSystemAudioVolume(uint32_t volumePercentage) + { + return GuardRecorderCall(L"SetScreenRecordingSystemAudioVolume", CaptureRecorderStatus::InvalidState, [volumePercentage] { + std::lock_guard lock(g_recorderMutex); + return GetRecorder().SetSystemAudioVolume(volumePercentage) ? Success() : NoActiveSession(); + }); + } + __declspec(dllexport) CaptureRecorderResult SetScreenRecordingAudioInputSource(const wchar_t* sourceId) { return GuardRecorderCall(L"SetScreenRecordingAudioInputSource", CaptureRecorderStatus::InvalidState, [sourceId] { diff --git a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h index 6507df7..de0feef 100644 --- a/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h +++ b/src/CaptureKit.Windows.Native.Recording/ScreenRecorder.h @@ -41,6 +41,7 @@ struct CaptureRecordingOptions uint32_t audioBitrate; const wchar_t* audioInputSourceId; uint32_t audioInputVolumePercentage; + uint32_t systemAudioVolumePercentage; }; extern "C" @@ -50,6 +51,7 @@ extern "C" __declspec(dllexport) CaptureRecorderResult ResumeScreenRecording(); __declspec(dllexport) CaptureRecorderResult StopScreenRecording(); __declspec(dllexport) CaptureRecorderResult SetScreenRecordingAudioEnabled(uint32_t enabled); + __declspec(dllexport) CaptureRecorderResult SetScreenRecordingSystemAudioVolume(uint32_t volumePercentage); __declspec(dllexport) CaptureRecorderResult SetScreenRecordingAudioInputSource(const wchar_t* sourceId); __declspec(dllexport) CaptureRecorderResult SetScreenRecordingAudioInputVolume(uint32_t volumePercentage); diff --git a/src/CaptureKit.Windows/NativeInterop.cs b/src/CaptureKit.Windows/NativeInterop.cs index 90dfc3f..0faf061 100644 --- a/src/CaptureKit.Windows/NativeInterop.cs +++ b/src/CaptureKit.Windows/NativeInterop.cs @@ -23,6 +23,9 @@ internal static partial class NativeInterop [DllImport(RecordingNativeLibraryName)] internal static extern CaptureRecorderResult SetScreenRecordingAudioEnabled(uint enabled); + [DllImport(RecordingNativeLibraryName)] + internal static extern CaptureRecorderResult SetScreenRecordingSystemAudioVolume(uint volumePercentage); + [DllImport(RecordingNativeLibraryName, CharSet = CharSet.Unicode)] internal static extern CaptureRecorderResult SetScreenRecordingAudioInputSource(string? sourceId); @@ -132,6 +135,7 @@ public NativeVideoCaptureOptions(VideoCaptureOptions options) AudioBitrate = options.AudioBitrate; AudioInputSourceId = options.AudioInputSourceId; AudioInputVolumePercentage = (uint)Math.Clamp(options.AudioInputVolumePercentage, 0, 100); + SystemAudioVolumePercentage = (uint)Math.Clamp(options.SystemAudioVolumePercentage, 0, 100); } public readonly CaptureTargetKind TargetKind; @@ -154,6 +158,7 @@ public NativeVideoCaptureOptions(VideoCaptureOptions options) public readonly string? AudioInputSourceId; public readonly uint AudioInputVolumePercentage; + public readonly uint SystemAudioVolumePercentage; } [StructLayout(LayoutKind.Sequential)] diff --git a/src/CaptureKit.Windows/VideoCaptureService.cs b/src/CaptureKit.Windows/VideoCaptureService.cs index e9a2bea..c570559 100644 --- a/src/CaptureKit.Windows/VideoCaptureService.cs +++ b/src/CaptureKit.Windows/VideoCaptureService.cs @@ -178,6 +178,13 @@ public void SetAudioCaptureEnabled(bool enabled) _nativeApi.SetScreenRecordingAudioEnabled(enabled ? 1u : 0u).EnsureSuccess(); } + public void SetSystemAudioVolume(int volumePercentage) + { + ThrowIfDisposed(); + ThrowIfInsideNativeCallback(); + _nativeApi.SetScreenRecordingSystemAudioVolume((uint)Math.Clamp(volumePercentage, 0, 100)).EnsureSuccess(); + } + public void SetAudioInputSource(string? sourceId) { ThrowIfDisposed(); @@ -411,6 +418,7 @@ internal interface IVideoCaptureNativeApi CaptureRecorderResult ResumeScreenRecording(); CaptureRecorderResult StopScreenRecording(); CaptureRecorderResult SetScreenRecordingAudioEnabled(uint enabled); + CaptureRecorderResult SetScreenRecordingSystemAudioVolume(uint volumePercentage); CaptureRecorderResult SetScreenRecordingAudioInputSource(string? sourceId); CaptureRecorderResult SetScreenRecordingAudioInputVolume(uint volumePercentage); CaptureRecorderResult RegisterVideoFrameCallback(VideoFrameCallback? callback); @@ -429,6 +437,9 @@ public CaptureRecorderResult StartScreenRecording(in NativeVideoCaptureOptions o public CaptureRecorderResult SetScreenRecordingAudioEnabled(uint enabled) => NativeInterop.SetScreenRecordingAudioEnabled(enabled); + public CaptureRecorderResult SetScreenRecordingSystemAudioVolume(uint volumePercentage) + => NativeInterop.SetScreenRecordingSystemAudioVolume(volumePercentage); + public CaptureRecorderResult SetScreenRecordingAudioInputSource(string? sourceId) => NativeInterop.SetScreenRecordingAudioInputSource(sourceId); diff --git a/tests/CaptureKit.Windows.Native.Tests/CaptureRecorderBoundaryTests.cpp b/tests/CaptureKit.Windows.Native.Tests/CaptureRecorderBoundaryTests.cpp index 1b2d505..18dc8c4 100644 --- a/tests/CaptureKit.Windows.Native.Tests/CaptureRecorderBoundaryTests.cpp +++ b/tests/CaptureKit.Windows.Native.Tests/CaptureRecorderBoundaryTests.cpp @@ -73,6 +73,7 @@ namespace CaptureInteropTests void Pause() override {} void Resume() override {} void ToggleAudioCapture(bool) override {} + void SetSystemAudioVolume(uint32_t) override {} bool SetAudioInputSource(const wchar_t*) override { return true; } void SetAudioInputVolume(uint32_t) override {} bool IsActive() const override { return m_active; } diff --git a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp index 1c438f6..c42a779 100644 --- a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp +++ b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp @@ -27,7 +27,8 @@ namespace CaptureInteropTests int stopCalls = 0; int setClockWriterCalls = 0; int setCallbackCalls = 0; - uint32_t volumePercentage = 100; + uint32_t inputVolumePercentage = 100; + uint32_t systemVolumePercentage = 100; bool enabled = true; bool running = false; }; @@ -90,7 +91,12 @@ namespace CaptureInteropTests void SetVolume(uint32_t volumePercentage) override { - m_state->volumePercentage = volumePercentage; + m_state->inputVolumePercentage = volumePercentage; + } + + void SetSystemVolume(uint32_t volumePercentage) override + { + m_state->systemVolumePercentage = volumePercentage; } bool IsRunning() const override @@ -294,6 +300,7 @@ namespace CaptureInteropTests auto sink = std::make_shared(); auto config = CreateConfig(false); config.audioInputVolumePercentage = 37; + config.systemAudioVolumePercentage = 42; auto session = CreateSession(config, audio, video, sink); HRESULT hr = S_OK; @@ -304,17 +311,20 @@ namespace CaptureInteropTests Assert::AreEqual(1, audio->initializeCalls); Assert::AreEqual(1, audio->startCalls); Assert::IsFalse(audio->enabled, L"Desktop audio must honor its initial muted state"); - Assert::AreEqual(37u, audio->volumePercentage); + Assert::AreEqual(37u, audio->inputVolumePercentage); + Assert::AreEqual(42u, audio->systemVolumePercentage); Assert::AreEqual(1, sink->initializeAudioCalls); Assert::AreEqual(1, sink->initializeCalls); Assert::AreEqual(1, sink->beginWritingCalls); Assert::AreEqual(1, sink->writeFrameCalls); session->ToggleAudioCapture(true); - session->SetAudioInputVolume(64); + session->SetSystemAudioVolume(64); + session->SetAudioInputVolume(58); Assert::IsTrue(audio->enabled, L"Desktop audio must support false-to-true transitions"); - Assert::AreEqual(64u, audio->volumePercentage, L"Desktop audio volume must remain mutable"); + Assert::AreEqual(64u, audio->systemVolumePercentage, L"Desktop audio volume must remain mutable"); + Assert::AreEqual(58u, audio->inputVolumePercentage, L"Microphone volume must remain independent"); session->Stop(); } diff --git a/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs b/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs index d1e234e..b5dad34 100644 --- a/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs +++ b/tests/CaptureKit.Windows.Tests/CaptureRecorderBoundaryTests.cs @@ -31,6 +31,35 @@ public void EnsureSuccess_Success_DoesNotThrow() act.Should().NotThrow(); } + [TestMethod] + public void NativeVideoOptions_KeepSystemAndInputVolumesIndependent() + { + var options = new VideoCaptureOptions( + CaptureTarget.Monitor((nint)1), + "capture.mp4", + AudioInputVolumePercentage: 37, + SystemAudioVolumePercentage: 64); + + var nativeOptions = new NativeVideoCaptureOptions(options); + + nativeOptions.AudioInputVolumePercentage.Should().Be(37u); + nativeOptions.SystemAudioVolumePercentage.Should().Be(64u); + } + + [TestMethod] + public void SetSystemAudioVolume_ClampsAndUsesDedicatedNativeControl() + { + var nativeApi = new TrackingNativeApi(); + using var session = new VideoCaptureSession( + new VideoCaptureOptions(CaptureTarget.Monitor((nint)1), "capture.mp4"), + new StubSupportService(VideoCaptureSupportResult.Supported()), + nativeApi); + + session.SetSystemAudioVolume(164); + + nativeApi.LastSystemAudioVolumePercentage.Should().Be(100u); + } + [TestMethod] public void AudioSample_DoesNotReportRecordingStarted() { @@ -208,6 +237,7 @@ private sealed class TrackingNativeApi : IVideoCaptureNativeApi public bool FailCallbackUnregistration { get; set; } public bool HasVideoCallback => _videoCallback is not null; public bool HasAudioCallback => _audioCallback is not null; + public uint? LastSystemAudioVolumePercentage { get; private set; } public CaptureRecorderResult StartScreenRecording(in NativeVideoCaptureOptions options) => Called(); public CaptureRecorderResult PauseScreenRecording() => Called(); @@ -218,6 +248,11 @@ public CaptureRecorderResult StopScreenRecording() return Called(); } public CaptureRecorderResult SetScreenRecordingAudioEnabled(uint enabled) => Called(); + public CaptureRecorderResult SetScreenRecordingSystemAudioVolume(uint volumePercentage) + { + LastSystemAudioVolumePercentage = volumePercentage; + return Called(); + } public CaptureRecorderResult SetScreenRecordingAudioInputSource(string? sourceId) => Called(); public CaptureRecorderResult SetScreenRecordingAudioInputVolume(uint volumePercentage) => Called(); public CaptureRecorderResult RegisterVideoFrameCallback(VideoFrameCallback? callback) From 5327e9014fbec2f1487e2ac1fce490dfa3c350ce Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 4 Aug 2026 18:04:12 -0700 Subject: [PATCH 4/5] Validate audio bitrate for muted video sessions --- .../CaptureSessionConfig.h | 14 +++++--------- .../ConfigValidationTests.cpp | 9 ++++----- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h index 042b2d6..fbc5881 100644 --- a/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h +++ b/src/CaptureKit.Windows.Native.Core/CaptureSessionConfig.h @@ -348,16 +348,12 @@ struct CaptureSessionConfig constexpr uint32_t MIN_AUDIO_BITRATE = 32'000; // 32 kbps constexpr uint32_t MAX_AUDIO_BITRATE = 320'000; // 320 kbps - if (audioEnabled) + if (audioBitrate < MIN_AUDIO_BITRATE || audioBitrate > MAX_AUDIO_BITRATE) { - if (audioBitrate < MIN_AUDIO_BITRATE || audioBitrate > MAX_AUDIO_BITRATE) - { - result.AddError("audioBitrate must be between " + - std::to_string(MIN_AUDIO_BITRATE) + " and " + - std::to_string(MAX_AUDIO_BITRATE) + " (got " + - std::to_string(audioBitrate) + ")"); - } - + result.AddError("audioBitrate must be between " + + std::to_string(MIN_AUDIO_BITRATE) + " and " + + std::to_string(MAX_AUDIO_BITRATE) + " (got " + + std::to_string(audioBitrate) + ")"); } if (audioInputVolumePercentage > 100) diff --git a/tests/CaptureKit.Windows.Native.Tests/ConfigValidationTests.cpp b/tests/CaptureKit.Windows.Native.Tests/ConfigValidationTests.cpp index 357023a..224dbe4 100644 --- a/tests/CaptureKit.Windows.Native.Tests/ConfigValidationTests.cpp +++ b/tests/CaptureKit.Windows.Native.Tests/ConfigValidationTests.cpp @@ -290,9 +290,9 @@ namespace CaptureInteropTests Assert::IsTrue(foundError, L"Should have error about audioBitrate being too high"); } - TEST_METHOD(Validate_AudioBitrateInvalid_WithAudioDisabled_NoError) + TEST_METHOD(Validate_AudioBitrateInvalid_WhenInitiallyMuted_ReturnsError) { - // Arrange - audio disabled with invalid bitrate + // Arrange - initially muted audio still provisions an AAC stream HMONITOR hMonitor = reinterpret_cast(0x12345678); CaptureSessionConfig config(hMonitor, L"C:\\test\\output.mp4", false, 30, 5'000'000, 16'000); // 16 kbps @@ -300,9 +300,8 @@ namespace CaptureInteropTests ConfigValidationResult result = config.Validate(); // Assert - Assert::IsTrue(result.isValid, L"Configuration should be valid when audio is disabled"); + Assert::IsFalse(result.isValid, L"The always-prepared audio stream requires a valid bitrate"); - // Should not have audio bitrate errors when audio is disabled bool foundAudioBitrateError = false; for (const auto& error : result.errors) { @@ -312,7 +311,7 @@ namespace CaptureInteropTests break; } } - Assert::IsFalse(foundAudioBitrateError, L"Should not have audioBitrate error when audio is disabled"); + Assert::IsTrue(foundAudioBitrateError, L"Should have an audioBitrate error while initially muted"); } TEST_METHOD(Validate_MultipleErrors_ReturnsAllErrors) From 6f88a68a329b6161674fc7ad65402461b5da3da6 Mon Sep 17 00:00:00 2001 From: Shane Weaver Date: Tue, 4 Aug 2026 19:45:36 -0700 Subject: [PATCH 5/5] Version mutable audio API as CaptureKit 1.0.5 --- Directory.Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Version.props b/Directory.Version.props index c2ba0cb..3823f1c 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,6 +1,6 @@ - 1.0.4 + 1.0.5 $(CaptureKitVersion)