loading

Support and segment types

IsSupported / IsManagedSupported / IsTypeSupported / GetSupportedTypes

isTypeSupported answers whether the engine can parse a container and decode its codec - the check to run before building a buffer for it. ManagedMediaSource is the iOS Safari variant, and the only Media Source Extensions an iPhone has.

C#
@inject Bit.Butil.MediaSource mediaSource

var isSupported = await mediaSource.IsSupported();
var isManaged = await mediaSource.IsManagedSupported();

var ok = await mediaSource.IsTypeSupported("video/mp4;codecs=\"avc1.42E01E\"");

// the ones this engine will actually take, in your order of preference:
var usable = await mediaSource.GetSupportedTypes();
Live sample
Probe a type
support output
Results will appear here when you interact with the samples.

Open a source on a video element

Open / GetReadyState / Subscribe

Open attaches a media source to the element and hands back a handle only once the element has adopted it - so a buffer can be added straight away, with no sourceopen event to wait for. Subscribe reports the state afterwards, including the close that happens when the element is torn down.

C#
<video @ref="videoElement" controls playsinline></video>

@code {
    private ElementReference videoElement;

    var source = await mediaSource.Open(videoElement);
    if (source is null) return; // no MSE, or the element never opened it

    await source.Subscribe(state => Console.WriteLine($"ready state: {state}"));

    var state = await source.GetReadyState();   // Open
    await source.DisposeAsync();                // detaches and frees the buffered media
}
Live sample
source output
Results will appear here when you interact with the samples.

Buffers and appends

AddSourceBuffer / Append / GetBuffered / IsUpdating

One buffer per container-and-codec combination; the first thing appended has to be the stream's initialization segment. Append waits for the browser to finish and reports what became of it - QuotaExceeded above all, which is routine and means 'remove what has played and try again', not 'give up'. Pick a fragmented MP4 or WebM below to append it for real.

Razor
@code {
    // The handle from the section above, already attached to a <video> element.
    private MediaSourceHandle? source;

    private async Task Feed(byte[] initSegmentBytes, byte[] mediaSegmentBytes, double currentTime)
    {
        var buffer = await source!.AddSourceBuffer("video/mp4;codecs=\"avc1.42E01E,mp4a.40.2\"");

        var status = await buffer!.Append(initSegmentBytes);   // Success
        status = await buffer.Append(mediaSegmentBytes);

        if (status == SourceBufferAppendStatus.QuotaExceeded)
        {
            await buffer.Remove(0, currentTime - 10);          // evict what has already played
            status = await buffer.Append(mediaSegmentBytes);   // and try the same segment again
        }

        foreach (var range in await buffer.GetBuffered())
        {
            Console.WriteLine($"{range.Start:0.00}s - {range.End:0.00}s");
        }
    }
}
Live sample
Buffer type
Append a segment file (init segment first)
buffer output
Results will appear here when you interact with the samples.

Timeline control

SetDuration / SetMode / SetTimestampOffset / SetAppendWindow / Remove

What splicing is made of: the mode decides whether a segment keeps its own timestamps or is laid after the last one, the timestamp offset moves a segment along the timeline, and the append window trims what falls outside it. Remove is the eviction half of buffer management.

Razor
@code {
    private MediaSourceHandle? source;    // attached to a <video> element
    private SourceBufferHandle? buffer;   // from AddSourceBuffer

    private async Task Timeline()
    {
        await source!.SetDuration(120);                       // what the seek bar is drawn from

        await buffer!.SetMode(SourceBufferMode.Sequence);     // lay segments end to end
        await buffer.SetTimestampOffset(30);                  // or place them at a point you choose
        await buffer.SetAppendWindow(0, 10);                  // and drop whatever falls outside

        await buffer.Remove(0, 30);                           // free what has already been played
    }
}
Live sample
Duration (seconds)
Timestamp offset
timeline output
Results will appear here when you interact with the samples.

Ending the stream

EndOfStream / SetLiveSeekableRange / ClearLiveSeekableRange

EndOfStream declares that everything that will ever be appended has been - playback runs to the end of what is buffered and the element fires ended. Ending with a reason instead puts the element in an error state, which is how a failed stream is told apart from a finished one. A live stream uses the seekable range to give itself a DVR window.

