diff --git a/src/components/video-container/Video-container.component.ts b/src/components/video-container/Video-container.component.ts index 63ce411..7286f0f 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"; @@ -24,13 +25,13 @@ const INIT_NATIVE_HLS_RE = /^((?!chrome|android).)*safari/i; // In Safari on live streams video.duration = Infinity const getVideoDuration = (video: HTMLVideoElement): number => { if (video.duration && video.duration !== Infinity) { - return video.duration + return video.duration; } if (video.seekable.length > 0) { - return video.seekable.end(0) + return video.seekable.end(0); } - return Infinity -} + return Infinity; +}; /** * @slot - Video-container main content @@ -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. @@ -231,12 +235,19 @@ export class VideoContainer extends LitElement { @listen(Types.Command.init, { isSourceSupported: true }) initNative() { - this.sources.enableSource(); - if (this.muxData) + if (this.muxData) { connectMuxData(this.videos[0], { ...this.muxData, player_init_time: this.initTime, }); + } + + if (this.drmOptions?.[KeySystems.fps]) { + initFairPlayDRM(this.videos[0], this.drmOptions[KeySystems.fps]); + } + + // Init source after the video events are set + this.sources.enableSource(); } @listen(Types.Command.initCustomHLS) @@ -258,6 +269,22 @@ 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].licenseUrl, + }, + "com.microsoft.playready": { + licenseUrl: this.drmOptions[KeySystems.playready].licenseUrl, + }, + } + : {}, }); if (this.muxData) @@ -329,6 +356,7 @@ export class VideoContainer extends LitElement { handleVideoEvent(e: Event & { target: HTMLVideoElement }) { const type = e.type; const video = this.videos[0]; + switch (type) { case "play": dispatch(this, Types.Action.play); @@ -351,7 +379,7 @@ export class VideoContainer extends LitElement { case "loadeddata": dispatch(this, Types.Action.updateDuration, { initialized: true, - duration: getVideoDuration(video) + duration: getVideoDuration(video), }); break; case "ratechange": @@ -380,6 +408,13 @@ export class VideoContainer extends LitElement { break; case "loadedmetadata": dispatch(this, Types.Action.canPlay); + const duration = getVideoDuration(video); + if (duration && duration !== Infinity) { + dispatch(this, Types.Action.updateDuration, { + initialized: true, + duration, + }); + } break; case "error": if (!this.isSourceSupported) return; 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..5b4c041 --- /dev/null +++ b/src/helpers/drm.ts @@ -0,0 +1,57 @@ +import { 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: ArrayBuffer, licenseServerUrl: string) => { + return async (event: MediaEncryptedEvent) => { + const video = event.target as HTMLVideoElement; + 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: MediaKeySessionEventMap["message"], licenseServerUrl: string) => { + let licenseResponse = await fetch(licenseServerUrl, { + method: 'POST', + headers: new Headers({'Content-type': 'application/octet-stream'}), + body: event.message, + }); + 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 +>;