diff --git a/Directory.Version.props b/Directory.Version.props index 84bd89c..3823f1c 100644 --- a/Directory.Version.props +++ b/Directory.Version.props @@ -1,6 +1,6 @@ - 1.0.3 + 1.0.5 $(CaptureKitVersion) 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..fbc5881 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; @@ -335,21 +348,24 @@ 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) - { - 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 8491adf..7fb5ed1 100644 --- a/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp +++ b/src/CaptureKit.Windows.Native.Core/WindowsGraphicsCaptureSession.cpp @@ -98,7 +98,6 @@ WindowsGraphicsCaptureSession::~WindowsGraphicsCaptureSession() bool WindowsGraphicsCaptureSession::Initialize(HRESULT* outHr) { HRESULT hr = S_OK; - const bool audioRequested = m_config.audioEnabled || !m_config.audioInputSourceId.empty(); // Validate state - must be in Created state to initialize if (m_stateMachine.GetState() != CaptureSessionState::Created) @@ -114,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"); @@ -135,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 @@ -426,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 @@ -563,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/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) diff --git a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp index 7a5fef0..c42a779 100644 --- a/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp +++ b/tests/CaptureKit.Windows.Native.Tests/WindowsGraphicsCaptureSessionAudioTests.cpp @@ -27,6 +27,8 @@ namespace CaptureInteropTests int stopCalls = 0; int setClockWriterCalls = 0; int setCallbackCalls = 0; + uint32_t inputVolumePercentage = 100; + uint32_t systemVolumePercentage = 100; bool enabled = true; bool running = false; }; @@ -87,7 +89,15 @@ namespace CaptureInteropTests return m_state->enabled; } - void SetVolume(uint32_t) override {} + void SetVolume(uint32_t volumePercentage) override + { + m_state->inputVolumePercentage = volumePercentage; + } + + void SetSystemVolume(uint32_t volumePercentage) override + { + m_state->systemVolumePercentage = volumePercentage; + } bool IsRunning() const override { @@ -283,25 +293,38 @@ 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; + config.systemAudioVolumePercentage = 42; + 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->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->SetSystemAudioVolume(64); + session->SetAudioInputVolume(58); + + Assert::IsTrue(audio->enabled, L"Desktop audio must support false-to-true transitions"); + 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)