MediaRecorder
Record a live stream - a camera, a microphone or a screen share - into a file the browser encodes for you, and get it back as bytes or as a blob URL you can play straight away.
@inject Bit.Butil.MediaRecorder recorderMDN reference
MediaStreamHandle from MediaDevices first -
GetUserMedia for a camera or microphone, GetDisplayMedia for a screen
share. Stopping the recording does not stop that stream, and disposing the stream does not
save the recording: they are two separate lifetimes.
Which containers and codecs exist differs per engine - Chromium and Firefox speak WebM, Safari only speaks MP4 - so probe rather than hard-coding a MIME type. GetSupportedTypes filters a candidate list down to what this browser can actually record, keeping your order, so the first entry is your best available choice.
@inject Bit.Butil.MediaRecorder recorder
var supported = await recorder.IsSupported();
var webm = await recorder.IsTypeSupported("video/webm;codecs=vp9,opus");
// MediaRecorder.CommonVideoTypes / CommonAudioTypes are ready-made candidate lists:
var available = await recorder.GetSupportedTypes(MediaRecorder.CommonVideoTypes);
var best = available.FirstOrDefault();Start returns a handle, or null when the container isn't supported or the stream is already stopped. Stop is what produces the recording - disposing without stopping throws the take away. StopAndCreateObjectUrl keeps the bytes inside the browser and hands back a blob: URL a media element can play directly, which is much cheaper than pulling a whole video across the interop boundary.
private MediaStreamHandle? _stream;
private MediaRecordingHandle? _recording;
private string? _playbackUrl;
// 1. open a stream, 2. record it
_stream = await mediaDevices.GetUserMedia(audio: true, video: true);
_recording = await recorder.Start(_stream!, new MediaRecorderOptions
{
MimeType = (await recorder.GetSupportedTypes()).FirstOrDefault(),
});
// later - the blob stays in the browser, only the URL crosses:
var media = await _recording!.StopAndCreateObjectUrl();
_playbackUrl = media?.ObjectUrl; // <video src="@_playbackUrl" controls />
// or pull the bytes into C# to upload or save:
// var media = await _recording.Stop();
// await File.WriteAllBytesAsync(path, media!.Data!);
// release the blob when you're done with it (the service, not the handle - stopping
// the recording already retired the handle):
await recorder.RevokeObjectUrl(_playbackUrl);Pass a timeslice and an onData callback and the browser hands you each encoded slice as it is produced, so a long recording can be uploaded while it runs instead of being held whole in memory. RequestData flushes what has been captured so far without ending the take.
Bit.Butil.MediaRecorder recorder
{
private long _bytesSoFar;
private MediaStreamHandle? _stream; // from mediaDevices.GetUserMedia
private MediaRecordingHandle? _recording;
private async Task Start()
{
_recording = await recorder.Start(
_stream!,
onData: chunk => InvokeAsync(() =>
{
_bytesSoFar += chunk.Length; // upload it, append it to a file, ...
StateHasChanged();
}),
timesliceMs: 1000); // one slice per second
}
// flush early, without stopping:
private async Task Flush() => await _recording!.RequestData();
}MediaStreamHandle when the user is finished, or the indicator light
stays on.
API reference
ValueTask<bool> IsSupported()ValueTask<bool> IsTypeSupported(string mimeType)ValueTask<string[]> GetSupportedTypes(string[]? candidates = null)static readonly string[] CommonVideoTypesstatic readonly string[] CommonAudioTypesValueTask<MediaRecordingHandle?> Start(MediaStreamHandle stream, MediaRecorderOptions? options = null, Action<byte[]>? onData = null, int? timesliceMs = null, Action<string>? onError = null)ValueTask RevokeObjectUrl(string? objectUrl)ValueTask DisposeAsync()ValueTask<string> GetState()ValueTask<string> GetMimeType()ValueTask Pause()ValueTask Resume()ValueTask RequestData()ValueTask<RecordedMedia?> Stop()ValueTask<RecordedMedia?> StopAndCreateObjectUrl()ValueTask RevokeObjectUrl(string? objectUrl)ValueTask DisposeAsync()