Razor
@code {
    private MediaSourceHandle? source;   // attached to a <video> element

    private async Task Finish(double liveEdge)
    {
        await source!.EndOfStream();                                   // finished cleanly

        await source.EndOfStream(MediaSourceEndOfStreamError.Network); // the next segment never arrived

        await source.SetLiveSeekableRange(liveEdge - 60, liveEdge);    // a one-minute DVR window
        await source.ClearLiveSeekableRange();
    }
}
Live sample
end-of-stream output
Results will appear here when you interact with the samples.
Note:
Segments, not files A source buffer takes fragmented media: an initialization segment, then media segments. A plain progressive MP4 is rejected - it has to be fragmented (moof/mdat), which is what ffmpeg -movflags frag_keyframe+empty_moov produces and what every DASH or HLS packager emits already.
Warning:
QuotaExceeded is normal A long playback fills the buffer. The accepted answer is to remove the ranges that have already been played and append the same segment again - treating it as fatal is the classic way to make a player stall halfway through a film.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes MediaSource or ManagedMediaSource.
IsManagedSupported
ValueTask<bool> IsManagedSupported()
True when ManagedMediaSource exists - the iOS Safari variant.
IsTypeSupported
ValueTask<bool> IsTypeSupported(string mimeType)
Whether the engine can play segments of this container and codec.
GetSupportedTypes
ValueTask<string[]> GetSupportedTypes(string[]? candidates = null)
Filters candidates down to what this engine accepts, preserving your preference order.
CommonTypes
static readonly string[] CommonTypes
Segment types worth probing on today's engines, most broadly supported first.
Open
ValueTask<MediaSourceHandle?> Open(ElementReference mediaElement, bool preferManagedMediaSource = false)
Attaches a media source to the element and returns a handle once it is open.
MediaSourceHandle.AddSourceBuffer
ValueTask<SourceBufferHandle?> AddSourceBuffer(string mimeType)
Creates a buffer for one container and codec combination.
MediaSourceHandle.GetReadyState
ValueTask<MediaSourceReadyState> GetReadyState()
Closed, Open or Ended.
MediaSourceHandle.GetDuration
ValueTask<double?> GetDuration()
The timeline's length in seconds, or null when it has none yet.
MediaSourceHandle.SetDuration
ValueTask<bool> SetDuration(double seconds)
Sets the total length - what the element reports and the seek bar is drawn from.
MediaSourceHandle.EndOfStream
ValueTask<bool> EndOfStream(MediaSourceEndOfStreamError error = None)
Declares the stream complete, optionally as a failure.
MediaSourceHandle.SetLiveSeekableRange
ValueTask<bool> SetLiveSeekableRange(double start, double end)
The window a live stream lets the user seek within.
MediaSourceHandle.ClearLiveSeekableRange
ValueTask<bool> ClearLiveSeekableRange()
Drops that window.
MediaSourceHandle.Subscribe
ValueTask<ButilSubscription> Subscribe(Action<MediaSourceReadyState> handler)
Watches the source opening, ending and closing.
MediaSourceHandle.DisposeAsync
ValueTask DisposeAsync()
Detaches the source, revokes its object URL and frees the buffered media.
SourceBufferHandle.Append
ValueTask<SourceBufferAppendStatus> Append(byte[] data)
Appends one segment and completes once the browser has taken it. Appends are serialized per buffer.
SourceBufferHandle.Remove
ValueTask<bool> Remove(double start, double end)
Drops the media between two points on the timeline.
SourceBufferHandle.Abort
ValueTask Abort()
Cancels the current operation and resets the parser - what a quality switch calls first.
SourceBufferHandle.ChangeType
ValueTask<bool> ChangeType(string mimeType)
Re-points the buffer at a different container or codec, for a seamless switch.
SourceBufferHandle.SetMode
ValueTask<bool> SetMode(SourceBufferMode mode)
Whether segments keep their own timestamps or are laid end to end.
SourceBufferHandle.SetTimestampOffset
ValueTask<bool> SetTimestampOffset(double seconds)
Shifts subsequent appends along the timeline.
SourceBufferHandle.SetAppendWindow
ValueTask<bool> SetAppendWindow(double start, double end)
Media outside this window is dropped as it is appended.
SourceBufferHandle.GetBuffered
ValueTask<BufferedTimeRange[]> GetBuffered()
The stretches of media the buffer currently holds.
SourceBufferHandle.IsUpdating
ValueTask<bool> IsUpdating()
True while the browser is still working through an append or removal.
SourceBufferHandle.DisposeAsync
ValueTask DisposeAsync()
Removes the buffer from its source, discarding what it holds.
An unhandled error has occurred. Reload 🗙