mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-03 15:57:01 +00:00
Receivers: Added playlist support
This commit is contained in:
parent
72d5c10918
commit
1afd421f7d
22 changed files with 1613 additions and 453 deletions
|
@ -1,3 +1,4 @@
|
|||
import { PlayMessage } from 'common/Packets';
|
||||
import dashjs from 'modules/dashjs';
|
||||
import Hls from 'modules/hls.js';
|
||||
|
||||
|
@ -10,28 +11,69 @@ export enum PlayerType {
|
|||
}
|
||||
|
||||
export class Player {
|
||||
private player: dashjs.MediaPlayerClass | HTMLVideoElement;
|
||||
private hlsPlayer: Hls | undefined;
|
||||
private player: HTMLVideoElement;
|
||||
private playMessage: PlayMessage;
|
||||
private source: string;
|
||||
public playerType: PlayerType;
|
||||
|
||||
constructor(playerType: PlayerType, player: dashjs.MediaPlayerClass | HTMLVideoElement, source: string, hlsPlayer?: Hls) {
|
||||
this.playerType = playerType;
|
||||
// Todo: use a common event handler interface instead of exposing internal players
|
||||
public playerType: PlayerType;
|
||||
public dashPlayer: dashjs.MediaPlayerClass = null;
|
||||
public hlsPlayer: Hls = null;
|
||||
|
||||
constructor(player: HTMLVideoElement, message: PlayMessage) {
|
||||
this.player = player;
|
||||
this.source = source;
|
||||
this.hlsPlayer = playerType === PlayerType.Hls ? hlsPlayer : null;
|
||||
this.playMessage = message;
|
||||
|
||||
if (message.container === 'application/dash+xml') {
|
||||
this.playerType = PlayerType.Dash;
|
||||
this.source = message.content ? message.content : message.url;
|
||||
this.dashPlayer = dashjs.MediaPlayer().create();
|
||||
|
||||
this.dashPlayer.extend("RequestModifier", () => {
|
||||
return {
|
||||
modifyRequestHeader: function (xhr) {
|
||||
if (message.headers) {
|
||||
for (const [key, val] of Object.entries(message.headers)) {
|
||||
xhr.setRequestHeader(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
return xhr;
|
||||
}
|
||||
};
|
||||
}, true);
|
||||
|
||||
} else if ((message.container === 'application/vnd.apple.mpegurl' || message.container === 'application/x-mpegURL') && !player.canPlayType(message.container)) {
|
||||
this.playerType = PlayerType.Hls;
|
||||
this.source = message.url;
|
||||
|
||||
const config = {
|
||||
xhrSetup: function (xhr: XMLHttpRequest) {
|
||||
if (message.headers) {
|
||||
for (const [key, val] of Object.entries(message.headers)) {
|
||||
xhr.setRequestHeader(key, val);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.hlsPlayer = new Hls(config);
|
||||
|
||||
} else {
|
||||
this.playerType = PlayerType.Html;
|
||||
this.source = message.url;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
public destroy() {
|
||||
switch (this.playerType) {
|
||||
case PlayerType.Dash:
|
||||
try {
|
||||
(this.player as dashjs.MediaPlayerClass).destroy();
|
||||
this.dashPlayer.destroy();
|
||||
} catch (e) {
|
||||
logger.warn("Failed to destroy dash player", e);
|
||||
}
|
||||
this.player = null;
|
||||
this.playerType = null;
|
||||
|
||||
break;
|
||||
|
||||
case PlayerType.Hls:
|
||||
|
@ -41,158 +83,231 @@ export class Player {
|
|||
} catch (e) {
|
||||
logger.warn("Failed to destroy hls player", e);
|
||||
}
|
||||
// fall through
|
||||
// fallthrough
|
||||
|
||||
case PlayerType.Html: {
|
||||
const videoPlayer = this.player as HTMLVideoElement;
|
||||
this.player.src = "";
|
||||
// this.player.onerror = null;
|
||||
this.player.onloadedmetadata = null;
|
||||
this.player.ontimeupdate = null;
|
||||
this.player.onplay = null;
|
||||
this.player.onpause = null;
|
||||
this.player.onended = null;
|
||||
this.player.ontimeupdate = null;
|
||||
this.player.onratechange = null;
|
||||
this.player.onvolumechange = null;
|
||||
|
||||
videoPlayer.src = "";
|
||||
// videoPlayer.onerror = null;
|
||||
videoPlayer.onloadedmetadata = null;
|
||||
videoPlayer.ontimeupdate = null;
|
||||
videoPlayer.onplay = null;
|
||||
videoPlayer.onpause = null;
|
||||
videoPlayer.onended = null;
|
||||
videoPlayer.ontimeupdate = null;
|
||||
videoPlayer.onratechange = null;
|
||||
videoPlayer.onvolumechange = null;
|
||||
|
||||
this.player = null;
|
||||
this.playerType = null;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.player = null;
|
||||
this.playerType = null;
|
||||
this.dashPlayer = null;
|
||||
this.hlsPlayer = null;
|
||||
this.playMessage = null;
|
||||
this.source = null;
|
||||
}
|
||||
|
||||
play() { logger.info("Player: play"); this.player.play(); }
|
||||
|
||||
isPaused(): boolean {
|
||||
/**
|
||||
* Load media specified in the PlayMessage provided on object initialization
|
||||
*/
|
||||
public load() {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).isPaused();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).paused;
|
||||
if (this.playMessage.content) {
|
||||
this.dashPlayer.initialize(this.player, `data:${this.playMessage.container};base64,` + window.btoa(this.playMessage.content), true, this.playMessage.time);
|
||||
// dashPlayer.initialize(videoElement, "https://dash.akamaized.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1_https.mpd", true);
|
||||
} else {
|
||||
// value.url = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd';
|
||||
this.dashPlayer.initialize(this.player, this.playMessage.url, true, this.playMessage.time);
|
||||
}
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
// value.url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8?ref=developerinsider.co";
|
||||
this.hlsPlayer.loadSource(this.playMessage.url);
|
||||
this.hlsPlayer.attachMedia(this.player);
|
||||
// hlsPlayer.subtitleDisplay = true;
|
||||
} else { // HTML
|
||||
this.player.src = this.playMessage.url;
|
||||
this.player.load();
|
||||
}
|
||||
}
|
||||
pause() { logger.info("Player: pause"); this.player.pause(); }
|
||||
|
||||
getVolume(): number {
|
||||
public play() {
|
||||
logger.info("Player: play");
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).getVolume();
|
||||
this.dashPlayer.play();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).volume;
|
||||
this.player.play();
|
||||
}
|
||||
}
|
||||
setVolume(value: number) {
|
||||
logger.info(`Player: setVolume ${value}`);
|
||||
|
||||
public isPaused(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return this.dashPlayer.isPaused();
|
||||
} else { // HLS, HTML
|
||||
return this.player.paused;
|
||||
}
|
||||
}
|
||||
|
||||
public pause() {
|
||||
logger.info("Player: pause");
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
this.dashPlayer.pause();
|
||||
} else { // HLS, HTML
|
||||
this.player.pause();
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
const playbackRate = this.getPlaybackRate();
|
||||
const volume = this.getVolume();
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
if (this.playMessage.content) {
|
||||
this.dashPlayer.initialize(this.player, `data:${this.playMessage.container};base64,` + window.btoa(this.playMessage.content), false);
|
||||
} else {
|
||||
this.dashPlayer.initialize(this.player, this.playMessage.url, false);
|
||||
}
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
this.hlsPlayer.loadSource(this.source);
|
||||
} else {
|
||||
this.player.load();
|
||||
}
|
||||
|
||||
this.setPlaybackRate(playbackRate);
|
||||
this.setVolume(volume);
|
||||
}
|
||||
|
||||
public getVolume(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return this.dashPlayer.getVolume();
|
||||
} else { // HLS, HTML
|
||||
return this.player.volume;
|
||||
}
|
||||
}
|
||||
public setVolume(value: number) {
|
||||
// logger.info(`Player: setVolume ${value}`);
|
||||
const sanitizedVolume = Math.min(1.0, Math.max(0.0, value));
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).setVolume(sanitizedVolume);
|
||||
this.dashPlayer.setVolume(sanitizedVolume);
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).volume = sanitizedVolume;
|
||||
this.player.volume = sanitizedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
public isMuted(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).isMuted();
|
||||
return this.dashPlayer.isMuted();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).muted;
|
||||
return this.player.muted;
|
||||
}
|
||||
}
|
||||
setMute(value: boolean) {
|
||||
public setMute(value: boolean) {
|
||||
logger.info(`Player: setMute ${value}`);
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).setMute(value);
|
||||
this.dashPlayer.setMute(value);
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).muted = value;
|
||||
this.player.muted = value;
|
||||
}
|
||||
}
|
||||
|
||||
getPlaybackRate(): number {
|
||||
public getPlaybackRate(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).getPlaybackRate();
|
||||
return this.dashPlayer.getPlaybackRate();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).playbackRate;
|
||||
return this.player.playbackRate;
|
||||
}
|
||||
}
|
||||
setPlaybackRate(value: number) {
|
||||
public setPlaybackRate(value: number) {
|
||||
logger.info(`Player: setPlaybackRate ${value}`);
|
||||
const sanitizedSpeed = Math.min(16.0, Math.max(0.0, value));
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).setPlaybackRate(sanitizedSpeed);
|
||||
this.dashPlayer.setPlaybackRate(sanitizedSpeed);
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).playbackRate = sanitizedSpeed;
|
||||
this.player.playbackRate = sanitizedSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
getDuration(): number {
|
||||
public getDuration(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
const videoPlayer = this.player as dashjs.MediaPlayerClass;
|
||||
return isFinite(videoPlayer.duration()) ? videoPlayer.duration() : 0;
|
||||
return isFinite(this.dashPlayer.duration()) ? this.dashPlayer.duration() : 0;
|
||||
} else { // HLS, HTML
|
||||
const videoPlayer = this.player as HTMLVideoElement;
|
||||
return isFinite(videoPlayer.duration) ? videoPlayer.duration : 0;
|
||||
return isFinite(this.player.duration) ? this.player.duration : 0;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
public getCurrentTime(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).time();
|
||||
return this.dashPlayer.time();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).currentTime;
|
||||
return this.player.currentTime;
|
||||
}
|
||||
}
|
||||
setCurrentTime(value: number) {
|
||||
public setCurrentTime(value: number) {
|
||||
// logger.info(`Player: setCurrentTime ${value}`);
|
||||
const sanitizedTime = Math.min(this.getDuration(), Math.max(0.0, value));
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).seek(sanitizedTime);
|
||||
const videoPlayer = this.player as dashjs.MediaPlayerClass;
|
||||
this.dashPlayer.seek(sanitizedTime);
|
||||
|
||||
if (!videoPlayer.isSeeking()) {
|
||||
videoPlayer.seek(sanitizedTime);
|
||||
if (!this.dashPlayer.isSeeking()) {
|
||||
this.dashPlayer.seek(sanitizedTime);
|
||||
}
|
||||
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).currentTime = sanitizedTime;
|
||||
this.player.currentTime = sanitizedTime;
|
||||
}
|
||||
}
|
||||
|
||||
getSource(): string {
|
||||
public getSource(): string {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
getBufferLength(): number {
|
||||
public getAutoplay(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
const dashPlayer = this.player as dashjs.MediaPlayerClass;
|
||||
return this.dashPlayer.getAutoPlay();
|
||||
} else { // HLS, HTML
|
||||
return this.player.autoplay;
|
||||
}
|
||||
}
|
||||
|
||||
let dashBufferLength = dashPlayer.getBufferLength("video")
|
||||
?? dashPlayer.getBufferLength("audio")
|
||||
?? dashPlayer.getBufferLength("text")
|
||||
?? dashPlayer.getBufferLength("image")
|
||||
public setAutoPlay(value: boolean) {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return this.dashPlayer.setAutoPlay(value);
|
||||
} else { // HLS, HTML
|
||||
return this.player.autoplay = value;
|
||||
}
|
||||
}
|
||||
|
||||
public getBufferLength(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
let dashBufferLength = this.dashPlayer.getBufferLength("video")
|
||||
?? this.dashPlayer.getBufferLength("audio")
|
||||
?? this.dashPlayer.getBufferLength("text")
|
||||
?? this.dashPlayer.getBufferLength("image")
|
||||
?? 0;
|
||||
if (Number.isNaN(dashBufferLength))
|
||||
dashBufferLength = 0;
|
||||
|
||||
dashBufferLength += dashPlayer.time();
|
||||
dashBufferLength += this.dashPlayer.time();
|
||||
return dashBufferLength;
|
||||
} else { // HLS, HTML
|
||||
const videoPlayer = this.player as HTMLVideoElement;
|
||||
let maxBuffer = 0;
|
||||
|
||||
if (videoPlayer.buffered) {
|
||||
for (let i = 0; i < videoPlayer.buffered.length; i++) {
|
||||
const start = videoPlayer.buffered.start(i);
|
||||
const end = videoPlayer.buffered.end(i);
|
||||
if (this.player.buffered) {
|
||||
for (let i = 0; i < this.player.buffered.length; i++) {
|
||||
const start = this.player.buffered.start(i);
|
||||
const end = this.player.buffered.end(i);
|
||||
|
||||
if (videoPlayer.currentTime >= start && videoPlayer.currentTime <= end) {
|
||||
if (this.player.currentTime >= start && this.player.currentTime <= end) {
|
||||
maxBuffer = end;
|
||||
}
|
||||
}
|
||||
|
@ -202,9 +317,9 @@ export class Player {
|
|||
}
|
||||
}
|
||||
|
||||
isCaptionsSupported(): boolean {
|
||||
public isCaptionsSupported(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).getTracksFor('text').length > 0;
|
||||
return this.dashPlayer.getTracksFor('text').length > 0;
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
return this.hlsPlayer.allSubtitleTracks.length > 0;
|
||||
} else {
|
||||
|
@ -212,9 +327,9 @@ export class Player {
|
|||
}
|
||||
}
|
||||
|
||||
isCaptionsEnabled(): boolean {
|
||||
public isCaptionsEnabled(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).isTextEnabled();
|
||||
return this.dashPlayer.isTextEnabled();
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
return this.hlsPlayer.subtitleDisplay;
|
||||
} else {
|
||||
|
@ -222,9 +337,9 @@ export class Player {
|
|||
}
|
||||
}
|
||||
|
||||
enableCaptions(enable: boolean) {
|
||||
public enableCaptions(enable: boolean) {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).enableText(enable);
|
||||
this.dashPlayer.enableText(enable);
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
this.hlsPlayer.subtitleDisplay = enable;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage, Opcode, EventMessage } from 'common/Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage, EventMessage, PlayMessage } from 'common/Packets';
|
||||
import { Logger, LoggerType } from 'common/Logger';
|
||||
const logger = new Logger('PlayerWindow', LoggerType.FRONTEND);
|
||||
|
||||
|
@ -36,28 +36,29 @@ if (TARGET === 'electron') {
|
|||
const electronAPI = __non_webpack_require__('electron');
|
||||
|
||||
electronAPI.ipcRenderer.on("event-subscribed-keys-update", (_event, value: { keyDown: Set<string>, keyUp: Set<string> }) => {
|
||||
logger.info('PLAYER Updated key subscriptions', value);
|
||||
preloadData.subscribedKeys.keyDown = value.keyDown;
|
||||
preloadData.subscribedKeys.keyUp = value.keyUp;
|
||||
})
|
||||
|
||||
electronAPI.contextBridge.exposeInMainWorld('targetAPI', {
|
||||
sendPlaybackError: (error: PlaybackErrorMessage) => electronAPI.ipcRenderer.send('send-playback-error', error),
|
||||
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => electronAPI.ipcRenderer.send('send-playback-update', update),
|
||||
sendVolumeUpdate: (update: VolumeUpdateMessage) => electronAPI.ipcRenderer.send('send-volume-update', update),
|
||||
sendPlaybackError: (error: PlaybackErrorMessage) => electronAPI.ipcRenderer.send('send-playback-error', error),
|
||||
sendEvent: (message: EventMessage) => electronAPI.ipcRenderer.send('send-event', message),
|
||||
onPlay: (callback: any) => electronAPI.ipcRenderer.on("play", callback),
|
||||
onPause: (callback: any) => electronAPI.ipcRenderer.on("pause", callback),
|
||||
onResume: (callback: any) => electronAPI.ipcRenderer.on("resume", callback),
|
||||
onSeek: (callback: any) => electronAPI.ipcRenderer.on("seek", callback),
|
||||
onSetVolume: (callback: any) => electronAPI.ipcRenderer.on("setvolume", callback),
|
||||
onSetSpeed: (callback: any) => electronAPI.ipcRenderer.on("setspeed", callback),
|
||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback),
|
||||
onSetPlaylistItem: (callback: any) => electronAPI.ipcRenderer.on("setplaylistitem", callback),
|
||||
emitEvent: (message: EventMessage) => electronAPI.ipcRenderer.send('emit-event', message),
|
||||
|
||||
sendPlayRequest: (message: PlayMessage, playlistIndex: number) => electronAPI.ipcRenderer.send('play-request', message, playlistIndex),
|
||||
getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'),
|
||||
getSubscribedKeys: () => preloadData.subscribedKeys,
|
||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback),
|
||||
onPlayPlaylist: (callback: any) => electronAPI.ipcRenderer.on('play-playlist', callback),
|
||||
logger: loggerInterface,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import dashjs from 'modules/dashjs';
|
||||
import Hls, { LevelLoadedData } from 'modules/hls.js';
|
||||
import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
|
||||
import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlaybackState, PlaybackUpdateMessage, PlaylistContent, PlayMessage, SeekMessage, SetPlaylistItemMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
|
||||
import { Player, PlayerType } from './Player';
|
||||
import * as connectionMonitor from '../ConnectionMonitor';
|
||||
import { toast, ToastIcon } from '../components/Toast';
|
||||
import * as connectionMonitor from 'common/ConnectionMonitor';
|
||||
import { supportedAudioTypes } from 'common/MimeTypes';
|
||||
import { mediaItemFromPlayMessage, playMessageFromMediaItem } from 'common/UtilityFrontend';
|
||||
import { toast, ToastIcon } from 'common/components/Toast';
|
||||
import {
|
||||
targetPlayerCtrlStateUpdate,
|
||||
targetKeyDownEventListener,
|
||||
|
@ -34,8 +36,9 @@ function formatDuration(duration: number) {
|
|||
}
|
||||
}
|
||||
|
||||
function sendPlaybackUpdate(updateState: number) {
|
||||
function sendPlaybackUpdate(updateState: PlaybackState) {
|
||||
const updateMessage = new PlaybackUpdateMessage(Date.now(), updateState, player.getCurrentTime(), player.getDuration(), player.getPlaybackRate());
|
||||
playbackState = updateState;
|
||||
|
||||
if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) {
|
||||
lastPlayerUpdateGenerationTime = updateMessage.generationTime;
|
||||
|
@ -43,41 +46,53 @@ function sendPlaybackUpdate(updateState: number) {
|
|||
}
|
||||
};
|
||||
|
||||
function onPlayerLoad(value: PlayMessage, currentPlaybackRate?: number, currentVolume?: number) {
|
||||
function onPlayerLoad(value: PlayMessage) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Load);
|
||||
loadingSpinner.style.display = 'none';
|
||||
|
||||
// Subtitles break when seeking post stream initialization for the DASH player.
|
||||
// Its currently done on player initialization.
|
||||
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
|
||||
if (value.time) {
|
||||
player.setCurrentTime(value.time);
|
||||
if (player.getAutoplay()) {
|
||||
if (!supportedAudioTypes.find(v => v === value.container.toLocaleLowerCase())) {
|
||||
idleIcon.style.display = 'none';
|
||||
idleBackground.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
idleIcon.style.display = 'block';
|
||||
idleBackground.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
if (value.speed) {
|
||||
player.setPlaybackRate(value.speed);
|
||||
} else if (currentPlaybackRate) {
|
||||
player.setPlaybackRate(currentPlaybackRate);
|
||||
} else {
|
||||
player.setPlaybackRate(1.0);
|
||||
}
|
||||
playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate);
|
||||
// Subtitles break when seeking post stream initialization for the DASH player.
|
||||
// Its currently done on player initialization.
|
||||
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
|
||||
if (value.time) {
|
||||
player.setCurrentTime(value.time);
|
||||
}
|
||||
}
|
||||
if (value.speed) {
|
||||
player.setPlaybackRate(value.speed);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate);
|
||||
}
|
||||
if (value.volume) {
|
||||
volumeChangeHandler(value.volume);
|
||||
}
|
||||
else {
|
||||
// Protocol v2 FCast PlayMessage does not contain volume field and could result in the receiver
|
||||
// getting out-of-sync with the sender on 1st playback.
|
||||
volumeChangeHandler(1.0);
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: 1.0 });
|
||||
}
|
||||
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
|
||||
|
||||
if (currentVolume) {
|
||||
volumeChangeHandler(currentVolume);
|
||||
playbackState = PlaybackState.Playing;
|
||||
logger.info('Media playback start:', cachedPlayMediaItem);
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem)));
|
||||
player.play();
|
||||
}
|
||||
else {
|
||||
// FCast PlayMessage does not contain volume field and could result in the receiver
|
||||
// getting out-of-sync with the sender on 1st playback.
|
||||
volumeChangeHandler(1.0);
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: 1.0 });
|
||||
}
|
||||
|
||||
window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem)));
|
||||
player.play();
|
||||
}
|
||||
|
||||
// HTML elements
|
||||
const idleIcon = document.getElementById('title-icon');
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
const idleBackground = document.getElementById('idle-background');
|
||||
const videoElement = document.getElementById("videoPlayer") as HTMLVideoElement;
|
||||
const videoCaptions = document.getElementById("videoCaptions") as HTMLDivElement;
|
||||
|
||||
|
@ -112,27 +127,38 @@ let playerCtrlSpeedMenuShown = false;
|
|||
|
||||
const playbackRates = ["0.25", "0.50", "0.75", "1.00", "1.25", "1.50", "1.75", "2.00"];
|
||||
const playbackUpdateInterval = 1.0;
|
||||
const playerVolumeUpdateInterval = 0.01;
|
||||
const livePositionDelta = 5.0;
|
||||
const livePositionWindow = livePositionDelta * 4;
|
||||
let player: Player;
|
||||
let playerPrevTime: number = 0;
|
||||
let playbackState: PlaybackState = PlaybackState.Idle;
|
||||
let playerPrevTime: number = 1;
|
||||
let playerPrevVolume: number = 1;
|
||||
let lastPlayerUpdateGenerationTime = 0;
|
||||
let isLive = false;
|
||||
let isLivePosition = false;
|
||||
let captionsBaseHeight = 0;
|
||||
let captionsContentHeight = 0;
|
||||
|
||||
let cachedPlaylist: PlaylistContent = null;
|
||||
let cachedPlayMediaItem: MediaItem = null;
|
||||
let showDurationTimeout: number = null;
|
||||
let playlistIndex = 0;
|
||||
let isMediaItem = false;
|
||||
let playItemCached = false;
|
||||
|
||||
function onPlay(_event, value: PlayMessage) {
|
||||
logger.info("Handle play message renderer", JSON.stringify(value));
|
||||
const currentVolume = player ? player.getVolume() : null;
|
||||
const currentPlaybackRate = player ? player.getPlaybackRate() : null;
|
||||
cachedPlayMediaItem = new MediaItem(
|
||||
value.container, value.url, value.content,
|
||||
value.time, value.volume, value.speed,
|
||||
null, null, value.headers, value.metadata
|
||||
);
|
||||
window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
|
||||
if (!playItemCached) {
|
||||
cachedPlayMediaItem = mediaItemFromPlayMessage(value);
|
||||
isMediaItem = false;
|
||||
}
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem)));
|
||||
logger.info('Media playback changed:', cachedPlayMediaItem);
|
||||
playItemCached = false;
|
||||
|
||||
idleIcon.style.display = 'none';
|
||||
loadingSpinner.style.display = 'block';
|
||||
idleBackground.style.display = 'block';
|
||||
|
||||
if (player) {
|
||||
if ((player.getSource() === value.url) || (player.getSource() === value.content)) {
|
||||
|
@ -145,6 +171,7 @@ function onPlay(_event, value: PlayMessage) {
|
|||
player.destroy();
|
||||
}
|
||||
|
||||
playbackState = PlaybackState.Idle;
|
||||
playerPrevTime = 0;
|
||||
lastPlayerUpdateGenerationTime = 0;
|
||||
isLive = false;
|
||||
|
@ -152,63 +179,48 @@ function onPlay(_event, value: PlayMessage) {
|
|||
captionsBaseHeight = captionsBaseHeightExpanded;
|
||||
|
||||
if ((value.url || value.content) && value.container && videoElement) {
|
||||
player = new Player(videoElement, value);
|
||||
logger.info(`Loaded ${PlayerType[player.playerType]} player`);
|
||||
|
||||
if (value.container === 'application/dash+xml') {
|
||||
logger.info("Loading dash player");
|
||||
const dashPlayer = dashjs.MediaPlayer().create();
|
||||
const source = value.content ? value.content : value.url;
|
||||
player = new Player(PlayerType.Dash, dashPlayer, source);
|
||||
|
||||
dashPlayer.extend("RequestModifier", () => {
|
||||
return {
|
||||
modifyRequestHeader: function (xhr) {
|
||||
if (value.headers) {
|
||||
for (const [key, val] of Object.entries(value.headers)) {
|
||||
xhr.setRequestHeader(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
return xhr;
|
||||
}
|
||||
};
|
||||
}, true);
|
||||
|
||||
// Player event handlers
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); });
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); });
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => {
|
||||
sendPlaybackUpdate(0);
|
||||
window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem)));
|
||||
});
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => {
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, () => { mediaStartHandler(value); });
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => { sendPlaybackUpdate(PlaybackState.Paused); playerCtrlStateUpdate(PlayerControlEvent.Pause); });
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => { mediaEndHandler(); });
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||
|
||||
if (Math.abs(dashPlayer.time() - playerPrevTime) >= playbackUpdateInterval) {
|
||||
sendPlaybackUpdate(dashPlayer.isPaused() ? 2 : 1);
|
||||
playerPrevTime = dashPlayer.time();
|
||||
if (Math.abs(player.dashPlayer.time() - playerPrevTime) >= playbackUpdateInterval) {
|
||||
sendPlaybackUpdate(playbackState);
|
||||
playerPrevTime = player.dashPlayer.time();
|
||||
}
|
||||
});
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_RATE_CHANGED, () => { sendPlaybackUpdate(dashPlayer.isPaused() ? 2 : 1) });
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_RATE_CHANGED, () => { sendPlaybackUpdate(playbackState); });
|
||||
|
||||
// Buffering UI update when paused
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PROGRESS, () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); });
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PROGRESS, () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); });
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_VOLUME_CHANGED, () => {
|
||||
const updateVolume = dashPlayer.isMuted() ? 0 : dashPlayer.getVolume();
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_VOLUME_CHANGED, () => {
|
||||
const updateVolume = player.dashPlayer.isMuted() ? 0 : player.dashPlayer.getVolume();
|
||||
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
|
||||
|
||||
if (Math.abs(updateVolume - playerPrevVolume) >= playerVolumeUpdateInterval) {
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
|
||||
playerPrevVolume = updateVolume;
|
||||
}
|
||||
});
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (data) => { window.targetAPI.sendPlaybackError({
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (data) => { window.targetAPI.sendPlaybackError({
|
||||
message: `DashJS ERROR: ${JSON.stringify(data)}`
|
||||
})});
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (data) => { window.targetAPI.sendPlaybackError({
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (data) => { window.targetAPI.sendPlaybackError({
|
||||
message: `DashJS PLAYBACK_ERROR: ${JSON.stringify(data)}`
|
||||
})});
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { onPlayerLoad(value, currentPlaybackRate, currentVolume); });
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { onPlayerLoad(value); });
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.CUE_ENTER, (e: any) => {
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.CUE_ENTER, (e: any) => {
|
||||
const subtitle = document.createElement("p")
|
||||
subtitle.setAttribute("id", "subtitle-" + e.cueID)
|
||||
|
||||
|
@ -225,11 +237,11 @@ function onPlay(_event, value: PlayMessage) {
|
|||
}
|
||||
});
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.CUE_EXIT, (e: any) => {
|
||||
player.dashPlayer.on(dashjs.MediaPlayer.events.CUE_EXIT, (e: any) => {
|
||||
document.getElementById("subtitle-" + e.cueID)?.remove();
|
||||
});
|
||||
|
||||
dashPlayer.updateSettings({
|
||||
player.dashPlayer.updateSettings({
|
||||
// debug: {
|
||||
// logLevel: dashjs.LogLevel.LOG_LEVEL_INFO
|
||||
// },
|
||||
|
@ -240,36 +252,14 @@ function onPlay(_event, value: PlayMessage) {
|
|||
}
|
||||
});
|
||||
|
||||
if (value.content) {
|
||||
dashPlayer.initialize(videoElement, `data:${value.container};base64,` + window.btoa(value.content), true, value.time);
|
||||
// dashPlayer.initialize(videoElement, "https://dash.akamaized.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1_https.mpd", true);
|
||||
} else {
|
||||
// value.url = 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd';
|
||||
dashPlayer.initialize(videoElement, value.url, true, value.time);
|
||||
}
|
||||
|
||||
} else if ((value.container === 'application/vnd.apple.mpegurl' || value.container === 'application/x-mpegURL') && !videoElement.canPlayType(value.container)) {
|
||||
logger.info("Loading hls player");
|
||||
|
||||
const config = {
|
||||
xhrSetup: function (xhr: XMLHttpRequest) {
|
||||
if (value.headers) {
|
||||
for (const [key, val] of Object.entries(value.headers)) {
|
||||
xhr.setRequestHeader(key, val);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const hlsPlayer = new Hls(config);
|
||||
|
||||
hlsPlayer.on(Hls.Events.ERROR, (eventName, data) => {
|
||||
player.hlsPlayer.on(Hls.Events.ERROR, (eventName, data) => {
|
||||
window.targetAPI.sendPlaybackError({
|
||||
message: `HLS player error: ${JSON.stringify(data)}`
|
||||
});
|
||||
});
|
||||
|
||||
hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => {
|
||||
player.hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => {
|
||||
isLive = level.details.live;
|
||||
isLivePosition = isLive ? true : false;
|
||||
|
||||
|
@ -282,44 +272,32 @@ function onPlay(_event, value: PlayMessage) {
|
|||
}
|
||||
});
|
||||
|
||||
player = new Player(PlayerType.Hls, videoElement, value.url, hlsPlayer);
|
||||
|
||||
// value.url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8?ref=developerinsider.co";
|
||||
hlsPlayer.loadSource(value.url);
|
||||
hlsPlayer.attachMedia(videoElement);
|
||||
// hlsPlayer.subtitleDisplay = true;
|
||||
|
||||
} else {
|
||||
logger.info("Loading html player");
|
||||
player = new Player(PlayerType.Html, videoElement, value.url);
|
||||
|
||||
videoElement.src = value.url;
|
||||
videoElement.load();
|
||||
}
|
||||
|
||||
// Player event handlers
|
||||
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
|
||||
videoElement.onplay = () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); };
|
||||
videoElement.onpause = () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); };
|
||||
videoElement.onended = () => {
|
||||
sendPlaybackUpdate(0);
|
||||
window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem)));
|
||||
};
|
||||
videoElement.onplay = () => { mediaStartHandler(value); };
|
||||
videoElement.onpause = () => { sendPlaybackUpdate(PlaybackState.Paused); playerCtrlStateUpdate(PlayerControlEvent.Pause); };
|
||||
videoElement.onended = () => { mediaEndHandler(); };
|
||||
videoElement.ontimeupdate = () => {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||
|
||||
if (Math.abs(videoElement.currentTime - playerPrevTime) >= playbackUpdateInterval) {
|
||||
sendPlaybackUpdate(videoElement.paused ? 2 : 1);
|
||||
sendPlaybackUpdate(playbackState);
|
||||
playerPrevTime = videoElement.currentTime;
|
||||
}
|
||||
};
|
||||
// Buffering UI update when paused
|
||||
videoElement.onprogress = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); };
|
||||
videoElement.onratechange = () => { sendPlaybackUpdate(videoElement.paused ? 2 : 1) };
|
||||
videoElement.onratechange = () => { sendPlaybackUpdate(playbackState); };
|
||||
videoElement.onvolumechange = () => {
|
||||
const updateVolume = videoElement.muted ? 0 : videoElement.volume;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
|
||||
|
||||
if (Math.abs(updateVolume - playerPrevVolume) >= playerVolumeUpdateInterval) {
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
|
||||
playerPrevVolume = updateVolume;
|
||||
}
|
||||
};
|
||||
|
||||
videoElement.onerror = (event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error) => {
|
||||
|
@ -336,8 +314,16 @@ function onPlay(_event, value: PlayMessage) {
|
|||
isLivePosition = false;
|
||||
}
|
||||
|
||||
onPlayerLoad(value, currentPlaybackRate, currentVolume); };
|
||||
onPlayerLoad(value);
|
||||
};
|
||||
}
|
||||
|
||||
player.setAutoPlay(true);
|
||||
player.load();
|
||||
}
|
||||
|
||||
if (isMediaItem && cachedPlayMediaItem.showDuration && cachedPlayMediaItem.showDuration > 0) {
|
||||
showDurationTimeout = window.setTimeout(mediaEndHandler, cachedPlayMediaItem.showDuration * 1000);
|
||||
}
|
||||
|
||||
// Sender generated event handlers
|
||||
|
@ -346,7 +332,43 @@ function onPlay(_event, value: PlayMessage) {
|
|||
window.targetAPI.onSeek((_event, value: SeekMessage) => { player.setCurrentTime(value.time); });
|
||||
window.targetAPI.onSetVolume((_event, value: SetVolumeMessage) => { volumeChangeHandler(value.volume); });
|
||||
window.targetAPI.onSetSpeed((_event, value: SetSpeedMessage) => { player.setPlaybackRate(value.speed); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); });
|
||||
};
|
||||
}
|
||||
|
||||
function onPlayPlaylist(_event, value: PlaylistContent) {
|
||||
logger.info('Handle play playlist message', JSON.stringify(value));
|
||||
cachedPlaylist = value;
|
||||
|
||||
const offset = value.offset ? value.offset : 0;
|
||||
const volume = value.items[offset].volume ? value.items[offset].volume : value.volume;
|
||||
const speed = value.items[offset].speed ? value.items[offset].speed : value.speed;
|
||||
const playMessage = new PlayMessage(
|
||||
value.items[offset].container, value.items[offset].url, value.items[offset].content,
|
||||
value.items[offset].time, volume, speed, value.items[offset].headers, value.items[offset].metadata
|
||||
);
|
||||
|
||||
isMediaItem = true;
|
||||
cachedPlayMediaItem = value.items[offset];
|
||||
playItemCached = true;
|
||||
window.targetAPI.sendPlayRequest(playMessage, playlistIndex);
|
||||
}
|
||||
|
||||
window.targetAPI.onSetPlaylistItem((_event, value: SetPlaylistItemMessage) => {
|
||||
if (value.itemIndex >= 0 && value.itemIndex < cachedPlaylist.items.length) {
|
||||
logger.info(`Setting playlist item to index ${value.itemIndex}`);
|
||||
playlistIndex = value.itemIndex;
|
||||
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
|
||||
playItemCached = true;
|
||||
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
|
||||
|
||||
if (showDurationTimeout) {
|
||||
window.clearTimeout(showDurationTimeout);
|
||||
showDurationTimeout = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.warn(`Playlist index out of bounds ${value.itemIndex}, ignoring...`);
|
||||
}
|
||||
});
|
||||
|
||||
connectionMonitor.setUiUpdateCallbacks({
|
||||
onConnect: (connections: string[], initialUpdate: boolean = false) => {
|
||||
|
@ -360,6 +382,7 @@ connectionMonitor.setUiUpdateCallbacks({
|
|||
});
|
||||
|
||||
window.targetAPI.onPlay(onPlay);
|
||||
window.targetAPI.onPlayPlaylist(onPlayPlaylist);
|
||||
|
||||
let scrubbing = false;
|
||||
let volumeChanging = false;
|
||||
|
@ -683,7 +706,7 @@ playbackRates.forEach(r => {
|
|||
};
|
||||
});
|
||||
|
||||
videoElement.onclick = () => {
|
||||
function videoClickedHandler() {
|
||||
if (!playerCtrlSpeedMenuShown) {
|
||||
if (player?.isPaused()) {
|
||||
player?.play();
|
||||
|
@ -691,7 +714,67 @@ videoElement.onclick = () => {
|
|||
player?.pause();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
videoElement.onclick = () => { videoClickedHandler(); };
|
||||
idleBackground.onclick = () => { videoClickedHandler(); };
|
||||
idleIcon.onclick = () => { videoClickedHandler(); };
|
||||
|
||||
function mediaStartHandler(message: PlayMessage) {
|
||||
if (playbackState === PlaybackState.Idle) {
|
||||
logger.info('Media playback start:', cachedPlayMediaItem);
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem)));
|
||||
|
||||
if (!supportedAudioTypes.find(v => v === message.container.toLocaleLowerCase())) {
|
||||
idleIcon.style.display = 'none';
|
||||
idleBackground.style.display = 'none';
|
||||
}
|
||||
else {
|
||||
idleIcon.style.display = 'block';
|
||||
idleBackground.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
sendPlaybackUpdate(PlaybackState.Playing);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Play);
|
||||
}
|
||||
|
||||
function mediaEndHandler() {
|
||||
if (showDurationTimeout) {
|
||||
window.clearTimeout(showDurationTimeout);
|
||||
showDurationTimeout = null;
|
||||
}
|
||||
|
||||
if (isMediaItem) {
|
||||
playlistIndex++;
|
||||
|
||||
if (playlistIndex < cachedPlaylist.items.length) {
|
||||
cachedPlayMediaItem = cachedPlaylist.items[playlistIndex];
|
||||
playItemCached = true;
|
||||
window.targetAPI.sendPlayRequest(playMessageFromMediaItem(cachedPlaylist.items[playlistIndex]), playlistIndex);
|
||||
}
|
||||
else {
|
||||
logger.info('End of playlist:', cachedPlayMediaItem);
|
||||
sendPlaybackUpdate(PlaybackState.Idle);
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem)));
|
||||
|
||||
idleIcon.style.display = 'block';
|
||||
idleBackground.style.display = 'block';
|
||||
player.setAutoPlay(false);
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.info('Media playback ended:', cachedPlayMediaItem);
|
||||
sendPlaybackUpdate(PlaybackState.Idle);
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem)));
|
||||
|
||||
idleIcon.style.display = 'block';
|
||||
idleBackground.style.display = 'block';
|
||||
player.setAutoPlay(false);
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Component hiding
|
||||
let uiHideTimer = null;
|
||||
|
@ -757,17 +840,6 @@ function keyDownEventListener(event: KeyboardEvent) {
|
|||
|
||||
if (!handledCase) {
|
||||
switch (event.code) {
|
||||
case 'KeyF':
|
||||
case 'F11':
|
||||
playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'Escape':
|
||||
playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen);
|
||||
event.preventDefault();
|
||||
handledCase = true;
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
skipBack();
|
||||
event.preventDefault();
|
||||
|
@ -826,7 +898,7 @@ function keyDownEventListener(event: KeyboardEvent) {
|
|||
}
|
||||
|
||||
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
|
||||
window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -843,7 +915,7 @@ function skipForward() {
|
|||
document.addEventListener('keydown', keyDownEventListener);
|
||||
document.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||
if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) {
|
||||
window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
|
||||
window.targetAPI.sendEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false)));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -15,6 +15,57 @@ body {
|
|||
max-height: 100%;
|
||||
}
|
||||
|
||||
#title-icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
background-image: url(../assets/icons/app/icon.svg);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.lds-ring div {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
position: absolute;
|
||||
margin: 8px;
|
||||
border: 8px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
|
||||
border-color: #fff transparent transparent transparent;
|
||||
}
|
||||
.lds-ring div:nth-child(1) {
|
||||
animation-delay: -0.45s;
|
||||
}
|
||||
.lds-ring div:nth-child(2) {
|
||||
animation-delay: -0.3s;
|
||||
}
|
||||
.lds-ring div:nth-child(3) {
|
||||
animation-delay: -0.15s;
|
||||
}
|
||||
@keyframes lds-ring {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#idle-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#videoPlayer {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
|
@ -528,6 +579,20 @@ body {
|
|||
|
||||
/* Display scaling (Minimum supported resolution is 960x540) */
|
||||
@media only screen and ((min-width: 2560px) or (min-height: 1440px)) {
|
||||
#title-icon {
|
||||
width: 164px;
|
||||
height: 164px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
}
|
||||
|
@ -545,6 +610,20 @@ body {
|
|||
}
|
||||
|
||||
@media only screen and ((max-width: 2559px) or (max-height: 1439px)) {
|
||||
#title-icon {
|
||||
width: 124px;
|
||||
height: 124px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 104px;
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
}
|
||||
|
@ -562,6 +641,20 @@ body {
|
|||
}
|
||||
|
||||
@media only screen and ((max-width: 1919px) or (max-height: 1079px)) {
|
||||
#title-icon {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 8px;
|
||||
}
|
||||
|
@ -579,6 +672,20 @@ body {
|
|||
}
|
||||
|
||||
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
|
||||
#title-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.lds-ring {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.lds-ring div {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
padding: 4px;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue