Skip to content
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
NetworkConfiguration,
PlayerConfiguration,
PlayerEmeConfiguration,
PlayerHlsConfiguration,
PlayerMseConfiguration,
PlayerNetworkConfiguration,
Expand All @@ -24,6 +25,12 @@ export const getPlayerNetworkConfigurationDefaults = (): PlayerNetworkConfigurat
[RequestType.MediaSegment]: getNetworkConfigurationDefaults(),
});

export const getPlayerEmeConfigurationDefaults = (): PlayerEmeConfiguration => ({
reusePersistentKeySessions: false,
audioRobustness: '',
videoRobustness: '',
});

export const getPlayerMseConfigurationDefaults = (): PlayerMseConfiguration => ({
useManagedMediaSourceIfAvailable: true,
requiredBufferDuration: 30,
Expand All @@ -40,4 +47,5 @@ export const getPlayerConfigurationDefaults = (): PlayerConfiguration => ({
network: getPlayerNetworkConfigurationDefaults(),
mse: getPlayerMseConfigurationDefaults(),
hls: getPlayerHlsConfigurationDefaults(),
eme: getPlayerEmeConfigurationDefaults(),
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { StoreNode } from '../../utils/store';
import type { PlayerConfiguration } from '../../types/configuration.declarations';
import PlayerMseConfigurationImpl from './player-mse-configuration-node';
import PlayerHlsConfigurationImpl from './player-hls-configuration-node';
import PlayerEmeConfigurationImpl from './player-eme-configuration-node';

export default class PlayerConfigurationImpl extends StoreNode<PlayerConfiguration> {
public static default(): PlayerConfigurationImpl {
return new PlayerConfigurationImpl({
network: PlayerNetworkConfigurationImpl.default(),
mse: PlayerMseConfigurationImpl.default(),
hls: PlayerHlsConfigurationImpl.default(),
eme: PlayerEmeConfigurationImpl.default(),
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StoreNode } from '../../utils/store';
import type { PlayerEmeConfiguration } from '../../types/configuration.declarations';
import { getPlayerEmeConfigurationDefaults } from '../configuration-defaults';

export default class PlayerEmeConfigurationImpl extends StoreNode<PlayerEmeConfiguration> {
public static default(): PlayerEmeConfigurationImpl {
return new PlayerEmeConfigurationImpl(getPlayerEmeConfigurationDefaults());
}
}
2 changes: 2 additions & 0 deletions packages/playback/src/lib/consts/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export enum PlayerEventType {
NetworkRequestAttemptCompletedSuccessfully = 'NetworkRequestAttemptCompletedSuccessfully',
NetworkRequestAttemptCompletedUnsuccessfully = 'NetworkRequestAttemptCompletedUnsuccessfully',
NetworkRequestAttemptFailed = 'NetworkRequestAttemptFailed',
HlsPlaylistParsed = 'HlsPlaylistParsed',
DashManifestParsed = 'DashManifestParsed',
}
196 changes: 194 additions & 2 deletions packages/playback/src/lib/eme/eme-manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { IEmeManager, IEmeManagerDependencies } from '../types/eme-manager.declarations';
import type {
IEmeManager,
IEmeManagerDependencies,
IKeySystemConfiguration
} from '../types/eme-manager.declarations';
import type { INetworkManager } from '../types/network.declarations';
import type { ILogger } from '../types/logger.declarations';
import type { IPlayerSource } from '../types/source.declarations';

import { IEventEmitter } from '../types/event-emitter.declarations';
import { PrivateEventTypeToEventMap } from '../types/mappers/event-type-to-event-map.declarations';
import { PlayerEventType } from '../consts/events';
/**
* Eme Manager should be shipped as a separate bundle and included in the player as opt-in feature
*/
Expand All @@ -28,24 +34,63 @@ export class EmeManager implements IEmeManager {

protected readonly networkManager_: INetworkManager;
protected readonly logger_: ILogger;
protected readonly privateEventEmitter_: IEventEmitter<PrivateEventTypeToEventMap>;

protected activeVideoElement_: HTMLVideoElement | null = null;
protected activeSource_: IPlayerSource | null = null;
protected initDataType_: string | null = null;
protected initData_: ArrayBuffer | null = null;
protected activeMediaKeys_: MediaKeys | null = null;
protected activeKeySystem_: string | null = null;
protected activeKeySystemConfig_: IKeySystemConfiguration | null = null;

public constructor(dependencies: IEmeManagerDependencies) {
this.networkManager_ = dependencies.networkManager;
this.logger_ = dependencies.logger;
this.privateEventEmitter_ = dependencies.privateEventEmitter;
}

public setSource(source: IPlayerSource): void {
this.activeSource_ = source;
}

public setInitData(type: string, data: ArrayBuffer): void {
// As of now, this is only being called on encrypted event
// We will probably want to call this on parse events as well

// can receive from both pssh or encrypted event

// Check if init data is already set!!

if (!this.activeSource_) {
// return error that source is not set
return;
}

this.getKeySystemAccess_().then((keySystemAccess) => {
this.selectKeySystem_(keySystemAccess).then((keySystem) => {
if (!keySystem || !this.activeKeySystemConfig_) {
// error, there is no selected key system
return;
}

// Key system was successfully added to the video element

// Create a session and init a request
this.createKeySession_(this.activeKeySystemConfig_).then(() => {
//
}).catch(() => {
// error creating key session
});
}).catch(() => {
// error while selecting the key system
});
}).catch(() => {
// error while getting the key system access
});

// TODO: setCertificate logic

if (this.initDataType_ !== null && this.initData_ !== null) {
if (this.initDataType_ === type && EmeManager.areInitDataEqual_(this.initData_, data)) {
// received duplicate
Expand Down Expand Up @@ -80,6 +125,8 @@ export class EmeManager implements IEmeManager {
}

this.activeVideoElement_ = videoElement;

this.initEmeManager_();
}

public detach(): void {
Expand All @@ -89,5 +136,150 @@ export class EmeManager implements IEmeManager {
public dispose(): void {
this.stop();
this.detach();
this.privateEventEmitter_.removeAllEventListeners();
}

private initEmeManager_(): void {
this.privateEventEmitter_.addEventListener(
PlayerEventType.HlsPlaylistParsed,
this.handleParsedManifestEvent_
);
this.privateEventEmitter_.addEventListener(
PlayerEventType.DashManifestParsed,
this.handleParsedManifestEvent_
)
}

private getKeySystemConfig_(): Record<string, IKeySystemConfiguration> {
// TODO: Write logic to get this info from manifests and segment data
// We will probably need to pass in a list of key systems

return {
'com.widevine.alpha': {
videoCapabilities: [{
contentType: 'video/webm; codecs="vp9"',
robustness: 'SW_SECURE_CRYPTO'
}],
audioCapabilities: [{
contentType: 'audio/webm; codecs="vorbis"',
robustness: 'SW_SECURE_CRYPTO'
}]
}
};

}

private handleParsedManifestEvent_(): void {
let mediaKeySystemAccess = {} as MediaKeySystemAccess;

// TODO: update this function to take in parsed data and turn it into keySystemConfig values
// We may need diffrent functions for DASH and HLS
// We may want to call `setInitData` in here
}

/**
* First, this function creates keySystemConfigurations for each key system
* the source allows. Once those are created, we request a MediaKeySystemAccess
* using the aforementioned config. This method returns a promise containing
* a MediaKeySystemAccess instance.
*
* @returns A promise containing the MediaKeySystemAccess
*/
private async getKeySystemAccess_(): Promise<MediaKeySystemAccess> {
let mediaKeySystemAccess = {} as MediaKeySystemAccess;

const keySystems = this.activeSource_?.keySystems;

if (!keySystems) {
// TODO: thow error and ignore EME
return mediaKeySystemAccess;
}

// If `requestMediaKeySystemAccess` report an error
if (navigator.requestMediaKeySystemAccess === undefined ||
typeof navigator.requestMediaKeySystemAccess !== 'function') {
// trigger error
return mediaKeySystemAccess;
}

// TODO: Sort by priority before this

for (const keySystem in keySystems) {
const keySystemConfig = this.getKeySystemConfig_();
// const keySystemConfig = this.getKeySystemConfig_(keySystem);

try {
// TODO: Create an event for request media key system
this.logger_.debug();

const mediaKeySystemAccess = await navigator.requestMediaKeySystemAccess(keySystem, [keySystemConfig]);
return mediaKeySystemAccess;
} catch (error) {
// Warn about a failed request, but loop should continue.
}
}

return mediaKeySystemAccess;
}

/**
* Creates media keys and adds them to the video element.
*
* @param keySystemAccess
* @returns A promise containing the Key System name or null if no key system was valid.
*/
private async selectKeySystem_(keySystemAccess: MediaKeySystemAccess): Promise<string | null> {
// Return early if the mediaKeys are already set.
if (this.activeVideoElement_?.mediaKeys) {
this.activeMediaKeys_ = this.activeVideoElement_.mediaKeys;
this.activeKeySystem_ = keySystemAccess.keySystem;
this.activeKeySystemConfig_ = this.activeSource_?.keySystems[this.activeKeySystem_] || null;
return this.activeKeySystem_;
}

return new Promise((resolve, reject) => {
keySystemAccess.createMediaKeys().then((mediaKeys) => {
this.activeKeySystem_ = keySystemAccess.keySystem;
this.activeMediaKeys_ = mediaKeys;
this.activeKeySystemConfig_ = this.activeSource_?.keySystems[this.activeKeySystem_] || null;

if (this.activeVideoElement_) {
return this.activeVideoElement_.setMediaKeys(this.activeMediaKeys_);
} else {
this.logger_.warn(`WARNING: Attempting to set media keys on an invalid media element.`)
Promise.resolve();
}
}).then(() => {
this.logger_.debug(`Successfully set media keys in the video element for ${this.activeKeySystem_}.`)
resolve(this.activeKeySystem_)
}).catch(function () {
reject();
// error could not create media keys
});
})
}

/**
* Creates a key session.
*
* @param keySystemConfig
* @returns an empty promise
*/
private async createKeySession_(keySystemConfig: IKeySystemConfiguration): Promise<void> {
if (!this.activeKeySystem_ || !this.activeMediaKeys_){
// error
return;
}

if (!this.activeMediaKeys_?.createSession) {
// issue with createSession, error
return;
}

// TODO: We may want to pass in sessionType from the list of sessionTypes from the config.
const mediaKeySession = this.activeMediaKeys_.createSession();

// TODO: create a session token to handle key status changes and other stuff
// TODO: generateRequest with keySystemConfig to initialize a request.
}
}
25 changes: 25 additions & 0 deletions packages/playback/src/lib/events/parse-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PlayerEventType } from '../consts/events';
import { PlayerEvent } from './base-player-event';
// We may want to move the following somehwere else.
// We will also need a parsed DASH manifest type.
// import { ParsedPlaylist } from '../../../../../node_modules/@videojs/hls-parser/dist/types/index';

export class HlsPlaylistParsedEvent extends PlayerEvent {
public readonly type = PlayerEventType.HlsPlaylistParsed;
// public readonly playlist: ParsedPlaylist;

// TODO: pass and set playlist here
public constructor() {
super();
}
}

export class DashManifestParsedEvent extends PlayerEvent {
public readonly type = PlayerEventType.DashManifestParsed;
// public readonly manifest: ParsedManifest;

// TODO: pass and set manifest here
public constructor() {
super();
}
}
21 changes: 18 additions & 3 deletions packages/playback/src/lib/player/base/base-player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { PlayerConfiguration } from '../../types/configuration.declarations
import type { DeepPartial } from '../../types/utility.declarations';
import type { IStore } from '../../types/store.declarations';
import type { EventListener, IEventEmitter } from '../../types/event-emitter.declarations';
import type { EventTypeToEventMap } from '../../types/mappers/event-type-to-event-map.declarations';
import type { EventTypeToEventMap, PrivateEventTypeToEventMap } from '../../types/mappers/event-type-to-event-map.declarations';
// events
import {
ConfigurationChangedEvent,
Expand Down Expand Up @@ -46,7 +46,7 @@ import {
NetworkRequestAttemptFailedEvent,
NetworkRequestAttemptStartedEvent,
} from '../../events/network-events';
import type { IEmeManager, IEmeManagerDependencies } from '../../types/eme-manager.declarations';
import type { IEmeManager, IEmeManagerDependencies, IEmeApiAdapter } from '../../types/eme-manager.declarations';
import { EncryptedEvent, WaitingForKeyEvent } from '../../events/eme-events';
import type { PipelineLoaderFactoryStorage } from './pipeline-loader-factory-storage';

Expand All @@ -59,6 +59,7 @@ export interface PlayerDependencies {
readonly interceptorsStorage: IInterceptorsStorage<InterceptorTypeToInterceptorPayloadMap>;
readonly configurationManager: IStore<PlayerConfiguration>;
readonly eventEmitter: IEventEmitter<EventTypeToEventMap>;
readonly privateEventEmitter: IEventEmitter<PrivateEventTypeToEventMap>;
readonly pipelineLoaderFactoryStorage: PipelineLoaderFactoryStorage;
// we have to duplicate network manager in both main and worker threads since we may have eme controller on main thread, which requires network manager
readonly networkManager: INetworkManager;
Expand Down Expand Up @@ -133,10 +134,15 @@ export abstract class BasePlayer {
protected readonly logger_: ILogger;

/**
* internal event emitter service
* internal event emitter service for public events
*/
protected readonly eventEmitter_: IEventEmitter<EventTypeToEventMap>;

/**
* internal event emitter service for private events used throughout the application.
*/
protected readonly privateEventEmitter_: IEventEmitter<PrivateEventTypeToEventMap>;

/**
* internal interceptor's storage service
*/
Expand Down Expand Up @@ -168,6 +174,7 @@ export abstract class BasePlayer {
protected constructor(dependencies: PlayerDependencies) {
this.logger_ = dependencies.logger;
this.eventEmitter_ = dependencies.eventEmitter;
this.privateEventEmitter_ = dependencies.privateEventEmitter;
this.interceptorsStorage_ = dependencies.interceptorsStorage;
this.configurationManager_ = dependencies.configurationManager;
this.networkManager_ = dependencies.networkManager;
Expand All @@ -188,6 +195,7 @@ export abstract class BasePlayer {
this.emeManager_ = factory({
logger: this.logger_.createSubLogger('EmeManager'),
networkManager: this.networkManager_,
privateEventEmitter: this.privateEventEmitter_,
});

if (this.activeVideoElement_) {
Expand All @@ -204,6 +212,13 @@ export abstract class BasePlayer {
this.emeManager_ = null;
}

// TODO: create adapter type
// eslint-disable-next-line
public registerEmeApiAdapter(adapter: IEmeApiAdapter): void {
// TODO: implement functionality to register adapter
// For example, this will be used for legacy fairplay.
}

protected readonly networkRequestInterceptor_ = (requestInfo: INetworkRequestInfo): Promise<INetworkRequestInfo> => {
return this.interceptorsStorage_.executeInterceptors(InterceptorType.NetworkRequest, requestInfo);
};
Expand Down
Loading
Loading