MediaSource
Media Source Extensions: feed a video element with segments your own code fetched, instead of pointing it at a URL. The foundation every adaptive player - HLS, DASH - is built on.
@inject Bit.Butil.MediaSource mediaSourceMDN reference
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.
@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();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.
<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
}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.
{
// 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");
}
}
}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.
{
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
}
}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.
{
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();
}
}ffmpeg -movflags
frag_keyframe+empty_moov produces and what every DASH or HLS packager emits already.
API reference
ValueTask<bool> IsSupported()ValueTask<bool> IsManagedSupported()ValueTask<bool> IsTypeSupported(string mimeType)ValueTask<string[]> GetSupportedTypes(string[]? candidates = null)static readonly string[] CommonTypesValueTask<MediaSourceHandle?> Open(ElementReference mediaElement, bool preferManagedMediaSource = false)ValueTask<SourceBufferHandle?> AddSourceBuffer(string mimeType)ValueTask<MediaSourceReadyState> GetReadyState()ValueTask<double?> GetDuration()ValueTask<bool> SetDuration(double seconds)ValueTask<bool> EndOfStream(MediaSourceEndOfStreamError error = None)ValueTask<bool> SetLiveSeekableRange(double start, double end)ValueTask<bool> ClearLiveSeekableRange()ValueTask<ButilSubscription> Subscribe(Action<MediaSourceReadyState> handler)ValueTask DisposeAsync()ValueTask<SourceBufferAppendStatus> Append(byte[] data)ValueTask<bool> Remove(double start, double end)ValueTask Abort()ValueTask<bool> ChangeType(string mimeType)ValueTask<bool> SetMode(SourceBufferMode mode)ValueTask<bool> SetTimestampOffset(double seconds)ValueTask<bool> SetAppendWindow(double start, double end)ValueTask<BufferedTimeRange[]> GetBuffered()ValueTask<bool> IsUpdating()ValueTask DisposeAsync()