loading

Support, and whether a configuration is real

IsSupported / IsConfigSupported

Support depends on the whole configuration - the codec string with its profile and level, the resolution, and whether a hardware encoder exists when one was asked for. Probe before building, because an unsupported configuration comes back as a null handle rather than an exception.

C#
@inject Bit.Butil.WebCodecs webCodecs

var isSupported = await webCodecs.IsSupported();

var ok = await webCodecs.IsConfigSupported(new VideoEncoderConfig
{
    Codec = "vp8",
    Width = 640,
    Height = 480,
    Bitrate = 1_000_000,
    Framerate = 30,
    LatencyMode = "realtime"
});
Live sample
support output
Results will appear here when you interact with the samples.

Frames in, chunks out

CreateFrame / CreateVideoEncoder / Encode / Flush

A frame is built here from pixels generated in C#, handed to an encoder, and comes back as compressed chunks on the output callback - the first of which carries the decoder description a matching decoder will need. Nothing writes a container: what you do with the chunks is the point of the API.

C#
var encoder = await webCodecs.CreateVideoEncoder(
    new VideoEncoderConfig { Codec = "vp8", Width = 320, Height = 240, Bitrate = 500_000, Framerate = 10 },
    onChunk: chunk =>
    {
        // mux it, send it, store it - the chunk is yours
        Console.WriteLine($"{chunk.Type} {chunk.Data.Length} bytes at {chunk.Timestamp}us");
    },
    onError: message => Console.WriteLine(message));

await using var frame = await webCodecs.CreateFrame(rgbaBytes, "RGBA", 320, 240, timestamp: 0);
await encoder!.Encode(frame!, keyFrame: true);

await encoder.Flush();   // an encoder holds frames back; this is what makes it hand them over
Live sample
Codec
Frames to encode (10)
encoder output
Results will appear here when you interact with the samples.

Chunks in, frames out

CreateVideoDecoder / Decode / VideoFrameHandle

The chunks produced above are decoded straight back, and each frame is drawn to a canvas and disposed. Disposal is not optional here: a frame holds real memory - often a GPU surface - and a decoder whose frames pile up undisposed stalls.

Razor
@inject Bit.Butil.WebCodecs webCodecs

<canvas @ref="canvasElement" width="640" height="480"></canvas>

@code {
    private ElementReference canvasElement;

    private async Task Play(EncodedVideoChunk firstChunk, IEnumerable<EncodedVideoChunk> chunks)
    {
        var decoder = await webCodecs.CreateVideoDecoder(
            new VideoDecoderConfig { Codec = "vp8", Description = firstChunk.DecoderDescription },
            onFrame: async frame =>
            {
                // onFrame is an Action, so this lambda is async void: a throw would be unobserved
                try { await frame.DrawTo(canvasElement); }
                finally { await frame.DisposeAsync(); }   // required - the decoder stalls without it
            });

        foreach (var chunk in chunks) await decoder!.Decode(chunk);
        await decoder!.Flush();
    }
}
Live sample
decoder output
Results will appear here when you interact with the samples.

Capturing a frame from an element

CaptureFrame

A frame can also be grabbed from a video, canvas or image element - which is how a camera stream reaches an encoder without a recorder in between. The timestamp is yours to choose: it is the app's timeline, and an encoder paces its output from it.

Razor
@inject Bit.Butil.WebCodecs webCodecs

@* A <video> playing a camera stream, a <canvas> you draw into, an <img> that has loaded. *@
<canvas @ref="canvasElement" width="640" height="480"></canvas>

@code {
    private ElementReference canvasElement;
    private VideoEncoderHandle? encoder;   // from CreateVideoEncoder

    // The timestamp is microseconds, and it is what orders the encoded chunks - 33,333 is one frame
    // at 30fps.
    private async Task Capture(int frameIndex)
    {
        await using var frame = await webCodecs.CaptureFrame(canvasElement, timestamp: frameIndex * 33_333);

        await encoder!.Encode(frame!);
    }
}
Live sample sample
capture output
Results will appear here when you interact with the samples.

Audio

CreateAudioData / CreateAudioEncoder / CreateAudioDecoder

The same shape, one dimension down: build AudioData from PCM samples, encode it to Opus or AAC, and decode it back. Sample rate and channel count have to match the configuration - an encoder does not resample.

C#
var encoder = await webCodecs.CreateAudioEncoder(
    new AudioEncoderConfig { Codec = "opus", SampleRate = 48000, NumberOfChannels = 1, Bitrate = 64_000 },
    onChunk: chunk => Console.WriteLine($"{chunk.Data.Length} bytes at {chunk.Timestamp}us"));

