Skip to content

Commit

Permalink
feat: basic drm support
Browse files Browse the repository at this point in the history
  • Loading branch information
sck-v committed Aug 19, 2024
1 parent a81944a commit a393afc
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 2 deletions.
23 changes: 22 additions & 1 deletion src/components/video-container/Video-container.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion src/components/video-player/Video-player.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
63 changes: 63 additions & 0 deletions src/helpers/drm.ts
Original file line number Diff line number Diff line change
@@ -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();
}

20 changes: 20 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export enum Action {
setMuxParams = "setMuxParams",
setVideoOffset = "setVideoOffset",
live = "live",
setDRMOptions = "setDRMOptions",
}

export enum Event {
Expand Down Expand Up @@ -138,6 +139,7 @@ export type State = Partial<
muxData: MuxParams;
live: boolean;
initialized: boolean;
drmOptions?: DRMOptions;
} & typeof device
>;

Expand Down Expand Up @@ -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<KeySystems, DRMSystemConfiguration>
>;

0 comments on commit a393afc

Please sign in to comment.