-
-
Notifications
You must be signed in to change notification settings - Fork 55
Middleware
The WebRTCme.Middleware package: everything between the unified API and your
UI. A video tile that works on all five platforms, managers for streams, data and recording, and —
folded into the same package — the whole connection layer.
Reach for it when you want a call app rather than a WebRTC library. Skip it entirely if you have
your own signalling and your own rendering; WebRTCme alone is a complete API.
WebRTCme.Middleware.nupkg
├── Middleware this page — tile, managers, view models
└── Connection Connection · Connection-Signaling · Connection-MediaSoup
One project, one assembly, two implementations: net10.0 builds Core/ + Blazor/, each platform
target framework builds Core/ + Maui/. Same bait-and-switch as the layer below, so you
reference the package and get whichever implementation matches what you are building.
Be aware of which half of this package you are standing on:
| Supported |
Media, ILocalMediaStream, IMediaStreamManager, IDataManager, IMediaRecorderManager, and the Add*Middleware registration |
Build on these |
| Provided, shape may change |
CallViewModel, ChatViewModel, ConnectionParametersViewModel
|
The demo apps' view models, shipped so you can reuse or copy them. They move when the demos move |
The second group is genuinely useful — a working call is a lot of state — but it is not a surface
with a stability promise. Copying CallViewModel into your own app is a supported thing to do.
services.AddMiddleware(); // Core only — managers, view models, the connection layer
services.AddBlazorMiddleware(); // + Blazor popups, navigation, file streams (calls AddMiddleware)
services.AddMauiMiddleware(); // + MAUI popups, navigation, file streams (calls AddMiddleware)On MAUI, IModalPopup shows the platform's own alert and prompt dialogs rather than a popup
page. A page pushed over a call page makes it disappear, and a call page that leaves the call in
OnDisappearing would hang up on every error. (After 26.9.21; before that it was a
CommunityToolkit popup, which did exactly that.)
Call exactly one of the platform ones. Each registers five platform services — IModalPopup,
INavigation, IRunOnUiThread, IWebRtcIncomingFileStreamFactory,
IMediaRecorderFileStreamFactory — and then chains into AddMiddleware, which registers the
managers, the view models and AddConnection().
The plug-in itself is registered separately because it is a singleton the platform owns rather than something the container constructs. See Getting started for the full startup in context.
The one piece of UI in the whole framework, and the reason the middleware has a per-platform half
at all. A <video> element on Blazor; a View with a handler per platform on MAUI.
flowchart LR
cam["camera / screen"]
track["IMediaStreamTrack"]
stream["IMediaStream"]
tile["Media"]
subgraph R["renderer, per platform"]
direction TB
r1["Blazor — HTML video element"]
r2["Android — SurfaceViewRenderer"]
r3["iOS / Mac Catalyst — RTCVideoView"]
r4["Windows — frames to a XAML surface"]
end
cam --> track --> stream --> tile --> R
MAUI, with the handler registered in MauiProgram:
<middleware:Media Stream="{Binding Stream}"
Label="{Binding Label}"
PeerAudioMuted="{Binding PeerAudioMuted}"
PeerSpeaking="{Binding PeerSpeaking}" />Blazor, same properties:
<Media Stream="@p.Stream"
Label="@p.Label"
PeerAudioMuted="@p.PeerAudioMuted"
PeerSpeaking="@p.PeerSpeaking" />Two deliberate choices in it:
The tile carries no component library. No MudBlazor, no Syncfusion, no icon font. This is library code, and a tile that renders correctly only when someone else's UI stack happens to be loaded renders wrong in most applications. Mute and speaking are shown with colour, a ring and words, all of which are always available.
Remote tiles cover, the self-view contain. A fixed-aspect tile has to do something with a
480x640 phone stream in a 16:9 box. Cropping someone else is cosmetic; cropping yourself hides what
you are actually sending, which is the one thing a self-view exists to tell you.
Everything a view needs to render one tile. The split is load-bearing rather than tidy:
-
Stream,Label,AudioMuted,Hangupdescribe how to attach a stream. Changing one means rebuilding the tile. -
PeerAudioMuted,PeerVideoMuted,PeerSpeakingdescribe what the peer is doing. They change constantly and raisePropertyChanged, so a view follows them without being rebuilt. -
VideoMutedis on that side too, although it reads like an attach property. It is this machine's own camera being muted, which a person does over and over during a call, andCallViewModelsets it on the local tile so the self-view blanks. (After 26.9.21.)
MediaStreamManager.Add and Update both remove the tile and re-insert it — they have to, because
MAUI's BindableLayout ignores a Replace and keeps the stream the tile was first given. Every
one of those round trips tears down and recreates the platform video renderer. Routing "this peer
started speaking" through that path would recreate the video view on every phrase.
So: anything that changes during a call is a property set in place; anything that identifies the stream goes through the manager. Worth knowing before adding a property.
ILocalMediaStream |
Getting a camera or a screen. GetCameraMediaStreamAsync(CameraType) turns a camera choice into a facingMode constraint; GetDisplayMediaStreamAync() starts a share. Also OnDeviceChange. |
IMediaStreamManager |
The ObservableCollection<MediaStreamParameters> a call's tiles bind to — Add, Remove, Update, Clear. |
IDataManager |
Chat and file transfer over the data channel. |
IMediaRecorderManager |
Recording a stream to a file, over IMediaRecorder. |
ILocalMediaStream.OnDeviceChange is the only route to device-change notifications.
IMediaDevices.OnDeviceChange exists on the API, but on the native platforms the devices object is
held privately by the implementation, so an event declared on four platform classes had no way of
reaching a caller even once raised. This is that route. No payload, matching the web's
devicechange — ask EnumerateDevices if you need to know what changed.
public Task<IMediaStream> GetCameraMediaStreamAsync(
CameraType cameraType = CameraType.Default,
MediaStreamConstraints mediaStreamConstraints = null);CameraType is Default, Front, Back or External, and becomes an ideal facingMode
constraint rather than an exact one — a device with no camera facing the requested way should still
produce a call with the camera it has. Explicit constraints win outright when supplied.
CallViewModel is the interesting one: a whole call's worth of state and commands, driving the
connection layer underneath.
ObservableCollection<MediaStreamParameters> MediaStreamParametersList { get; }
bool IsMicrophoneMuted { get; }
bool IsCameraMuted { get; }
bool IsSharingScreen { get; }
bool IsRecording { get; }
ICommand ToggleMicrophoneCommand;
ICommand ToggleCameraCommand;
ICommand ShareScreenCommand;
ICommand RecordCommand;
ICommand RestartIceCommand;
ICommand ToggleSpatialLayerCommand;ChatViewModel covers data-channel messaging and file transfer; ConnectionParametersViewModel
covers the join screen — which server, which room, which name.
The demo apps bind straight to these, which is the best way to see what each one expects: see Demo apps.
Nothing in this package proves a frame was drawn. The tile is UI and only exists as UI. The test suite can prove frames arrive — it cannot prove they are drawn, the right way up, at the right size, which is precisely the frame-rotation bug class that cost four commits across three platforms. So rendering stays a manual check against the demo apps. Testing is explicit about this rather than implying coverage that does not exist.
| Connection | Inside the same package: signalling, peer-to-peer or SFU |
| The unified API | The layer below |
| Demo apps | Both view-model sets, in working applications |
Start here
The stack
- WebRTCnative
- Bindings
- The unified API
- Middleware
- Connection
- · Signaling (mesh)
- · MediaSoup (SFU)
- Demo apps
Reference
When it breaks