Two-Way WebRTC
The WebRtcTwoWayPage demonstrates two-way WebRTC communication between peers. It captures video and audio from local devices, streams them to remote peers via WebRTC, and simultaneously plays back media received from remote peers — enabling video conferencing.
Overview
The WebRtcTwoWayPage performs the following:
- Enumerates video and audio capture devices
- Creates capture sources with H.264 video encoding and PCM audio
- Connects to a signaling server and joins a conference room via WebRtcWsTwoWaySession
- Sends captured media to remote peers through a shared
WebRtcPublisherSink - Receives media from remote peers via per-peer WebRtcClientSource instances
- Displays local camera preview and remote peer video side by side
- Applies echo cancellation to prevent audio feedback
Differences from One-Way WebRTC
The One-Way WebRTC page only receives media from a server. The Two-Way page both sends and receives:
| Aspect | One-Way | Two-Way |
|---|---|---|
| Direction | Receive only | Send and receive |
| Session class | VastSignalingPlaybackClient (demo code) |
WebRtcWsTwoWaySession (library) |
| Signaling URL | sign-in?channel={publishingPath} |
sign-in?room={room} |
| Capture | None | Camera and microphone |
| Preview | Remote video only | Local preview + remote video |
| Echo cancellation | None | Built-in |
| Multi-peer | Single server peer | Multiple peers |
Device Enumeration
Video and audio devices are enumerated the same way as in Simple Capture, with "No Video" and "No Audio" options at index 0.
Capture Source Configuration
Video
The video capture source is configured for H.264 output with constrained baseline profile:
var mt = new VAST.Common.MediaType
{
ContentType = VAST.Common.ContentType.Video,
CodecId = VAST.Common.Codec.H264,
Width = this.videoWidth,
Height = this.videoHeight,
Framerate = this.videoFramerate,
Bitrate = this.videoWidth * this.videoHeight * 3,
};
var captureMode = dev.FindClosestMatch(mt);
videoCaptureSource = VAST.Media.SourceFactory.CreateVideoCapture(dev.DeviceId);
videoCaptureSource.CaptureMode = captureMode;
videoCaptureSource.Rotation = this.videoRotation;
videoCaptureSource.Renderer = this.localPreview.Renderer;
mt.Metadata.Add("KeyframeInterval", "30");
mt.Metadata.Add("Profile", (66 | 0x100).ToString()); // constrained baseline
await videoCaptureSource.SetDesiredOutputType(0, mt);
FindClosestMatch selects the capture mode closest to the requested resolution and frame rate. The local preview control is connected to the capture source's Renderer to display what the camera sees.
Audio
The audio capture source is configured for PCM output:
audioCaptureSource = VAST.Media.SourceFactory.CreateAudioCapture(deviceId);
await audioCaptureSource.SetDesiredOutputType(0, new VAST.Common.MediaType
{
ContentType = VAST.Common.ContentType.Audio,
CodecId = VAST.Common.Codec.PCM,
SampleFormat = VAST.Common.SampleFormat.S16,
SampleRate = 44100,
Channels = 1,
});
The actual audio codec used for communication is decided and applied by the Google native WebRTC library.
Two-Way Session
The WebRtcWsTwoWaySession manages the complete two-way WebRTC lifecycle — signaling, local media pipeline, remote peer connections, and echo cancellation:
this.rtcWsTwoWaySession = new VAST.WebRTC.WebRtcWsTwoWaySession(
this.textUri.Text, this.textRoom.Text);
this.rtcWsTwoWaySession.IceServers = "turn:test.vastreaming.net:3478";
this.rtcWsTwoWaySession.SetLocalPeer(videoCaptureSource, audioCaptureSource);
this.rtcWsTwoWaySession.AddRemotePeer(this.remotePreview);
this.rtcWsTwoWaySession.Start();
SetLocalPeer specifies the capture sources to send. AddRemotePeer provides a MediaPlayerControl for displaying received media. Start() begins the signaling connection, local media pipeline, and peer negotiation.
Note
The WebRtcWsTwoWaySession is designed to work with VASTreaming StreamingServer and implements its signaling protocol. It doesn't work with other signaling servers.
Sending Media
Internally, the session creates a MediaSession with the capture sources and a shared WebRtcPublisherSink:
this.localMediaSession = new MediaSession();
if (this.videoSource != null) this.localMediaSession.AddSource(this.videoSource);
if (this.audioSource != null) this.localMediaSession.AddSource(this.audioSource);
this.commonWebRtcSink = new WebRtcPublisherSink();
this.commonWebRtcSink.Signalling = this;
this.localMediaSession.AddSink(this.commonWebRtcSink);
this.localMediaSession.Start();
The single WebRtcPublisherSink is shared across all peer connections. When peers join or leave, the sink's peer list is updated dynamically.
Receiving Media
For each remote peer that sends an SDP offer, the session creates a WebRtcClientSource and assigns it to a player:
remotePeer.WebRtcSource = new WebRtcClientSource();
remotePeer.WebRtcSource.Signalling = this;
remotePeer.WebRtcSource.OurPeerId = this.ourPeerId;
remotePeer.WebRtcSource.RemotePeerId = remotePeer.PeerId;
remotePeer.WebRtcSource.Open();
// wait for Opened state, then process the SDP offer
remotePeer.WebRtcSource.ProcessPeerMessage(remotePeer.PeerId, unparsedMessage);
remotePeer.Player.SourceMedia = remotePeer.WebRtcSource;
remotePeer.Player.Play();
Signaling
The session connects to the signaling server via WebSocket at {uri}/sign-in?room={room} and handles the following message types:
| Message Type | Description |
|---|---|
peer-list |
Initial peer list with assigned peer IDs |
peer-list-update |
Dynamic peer join/leave notifications |
message (SDP offer) |
Remote peer's SDP offer — creates a receiving connection |
message (SDP answer) |
Remote peer's SDP answer — completes a publishing connection |
message (ICE candidate) |
ICE candidates routed by subtype |
ping |
Keep-alive sent every 10 seconds |
action (disconnect) |
Server-initiated disconnection |
ICE Candidate Routing
Because two-way communication involves both a sending sink and a receiving source per peer, ICE candidates must be routed to the correct WebRTC object. The session uses a subtype field in signaling messages:
| Subtype | Target | Description |
|---|---|---|
local |
WebRtcClientSource |
Candidate for the receiving (playback) side |
remote |
WebRtcPublisherSink |
Candidate for the sending (publishing) side |
The subtype is determined automatically in SendToPeer by inspecting the caller type.
Echo Cancellation
The session automatically creates an echo canceller to prevent audio feedback between the microphone and speaker:
this.echoCanceller = EchoCancellerFactory.Create();
this.audioSource.EchoCanceller = this.echoCanceller;
remotePeer.Player.EchoCanceller = this.echoCanceller;
The echo canceller is attached to both the audio capture source and each remote peer's player, allowing it to subtract played-back audio from the captured microphone signal.
Device Orientation
On mobile devices, the camera rotation is adjusted when the device orientation changes. The page monitors orientation via OnSizeAllocated and restarts the conference with the updated rotation to apply the new camera angle.
Send Log
The page includes a Send Log button that uploads the application log file to VASTreaming support for diagnostics:
await VAST.Common.License.SendLog("MAUI WebRTC two-way issue");
SendLog sends the current log file to the support server. A valid license key must be configured for this feature to work.
See Also
- Sample Applications — overview of all demo projects
- MAUI App Demo — parent page with app initialization and demo page overview