From 2489bbb6a99ad961f52f810b401a7c19c62dc893 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Tue, 16 Jun 2026 12:14:08 -0700 Subject: [PATCH 1/3] Implement MediaTrackCapabilities, MediaTrackSettings, and constraint marshalling - Add VideoFacingModeValue and VideoResizeModeValue wrapper types (VideoModeValues.cs) for extensible string-domain constraints; replace raw VideoFacingModes/VideoResizeModes enums in MediaTrackCapabilities, MediaTrackSettings, and MediaTrackConstraintSet - Implement full constraint marshalling in MarshalMediaConstraints.h: native structs (MediaTrackConstraintSet, MediaTrackConstraints) and marshal_as<> specializations covering all constrainable properties - Expand MarshalMedia.h with VideoFacingModeValue and VideoResizeModeValue marshal_as<> specializations; refactor existing media marshalling - Remove reflection-based Create() calls in MediaStreamTrack.cpp; call MediaTrackCapabilities::Create and MediaTrackSettings::Create directly via InternalsVisibleTo - Add/expand unit tests for constraint types and unsigned numeric constraint marshalling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Marshaling/MarshalMediaTests.cpp | 103 +++++++ WebRtcInterop/Marshaling/MarshalEnums.h | 7 +- WebRtcInterop/Marshaling/MarshalMedia.h | 247 ++++++++++----- .../Marshaling/MarshalMediaConstraints.h | 280 +++++++++++++++++- WebRtcInterop/Media/Marshaling/MarshalMedia.h | 62 +--- WebRtcInterop/Media/MediaDevices.cpp | 2 +- WebRtcInterop/Media/MediaStreamTrack.cpp | 200 ++++++++++++- .../MediaTrackConstraintTests.cs | 62 +++- .../MediaTrackUnsignedNumericTypesTests.cs | 8 +- WebRtcNet.Api/Media/InputDeviceInfo.cs | 8 +- WebRtcNet.Api/Media/MediaTrackCapabilities.cs | 14 +- .../Media/MediaTrackConstraintSet.cs | 8 +- WebRtcNet.Api/Media/MediaTrackSettings.cs | 14 +- WebRtcNet.Api/Media/VideoModeValues.cs | 165 +++++++++++ 14 files changed, 1013 insertions(+), 167 deletions(-) create mode 100644 WebRtcNet.Api/Media/VideoModeValues.cs diff --git a/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp b/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp index 8fce8bf..f5adbcb 100644 --- a/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp +++ b/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp @@ -5,6 +5,7 @@ #include "gtest/gtest.h" #include "Media/Marshaling/MarshalMedia.h" +#include "Marshaling/MarshalMediaConstraints.h" using namespace msclr::interop; using namespace System; @@ -100,3 +101,105 @@ TEST(marshal_media_track_kind_tests, marshal_native_track_kind_invalid_throws) { } } + +TEST(marshal_media_mode_value_tests, marshal_unknown_native_facing_mode_preserves_raw_value) +{ + auto result = marshal_as(std::string("vendor-facing-mode")); + ASSERT_EQ(result.IsKnown, false); + ASSERT_EQ(marshal_as(result.RawValue), "vendor-facing-mode"); +} + +TEST(marshal_media_mode_value_tests, marshal_unknown_native_resize_mode_preserves_raw_value) +{ + auto result = marshal_as(std::string("vendor-resize-mode")); + ASSERT_EQ(result.IsKnown, false); + ASSERT_EQ(marshal_as(result.RawValue), "vendor-resize-mode"); +} + +TEST(marshal_media_mode_value_tests, marshal_unknown_managed_facing_mode_to_native_throws) +{ + try + { + VideoFacingModeValue value(gcnew String("vendor-facing-mode")); + const auto _ = marshal_video_facing_mode_value_to_native(value); + FAIL(); + } + catch (InvalidCastException^) + { + } +} + +TEST(marshal_media_mode_value_tests, marshal_unknown_managed_resize_mode_to_native_throws) +{ + try + { + VideoResizeModeValue value(gcnew String("vendor-resize-mode")); + const auto _ = marshal_video_resize_mode_value_to_native(value); + FAIL(); + } + catch (InvalidCastException^) + { + } +} + +TEST(marshal_media_constraints_tests, marshal_media_stream_constraints_includes_advanced_and_echo_cancellation) +{ + auto videoTrackConstraints = gcnew MediaTrackConstraints(); + videoTrackConstraints->Width = gcnew MediaTrackConstraints::PositiveUIntRangeConstraint(); + videoTrackConstraints->Width->Min = 640; + videoTrackConstraints->Width->Max = 1280; + videoTrackConstraints->Width->Ideal = 800; + + videoTrackConstraints->EchoCancellation = gcnew EchoCancellationConstraint(); + videoTrackConstraints->EchoCancellation->Ideal = EchoCancellationValue(EchoCancellationMode::RemoteOnly); + + videoTrackConstraints->Advanced = gcnew System::Collections::Generic::List(); + auto advancedSet = gcnew MediaTrackConstraintSet(); + advancedSet->BackgroundBlur = gcnew MediaTrackConstraints::Constraint(true); + videoTrackConstraints->Advanced->Add(advancedSet); + + auto streamConstraints = gcnew MediaStreamConstraints(true, videoTrackConstraints); + auto marshaled = marshal_as(streamConstraints); + + ASSERT_EQ(marshaled.audio_requested, true); + ASSERT_EQ(marshaled.video_requested, true); + ASSERT_TRUE(marshaled.video_constraints.has_value()); + ASSERT_EQ(marshaled.video_constraints->advanced.size(), 1u); + ASSERT_TRUE(marshaled.video_constraints->basic.width.has_value()); + ASSERT_TRUE(marshaled.video_constraints->basic.echo_cancellation.has_value()); + ASSERT_TRUE(marshaled.video_constraints->basic.echo_cancellation->ideal.has_value()); + ASSERT_TRUE(marshaled.video_constraints->basic.echo_cancellation->ideal->mode_value.has_value()); + ASSERT_EQ(marshaled.video_constraints->basic.echo_cancellation->ideal->mode_value.value(), "remote-only"); +} + +TEST(media_stream_track_constraint_plumbing_tests, apply_constraints_with_unknown_facing_mode_throws) +{ + auto constraints = gcnew MediaTrackConstraints(); + constraints->FacingMode = gcnew MediaTrackConstraints::Constraint(VideoFacingModes::User); + constraints->FacingMode->Exact = VideoFacingModeValue(gcnew String("vendor-facing-mode")); + + try + { + const auto _ = marshal_as(constraints); + FAIL(); + } + catch (InvalidCastException^) + { + } +} + +TEST(media_stream_track_constraint_plumbing_tests, apply_constraints_with_unknown_resize_mode_throws) +{ + auto constraints = gcnew MediaTrackConstraints(); + constraints->ResizeMode = gcnew MediaTrackConstraints::Constraint(VideoResizeModes::None); + constraints->ResizeMode->Exact = VideoResizeModeValue(gcnew String("vendor-resize-mode")); + + try + { + const auto _ = marshal_as(constraints); + FAIL(); + } + catch (InvalidCastException^) + { + } +} diff --git a/WebRtcInterop/Marshaling/MarshalEnums.h b/WebRtcInterop/Marshaling/MarshalEnums.h index 2ad63a0..0aa0921 100644 --- a/WebRtcInterop/Marshaling/MarshalEnums.h +++ b/WebRtcInterop/Marshaling/MarshalEnums.h @@ -8,8 +8,7 @@ managed_type marshal_mapped_native_type(const std::mapFullName, static_cast(from), managed_type::typeid->FullName)); + throw gcnew System::InvalidCastException("Unable to convert native value to managed type."); } return entry->second; @@ -23,7 +22,5 @@ native_type marshal_mapped_managed_type(const std::mapFullName, System::Enum::GetName(managed_type::typeid, from), - native_type::typeid->FullName)); + throw gcnew System::InvalidCastException("Unable to convert managed value to native type."); } diff --git a/WebRtcInterop/Marshaling/MarshalMedia.h b/WebRtcInterop/Marshaling/MarshalMedia.h index c7e8f4c..3598663 100644 --- a/WebRtcInterop/Marshaling/MarshalMedia.h +++ b/WebRtcInterop/Marshaling/MarshalMedia.h @@ -1,117 +1,220 @@ #pragma once +#include +#include +#include +#include + #include #include +#include +#include + +#include "MarshalEnums.h" + namespace msclr { namespace interop { - // Marshal ValueRange from native to managed - // ValueRange: handles uint?, double? + using namespace WebRtcNet::Logging; + + inline void WriteMediaInteropWarning(System::String^ message) + { + WebRtcLogWriterBridge::WriteInteropLog( + 3, + 9300, + "Interop.Media.Marshaling", + System::Threading::Thread::CurrentThread->ManagedThreadId, + message != nullptr ? message : System::String::Empty); + } + + static const std::map + media_stream_track_state_map{ + {webrtc::MediaStreamTrackInterface::TrackState::kLive, WebRtcNet::Media::MediaStreamTrackState::Live}, + {webrtc::MediaStreamTrackInterface::TrackState::kEnded, WebRtcNet::Media::MediaStreamTrackState::Ended}, + }; + + static const std::map video_facing_mode_map{ + {"user", WebRtcNet::Media::VideoFacingModes::User}, + {"environment", WebRtcNet::Media::VideoFacingModes::Environment}, + {"left", WebRtcNet::Media::VideoFacingModes::Left}, + {"right", WebRtcNet::Media::VideoFacingModes::Right}, + }; + + static const std::map video_resize_mode_map{ + {"none", WebRtcNet::Media::VideoResizeModes::None}, + {"crop-and-scale", WebRtcNet::Media::VideoResizeModes::CropAndScale}, + }; + + static const std::map echo_cancellation_mode_map{ + {"all", WebRtcNet::Media::EchoCancellationMode::All}, + {"remote-only", WebRtcNet::Media::EchoCancellationMode::RemoteOnly}, + }; + template<> - inline WebRtcNet::ValueRange^ marshal_as(const std::pair& from) + inline WebRtcNet::Media::MediaStreamTrackState marshal_as(const webrtc::MediaStreamTrackInterface::TrackState& from) { - auto range = gcnew WebRtcNet::ValueRange(); - range->Min = from.first; - range->Max = from.second; - return range; + return marshal_mapped_native_type(media_stream_track_state_map, from); } template<> - inline WebRtcNet::ValueRange^ marshal_as(const std::pair& from) + inline webrtc::MediaStreamTrackInterface::TrackState + marshal_as( + const WebRtcNet::Media::MediaStreamTrackState& from) { - auto range = gcnew WebRtcNet::ValueRange(); - range->Min = from.first; - range->Max = from.second; - return range; + return marshal_mapped_managed_type(media_stream_track_state_map, from); } - // Marshal MediaDeviceKind enum template<> - inline WebRtcNet::Media::MediaDeviceKind marshal_as(webrtc::MediaDeviceInfo::Kind from) + inline WebRtcNet::Media::MediaStreamTrackKind marshal_as(const std::string& from) { - switch (from) - { - case webrtc::MediaDeviceInfo::Kind::kAudioInput: - return WebRtcNet::Media::MediaDeviceKind::AudioInput; - case webrtc::MediaDeviceInfo::Kind::kAudioOutput: - return WebRtcNet::Media::MediaDeviceKind::AudioOutput; - case webrtc::MediaDeviceInfo::Kind::kVideoInput: - return WebRtcNet::Media::MediaDeviceKind::VideoInput; - default: - throw gcnew System::ArgumentException("Unknown device kind"); - } + if (from == webrtc::MediaStreamTrackInterface::kAudioKind) return WebRtcNet::Media::MediaStreamTrackKind::Audio; + if (from == webrtc::MediaStreamTrackInterface::kVideoKind) return WebRtcNet::Media::MediaStreamTrackKind::Video; + + throw gcnew System::InvalidCastException( + System::String::Format("Unable to convert track kind '{0}' to {1}.", + marshal_as(from), + WebRtcNet::Media::MediaStreamTrackKind::typeid->FullName)); } template<> - inline webrtc::MediaDeviceInfo::Kind marshal_as(WebRtcNet::Media::MediaDeviceKind from) + inline std::string marshal_as( + const WebRtcNet::Media::MediaStreamTrackKind& from) { switch (from) { - case WebRtcNet::Media::MediaDeviceKind::AudioInput: - return webrtc::MediaDeviceInfo::Kind::kAudioInput; - case WebRtcNet::Media::MediaDeviceKind::AudioOutput: - return webrtc::MediaDeviceInfo::Kind::kAudioOutput; - case WebRtcNet::Media::MediaDeviceKind::VideoInput: - return webrtc::MediaDeviceInfo::Kind::kVideoInput; - default: - throw gcnew System::ArgumentException("Unknown device kind"); + case WebRtcNet::Media::MediaStreamTrackKind::Audio: + return webrtc::MediaStreamTrackInterface::kAudioKind; + case WebRtcNet::Media::MediaStreamTrackKind::Video: + return webrtc::MediaStreamTrackInterface::kVideoKind; } + + throw gcnew System::InvalidCastException( + System::String::Format("Unable to convert {0} value '{1}' to native track kind.", + WebRtcNet::Media::MediaStreamTrackKind::typeid->FullName, + System::Enum::GetName(WebRtcNet::Media::MediaStreamTrackKind::typeid, from))); } - // Marshal MediaDeviceInfo: native to managed (one-way) template<> - inline WebRtcNet::Media::MediaDeviceInfo^ marshal_as(const webrtc::MediaDeviceInfo& from) + inline WebRtcNet::Media::VideoFacingModes marshal_as(const std::string& from) { - auto deviceId = marshal_as(from.device_id()); - auto kind = marshal_as(from.kind()); - auto label = marshal_as(from.label()); - auto groupId = marshal_as(from.group_id()); + return marshal_mapped_native_type(video_facing_mode_map, from); + } - // Use reflection to call internal constructor - auto ctor = WebRtcNet::Media::MediaDeviceInfo::typeid->GetConstructor( - System::Reflection::BindingFlags::NonPublic | System::Reflection::BindingFlags::Instance, - nullptr, - gcnew array { - System::String::typeid, - WebRtcNet::Media::MediaDeviceKind::typeid, - System::String::typeid, - System::String::typeid - }, - nullptr); + template<> + inline std::string marshal_as( + const WebRtcNet::Media::VideoFacingModes& from) + { + return marshal_mapped_managed_type(video_facing_mode_map, from); + } - if (ctor == nullptr) - throw gcnew System::InvalidOperationException("Cannot find internal MediaDeviceInfo constructor"); + template<> + inline WebRtcNet::Media::VideoResizeModes marshal_as(const std::string& from) + { + return marshal_mapped_native_type(video_resize_mode_map, from); + } - return safe_cast( - ctor->Invoke(gcnew array { deviceId, kind, label, groupId })); + template<> + inline std::string marshal_as( + const WebRtcNet::Media::VideoResizeModes& from) + { + return marshal_mapped_managed_type(video_resize_mode_map, from); } - // Marshal MediaStreamTrackState enum template<> - inline WebRtcNet::Media::MediaStreamTrackState marshal_as(webrtc::MediaStreamTrackInterface::TrackState from) + inline WebRtcNet::Media::VideoFacingModeValue marshal_as(const std::string& from) { - switch (from) + auto entry = video_facing_mode_map.find(from); + if (entry != video_facing_mode_map.end()) + return WebRtcNet::Media::VideoFacingModeValue(entry->second); + + WriteMediaInteropWarning(System::String::Format( + "Unknown native facing mode '{0}' preserved as raw value.", + marshal_as(from))); + + return WebRtcNet::Media::VideoFacingModeValue(marshal_as(from)); + } + + inline std::string marshal_video_facing_mode_value_to_native(WebRtcNet::Media::VideoFacingModeValue from) + { + if (!from.IsKnown) { - case webrtc::MediaStreamTrackInterface::TrackState::kLive: - return WebRtcNet::Media::MediaStreamTrackState::Live; - case webrtc::MediaStreamTrackInterface::TrackState::kEnded: - return WebRtcNet::Media::MediaStreamTrackState::Ended; - default: - throw gcnew System::ArgumentException("Unknown track state"); + throw gcnew System::InvalidCastException( + System::String::Format("Unable to convert unknown facing mode value '{0}' to native facing mode.", + from.RawValue)); } + + auto knownValue = from.KnownValue; + return marshal_as(knownValue.Value); } template<> - inline webrtc::MediaStreamTrackInterface::TrackState marshal_as(WebRtcNet::Media::MediaStreamTrackState from) + inline WebRtcNet::Media::VideoResizeModeValue marshal_as(const std::string& from) { - switch (from) + auto entry = video_resize_mode_map.find(from); + if (entry != video_resize_mode_map.end()) + return WebRtcNet::Media::VideoResizeModeValue(entry->second); + + WriteMediaInteropWarning(System::String::Format( + "Unknown native resize mode '{0}' preserved as raw value.", + marshal_as(from))); + + return WebRtcNet::Media::VideoResizeModeValue(marshal_as(from)); + } + + inline std::string marshal_video_resize_mode_value_to_native(WebRtcNet::Media::VideoResizeModeValue from) + { + if (!from.IsKnown) { - case WebRtcNet::Media::MediaStreamTrackState::Live: - return webrtc::MediaStreamTrackInterface::TrackState::kLive; - case WebRtcNet::Media::MediaStreamTrackState::Ended: - return webrtc::MediaStreamTrackInterface::TrackState::kEnded; - default: - throw gcnew System::ArgumentException("Unknown track state"); + throw gcnew System::InvalidCastException( + System::String::Format("Unable to convert unknown resize mode value '{0}' to native resize mode.", + from.RawValue)); } + + auto knownValue = from.KnownValue; + return marshal_as(knownValue.Value); + } + + template<> + inline WebRtcNet::Media::EchoCancellationMode marshal_as(const std::string& from) + { + return marshal_mapped_native_type(echo_cancellation_mode_map, from); + } + + template<> + inline std::string marshal_as( + const WebRtcNet::Media::EchoCancellationMode& from) + { + return marshal_mapped_managed_type(echo_cancellation_mode_map, from); + } + + template<> + inline WebRtcNet::Media::EchoCancellationValue marshal_as(const bool& from) + { + return WebRtcNet::Media::EchoCancellationValue(from); + } + + template<> + inline WebRtcNet::Media::EchoCancellationValue marshal_as(const std::string& from) + { + return WebRtcNet::Media::EchoCancellationValue(marshal_as(from)); + } + + template<> + inline WebRtcNet::ValueRange^ marshal_as(const std::pair& from) + { + auto range = gcnew WebRtcNet::ValueRange(); + range->Min = from.first; + range->Max = from.second; + return range; } + + template<> + inline WebRtcNet::ValueRange^ marshal_as(const std::pair& from) + { + auto range = gcnew WebRtcNet::ValueRange(); + range->Min = from.first; + range->Max = from.second; + return range; + } + }} diff --git a/WebRtcInterop/Marshaling/MarshalMediaConstraints.h b/WebRtcInterop/Marshaling/MarshalMediaConstraints.h index 69eef93..6119c93 100644 --- a/WebRtcInterop/Marshaling/MarshalMediaConstraints.h +++ b/WebRtcInterop/Marshaling/MarshalMediaConstraints.h @@ -1,11 +1,287 @@ #pragma once +#include +#include +#include +#include #include "MarshalCollections.h" - +#include "MarshalMedia.h" #include +namespace WebRtcInterop::Marshaling +{ + struct StringConstraint + { + std::optional ideal; + std::optional exact; + }; + + struct BooleanConstraint + { + std::optional ideal; + std::optional exact; + }; + + struct UintConstraint + { + std::optional ideal; + std::optional exact; + std::optional min; + std::optional max; + }; + + struct DoubleConstraint + { + std::optional ideal; + std::optional exact; + std::optional min; + std::optional max; + }; + + struct EchoCancellationValue + { + std::optional boolean_value; + std::optional mode_value; + }; + + struct EchoCancellationConstraint + { + std::optional ideal; + std::optional exact; + }; + + struct MediaTrackConstraintSet + { + std::optional width; + std::optional height; + std::optional aspect_ratio; + std::optional frame_rate; + std::optional facing_mode; + std::optional resize_mode; + std::optional sample_rate; + std::optional sample_size; + std::optional background_blur; + std::optional echo_cancellation; + std::optional auto_gain_control; + std::optional noise_suppression; + std::optional latency; + std::optional channel_count; + std::optional device_id; + std::optional group_id; + }; + + struct MediaTrackConstraints + { + MediaTrackConstraintSet basic; + std::vector advanced; + }; + + struct MediaStreamConstraints + { + bool audio_requested = false; + bool video_requested = false; + std::optional audio_constraints; + std::optional video_constraints; + }; +} + namespace msclr { namespace interop { - + inline WebRtcInterop::Marshaling::StringConstraint marshal_as(WebRtcNet::Media::MediaTrackConstraints::StringConstraint^ from) + { + WebRtcInterop::Marshaling::StringConstraint to{}; + if (from == nullptr) + return to; + + if (from->Ideal != nullptr) + { + auto ideal = from->Ideal; + to.ideal = marshal_as(ideal); + } + if (from->Exact != nullptr) + { + auto exact = from->Exact; + to.exact = marshal_as(exact); + } + + return to; + } + + inline WebRtcInterop::Marshaling::BooleanConstraint marshal_as(WebRtcNet::Media::MediaTrackConstraints::Constraint^ from) + { + WebRtcInterop::Marshaling::BooleanConstraint to{}; + if (from == nullptr) + return to; + + if (from->Ideal.HasValue) + to.ideal = from->Ideal.Value; + if (from->Exact.HasValue) + to.exact = from->Exact.Value; + + return to; + } + + inline WebRtcInterop::Marshaling::UintConstraint marshal_as(WebRtcNet::Media::MediaTrackConstraints::UIntRangeConstraint^ from) + { + WebRtcInterop::Marshaling::UintConstraint to{}; + if (from == nullptr) + return to; + + if (from->Ideal.HasValue) + to.ideal = from->Ideal.Value; + if (from->Exact.HasValue) + to.exact = from->Exact.Value; + if (from->Min.HasValue) + to.min = from->Min.Value; + if (from->Max.HasValue) + to.max = from->Max.Value; + + return to; + } + + inline WebRtcInterop::Marshaling::DoubleConstraint marshal_as(WebRtcNet::Media::MediaTrackConstraints::DoubleRangeConstraint^ from) + { + WebRtcInterop::Marshaling::DoubleConstraint to{}; + if (from == nullptr) + return to; + + if (from->Ideal.HasValue) + to.ideal = from->Ideal.Value; + if (from->Exact.HasValue) + to.exact = from->Exact.Value; + if (from->Min.HasValue) + to.min = from->Min.Value; + if (from->Max.HasValue) + to.max = from->Max.Value; + + return to; + } + + inline WebRtcInterop::Marshaling::EchoCancellationValue marshal_as(WebRtcNet::Media::EchoCancellationValue from) + { + WebRtcInterop::Marshaling::EchoCancellationValue to{}; + if (from.IsBoolean) + { + to.boolean_value = from.BooleanValue.GetValueOrDefault(); + return to; + } + + if (from.IsMode) + { + to.mode_value = marshal_as(from.ModeValue); + return to; + } + + throw gcnew System::InvalidCastException("EchoCancellationValue must contain either a boolean value or a mode value."); + } + + inline WebRtcInterop::Marshaling::EchoCancellationConstraint marshal_as(WebRtcNet::Media::EchoCancellationConstraint^ from) + { + WebRtcInterop::Marshaling::EchoCancellationConstraint to{}; + if (from == nullptr) + return to; + + if (from->Ideal.HasValue) + to.ideal = marshal_as(from->Ideal.Value); + if (from->Exact.HasValue) + to.exact = marshal_as(from->Exact.Value); + + return to; + } + + inline WebRtcInterop::Marshaling::MediaTrackConstraintSet marshal_as(WebRtcNet::Media::MediaTrackConstraintSet^ from) + { + WebRtcInterop::Marshaling::MediaTrackConstraintSet to{}; + if (from == nullptr) + return to; + + if (from->Width != nullptr) + to.width = marshal_as(safe_cast(from->Width)); + if (from->Height != nullptr) + to.height = marshal_as(safe_cast(from->Height)); + if (from->AspectRatio != nullptr) + to.aspect_ratio = marshal_as(safe_cast(from->AspectRatio)); + if (from->FrameRate != nullptr) + to.frame_rate = marshal_as(safe_cast(from->FrameRate)); + + if (from->FacingMode != nullptr) + { + WebRtcInterop::Marshaling::StringConstraint facing{}; + if (from->FacingMode->Ideal.HasValue) + facing.ideal = marshal_video_facing_mode_value_to_native(from->FacingMode->Ideal.Value); + if (from->FacingMode->Exact.HasValue) + facing.exact = marshal_video_facing_mode_value_to_native(from->FacingMode->Exact.Value); + to.facing_mode = facing; + } + + if (from->ResizeMode != nullptr) + { + WebRtcInterop::Marshaling::StringConstraint resize{}; + if (from->ResizeMode->Ideal.HasValue) + resize.ideal = marshal_video_resize_mode_value_to_native(from->ResizeMode->Ideal.Value); + if (from->ResizeMode->Exact.HasValue) + resize.exact = marshal_video_resize_mode_value_to_native(from->ResizeMode->Exact.Value); + to.resize_mode = resize; + } + + if (from->SampleRate != nullptr) + to.sample_rate = marshal_as(safe_cast(from->SampleRate)); + if (from->SampleSize != nullptr) + to.sample_size = marshal_as(safe_cast(from->SampleSize)); + if (from->BackgroundBlur != nullptr) + to.background_blur = marshal_as(from->BackgroundBlur); + if (from->EchoCancellation != nullptr) + to.echo_cancellation = marshal_as(from->EchoCancellation); + if (from->AutoGainControl != nullptr) + to.auto_gain_control = marshal_as(from->AutoGainControl); + if (from->NoiseSuppression != nullptr) + to.noise_suppression = marshal_as(from->NoiseSuppression); + if (from->Latency != nullptr) + to.latency = marshal_as(safe_cast(from->Latency)); + if (from->ChannelCount != nullptr) + to.channel_count = marshal_as(safe_cast(from->ChannelCount)); + if (from->DeviceId != nullptr) + to.device_id = marshal_as(from->DeviceId); + if (from->GroupId != nullptr) + to.group_id = marshal_as(from->GroupId); + + return to; + } + + inline WebRtcInterop::Marshaling::MediaTrackConstraints marshal_as(WebRtcNet::Media::MediaTrackConstraints^ from) + { + WebRtcInterop::Marshaling::MediaTrackConstraints to{}; + if (from == nullptr) + return to; + + to.basic = marshal_as(safe_cast(from)); + + if (from->Advanced != nullptr) + { + for each (auto advanced_constraint in from->Advanced) + { + to.advanced.push_back(marshal_as(advanced_constraint)); + } + } + + return to; + } + + inline WebRtcInterop::Marshaling::MediaStreamConstraints marshal_as(WebRtcNet::Media::MediaStreamConstraints^ from) + { + WebRtcInterop::Marshaling::MediaStreamConstraints to{}; + if (from == nullptr) + return to; + + to.audio_requested = from->Audio; + to.video_requested = from->Video; + + if (from->AudioConstraints != nullptr) + to.audio_constraints = marshal_as(from->AudioConstraints); + if (from->VideoConstraints != nullptr) + to.video_constraints = marshal_as(from->VideoConstraints); + + return to; + } }} \ No newline at end of file diff --git a/WebRtcInterop/Media/Marshaling/MarshalMedia.h b/WebRtcInterop/Media/Marshaling/MarshalMedia.h index 305fb6c..63d0876 100644 --- a/WebRtcInterop/Media/Marshaling/MarshalMedia.h +++ b/WebRtcInterop/Media/Marshaling/MarshalMedia.h @@ -1,63 +1,3 @@ #pragma once -#include - -#include -#include - -#include - -#include "Marshaling/MarshalEnums.h" - -namespace msclr::interop -{ - static const std::map - media_stream_track_state_map{ - {webrtc::MediaStreamTrackInterface::TrackState::kLive, WebRtcNet::Media::MediaStreamTrackState::Live}, - {webrtc::MediaStreamTrackInterface::TrackState::kEnded, WebRtcNet::Media::MediaStreamTrackState::Ended}, - }; - - template<> - inline WebRtcNet::Media::MediaStreamTrackState marshal_as(const webrtc::MediaStreamTrackInterface::TrackState& from) - { - return marshal_mapped_native_type(media_stream_track_state_map, from); - } - - template<> - inline webrtc::MediaStreamTrackInterface::TrackState - marshal_as( - const WebRtcNet::Media::MediaStreamTrackState& from) - { - return marshal_mapped_managed_type(media_stream_track_state_map, from); - } - - template<> - inline WebRtcNet::Media::MediaStreamTrackKind marshal_as(const std::string& from) - { - if (from == webrtc::MediaStreamTrackInterface::kAudioKind) return WebRtcNet::Media::MediaStreamTrackKind::Audio; - if (from == webrtc::MediaStreamTrackInterface::kVideoKind) return WebRtcNet::Media::MediaStreamTrackKind::Video; - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert track kind '{0}' to {1}.", - marshal_as(from), - WebRtcNet::Media::MediaStreamTrackKind::typeid->FullName)); - } - - template<> - inline std::string marshal_as( - const WebRtcNet::Media::MediaStreamTrackKind& from) - { - switch (from) - { - case WebRtcNet::Media::MediaStreamTrackKind::Audio: - return webrtc::MediaStreamTrackInterface::kAudioKind; - case WebRtcNet::Media::MediaStreamTrackKind::Video: - return webrtc::MediaStreamTrackInterface::kVideoKind; - } - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert {0} value '{1}' to native track kind.", - WebRtcNet::Media::MediaStreamTrackKind::typeid->FullName, - System::Enum::GetName(WebRtcNet::Media::MediaStreamTrackKind::typeid, from))); - } -} +#include "../../Marshaling/MarshalMedia.h" diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index e5ccd1c..4128115 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -4,8 +4,8 @@ #include "CameraVideoSource.h" #include "Logging/InteropHResult.h" #include "MediaStream.h" -#include "Marshaling/MarshalMedia.h" #include "Marshaling/MarshalMediaDevices.h" +#include "Marshaling/MarshalMediaConstraints.h" #include "RtcPeerConnectionFactory.h" #include diff --git a/WebRtcInterop/Media/MediaStreamTrack.cpp b/WebRtcInterop/Media/MediaStreamTrack.cpp index 581f403..78c792c 100644 --- a/WebRtcInterop/Media/MediaStreamTrack.cpp +++ b/WebRtcInterop/Media/MediaStreamTrack.cpp @@ -3,14 +3,54 @@ #include "MediaStreamTrack.h" #include -#include +#include -#include "Media/Marshaling/MarshalMedia.h" +#include "Marshaling/MarshalMediaConstraints.h" using namespace System; using namespace WebRtcNet; using namespace WebRtcNet::Media; +namespace +{ + Nullable ResolveFacingMode(MediaTrackConstraints^ constraints) + { + if (constraints == nullptr || constraints->FacingMode == nullptr) + return System::Nullable(); + + if (constraints->FacingMode->Exact.HasValue) + return constraints->FacingMode->Exact.Value; + if (constraints->FacingMode->Ideal.HasValue) + return constraints->FacingMode->Ideal.Value; + + return System::Nullable(); + } + + Nullable ResolveResizeMode(MediaTrackConstraints^ constraints) + { + if (constraints == nullptr || constraints->ResizeMode == nullptr) + return System::Nullable(); + + if (constraints->ResizeMode->Exact.HasValue) + return constraints->ResizeMode->Exact.Value; + if (constraints->ResizeMode->Ideal.HasValue) + return constraints->ResizeMode->Ideal.Value; + + return System::Nullable(); + } + + ValueRange^ CreateSingleUIntRange(int value) + { + if (value <= 0) + return nullptr; + + auto range = gcnew ValueRange(); + range->Min = static_cast(value); + range->Max = static_cast(value); + return range; + } +} + namespace WebRtcInterop::Media { MediaStreamTrack::MediaStreamTrack() @@ -122,7 +162,87 @@ namespace WebRtcInterop::Media MediaTrackCapabilities^ MediaStreamTrack::GetCapabilities() { - return gcnew MediaTrackCapabilities(); + auto native = GetNativeMediaStreamTrackInterface(false); + if (native == nullptr) + { + return MediaTrackCapabilities::Create( + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + String::Empty, + String::Empty); + } + + auto width = static_cast^>(nullptr); + auto height = static_cast^>(nullptr); + auto autoGainControl = gcnew List(); + auto noiseSuppression = gcnew List(); + auto echoCancellation = gcnew List(); + + if (native->kind() == webrtc::MediaStreamTrackInterface::kVideoKind) + { + auto video_track = static_cast(native.get()); + if (video_track != nullptr) + { + auto source = video_track->GetSource(); + if (source != nullptr) + { + webrtc::VideoTrackSourceInterface::Stats stats{}; + if (source->GetStats(&stats)) + { + width = CreateSingleUIntRange(stats.input_width); + height = CreateSingleUIntRange(stats.input_height); + } + } + } + } + else if (native->kind() == webrtc::MediaStreamTrackInterface::kAudioKind) + { + auto audio_track = static_cast(native.get()); + if (audio_track != nullptr) + { + auto source = audio_track->GetSource(); + if (source != nullptr) + { + auto options = source->options(); + if (options.echo_cancellation.has_value()) + echoCancellation->Add(EchoCancellationValue(options.echo_cancellation.value())); + if (options.auto_gain_control.has_value()) + autoGainControl->Add(options.auto_gain_control.value()); + if (options.noise_suppression.has_value()) + noiseSuppression->Add(options.noise_suppression.value()); + } + } + } + + return MediaTrackCapabilities::Create( + width, + height, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + echoCancellation, + nullptr, + autoGainControl, + noiseSuppression, + nullptr, + nullptr, + Id != nullptr ? Id : String::Empty, + String::Empty); } MediaTrackConstraints^ MediaStreamTrack::GetConstraints() @@ -135,14 +255,82 @@ namespace WebRtcInterop::Media MediaTrackSettings^ MediaStreamTrack::GetSettings() { - return gcnew MediaTrackSettings(); + auto native = GetNativeMediaStreamTrackInterface(false); + auto width = 0u; + auto height = 0u; + auto echoCancellation = EchoCancellationValue(false); + Nullable autoGainControl; + Nullable noiseSuppression; + + if (native != nullptr) + { + if (native->kind() == webrtc::MediaStreamTrackInterface::kVideoKind) + { + auto video_track = static_cast(native.get()); + if (video_track != nullptr) + { + auto source = video_track->GetSource(); + if (source != nullptr) + { + webrtc::VideoTrackSourceInterface::Stats stats{}; + if (source->GetStats(&stats)) + { + width = stats.input_width > 0 ? static_cast(stats.input_width) : 0u; + height = stats.input_height > 0 ? static_cast(stats.input_height) : 0u; + } + } + } + } + else if (native->kind() == webrtc::MediaStreamTrackInterface::kAudioKind) + { + auto audio_track = static_cast(native.get()); + if (audio_track != nullptr) + { + auto source = audio_track->GetSource(); + if (source != nullptr) + { + auto options = source->options(); + if (options.echo_cancellation.has_value()) + echoCancellation = EchoCancellationValue(options.echo_cancellation.value()); + if (options.auto_gain_control.has_value()) + autoGainControl = options.auto_gain_control.value(); + if (options.noise_suppression.has_value()) + noiseSuppression = options.noise_suppression.value(); + } + } + } + } + + return MediaTrackSettings::Create( + width, + height, + 0.0, + 0.0, + ResolveFacingMode(applied_constraints_), + ResolveResizeMode(applied_constraints_), + 0, + 0, + echoCancellation, + false, + autoGainControl, + noiseSuppression, + 0.0, + 0, + Id != nullptr ? Id : String::Empty, + String::Empty); } void MediaStreamTrack::ApplyConstraints(MediaTrackConstraints^ constraints) { if (constraints == nullptr) + { applied_constraints_ = gcnew MediaTrackConstraints(); - else - applied_constraints_ = constraints; + return; + } + + const auto marshaled_constraints = marshal_as(constraints); + (void)marshaled_constraints; + + applied_constraints_ = constraints; } } diff --git a/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs b/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs index 6f97593..d10a0c2 100644 --- a/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs +++ b/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs @@ -65,6 +65,14 @@ public void MediaTrackSettings_Defaults_ResizeMode_To_Null() Assert.IsNull(settings.ResizeMode); } + [Test] + public void MediaTrackSettings_Defaults_FacingMode_To_Null() + { + var settings = new MediaTrackSettings(); + + Assert.IsNull(settings.FacingMode); + } + [Test] public void MediaTrackSettings_Does_Not_Expose_Volume() { @@ -167,7 +175,7 @@ public void MediaTrackConstraintSet_IdlParity_HasRequiredConstraints_IsFalse_For var constraints = new MediaTrackConstraintSet { Width = new MediaTrackConstraints.PositiveUIntRangeConstraint { Ideal = 1280 }, - FacingMode = new MediaTrackConstraints.Constraint(VideoFacingModes.User) + FacingMode = new MediaTrackConstraints.Constraint(VideoFacingModes.User) { Exact = null, Ideal = VideoFacingModes.User, @@ -240,4 +248,54 @@ public void EchoCancellationValue_RawString_PreservesUnknownMode() Assert.That(value.Mode, Is.Null); Assert.That(value.ModeValue, Is.EqualTo("vendor-advanced-mode")); } -} \ No newline at end of file + + [Test] + public void VideoFacingModeValue_KnownEnum_UsesKnownValueAndRawString() + { + VideoFacingModeValue value = VideoFacingModes.Environment; + + Assert.That(value.IsKnown, Is.True); + Assert.That(value.KnownValue, Is.EqualTo(VideoFacingModes.Environment)); + Assert.That(value.RawValue, Is.EqualTo("environment")); + } + + [Test] + public void VideoFacingModeValue_RawString_PreservesUnknownValue() + { + var value = new VideoFacingModeValue("vendor-facing-mode"); + + Assert.That(value.IsKnown, Is.False); + Assert.That(value.KnownValue, Is.Null); + Assert.That(value.RawValue, Is.EqualTo("vendor-facing-mode")); + } + + [Test] + public void VideoResizeModeValue_KnownEnum_UsesKnownValueAndRawString() + { + VideoResizeModeValue value = VideoResizeModes.CropAndScale; + + Assert.That(value.IsKnown, Is.True); + Assert.That(value.KnownValue, Is.EqualTo(VideoResizeModes.CropAndScale)); + Assert.That(value.RawValue, Is.EqualTo("crop-and-scale")); + } + + [Test] + public void VideoResizeModeValue_RawString_PreservesUnknownValue() + { + var value = new VideoResizeModeValue("vendor-resize-mode"); + + Assert.That(value.IsKnown, Is.False); + Assert.That(value.KnownValue, Is.Null); + Assert.That(value.RawValue, Is.EqualTo("vendor-resize-mode")); + } + + [Test] + public void InputDeviceInfo_GetCapabilities_PopulatesIdentityFields() + { + var device = InputDeviceInfo.Create("device-1", MediaDeviceKind.VideoInput, "Camera", "group-1"); + var capabilities = device.GetCapabilities(); + + Assert.That(capabilities.DeviceId, Is.EqualTo("device-1")); + Assert.That(capabilities.GroupId, Is.EqualTo("group-1")); + } +} diff --git a/WebRtcNet.Api.UnitTests/MediaTrackUnsignedNumericTypesTests.cs b/WebRtcNet.Api.UnitTests/MediaTrackUnsignedNumericTypesTests.cs index a953471..5842362 100644 --- a/WebRtcNet.Api.UnitTests/MediaTrackUnsignedNumericTypesTests.cs +++ b/WebRtcNet.Api.UnitTests/MediaTrackUnsignedNumericTypesTests.cs @@ -41,14 +41,14 @@ public void MediaTrackCapabilities_UsesUintValueRangesForConstrainULongMembers() public void MediaTrackCapabilities_UsesCollectionForFacingMode() { Assert.That(typeof(MediaTrackCapabilities).GetProperty(nameof(MediaTrackCapabilities.FacingMode))!.PropertyType, - Is.EqualTo(typeof(IReadOnlyList))); + Is.EqualTo(typeof(IReadOnlyList))); } [Test] public void MediaTrackCapabilities_UsesCollectionForResizeMode() { Assert.That(typeof(MediaTrackCapabilities).GetProperty(nameof(MediaTrackCapabilities.ResizeMode))!.PropertyType, - Is.EqualTo(typeof(IReadOnlyList))); + Is.EqualTo(typeof(IReadOnlyList))); } [Test] @@ -94,7 +94,9 @@ public void MediaTrackSettings_UsesUintForConstrainULongMembers() public void MediaTrackSettings_UsesNullableMembersForOptionalResizeAndAudioProcessingFlags() { Assert.That(typeof(MediaTrackSettings).GetProperty(nameof(MediaTrackSettings.ResizeMode))!.PropertyType, - Is.EqualTo(typeof(VideoResizeModes?))); + Is.EqualTo(typeof(VideoResizeModeValue?))); + Assert.That(typeof(MediaTrackSettings).GetProperty(nameof(MediaTrackSettings.FacingMode))!.PropertyType, + Is.EqualTo(typeof(VideoFacingModeValue?))); Assert.That(typeof(MediaTrackSettings).GetProperty(nameof(MediaTrackSettings.AutoGainControl))!.PropertyType, Is.EqualTo(typeof(bool?))); Assert.That(typeof(MediaTrackSettings).GetProperty(nameof(MediaTrackSettings.NoiseSuppression))!.PropertyType, diff --git a/WebRtcNet.Api/Media/InputDeviceInfo.cs b/WebRtcNet.Api/Media/InputDeviceInfo.cs index 791d202..d06878a 100644 --- a/WebRtcNet.Api/Media/InputDeviceInfo.cs +++ b/WebRtcNet.Api/Media/InputDeviceInfo.cs @@ -1,6 +1,4 @@ -using System; - -namespace WebRtcNet.Media; +namespace WebRtcNet.Media; /// /// The InputDeviceInfo interface gives access to the capabilities of the input device it represents. @@ -20,7 +18,9 @@ internal InputDeviceInfo(string deviceId, MediaDeviceKind kind, string label, st /// public MediaTrackCapabilities GetCapabilities() { - throw new NotImplementedException(); + return MediaTrackCapabilities.Create( + deviceId: DeviceId, + groupId: GroupId); } /// diff --git a/WebRtcNet.Api/Media/MediaTrackCapabilities.cs b/WebRtcNet.Api/Media/MediaTrackCapabilities.cs index 683c952..c7847fa 100644 --- a/WebRtcNet.Api/Media/MediaTrackCapabilities.cs +++ b/WebRtcNet.Api/Media/MediaTrackCapabilities.cs @@ -88,16 +88,22 @@ public sealed record MediaTrackCapabilities /// /// The facing modes supported by this track's video capture source. /// + /// + /// Known values map to , while unknown raw strings are preserved. + /// /// /// - public IReadOnlyList FacingMode { get; init; } = []; + public IReadOnlyList FacingMode { get; init; } = []; /// /// The resize modes supported by the application for this track. /// + /// + /// Known values map to , while unknown raw strings are preserved. + /// /// /// - public IReadOnlyList ResizeMode { get; init; } = []; + public IReadOnlyList ResizeMode { get; init; } = []; /// /// The range of sample rates (in samples per second) supported by this track's audio source. @@ -189,8 +195,8 @@ internal static MediaTrackCapabilities Create( ValueRange? height = null, ValueRange? aspectRatio = null, ValueRange? frameRate = null, - IReadOnlyList? facingMode = null, - IReadOnlyList? resizeMode = null, + IReadOnlyList? facingMode = null, + IReadOnlyList? resizeMode = null, ValueRange? sampleRate = null, ValueRange? sampleSize = null, IReadOnlyList? echoCancellation = null, diff --git a/WebRtcNet.Api/Media/MediaTrackConstraintSet.cs b/WebRtcNet.Api/Media/MediaTrackConstraintSet.cs index 3100ffe..69d6e27 100644 --- a/WebRtcNet.Api/Media/MediaTrackConstraintSet.cs +++ b/WebRtcNet.Api/Media/MediaTrackConstraintSet.cs @@ -40,16 +40,18 @@ public class MediaTrackConstraintSet /// /// The directions that the camera can face, as seen from the user's perspective. + /// Known values map to , while unknown raw strings are preserved. /// /// - public MediaTrackConstraints.Constraint? FacingMode { get; set; } + public MediaTrackConstraints.Constraint? FacingMode { get; set; } /// /// The means by which the resolution can be derived by the application. In other words, whether the application is - /// allowed to use cropping and down-scaling on the camera output. + /// allowed to use cropping and down-scaling on the camera output. Known values map to + /// , while unknown raw strings are preserved. /// /// - public MediaTrackConstraints.Constraint? ResizeMode { get; set; } + public MediaTrackConstraints.Constraint? ResizeMode { get; set; } /// /// The sample rate in samples per second for the audio data. diff --git a/WebRtcNet.Api/Media/MediaTrackSettings.cs b/WebRtcNet.Api/Media/MediaTrackSettings.cs index e785498..e0b3cd6 100644 --- a/WebRtcNet.Api/Media/MediaTrackSettings.cs +++ b/WebRtcNet.Api/Media/MediaTrackSettings.cs @@ -35,16 +35,22 @@ public sealed record MediaTrackSettings /// /// Current facing mode setting for the track, when exposed by the platform. /// + /// + /// Known values map to , while unknown raw strings are preserved. + /// /// /// - public VideoFacingModes? FacingMode { get; init; } + public VideoFacingModeValue? FacingMode { get; init; } /// /// Current resize mode for the track, when exposed by the platform. /// + /// + /// Known values map to , while unknown raw strings are preserved. + /// /// /// - public VideoResizeModes? ResizeMode { get; init; } + public VideoResizeModeValue? ResizeMode { get; init; } /// /// Current audio sample rate setting for the track. @@ -119,8 +125,8 @@ internal static MediaTrackSettings Create( uint height, double aspectRatio, double frameRate, - VideoFacingModes? facingMode, - VideoResizeModes? resizeMode, + VideoFacingModeValue? facingMode, + VideoResizeModeValue? resizeMode, uint sampleRate, uint sampleSize, EchoCancellationValue echoCancellation, diff --git a/WebRtcNet.Api/Media/VideoModeValues.cs b/WebRtcNet.Api/Media/VideoModeValues.cs new file mode 100644 index 0000000..463c61b --- /dev/null +++ b/WebRtcNet.Api/Media/VideoModeValues.cs @@ -0,0 +1,165 @@ +using System; + +namespace WebRtcNet.Media; + +/// +/// Represents a single facing-mode value, preserving unknown raw strings for forward compatibility. +/// +/// +public readonly record struct VideoFacingModeValue +{ + private readonly string _rawValue; + + /// + /// Creates a facing-mode value from a known enum value. + /// + /// Known facing-mode value. + public VideoFacingModeValue(VideoFacingModes value) + { + _rawValue = value switch + { + VideoFacingModes.User => "user", + VideoFacingModes.Environment => "environment", + VideoFacingModes.Left => "left", + VideoFacingModes.Right => "right", + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; + } + + /// + /// Creates a facing-mode value from a raw string. + /// + /// Raw facing-mode string value. + public VideoFacingModeValue(string value) + { + if (value is null) throw new ArgumentNullException(nameof(value)); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Facing mode value must not be empty.", nameof(value)); + + _rawValue = value; + } + + /// + /// Gets whether maps to a known enum value. + /// + public bool IsKnown => KnownValue.HasValue; + + /// + /// Gets the known enum value when recognized; otherwise . + /// + public VideoFacingModes? KnownValue => RawValue switch + { + "user" => VideoFacingModes.User, + "environment" => VideoFacingModes.Environment, + "left" => VideoFacingModes.Left, + "right" => VideoFacingModes.Right, + _ => null + }; + + /// + /// Gets the raw string representation of this value. + /// + public string RawValue => _rawValue ?? string.Empty; + + /// + /// Returns the serialized facing-mode string. + /// + public override string ToString() + { + return RawValue; + } + + /// + /// Converts a known enum value to a . + /// + public static implicit operator VideoFacingModeValue(VideoFacingModes from) + { + return new VideoFacingModeValue(from); + } + + /// + /// Converts a raw string value to a . + /// + public static implicit operator VideoFacingModeValue(string from) + { + return new VideoFacingModeValue(from); + } +} + +/// +/// Represents a single resize-mode value, preserving unknown raw strings for forward compatibility. +/// +/// +public readonly record struct VideoResizeModeValue +{ + private readonly string _rawValue; + + /// + /// Creates a resize-mode value from a known enum value. + /// + /// Known resize-mode value. + public VideoResizeModeValue(VideoResizeModes value) + { + _rawValue = value switch + { + VideoResizeModes.None => "none", + VideoResizeModes.CropAndScale => "crop-and-scale", + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; + } + + /// + /// Creates a resize-mode value from a raw string. + /// + /// Raw resize-mode string value. + public VideoResizeModeValue(string value) + { + if (value is null) throw new ArgumentNullException(nameof(value)); + if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException("Resize mode value must not be empty.", nameof(value)); + + _rawValue = value; + } + + /// + /// Gets whether maps to a known enum value. + /// + public bool IsKnown => KnownValue.HasValue; + + /// + /// Gets the known enum value when recognized; otherwise . + /// + public VideoResizeModes? KnownValue => RawValue switch + { + "none" => VideoResizeModes.None, + "crop-and-scale" => VideoResizeModes.CropAndScale, + _ => null + }; + + /// + /// Gets the raw string representation of this value. + /// + public string RawValue => _rawValue ?? string.Empty; + + /// + /// Returns the serialized resize-mode string. + /// + public override string ToString() + { + return RawValue; + } + + /// + /// Converts a known enum value to a . + /// + public static implicit operator VideoResizeModeValue(VideoResizeModes from) + { + return new VideoResizeModeValue(from); + } + + /// + /// Converts a raw string value to a . + /// + public static implicit operator VideoResizeModeValue(string from) + { + return new VideoResizeModeValue(from); + } +} From 5ed6a2e890a57cf556eec8342971a928d952207c Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Wed, 17 Jun 2026 17:27:54 -0700 Subject: [PATCH 2/3] Implement InputDeviceInfo.GetCapabilities via delegate pattern - Add Func? delegate to InputDeviceInfo; set at device enumeration time, invoked lazily in GetCapabilities() - VideoCapabilityQuery: DirectShow width/height/frameRate/aspectRatio, ResizeMode, FacingMode (via Media Foundation); split to own files - AudioCapabilityQuery: WASAPI sampleRate/sampleSize/channelCount; hardcode autoGainControl and noiseSuppression as [true, false] per WebRTC APM (mirrors Blink input_device_info.cc); echoCancellation reports [Software, false] always -- System mode added later once WASAPI device effects are queried - Remove reflection from MediaStreamTrack.cpp; use direct Create() calls - MarshalMediaTrackCapabilities.h: remove stale DoubleRange/IntRange specs - MediaTrackCapabilities.Create/CreateIdentity made public for C++/CLI ([EditorBrowsable(Never)] hides from IDE); same for MediaTrackSettings - Rename EchoCancellationMode: All/RemoteOnly -> System/Software 'software' = WebRTC AEC3 in app layer (raw WASAPI, always available) 'system' = OS/driver AEC (default WASAPI mode, device-dependent) boolean true = attempt System first, fall back to Software - Update marshaling maps, C++/CLI tests, and managed tests accordingly - Add unit tests for delegate and fallback paths (118 tests pass) Closes #59 Related to #58 (GetSupportedConstraints -- now has data to aggregate) Related to #51 and #68 (capability data is a prerequisite for both) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Marshaling/MarshalMediaTests.cpp | 4 +- WebRtcInterop/Marshaling/MarshalMedia.h | 4 +- WebRtcInterop/Media/AudioCapabilityQuery.cpp | 144 ++++++++++ WebRtcInterop/Media/AudioCapabilityQuery.h | 16 ++ .../MarshalMediaTrackCapabilities.h | 153 +--------- WebRtcInterop/Media/MediaDevices.cpp | 11 +- WebRtcInterop/Media/VideoCapabilityQuery.cpp | 262 ++++++++++++++++++ WebRtcInterop/Media/VideoCapabilityQuery.h | 16 ++ WebRtcInterop/WebRtcInterop.Shared.vcxitems | 4 + .../MediaTrackConstraintTests.cs | 31 ++- WebRtcNet.Api/Media/EchoCancellation.cs | 47 +++- WebRtcNet.Api/Media/InputDeviceInfo.cs | 22 +- WebRtcNet.Api/Media/MediaTrackCapabilities.cs | 10 +- WebRtcNet.Api/Media/MediaTrackSettings.cs | 2 +- 14 files changed, 545 insertions(+), 181 deletions(-) create mode 100644 WebRtcInterop/Media/AudioCapabilityQuery.cpp create mode 100644 WebRtcInterop/Media/AudioCapabilityQuery.h create mode 100644 WebRtcInterop/Media/VideoCapabilityQuery.cpp create mode 100644 WebRtcInterop/Media/VideoCapabilityQuery.h diff --git a/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp b/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp index f5adbcb..ebff996 100644 --- a/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp +++ b/WebRtcInterop.UnitTests/Marshaling/MarshalMediaTests.cpp @@ -151,7 +151,7 @@ TEST(marshal_media_constraints_tests, marshal_media_stream_constraints_includes_ videoTrackConstraints->Width->Ideal = 800; videoTrackConstraints->EchoCancellation = gcnew EchoCancellationConstraint(); - videoTrackConstraints->EchoCancellation->Ideal = EchoCancellationValue(EchoCancellationMode::RemoteOnly); + videoTrackConstraints->EchoCancellation->Ideal = EchoCancellationValue(EchoCancellationMode::Software); videoTrackConstraints->Advanced = gcnew System::Collections::Generic::List(); auto advancedSet = gcnew MediaTrackConstraintSet(); @@ -169,7 +169,7 @@ TEST(marshal_media_constraints_tests, marshal_media_stream_constraints_includes_ ASSERT_TRUE(marshaled.video_constraints->basic.echo_cancellation.has_value()); ASSERT_TRUE(marshaled.video_constraints->basic.echo_cancellation->ideal.has_value()); ASSERT_TRUE(marshaled.video_constraints->basic.echo_cancellation->ideal->mode_value.has_value()); - ASSERT_EQ(marshaled.video_constraints->basic.echo_cancellation->ideal->mode_value.value(), "remote-only"); + ASSERT_EQ(marshaled.video_constraints->basic.echo_cancellation->ideal->mode_value.value(), "software"); } TEST(media_stream_track_constraint_plumbing_tests, apply_constraints_with_unknown_facing_mode_throws) diff --git a/WebRtcInterop/Marshaling/MarshalMedia.h b/WebRtcInterop/Marshaling/MarshalMedia.h index 3598663..df29559 100644 --- a/WebRtcInterop/Marshaling/MarshalMedia.h +++ b/WebRtcInterop/Marshaling/MarshalMedia.h @@ -46,8 +46,8 @@ namespace msclr { namespace interop }; static const std::map echo_cancellation_mode_map{ - {"all", WebRtcNet::Media::EchoCancellationMode::All}, - {"remote-only", WebRtcNet::Media::EchoCancellationMode::RemoteOnly}, + {"software", WebRtcNet::Media::EchoCancellationMode::Software}, + {"system", WebRtcNet::Media::EchoCancellationMode::System}, }; template<> diff --git a/WebRtcInterop/Media/AudioCapabilityQuery.cpp b/WebRtcInterop/Media/AudioCapabilityQuery.cpp new file mode 100644 index 0000000..95772e8 --- /dev/null +++ b/WebRtcInterop/Media/AudioCapabilityQuery.cpp @@ -0,0 +1,144 @@ +#include "pch.h" + +#include "AudioCapabilityQuery.h" +#include "Marshaling/MarshalMediaTrackCapabilities.h" + +#include +#include + +namespace WebRtcInterop::Media +{ + using namespace WebRtcNet; + + namespace + { + MediaTrackCapabilities^ QueryWithDevice(String^ endpointId, IMMDevice* device) + { + IAudioClient* audioClient = nullptr; + WAVEFORMATEX* format = nullptr; + + try + { + auto hr = device->Activate( + __uuidof(IAudioClient), + CLSCTX_ALL, + nullptr, + reinterpret_cast(&audioClient)); + if (FAILED(hr) || audioClient == nullptr) + return MediaTrackCapabilities::CreateIdentity(endpointId); + + hr = audioClient->GetMixFormat(&format); + if (FAILED(hr) || format == nullptr) + return MediaTrackCapabilities::CreateIdentity(endpointId); + + ValueRange^ sampleRate = nullptr; + ValueRange^ sampleSize = nullptr; + ValueRange^ channelCount = nullptr; + + if (format->nSamplesPerSec > 0) + { + sampleRate = MarshalToValueRange( + System::Nullable( + static_cast(format->nSamplesPerSec))); + } + + if (format->wBitsPerSample > 0) + { + sampleSize = MarshalToValueRange( + System::Nullable( + static_cast(format->wBitsPerSample))); + } + + if (format->nChannels > 0) + { + channelCount = MarshalToValueRange( + System::Nullable( + static_cast(format->nChannels))); + } + + // WebRTC's software APM always provides AGC and noise suppression regardless of hardware. + // This mirrors Blink's input_device_info.cc which hardcodes {true, false} for both. + auto alwaysSupported = gcnew Collections::Generic::List(); + alwaysSupported->Add(true); + alwaysSupported->Add(false); + + // Software (WebRTC AEC3) is always available. System mode requires OS/driver support + // and is added separately when WASAPI device effects are queried (not yet implemented). + // false = disabled is always a valid option. + auto echoCancellation = gcnew Collections::Generic::List(); + echoCancellation->Add(EchoCancellationValue(EchoCancellationMode::Software)); + echoCancellation->Add(EchoCancellationValue(false)); + + return MediaTrackCapabilities::Create( + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + sampleRate, + sampleSize, + echoCancellation, + nullptr, + alwaysSupported, + alwaysSupported, + nullptr, + channelCount, + endpointId, + String::Empty); + } + finally + { + if (format != nullptr) + CoTaskMemFree(format); + if (audioClient != nullptr) + audioClient->Release(); + } + } + } + + AudioCapabilityQuery::AudioCapabilityQuery(String^ endpointId) + : endpoint_id_(endpointId) + { + } + + MediaTrackCapabilities^ AudioCapabilityQuery::Query() + { + using namespace msclr::interop; + + auto hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + const auto uninitialize = SUCCEEDED(hr) && hr != S_FALSE; + + IMMDeviceEnumerator* enumerator = nullptr; + IMMDevice* device = nullptr; + + try + { + hr = CoCreateInstance( + __uuidof(MMDeviceEnumerator), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IMMDeviceEnumerator), + reinterpret_cast(&enumerator)); + if (FAILED(hr) || enumerator == nullptr) + return MediaTrackCapabilities::CreateIdentity(endpoint_id_); + + String^ endpointId = endpoint_id_; + const auto nativeId = marshal_as(endpointId); + hr = enumerator->GetDevice(nativeId.c_str(), &device); + if (FAILED(hr) || device == nullptr) + return MediaTrackCapabilities::CreateIdentity(endpoint_id_); + + return QueryWithDevice(endpoint_id_, device); + } + finally + { + if (device != nullptr) + device->Release(); + if (enumerator != nullptr) + enumerator->Release(); + if (uninitialize) + CoUninitialize(); + } + } +} diff --git a/WebRtcInterop/Media/AudioCapabilityQuery.h b/WebRtcInterop/Media/AudioCapabilityQuery.h new file mode 100644 index 0000000..00dcbbb --- /dev/null +++ b/WebRtcInterop/Media/AudioCapabilityQuery.h @@ -0,0 +1,16 @@ +#pragma once + +namespace WebRtcInterop::Media +{ + using namespace System; + using namespace WebRtcNet::Media; + + ref class AudioCapabilityQuery sealed + { + initonly String^ endpoint_id_; + + public: + AudioCapabilityQuery(String^ endpointId); + MediaTrackCapabilities^ Query(); + }; +} diff --git a/WebRtcInterop/Media/Marshaling/MarshalMediaTrackCapabilities.h b/WebRtcInterop/Media/Marshaling/MarshalMediaTrackCapabilities.h index 07b0dcd..fb8de07 100644 --- a/WebRtcInterop/Media/Marshaling/MarshalMediaTrackCapabilities.h +++ b/WebRtcInterop/Media/Marshaling/MarshalMediaTrackCapabilities.h @@ -2,29 +2,12 @@ #include -#include "../Marshaling/MarshalEnums.h" +#include "../../Marshaling/MarshalMedia.h" namespace msclr::interop { /// - /// Marshals EchoCancellationValue from a boolean. - /// - inline WebRtcNet::Media::EchoCancellationValue MarshalEchoCancellationValue(bool value) - { - return WebRtcNet::Media::EchoCancellationValue(value); - } - - /// - /// Marshals EchoCancellationValue from a mode string. - /// - inline WebRtcNet::Media::EchoCancellationValue MarshalEchoCancellationValue(const std::string& modeStr) - { - return WebRtcNet::Media::EchoCancellationValue(marshal_as(modeStr)); - } - - /// - /// Marshals a nullable value to a ValueRange by setting both Min and Max. - /// If the value is null, returns an empty range. + /// Marshals a nullable value to a ValueRange<T> by setting both Min and Max to that value. /// template inline WebRtcNet::ValueRange^ MarshalToValueRange(System::Nullable value) @@ -39,8 +22,7 @@ namespace msclr::interop } /// - /// Marshals a range (min, max) to a ValueRange. - /// If both are invalid/unset, returns an empty range. + /// Marshals a (min, max) pair to a ValueRange<T>. /// template inline WebRtcNet::ValueRange^ MarshalToValueRange(System::Nullable min, System::Nullable max) @@ -51,133 +33,4 @@ namespace msclr::interop return range; } - /// - /// Marshals a native double range to a managed ValueRange. - /// Used for video frame rate, audio latency, aspect ratio. - /// - template<> - inline WebRtcNet::ValueRange^ marshal_as(const webrtc::DoubleRange& from) - { - return MarshalToValueRange( - from.min() > 0.0 ? from.min() : System::Nullable(), - from.max() > 0.0 ? from.max() : System::Nullable() - ); - } - - /// - /// Marshals a native int range to a managed ValueRange. - /// Used for video width/height, audio sample rate/size, channel count. - /// - template<> - inline WebRtcNet::ValueRange^ marshal_as(const webrtc::IntRange& from) - { - return MarshalToValueRange( - from.min() > 0 ? static_cast(from.min()) : System::Nullable(), - from.max() > 0 ? static_cast(from.max()) : System::Nullable() - ); - } - - // VideoFacingMode enum mappings - static const std::map video_facing_mode_map{ - {"user", WebRtcNet::Media::VideoFacingModes::User}, - {"environment", WebRtcNet::Media::VideoFacingModes::Environment}, - {"left", WebRtcNet::Media::VideoFacingModes::Left}, - {"right", WebRtcNet::Media::VideoFacingModes::Right}, - }; - - template<> - inline WebRtcNet::Media::VideoFacingModes marshal_as(const std::string& from) - { - auto entry = video_facing_mode_map.find(from); - if (entry != video_facing_mode_map.end()) - return entry->second; - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert video facing mode '{0}' to {1}.", - marshal_as(from), - WebRtcNet::Media::VideoFacingModes::typeid->FullName)); - } - - template<> - inline std::string marshal_as( - const WebRtcNet::Media::VideoFacingModes& from) - { - for (auto [key, value] : video_facing_mode_map) - { - if (value == from) return key; - } - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert {0} value '{1}' to native video facing mode.", - WebRtcNet::Media::VideoFacingModes::typeid->FullName, - System::Enum::GetName(WebRtcNet::Media::VideoFacingModes::typeid, from))); - } - - // VideoResizeMode enum mappings - static const std::map video_resize_mode_map{ - {"none", WebRtcNet::Media::VideoResizeModes::None}, - {"crop-and-scale", WebRtcNet::Media::VideoResizeModes::CropAndScale}, - }; - - template<> - inline WebRtcNet::Media::VideoResizeModes marshal_as(const std::string& from) - { - auto entry = video_resize_mode_map.find(from); - if (entry != video_resize_mode_map.end()) - return entry->second; - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert video resize mode '{0}' to {1}.", - marshal_as(from), - WebRtcNet::Media::VideoResizeModes::typeid->FullName)); - } - - template<> - inline std::string marshal_as( - const WebRtcNet::Media::VideoResizeModes& from) - { - for (auto [key, value] : video_resize_mode_map) - { - if (value == from) return key; - } - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert {0} value '{1}' to native video resize mode.", - WebRtcNet::Media::VideoResizeModes::typeid->FullName, - System::Enum::GetName(WebRtcNet::Media::VideoResizeModes::typeid, from))); - } - - // EchoCancellationMode enum mappings - static const std::map echo_cancellation_mode_map{ - {"all", WebRtcNet::Media::EchoCancellationMode::All}, - {"remote-only", WebRtcNet::Media::EchoCancellationMode::RemoteOnly}, - }; - - template<> - inline WebRtcNet::Media::EchoCancellationMode marshal_as(const std::string& from) - { - auto entry = echo_cancellation_mode_map.find(from); - if (entry != echo_cancellation_mode_map.end()) - return entry->second; - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert echo cancellation mode '{0}' to {1}.", - marshal_as(from), - WebRtcNet::Media::EchoCancellationMode::typeid->FullName)); - } - - template<> - inline std::string marshal_as( - const WebRtcNet::Media::EchoCancellationMode& from) - { - for (auto [key, value] : echo_cancellation_mode_map) - { - if (value == from) return key; - } - - throw gcnew System::InvalidCastException( - System::String::Format("Unable to convert {0} value '{1}' to native echo cancellation mode.", - WebRtcNet::Media::EchoCancellationMode::typeid->FullName, - System::Enum::GetName(WebRtcNet::Media::EchoCancellationMode::typeid, from))); - } } diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 4128115..82b7658 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -1,5 +1,7 @@ #include "pch.h" +#include "AudioCapabilityQuery.h" +#include "VideoCapabilityQuery.h" #include "MediaDevices.h" #include "CameraVideoSource.h" #include "Logging/InteropHResult.h" @@ -24,6 +26,7 @@ using namespace System::Timers; namespace WebRtcInterop::Media { + using namespace WebRtcNet; using namespace WebRtcNet::Media; static List^ EnumerateAudioDevices(); @@ -114,8 +117,9 @@ namespace WebRtcInterop::Media CoTaskMemFree(deviceId); devices->Add(isInput - ? InputDeviceInfo::Create(id, kind, label, String::Empty) - : MediaDeviceInfo::Create(id, kind, label, String::Empty)); + ? InputDeviceInfo::Create(id, kind, label, String::Empty, + gcnew Func(gcnew AudioCapabilityQuery(id), &AudioCapabilityQuery::Query)) + : MediaDeviceInfo::Create(id, kind, label, String::Empty)); device->Release(); } @@ -403,7 +407,8 @@ namespace WebRtcInterop::Media id, MediaDeviceKind::VideoInput, label, - groupId)); + groupId, + gcnew Func(gcnew VideoCapabilityQuery(id), &VideoCapabilityQuery::Query))); } } catch (...) diff --git a/WebRtcInterop/Media/VideoCapabilityQuery.cpp b/WebRtcInterop/Media/VideoCapabilityQuery.cpp new file mode 100644 index 0000000..eca8b53 --- /dev/null +++ b/WebRtcInterop/Media/VideoCapabilityQuery.cpp @@ -0,0 +1,262 @@ +#include "pch.h" + +#include "VideoCapabilityQuery.h" +#include "Marshaling/MarshalMediaTrackCapabilities.h" + +#include +#include +#include + +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfplat.lib") + +// MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_PANEL_INFO and MF_CAMERA_FACING_DIRECTION +// are not present in all Windows SDK versions. Define them manually if needed. +#ifndef MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_PANEL_INFO +EXTERN_GUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_PANEL_INFO, + 0xf9e8a569, 0x7f2c, 0x458f, 0xad, 0xcf, 0x38, 0xee, 0x7d, 0x8c, 0x63, 0x42); +#endif + +#ifndef MF_CAMERA_FACING_DIRECTION_UNKNOWN +enum MF_CAMERA_FACING_DIRECTION +{ + MF_CAMERA_FACING_DIRECTION_UNKNOWN = 0, + MF_CAMERA_FACING_DIRECTION_ENVIRONMENT = 1, + MF_CAMERA_FACING_DIRECTION_USER = 2, +}; +#endif + +namespace WebRtcInterop::Media +{ + using namespace WebRtcNet; + using namespace System::Collections::Generic; + + namespace + { + void ScanDirectShow( + String^ deviceId, + ValueRange^% width, + ValueRange^% height, + ValueRange^% aspectRatio, + ValueRange^% frameRate) + { + using namespace msclr::interop; + + width = nullptr; + height = nullptr; + aspectRatio = nullptr; + frameRate = nullptr; + + const std::unique_ptr deviceInfo( + webrtc::VideoCaptureFactory::CreateDeviceInfo()); + + const auto nativeDeviceId = marshal_as(deviceId); + const int32_t count = deviceInfo + ? deviceInfo->NumberOfCapabilities(nativeDeviceId.c_str()) + : 0; + if (count <= 0) + return; + + int32_t minWidth = INT_MAX; + int32_t maxWidth = 0; + int32_t minHeight = INT_MAX; + int32_t maxHeight = 0; + int32_t maxFps = 0; + double minAspect = DBL_MAX; + double maxAspect = 0.0; + + for (uint32_t i = 0; i < static_cast(count); ++i) + { + webrtc::VideoCaptureCapability cap{}; + if (deviceInfo->GetCapability(nativeDeviceId.c_str(), i, cap) != 0) + continue; + + if (cap.width > 0) + { + if (cap.width < minWidth) + minWidth = cap.width; + if (cap.width > maxWidth) + maxWidth = cap.width; + } + + if (cap.height > 0) + { + if (cap.height < minHeight) + minHeight = cap.height; + if (cap.height > maxHeight) + maxHeight = cap.height; + } + + if (cap.maxFPS > 0 && cap.maxFPS > maxFps) + maxFps = cap.maxFPS; + + if (cap.width > 0 && cap.height > 0) + { + const auto ar = static_cast(cap.width) / cap.height; + if (ar < minAspect) + minAspect = ar; + if (ar > maxAspect) + maxAspect = ar; + } + } + + if (maxWidth > 0) + { + width = MarshalToValueRange( + System::Nullable(static_cast(minWidth)), + System::Nullable(static_cast(maxWidth))); + } + + if (maxHeight > 0) + { + height = MarshalToValueRange( + System::Nullable(static_cast(minHeight)), + System::Nullable(static_cast(maxHeight))); + } + + if (maxFps > 0) + { + frameRate = MarshalToValueRange( + System::Nullable(0.0), + System::Nullable(static_cast(maxFps))); + } + + if (maxAspect > 0.0) + { + aspectRatio = MarshalToValueRange( + System::Nullable(minAspect), + System::Nullable(maxAspect)); + } + } + + List^ QueryFacingMode(String^ deviceId) + { + using namespace msclr::interop; + + bool mfStarted = false; + IMFAttributes* attributes = nullptr; + IMFActivate** devices = nullptr; + UINT32 deviceCount = 0; + + try + { + if (FAILED(MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET))) + return nullptr; + mfStarted = true; + + if (FAILED(MFCreateAttributes(&attributes, 1)) || attributes == nullptr) + return nullptr; + + if (FAILED(attributes->SetGUID( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID))) + { + return nullptr; + } + + if (FAILED(MFEnumDeviceSources(attributes, &devices, &deviceCount))) + return nullptr; + + for (UINT32 i = 0; i < deviceCount; ++i) + { + if (devices[i] == nullptr) + continue; + + WCHAR* link = nullptr; + UINT32 linkLen = 0; + if (FAILED(devices[i]->GetAllocatedString( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, + &link, + &linkLen))) + { + continue; + } + + const auto matches = link != nullptr && String::Equals( + marshal_as(link), + deviceId, + StringComparison::OrdinalIgnoreCase); + CoTaskMemFree(link); + + if (!matches) + continue; + + UINT32 panel = MF_CAMERA_FACING_DIRECTION_UNKNOWN; + devices[i]->GetUINT32( + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_PANEL_INFO, + &panel); + + if (panel == MF_CAMERA_FACING_DIRECTION_USER) + { + auto modes = gcnew List(); + modes->Add(VideoFacingModeValue(VideoFacingModes::User)); + return modes; + } + + if (panel == MF_CAMERA_FACING_DIRECTION_ENVIRONMENT) + { + auto modes = gcnew List(); + modes->Add(VideoFacingModeValue(VideoFacingModes::Environment)); + return modes; + } + + return nullptr; + } + + return nullptr; + } + finally + { + for (UINT32 i = 0; i < deviceCount; ++i) + if (devices[i] != nullptr) + devices[i]->Release(); + + CoTaskMemFree(devices); + + if (attributes != nullptr) + attributes->Release(); + + if (mfStarted) + MFShutdown(); + } + } + } + + VideoCapabilityQuery::VideoCapabilityQuery(String^ deviceId) + : device_id_(deviceId) + { + } + + MediaTrackCapabilities^ VideoCapabilityQuery::Query() + { + ValueRange^ width = nullptr; + ValueRange^ height = nullptr; + ValueRange^ aspectRatio = nullptr; + ValueRange^ frameRate = nullptr; + ScanDirectShow(device_id_, width, height, aspectRatio, frameRate); + + auto facingMode = QueryFacingMode(device_id_); + + auto resizeMode = gcnew List(); + resizeMode->Add(VideoResizeModeValue(VideoResizeModes::None)); + resizeMode->Add(VideoResizeModeValue(VideoResizeModes::CropAndScale)); + + return MediaTrackCapabilities::Create( + width, + height, + aspectRatio, + frameRate, + facingMode, + resizeMode, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr, + device_id_, + String::Empty); + } +} diff --git a/WebRtcInterop/Media/VideoCapabilityQuery.h b/WebRtcInterop/Media/VideoCapabilityQuery.h new file mode 100644 index 0000000..48628b5 --- /dev/null +++ b/WebRtcInterop/Media/VideoCapabilityQuery.h @@ -0,0 +1,16 @@ +#pragma once + +namespace WebRtcInterop::Media +{ + using namespace System; + using namespace WebRtcNet::Media; + + ref class VideoCapabilityQuery sealed + { + initonly String^ device_id_; + + public: + VideoCapabilityQuery(String^ deviceId); + MediaTrackCapabilities^ Query(); + }; +} diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index 4dedf92..ad52fba 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -138,10 +138,12 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + + @@ -153,10 +155,12 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + + diff --git a/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs b/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs index d10a0c2..c47dba5 100644 --- a/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs +++ b/WebRtcNet.Api.UnitTests/MediaTrackConstraintTests.cs @@ -230,13 +230,13 @@ public void EchoCancellationConstraint_ImplicitBool_SetsExactBoolean() [Test] public void EchoCancellationConstraint_ImplicitEnum_SetsExactMode() { - EchoCancellationConstraint constraint = EchoCancellationMode.RemoteOnly; + EchoCancellationConstraint constraint = EchoCancellationMode.Software; var exact = constraint.Exact; Assert.That(exact.HasValue, Is.True); Assert.That(exact.GetValueOrDefault().IsMode, Is.True); - Assert.That(exact.GetValueOrDefault().Mode, Is.EqualTo(EchoCancellationMode.RemoteOnly)); - Assert.That(exact.GetValueOrDefault().ModeValue, Is.EqualTo("remote-only")); + Assert.That(exact.GetValueOrDefault().Mode, Is.EqualTo(EchoCancellationMode.Software)); + Assert.That(exact.GetValueOrDefault().ModeValue, Is.EqualTo("software")); } [Test] @@ -298,4 +298,29 @@ public void InputDeviceInfo_GetCapabilities_PopulatesIdentityFields() Assert.That(capabilities.DeviceId, Is.EqualTo("device-1")); Assert.That(capabilities.GroupId, Is.EqualTo("group-1")); } + + [Test] + public void InputDeviceInfo_GetCapabilities_InvokesDelegate_WhenProvided() + { + var expected = MediaTrackCapabilities.Create( + deviceId: "device-2", + groupId: "group-2", + width: new ValueRange { Min = 640, Max = 1920 }); + + var device = InputDeviceInfo.Create("device-2", MediaDeviceKind.VideoInput, "Camera", "group-2", + () => expected); + + Assert.That(device.GetCapabilities(), Is.SameAs(expected)); + } + + [Test] + public void InputDeviceInfo_GetCapabilities_FallsBackToIdentityFields_WhenNoDelegateProvided() + { + var device = InputDeviceInfo.Create("device-3", MediaDeviceKind.AudioInput, "Mic", "group-3"); + var capabilities = device.GetCapabilities(); + + Assert.That(capabilities.DeviceId, Is.EqualTo("device-3")); + Assert.That(capabilities.GroupId, Is.EqualTo("group-3")); + Assert.That(capabilities.Width, Is.Null); + } } diff --git a/WebRtcNet.Api/Media/EchoCancellation.cs b/WebRtcNet.Api/Media/EchoCancellation.cs index 3fc95d3..0dad62b 100644 --- a/WebRtcNet.Api/Media/EchoCancellation.cs +++ b/WebRtcNet.Api/Media/EchoCancellation.cs @@ -3,22 +3,38 @@ namespace WebRtcNet.Media; /// -/// Named echo-cancellation modes defined by the Media Capture and Streams specification. +/// Named echo-cancellation modes indicating which pipeline performs the cancellation. /// -/// +/// +/// +/// These modes are mutually exclusive: opens the audio device in raw +/// (unprocessed) mode and runs WebRTC's AEC3 algorithm in the application layer, while +/// opens the device in default mode and relies on the OS audio engine or +/// driver to apply echo cancellation before samples reach the application. +/// +/// +/// Running both simultaneously causes artifacts because each canceller assumes it is the only +/// one in the chain. A boolean constraint value of attempts +/// first and falls back to if the OS does not +/// provide echo cancellation for the selected device. +/// +/// +/// public enum EchoCancellationMode { /// - /// Attempt to remove all system-rendered sound from the captured microphone signal. + /// Echo cancellation is performed by WebRTC's software APM (AEC3) in the application layer. + /// The audio device is opened in raw (unprocessed) mode, bypassing OS audio effects. + /// Always available regardless of hardware or OS capabilities. /// - /// - All, + Software, /// - /// Attempt to remove only remote-party playback from the captured microphone signal. + /// Echo cancellation is performed by the OS audio engine or audio driver. + /// The audio device is opened in default mode, which allows the OS to apply its own + /// processing pipeline. Availability is device- and driver-dependent. /// - /// - RemoteOnly + System } /// @@ -37,7 +53,12 @@ public readonly record struct EchoCancellationValue /// /// Creates a boolean echo-cancellation value. /// - /// The requested or reported boolean state. + /// + /// The requested or reported boolean state. When used as a constraint, + /// attempts first and falls back to + /// if OS-level echo cancellation is unavailable + /// for the selected device. + /// public EchoCancellationValue(bool value) { _booleanValue = value; @@ -53,8 +74,8 @@ public EchoCancellationValue(EchoCancellationMode mode) _booleanValue = null; _modeValue = mode switch { - EchoCancellationMode.All => "all", - EchoCancellationMode.RemoteOnly => "remote-only", + EchoCancellationMode.Software => "software", + EchoCancellationMode.System => "system", _ => throw new ArgumentOutOfRangeException(nameof(mode)) }; } @@ -104,8 +125,8 @@ public EchoCancellationValue(string mode) public EchoCancellationMode? Mode => ModeValue switch { - "all" => EchoCancellationMode.All, - "remote-only" => EchoCancellationMode.RemoteOnly, + "software" => EchoCancellationMode.Software, + "system" => EchoCancellationMode.System, _ => null }; diff --git a/WebRtcNet.Api/Media/InputDeviceInfo.cs b/WebRtcNet.Api/Media/InputDeviceInfo.cs index d06878a..70fb174 100644 --- a/WebRtcNet.Api/Media/InputDeviceInfo.cs +++ b/WebRtcNet.Api/Media/InputDeviceInfo.cs @@ -1,4 +1,6 @@ -namespace WebRtcNet.Media; +using System; + +namespace WebRtcNet.Media; /// /// The InputDeviceInfo interface gives access to the capabilities of the input device it represents. @@ -6,18 +8,25 @@ /// public sealed record InputDeviceInfo : MediaDeviceInfo { - internal InputDeviceInfo(string deviceId, MediaDeviceKind kind, string label, string groupId) + private readonly Func? _getCapabilities; + + internal InputDeviceInfo(string deviceId, MediaDeviceKind kind, string label, string groupId, + Func? getCapabilities = null) : base(deviceId, kind, label, groupId) { + _getCapabilities = getCapabilities; } /// - /// Returns a MediaTrackCapabilities object describing the primary audio or video track of a device's MediaStream - /// (according to its kind value), in the absence of any user-supplied constraints. + /// Returns a object describing the primary audio or video track of a + /// device's MediaStream (according to its kind value), in the absence of any user-supplied constraints. /// /// public MediaTrackCapabilities GetCapabilities() { + if (_getCapabilities is not null) + return _getCapabilities(); + return MediaTrackCapabilities.Create( deviceId: DeviceId, groupId: GroupId); @@ -27,6 +36,7 @@ public MediaTrackCapabilities GetCapabilities() /// Factory method for creating InputDeviceInfo instances (for interop use only). /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - public new static InputDeviceInfo Create(string deviceId, MediaDeviceKind kind, string label, string groupId) - => new(deviceId, kind, label, groupId); + public new static InputDeviceInfo Create(string deviceId, MediaDeviceKind kind, string label, string groupId, + Func? getCapabilities = null) + => new(deviceId, kind, label, groupId, getCapabilities); } \ No newline at end of file diff --git a/WebRtcNet.Api/Media/MediaTrackCapabilities.cs b/WebRtcNet.Api/Media/MediaTrackCapabilities.cs index c7847fa..2c60e65 100644 --- a/WebRtcNet.Api/Media/MediaTrackCapabilities.cs +++ b/WebRtcNet.Api/Media/MediaTrackCapabilities.cs @@ -190,7 +190,7 @@ public sealed record MediaTrackCapabilities /// Factory method for creating MediaTrackCapabilities instances (for interop use only). /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - internal static MediaTrackCapabilities Create( + public static MediaTrackCapabilities Create( ValueRange? width = null, ValueRange? height = null, ValueRange? aspectRatio = null, @@ -226,4 +226,12 @@ internal static MediaTrackCapabilities Create( DeviceId = deviceId ?? string.Empty, GroupId = groupId ?? string.Empty }; + + /// + /// Creates a minimal containing only identity fields. + /// Used by interop as a fallback when platform capability queries fail. + /// + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public static MediaTrackCapabilities CreateIdentity(string? deviceId) + => Create(deviceId: deviceId); } \ No newline at end of file diff --git a/WebRtcNet.Api/Media/MediaTrackSettings.cs b/WebRtcNet.Api/Media/MediaTrackSettings.cs index e0b3cd6..cae2ab7 100644 --- a/WebRtcNet.Api/Media/MediaTrackSettings.cs +++ b/WebRtcNet.Api/Media/MediaTrackSettings.cs @@ -120,7 +120,7 @@ public sealed record MediaTrackSettings /// Factory method for creating MediaTrackSettings instances (for interop use only). /// [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] - internal static MediaTrackSettings Create( + public static MediaTrackSettings Create( uint width, uint height, double aspectRatio, From be1b1eb4baf16f844b9b7e97009d864cb30ccdbd Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Wed, 17 Jun 2026 17:51:39 -0700 Subject: [PATCH 3/3] Add HRESULT logging and diagnostics to capability query classes - AudioCapabilityQuery: replace silent FAILED() checks with InteropHResult::LogIfFailed for COM init, device lookup, IAudioClient activation, and GetMixFormat; add trace log with mix format details - VideoCapabilityQuery: replace silent FAILED() checks with InteropHResult::LogIfFailed for MFStartup, MFCreateAttributes, SetGUID, MFEnumDeviceSources, and symbolic link lookup; add trace log with DirectShow capability count and facing mode result - Add WebRtcLogEventId.AudioCapabilityQueryCompleted (2004) and VideoCapabilityQueryCompleted (2005) for structured log filtering - Restore accidentally removed #include in InteropHResult.h - EchoCancellationMode: All/RemoteOnly -> System/Software; update serialized strings ('system'/'software'), marshaling maps, and tests - EchoCancellationValue(bool): document that true attempts System first, falls back to Software Closes #50 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- WebRtcInterop/Media/AudioCapabilityQuery.cpp | 25 ++++++++--- WebRtcInterop/Media/VideoCapabilityQuery.cpp | 41 ++++++++++++++----- .../WebRtcInterop.Shared.vcxitems.filters | 4 ++ WebRtcNet.Api/Logging/WebRtcLogEventId.cs | 6 ++- 4 files changed, 58 insertions(+), 18 deletions(-) diff --git a/WebRtcInterop/Media/AudioCapabilityQuery.cpp b/WebRtcInterop/Media/AudioCapabilityQuery.cpp index 95772e8..45a4c21 100644 --- a/WebRtcInterop/Media/AudioCapabilityQuery.cpp +++ b/WebRtcInterop/Media/AudioCapabilityQuery.cpp @@ -2,6 +2,7 @@ #include "AudioCapabilityQuery.h" #include "Marshaling/MarshalMediaTrackCapabilities.h" +#include "../Logging/InteropHResult.h" #include #include @@ -9,6 +10,7 @@ namespace WebRtcInterop::Media { using namespace WebRtcNet; + using namespace WebRtcNet::Logging; namespace { @@ -24,13 +26,23 @@ namespace WebRtcInterop::Media CLSCTX_ALL, nullptr, reinterpret_cast(&audioClient)); - if (FAILED(hr) || audioClient == nullptr) + if (InteropHResult::LogIfFailed(hr, "IMMDevice::Activate(IAudioClient)", "Interop.Media.Audio.Capabilities") + || audioClient == nullptr) return MediaTrackCapabilities::CreateIdentity(endpointId); hr = audioClient->GetMixFormat(&format); - if (FAILED(hr) || format == nullptr) + if (InteropHResult::LogIfFailed(hr, "IAudioClient::GetMixFormat", "Interop.Media.Audio.Capabilities") + || format == nullptr) return MediaTrackCapabilities::CreateIdentity(endpointId); + WebRtcLogWriterBridge::WriteInteropLog( + 0, + static_cast(WebRtcLogEventId::AudioCapabilityQueryCompleted), + "Interop.Media.Audio.Capabilities", + Threading::Thread::CurrentThread->ManagedThreadId, + String::Format("Audio mix format for device {0}: {1}Hz, {2}-bit, {3}ch", + endpointId, format->nSamplesPerSec, format->wBitsPerSample, format->nChannels)); + ValueRange^ sampleRate = nullptr; ValueRange^ sampleSize = nullptr; ValueRange^ channelCount = nullptr; @@ -52,8 +64,7 @@ namespace WebRtcInterop::Media if (format->nChannels > 0) { channelCount = MarshalToValueRange( - System::Nullable( - static_cast(format->nChannels))); + System::Nullable(format->nChannels)); } // WebRTC's software APM always provides AGC and noise suppression regardless of hardware. @@ -120,13 +131,15 @@ namespace WebRtcInterop::Media CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); - if (FAILED(hr) || enumerator == nullptr) + if (InteropHResult::LogIfFailed(hr, "CoCreateInstance(MMDeviceEnumerator)", "Interop.Media.Audio.Capabilities") + || enumerator == nullptr) return MediaTrackCapabilities::CreateIdentity(endpoint_id_); String^ endpointId = endpoint_id_; const auto nativeId = marshal_as(endpointId); hr = enumerator->GetDevice(nativeId.c_str(), &device); - if (FAILED(hr) || device == nullptr) + if (InteropHResult::LogIfFailed(hr, "IMMDeviceEnumerator::GetDevice", "Interop.Media.Audio.Capabilities") + || device == nullptr) return MediaTrackCapabilities::CreateIdentity(endpoint_id_); return QueryWithDevice(endpoint_id_, device); diff --git a/WebRtcInterop/Media/VideoCapabilityQuery.cpp b/WebRtcInterop/Media/VideoCapabilityQuery.cpp index eca8b53..48e7e68 100644 --- a/WebRtcInterop/Media/VideoCapabilityQuery.cpp +++ b/WebRtcInterop/Media/VideoCapabilityQuery.cpp @@ -2,6 +2,7 @@ #include "VideoCapabilityQuery.h" #include "Marshaling/MarshalMediaTrackCapabilities.h" +#include "../Logging/InteropHResult.h" #include #include @@ -29,6 +30,7 @@ enum MF_CAMERA_FACING_DIRECTION namespace WebRtcInterop::Media { using namespace WebRtcNet; + using namespace WebRtcNet::Logging; using namespace System::Collections::Generic; namespace @@ -54,6 +56,14 @@ namespace WebRtcInterop::Media const int32_t count = deviceInfo ? deviceInfo->NumberOfCapabilities(nativeDeviceId.c_str()) : 0; + + WebRtcLogWriterBridge::WriteInteropLog( + 0, + static_cast(WebRtcLogEventId::VideoCapabilityQueryCompleted), + "Interop.Media.Video.Capabilities", + System::Threading::Thread::CurrentThread->ManagedThreadId, + String::Format("DirectShow: {0} capability entries for device {1}", count, deviceId)); + if (count <= 0) return; @@ -140,21 +150,24 @@ namespace WebRtcInterop::Media try { - if (FAILED(MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET))) + auto hr = MFStartup(MF_VERSION, MFSTARTUP_NOSOCKET); + if (InteropHResult::LogIfFailed(hr, "MFStartup", "Interop.Media.Video.Capabilities")) return nullptr; mfStarted = true; - if (FAILED(MFCreateAttributes(&attributes, 1)) || attributes == nullptr) + hr = MFCreateAttributes(&attributes, 1); + if (InteropHResult::LogIfFailed(hr, "MFCreateAttributes", "Interop.Media.Video.Capabilities") + || attributes == nullptr) return nullptr; - if (FAILED(attributes->SetGUID( + hr = attributes->SetGUID( MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, - MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID))) - { + MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID); + if (InteropHResult::LogIfFailed(hr, "IMFAttributes::SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE)", "Interop.Media.Video.Capabilities")) return nullptr; - } - if (FAILED(MFEnumDeviceSources(attributes, &devices, &deviceCount))) + hr = MFEnumDeviceSources(attributes, &devices, &deviceCount); + if (InteropHResult::LogIfFailed(hr, "MFEnumDeviceSources", "Interop.Media.Video.Capabilities")) return nullptr; for (UINT32 i = 0; i < deviceCount; ++i) @@ -164,13 +177,12 @@ namespace WebRtcInterop::Media WCHAR* link = nullptr; UINT32 linkLen = 0; - if (FAILED(devices[i]->GetAllocatedString( + hr = devices[i]->GetAllocatedString( MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, &link, - &linkLen))) - { + &linkLen); + if (InteropHResult::LogIfFailed(hr, "IMFActivate::GetAllocatedString(SYMBOLIC_LINK)", "Interop.Media.Video.Capabilities")) continue; - } const auto matches = link != nullptr && String::Equals( marshal_as(link), @@ -186,6 +198,13 @@ namespace WebRtcInterop::Media MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_PANEL_INFO, &panel); + WebRtcLogWriterBridge::WriteInteropLog( + 0, + static_cast(WebRtcLogEventId::VideoCapabilityQueryCompleted), + "Interop.Media.Video.Capabilities", + System::Threading::Thread::CurrentThread->ManagedThreadId, + String::Format("Facing mode panel={0} for device {1}", panel, deviceId)); + if (panel == MF_CAMERA_FACING_DIRECTION_USER) { auto modes = gcnew List(); diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters index c9d68b2..bec045f 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters @@ -49,6 +49,8 @@ Logging + + @@ -108,5 +110,7 @@ Logging + + \ No newline at end of file diff --git a/WebRtcNet.Api/Logging/WebRtcLogEventId.cs b/WebRtcNet.Api/Logging/WebRtcLogEventId.cs index c42028b..2119d7d 100644 --- a/WebRtcNet.Api/Logging/WebRtcLogEventId.cs +++ b/WebRtcNet.Api/Logging/WebRtcLogEventId.cs @@ -4,7 +4,7 @@ namespace WebRtcNet.Logging; /// Event ID constants for structured logging across WebRtcNet layers. /// Event IDs are grouped by category to enable telemetry aggregation. /// -public enum WebRtcLogEventId : int +public enum WebRtcLogEventId { // WebRTC events (1000-1999) // PeerConnection events (1000-1099) @@ -66,6 +66,10 @@ public enum WebRtcLogEventId : int VideoEnumerationStarted = 2002, /// Video device enumeration failed. VideoEnumerationFailed = 2003, + /// Audio capability query completed successfully. + AudioCapabilityQueryCompleted = 2004, + /// Video DirectShow capability scan completed. + VideoCapabilityQueryCompleted = 2005, // Media constraints events (2100-2199) /// Constraint validation event.