Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Skip slow stages during iteration: `-SkipToolchain`, `-SkipSync`.
- `WebRtcInterop.Core.vcxproj` — targets .NET Core (`CLRSupport=NetCore`, experimental)
- Both share source via `WebRtcInterop.Shared.vcxitems`
- `RtcPeerConnectionFactory` owns the native `PeerConnectionFactoryInterface` singleton and the signaling thread.
- `RtcPeerConnection`, `RtcDataChannel`, `MediaStream`, and related wrappers hold native `scoped_refptr` handles via the `NativeWrapper<T>` template base class (`NativeWrapper.h`).
- `RtcPeerConnection`, `RtcDataChannel`, `MediaStream`, and related wrappers keep internal `GetNative*` helpers inside WebRtcInterop and return `scoped_refptr<T>` by value; native ownership is handled by `NativeWrapper<T>` composition where needed (`NativeWrapper.h`, ADR-0004).
- `WebRtcInterop\Marshaling\` contains all `marshal_as<>` specializations for converting between managed DTOs/enums and native WebRTC types. Cross-boundary value translation belongs here, not inline in business logic.
- `WebRtcInterop\Observers\` adapts native callbacks into managed events via observer classes that call `FireOn...` helpers on the wrapper types.
- `WebRtcInterop\MediaStream.cpp` contains the concrete `Media::GetUserMedia` implementation; device enumeration and native audio/video source creation happen here, not in C#.
Expand Down Expand Up @@ -91,7 +91,7 @@ Individual-stage Dockerfiles in `docker\` — not a monolithic file:
- `WebRtcNet.Api` is the contract assembly. If you add API surface there, you also need matching wrapper, marshalling, and observer work in `WebRtcInterop`.
- Do not assume every interface member is implemented. Many interop methods still throw `NotImplementedException`, especially around stream enumeration, stats, identity, and parts of peer connection setup.
- Managed argument validation uses `System.Diagnostics.Contracts.Contract.Requires(...)` rather than ad hoc null checks.
- Interop wrapper classes follow the same lifetime pattern: destructor/finalizer pair, and a `GetNative...(throwOnDisposed)` helper that throws `ObjectDisposedException` when the native handle is gone.
- Interop wrapper classes follow the same lifetime pattern: destructor/finalizer pair, and an internal `GetNative...(throwOnDisposed)` helper that returns `scoped_refptr<T>` and throws `ObjectDisposedException` when the native handle is gone.
- Some interop methods require concrete wrapper instances, not arbitrary interface implementations. Peer connection stream operations `dynamic_cast` `IMediaStream` to `WebRtcInterop::MediaStream` before unwrapping the native object.
- `marshal_as<>` specializations in `Marshaling\` use either switch-based dispatch (simple enums) or bidirectional `std::map` helpers (`marshal_mapped_native_type` / `marshal_mapped_managed_type` in `MarshalEnums.h`) for types where both directions are needed.
- The solution uses standard `Debug` and `Release` configurations; use these directly for normal development and CI.
Expand Down
63 changes: 37 additions & 26 deletions WebRtcInterop/Media/MediaStream.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ using namespace WebRtcNet;
namespace WebRtcInterop::Media
{
MediaStream::MediaStream(WebRtcNet::Media::MediaStream^ stream)
: _rpMediaStreamInterface(nullptr),
: _nativeStreamInterface(nullptr),
on_active_(nullptr),
on_inactive_(nullptr),
on_add_track_(nullptr),
Expand All @@ -25,13 +25,12 @@ namespace WebRtcInterop::Media
throw gcnew InvalidCastException("The provided stream must be a WebRtcInterop::Media::MediaStream instance.");
}

auto native_stream = interop_stream->GetNativeMediaStreamInterface(true);
_rpMediaStreamInterface = new webrtc::scoped_refptr(
reinterpret_cast<webrtc::MediaStreamInterface*>(native_stream.ToPointer()));
_nativeStreamInterface = gcnew NativeWrapper<webrtc::MediaStreamInterface>(
interop_stream->GetNativeMediaStreamInterface(true));
}

MediaStream::MediaStream(webrtc::scoped_refptr<webrtc::MediaStreamInterface> stream)
: _rpMediaStreamInterface(new webrtc::scoped_refptr(stream)),
: _nativeStreamInterface(gcnew NativeWrapper<webrtc::MediaStreamInterface>(stream)),
on_active_(nullptr),
on_inactive_(nullptr),
on_add_track_(nullptr),
Expand All @@ -46,30 +45,30 @@ namespace WebRtcInterop::Media

MediaStream::!MediaStream()
{
delete _rpMediaStreamInterface;
_rpMediaStreamInterface = nullptr;
delete _nativeStreamInterface;
_nativeStreamInterface = nullptr;
}

IntPtr MediaStream::GetNativeMediaStreamInterface(bool throwOnDisposed)
webrtc::scoped_refptr<webrtc::MediaStreamInterface> MediaStream::GetNativeMediaStreamInterface(bool throwOnDisposed)
{
if (_rpMediaStreamInterface == nullptr || _rpMediaStreamInterface->get() == nullptr)
if (_nativeStreamInterface == nullptr || !_nativeStreamInterface->HasValue())
{
if (throwOnDisposed) throw gcnew ObjectDisposedException(NAMEOF(MediaStream));
return IntPtr::Zero;
return webrtc::scoped_refptr<webrtc::MediaStreamInterface>();
}

return IntPtr(_rpMediaStreamInterface->get());
return _nativeStreamInterface->GetScopedRef();
}

String^ MediaStream::Id::get()
{
const auto native = _rpMediaStreamInterface->get();
const auto native = _nativeStreamInterface->Get();
return marshal_as<String^>(native->id());
}

IEnumerable<WebRtcNet::Media::MediaStreamTrack^>^ MediaStream::GetAudioTracks()
{
const auto native = _rpMediaStreamInterface->get();
const auto native = _nativeStreamInterface->Get();
auto tracks = gcnew List<WebRtcNet::Media::MediaStreamTrack^>();

for (const auto& track : native->GetAudioTracks())
Expand All @@ -82,7 +81,7 @@ namespace WebRtcInterop::Media

IEnumerable<WebRtcNet::Media::MediaStreamTrack^>^ MediaStream::GetVideoTracks()
{
const auto native = _rpMediaStreamInterface->get();
const auto native = _nativeStreamInterface->Get();
auto tracks = gcnew List<WebRtcNet::Media::MediaStreamTrack^>();

for (const auto& track : native->GetVideoTracks())
Expand All @@ -105,7 +104,7 @@ namespace WebRtcInterop::Media
{
if (trackId == nullptr) throw gcnew ArgumentNullException(NAMEOF(trackId));

const auto native = _rpMediaStreamInterface->get();
const auto native = _nativeStreamInterface->Get();
const auto native_track_id = marshal_as<std::string>(trackId);

const auto audio = native->FindAudioTrack(native_track_id);
Expand All @@ -121,19 +120,25 @@ namespace WebRtcInterop::Media
{
if (track == nullptr) throw gcnew ArgumentNullException(NAMEOF(track));

const auto native_stream = _rpMediaStreamInterface->get();
const auto native_track = reinterpret_cast<webrtc::MediaStreamTrackInterface*>(
track->GetNativeMediaStreamTrackInterface(true).ToPointer());
const auto native_stream = _nativeStreamInterface->Get();
const auto interop_track = dynamic_cast<MediaStreamTrack^>(track);
if (interop_track == nullptr)
{
throw gcnew InvalidCastException("The provided track must be a WebRtcInterop::Media::MediaStreamTrack instance.");
}

const auto native_track_ref = interop_track->GetNativeMediaStreamTrackInterface(true);
const auto native_track = native_track_ref.get();

if (native_track->kind() == webrtc::MediaStreamTrackInterface::kAudioKind)
{
native_stream->AddTrack(
webrtc::scoped_refptr(static_cast<webrtc::AudioTrackInterface*>(native_track)));
webrtc::scoped_refptr(static_cast<webrtc::AudioTrackInterface*>(native_track_ref.get())));
}
else if (native_track->kind() == webrtc::MediaStreamTrackInterface::kVideoKind)
{
native_stream->AddTrack(
webrtc::scoped_refptr(static_cast<webrtc::VideoTrackInterface*>(native_track)));
webrtc::scoped_refptr(static_cast<webrtc::VideoTrackInterface*>(native_track_ref.get())));
}
else
{
Expand All @@ -148,19 +153,25 @@ namespace WebRtcInterop::Media
{
if (track == nullptr) throw gcnew ArgumentNullException(NAMEOF(track));

const auto native_stream = _rpMediaStreamInterface->get();
const auto native_track = reinterpret_cast<webrtc::MediaStreamTrackInterface*>(
track->GetNativeMediaStreamTrackInterface(true).ToPointer());
const auto native_stream = _nativeStreamInterface->Get();
const auto interop_track = dynamic_cast<MediaStreamTrack^>(track);
if (interop_track == nullptr)
{
throw gcnew InvalidCastException("The provided track must be a WebRtcInterop::Media::MediaStreamTrack instance.");
}

const auto native_track_ref = interop_track->GetNativeMediaStreamTrackInterface(true);
const auto native_track = native_track_ref.get();

if (native_track->kind() == webrtc::MediaStreamTrackInterface::kAudioKind)
{
native_stream->RemoveTrack(
webrtc::scoped_refptr(static_cast<webrtc::AudioTrackInterface*>(native_track)));
webrtc::scoped_refptr(static_cast<webrtc::AudioTrackInterface*>(native_track_ref.get())));
}
else if (native_track->kind() == webrtc::MediaStreamTrackInterface::kVideoKind)
{
native_stream->RemoveTrack(
webrtc::scoped_refptr(static_cast<webrtc::VideoTrackInterface*>(native_track)));
webrtc::scoped_refptr(static_cast<webrtc::VideoTrackInterface*>(native_track_ref.get())));
}
else
{
Expand All @@ -178,7 +189,7 @@ namespace WebRtcInterop::Media

Boolean MediaStream::Active::get()
{
const auto native = _rpMediaStreamInterface->get();
const auto native = _nativeStreamInterface->Get();

for (const auto& track : native->GetAudioTracks())
{
Expand Down
7 changes: 3 additions & 4 deletions WebRtcInterop/Media/MediaStream.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <api/scoped_refptr.h>
#include "../NativeWrapper.h"

namespace webrtc
{
Expand Down Expand Up @@ -54,12 +55,10 @@ namespace WebRtcInterop::Media
internal:
MediaStream(webrtc::scoped_refptr<webrtc::MediaStreamInterface> stream);
!MediaStream();

public:
IntPtr GetNativeMediaStreamInterface(bool throwOnDisposed) override;
webrtc::scoped_refptr<webrtc::MediaStreamInterface> GetNativeMediaStreamInterface(bool throwOnDisposed);

private:
webrtc::scoped_refptr<webrtc::MediaStreamInterface>* _rpMediaStreamInterface;
NativeWrapper<webrtc::MediaStreamInterface>^ _nativeStreamInterface;
EventHandler^ on_active_;
EventHandler^ on_inactive_;
EventHandler<WebRtcNet::Media::MediaStreamTrack^>^ on_add_track_;
Expand Down
30 changes: 15 additions & 15 deletions WebRtcInterop/Media/MediaStreamTrack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ using namespace WebRtcNet::Media;
namespace WebRtcInterop::Media
{
MediaStreamTrack::MediaStreamTrack()
: _rpMediaStreamTrackInterface(nullptr),
: _nativeMediaStreamTrackInterface(nullptr),
applied_constraints_(gcnew MediaTrackConstraints()),
on_mute_(nullptr),
on_unmute_(nullptr),
Expand All @@ -23,7 +23,7 @@ namespace WebRtcInterop::Media
}

MediaStreamTrack::MediaStreamTrack(webrtc::scoped_refptr<webrtc::MediaStreamTrackInterface> track)
: _rpMediaStreamTrackInterface(new webrtc::scoped_refptr(track)),
: _nativeMediaStreamTrackInterface(gcnew NativeWrapper<webrtc::MediaStreamTrackInterface>(track)),
applied_constraints_(gcnew MediaTrackConstraints()),
on_mute_(nullptr),
on_unmute_(nullptr),
Expand All @@ -39,30 +39,30 @@ namespace WebRtcInterop::Media

MediaStreamTrack::!MediaStreamTrack()
{
delete _rpMediaStreamTrackInterface;
_rpMediaStreamTrackInterface = nullptr;
delete _nativeMediaStreamTrackInterface;
_nativeMediaStreamTrackInterface = nullptr;
}

IntPtr MediaStreamTrack::GetNativeMediaStreamTrackInterface(bool throwOnDisposed)
webrtc::scoped_refptr<webrtc::MediaStreamTrackInterface> MediaStreamTrack::GetNativeMediaStreamTrackInterface(bool throwOnDisposed)
{
if (_rpMediaStreamTrackInterface == nullptr || _rpMediaStreamTrackInterface->get() == nullptr)
if (_nativeMediaStreamTrackInterface == nullptr || !_nativeMediaStreamTrackInterface->HasValue())
{
if (throwOnDisposed) throw gcnew ObjectDisposedException("MediaStreamTrack");
return IntPtr::Zero;
return webrtc::scoped_refptr<webrtc::MediaStreamTrackInterface>();
}

return IntPtr(_rpMediaStreamTrackInterface->get());
return _nativeMediaStreamTrackInterface->GetScopedRef();
}

MediaStreamTrackKind MediaStreamTrack::Kind::get()
{
const auto native = _rpMediaStreamTrackInterface->get();
const auto native = _nativeMediaStreamTrackInterface->Get();
return marshal_as<MediaStreamTrackKind>(native->kind());
}

String^ MediaStreamTrack::Id::get()
{
const auto native = _rpMediaStreamTrackInterface->get();
const auto native = _nativeMediaStreamTrackInterface->Get();
return marshal_as<String^>(native->id());
}

Expand All @@ -73,19 +73,19 @@ namespace WebRtcInterop::Media

bool MediaStreamTrack::Enabled::get()
{
const auto native = _rpMediaStreamTrackInterface->get();
const auto native = _nativeMediaStreamTrackInterface->Get();
return native->enabled();
}

void MediaStreamTrack::Enabled::set(bool value)
{
const auto native = _rpMediaStreamTrackInterface->get();
const auto native = _nativeMediaStreamTrackInterface->Get();
native->set_enabled(value);
}

bool MediaStreamTrack::Muted::get()
{
const auto native = _rpMediaStreamTrackInterface->get();
const auto native = _nativeMediaStreamTrackInterface->Get();

if (native->kind() == webrtc::MediaStreamTrackInterface::kAudioKind)
{
Expand All @@ -106,13 +106,13 @@ namespace WebRtcInterop::Media

MediaStreamTrackState MediaStreamTrack::ReadyState::get()
{
const auto native = _rpMediaStreamTrackInterface->get();
const auto native = _nativeMediaStreamTrackInterface->Get();
return marshal_as<MediaStreamTrackState>(native->state());
}

WebRtcNet::Media::MediaStreamTrack^ MediaStreamTrack::Clone()
{
return gcnew MediaStreamTrack(*_rpMediaStreamTrackInterface);
return gcnew MediaStreamTrack(_nativeMediaStreamTrackInterface->GetScopedRef());
}

void MediaStreamTrack::Stop()
Expand Down
7 changes: 4 additions & 3 deletions WebRtcInterop/Media/MediaStreamTrack.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <api/scoped_refptr.h>
#include "../NativeWrapper.h"

namespace webrtc
{
Expand Down Expand Up @@ -53,11 +54,11 @@ namespace WebRtcInterop::Media
MediaTrackSettings^ GetSettings() override;
void ApplyConstraints(MediaTrackConstraints^ constraints) override;

public:
IntPtr GetNativeMediaStreamTrackInterface(bool throwOnDisposed) override;
internal:
webrtc::scoped_refptr<webrtc::MediaStreamTrackInterface> GetNativeMediaStreamTrackInterface(bool throwOnDisposed);

private:
webrtc::scoped_refptr<webrtc::MediaStreamTrackInterface>* _rpMediaStreamTrackInterface;
NativeWrapper<webrtc::MediaStreamTrackInterface>^ _nativeMediaStreamTrackInterface;
MediaTrackConstraints^ applied_constraints_;
EventHandler^ on_mute_;
EventHandler^ on_unmute_;
Expand Down
11 changes: 6 additions & 5 deletions WebRtcInterop/NativeWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace WebRtcInterop
#include <api/scoped_refptr.h>

template <typename T>
public ref class NativeWrapper abstract
public ref class NativeWrapper
{
public:
NativeWrapper(T* native)
Expand All @@ -14,16 +14,17 @@ namespace WebRtcInterop
}

NativeWrapper(webrtc::scoped_refptr<T> rp_native)
: rp_native_(rp_native)
: rp_native_(new webrtc::scoped_refptr<T>(rp_native))
{
}

~NativeWrapper() { this->!NativeWrapper(); }

T* Get() { return rp_native_; }
bool HasValue() { return rp_native_ != nullptr && rp_native_->get() != nullptr; }
T* Get() { return rp_native_ == nullptr ? nullptr : rp_native_->get(); }
webrtc::scoped_refptr<T> GetScopedRef() { return rp_native_ == nullptr ? webrtc::scoped_refptr<T>() : *rp_native_; }

explicit operator bool() { return rp_native_ != nullptr && rp_native_->get() != nullptr; }
T* operator->() { return rp_native_; }
explicit operator bool() { return HasValue(); }

internal:
!NativeWrapper()
Expand Down
11 changes: 3 additions & 8 deletions WebRtcInterop/RtcDataChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,21 +40,16 @@ namespace WebRtcInterop
rp_data_channel_interface_ = nullptr;
}

webrtc::DataChannelInterface* RtcDataChannel::GetNativeDataChannelInterface(const bool throwOnDisposed)
webrtc::scoped_refptr<webrtc::DataChannelInterface> RtcDataChannel::GetNativeDataChannelInterface(const bool throwOnDisposed)
{
const auto result = rp_data_channel_interface_.Get();
if (result == nullptr)
{
if (throwOnDisposed) throw gcnew ObjectDisposedException(NAMEOF(RtcDataChannel));
return nullptr;
return webrtc::scoped_refptr<webrtc::DataChannelInterface>();
}

return result;
}

IntPtr RtcDataChannel::GetNativeDataChannelHandle(bool throwOnDisposed)
{
return IntPtr(GetNativeDataChannelInterface(throwOnDisposed));
return webrtc::scoped_refptr<webrtc::DataChannelInterface>(result);
}

String^ RtcDataChannel::Label::get()
Expand Down
6 changes: 3 additions & 3 deletions WebRtcInterop/RtcDataChannel.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace webrtc
{
template <class T>
class scoped_refptr;
class DataChannelInterface;
}

Expand Down Expand Up @@ -69,12 +71,10 @@ namespace WebRtcInterop
void Send(Collections::Generic::IEnumerable<Byte>^ data) override;
void Send(array<Byte>^ data) override;

IntPtr GetNativeDataChannelHandle(bool throwOnDisposed) override;

internal:
RtcDataChannel(webrtc::DataChannelInterface* data_channel_interface);
!RtcDataChannel();
webrtc::DataChannelInterface* GetNativeDataChannelInterface(bool throwOnDisposed);
webrtc::scoped_refptr<webrtc::DataChannelInterface> GetNativeDataChannelInterface(bool throwOnDisposed);

internal:
//Event invocation
Expand Down
Loading
Loading