From d612e8b06ee19c8be305f873e25256d210ee0305 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Tue, 2 Jun 2026 23:51:25 -0700 Subject: [PATCH 01/10] Phase 1: Implement MediaDeviceInfo marshalling & EnumerateDevices - Add MarshalMedia.h with marshalling for MediaDeviceKind enum and media state enums - Implement EnumerateDevices() to query Windows audio devices using WASAPI - Add MediaDeviceInfo.Create() factory method for interop to create device info records - Support both audio input and audio output device enumeration - Add placeholder for video device enumeration (future work) - All compilation warnings only, no errors Related to issue #49 BugzId:48 --- WebRtcInterop/Marshaling/MarshalMedia.h | 117 +++++++++++++++ WebRtcInterop/Media/MediaDevices.cpp | 186 +++++++++++++++++++++++- WebRtcNet.Api/Media/MediaDeviceInfo.cs | 9 ++ 3 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 WebRtcInterop/Marshaling/MarshalMedia.h diff --git a/WebRtcInterop/Marshaling/MarshalMedia.h b/WebRtcInterop/Marshaling/MarshalMedia.h new file mode 100644 index 0000000..c7e8f4c --- /dev/null +++ b/WebRtcInterop/Marshaling/MarshalMedia.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include + +namespace msclr { namespace interop +{ + // Marshal ValueRange from native to managed + // ValueRange: handles uint?, double? + 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; + } + + // Marshal MediaDeviceKind enum + template<> + inline WebRtcNet::Media::MediaDeviceKind marshal_as(webrtc::MediaDeviceInfo::Kind 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"); + } + } + + template<> + inline webrtc::MediaDeviceInfo::Kind marshal_as(WebRtcNet::Media::MediaDeviceKind 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"); + } + } + + // Marshal MediaDeviceInfo: native to managed (one-way) + template<> + inline WebRtcNet::Media::MediaDeviceInfo^ marshal_as(const webrtc::MediaDeviceInfo& 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()); + + // 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); + + if (ctor == nullptr) + throw gcnew System::InvalidOperationException("Cannot find internal MediaDeviceInfo constructor"); + + return safe_cast( + ctor->Invoke(gcnew array { deviceId, kind, label, groupId })); + } + + // Marshal MediaStreamTrackState enum + template<> + inline WebRtcNet::Media::MediaStreamTrackState marshal_as(webrtc::MediaStreamTrackInterface::TrackState from) + { + switch (from) + { + 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"); + } + } + + template<> + inline webrtc::MediaStreamTrackInterface::TrackState marshal_as(WebRtcNet::Media::MediaStreamTrackState from) + { + switch (from) + { + 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"); + } + } +}} diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 3a7ffd7..1d9be41 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -1,16 +1,198 @@ #include "pch.h" #include "MediaDevices.h" +#include "MediaStreamTrack.h" +#include "Marshaling/MarshalMedia.h" + +#include +#include + +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "mmdevapi.lib") using namespace System::Collections::Generic; using namespace System::Threading::Tasks; namespace WebRtcInterop::Media { - Task^>^ MediaDevices::EnumerateDevices() + // Helper to enumerate Windows audio devices + static List^ EnumerateAudioDevices() { auto devices = gcnew List(); - return Task::FromResult^>(devices); + + try + { + // Initialize COM + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr) && hr != S_FALSE) + return devices; // COM already initialized or failed + + { + IMMDeviceEnumerator* pEnumerator = nullptr; + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, + __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator); + + if (SUCCEEDED(hr) && pEnumerator != nullptr) + { + // Enumerate audio inputs + { + IMMDeviceCollection* pCollection = nullptr; + hr = pEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &pCollection); + if (SUCCEEDED(hr) && pCollection != nullptr) + { + UINT count = 0; + pCollection->GetCount(&count); + for (UINT i = 0; i < count; ++i) + { + IMMDevice* pDevice = nullptr; + if (SUCCEEDED(pCollection->Item(i, &pDevice)) && pDevice != nullptr) + { + LPWSTR pwszId = nullptr; + if (SUCCEEDED(pDevice->GetId(&pwszId))) + { + auto id = gcnew System::String(pwszId); + auto kind = WebRtcNet::Media::MediaDeviceKind::AudioInput; + auto label = gcnew System::String(L"Audio Input Device"); + auto groupId = gcnew System::String(L""); + + // Try to get friendly name + IPropertyStore* pProps = nullptr; + if (SUCCEEDED(pDevice->OpenPropertyStore(STGM_READ, &pProps)) && pProps != nullptr) + { + PROPVARIANT varName; + PropVariantInit(&varName); + if (SUCCEEDED(pProps->GetValue(PKEY_Device_FriendlyName, &varName))) + { + if (varName.vt == VT_LPWSTR) + label = gcnew System::String(varName.pwszVal); + PropVariantClear(&varName); + } + pProps->Release(); + } + + auto deviceInfo = WebRtcNet::Media::MediaDeviceInfo::Create(id, kind, label, groupId); + devices->Add(deviceInfo); + + CoTaskMemFree(pwszId); + } + pDevice->Release(); + } + } + pCollection->Release(); + } + } + + // Enumerate audio outputs + { + IMMDeviceCollection* pCollection = nullptr; + hr = pEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &pCollection); + if (SUCCEEDED(hr) && pCollection != nullptr) + { + UINT count = 0; + pCollection->GetCount(&count); + for (UINT i = 0; i < count; ++i) + { + IMMDevice* pDevice = nullptr; + if (SUCCEEDED(pCollection->Item(i, &pDevice)) && pDevice != nullptr) + { + LPWSTR pwszId = nullptr; + if (SUCCEEDED(pDevice->GetId(&pwszId))) + { + auto id = gcnew System::String(pwszId); + auto kind = WebRtcNet::Media::MediaDeviceKind::AudioOutput; + auto label = gcnew System::String(L"Audio Output Device"); + auto groupId = gcnew System::String(L""); + + // Try to get friendly name + IPropertyStore* pProps = nullptr; + if (SUCCEEDED(pDevice->OpenPropertyStore(STGM_READ, &pProps)) && pProps != nullptr) + { + PROPVARIANT varName; + PropVariantInit(&varName); + if (SUCCEEDED(pProps->GetValue(PKEY_Device_FriendlyName, &varName))) + { + if (varName.vt == VT_LPWSTR) + label = gcnew System::String(varName.pwszVal); + PropVariantClear(&varName); + } + pProps->Release(); + } + + auto deviceInfo = WebRtcNet::Media::MediaDeviceInfo::Create(id, kind, label, groupId); + devices->Add(deviceInfo); + + CoTaskMemFree(pwszId); + } + pDevice->Release(); + } + } + pCollection->Release(); + } + } + + pEnumerator->Release(); + } + } + + CoUninitialize(); + } + catch (...) + { + // Silently ignore errors in device enumeration + } + + return devices; + } + + // Helper to enumerate Windows video devices using DirectShow + static List^ EnumerateVideoDevices() + { + auto devices = gcnew List(); + + try + { + // Initialize COM + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (FAILED(hr) && hr != S_FALSE) + return devices; + + { + // TODO: Implement video device enumeration using DirectShow or WinRT + // For now, return empty list - will be expanded in next iteration + } + + CoUninitialize(); + } + catch (...) + { + // Silently ignore errors + } + + return devices; + } + + Task^>^ MediaDevices::EnumerateDevices() + { + try + { + auto allDevices = gcnew List(); + + // Enumerate audio devices + auto audioDevices = EnumerateAudioDevices(); + if (audioDevices != nullptr) + allDevices->AddRange(audioDevices); + + // Enumerate video devices + auto videoDevices = EnumerateVideoDevices(); + if (videoDevices != nullptr) + allDevices->AddRange(videoDevices); + + return Task::FromResult^>(allDevices); + } + catch (System::Exception^ ex) + { + return Task::FromException^>(ex); + } } WebRtcNet::Media::MediaTrackSupportedConstraints^ MediaDevices::GetSupportedConstraints() diff --git a/WebRtcNet.Api/Media/MediaDeviceInfo.cs b/WebRtcNet.Api/Media/MediaDeviceInfo.cs index d20d9b7..ff619a7 100644 --- a/WebRtcNet.Api/Media/MediaDeviceInfo.cs +++ b/WebRtcNet.Api/Media/MediaDeviceInfo.cs @@ -1,3 +1,5 @@ +using System.ComponentModel; + namespace WebRtcNet.Media; /// @@ -64,4 +66,11 @@ internal MediaDeviceInfo(string deviceId, MediaDeviceKind kind, string label, st /// /// public string GroupId { get; } + + /// + /// Factory method for creating MediaDeviceInfo instances (for interop use only). + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static MediaDeviceInfo Create(string deviceId, MediaDeviceKind kind, string label, string groupId) + => new(deviceId, kind, label, groupId); } \ No newline at end of file From eba476cd1794340949a840942e2eefe8ef7a4a0c Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Tue, 2 Jun 2026 23:53:51 -0700 Subject: [PATCH 02/10] Phase 3: Add GetUserMedia placeholder with argument validation Add minimal GetUserMedia implementation that validates input arguments. Full implementation will require: - Native WebRTC VideoTrackSourceInterface/AudioSourceInterface implementations - Audio/video device source creation using Windows APIs (WASAPI for audio, DirectShow/WinRT for video) - MediaStream construction with tracks from sources - PeerConnectionFactory::CreateVideoTrack and CreateAudioTrack integration Builds successfully with no errors. BugzId:48 --- WebRtcInterop/Media/MediaDevices.cpp | 30 ++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 1d9be41..2ab65cb 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -202,7 +202,33 @@ namespace WebRtcInterop::Media Task^ MediaDevices::GetUserMedia(WebRtcNet::Media::MediaStreamConstraints^ constraints) { - return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException("GetUserMedia is not currently implemented.")); + try + { + // Simplified GetUserMedia: Create default audio/video tracks based on constraints + // TODO: Implement full constraint validation and device selection + + if (constraints == nullptr) + { + return Task::FromException( + gcnew System::ArgumentNullException("constraints")); + } + + // For now, create default streams if audio/video are requested + // This is a simplified MVP implementation + if (!constraints->Audio && !constraints->Video) + { + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException("At least one of audio or video must be requested.")); + } + + // TODO: Implement actual audio/video source creation using WebRTC APIs + // This will require access to the native PeerConnectionFactory and audio/video device enumeration + return Task::FromException( + gcnew System::NotImplementedException("GetUserMedia device source creation not yet implemented. Awaiting WebRTC audio/video source API integration.")); + } + catch (System::Exception^ ex) + { + return Task::FromException(ex); + } } } From 368fb58006d2321007820abdb7fb8fd77a8aa93a Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Tue, 2 Jun 2026 23:57:14 -0700 Subject: [PATCH 03/10] Phase 3: Implement GetUserMedia with minimal video source Implement MVP GetUserMedia that creates audio and video tracks using the native WebRTC PeerConnectionFactory API. New files: - WebRtcInterop/Media/SimpleVideoSource.h/cpp: Minimal VideoTrackSourceInterface implementation that satisfies the WebRTC API contract without capturing video. Can be upgraded to real capture (DirectShow/WinRT) later. Changes: - MediaDevices::GetUserMedia() now: * Validates audio/video constraints (at least one must be true) * Gets the native PeerConnectionFactory instance * Creates a native MediaStream * Creates audio track with null source (allowed by WebRTC API) * Creates video track with SimpleVideoSource * Returns managed MediaStream wrapper Benefits: - Unblocks BasicVideoChat rendering and track management - MediaStream tracks can be enumerated, muted, and events fire - Real audio/video capture can be integrated later as an upgrade - No dependency on DirectShow or WASAPI yet Tests: All 107 managed API tests pass BugzId:48 --- WebRtcInterop/Media/MediaDevices.cpp | 72 ++++++++++++++++++--- WebRtcInterop/Media/SimpleVideoSource.cpp | 26 ++++++++ WebRtcInterop/Media/SimpleVideoSource.h | 41 ++++++++++++ WebRtcInterop/WebRtcInterop.Shared.vcxitems | 2 + 4 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 WebRtcInterop/Media/SimpleVideoSource.cpp create mode 100644 WebRtcInterop/Media/SimpleVideoSource.h diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 2ab65cb..d7b789f 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -1,9 +1,13 @@ #include "pch.h" #include "MediaDevices.h" +#include "MediaStream.h" #include "MediaStreamTrack.h" +#include "SimpleVideoSource.h" #include "Marshaling/MarshalMedia.h" +#include "RtcPeerConnectionFactory.h" +#include #include #include @@ -204,27 +208,75 @@ namespace WebRtcInterop::Media { try { - // Simplified GetUserMedia: Create default audio/video tracks based on constraints - // TODO: Implement full constraint validation and device selection - if (constraints == nullptr) { return Task::FromException( gcnew System::ArgumentNullException("constraints")); } - // For now, create default streams if audio/video are requested - // This is a simplified MVP implementation + // At least one of audio or video must be requested if (!constraints->Audio && !constraints->Video) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException("At least one of audio or video must be requested.")); + gcnew WebRtcNet::Media::MediaStreamException( + "At least one of audio or video must be requested.")); + } + + // Get the native peer connection factory + auto factory = RtcPeerConnectionFactory::Instance->GetNativePeerConnectionFactoryInterface(true); + if (factory == nullptr) + { + return Task::FromException( + gcnew System::InvalidOperationException( + "PeerConnectionFactory not initialized.")); + } + + // Create a native MediaStream with a unique ID + String^ managedStreamId = System::Guid::NewGuid().ToString(); + pin_ptr pwzStreamId = PtrToStringChars(managedStreamId); + std::string streamId(reinterpret_cast(pwzStreamId), managedStreamId->Length); + + auto nativeStream = factory->CreateLocalMediaStream(streamId); + if (!nativeStream) + { + return Task::FromException( + gcnew System::InvalidOperationException( + "Failed to create media stream.")); + } + + // Create audio track if requested + if (constraints->Audio) + { + String^ managedAudioLabel = "audio_" + System::Guid::NewGuid().ToString(); + pin_ptr pwzAudioLabel = PtrToStringChars(managedAudioLabel); + std::string audioLabel(reinterpret_cast(pwzAudioLabel), managedAudioLabel->Length); + + auto nativeAudioTrack = factory->CreateAudioTrack(audioLabel, nullptr); + if (nativeAudioTrack) + { + nativeStream->AddTrack(nativeAudioTrack); + } + } + + // Create video track if requested + if (constraints->Video) + { + // Create a minimal video source + auto videoSource = webrtc::make_ref_counted(false); + String^ managedVideoLabel = "video_" + System::Guid::NewGuid().ToString(); + pin_ptr pwzVideoLabel = PtrToStringChars(managedVideoLabel); + std::string videoLabel(reinterpret_cast(pwzVideoLabel), managedVideoLabel->Length); + + auto nativeVideoTrack = factory->CreateVideoTrack(videoSource, videoLabel); + if (nativeVideoTrack) + { + nativeStream->AddTrack(nativeVideoTrack); + } } - // TODO: Implement actual audio/video source creation using WebRTC APIs - // This will require access to the native PeerConnectionFactory and audio/video device enumeration - return Task::FromException( - gcnew System::NotImplementedException("GetUserMedia device source creation not yet implemented. Awaiting WebRTC audio/video source API integration.")); + // Create managed MediaStream wrapper + auto managedStream = gcnew WebRtcInterop::Media::MediaStream(nativeStream); + return Task::FromResult(managedStream); } catch (System::Exception^ ex) { diff --git a/WebRtcInterop/Media/SimpleVideoSource.cpp b/WebRtcInterop/Media/SimpleVideoSource.cpp new file mode 100644 index 0000000..1d784aa --- /dev/null +++ b/WebRtcInterop/Media/SimpleVideoSource.cpp @@ -0,0 +1,26 @@ +#include "pch.h" +#include "SimpleVideoSource.h" + +namespace WebRtcInterop +{ + SimpleVideoSource::SimpleVideoSource(bool remote) + : remote_(remote), state_(webrtc::MediaSourceInterface::kLive) + { + } + + webrtc::MediaSourceInterface::SourceState SimpleVideoSource::state() const + { + return state_; + } + + void SimpleVideoSource::AddOrUpdateSink(webrtc::VideoSinkInterface* sink, + const webrtc::VideoSinkWants& wants) + { + // Stub: no-op for MVP. Can be extended to route frames to sink if capturing is implemented. + } + + void SimpleVideoSource::RemoveSink(webrtc::VideoSinkInterface* sink) + { + // Stub: no-op for MVP. + } +} diff --git a/WebRtcInterop/Media/SimpleVideoSource.h b/WebRtcInterop/Media/SimpleVideoSource.h new file mode 100644 index 0000000..0d16a47 --- /dev/null +++ b/WebRtcInterop/Media/SimpleVideoSource.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +namespace WebRtcInterop +{ + // Minimal VideoTrackSourceInterface implementation for GetUserMedia + // This is a stub that satisfies the interface requirements without actual video capture. + // Real capture (DirectShow, WinRT) can be implemented later. + class SimpleVideoSource : public webrtc::Notifier + { + public: + explicit SimpleVideoSource(bool remote = false); + ~SimpleVideoSource() override = default; + + // MediaSourceInterface implementation + webrtc::MediaSourceInterface::SourceState state() const override; + bool remote() const override { return remote_; } + + // VideoTrackSourceInterface implementation + bool is_screencast() const override { return false; } + std::optional needs_denoising() const override { return std::nullopt; } + bool GetStats(Stats* /* stats */) override { return false; } + + void AddOrUpdateSink(webrtc::VideoSinkInterface* sink, + const webrtc::VideoSinkWants& wants) override; + void RemoveSink(webrtc::VideoSinkInterface* sink) override; + + bool SupportsEncodedOutput() const override { return false; } + void GenerateKeyFrame() override {} + void AddEncodedSink( + webrtc::VideoSinkInterface* /* sink */) override {} + void RemoveEncodedSink( + webrtc::VideoSinkInterface* /* sink */) override {} + + private: + bool remote_; + webrtc::MediaSourceInterface::SourceState state_ = webrtc::MediaSourceInterface::kLive; + }; +} diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index 09eeb78..0de4880 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -138,6 +138,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + @@ -151,6 +152,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + From 6937cb8fb9da8dc0f351fbc5d263b438d5a0390b Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Wed, 3 Jun 2026 00:05:44 -0700 Subject: [PATCH 04/10] Phase 4: Implement MediaStreamTrack constraint persistence Implement MVP constrainable behavior for MediaStreamTrack by removing NotImplementedException paths in constraint APIs. - Add per-track applied_constraints_ state in WebRtcInterop::Media::MediaStreamTrack - Initialize to empty MediaTrackConstraints in constructors - Implement GetConstraints() to return current applied constraints - Implement ApplyConstraints(null) to clear constraints to an empty set - Implement ApplyConstraints(non-null) to persist supplied constraints This provides a stable, non-throwing baseline for apps using the constraint APIs while full native capability/settings marshalling is completed later. BugzId:48 --- WebRtcInterop/Media/MediaStreamTrack.cpp | 12 ++++++++++-- WebRtcInterop/Media/MediaStreamTrack.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/WebRtcInterop/Media/MediaStreamTrack.cpp b/WebRtcInterop/Media/MediaStreamTrack.cpp index baf730b..d9c16c0 100644 --- a/WebRtcInterop/Media/MediaStreamTrack.cpp +++ b/WebRtcInterop/Media/MediaStreamTrack.cpp @@ -16,6 +16,7 @@ namespace WebRtcInterop::Media MediaStreamTrack::MediaStreamTrack() { _rpMediaStreamTrackInterface = nullptr; + applied_constraints_ = gcnew MediaTrackConstraints(); on_mute_ = nullptr; on_unmute_ = nullptr; on_ended_ = nullptr; @@ -24,6 +25,7 @@ namespace WebRtcInterop::Media MediaStreamTrack::MediaStreamTrack(webrtc::scoped_refptr track) { _rpMediaStreamTrackInterface = new webrtc::scoped_refptr(track); + applied_constraints_ = gcnew MediaTrackConstraints(); on_mute_ = nullptr; on_unmute_ = nullptr; on_ended_ = nullptr; @@ -131,7 +133,10 @@ namespace WebRtcInterop::Media MediaTrackConstraints^ MediaStreamTrack::GetConstraints() { - throw gcnew NotImplementedException(); + if (applied_constraints_ == nullptr) + applied_constraints_ = gcnew MediaTrackConstraints(); + + return applied_constraints_; } MediaTrackSettings^ MediaStreamTrack::GetSettings() @@ -141,6 +146,9 @@ namespace WebRtcInterop::Media void MediaStreamTrack::ApplyConstraints(MediaTrackConstraints^ constraints) { - throw gcnew NotImplementedException(); + if (constraints == nullptr) + applied_constraints_ = gcnew MediaTrackConstraints(); + else + applied_constraints_ = constraints; } } diff --git a/WebRtcInterop/Media/MediaStreamTrack.h b/WebRtcInterop/Media/MediaStreamTrack.h index 657d3b0..d1dd3ec 100644 --- a/WebRtcInterop/Media/MediaStreamTrack.h +++ b/WebRtcInterop/Media/MediaStreamTrack.h @@ -57,6 +57,7 @@ public ref class MediaStreamTrack : WebRtcNet::Media::MediaStreamTrack private: webrtc::scoped_refptr* _rpMediaStreamTrackInterface; + MediaTrackConstraints^ applied_constraints_; System::EventHandler^ on_mute_; System::EventHandler^ on_unmute_; System::EventHandler^ on_ended_; From e1f862cedc5a19227a8c44bdead049d5c7aaf42b Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Wed, 3 Jun 2026 00:21:36 -0700 Subject: [PATCH 05/10] MediaDevices: use WebRTC DeviceInfo for video enumeration Use Google WebRTC's own VideoCaptureFactory::CreateDeviceInfo path to enumerate Windows video input devices instead of the local stub. - Implement EnumerateVideoDevices() via VideoCaptureModule::DeviceInfo NumberOfDevices/GetDeviceName and map to MediaDeviceInfo - Populate video device id/label/groupId from WebRTC-provided unique/product ids - Fix managed->native string conversion in GetUserMedia track/stream ids using marshal_as instead of wide-char reinterpret casts - Add strmiids.lib to interop link dependencies (required by DirectShow IIDs referenced by webrtc.lib device_info_ds/video_capture_ds objects) BugzId:48 --- WebRtcInterop/Media/MediaDevices.cpp | 50 +++++++++++++++------ WebRtcInterop/WebRtcInterop.Shared.vcxitems | 2 +- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index d7b789f..3594150 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -8,6 +8,7 @@ #include "RtcPeerConnectionFactory.h" #include +#include #include #include @@ -155,17 +156,43 @@ namespace WebRtcInterop::Media try { - // Initialize COM - HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - if (FAILED(hr) && hr != S_FALSE) + std::unique_ptr deviceInfo( + webrtc::VideoCaptureFactory::CreateDeviceInfo()); + if (!deviceInfo) return devices; + const uint32_t count = deviceInfo->NumberOfDevices(); + for (uint32_t i = 0; i < count; ++i) { - // TODO: Implement video device enumeration using DirectShow or WinRT - // For now, return empty list - will be expanded in next iteration + char deviceName[512] = {}; + char deviceUniqueId[512] = {}; + char productUniqueId[512] = {}; + + const int32_t result = deviceInfo->GetDeviceName( + i, + deviceName, + sizeof(deviceName), + deviceUniqueId, + sizeof(deviceUniqueId), + productUniqueId, + sizeof(productUniqueId)); + + if (result != 0) + continue; + + String^ id = marshal_as(std::string(deviceUniqueId)); + String^ label = marshal_as(std::string(deviceName)); + String^ groupId = + productUniqueId[0] != '\0' + ? marshal_as(std::string(productUniqueId)) + : String::Empty; + + devices->Add(WebRtcNet::Media::MediaDeviceInfo::Create( + id, + WebRtcNet::Media::MediaDeviceKind::VideoInput, + label, + groupId)); } - - CoUninitialize(); } catch (...) { @@ -233,8 +260,7 @@ namespace WebRtcInterop::Media // Create a native MediaStream with a unique ID String^ managedStreamId = System::Guid::NewGuid().ToString(); - pin_ptr pwzStreamId = PtrToStringChars(managedStreamId); - std::string streamId(reinterpret_cast(pwzStreamId), managedStreamId->Length); + std::string streamId = marshal_as(managedStreamId); auto nativeStream = factory->CreateLocalMediaStream(streamId); if (!nativeStream) @@ -248,8 +274,7 @@ namespace WebRtcInterop::Media if (constraints->Audio) { String^ managedAudioLabel = "audio_" + System::Guid::NewGuid().ToString(); - pin_ptr pwzAudioLabel = PtrToStringChars(managedAudioLabel); - std::string audioLabel(reinterpret_cast(pwzAudioLabel), managedAudioLabel->Length); + std::string audioLabel = marshal_as(managedAudioLabel); auto nativeAudioTrack = factory->CreateAudioTrack(audioLabel, nullptr); if (nativeAudioTrack) @@ -264,8 +289,7 @@ namespace WebRtcInterop::Media // Create a minimal video source auto videoSource = webrtc::make_ref_counted(false); String^ managedVideoLabel = "video_" + System::Guid::NewGuid().ToString(); - pin_ptr pwzVideoLabel = PtrToStringChars(managedVideoLabel); - std::string videoLabel(reinterpret_cast(pwzVideoLabel), managedVideoLabel->Length); + std::string videoLabel = marshal_as(managedVideoLabel); auto nativeVideoTrack = factory->CreateVideoTrack(videoSource, videoLabel); if (nativeVideoTrack) diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index 0de4880..ca7c3f3 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -66,7 +66,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" %(AdditionalLibraryDirectories);$(WebRtcOutRoot)\$(Configuration)\$(Platform) - webrtc.lib;Ws2_32.lib;iphlpapi.lib;Winmm.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + webrtc.lib;Ws2_32.lib;iphlpapi.lib;Winmm.lib;strmiids.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) libcmtd;%(IgnoreSpecificDefaultLibraries) From 69aec08594c270d8c509dc68f03c690ef33c5dff Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Wed, 3 Jun 2026 00:24:49 -0700 Subject: [PATCH 06/10] MediaDevices: add device-change polling and stricter GetUserMedia errors Implement the next post-MVP MediaDevices hardening items. - Add MediaDevices lifetime management with timer-based device polling - Track known devices and raise OnDeviceChange when device snapshots differ - Populate DeviceChangeEventArgs with full current list and inserted-device subset - Enforce requested device availability in GetUserMedia (audio/video) - Convert track creation/add failures to MediaStreamException instead of returning partial/empty streams This keeps MediaDevices behavior predictable for app callers while preserving MVP capture architecture. BugzId:48 --- WebRtcInterop/Media/MediaDevices.cpp | 148 ++++++++++++++++++++++++++- WebRtcInterop/Media/MediaDevices.h | 11 ++ 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 3594150..2e33ba6 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -17,9 +17,109 @@ using namespace System::Collections::Generic; using namespace System::Threading::Tasks; +using namespace System::Timers; namespace WebRtcInterop::Media { + namespace + { + String^ GetDeviceMapKey(WebRtcNet::Media::MediaDeviceInfo^ device) + { + return String::Format("{0}|{1}", (int)device->Kind, device->DeviceId); + } + } + + MediaDevices::MediaDevices() + : known_devices_(gcnew Dictionary()), + device_poll_timer_(gcnew Timer(2000.0)), + device_poll_gate_(gcnew Object()), + on_device_change_(nullptr) + { + RefreshKnownDevices(false); + device_poll_timer_->AutoReset = true; + device_poll_timer_->Elapsed += gcnew ElapsedEventHandler(this, &MediaDevices::OnDevicePoll); + device_poll_timer_->Start(); + } + + MediaDevices::~MediaDevices() + { + this->!MediaDevices(); + } + + MediaDevices::!MediaDevices() + { + StopDevicePolling(); + } + + void MediaDevices::StopDevicePolling() + { + if (device_poll_timer_ == nullptr) + return; + + device_poll_timer_->Stop(); + device_poll_timer_->Close(); + device_poll_timer_ = nullptr; + } + + void MediaDevices::OnDevicePoll(Object^ sender, ElapsedEventArgs^ args) + { + RefreshKnownDevices(true); + } + + void MediaDevices::RefreshKnownDevices(bool raiseEvent) + { + System::Threading::Monitor::Enter(device_poll_gate_); + try + { + auto enumerateTask = EnumerateDevices(); + if (enumerateTask == nullptr) + return; + + auto devices = enumerateTask->GetAwaiter().GetResult(); + auto current = gcnew Dictionary(); + auto inserted = gcnew List(); + auto all = gcnew List(); + + for each (auto device in devices) + { + auto key = GetDeviceMapKey(device); + current[key] = device; + all->Add(device); + + if (!known_devices_->ContainsKey(key)) + inserted->Add(device); + } + + bool changed = current->Count != known_devices_->Count; + if (!changed) + { + for each (auto key in current->Keys) + { + if (!known_devices_->ContainsKey(key)) + { + changed = true; + break; + } + } + } + + known_devices_->Clear(); + for each (auto kvp in current) + known_devices_->Add(kvp.Key, kvp.Value); + + if (raiseEvent && changed && on_device_change_ != nullptr) + on_device_change_(this, gcnew WebRtcNet::Media::DeviceChangeEventArgs(all, inserted)); + } + catch (System::Exception^) + { + // Device polling must not crash the process. Failures are retried on next poll. + } + finally + { + System::Threading::Monitor::Exit(device_poll_gate_); + } + } + // Helper to enumerate Windows audio devices static List^ EnumerateAudioDevices() { @@ -249,6 +349,28 @@ namespace WebRtcInterop::Media "At least one of audio or video must be requested.")); } + if (constraints->Audio) + { + auto audioDevices = EnumerateAudioDevices(); + if (audioDevices == nullptr || audioDevices->Count == 0) + { + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException( + "No audio input devices are currently available.")); + } + } + + if (constraints->Video) + { + auto videoDevices = EnumerateVideoDevices(); + if (videoDevices == nullptr || videoDevices->Count == 0) + { + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException( + "No video input devices are currently available.")); + } + } + // Get the native peer connection factory auto factory = RtcPeerConnectionFactory::Instance->GetNativePeerConnectionFactoryInterface(true); if (factory == nullptr) @@ -277,9 +399,18 @@ namespace WebRtcInterop::Media std::string audioLabel = marshal_as(managedAudioLabel); auto nativeAudioTrack = factory->CreateAudioTrack(audioLabel, nullptr); - if (nativeAudioTrack) + if (!nativeAudioTrack) + { + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException( + "Failed to create the requested audio track.")); + } + + if (!nativeStream->AddTrack(nativeAudioTrack)) { - nativeStream->AddTrack(nativeAudioTrack); + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException( + "Failed to add the audio track to the media stream.")); } } @@ -292,9 +423,18 @@ namespace WebRtcInterop::Media std::string videoLabel = marshal_as(managedVideoLabel); auto nativeVideoTrack = factory->CreateVideoTrack(videoSource, videoLabel); - if (nativeVideoTrack) + if (!nativeVideoTrack) + { + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException( + "Failed to create the requested video track.")); + } + + if (!nativeStream->AddTrack(nativeVideoTrack)) { - nativeStream->AddTrack(nativeVideoTrack); + return Task::FromException( + gcnew WebRtcNet::Media::MediaStreamException( + "Failed to add the video track to the media stream.")); } } diff --git a/WebRtcInterop/Media/MediaDevices.h b/WebRtcInterop/Media/MediaDevices.h index 7ee6a2f..4fdc2d1 100644 --- a/WebRtcInterop/Media/MediaDevices.h +++ b/WebRtcInterop/Media/MediaDevices.h @@ -9,6 +9,10 @@ namespace WebRtcInterop::Media public ref class MediaDevices : WebRtcNet::Media::MediaDevices { public: + MediaDevices(); + ~MediaDevices(); + !MediaDevices(); + virtual event EventHandler^ OnDeviceChange { void add(EventHandler^ value) override { on_device_change_ += value; } @@ -20,6 +24,13 @@ namespace WebRtcInterop::Media virtual Task^ GetUserMedia(WebRtcNet::Media::MediaStreamConstraints^ constraints) override; private: + void RefreshKnownDevices(bool raiseEvent); + void OnDevicePoll(Object^ sender, System::Timers::ElapsedEventArgs^ args); + void StopDevicePolling(); + + System::Collections::Generic::Dictionary^ known_devices_; + System::Timers::Timer^ device_poll_timer_; + Object^ device_poll_gate_; EventHandler^ on_device_change_; }; } From 62e55f02b36cd7073742d9b060654f7cad0276d4 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Thu, 4 Jun 2026 21:57:11 -0700 Subject: [PATCH 07/10] Implement WebRTC interop media updates --- WebRtcInterop/Media/MediaDevices.cpp | 100 +++---- WebRtcInterop/Media/SimpleVideoSource.cpp | 2 +- WebRtcInterop/RtcPeerConnection.cpp | 8 +- WebRtcInterop/RtcPeerConnection.h | 8 +- WebRtcInterop/WebRtcInterop.Shared.vcxitems | 308 ++++++++++---------- 5 files changed, 213 insertions(+), 213 deletions(-) diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 2e33ba6..2cd1b15 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -23,14 +23,14 @@ namespace WebRtcInterop::Media { namespace { - String^ GetDeviceMapKey(WebRtcNet::Media::MediaDeviceInfo^ device) + String^ GetDeviceMapKey(MediaDeviceInfo^ device) { return String::Format("{0}|{1}", (int)device->Kind, device->DeviceId); } } MediaDevices::MediaDevices() - : known_devices_(gcnew Dictionary()), + : known_devices_(gcnew Dictionary()), device_poll_timer_(gcnew Timer(2000.0)), device_poll_gate_(gcnew Object()), on_device_change_(nullptr) @@ -68,7 +68,7 @@ namespace WebRtcInterop::Media void MediaDevices::RefreshKnownDevices(bool raiseEvent) { - System::Threading::Monitor::Enter(device_poll_gate_); + Threading::Monitor::Enter(device_poll_gate_); try { auto enumerateTask = EnumerateDevices(); @@ -76,9 +76,9 @@ namespace WebRtcInterop::Media return; auto devices = enumerateTask->GetAwaiter().GetResult(); - auto current = gcnew Dictionary(); - auto inserted = gcnew List(); - auto all = gcnew List(); + auto current = gcnew Dictionary(); + auto inserted = gcnew List(); + auto all = gcnew List(); for each (auto device in devices) { @@ -108,22 +108,22 @@ namespace WebRtcInterop::Media known_devices_->Add(kvp.Key, kvp.Value); if (raiseEvent && changed && on_device_change_ != nullptr) - on_device_change_(this, gcnew WebRtcNet::Media::DeviceChangeEventArgs(all, inserted)); + on_device_change_(this, gcnew DeviceChangeEventArgs(all, inserted)); } - catch (System::Exception^) + catch (Exception^) { // Device polling must not crash the process. Failures are retried on next poll. } finally { - System::Threading::Monitor::Exit(device_poll_gate_); + Threading::Monitor::Exit(device_poll_gate_); } } // Helper to enumerate Windows audio devices - static List^ EnumerateAudioDevices() + static List^ EnumerateAudioDevices() { - auto devices = gcnew List(); + auto devices = gcnew List(); try { @@ -155,10 +155,10 @@ namespace WebRtcInterop::Media LPWSTR pwszId = nullptr; if (SUCCEEDED(pDevice->GetId(&pwszId))) { - auto id = gcnew System::String(pwszId); - auto kind = WebRtcNet::Media::MediaDeviceKind::AudioInput; - auto label = gcnew System::String(L"Audio Input Device"); - auto groupId = gcnew System::String(L""); + auto id = gcnew String(pwszId); + auto kind = MediaDeviceKind::AudioInput; + auto label = gcnew String(L"Audio Input Device"); + auto groupId = gcnew String(L""); // Try to get friendly name IPropertyStore* pProps = nullptr; @@ -169,13 +169,13 @@ namespace WebRtcInterop::Media if (SUCCEEDED(pProps->GetValue(PKEY_Device_FriendlyName, &varName))) { if (varName.vt == VT_LPWSTR) - label = gcnew System::String(varName.pwszVal); + label = gcnew String(varName.pwszVal); PropVariantClear(&varName); } pProps->Release(); } - auto deviceInfo = WebRtcNet::Media::MediaDeviceInfo::Create(id, kind, label, groupId); + auto deviceInfo = MediaDeviceInfo::Create(id, kind, label, groupId); devices->Add(deviceInfo); CoTaskMemFree(pwszId); @@ -203,10 +203,10 @@ namespace WebRtcInterop::Media LPWSTR pwszId = nullptr; if (SUCCEEDED(pDevice->GetId(&pwszId))) { - auto id = gcnew System::String(pwszId); - auto kind = WebRtcNet::Media::MediaDeviceKind::AudioOutput; - auto label = gcnew System::String(L"Audio Output Device"); - auto groupId = gcnew System::String(L""); + auto id = gcnew String(pwszId); + auto kind = MediaDeviceKind::AudioOutput; + auto label = gcnew String(L"Audio Output Device"); + auto groupId = gcnew String(L""); // Try to get friendly name IPropertyStore* pProps = nullptr; @@ -217,13 +217,13 @@ namespace WebRtcInterop::Media if (SUCCEEDED(pProps->GetValue(PKEY_Device_FriendlyName, &varName))) { if (varName.vt == VT_LPWSTR) - label = gcnew System::String(varName.pwszVal); + label = gcnew String(varName.pwszVal); PropVariantClear(&varName); } pProps->Release(); } - auto deviceInfo = WebRtcNet::Media::MediaDeviceInfo::Create(id, kind, label, groupId); + auto deviceInfo = MediaDeviceInfo::Create(id, kind, label, groupId); devices->Add(deviceInfo); CoTaskMemFree(pwszId); @@ -250,9 +250,9 @@ namespace WebRtcInterop::Media } // Helper to enumerate Windows video devices using DirectShow - static List^ EnumerateVideoDevices() + static List^ EnumerateVideoDevices() { - auto devices = gcnew List(); + auto devices = gcnew List(); try { @@ -287,9 +287,9 @@ namespace WebRtcInterop::Media ? marshal_as(std::string(productUniqueId)) : String::Empty; - devices->Add(WebRtcNet::Media::MediaDeviceInfo::Create( + devices->Add(MediaDeviceInfo::Create( id, - WebRtcNet::Media::MediaDeviceKind::VideoInput, + MediaDeviceKind::VideoInput, label, groupId)); } @@ -302,11 +302,11 @@ namespace WebRtcInterop::Media return devices; } - Task^>^ MediaDevices::EnumerateDevices() + Task^>^ MediaDevices::EnumerateDevices() { try { - auto allDevices = gcnew List(); + auto allDevices = gcnew List(); // Enumerate audio devices auto audioDevices = EnumerateAudioDevices(); @@ -318,34 +318,34 @@ namespace WebRtcInterop::Media if (videoDevices != nullptr) allDevices->AddRange(videoDevices); - return Task::FromResult^>(allDevices); + return Task::FromResult^>(allDevices); } - catch (System::Exception^ ex) + catch (Exception^ ex) { - return Task::FromException^>(ex); + return Task::FromException^>(ex); } } - WebRtcNet::Media::MediaTrackSupportedConstraints^ MediaDevices::GetSupportedConstraints() + MediaTrackSupportedConstraints^ MediaDevices::GetSupportedConstraints() { - return gcnew WebRtcNet::Media::MediaTrackSupportedConstraints(); + return gcnew MediaTrackSupportedConstraints(); } - Task^ MediaDevices::GetUserMedia(WebRtcNet::Media::MediaStreamConstraints^ constraints) + Task^ MediaDevices::GetUserMedia(MediaStreamConstraints^ constraints) { try { if (constraints == nullptr) { return Task::FromException( - gcnew System::ArgumentNullException("constraints")); + gcnew ArgumentNullException("constraints")); } // At least one of audio or video must be requested if (!constraints->Audio && !constraints->Video) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "At least one of audio or video must be requested.")); } @@ -355,7 +355,7 @@ namespace WebRtcInterop::Media if (audioDevices == nullptr || audioDevices->Count == 0) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "No audio input devices are currently available.")); } } @@ -366,7 +366,7 @@ namespace WebRtcInterop::Media if (videoDevices == nullptr || videoDevices->Count == 0) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "No video input devices are currently available.")); } } @@ -376,40 +376,40 @@ namespace WebRtcInterop::Media if (factory == nullptr) { return Task::FromException( - gcnew System::InvalidOperationException( + gcnew InvalidOperationException( "PeerConnectionFactory not initialized.")); } // Create a native MediaStream with a unique ID - String^ managedStreamId = System::Guid::NewGuid().ToString(); + String^ managedStreamId = Guid::NewGuid().ToString(); std::string streamId = marshal_as(managedStreamId); auto nativeStream = factory->CreateLocalMediaStream(streamId); if (!nativeStream) { return Task::FromException( - gcnew System::InvalidOperationException( + gcnew InvalidOperationException( "Failed to create media stream.")); } // Create audio track if requested if (constraints->Audio) { - String^ managedAudioLabel = "audio_" + System::Guid::NewGuid().ToString(); + String^ managedAudioLabel = "audio_" + Guid::NewGuid().ToString(); std::string audioLabel = marshal_as(managedAudioLabel); auto nativeAudioTrack = factory->CreateAudioTrack(audioLabel, nullptr); if (!nativeAudioTrack) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "Failed to create the requested audio track.")); } if (!nativeStream->AddTrack(nativeAudioTrack)) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "Failed to add the audio track to the media stream.")); } } @@ -419,30 +419,30 @@ namespace WebRtcInterop::Media { // Create a minimal video source auto videoSource = webrtc::make_ref_counted(false); - String^ managedVideoLabel = "video_" + System::Guid::NewGuid().ToString(); + String^ managedVideoLabel = "video_" + Guid::NewGuid().ToString(); std::string videoLabel = marshal_as(managedVideoLabel); auto nativeVideoTrack = factory->CreateVideoTrack(videoSource, videoLabel); if (!nativeVideoTrack) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "Failed to create the requested video track.")); } if (!nativeStream->AddTrack(nativeVideoTrack)) { return Task::FromException( - gcnew WebRtcNet::Media::MediaStreamException( + gcnew MediaStreamException( "Failed to add the video track to the media stream.")); } } // Create managed MediaStream wrapper - auto managedStream = gcnew WebRtcInterop::Media::MediaStream(nativeStream); + auto managedStream = gcnew MediaStream(nativeStream); return Task::FromResult(managedStream); } - catch (System::Exception^ ex) + catch (Exception^ ex) { return Task::FromException(ex); } diff --git a/WebRtcInterop/Media/SimpleVideoSource.cpp b/WebRtcInterop/Media/SimpleVideoSource.cpp index 1d784aa..f076ebf 100644 --- a/WebRtcInterop/Media/SimpleVideoSource.cpp +++ b/WebRtcInterop/Media/SimpleVideoSource.cpp @@ -4,7 +4,7 @@ namespace WebRtcInterop { SimpleVideoSource::SimpleVideoSource(bool remote) - : remote_(remote), state_(webrtc::MediaSourceInterface::kLive) + : remote_(remote), state_(kLive) { } diff --git a/WebRtcInterop/RtcPeerConnection.cpp b/WebRtcInterop/RtcPeerConnection.cpp index 87322ef..dca163e 100644 --- a/WebRtcInterop/RtcPeerConnection.cpp +++ b/WebRtcInterop/RtcPeerConnection.cpp @@ -101,7 +101,7 @@ namespace WebRtcInterop is_closed_ = true; } - Task^ RtcPeerConnection::GetStats(WebRtcNet::Media::MediaStreamTrack^ selector) + Task^ RtcPeerConnection::GetStats(Media::MediaStreamTrack^ selector) { ThrowShimNotImplemented("RtcPeerConnection.GetStats"); return nullptr; @@ -122,19 +122,19 @@ namespace WebRtcInterop return gcnew List(); } - RtcRtpSender^ RtcPeerConnection::AddTrack(WebRtcNet::Media::MediaStreamTrack^ track, ... array^ streams) + RtcRtpSender^ RtcPeerConnection::AddTrack(Media::MediaStreamTrack^ track, ... array^ streams) { ThrowShimNotImplemented("RtcPeerConnection.AddTrack"); return nullptr; } - RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(WebRtcNet::Media::MediaStreamTrack^ track, RtcRtpTransceiverInit^ init) + RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(Media::MediaStreamTrack^ track, RtcRtpTransceiverInit^ init) { ThrowShimNotImplemented("RtcPeerConnection.AddTransceiver(track)"); return nullptr; } - RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(WebRtcNet::Media::MediaStreamTrackKind kind, RtcRtpTransceiverInit^ init) + RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(Media::MediaStreamTrackKind kind, RtcRtpTransceiverInit^ init) { ThrowShimNotImplemented("RtcPeerConnection.AddTransceiver(kind)"); return nullptr; diff --git a/WebRtcInterop/RtcPeerConnection.h b/WebRtcInterop/RtcPeerConnection.h index fdd9d14..43f71dc 100644 --- a/WebRtcInterop/RtcPeerConnection.h +++ b/WebRtcInterop/RtcPeerConnection.h @@ -79,14 +79,14 @@ namespace WebRtcInterop virtual Task^ AddIceCandidate([System::Runtime::InteropServices::Optional] RtcIceCandidate^ candidate) override; virtual void RestartIce() override; virtual void Close() override; - virtual Task^ GetStats([System::Runtime::InteropServices::Optional] WebRtcNet::Media::MediaStreamTrack^ selector) override; + virtual Task^ GetStats([System::Runtime::InteropServices::Optional] Media::MediaStreamTrack^ selector) override; virtual IEnumerable^ GetSenders() override; virtual IEnumerable^ GetReceivers() override; virtual IEnumerable^ GetTransceivers() override; - virtual RtcRtpSender^ AddTrack(WebRtcNet::Media::MediaStreamTrack^ track, ... array^ streams) override; - virtual RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrack^ track, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; - virtual RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrackKind kind, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; + virtual RtcRtpSender^ AddTrack(Media::MediaStreamTrack^ track, ... array^ streams) override; + virtual RtcRtpTransceiver^ AddTransceiver(Media::MediaStreamTrack^ track, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; + virtual RtcRtpTransceiver^ AddTransceiver(Media::MediaStreamTrackKind kind, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; virtual void RemoveTrack(RtcRtpSender^ sender) override; virtual RtcDataChannel^ CreateDataChannel(String^ label, [System::Runtime::InteropServices::Optional] RtcDataChannelInit^ dataChannelInit) override; diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index ca7c3f3..23bede2 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -1,169 +1,169 @@  - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - {fac2e2bf-5a09-428f-a224-1ce7a160a2d1} - - - - cd "$(WebRtcSrcRoot)" + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + {fac2e2bf-5a09-428f-a224-1ce7a160a2d1} + + + + cd "$(WebRtcSrcRoot)" echo Generating WebRtc Build Files cmd /c gn gen --ide=vs "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" --filters=//:webrtc --args=" - " + " echo Compiling WebRtc Build Files ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - use_custom_libcxx=false libcxx_is_shared=true use_lld=false is_component_build=false clang_use_chrome_plugins=false rtc_include_tests=false rtc_build_tools=false rtc_build_examples=false rtc_enable_symbol_export=true rtc_enable_protobuf=false enable_libaom=false - - - $(ProjectDir);$(IncludePath) - PreBuildEvent - - - - Use - pch.h - Level3 - stdcpp20 - stdc17 - - - - - - _DEBUG;%(PreprocessorDefinitions) - - - - - NDEBUG;%(PreprocessorDefinitions) - - - - - WIN32;%(PreprocessorDefinitions) - - - - - %(PreprocessorDefinitions) - - - - - - ARM64;%(PreprocessorDefinitions);_ITERATOR_DEBUG_LEVEL=0 - - - - - %(ExternalIncludePath);$(WebRtcSrcRoot);$(WebRtcSrcRoot)\system_wrappers;$(WebRtcSrcRoot)\third_party\abseil-cpp;$(WebRtcSrcRoot)\buildtools\third_party\libc++;$(WebRtcSrcRoot)\third_party\libyuv\include;$(WebRtcSrcRoot)\third_party\jsoncpp\source\include - TurnOffAllWarnings - %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory);$(WebRtcSrcRoot);$(WebRtcSrcRoot)\system_wrappers;$(WebRtcSrcRoot)\third_party\abseil-cpp;$(WebRtcSrcRoot)\buildtools\third_party\libc++;$(WebRtcSrcRoot)\third_party\libyuv\include;$(WebRtcSrcRoot)\third_party\jsoncpp\source\include - %(PreprocessorDefinitions);_HAS_EXCEPTIONS=0;__STD_C;_CRT_RAND_S;_CRT_SECURE_NO_DEPRECATE;_SCL_SECURE_NO_DEPRECATE;_ATL_NO_OPENGL;_WINDOWS;CERT_CHAIN_PARA_HAS_EXTRA_FIELDS;PSAPI_VERSION=2;WIN32;_SECURE_ATL;__WRL_NO_DEFAULT_LIB__;WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WIN10=_WIN32_WINNT_WIN10;WIN32_LEAN_AND_MEAN;NOMINMAX;_UNICODE;UNICODE;NTDDI_VERSION=NTDDI_WIN10_RS2;_WIN32_WINNT=0x0A00;WINVER=0x0A00;NVALGRIND;DYNAMIC_ANNOTATIONS_ENABLED=0;WEBRTC_ENABLE_PROTOBUF=0;WEBRTC_INCLUDE_INTERNAL_AUDIO_DEVICE;RTC_ENABLE_VP9;HAVE_SCTP;WEBRTC_LIBRARY_IMPL;WEBRTC_NON_STATIC_TRACE_EVENT_HANDLERS=0;WEBRTC_WIN;ABSL_ALLOCATOR_NOTHROW=1;HAVE_SCTP - - - %(AdditionalLibraryDirectories);$(WebRtcOutRoot)\$(Configuration)\$(Platform) - webrtc.lib;Ws2_32.lib;iphlpapi.lib;Winmm.lib;strmiids.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) - libcmtd;%(IgnoreSpecificDefaultLibraries) - - - - - $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + use_custom_libcxx=false libcxx_is_shared=true use_lld=false is_component_build=false clang_use_chrome_plugins=false rtc_include_tests=false rtc_build_tools=false rtc_build_examples=false rtc_enable_symbol_export=true rtc_enable_protobuf=false enable_libaom=false + + + $(ProjectDir);$(IncludePath) + PreBuildEvent + + + + Use + pch.h + Level3 + stdcpp20 + stdc17 + + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + + + + + WIN32;%(PreprocessorDefinitions) + + + + + %(PreprocessorDefinitions) + + + + + + ARM64;%(PreprocessorDefinitions);_ITERATOR_DEBUG_LEVEL=0 + + + + + %(ExternalIncludePath);$(WebRtcSrcRoot);$(WebRtcSrcRoot)\system_wrappers;$(WebRtcSrcRoot)\third_party\abseil-cpp;$(WebRtcSrcRoot)\buildtools\third_party\libc++;$(WebRtcSrcRoot)\third_party\libyuv\include;$(WebRtcSrcRoot)\third_party\jsoncpp\source\include + TurnOffAllWarnings + %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory);$(WebRtcSrcRoot);$(WebRtcSrcRoot)\system_wrappers;$(WebRtcSrcRoot)\third_party\abseil-cpp;$(WebRtcSrcRoot)\buildtools\third_party\libc++;$(WebRtcSrcRoot)\third_party\libyuv\include;$(WebRtcSrcRoot)\third_party\jsoncpp\source\include + %(PreprocessorDefinitions);_HAS_EXCEPTIONS=0;__STD_C;_CRT_RAND_S;_CRT_SECURE_NO_DEPRECATE;_SCL_SECURE_NO_DEPRECATE;_ATL_NO_OPENGL;_WINDOWS;CERT_CHAIN_PARA_HAS_EXTRA_FIELDS;PSAPI_VERSION=2;WIN32;_SECURE_ATL;__WRL_NO_DEFAULT_LIB__;WINAPI_FAMILY=WINAPI_FAMILY_DESKTOP_APP;WIN10=_WIN32_WINNT_WIN10;WIN32_LEAN_AND_MEAN;NOMINMAX;_UNICODE;UNICODE;NTDDI_VERSION=NTDDI_WIN10_RS2;_WIN32_WINNT=0x0A00;WINVER=0x0A00;NVALGRIND;DYNAMIC_ANNOTATIONS_ENABLED=0;WEBRTC_ENABLE_PROTOBUF=0;WEBRTC_INCLUDE_INTERNAL_AUDIO_DEVICE;RTC_ENABLE_VP9;HAVE_SCTP;WEBRTC_LIBRARY_IMPL;WEBRTC_NON_STATIC_TRACE_EVENT_HANDLERS=0;WEBRTC_WIN;ABSL_ALLOCATOR_NOTHROW=1;HAVE_SCTP + + + %(AdditionalLibraryDirectories);$(WebRtcOutRoot)\$(Configuration)\$(Platform) + webrtc.lib;Ws2_32.lib;iphlpapi.lib;Winmm.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + libcmtd;%(IgnoreSpecificDefaultLibraries) + + + + + $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) - Building Google WebRtc Library - $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib - - - - - $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + Building Google WebRtc Library + $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib + + + + + $(WebRtcGnCommandPrefix)target_cpu="x64" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) - Building Google WebRtc Library - $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib - - - - - $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=false enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + Building Google WebRtc Library + $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib + + + + + $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=false enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) - Building Google WebRtc Library - $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib - - - - - $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=true enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + Building Google WebRtc Library + $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib + + + + + $(WebRtcGnCommandPrefix)target_cpu="arm64" is_debug=true enable_iterator_debugging=false rtc_enable_win_wgc=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) - Building Google WebRtc Library - $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib - - - - - $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + Building Google WebRtc Library + $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib + + + + + $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=false enable_iterator_debugging=false $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) - Building Google WebRtc Library - $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib - - - - - $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) + Building Google WebRtc Library + $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib + + + + + $(WebRtcGnCommandPrefix)target_cpu="x86" is_debug=true enable_iterator_debugging=true $(WebRtcGnCommonArgs)$(WebRtcGnCommandSuffix) - Building Google WebRtc Library - $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib - - - - - - - - {98433217-c99b-4b08-a19b-3f97a1f1e633} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Create - - - + Building Google WebRtc Library + $(WebRtcOutRoot)\$(Configuration)\$(Platform)\webrtc.lib + + + + + + + + {98433217-c99b-4b08-a19b-3f97a1f1e633} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + \ No newline at end of file From 3cbf4eab279a9306584b038b89d74e07c1be9ca8 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Thu, 4 Jun 2026 23:14:37 -0700 Subject: [PATCH 08/10] Refactor media device interop and add HRESULT helper --- WebRtcInterop/InteropHResult.cpp | 20 + WebRtcInterop/InteropHResult.h | 12 + WebRtcInterop/Media/CameraVideoSource.cpp | 122 ++++++ WebRtcInterop/Media/CameraVideoSource.h | 53 +++ WebRtcInterop/Media/MediaDevices.cpp | 393 +++++++++--------- WebRtcInterop/Media/SimpleVideoSource.cpp | 26 -- WebRtcInterop/Media/SimpleVideoSource.h | 41 -- WebRtcInterop/RtcPeerConnection.cpp | 8 +- WebRtcInterop/RtcPeerConnection.h | 8 +- WebRtcInterop/WebRtcInterop.Shared.vcxitems | 8 +- .../WebRtcInterop.Shared.vcxitems.filters | 24 +- WebRtcNet.sln.DotSettings | 4 + 12 files changed, 438 insertions(+), 281 deletions(-) create mode 100644 WebRtcInterop/InteropHResult.cpp create mode 100644 WebRtcInterop/InteropHResult.h create mode 100644 WebRtcInterop/Media/CameraVideoSource.cpp create mode 100644 WebRtcInterop/Media/CameraVideoSource.h delete mode 100644 WebRtcInterop/Media/SimpleVideoSource.cpp delete mode 100644 WebRtcInterop/Media/SimpleVideoSource.h diff --git a/WebRtcInterop/InteropHResult.cpp b/WebRtcInterop/InteropHResult.cpp new file mode 100644 index 0000000..144959f --- /dev/null +++ b/WebRtcInterop/InteropHResult.cpp @@ -0,0 +1,20 @@ +#include "pch.h" + +#include "InteropHResult.h" + +using namespace System::ComponentModel; +using namespace System::Runtime::InteropServices; + +namespace WebRtcInterop +{ + void InteropHResult::ThrowIfFailed(HRESULT hr, System::String^ message) + { + if (SUCCEEDED(hr)) + return; + + if (HRESULT_FACILITY(hr) == FACILITY_WIN32) + throw gcnew Win32Exception(HRESULT_CODE(hr), message); + + throw gcnew COMException(message, hr); + } +} diff --git a/WebRtcInterop/InteropHResult.h b/WebRtcInterop/InteropHResult.h new file mode 100644 index 0000000..e557bf1 --- /dev/null +++ b/WebRtcInterop/InteropHResult.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace WebRtcInterop +{ + public ref class InteropHResult abstract sealed + { + public: + static void ThrowIfFailed(HRESULT hr, System::String^ message); + }; +} diff --git a/WebRtcInterop/Media/CameraVideoSource.cpp b/WebRtcInterop/Media/CameraVideoSource.cpp new file mode 100644 index 0000000..304a21f --- /dev/null +++ b/WebRtcInterop/Media/CameraVideoSource.cpp @@ -0,0 +1,122 @@ +#include "pch.h" + +#include "CameraVideoSource.h" + +#include +#include +#include + +namespace webrtc +{ + VideoCaptureCapability SelectCapability(const std::string& deviceId) + { + VideoCaptureCapability requested; + requested.width = 1280; + requested.height = 720; + requested.maxFPS = 30; + + VideoCaptureCapability selected = requested; + std::unique_ptr deviceInfo( + VideoCaptureFactory::CreateDeviceInfo()); + if (!deviceInfo) + return selected; + + VideoCaptureCapability matched; + if (deviceInfo->GetBestMatchedCapability(deviceId.c_str(), requested, matched) >= 0) + selected = matched; + + return selected; + } +} + +namespace WebRtcInterop +{ + CameraVideoSource::CameraVideoSource(const webrtc::scoped_refptr& captureModule) + : state_(static_cast(kInitializing)), + stopping_(false), + capture_started_(false), + capture_module_(captureModule) + { + } + + CameraVideoSource::~CameraVideoSource() + { + Stop(); + } + + webrtc::scoped_refptr CameraVideoSource::Create(const std::string& deviceId) + { + if (deviceId.empty()) + return nullptr; + + auto captureModule = webrtc::VideoCaptureFactory::Create(deviceId.c_str()); + if (!captureModule) + return nullptr; + + auto source = webrtc::make_ref_counted(captureModule); + if (!source->Start(deviceId)) + return nullptr; + + return source; + } + + webrtc::MediaSourceInterface::SourceState CameraVideoSource::state() const + { + return static_cast(state_.load()); + } + + void CameraVideoSource::AddOrUpdateSink(VideoSinkInterface* sink, + const webrtc::VideoSinkWants& wants) + { + broadcaster_.AddOrUpdateSink(sink, wants); + } + + void CameraVideoSource::RemoveSink(VideoSinkInterface* sink) + { + broadcaster_.RemoveSink(sink); + } + + void CameraVideoSource::OnFrame(const webrtc::VideoFrame& frame) + { + if (stopping_.load()) + return; + + broadcaster_.OnFrame(frame); + } + + bool CameraVideoSource::Start(const std::string& deviceId) + { + if (!capture_module_) + return false; + + const auto capability = webrtc::SelectCapability(deviceId); + capture_module_->RegisterCaptureDataCallback(this); + if (capture_module_->StartCapture(capability) != 0) + { + capture_module_->DeRegisterCaptureDataCallback(); + state_.store(kEnded); + return false; + } + + capture_started_.store(true); + state_.store(kLive); + return true; + } + + void CameraVideoSource::Stop() + { + if (stopping_.exchange(true)) + return; + + if (capture_module_) + { + if (capture_started_.exchange(false)) + capture_module_->StopCapture(); + + capture_module_->DeRegisterCaptureDataCallback(); + capture_module_ = nullptr; + } + + state_.store(kEnded); + } +} diff --git a/WebRtcInterop/Media/CameraVideoSource.h b/WebRtcInterop/Media/CameraVideoSource.h new file mode 100644 index 0000000..6335ed5 --- /dev/null +++ b/WebRtcInterop/Media/CameraVideoSource.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace WebRtcInterop +{ + class CameraVideoSource : public webrtc::Notifier, + public webrtc::VideoSinkInterface + { + public: + static webrtc::scoped_refptr Create(const std::string& deviceId); + explicit CameraVideoSource(const webrtc::scoped_refptr& captureModule); + ~CameraVideoSource() override; + + SourceState state() const override; + bool remote() const override { return false; } + bool is_screencast() const override { return false; } + std::optional needs_denoising() const override { return std::nullopt; } + bool GetStats(Stats* /* stats */) override { return false; } + + void AddOrUpdateSink(VideoSinkInterface* sink, + const webrtc::VideoSinkWants& wants) override; + void RemoveSink(VideoSinkInterface* sink) override; + + bool SupportsEncodedOutput() const override { return false; } + void GenerateKeyFrame() override {} + void AddEncodedSink( + VideoSinkInterface* /* sink */) override {} + void RemoveEncodedSink( + VideoSinkInterface* /* sink */) override {} + + void OnFrame(const webrtc::VideoFrame& frame) override; + + private: + bool Start(const std::string& deviceId); + void Stop(); + + std::atomic state_; + std::atomic stopping_; + std::atomic capture_started_; + webrtc::scoped_refptr capture_module_; + webrtc::VideoBroadcaster broadcaster_; + }; +} diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index 2cd1b15..c065557 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -1,9 +1,11 @@ #include "pch.h" +#include #include "MediaDevices.h" +#include "CameraVideoSource.h" +#include "InteropHResult.h" #include "MediaStream.h" #include "MediaStreamTrack.h" -#include "SimpleVideoSource.h" #include "Marshaling/MarshalMedia.h" #include "RtcPeerConnectionFactory.h" @@ -21,12 +23,175 @@ using namespace System::Timers; namespace WebRtcInterop::Media { + static List^ EnumerateAudioDevices(); + static List^ EnumerateVideoDevices(); + namespace { String^ GetDeviceMapKey(MediaDeviceInfo^ device) { return String::Format("{0}|{1}", (int)device->Kind, device->DeviceId); } + + String^ GetAudioDeviceLabel(IMMDevice* device, String^ fallbackLabel) + { + auto label = fallbackLabel; + IPropertyStore* propertyStore = nullptr; + if (FAILED(device->OpenPropertyStore(STGM_READ, &propertyStore)) || propertyStore == nullptr) + return label; + + PROPVARIANT friendlyName; + PropVariantInit(&friendlyName); + if (SUCCEEDED(propertyStore->GetValue(PKEY_Device_FriendlyName, &friendlyName)) && + friendlyName.vt == VT_LPWSTR) + label = gcnew String(friendlyName.pwszVal); + + PropVariantClear(&friendlyName); + propertyStore->Release(); + return label; + } + + void EnumerateAudioEndpoints( + IMMDeviceEnumerator* enumerator, + const EDataFlow flow, + const MediaDeviceKind kind, + String^ fallbackLabel, + List^ devices) + { + IMMDeviceCollection* collection = nullptr; + auto hr = enumerator->EnumAudioEndpoints(flow, DEVICE_STATE_ACTIVE, &collection); + if (FAILED(hr) || collection == nullptr) + return; + + UINT count = 0; + hr = collection->GetCount(&count); + if (FAILED(hr)) + { + collection->Release(); + return; + } + + for (UINT i = 0; i < count; ++i) + { + IMMDevice* device = nullptr; + if (FAILED(collection->Item(i, &device)) || device == nullptr) + continue; + + LPWSTR deviceId = nullptr; + if (SUCCEEDED(device->GetId(&deviceId))) + { + auto id = gcnew String(deviceId); + auto label = GetAudioDeviceLabel(device, fallbackLabel); + devices->Add(MediaDeviceInfo::Create(id, kind, label, String::Empty)); + CoTaskMemFree(deviceId); + } + + device->Release(); + } + + collection->Release(); + } + + void ValidateGetUserMediaConstraints(MediaStreamConstraints^ constraints) + { + if (constraints == nullptr) + throw gcnew ArgumentNullException("constraints"); + + if (!constraints->Audio && !constraints->Video) + throw gcnew MediaStreamException("At least one of audio or video must be requested."); + } + + void ValidateRequestedAudioDevices(MediaStreamConstraints^ constraints) + { + if (!constraints->Audio) + return; + + auto audioDevices = EnumerateAudioDevices(); + if (audioDevices == nullptr || audioDevices->Count == 0) + throw gcnew MediaStreamException("No audio input devices are currently available."); + } + + List^ ValidateAndGetRequestedVideoDevices(MediaStreamConstraints^ constraints) + { + if (!constraints->Video) + return nullptr; + + auto videoDevices = EnumerateVideoDevices(); + if (videoDevices == nullptr || videoDevices->Count == 0) + throw gcnew MediaStreamException("No video input devices are currently available."); + + return videoDevices; + } + + std::string CreateGuidLabel(String^ prefix) + { + return marshal_as(prefix + "_" + Guid::NewGuid().ToString()); + } + + webrtc::scoped_refptr CreateNativeStream( + webrtc::PeerConnectionFactoryInterface* factory) + { + auto streamId = marshal_as(Guid::NewGuid().ToString()); + auto nativeStream = factory->CreateLocalMediaStream(streamId); + if (!nativeStream) + throw gcnew InvalidOperationException("Failed to create media stream."); + + return nativeStream; + } + + void AddAudioTrack( + webrtc::PeerConnectionFactoryInterface* factory, + webrtc::scoped_refptr nativeStream) + { + webrtc::AudioOptions audioOptions; + auto nativeAudioSource = factory->CreateAudioSource(audioOptions); + if (!nativeAudioSource) + throw gcnew MediaStreamException("Failed to create an audio source for the requested track."); + + auto nativeAudioTrack = factory->CreateAudioTrack( + CreateGuidLabel(gcnew String(L"audio")), + nativeAudioSource.get()); + if (!nativeAudioTrack) + throw gcnew MediaStreamException("Failed to create the requested audio track."); + + if (!nativeStream->AddTrack(nativeAudioTrack)) + throw gcnew MediaStreamException("Failed to add the audio track to the media stream."); + } + + webrtc::scoped_refptr CreateVideoSource( + List^ videoDevices) + { + for each (auto videoDevice in videoDevices) + { + if (videoDevice == nullptr || + String::IsNullOrEmpty(videoDevice->DeviceId) || + videoDevice->Kind != MediaDeviceKind::VideoInput) + continue; + + auto videoDeviceId = marshal_as(videoDevice->DeviceId); + auto candidateSource = CameraVideoSource::Create(videoDeviceId); + if (candidateSource) + return candidateSource; + } + + throw gcnew MediaStreamException("Failed to create a camera-backed video source for the requested track."); + } + + void AddVideoTrack( + webrtc::PeerConnectionFactoryInterface* factory, + webrtc::scoped_refptr nativeStream, + List^ videoDevices) + { + auto videoSource = CreateVideoSource(videoDevices); + auto nativeVideoTrack = factory->CreateVideoTrack( + videoSource, + CreateGuidLabel(gcnew String(L"video"))); + if (!nativeVideoTrack) + throw gcnew MediaStreamException("Failed to create the requested video track."); + + if (!nativeStream->AddTrack(nativeVideoTrack)) + throw gcnew MediaStreamException("Failed to add the video track to the media stream."); + } } MediaDevices::MediaDevices() @@ -66,7 +231,7 @@ namespace WebRtcInterop::Media RefreshKnownDevices(true); } - void MediaDevices::RefreshKnownDevices(bool raiseEvent) + void MediaDevices::RefreshKnownDevices(const bool raiseEvent) { Threading::Monitor::Enter(device_poll_gate_); try @@ -132,111 +297,25 @@ namespace WebRtcInterop::Media if (FAILED(hr) && hr != S_FALSE) return devices; // COM already initialized or failed - { - IMMDeviceEnumerator* pEnumerator = nullptr; - hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, - __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator); - - if (SUCCEEDED(hr) && pEnumerator != nullptr) - { - // Enumerate audio inputs - { - IMMDeviceCollection* pCollection = nullptr; - hr = pEnumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &pCollection); - if (SUCCEEDED(hr) && pCollection != nullptr) - { - UINT count = 0; - pCollection->GetCount(&count); - for (UINT i = 0; i < count; ++i) - { - IMMDevice* pDevice = nullptr; - if (SUCCEEDED(pCollection->Item(i, &pDevice)) && pDevice != nullptr) - { - LPWSTR pwszId = nullptr; - if (SUCCEEDED(pDevice->GetId(&pwszId))) - { - auto id = gcnew String(pwszId); - auto kind = MediaDeviceKind::AudioInput; - auto label = gcnew String(L"Audio Input Device"); - auto groupId = gcnew String(L""); - - // Try to get friendly name - IPropertyStore* pProps = nullptr; - if (SUCCEEDED(pDevice->OpenPropertyStore(STGM_READ, &pProps)) && pProps != nullptr) - { - PROPVARIANT varName; - PropVariantInit(&varName); - if (SUCCEEDED(pProps->GetValue(PKEY_Device_FriendlyName, &varName))) - { - if (varName.vt == VT_LPWSTR) - label = gcnew String(varName.pwszVal); - PropVariantClear(&varName); - } - pProps->Release(); - } - - auto deviceInfo = MediaDeviceInfo::Create(id, kind, label, groupId); - devices->Add(deviceInfo); - - CoTaskMemFree(pwszId); - } - pDevice->Release(); - } - } - pCollection->Release(); - } - } - - // Enumerate audio outputs - { - IMMDeviceCollection* pCollection = nullptr; - hr = pEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &pCollection); - if (SUCCEEDED(hr) && pCollection != nullptr) - { - UINT count = 0; - pCollection->GetCount(&count); - for (UINT i = 0; i < count; ++i) - { - IMMDevice* pDevice = nullptr; - if (SUCCEEDED(pCollection->Item(i, &pDevice)) && pDevice != nullptr) - { - LPWSTR pwszId = nullptr; - if (SUCCEEDED(pDevice->GetId(&pwszId))) - { - auto id = gcnew String(pwszId); - auto kind = MediaDeviceKind::AudioOutput; - auto label = gcnew String(L"Audio Output Device"); - auto groupId = gcnew String(L""); - - // Try to get friendly name - IPropertyStore* pProps = nullptr; - if (SUCCEEDED(pDevice->OpenPropertyStore(STGM_READ, &pProps)) && pProps != nullptr) - { - PROPVARIANT varName; - PropVariantInit(&varName); - if (SUCCEEDED(pProps->GetValue(PKEY_Device_FriendlyName, &varName))) - { - if (varName.vt == VT_LPWSTR) - label = gcnew String(varName.pwszVal); - PropVariantClear(&varName); - } - pProps->Release(); - } - - auto deviceInfo = MediaDeviceInfo::Create(id, kind, label, groupId); - devices->Add(deviceInfo); - - CoTaskMemFree(pwszId); - } - pDevice->Release(); - } - } - pCollection->Release(); - } - } + IMMDeviceEnumerator* enumerator = nullptr; + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, + __uuidof(IMMDeviceEnumerator), reinterpret_cast(&enumerator)); - pEnumerator->Release(); - } + if (SUCCEEDED(hr) && enumerator != nullptr) + { + EnumerateAudioEndpoints( + enumerator, + eCapture, + MediaDeviceKind::AudioInput, + gcnew String(L"Audio Input Device"), + devices); + EnumerateAudioEndpoints( + enumerator, + eRender, + MediaDeviceKind::AudioOutput, + gcnew String(L"Audio Output Device"), + devices); + enumerator->Release(); } CoUninitialize(); @@ -280,8 +359,8 @@ namespace WebRtcInterop::Media if (result != 0) continue; - String^ id = marshal_as(std::string(deviceUniqueId)); - String^ label = marshal_as(std::string(deviceName)); + auto id = marshal_as(std::string(deviceUniqueId)); + auto label = marshal_as(std::string(deviceName)); String^ groupId = productUniqueId[0] != '\0' ? marshal_as(std::string(productUniqueId)) @@ -335,108 +414,24 @@ namespace WebRtcInterop::Media { try { - if (constraints == nullptr) - { - return Task::FromException( - gcnew ArgumentNullException("constraints")); - } - - // At least one of audio or video must be requested - if (!constraints->Audio && !constraints->Video) - { - return Task::FromException( - gcnew MediaStreamException( - "At least one of audio or video must be requested.")); - } - - if (constraints->Audio) - { - auto audioDevices = EnumerateAudioDevices(); - if (audioDevices == nullptr || audioDevices->Count == 0) - { - return Task::FromException( - gcnew MediaStreamException( - "No audio input devices are currently available.")); - } - } - - if (constraints->Video) - { - auto videoDevices = EnumerateVideoDevices(); - if (videoDevices == nullptr || videoDevices->Count == 0) - { - return Task::FromException( - gcnew MediaStreamException( - "No video input devices are currently available.")); - } - } + ValidateGetUserMediaConstraints(constraints); + ValidateRequestedAudioDevices(constraints); + auto videoDevices = ValidateAndGetRequestedVideoDevices(constraints); // Get the native peer connection factory auto factory = RtcPeerConnectionFactory::Instance->GetNativePeerConnectionFactoryInterface(true); if (factory == nullptr) - { - return Task::FromException( - gcnew InvalidOperationException( - "PeerConnectionFactory not initialized.")); - } + throw gcnew InvalidOperationException("PeerConnectionFactory not initialized."); - // Create a native MediaStream with a unique ID - String^ managedStreamId = Guid::NewGuid().ToString(); - std::string streamId = marshal_as(managedStreamId); - - auto nativeStream = factory->CreateLocalMediaStream(streamId); - if (!nativeStream) - { - return Task::FromException( - gcnew InvalidOperationException( - "Failed to create media stream.")); - } + auto nativeStream = CreateNativeStream(factory); // Create audio track if requested if (constraints->Audio) - { - String^ managedAudioLabel = "audio_" + Guid::NewGuid().ToString(); - std::string audioLabel = marshal_as(managedAudioLabel); - - auto nativeAudioTrack = factory->CreateAudioTrack(audioLabel, nullptr); - if (!nativeAudioTrack) - { - return Task::FromException( - gcnew MediaStreamException( - "Failed to create the requested audio track.")); - } - - if (!nativeStream->AddTrack(nativeAudioTrack)) - { - return Task::FromException( - gcnew MediaStreamException( - "Failed to add the audio track to the media stream.")); - } - } + AddAudioTrack(factory, nativeStream); // Create video track if requested if (constraints->Video) - { - // Create a minimal video source - auto videoSource = webrtc::make_ref_counted(false); - String^ managedVideoLabel = "video_" + Guid::NewGuid().ToString(); - std::string videoLabel = marshal_as(managedVideoLabel); - - auto nativeVideoTrack = factory->CreateVideoTrack(videoSource, videoLabel); - if (!nativeVideoTrack) - { - return Task::FromException( - gcnew MediaStreamException( - "Failed to create the requested video track.")); - } - - if (!nativeStream->AddTrack(nativeVideoTrack)) - { - return Task::FromException( - gcnew MediaStreamException( - "Failed to add the video track to the media stream.")); - } - } + AddVideoTrack(factory, nativeStream, videoDevices); // Create managed MediaStream wrapper auto managedStream = gcnew MediaStream(nativeStream); diff --git a/WebRtcInterop/Media/SimpleVideoSource.cpp b/WebRtcInterop/Media/SimpleVideoSource.cpp deleted file mode 100644 index f076ebf..0000000 --- a/WebRtcInterop/Media/SimpleVideoSource.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "pch.h" -#include "SimpleVideoSource.h" - -namespace WebRtcInterop -{ - SimpleVideoSource::SimpleVideoSource(bool remote) - : remote_(remote), state_(kLive) - { - } - - webrtc::MediaSourceInterface::SourceState SimpleVideoSource::state() const - { - return state_; - } - - void SimpleVideoSource::AddOrUpdateSink(webrtc::VideoSinkInterface* sink, - const webrtc::VideoSinkWants& wants) - { - // Stub: no-op for MVP. Can be extended to route frames to sink if capturing is implemented. - } - - void SimpleVideoSource::RemoveSink(webrtc::VideoSinkInterface* sink) - { - // Stub: no-op for MVP. - } -} diff --git a/WebRtcInterop/Media/SimpleVideoSource.h b/WebRtcInterop/Media/SimpleVideoSource.h deleted file mode 100644 index 0d16a47..0000000 --- a/WebRtcInterop/Media/SimpleVideoSource.h +++ /dev/null @@ -1,41 +0,0 @@ -#pragma once - -#include -#include - -namespace WebRtcInterop -{ - // Minimal VideoTrackSourceInterface implementation for GetUserMedia - // This is a stub that satisfies the interface requirements without actual video capture. - // Real capture (DirectShow, WinRT) can be implemented later. - class SimpleVideoSource : public webrtc::Notifier - { - public: - explicit SimpleVideoSource(bool remote = false); - ~SimpleVideoSource() override = default; - - // MediaSourceInterface implementation - webrtc::MediaSourceInterface::SourceState state() const override; - bool remote() const override { return remote_; } - - // VideoTrackSourceInterface implementation - bool is_screencast() const override { return false; } - std::optional needs_denoising() const override { return std::nullopt; } - bool GetStats(Stats* /* stats */) override { return false; } - - void AddOrUpdateSink(webrtc::VideoSinkInterface* sink, - const webrtc::VideoSinkWants& wants) override; - void RemoveSink(webrtc::VideoSinkInterface* sink) override; - - bool SupportsEncodedOutput() const override { return false; } - void GenerateKeyFrame() override {} - void AddEncodedSink( - webrtc::VideoSinkInterface* /* sink */) override {} - void RemoveEncodedSink( - webrtc::VideoSinkInterface* /* sink */) override {} - - private: - bool remote_; - webrtc::MediaSourceInterface::SourceState state_ = webrtc::MediaSourceInterface::kLive; - }; -} diff --git a/WebRtcInterop/RtcPeerConnection.cpp b/WebRtcInterop/RtcPeerConnection.cpp index dca163e..87322ef 100644 --- a/WebRtcInterop/RtcPeerConnection.cpp +++ b/WebRtcInterop/RtcPeerConnection.cpp @@ -101,7 +101,7 @@ namespace WebRtcInterop is_closed_ = true; } - Task^ RtcPeerConnection::GetStats(Media::MediaStreamTrack^ selector) + Task^ RtcPeerConnection::GetStats(WebRtcNet::Media::MediaStreamTrack^ selector) { ThrowShimNotImplemented("RtcPeerConnection.GetStats"); return nullptr; @@ -122,19 +122,19 @@ namespace WebRtcInterop return gcnew List(); } - RtcRtpSender^ RtcPeerConnection::AddTrack(Media::MediaStreamTrack^ track, ... array^ streams) + RtcRtpSender^ RtcPeerConnection::AddTrack(WebRtcNet::Media::MediaStreamTrack^ track, ... array^ streams) { ThrowShimNotImplemented("RtcPeerConnection.AddTrack"); return nullptr; } - RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(Media::MediaStreamTrack^ track, RtcRtpTransceiverInit^ init) + RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(WebRtcNet::Media::MediaStreamTrack^ track, RtcRtpTransceiverInit^ init) { ThrowShimNotImplemented("RtcPeerConnection.AddTransceiver(track)"); return nullptr; } - RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(Media::MediaStreamTrackKind kind, RtcRtpTransceiverInit^ init) + RtcRtpTransceiver^ RtcPeerConnection::AddTransceiver(WebRtcNet::Media::MediaStreamTrackKind kind, RtcRtpTransceiverInit^ init) { ThrowShimNotImplemented("RtcPeerConnection.AddTransceiver(kind)"); return nullptr; diff --git a/WebRtcInterop/RtcPeerConnection.h b/WebRtcInterop/RtcPeerConnection.h index 43f71dc..fdd9d14 100644 --- a/WebRtcInterop/RtcPeerConnection.h +++ b/WebRtcInterop/RtcPeerConnection.h @@ -79,14 +79,14 @@ namespace WebRtcInterop virtual Task^ AddIceCandidate([System::Runtime::InteropServices::Optional] RtcIceCandidate^ candidate) override; virtual void RestartIce() override; virtual void Close() override; - virtual Task^ GetStats([System::Runtime::InteropServices::Optional] Media::MediaStreamTrack^ selector) override; + virtual Task^ GetStats([System::Runtime::InteropServices::Optional] WebRtcNet::Media::MediaStreamTrack^ selector) override; virtual IEnumerable^ GetSenders() override; virtual IEnumerable^ GetReceivers() override; virtual IEnumerable^ GetTransceivers() override; - virtual RtcRtpSender^ AddTrack(Media::MediaStreamTrack^ track, ... array^ streams) override; - virtual RtcRtpTransceiver^ AddTransceiver(Media::MediaStreamTrack^ track, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; - virtual RtcRtpTransceiver^ AddTransceiver(Media::MediaStreamTrackKind kind, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; + virtual RtcRtpSender^ AddTrack(WebRtcNet::Media::MediaStreamTrack^ track, ... array^ streams) override; + virtual RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrack^ track, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; + virtual RtcRtpTransceiver^ AddTransceiver(WebRtcNet::Media::MediaStreamTrackKind kind, [System::Runtime::InteropServices::Optional] RtcRtpTransceiverInit^ init) override; virtual void RemoveTrack(RtcRtpSender^ sender) override; virtual RtcDataChannel^ CreateDataChannel(String^ label, [System::Runtime::InteropServices::Optional] RtcDataChannelInit^ dataChannelInit) override; diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index 23bede2..c0f5201 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -66,7 +66,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" %(AdditionalLibraryDirectories);$(WebRtcOutRoot)\$(Configuration)\$(Platform) - webrtc.lib;Ws2_32.lib;iphlpapi.lib;Winmm.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + webrtc.lib;Ws2_32.lib;iphlpapi.lib;Winmm.lib;strmiids.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) libcmtd;%(IgnoreSpecificDefaultLibraries) @@ -128,17 +128,18 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + + - @@ -148,11 +149,12 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" + + - diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters index e955fac..61b500f 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters @@ -16,6 +16,7 @@ + Media @@ -28,13 +29,21 @@ - - + + Media + + + Media + + + Media + + Marshaling @@ -67,9 +76,16 @@ - - + + Media + + + Media + + + Media + \ No newline at end of file diff --git a/WebRtcNet.sln.DotSettings b/WebRtcNet.sln.DotSettings index f705b35..ab2c67a 100644 --- a/WebRtcNet.sln.DotSettings +++ b/WebRtcNet.sln.DotSettings @@ -13,6 +13,10 @@ Tab 2 False + <NamingElement Priority="7" Title="Local variables"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="local variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement> + <NamingElement Priority="6" Title="Parameters"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="function parameter" /><type Name="lambda parameter" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement> + <NamingElement Priority="10" Title="Global functions"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> + <NamingElement Priority="11" Title="Class and struct methods"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="member function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> True True True From 43b650d52373742752f59ad6a513fd58b279dde3 Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Thu, 4 Jun 2026 23:19:11 -0700 Subject: [PATCH 09/10] Code cleanup in WebRtcInterop::Media::MediaDevice. --- WebRtcInterop/Media/MediaDevices.cpp | 25 ++++++++++++------------- WebRtcNet.sln.DotSettings | 1 + 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/WebRtcInterop/Media/MediaDevices.cpp b/WebRtcInterop/Media/MediaDevices.cpp index c065557..b63c7ef 100644 --- a/WebRtcInterop/Media/MediaDevices.cpp +++ b/WebRtcInterop/Media/MediaDevices.cpp @@ -131,7 +131,7 @@ namespace WebRtcInterop::Media webrtc::scoped_refptr CreateNativeStream( webrtc::PeerConnectionFactoryInterface* factory) { - auto streamId = marshal_as(Guid::NewGuid().ToString()); + const auto streamId = marshal_as(Guid::NewGuid().ToString()); auto nativeStream = factory->CreateLocalMediaStream(streamId); if (!nativeStream) throw gcnew InvalidOperationException("Failed to create media stream."); @@ -141,14 +141,14 @@ namespace WebRtcInterop::Media void AddAudioTrack( webrtc::PeerConnectionFactoryInterface* factory, - webrtc::scoped_refptr nativeStream) + const webrtc::scoped_refptr& nativeStream) { - webrtc::AudioOptions audioOptions; - auto nativeAudioSource = factory->CreateAudioSource(audioOptions); + const webrtc::AudioOptions audioOptions; + const auto nativeAudioSource = factory->CreateAudioSource(audioOptions); if (!nativeAudioSource) throw gcnew MediaStreamException("Failed to create an audio source for the requested track."); - auto nativeAudioTrack = factory->CreateAudioTrack( + const auto nativeAudioTrack = factory->CreateAudioTrack( CreateGuidLabel(gcnew String(L"audio")), nativeAudioSource.get()); if (!nativeAudioTrack) @@ -169,8 +169,7 @@ namespace WebRtcInterop::Media continue; auto videoDeviceId = marshal_as(videoDevice->DeviceId); - auto candidateSource = CameraVideoSource::Create(videoDeviceId); - if (candidateSource) + if (auto candidateSource = CameraVideoSource::Create(videoDeviceId)) return candidateSource; } @@ -179,11 +178,11 @@ namespace WebRtcInterop::Media void AddVideoTrack( webrtc::PeerConnectionFactoryInterface* factory, - webrtc::scoped_refptr nativeStream, + const webrtc::scoped_refptr& nativeStream, List^ videoDevices) { - auto videoSource = CreateVideoSource(videoDevices); - auto nativeVideoTrack = factory->CreateVideoTrack( + const auto videoSource = CreateVideoSource(videoDevices); + const auto nativeVideoTrack = factory->CreateVideoTrack( videoSource, CreateGuidLabel(gcnew String(L"video"))); if (!nativeVideoTrack) @@ -335,7 +334,7 @@ namespace WebRtcInterop::Media try { - std::unique_ptr deviceInfo( + const std::unique_ptr deviceInfo( webrtc::VideoCaptureFactory::CreateDeviceInfo()); if (!deviceInfo) return devices; @@ -419,11 +418,11 @@ namespace WebRtcInterop::Media auto videoDevices = ValidateAndGetRequestedVideoDevices(constraints); // Get the native peer connection factory - auto factory = RtcPeerConnectionFactory::Instance->GetNativePeerConnectionFactoryInterface(true); + const auto factory = RtcPeerConnectionFactory::Instance->GetNativePeerConnectionFactoryInterface(true); if (factory == nullptr) throw gcnew InvalidOperationException("PeerConnectionFactory not initialized."); - auto nativeStream = CreateNativeStream(factory); + const auto nativeStream = CreateNativeStream(factory); // Create audio track if requested if (constraints->Audio) diff --git a/WebRtcNet.sln.DotSettings b/WebRtcNet.sln.DotSettings index ab2c67a..9671773 100644 --- a/WebRtcNet.sln.DotSettings +++ b/WebRtcNet.sln.DotSettings @@ -15,6 +15,7 @@ False <NamingElement Priority="7" Title="Local variables"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="local variable" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement> <NamingElement Priority="6" Title="Parameters"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="function parameter" /><type Name="lambda parameter" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="aaBb" /></NamingElement> + <NamingElement Priority="18" Title="Namespaces"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="namespace" /><type Name="namespace alias" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> <NamingElement Priority="10" Title="Global functions"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="global function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> <NamingElement Priority="11" Title="Class and struct methods"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="member function" /></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /></NamingElement> True From 89a133149f14c53894adfb255b6d518708aaf44b Mon Sep 17 00:00:00 2001 From: Cody Barnes Date: Thu, 4 Jun 2026 23:36:57 -0700 Subject: [PATCH 10/10] Move CameraVideoSource out of Media folder BugzId:48 --- .../{Media => }/CameraVideoSource.cpp | 83 ++++++++++++++----- WebRtcInterop/{Media => }/CameraVideoSource.h | 0 WebRtcInterop/WebRtcInterop.Shared.vcxitems | 4 +- .../WebRtcInterop.Shared.vcxitems.filters | 8 +- 4 files changed, 67 insertions(+), 28 deletions(-) rename WebRtcInterop/{Media => }/CameraVideoSource.cpp (50%) rename WebRtcInterop/{Media => }/CameraVideoSource.h (100%) diff --git a/WebRtcInterop/Media/CameraVideoSource.cpp b/WebRtcInterop/CameraVideoSource.cpp similarity index 50% rename from WebRtcInterop/Media/CameraVideoSource.cpp rename to WebRtcInterop/CameraVideoSource.cpp index 304a21f..b660c53 100644 --- a/WebRtcInterop/Media/CameraVideoSource.cpp +++ b/WebRtcInterop/CameraVideoSource.cpp @@ -5,27 +5,67 @@ #include #include #include +#include namespace webrtc { - VideoCaptureCapability SelectCapability(const std::string& deviceId) + VideoCaptureCapability CreateCapability(const int32_t width, const int32_t height, const int32_t fps) { - VideoCaptureCapability requested; - requested.width = 1280; - requested.height = 720; - requested.maxFPS = 30; + VideoCaptureCapability capability; + capability.width = width; + capability.height = height; + capability.maxFPS = fps; + return capability; + } - VideoCaptureCapability selected = requested; - std::unique_ptr deviceInfo( + bool AreCapabilitiesEqual(const VideoCaptureCapability& left, const VideoCaptureCapability& right) + { + return left.width == right.width && + left.height == right.height && + left.maxFPS == right.maxFPS && + left.videoType == right.videoType && + left.interlaced == right.interlaced; + } + + void AddCapabilityIfMissing(std::vector& capabilities, const VideoCaptureCapability& candidate) + { + for (const auto& existing : capabilities) + { + if (AreCapabilitiesEqual(existing, candidate)) + return; + } + + capabilities.push_back(candidate); + } + + std::vector BuildCaptureCapabilityCandidates(const std::string& deviceId) + { + std::vector candidates; + std::vector requested = { + CreateCapability(1280, 720, 30), + CreateCapability(1280, 720, 15), + CreateCapability(960, 540, 30), + CreateCapability(640, 480, 30), + CreateCapability(640, 480, 15), + CreateCapability(320, 240, 30) + }; + + const std::unique_ptr deviceInfo( VideoCaptureFactory::CreateDeviceInfo()); if (!deviceInfo) - return selected; + return requested; + + for (const auto& capability : requested) + { + VideoCaptureCapability matched; + if (deviceInfo->GetBestMatchedCapability(deviceId.c_str(), capability, matched) >= 0) + AddCapabilityIfMissing(candidates, matched); + } - VideoCaptureCapability matched; - if (deviceInfo->GetBestMatchedCapability(deviceId.c_str(), requested, matched) >= 0) - selected = matched; + for (const auto& capability : requested) + AddCapabilityIfMissing(candidates, capability); - return selected; + return candidates; } } @@ -89,18 +129,21 @@ namespace WebRtcInterop if (!capture_module_) return false; - const auto capability = webrtc::SelectCapability(deviceId); capture_module_->RegisterCaptureDataCallback(this); - if (capture_module_->StartCapture(capability) != 0) + const auto capabilities = webrtc::BuildCaptureCapabilityCandidates(deviceId); + for (const auto& capability : capabilities) { - capture_module_->DeRegisterCaptureDataCallback(); - state_.store(kEnded); - return false; + if (capture_module_->StartCapture(capability) == 0) + { + capture_started_.store(true); + state_.store(kLive); + return true; + } } - capture_started_.store(true); - state_.store(kLive); - return true; + capture_module_->DeRegisterCaptureDataCallback(); + state_.store(kEnded); + return false; } void CameraVideoSource::Stop() diff --git a/WebRtcInterop/Media/CameraVideoSource.h b/WebRtcInterop/CameraVideoSource.h similarity index 100% rename from WebRtcInterop/Media/CameraVideoSource.h rename to WebRtcInterop/CameraVideoSource.h diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems b/WebRtcInterop/WebRtcInterop.Shared.vcxitems index c0f5201..47dd28f 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems @@ -134,7 +134,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - + @@ -149,7 +149,7 @@ ninja -C "$(WebRtcOutRoot)\$(Configuration)\$(Platform)" - + diff --git a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters index 61b500f..1e08676 100644 --- a/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters +++ b/WebRtcInterop/WebRtcInterop.Shared.vcxitems.filters @@ -37,9 +37,7 @@ - - Media - + @@ -84,8 +82,6 @@ - - Media - + \ No newline at end of file