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)