loading

Which key systems does this browser have?

IsSupported / IsKeySystemSupported

The resolved configuration is the answer worth reading: capabilities the key system could not meet have been dropped from it, so it says which robustness level you actually get - and therefore whether the HD rendition may be offered at all. A key system that is not installed answers null.

C#
@inject Bit.Butil.EncryptedMedia encryptedMedia

var access = await encryptedMedia.IsKeySystemSupported("com.widevine.alpha",
    new MediaKeySystemConfiguration
    {
        InitDataTypes = ["cenc"],
        VideoCapabilities =
        [
            new MediaKeySystemMediaCapability { ContentType = "video/mp4;codecs=\"avc1.42E01E\"", Robustness = "HW_SECURE_ALL" },
            new MediaKeySystemMediaCapability { ContentType = "video/mp4;codecs=\"avc1.42E01E\"", Robustness = "SW_SECURE_CRYPTO" },
        ]
    });

var robustness = access?.Configuration.VideoCapabilities?[0].Robustness; // what was actually granted
Live sample
key system output
Results will appear here when you interact with the samples.

A complete Clear Key exchange

CreateMediaKeys / CreateSession / GenerateRequest / Update / GetKeyStatuses

Clear Key is the key system every browser has, and it needs no server - which makes it the one DRM flow that can run end to end on a documentation page. The steps here are exactly the ones Widevine or PlayReady would take: create the keys, open a session, generate a request from initialization data, and feed a licence back.

Razor
@inject HttpClient http
@inject Bit.Butil.EncryptedMedia encryptedMedia

@code {
    private MediaKeysHandle? keys;
    private MediaKeySessionHandle? session;

    private async Task Exchange(byte[] initData)
    {
        keys = await encryptedMedia.CreateMediaKeys("org.w3.clearkey", new MediaKeySystemConfiguration
        {
            InitDataTypes = ["keyids"],
            VideoCapabilities = [new MediaKeySystemMediaCapability { ContentType = "video/mp4;codecs=\"avc1.42E01E\"" }]
        });

        session = await keys!.CreateSession(
            onMessage: async message =>
            {
                // The message is opaque: post it to the licence server unchanged, and feed whatever
                // comes back straight into Update. Nothing here inspects either one.
                var licence = await RequestLicence(message.Message);
                await session!.Update(licence);
            },
            onKeyStatusesChange: statuses =>
            {
                foreach (var status in statuses) Console.WriteLine($"{status.KeyId}: {status.Status}");
            });

        await session!.GenerateRequest("keyids", initData);
    }

    private async Task<byte[]> RequestLicence(byte[] challenge)
    {
        var response = await http.PostAsync("/api/licence", new ByteArrayContent(challenge));
        return await response.Content.ReadAsByteArrayAsync();
    }
}
Live sample
session output
Results will appear here when you interact with the samples.

Learning that content is encrypted

SubscribeEncrypted

The encrypted event is how a page finds out that the media it is playing needs a key, and it carries the initialization data the licence request has to be built from. Subscribe before playback starts - an element that raises it with nobody listening simply stalls.

Razor
@implements IAsyncDisposable
@inject Bit.Butil.EncryptedMedia encryptedMedia

<video @ref="videoElement" controls></video>

@code {
    private ElementReference videoElement;
    private ButilSubscription? subscription;
    private MediaKeySessionHandle? session;   // from the exchange above

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        subscription = await encryptedMedia.SubscribeEncrypted(videoElement, async initData =>
        {
            // one per protection system in the stream; filter by InitDataType, and by key ids you
            // already hold
            await session!.GenerateRequest(initData);
        });
    }

    public async ValueTask DisposeAsync()
    {
        if (subscription is not null) await subscription.DisposeAsync();
    }
}
Live sample
encrypted event output
Results will appear here when you interact with the samples.

Server certificates and persistent licences

SetServerCertificate / MediaKeySessionType / Load / Remove

A server certificate lets the decryption module encrypt its very first licence request, which removes a round trip and stops device information travelling in the clear. A persistent-licence session stores the licence on the device and can be reloaded later by its session id - which is all offline playback is.

Razor
@inject Bit.Butil.LocalStorage localStorage

