Skip to content

Middleware

Melih Ercan edited this page Sep 23, 2026 · 2 revisions

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.

Two tiers of surface

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.

Registration

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 video tile

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
Loading

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.

MediaStreamParameters — and why it is split in two

Everything a view needs to render one tile. The split is load-bearing rather than tidy:

  • Stream, Label, AudioMuted, Hangup describe how to attach a stream. Changing one means rebuilding the tile.
  • PeerAudioMuted, PeerVideoMuted, PeerSpeaking describe what the peer is doing. They change constantly and raise PropertyChanged, so a view follows them without being rebuilt.
  • VideoMuted is 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, and CallViewModel sets 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.

The managers

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.

The view models

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.

What is deliberately not here

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.

Where next

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

Clone this wiki locally