await using var data = await webCodecs.CreateAudioData(pcmFloatBytes, "f32", 48000,
                                                       numberOfFrames: 480, numberOfChannels: 1, timestamp: 0);
await encoder!.Encode(data!);
await encoder.Flush();
Live sample
audio output
Results will appear here when you interact with the samples.
Warning:
Dispose every frame and every AudioData They hold memory the garbage collector cannot reclaim. A decoder that runs out of frames stops producing them, and a capture loop that leaks them will exhaust the tab within seconds. Dispose as soon as the frame has been drawn, copied or encoded - await using is the shape that gets this right.
Note:
Backpressure is the caller's job Codecs queue work and answer on a callback. Watch GetQueueSize and stop feeding when it climbs: every queued frame holds its pixels, so a producer that ignores it turns a smooth encode into an out-of-memory crash.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes the WebCodecs constructors.
IsConfigSupported
ValueTask<bool> IsConfigSupported(VideoEncoderConfig | VideoDecoderConfig | AudioEncoderConfig | AudioDecoderConfig config)
Whether this engine can encode or decode with this exact configuration.
CreateVideoEncoder
ValueTask<VideoEncoderHandle?> CreateVideoEncoder(VideoEncoderConfig config, Action<EncodedVideoChunk> onChunk, Action<string>? onError = null)
Creates and configures a VideoEncoder; frames in, chunks out on the callback.
CreateVideoDecoder
ValueTask<VideoDecoderHandle?> CreateVideoDecoder(VideoDecoderConfig config, Action<VideoFrameHandle> onFrame, Action<string>? onError = null)
Creates and configures a VideoDecoder; chunks in, frames out on the callback.
CreateAudioEncoder
ValueTask<AudioEncoderHandle?> CreateAudioEncoder(AudioEncoderConfig config, Action<EncodedAudioChunk> onChunk, Action<string>? onError = null)
Creates and configures an AudioEncoder.
CreateAudioDecoder
ValueTask<AudioDecoderHandle?> CreateAudioDecoder(AudioDecoderConfig config, Action<AudioDataHandle> onData, Action<string>? onError = null)
Creates and configures an AudioDecoder.
CaptureFrame
ValueTask<VideoFrameHandle?> CaptureFrame(ElementReference source, long timestamp, long? duration = null)
Grabs the current frame of a video, canvas or image element.
CreateFrame
ValueTask<VideoFrameHandle?> CreateFrame(byte[] data, string format, int width, int height, long timestamp, long? duration = null)
Builds a frame from raw pixels the app produced.
CreateAudioData
ValueTask<AudioDataHandle?> CreateAudioData(byte[] data, string format, int sampleRate, int numberOfFrames, int numberOfChannels, long timestamp)
Builds an AudioData from PCM samples.
WebCodecsHandle.GetState
ValueTask<CodecState> GetState()
Unconfigured, Configured or Closed.
WebCodecsHandle.GetQueueSize
ValueTask<int> GetQueueSize()
How much work is still queued - the backpressure signal.
WebCodecsHandle.Flush
ValueTask<bool> Flush()
Completes once everything queued has been emitted on the output callback.
WebCodecsHandle.Reset
ValueTask Reset()
Throws away the queue and the configuration - what a seek calls.
WebCodecsHandle.DisposeAsync
ValueTask DisposeAsync()
Closes the codec and releases its resources, a hardware encoder among them.
VideoEncoderHandle.Encode
ValueTask<bool> Encode(VideoFrameHandle frame, bool keyFrame = false)
Queues one frame; the chunk arrives on the output callback.
VideoDecoderHandle.Decode
ValueTask<bool> Decode(EncodedVideoChunk chunk)
Queues one compressed frame; the first after configuring has to be a key frame.
AudioEncoderHandle.Encode
ValueTask<bool> Encode(AudioDataHandle data)
Queues one block of samples.
AudioDecoderHandle.Decode
ValueTask<bool> Decode(EncodedAudioChunk chunk)
Queues one compressed packet.
VideoFrameHandle.DrawTo
ValueTask<bool> DrawTo(ElementReference canvas)
Draws the frame into a canvas, resizing it to the frame first.
VideoFrameHandle.CopyToBytes
ValueTask<byte[]?> CopyToBytes()
The raw pixels, in the frame's own format.
AudioDataHandle.CopyToBytes
ValueTask<byte[]?> CopyToBytes(int planeIndex = 0)
The raw samples of one plane.
An unhandled error has occurred. Reload 🗙