import { AudioTrack, HTMLMediaElement, TextTrack, VideoPlayer, VideoTrack } from '@amazon-devices/react-native-w3cmedia/dist/headless';
import {
Event,
EventListener,
TrackEvent,
} from '@amazon-devices/react-native-w3cmedia/dist/headless';
import { ShakaPlayer } from './shakaplayer/ShakaPlayer';
import {
IHttpHeader,
IPlayerSessionId,
IPlayerSessionLoadParams,
IPlayerSessionMediaInfo,
IPlayerSessionPosition,
IPlayerSessionState,
IPlayerSessionStatus,
ITimeRange,
ITrackType,
IViewHandle,
} from '@amazon-devices/kepler-player-server';
import {
IPlayerServer,
IPlayerServerHandler,
IPlayerServerFactory,
PlayerServerFactory,
} from '@amazon-devices/kepler-player-server';
class PlayerServerHandler implements IPlayerServerHandler {
#playerService: PlayerService;
constructor(playerService: PlayerService) {
console.log('[PlayerService] VegaPlayerServerHandler ctor');
this.#playerService = playerService;
}
handleLoad(mediaInfo: IPlayerSessionMediaInfo, loadParams?: IPlayerSessionLoadParams, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleLoad called with: ', {
mediaInfo,
loadParams,
sessionId,
});
this.#playerService?.onLoad(mediaInfo, loadParams, sessionId);
}
handleUnload(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleUnload called with: ', {
sessionId
});
this.#playerService?.onUnload();
}
handleSetVideoView(handle: IViewHandle, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetVideoView called with:', {
handle,
sessionId
});
this.#playerService?.onSurfaceViewCreated(handle.handle);
}
handleClearVideoView(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleClearVideoView called with sessionId:', sessionId);
this.#playerService?.onSurfaceViewDestroyed();
}
handleSetTextView(handle: IViewHandle, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetVideoView called with:', {
handle,
sessionId
});
this.#playerService?.onCaptionViewCreated(handle.handle);
}
handleClearTextView(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleClearTextView called with:', {
sessionId
});
this.#playerService?.onCaptionViewDestroyed(handle.handle);
}
handlePlay(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handlePlay called with sessionId:', sessionId);
this.#playerService?.handlePlay();
}
handlePause(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handlePause called with sessionId:', sessionId);
this.#playerService?.handlePause();
}
handleSeek(position: number, isRelative?: boolean, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSeek called with:', {
position,
sessionId
});
this.#playerService?.handleSeek(position);
}
handleSetMute(isMuted: boolean, sessionId?: IPlayerSessionId): void {
console.debug('[PlayerService] handleSetMute called with:', {
sessionId
});
this.#playerService?.handleSetMute(isMuted);
}
handleSetPlaybackRate(playbackRate: number, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetPlaybackRate called with:', {
sessionId
});
}
handleSetVolume(volume: number, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetVolume called with:', {
volume,
sessionId
});
this.#playerService?.handleSetVolume(volume);
}
handleSetActiveTrack(trackType: ITrackType, trackId: string, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleSetActiveTrack called with:', {
trackType,
trackId,
sessionId
});
this.#playerService.handleSetActiveTrack(String(trackType), trackId);
}
handleGetCurrentPosition(sessionId?: IPlayerSessionId): number {
console.log('[PlayerService] handleGetCurrentPosition called with:', {
sessionId
});
return this.#playerService.getCurrentPlaybackPosition().position;
}
handleMessage(message: any, sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleMessage called with:', {
sessionId
});
}
handleStartBufferedRangesUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartBufferedRangesUpdates called with:', {
sessionId
});
}
handleStopBufferedRangesUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopBufferedRangesUpdates called with:', {
sessionId
});
}
handleStartStatusUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartStatusUpdates called with:', {
sessionId
});
}
handleStopStatusUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopStatusUpdates called with:', {
sessionId
});
}
handleStartTrackUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartTrackUpdates called with:', {
sessionId
});
}
handleStopTrackUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopTrackUpdates called with:', {
sessionId
});
}
handleStartMessageUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartMessageUpdates called with:', {
sessionId
});
}
handleStopMessageUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopMessageUpdates called with:', {
sessionId
});
}
handleStartErrorUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStartErrorUpdates called with:', {
sessionId
});
}
handleStopErrorUpdates(sessionId?: IPlayerSessionId): void {
console.log('[PlayerService] handleStopErrorUpdates called with:', {
sessionId
});
}
}
class PlayerService {
#playerServerFactory: IPlayerServerFactory | undefined;
#playerServer: IPlayerServer | undefined;
#playerServerHandler: IPlayerServerHandler | undefined;
#msePlayer: ShakaPlayer | undefined;
#videoPlayer: VideoPlayer | undefined;
#serviceComponentId: string = "com.yourcompany.kepler.headlessjsmediaplayer.service";
#activeSurfaceHandle: string | undefined;
#activeCaptionHandle: string | undefined;
#activeSessionId: IPlayerSessionId | undefined;
#hasError: boolean = false;
#AUTOPLAY: boolean = true;
#initializeVideoPlayer = async (): Promise<void> => {
console.log('[PlayerService] initializeVideoPlayer');
this.#videoPlayer = new VideoPlayer();
// @ts-ignore
global.gmedia = this.#videoPlayer;
await this.#videoPlayer.initialize();
this.#setUpEventListeners();
this.#videoPlayer.autoplay = this.#AUTOPLAY;
this.#initialiseMsePlayer();
};
#initialiseMsePlayer = () => {
console.log('[PlayerService] initialiseMsePlayer');
if (this.#videoPlayer !== undefined) {
this.#msePlayer = new ShakaPlayer(this.#videoPlayer as HTMLMediaElement, {
secure: false, // Playback goes through secure or non-secure mode
abrEnabled: true, // Enables Adaptive Bit-Rate (ABR) switching
abrMaxWidth: 3840, // Maximum width allowed for ABR
abrMaxHeight: 2160, // Maximum height allowed for ABR
startPosition: 0
});
}
};
#setUpEventListeners = (): void => {
console.log('[PlayerService] setUpEventListeners');
this.#videoPlayer?.addEventListener('play', this.#onPlay);
this.#videoPlayer?.addEventListener('pause', this.#onPause);
this.#videoPlayer?.addEventListener('seeked', this.#onSeeked);
this.#videoPlayer?.addEventListener('ended', this.#onEnded);
this.#videoPlayer?.addEventListener('error', this.#onError);
this.#videoPlayer?.audioTracks.addEventListener('addtrack', this.#onAudioTrackAdded);
this.#videoPlayer?.videoTracks.addEventListener('addtrack', this.#onVideoTrackAdded);
this.#videoPlayer?.textTracks.addEventListener('addtrack', this.#onTextTrackAdded);
this.#videoPlayer?.audioTracks.addEventListener('removetrack', this.#onAudioTrackRemoved);
this.#videoPlayer?.videoTracks.addEventListener('removetrack', this.#onVideoTrackRemoved);
this.#videoPlayer?.textTracks.addEventListener('removetrack', this.#onTextTrackRemoved);
(this.#videoPlayer as HTMLMediaElement)?.addEventListener('waiting', this.#updateBufferedRanges);
(this.#videoPlayer as HTMLMediaElement)?.addEventListener('canplay', this.#updateBufferedRanges);
};
#removeEventListeners = (): void => {
console.log('[PlayerService] removeEventListeners');
this.#videoPlayer?.removeEventListener('play', this.#onPlay);
this.#videoPlayer?.removeEventListener('pause', this.#onPause);
this.#videoPlayer?.removeEventListener('seeked', this.#onSeeked);
this.#videoPlayer?.removeEventListener('ended', this.#onEnded);
this.#videoPlayer?.removeEventListener('error', this.#onError);
this.#videoPlayer?.audioTracks.removeEventListener('addtrack', this.#onAudioTrackAdded);
this.#videoPlayer?.videoTracks.removeEventListener('addtrack', this.#onVideoTrackAdded);
this.#videoPlayer?.textTracks.removeEventListener('addtrack', this.#onTextTrackAdded);
this.#videoPlayer?.audioTracks.removeEventListener('removetrack', this.#onAudioTrackRemoved);
this.#videoPlayer?.videoTracks.removeEventListener('removetrack', this.#onVideoTrackRemoved);
this.#videoPlayer?.textTracks.removeEventListener('removetrack', this.#onTextTrackRemoved);
(this.#videoPlayer as HTMLMediaElement)?.removeEventListener('waiting', this.#updateBufferedRanges);
(this.#videoPlayer as HTMLMediaElement)?.removeEventListener('canplay', this.#updateBufferedRanges);
};
#onPlay: EventListener = (event?: Event) => {
console.log(`[PlayerService] onPlay ${event?.type}`);
this.updateStatus(this.#activeSessionId);
}
#onPause: EventListener = (event?: Event) => {
console.log('[PlayerService] onPause');
this.updateStatus(this.#activeSessionId);
}
#onSeeked: EventListener = (event?: Event) => {
console.log('[PlayerService] onSeeked');
}
#onEnded: EventListener = (event?: Event) => {
console.log('[PlayerService] onEnded');
this.updateStatus(this.#activeSessionId);
this.onUnload();
}
#onError: EventListener = (event?: Event) => {
console.log(`[PlayerService] onError with code: {}, message: {}`,
this.#videoPlayer?.error.code, this.#videoPlayer?.error.message);
this.#hasError = true;
};
#onAudioTrackAdded: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.addTrack({
id: track.id,
type: "AUDIO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as AudioTrack).enabled
}, this.#activeSessionId);
// Do other operations if needed.
} else {
console.error("Undefined audio track.");
}
}
}
#onVideoTrackAdded: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.addTrack({
id: track.id,
type: "VIDEO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as VideoTrack).selected
}, this.#activeSessionId);
// Do other operations if needed.
} else {
console.error("Undefined video track.");
}
}
}
#onTextTrackAdded: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.addTrack({
id: track.id,
type: "TEXT",
kind: track.kind,
label: track.label,
language: track.language,
mode: (track as TextTrack).mode
}, this.#activeSessionId);
// Do other operations if needed.
} else {
console.error("Undefined text track.");
}
}
}
#onAudioTrackRemoved: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.removeTrack({
id: track.id,
type: "AUDIO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as AudioTrack).enabled
}, this.#activeSessionId);
// Do other operations if needed.
} else {
console.error("Undefined audio track.");
}
}
}
#onVideoTrackRemoved: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.removeTrack({
id: track.id,
type: "VIDEO",
kind: track.kind,
label: track.label,
language: track.language,
enabled: (track as VideoTrack).selected
}, this.#activeSessionId);
// Do other operations if needed.
} else {
console.error("Undefined video track.");
}
}
}
#onTextTrackRemoved: EventListener = (event?: Event) => {
if (event instanceof TrackEvent) {
let track = (event as TrackEvent).track;
if (track !== undefined) {
this.#playerServer?.removeTrack({
id: track.id,
type: "TEXT",
kind: track.kind,
label: track.label,
language: track.language,
mode: (track as TextTrack).mode
}, this.#activeSessionId);
// Do other operations if needed.
} else {
console.error("Undefined text track.");
}
}
}
#updateBufferedRanges: EventListener = (event?: Event) => {
const result: Array<ITimeRange> = [];
const bufferedTimeRanges = this.#videoPlayer?.buffered;
if (bufferedTimeRanges === undefined) {
return;
}
for (let i = 0; i < bufferedTimeRanges.length; ++i) {
result.push({
start: bufferedTimeRanges.start(i),
end: bufferedTimeRanges.end(i)
});
}
this.#playerServer?.updateBufferedRanges(result, this.#activeSessionId);
}
#getPlaybackState = () : IPlayerSessionState => {
if (!this.#videoPlayer) return IPlayerSessionState.ENDED;
if (this.#hasError) return IPlayerSessionState.ERROR;
if (this.#videoPlayer.ended) return IPlayerSessionState.ENDED;
if (this.#videoPlayer.seeking) return IPlayerSessionState.SEEKING;
if (this.#videoPlayer.paused) {
return IPlayerSessionState.PAUSED;
}
return IPlayerSessionState.PLAYING;
}
updateStatus = (sessionId?: IPlayerSessionId) => {
console.debug(`[PlayerService] updateStatus with sessionId: ${sessionId !== undefined ? sessionId?.id: "undefined"}`);
const playerSessionStatus: IPlayerSessionStatus = {
sessionId: sessionId,
playbackState: this.#getPlaybackState(),
playbackRate: this.#videoPlayer?.playbackRate || 1,
isMuted: this.#videoPlayer?.muted || false,
volume: this.#videoPlayer?.volume || 0,
seekable: (this.#videoPlayer?.seekable.length || 0) > 0,
duration: this.#videoPlayer?.duration
}
this.#playerServer?.updateStatus([playerSessionStatus]);
}
getCurrentPlaybackPosition = () : IPlayerSessionPosition => {
return {
sessionId: this.#activeSessionId,
position: this.#videoPlayer?.currentTime ?? 0 as number,
} as IPlayerSessionPosition;
}
onSurfaceViewCreated = (surfaceHandle: string): void => {
console.log('[PlayerService] onSurfaceViewCreated called with surfaceHandle: ', surfaceHandle);
this.#activeSurfaceHandle = surfaceHandle;
this.#videoPlayer?.setSurfaceHandle(surfaceHandle);
};
onSurfaceViewDestroyed = (): void => {
console.log('[PlayerService] onSurfaceViewDestroyed called');
if (this.#activeSurfaceHandle !== undefined) {
this.#videoPlayer?.clearSurfaceHandle(this.#activeSurfaceHandle);
}
}
onCaptionViewCreated = (surfaceHandle: string): void => {
console.log('[PlayerService] onCaptionViewCreated called with surfaceHandle: ', surfaceHandle);
this.#activeCaptionHandle = surfaceHandle;
this.#videoPlayer?.setCaptionViewHandle(surfaceHandle);
};
onCaptionViewDestroyed = (): void => {
console.log('[PlayerService] onCaptionViewDestroyed called');
if (this.#activeCaptionHandle !== undefined) {
this.#videoPlayer?.clearCaptionViewHandle(this.#activeCaptionHandle);
}
}
#findValueByKey = (httpHeaders: Array<IHttpHeader> | undefined, key: string) : string | undefined => {
if (httpHeaders !== undefined) {
for (let i : number = 0; i < httpHeaders.length; ++i) {
if (httpHeaders[i].name === key) {
return httpHeaders[i].value;
}
}
}
return undefined;
}
onLoad = async (urlInfo: IPlayerSessionMediaInfo,
loadParams?: IPlayerSessionLoadParams,
sessionId? : IPlayerSessionId): Promise<void> => {
console.log(`[PlayerService] onLoad with urlInfo: {}`, JSON.stringify(urlInfo));
if (loadParams !== undefined) {
console.log(`[PlayerService] onLoad with loadParams: {}`, JSON.stringify(loadParams));
}
this.#hasError = false;
const content = {
"uri" : urlInfo.mediaUrl.url,
"container": this.#findValueByKey(urlInfo.mediaUrl.httpHeaders, 'container') || 'FMP4'
};
this.onUnload();
this.#activeSessionId = sessionId;
await this.#initializeVideoPlayer();
this.#msePlayer?.load(content, this.#AUTOPLAY);
}
onUnload = () => {
console.log('[PlayerService] onUnload');
this.#removeEventListeners();
this.#msePlayer?.unload();
this.#msePlayer = undefined;
this.#videoPlayer?.deinitialize();
this.#videoPlayer = undefined;
this.#hasError = false;
// @ts-ignore
global.gmedia = null;
};
handlePlay = () => {
console.log('[PlayerService] handlePlay');
if (this.#videoPlayer) {
this.#videoPlayer.play();
} else {
console.log('[PlayerService] handlePlay this.#videoPlayer is undefined');
}
};
handlePause = () => {
console.log('[PlayerService] handlePause');
if (this.#videoPlayer) {
this.#videoPlayer.pause();
} else {
console.log('[PlayerService] handlePause this.#videoPlayer is undefined');
}
};
handleSeek = (seekTimeSeconds: number) => {
console.log(`[PlayerService] handleSeek with seekTimeSeconds: ${seekTimeSeconds}`);
if (this.#videoPlayer) {
const currentTime = this.#videoPlayer.currentTime;
this.#videoPlayer.currentTime = currentTime + seekTimeSeconds;
} else {
console.log('[PlayerService] handleSeek this.#videoPlayer is undefined');
}
}
handleSetMute = (isMuted: boolean) => {
console.debug('[PlayerService] handleSetMute');
if (this.#videoPlayer) {
this.#videoPlayer.muted = isMuted;
this.updateStatus(this.#activeSessionId);
} else {
console.log('[PlayerService] handleSetMute this.#videoPlayer is undefined');
}
};
handleSetVolume = (volume: number) => {
console.debug('[PlayerService] handleSetVolume');
if (this.#videoPlayer) {
this.#videoPlayer.volume = volume;
this.updateStatus(this.#activeSessionId);
}
}
handleSetActiveTrack = (trackType: string, trackId: string) => {
console.debug(`[PlayerService] handleSetActiveTrack ${trackType} ${trackId}`);
if (trackType === 'AUDIO') {
if (this.#msePlayer) {
const audioTrackList = this.#msePlayer.getAudioLanguages();
if (audioTrackList !== undefined && audioTrackList.length > 0) {
let trackLang: string = audioTrackList[parseInt(trackId)];
this.#msePlayer.selectAudioLanguage(trackLang);
} else {
console.error('[PlayerService] handleSetActiveTrack - audio track list is empty.');
}
} else {
console.error('[PlayerService]: handleSetActiveTrack msePlayer not initialised');
}
} else {
console.debug('[PlayerService] handleSetActiveTrack - only audio track handling is implemented.');
}
}
async start(): Promise<void> {
console.log('[PlayerService] start');
this.#playerServerFactory = new PlayerServerFactory();
this.#playerServer = this.#playerServerFactory.getOrMakeServer();
this.#playerServerHandler = new PlayerServerHandler(this);
if (this.#playerServerHandler !== undefined) {
console.log('[PlayerService] Calling setHandler');
this.#playerServer?.setHandler(
this.#playerServerHandler,
this.#serviceComponentId);
}
await this.#initializeVideoPlayer();
console.log('[PlayerService] Service started');
}
async stop(): Promise<void> {
this.onUnload();
}
}
const playerService: PlayerService = new PlayerService();
// Lifecycle methods
export async function onStartService(): Promise<void> {
console.log('[PlayerService] Called onStartService()');
// set global.navigator and globa.window needed by @amazon-devices/react-native-w3cmedia
// This is a temporary workaround till either @amazon-devices/react-native-w3cmedia or headless runtime
// do this on behalf of the app developer
// @ts-ignore
let navigator = global.navigator;
if (navigator === undefined) {
// @ts-ignore
global.navigator = navigator = {};
}
/**
* Sets up global variables for React Native.
* You can use this module directly, or just require InitializeCore.
*/
// @ts-ignore
if (global.window === undefined) {
// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it.
// @ts-ignore
global.window = global;
}
// @ts-ignore
if (global.self === undefined) {
// $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it.
// @ts-ignore
global.self = global;
}
await playerService.start();
console.log('[PlayerService] Service started');
}
export async function onStopService(): Promise<void> {
console.log('[PlayerService] Called onStopService()');
await playerService.stop();
console.log('[PlayerService] Service stopped');
}