@code {
    private MediaKeysHandle? keys;   // from CreateMediaKeys above

    private async Task GoOffline(byte[] certificateBytes, byte[] initData,
                                Action<MediaKeyMessage> onMessage)
    {
        // Optional, and only some key systems want it: it lets the CDM encrypt its challenge to a
        // licence server it can already identify.
        await keys!.SetServerCertificate(certificateBytes);

        var offline = await keys.CreateSession(onMessage, MediaKeySessionType.PersistentLicense);
        await offline!.GenerateRequest(initData);

        // The id is the only way back to the stored licence - the session itself does not survive.
        var storedId = await offline.GetSessionId();
        await localStorage.SetItem("licence-session", storedId);
    }

    private async Task Restore(Action<MediaKeyMessage> onMessage)
    {
        var storedId = await localStorage.GetItem("licence-session");

        // next time, with no network at all:
        var restored = await keys!.CreateSession(onMessage, MediaKeySessionType.PersistentLicense);
        await restored!.Load(storedId);

        await restored.Remove();   // releases the licence and deletes the stored copy
    }
}
Live sample
advanced output
Results will appear here when you interact with the samples.
Note:
Butil carries the bytes and never reads them Licence requests and responses are opaque to the browser and to this library alike: the contract is between your app and the key system's licence server. What Butil provides is the plumbing - negotiation, sessions, the message callback, and key statuses - in a shape that survives the interop boundary.
Warning:
Usable is not the only status that matters A licence can be accepted and still leave its keys unusable: OutputRestricted means the display path has no HDCP, OutputDownscaled means only a lower resolution is allowed, and UsableInFuture means the licence is valid but its window has not opened yet - waiting is the answer there, not a new licence. Watch the key-status callback rather than the return value of Update to know that playback can really start.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes navigator.requestMediaKeySystemAccess.
IsKeySystemSupported
ValueTask<MediaKeySystemAccessInfo?> IsKeySystemSupported(string keySystem, params MediaKeySystemConfiguration[] configurations)
Asks whether a key system can meet one of these configurations, and reports the one it settled on.
CreateMediaKeys
ValueTask<MediaKeysHandle?> CreateMediaKeys(string keySystem, params MediaKeySystemConfiguration[] configurations)
Negotiates the key system and instantiates its content decryption module.
SubscribeEncrypted
ValueTask<ButilSubscription> SubscribeEncrypted(ElementReference mediaElement, Action<EncryptedMediaInitData> handler)
Watches an element's encrypted event - how the app learns it needs a key.
MediaKeysHandle.Access
MediaKeySystemAccessInfo Access
The key system that answered, and the configuration it resolved.
MediaKeysHandle.AttachTo
ValueTask<bool> AttachTo(ElementReference mediaElement)
Hands the keys to the element that will play the protected content.
MediaKeysHandle.SetServerCertificate
ValueTask<bool> SetServerCertificate(byte[] certificate)
Gives the decryption module the licence server's certificate up front.
MediaKeysHandle.CreateSession
ValueTask<MediaKeySessionHandle?> CreateSession(Action<MediaKeyMessage> onMessage, MediaKeySessionType sessionType = Temporary, Action<MediaKeyStatusEntry[]>? onKeyStatusesChange = null)
Opens a session that will hold one licence.
MediaKeySessionHandle.GenerateRequest
ValueTask<bool> GenerateRequest(EncryptedMediaInitData initData)
Turns initialization data into a licence request, which arrives on the message callback.
MediaKeySessionHandle.GenerateRequest
ValueTask<bool> GenerateRequest(string initDataType, byte[] initData)
The same, with initialization data the app assembled itself.
MediaKeySessionHandle.Update
ValueTask<bool> Update(byte[] response)
Hands the licence server's answer to the key system.
MediaKeySessionHandle.Load
ValueTask<bool> Load(string storedSessionId)
Restores a stored persistent licence instead of asking the server for a new one.
MediaKeySessionHandle.GetSessionId
ValueTask<string> GetSessionId()
The key system's own session id - what Load needs later.
MediaKeySessionHandle.GetKeyStatuses
ValueTask<MediaKeyStatusEntry[]> GetKeyStatuses()
Every key the session holds and what it can currently decrypt.
MediaKeySessionHandle.GetExpiration
ValueTask<double?> GetExpiration()
When the licence expires, in milliseconds since the epoch, or null when it does not.
MediaKeySessionHandle.Remove
ValueTask<bool> Remove()
Releases the licence and deletes a stored copy.
MediaKeySessionHandle.DisposeAsync
ValueTask DisposeAsync()
Closes the session, releasing the keys it holds.
An unhandled error has occurred. Reload 🗙