From a393afcecc189468b3445947b877826104545e1f Mon Sep 17 00:00:00 2001 From: Ivan Kryak Date: Mon, 19 Aug 2024 17:16:13 +0500 Subject: [PATCH] feat: basic drm support --- .../Video-container.component.ts | 23 ++++++- .../video-player/Video-player.component.ts | 12 +++- src/helpers/drm.ts | 63 +++++++++++++++++++ src/types.ts | 20 ++++++ 4 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/helpers/drm.ts diff --git a/src/components/video-container/Video-container.component.ts b/src/components/video-container/Video-container.component.ts index 63ce411..983ca40 100644 --- a/src/components/video-container/Video-container.component.ts +++ b/src/components/video-container/Video-container.component.ts @@ -12,8 +12,9 @@ import styles from "./Video-container.styles.css?inline"; import type Hls from "hls.js"; import { getBufferedEnd } from "../../helpers/buffer"; import { connectMuxData } from "../../helpers/mux"; +import { initFairPlayDRM } from "../../helpers/drm"; import { createProvider, StorageProvider } from "../../helpers/storage"; -import { MuxParams } from "../../types"; +import { MuxParams, DRMOptions, KeySystems } from "../../types"; import { when } from "lit/directives/when.js"; import "../buttons/Play"; import { subtitlesController, SubtitlesController } from "./subtitles"; @@ -72,6 +73,9 @@ export class VideoContainer extends LitElement { @connect("live") live: boolean; + @connect("drmOptions") + drmOptions?: DRMOptions + /** * A unique identifier used for storing and retrieving user preferences related to video playback. * These preferences include volume level, selected quality level, active text track, playback rate, and mute status. @@ -237,6 +241,10 @@ export class VideoContainer extends LitElement { ...this.muxData, player_init_time: this.initTime, }); + + if (this.drmOptions && this.drmOptions[KeySystems.fps]) { + initFairPlayDRM(this.videos[0], this.drmOptions[KeySystems.fps]); + } } @listen(Types.Command.initCustomHLS) @@ -258,6 +266,19 @@ export class VideoContainer extends LitElement { levelLoadingMaxRetry: 4, backBufferLength: navigator.userAgent.match(/Android/i) ? 0 : 30, liveDurationInfinity: true, + emeEnabled: !!this.drmOptions, + drmSystems: this.drmOptions ? { + 'com.apple.fps': { + licenseUrl: this.drmOptions[KeySystems.fps].licenseUrl, + serverCertificateUrl: this.drmOptions[KeySystems.fps].certificateUrl, + }, + 'com.widevine.alpha': { + licenseUrl: this.drmOptions[KeySystems.widevine] + }, + 'com.microsoft.playready': { + licenseUrl: this.drmOptions[KeySystems.playready] + } + } : {} }); if (this.muxData) diff --git a/src/components/video-player/Video-player.component.ts b/src/components/video-player/Video-player.component.ts index 86a80d4..3a28f2d 100644 --- a/src/components/video-player/Video-player.component.ts +++ b/src/components/video-player/Video-player.component.ts @@ -11,7 +11,7 @@ import { FullscreenController } from "./controllers/Fullscreen"; import { IdleController } from "./controllers/Idle"; import { KeyboardController } from "./controllers/Keyboard"; import { emit } from "../../helpers/event"; -import { Action, MuxParams } from "../../types"; +import { Action, MuxParams, DRMOptions } from "../../types"; import { watch } from "../../decorators/watch"; import styles from "./Video-player.styles.css?inline"; @@ -79,6 +79,12 @@ export class VideoPlayer extends LitElement { @property({ type: Number }) offset: number; + /** + * DRM options + */ + @property({ type: Object, attribute: "drm-options" }) + drmOptions?: DRMOptions + @listen(Types.Command.toggleFullscreen) toggleFullscreen = () => { if (this.state.value.isFullscreen) { @@ -130,6 +136,10 @@ export class VideoPlayer extends LitElement { if (this.muxData?.env_key) { this.state.setState(Action.setMuxParams, { muxData: this.muxData }); } + + if (this.drmOptions) { + this.state.setState(Action.setDRMOptions, { drmOptions: this.drmOptions }); + } } disconnectedCallback(): void { diff --git a/src/helpers/drm.ts b/src/helpers/drm.ts new file mode 100644 index 0000000..a88992b --- /dev/null +++ b/src/helpers/drm.ts @@ -0,0 +1,63 @@ +import { MuxParams, DRMSystemConfiguration } from "../types"; + +export const initFairPlayDRM = async (element: HTMLElement, fairPlayOptions: DRMSystemConfiguration) => { + const certificateUrl = fairPlayOptions.certificateUrl; + const licenseServerUrl = fairPlayOptions.licenseUrl; + + const fairPlayCertificate = await loadFairPlayCertificate(certificateUrl); + + element.addEventListener('encrypted', fairplayEncryptedCallback(fairPlayCertificate, licenseServerUrl)); +}; + +const loadFairPlayCertificate = async (certificateUrl: string) => { + let response = await fetch(certificateUrl); + return response.arrayBuffer(); +} + +const fairplayEncryptedCallback = (fairPlayCertificate, licenseServerUrl) => { + return async (event) => { + console.log("fairplayEncrypted callback", event); + + const video = event.target; + const initDataType = event.initDataType; + + if (!video.mediaKeys) { + let access = await navigator.requestMediaKeySystemAccess("com.apple.fps", [{ + initDataTypes: [initDataType], + videoCapabilities: [{ contentType: 'application/vnd.apple.mpegurl' }], + }]); + + console.log(access); + let keys = await access.createMediaKeys(); + + await keys.setServerCertificate(fairPlayCertificate); + await video.setMediaKeys(keys); + } + + let initData = event.initData; + + let session = video.mediaKeys.createSession(); + session.generateRequest(initDataType, initData); + let message = await new Promise(resolve => { + session.addEventListener('message', resolve, { once: true }); + }); + + let response = await getLicenseResponse(message, licenseServerUrl); + await session.update(response); + return session; + } +} + +const getLicenseResponse = async (event, licenseServerUrl) => { + let spc_string = btoa(String.fromCharCode(...new Uint8Array(event.message))); + let licenseResponse = await fetch(licenseServerUrl, { + method: 'POST', + headers: new Headers({'Content-type': 'application/json', 'X-AxDRM-Message': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiIyMTFhYzFkYy1jOGEyLTQ1NzUtYmFmNy1mYTRiYTU2YzM4YWMiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifV19LCJjb250ZW50X2tleV91c2FnZV9wb2xpY2llcyI6W3sibmFtZSI6IlRoZU9uZVBvbGljeSIsInBsYXlyZWFkeSI6eyJwbGF5X2VuYWJsZXJzIjpbIjc4NjYyN0Q4LUMyQTYtNDRCRS04Rjg4LTA4QUUyNTVCMDFBNyJdfX1dfX0.D9FM9sbTFxBmcCOC8yMHrEtTwm0zy6ejZUCrlJbHz_U'}), + body: event.message, + // body: JSON.stringify({ + // "spc" : spc_string + // }), + }); + return licenseResponse.arrayBuffer(); +} + diff --git a/src/types.ts b/src/types.ts index 705b441..18d161f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,6 +84,7 @@ export enum Action { setMuxParams = "setMuxParams", setVideoOffset = "setVideoOffset", live = "live", + setDRMOptions = "setDRMOptions", } export enum Event { @@ -138,6 +139,7 @@ export type State = Partial< muxData: MuxParams; live: boolean; initialized: boolean; + drmOptions?: DRMOptions; } & typeof device >; @@ -165,3 +167,21 @@ export type MuxOptions = { Hls?: (typeof import("hls.js"))["default"]; hlsjs?: Hls; }; + + +export const enum KeySystems { + clearkey = 'org.w3.clearkey', + fps = 'com.apple.fps', + playready = 'com.microsoft.playready', + widevine = 'com.widevine.alpha', +}; + + +export type DRMSystemConfiguration = { + licenseUrl: string; + certificateUrl?: string; +}; + +export type DRMOptions = Partial< + Record +>;