EncryptedMedia
Encrypted Media Extensions: negotiate a key system, open key sessions, and carry licence requests between the browser's decryption module and your licence server. DRM playback, from C#.
@inject Bit.Butil.EncryptedMedia encryptedMediaMDN reference
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.
@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 grantedA complete Clear Key exchange
CreateMediaKeys / CreateSession / GenerateRequest / Update / GetKeyStatusesClear 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.
HttpClient http
Bit.Butil.EncryptedMedia encryptedMedia
{
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();
}
}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.
IAsyncDisposable
Bit.Butil.EncryptedMedia encryptedMedia
<video @ref="videoElement" controls></video>
{
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();
}
}Server certificates and persistent licences
SetServerCertificate / MediaKeySessionType / Load / RemoveA 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.
Bit.Butil.LocalStorage localStorage
{
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
}
}API reference
ValueTask<bool> IsSupported()ValueTask<MediaKeySystemAccessInfo?> IsKeySystemSupported(string keySystem, params MediaKeySystemConfiguration[] configurations)ValueTask<MediaKeysHandle?> CreateMediaKeys(string keySystem, params MediaKeySystemConfiguration[] configurations)ValueTask<ButilSubscription> SubscribeEncrypted(ElementReference mediaElement, Action<EncryptedMediaInitData> handler)MediaKeySystemAccessInfo AccessValueTask<bool> AttachTo(ElementReference mediaElement)ValueTask<bool> SetServerCertificate(byte[] certificate)ValueTask<MediaKeySessionHandle?> CreateSession(Action<MediaKeyMessage> onMessage, MediaKeySessionType sessionType = Temporary, Action<MediaKeyStatusEntry[]>? onKeyStatusesChange = null)ValueTask<bool> GenerateRequest(EncryptedMediaInitData initData)ValueTask<bool> GenerateRequest(string initDataType, byte[] initData)ValueTask<bool> Update(byte[] response)ValueTask<bool> Load(string storedSessionId)ValueTask<string> GetSessionId()ValueTask<MediaKeyStatusEntry[]> GetKeyStatuses()ValueTask<double?> GetExpiration()ValueTask<bool> Remove()ValueTask DisposeAsync()