mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-03 15:57:01 +00:00
Initial commit of WebOS receiver
This commit is contained in:
parent
b7e304b987
commit
90e1f4de1a
118 changed files with 18279 additions and 1746 deletions
235
receivers/common/web/player/Player.ts
Normal file
235
receivers/common/web/player/Player.ts
Normal file
|
@ -0,0 +1,235 @@
|
|||
import dashjs from 'modules/dashjs';
|
||||
import Hls from 'modules/hls.js';
|
||||
|
||||
export enum PlayerType {
|
||||
Html,
|
||||
Dash,
|
||||
Hls,
|
||||
}
|
||||
|
||||
export class Player {
|
||||
private player: dashjs.MediaPlayerClass | HTMLVideoElement
|
||||
private hlsPlayer: Hls | undefined
|
||||
public playerType: PlayerType
|
||||
|
||||
constructor(playerType: PlayerType, player: dashjs.MediaPlayerClass | HTMLVideoElement, hlsPlayer?: Hls) {
|
||||
this.playerType = playerType;
|
||||
this.player = player;
|
||||
this.hlsPlayer = playerType === PlayerType.Hls ? hlsPlayer : null;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
switch (this.playerType) {
|
||||
case PlayerType.Dash:
|
||||
try {
|
||||
(this.player as dashjs.MediaPlayerClass).destroy();
|
||||
} catch (e) {
|
||||
console.warn("Failed to destroy dash player", e);
|
||||
}
|
||||
this.player = null;
|
||||
this.playerType = null;
|
||||
break;
|
||||
|
||||
case PlayerType.Hls:
|
||||
// HLS also uses html player
|
||||
try {
|
||||
this.hlsPlayer.destroy();
|
||||
} catch (e) {
|
||||
console.warn("Failed to destroy hls player", e);
|
||||
}
|
||||
// fall through
|
||||
|
||||
case PlayerType.Html: {
|
||||
const videoPlayer = this.player as HTMLVideoElement;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
play() { console.log("Player: play"); this.player.play(); }
|
||||
|
||||
isPaused(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).isPaused();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).paused;
|
||||
}
|
||||
}
|
||||
pause() { console.log("Player: pause"); this.player.pause(); }
|
||||
|
||||
getVolume(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).getVolume();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).volume;
|
||||
}
|
||||
}
|
||||
setVolume(value: number) {
|
||||
console.log(`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);
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).volume = sanitizedVolume;
|
||||
}
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).isMuted();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).muted;
|
||||
}
|
||||
}
|
||||
setMute(value: boolean) {
|
||||
console.log(`Player: setMute ${value}`);
|
||||
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).setMute(value);
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).muted = value;
|
||||
}
|
||||
}
|
||||
|
||||
getPlaybackRate(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).getPlaybackRate();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).playbackRate;
|
||||
}
|
||||
}
|
||||
setPlaybackRate(value: number) {
|
||||
console.log(`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);
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).playbackRate = sanitizedSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
getDuration(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
const videoPlayer = this.player as dashjs.MediaPlayerClass;
|
||||
return isFinite(videoPlayer.duration()) ? videoPlayer.duration() : 0;
|
||||
} else { // HLS, HTML
|
||||
const videoPlayer = this.player as HTMLVideoElement;
|
||||
return isFinite(videoPlayer.duration) ? videoPlayer.duration : 0;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTime(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).time();
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).currentTime;
|
||||
}
|
||||
}
|
||||
setCurrentTime(value: number) {
|
||||
// console.log(`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;
|
||||
|
||||
if (!videoPlayer.isSeeking()) {
|
||||
videoPlayer.seek(sanitizedTime);
|
||||
}
|
||||
|
||||
} else { // HLS, HTML
|
||||
(this.player as HTMLVideoElement).currentTime = sanitizedTime;
|
||||
}
|
||||
}
|
||||
|
||||
getSource(): string {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
const videoPlayer = this.player as dashjs.MediaPlayerClass;
|
||||
return videoPlayer.getSource() instanceof String ? videoPlayer.getSource() as string : JSON.stringify(videoPlayer.getSource());
|
||||
} else { // HLS, HTML
|
||||
return (this.player as HTMLVideoElement).src;
|
||||
}
|
||||
}
|
||||
|
||||
getBufferLength(): number {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
const dashPlayer = this.player as dashjs.MediaPlayerClass;
|
||||
|
||||
let dashBufferLength = dashPlayer.getBufferLength("video")
|
||||
?? dashPlayer.getBufferLength("audio")
|
||||
?? dashPlayer.getBufferLength("text")
|
||||
?? dashPlayer.getBufferLength("image")
|
||||
?? 0;
|
||||
if (Number.isNaN(dashBufferLength))
|
||||
dashBufferLength = 0;
|
||||
|
||||
dashBufferLength += 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 (videoPlayer.currentTime >= start && videoPlayer.currentTime <= end) {
|
||||
maxBuffer = end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
isCaptionsSupported(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).getTracksFor('text').length > 0;
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
return this.hlsPlayer.allSubtitleTracks.length > 0;
|
||||
} else {
|
||||
return false; // HTML captions not currently supported
|
||||
}
|
||||
}
|
||||
|
||||
isCaptionsEnabled(): boolean {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
return (this.player as dashjs.MediaPlayerClass).isTextEnabled();
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
return this.hlsPlayer.subtitleDisplay;
|
||||
} else {
|
||||
return false; // HTML captions not currently supported
|
||||
}
|
||||
}
|
||||
|
||||
enableCaptions(enable: boolean) {
|
||||
if (this.playerType === PlayerType.Dash) {
|
||||
(this.player as dashjs.MediaPlayerClass).enableText(enable);
|
||||
} else if (this.playerType === PlayerType.Hls) {
|
||||
this.hlsPlayer.subtitleDisplay = enable;
|
||||
}
|
||||
// HTML captions not currently supported
|
||||
}
|
||||
|
||||
}
|
240
receivers/common/web/player/Preload.ts
Normal file
240
receivers/common/web/player/Preload.ts
Normal file
|
@ -0,0 +1,240 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: any;
|
||||
webOSAPI: any;
|
||||
webOS: any;
|
||||
targetAPI: any;
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (TARGET === 'electron') {
|
||||
// @ts-ignore
|
||||
const electronAPI = __non_webpack_require__('electron');
|
||||
|
||||
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),
|
||||
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)
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
} else if (TARGET === 'webOS') {
|
||||
require('lib/webOSTVjs-1.2.10/webOSTV.js');
|
||||
require('lib/webOSTVjs-1.2.10/webOSTV-dev.js');
|
||||
|
||||
const serviceId = 'com.futo.fcast.receiver.service';
|
||||
let onPlayCb, onPauseCb, onResumeCb;
|
||||
let onSeekCb, onSetVolumeCb, onSetSpeedCb;
|
||||
let playerWindowOpen = false;
|
||||
|
||||
const playService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"play",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
console.log(JSON.stringify(message));
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered play handler with service');
|
||||
}
|
||||
|
||||
if (message.value.playData !== null) {
|
||||
if (!playerWindowOpen) {
|
||||
playerWindowOpen = true;
|
||||
}
|
||||
|
||||
if (onPlayCb === undefined) {
|
||||
window.webOSAPI.pendingPlay = message.value.playData;
|
||||
}
|
||||
else {
|
||||
onPlayCb(null, message.value.playData);
|
||||
}
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: play ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
const pauseService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"pause",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered pause handler with service');
|
||||
}
|
||||
else {
|
||||
onPauseCb();
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: pause ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
const resumeService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"resume",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered resume handler with service');
|
||||
}
|
||||
else {
|
||||
onResumeCb();
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: resume ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
const stopService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"stop",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered stop handler with service');
|
||||
}
|
||||
else {
|
||||
playerWindowOpen = false;
|
||||
playService.cancel();
|
||||
pauseService.cancel();
|
||||
resumeService.cancel();
|
||||
stopService.cancel();
|
||||
seekService.cancel();
|
||||
setVolumeService.cancel();
|
||||
setSpeedService.cancel();
|
||||
|
||||
// window.open('../main_window/index.html');
|
||||
window.webOS.platformBack();
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: stop ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
const seekService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"seek",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered seek handler with service');
|
||||
}
|
||||
else {
|
||||
onSeekCb(null, message.value);
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: seek ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
const setVolumeService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"setvolume",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered setvolume handler with service');
|
||||
}
|
||||
else {
|
||||
onSetVolumeCb(null, message.value);
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: setvolume ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
const setSpeedService = window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method:"setspeed",
|
||||
parameters: {},
|
||||
onSuccess: (message: any) => {
|
||||
if (message.value.subscribed === true) {
|
||||
console.log('Player: Registered setspeed handler with service');
|
||||
}
|
||||
else {
|
||||
onSetSpeedCb(null, message.value);
|
||||
}
|
||||
},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: setspeed ${JSON.stringify(message)}`);
|
||||
},
|
||||
subscribe: true,
|
||||
resubscribe: true
|
||||
});
|
||||
|
||||
window.targetAPI = {
|
||||
sendPlaybackError: (error: PlaybackErrorMessage) => {
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'send_playback_error',
|
||||
parameters: { error },
|
||||
onSuccess: () => {},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: send_playback_error ${JSON.stringify(message)}`);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => {
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'send_playback_update',
|
||||
parameters: { update },
|
||||
// onSuccess: (message: any) => {
|
||||
// console.log(`Player: send_playback_update ${JSON.stringify(message)}`);
|
||||
// },
|
||||
onSuccess: () => {},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: send_playback_update ${JSON.stringify(message)}`);
|
||||
},
|
||||
});
|
||||
},
|
||||
sendVolumeUpdate: (update: VolumeUpdateMessage) => {
|
||||
window.webOS.service.request(`luna://${serviceId}/`, {
|
||||
method: 'send_volume_update',
|
||||
parameters: { update },
|
||||
onSuccess: () => {},
|
||||
onFailure: (message: any) => {
|
||||
console.error(`Player: send_volume_update ${JSON.stringify(message)}`);
|
||||
},
|
||||
});
|
||||
},
|
||||
onPlay: (callback: any) => { onPlayCb = callback; },
|
||||
onPause: (callback: any) => { onPauseCb = callback; },
|
||||
onResume: (callback: any) => { onResumeCb = callback; },
|
||||
onSeek: (callback: any) => { onSeekCb = callback; },
|
||||
onSetVolume: (callback: any) => { onSetVolumeCb = callback; },
|
||||
onSetSpeed: (callback: any) => { onSetSpeedCb = callback; }
|
||||
};
|
||||
|
||||
window.webOSAPI = {
|
||||
pendingPlay: null
|
||||
};
|
||||
|
||||
} else {
|
||||
// @ts-ignore
|
||||
console.log(`Attempting to run FCast player on unsupported target: ${TARGET}`);
|
||||
}
|
751
receivers/common/web/player/Renderer.ts
Normal file
751
receivers/common/web/player/Renderer.ts
Normal file
|
@ -0,0 +1,751 @@
|
|||
import dashjs from 'modules/dashjs';
|
||||
import Hls, { LevelLoadedData } from 'modules/hls.js';
|
||||
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
|
||||
import { Player, PlayerType } from './Player';
|
||||
import { targetPlayerCtrlStateUpdate, targetKeyDownEventListener } from 'src/player/Renderer';
|
||||
|
||||
function formatDuration(duration: number) {
|
||||
const totalSeconds = Math.floor(duration);
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = Math.floor(totalSeconds % 60);
|
||||
|
||||
const paddedMinutes = String(minutes).padStart(2, '0');
|
||||
const paddedSeconds = String(seconds).padStart(2, '0');
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${paddedMinutes}:${paddedSeconds}`;
|
||||
} else {
|
||||
return `${paddedMinutes}:${paddedSeconds}`;
|
||||
}
|
||||
}
|
||||
|
||||
function sendPlaybackUpdate(updateState: number) {
|
||||
const updateMessage = new PlaybackUpdateMessage(Date.now(), player.getCurrentTime(), player.getDuration(), updateState, player.getPlaybackRate());
|
||||
|
||||
if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) {
|
||||
lastPlayerUpdateGenerationTime = updateMessage.generationTime;
|
||||
window.targetAPI.sendPlaybackUpdate(updateMessage);
|
||||
}
|
||||
};
|
||||
|
||||
function onPlayerLoad(value: PlayMessage, currentPlaybackRate?: number, currentVolume?: number) {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.Load);
|
||||
|
||||
// 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);
|
||||
} else if (currentPlaybackRate) {
|
||||
player.setPlaybackRate(currentPlaybackRate);
|
||||
} else {
|
||||
player.setPlaybackRate(1.0);
|
||||
}
|
||||
playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate);
|
||||
|
||||
if (currentVolume) {
|
||||
volumeChangeHandler(currentVolume);
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
||||
player.play();
|
||||
}
|
||||
|
||||
// HTML elements
|
||||
const videoElement = document.getElementById("videoPlayer") as HTMLVideoElement;
|
||||
const videoCaptions = document.getElementById("videoCaptions") as HTMLDivElement;
|
||||
|
||||
const playerControls = document.getElementById("controls");
|
||||
|
||||
const playerCtrlAction = document.getElementById("action");
|
||||
const playerCtrlVolume = document.getElementById("volume");
|
||||
|
||||
const playerCtrlProgressBar = document.getElementById("progressBar");
|
||||
const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer");
|
||||
const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress");
|
||||
const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition");
|
||||
const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle");
|
||||
const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea");
|
||||
|
||||
const playerCtrlVolumeBar = document.getElementById("volumeBar");
|
||||
const playerCtrlVolumeBarProgress = document.getElementById("volumeBarProgress");
|
||||
const playerCtrlVolumeBarHandle = document.getElementById("volumeBarHandle");
|
||||
const playerCtrlVolumeBarInteractiveArea = document.getElementById("volumeBarInteractiveArea");
|
||||
|
||||
const playerCtrlLiveBadge = document.getElementById("liveBadge");
|
||||
const playerCtrlPosition = document.getElementById("position");
|
||||
const playerCtrlDuration = document.getElementById("duration");
|
||||
|
||||
const playerCtrlCaptions = document.getElementById("captions");
|
||||
const playerCtrlSpeed = document.getElementById("speed");
|
||||
|
||||
const playerCtrlSpeedMenu = document.getElementById("speedMenu");
|
||||
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 livePositionDelta = 5.0;
|
||||
const livePositionWindow = livePositionDelta * 4;
|
||||
let player: Player;
|
||||
let playerPrevTime: number = 0;
|
||||
let lastPlayerUpdateGenerationTime = 0;
|
||||
let isLive = false;
|
||||
let isLivePosition = false;
|
||||
|
||||
function onPlay(_event, value: PlayMessage) {
|
||||
console.log("Handle play message renderer", JSON.stringify(value));
|
||||
const currentVolume = player ? player.getVolume() : null;
|
||||
const currentPlaybackRate = player ? player.getPlaybackRate() : null;
|
||||
|
||||
playerPrevTime = 0;
|
||||
lastPlayerUpdateGenerationTime = 0;
|
||||
isLive = false;
|
||||
isLivePosition = false;
|
||||
|
||||
if (player) {
|
||||
if (player.getSource() === value.url) {
|
||||
if (value.time) {
|
||||
if (Math.abs(value.time - player.getCurrentTime()) < 5000) {
|
||||
console.warn(`Skipped changing video URL because URL and time is (nearly) unchanged: ${value.url}, ${player.getSource()}, ${formatDuration(value.time)}, ${formatDuration(player.getCurrentTime())}`);
|
||||
} else {
|
||||
console.info(`Skipped changing video URL because URL is the same, but time was changed, seeking instead: ${value.url}, ${player.getSource()}, ${formatDuration(value.time)}, ${formatDuration(player.getCurrentTime())}`);
|
||||
|
||||
player.setCurrentTime(value.time);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
player.destroy();
|
||||
}
|
||||
|
||||
if ((value.url || value.content) && value.container && videoElement) {
|
||||
if (value.container === 'application/dash+xml') {
|
||||
console.log("Loading dash player");
|
||||
const dashPlayer = dashjs.MediaPlayer().create();
|
||||
player = new Player(PlayerType.Dash, dashPlayer);
|
||||
|
||||
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) });
|
||||
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();
|
||||
}
|
||||
});
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_RATE_CHANGED, () => { sendPlaybackUpdate(dashPlayer.isPaused() ? 2 : 1) });
|
||||
|
||||
// Buffering UI update when paused
|
||||
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();
|
||||
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
|
||||
});
|
||||
|
||||
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({
|
||||
message: `DashJS PLAYBACK_ERROR: ${JSON.stringify(data)}`
|
||||
})});
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { onPlayerLoad(value, currentPlaybackRate, currentVolume); });
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.CUE_ENTER, (e: any) => {
|
||||
const subtitle = document.createElement("p")
|
||||
subtitle.setAttribute("id", "subtitle-" + e.cueID)
|
||||
|
||||
subtitle.textContent = e.text;
|
||||
videoCaptions.appendChild(subtitle);
|
||||
});
|
||||
|
||||
dashPlayer.on(dashjs.MediaPlayer.events.CUE_EXIT, (e: any) => {
|
||||
document.getElementById("subtitle-" + e.cueID)?.remove();
|
||||
});
|
||||
|
||||
dashPlayer.updateSettings({
|
||||
// debug: {
|
||||
// logLevel: dashjs.LogLevel.LOG_LEVEL_INFO
|
||||
// },
|
||||
streaming: {
|
||||
text: {
|
||||
dispatchForManualRendering: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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)) {
|
||||
console.log("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) => {
|
||||
window.targetAPI.sendPlaybackError({
|
||||
message: `HLS player error: ${JSON.stringify(data)}`
|
||||
});
|
||||
});
|
||||
|
||||
hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => {
|
||||
isLive = level.details.live;
|
||||
isLivePosition = isLive ? true : false;
|
||||
});
|
||||
|
||||
player = new Player(PlayerType.Hls, videoElement, 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 {
|
||||
console.log("Loading html player");
|
||||
player = new Player(PlayerType.Html, videoElement);
|
||||
|
||||
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) };
|
||||
videoElement.ontimeupdate = () => {
|
||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||
|
||||
if (Math.abs(videoElement.currentTime - playerPrevTime) >= playbackUpdateInterval) {
|
||||
sendPlaybackUpdate(videoElement.paused ? 2 : 1);
|
||||
playerPrevTime = videoElement.currentTime;
|
||||
}
|
||||
};
|
||||
// Buffering UI update when paused
|
||||
videoElement.onprogress = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); };
|
||||
videoElement.onratechange = () => { sendPlaybackUpdate(videoElement.paused ? 2 : 1) };
|
||||
videoElement.onvolumechange = () => {
|
||||
const updateVolume = videoElement.muted ? 0 : videoElement.volume;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
|
||||
window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
|
||||
};
|
||||
|
||||
videoElement.onerror = (event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error) => {
|
||||
console.error("Player error", {source, lineno, colno, error});
|
||||
};
|
||||
|
||||
videoElement.onloadedmetadata = () => { onPlayerLoad(value, currentPlaybackRate, currentVolume); };
|
||||
}
|
||||
}
|
||||
|
||||
// Sender generated event handlers
|
||||
window.targetAPI.onPause(() => { player.pause(); });
|
||||
window.targetAPI.onResume(() => { player.play(); });
|
||||
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); });
|
||||
};
|
||||
|
||||
window.targetAPI.onPlay(onPlay);
|
||||
|
||||
let scrubbing = false;
|
||||
let volumeChanging = false;
|
||||
|
||||
enum PlayerControlEvent {
|
||||
Load,
|
||||
Pause,
|
||||
Play,
|
||||
VolumeChange,
|
||||
TimeUpdate,
|
||||
UiFadeOut,
|
||||
UiFadeIn,
|
||||
SetCaptions,
|
||||
ToggleSpeedMenu,
|
||||
SetPlaybackRate,
|
||||
ToggleFullscreen,
|
||||
ExitFullscreen,
|
||||
}
|
||||
|
||||
// UI update handlers
|
||||
function playerCtrlStateUpdate(event: PlayerControlEvent) {
|
||||
switch (event) {
|
||||
case PlayerControlEvent.Load: {
|
||||
playerCtrlProgressBarBuffer.setAttribute("style", "width: 0px");
|
||||
playerCtrlProgressBarProgress.setAttribute("style", "width: 0px");
|
||||
playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetLeft}px`);
|
||||
|
||||
const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth);
|
||||
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
|
||||
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`);
|
||||
|
||||
if (isLive) {
|
||||
playerCtrlLiveBadge.setAttribute("style", "display: block");
|
||||
playerCtrlPosition.setAttribute("style", "display: none");
|
||||
playerCtrlDuration.setAttribute("style", "display: none");
|
||||
}
|
||||
else {
|
||||
playerCtrlLiveBadge.setAttribute("style", "display: none");
|
||||
playerCtrlPosition.setAttribute("style", "display: block");
|
||||
playerCtrlDuration.setAttribute("style", "display: block");
|
||||
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
|
||||
playerCtrlDuration.innerHTML = `/  ${formatDuration(player.getDuration())}`;
|
||||
}
|
||||
|
||||
if (player.isCaptionsSupported()) {
|
||||
playerCtrlCaptions.setAttribute("style", "display: block");
|
||||
videoCaptions.setAttribute("style", "display: block");
|
||||
}
|
||||
else {
|
||||
playerCtrlCaptions.setAttribute("style", "display: none");
|
||||
videoCaptions.setAttribute("style", "display: none");
|
||||
player.enableCaptions(false);
|
||||
}
|
||||
playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
|
||||
break;
|
||||
}
|
||||
|
||||
case PlayerControlEvent.Pause:
|
||||
playerCtrlAction.setAttribute("class", "play");
|
||||
stopUiHideTimer();
|
||||
break;
|
||||
|
||||
case PlayerControlEvent.Play:
|
||||
playerCtrlAction.setAttribute("class", "pause");
|
||||
startUiHideTimer();
|
||||
break;
|
||||
|
||||
case PlayerControlEvent.VolumeChange: {
|
||||
// console.log(`VolumeChange: isMute ${player.isMuted()}, volume: ${player.getVolume()}`);
|
||||
const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth);
|
||||
|
||||
if (player.isMuted()) {
|
||||
playerCtrlVolume.setAttribute("class", "mute");
|
||||
playerCtrlVolumeBarProgress.setAttribute("style", `width: 0px`);
|
||||
playerCtrlVolumeBarHandle.setAttribute("style", `left: 0px`);
|
||||
}
|
||||
else if (player.getVolume() >= 0.5) {
|
||||
playerCtrlVolume.setAttribute("class", "volume_high");
|
||||
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
|
||||
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume}px`);
|
||||
} else {
|
||||
playerCtrlVolume.setAttribute("class", "volume_low");
|
||||
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
|
||||
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume}px`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PlayerControlEvent.TimeUpdate: {
|
||||
// console.log(`TimeUpdate: Position: ${player.getCurrentTime()}, Duration: ${player.getDuration()}`);
|
||||
|
||||
if (isLive) {
|
||||
if (isLivePosition && player.getDuration() - player.getCurrentTime() > livePositionWindow) {
|
||||
isLivePosition = false;
|
||||
playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`);
|
||||
}
|
||||
else if (!isLivePosition && player.getDuration() - player.getCurrentTime() <= livePositionWindow) {
|
||||
isLivePosition = true;
|
||||
playerCtrlLiveBadge.setAttribute("style", `background-color: red`);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLivePosition) {
|
||||
playerCtrlProgressBarProgress.setAttribute("style", `width: ${playerCtrlProgressBar.offsetWidth}px`);
|
||||
playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetWidth + playerCtrlProgressBar.offsetLeft}px`);
|
||||
}
|
||||
else {
|
||||
const buffer = Math.round((player.getBufferLength() / player.getDuration()) * playerCtrlProgressBar.offsetWidth);
|
||||
const progress = Math.round((player.getCurrentTime() / player.getDuration()) * playerCtrlProgressBar.offsetWidth);
|
||||
const handle = progress + playerCtrlProgressBar.offsetLeft;
|
||||
|
||||
playerCtrlProgressBarBuffer.setAttribute("style", `width: ${buffer}px`);
|
||||
playerCtrlProgressBarProgress.setAttribute("style", `width: ${progress}px`);
|
||||
playerCtrlProgressBarHandle.setAttribute("style", `left: ${handle}px`);
|
||||
|
||||
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PlayerControlEvent.UiFadeOut:
|
||||
document.body.style.cursor = "none";
|
||||
playerControls.setAttribute("style", "opacity: 0");
|
||||
|
||||
if (player.isCaptionsEnabled()) {
|
||||
videoCaptions.setAttribute("style", "display: block; bottom: 75px;");
|
||||
} else {
|
||||
videoCaptions.setAttribute("style", "display: none; bottom: 75px;");
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
|
||||
case PlayerControlEvent.UiFadeIn:
|
||||
document.body.style.cursor = "default";
|
||||
playerControls.setAttribute("style", "opacity: 1");
|
||||
|
||||
if (player.isCaptionsEnabled()) {
|
||||
videoCaptions.setAttribute("style", "display: block; bottom: 160px;");
|
||||
} else {
|
||||
videoCaptions.setAttribute("style", "display: none; bottom: 160px;");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PlayerControlEvent.SetCaptions:
|
||||
if (player.isCaptionsEnabled()) {
|
||||
playerCtrlCaptions.setAttribute("class", "captions_on");
|
||||
videoCaptions.setAttribute("style", "display: block");
|
||||
} else {
|
||||
playerCtrlCaptions.setAttribute("class", "captions_off");
|
||||
videoCaptions.setAttribute("style", "display: none");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PlayerControlEvent.ToggleSpeedMenu: {
|
||||
if (playerCtrlSpeedMenuShown) {
|
||||
playerCtrlSpeedMenu.setAttribute("style", "display: none");
|
||||
} else {
|
||||
playerCtrlSpeedMenu.setAttribute("style", "display: block");
|
||||
}
|
||||
|
||||
playerCtrlSpeedMenuShown = !playerCtrlSpeedMenuShown;
|
||||
break;
|
||||
}
|
||||
|
||||
case PlayerControlEvent.SetPlaybackRate: {
|
||||
const rate = player.getPlaybackRate().toFixed(2);
|
||||
const entryElement = document.getElementById(`speedMenuEntry_${rate}_enabled`);
|
||||
|
||||
playbackRates.forEach(r => {
|
||||
const entry = document.getElementById(`speedMenuEntry_${r}_enabled`);
|
||||
entry.setAttribute("style", "opacity: 0");
|
||||
});
|
||||
|
||||
// Ignore updating GUI for custom rates
|
||||
if (entryElement !== null) {
|
||||
entryElement.setAttribute("style", "opacity: 1");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
targetPlayerCtrlStateUpdate(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function scrubbingMouseUIHandler(e: MouseEvent) {
|
||||
const progressBarOffset = e.offsetX - 8;
|
||||
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
|
||||
let time = isLive ? Math.round((1 - (progressBarOffset / progressBarWidth)) * player.getDuration()) : Math.round((progressBarOffset / progressBarWidth) * player.getDuration());
|
||||
time = Math.min(player.getDuration(), Math.max(0.0, time));
|
||||
|
||||
if (scrubbing && isLive && e.buttons === 1) {
|
||||
isLivePosition = false;
|
||||
playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`);
|
||||
}
|
||||
|
||||
const livePrefix = isLive && Math.floor(time) !== 0 ? "-" : "";
|
||||
playerCtrlProgressBarPosition.textContent = isLive ? `${livePrefix}${formatDuration(time)}` : formatDuration(time);
|
||||
|
||||
let offset = e.offsetX - (playerCtrlProgressBarPosition.offsetWidth / 2);
|
||||
offset = Math.min(PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBarPosition.offsetWidth / 1), Math.max(8, offset));
|
||||
playerCtrlProgressBarPosition.setAttribute("style", `display: block; left: ${offset}px`);
|
||||
}
|
||||
|
||||
// Receiver generated event handlers
|
||||
playerCtrlAction.onclick = () => {
|
||||
if (player.isPaused()) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
};
|
||||
|
||||
playerCtrlVolume.onclick = () => { player.setMute(!player.isMuted()); };
|
||||
|
||||
PlayerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) };
|
||||
PlayerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; };
|
||||
PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
|
||||
if (e.buttons === 0) {
|
||||
volumeChanging = false;
|
||||
}
|
||||
|
||||
scrubbingMouseUIHandler(e);
|
||||
};
|
||||
PlayerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); };
|
||||
PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
|
||||
|
||||
function scrubbingMouseHandler(e: MouseEvent) {
|
||||
const progressBarOffset = e.offsetX - 8;
|
||||
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
|
||||
let time = Math.round((progressBarOffset / progressBarWidth) * player.getDuration());
|
||||
time = Math.min(player.getDuration(), Math.max(0.0, time));
|
||||
|
||||
if (scrubbing && e.buttons === 1) {
|
||||
player.setCurrentTime(time);
|
||||
}
|
||||
|
||||
scrubbingMouseUIHandler(e);
|
||||
}
|
||||
|
||||
playerCtrlVolumeBarInteractiveArea.onmousedown = (e: MouseEvent) => { volumeChanging = true; volumeChangeMouseHandler(e) };
|
||||
playerCtrlVolumeBarInteractiveArea.onmouseup = () => { volumeChanging = false; };
|
||||
playerCtrlVolumeBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
|
||||
if (e.buttons === 0) {
|
||||
scrubbing = false;
|
||||
}
|
||||
};
|
||||
playerCtrlVolumeBarInteractiveArea.onmousemove = (e: MouseEvent) => { volumeChangeMouseHandler(e) };
|
||||
playerCtrlVolumeBarInteractiveArea.onwheel = (e: WheelEvent) => {
|
||||
const delta = -e.deltaY;
|
||||
|
||||
if (delta > 0 ) {
|
||||
volumeChangeHandler(Math.min(player.getVolume() + volumeIncrement, 1));
|
||||
} else if (delta < 0) {
|
||||
volumeChangeHandler(Math.max(player.getVolume() - volumeIncrement, 0));
|
||||
}
|
||||
};
|
||||
|
||||
function volumeChangeMouseHandler(e: MouseEvent) {
|
||||
if (volumeChanging && e.buttons === 1) {
|
||||
const volumeBarOffsetX = e.offsetX - 8;
|
||||
const volumeBarWidth = playerCtrlVolumeBarInteractiveArea.offsetWidth - 16;
|
||||
const volume = volumeBarOffsetX / volumeBarWidth;
|
||||
volumeChangeHandler(volume);
|
||||
}
|
||||
}
|
||||
|
||||
function volumeChangeHandler(volume: number) {
|
||||
if (!player.isMuted() && volume <= 0) {
|
||||
player.setMute(true);
|
||||
}
|
||||
else if (player.isMuted() && volume > 0) {
|
||||
player.setMute(false);
|
||||
}
|
||||
|
||||
player.setVolume(volume);
|
||||
}
|
||||
|
||||
playerCtrlLiveBadge.onclick = () => { setLivePosition(); };
|
||||
|
||||
function setLivePosition() {
|
||||
if (!isLivePosition) {
|
||||
isLivePosition = true;
|
||||
|
||||
player.setCurrentTime(player.getDuration() - livePositionDelta);
|
||||
playerCtrlLiveBadge.setAttribute("style", `background-color: red`);
|
||||
|
||||
if (player.isPaused()) {
|
||||
player.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playerCtrlCaptions.onclick = () => { player.enableCaptions(!player.isCaptionsEnabled()); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); };
|
||||
playerCtrlSpeed.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); };
|
||||
|
||||
playbackRates.forEach(r => {
|
||||
const entry = document.getElementById(`speedMenuEntry_${r}`);
|
||||
entry.onclick = () => {
|
||||
player.setPlaybackRate(parseFloat(r));
|
||||
playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate);
|
||||
playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu);
|
||||
};
|
||||
});
|
||||
|
||||
videoElement.onclick = () => {
|
||||
if (!playerCtrlSpeedMenuShown) {
|
||||
if (player.isPaused()) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Component hiding
|
||||
let uiHideTimer = null;
|
||||
let uiVisible = true;
|
||||
|
||||
function startUiHideTimer() {
|
||||
if (uiHideTimer === null) {
|
||||
uiHideTimer = window.setTimeout(() => {
|
||||
uiHideTimer = null;
|
||||
uiVisible = false;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopUiHideTimer() {
|
||||
if (uiHideTimer) {
|
||||
window.clearTimeout(uiHideTimer);
|
||||
uiHideTimer = null;
|
||||
}
|
||||
|
||||
if (!uiVisible) {
|
||||
uiVisible = true;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
|
||||
}
|
||||
}
|
||||
|
||||
document.onmouseout = () => {
|
||||
if (uiHideTimer) {
|
||||
window.clearTimeout(uiHideTimer);
|
||||
uiHideTimer = null;
|
||||
}
|
||||
|
||||
uiVisible = false;
|
||||
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
|
||||
}
|
||||
|
||||
document.onmousemove = () => {
|
||||
stopUiHideTimer();
|
||||
|
||||
if (player && !player.isPaused()) {
|
||||
startUiHideTimer();
|
||||
}
|
||||
};
|
||||
|
||||
window.onresize = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); };
|
||||
|
||||
// Listener for hiding speed menu when clicking outside element
|
||||
document.addEventListener('click', (event: MouseEvent) => {
|
||||
const node = event.target as Node;
|
||||
if (playerCtrlSpeedMenuShown && !playerCtrlSpeed.contains(node) && !playerCtrlSpeedMenu.contains(node)){
|
||||
playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu);
|
||||
}
|
||||
});
|
||||
|
||||
// Add the keydown event listener to the document
|
||||
const skipInterval = 10;
|
||||
const volumeIncrement = 0.1;
|
||||
|
||||
function keyDownEventListener(event: any) {
|
||||
// console.log("KeyDown", event);
|
||||
|
||||
switch (event.code) {
|
||||
case 'KeyF':
|
||||
case 'F11':
|
||||
playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'Escape':
|
||||
playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
// Skip back
|
||||
player.setCurrentTime(Math.max(player.getCurrentTime() - skipInterval, 0));
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
// Skip forward
|
||||
if (!isLivePosition) {
|
||||
player.setCurrentTime(Math.min(player.getCurrentTime() + skipInterval, player.getDuration()));
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "Home":
|
||||
player.setCurrentTime(0);
|
||||
event.preventDefault();
|
||||
break;
|
||||
case "End":
|
||||
if (isLive) {
|
||||
setLivePosition();
|
||||
}
|
||||
else {
|
||||
player.setCurrentTime(player.getDuration());
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyK':
|
||||
case 'Space':
|
||||
case 'Enter':
|
||||
// Pause/Continue
|
||||
if (player.isPaused()) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'KeyM':
|
||||
// Mute toggle
|
||||
player.setMute(!player.isMuted());
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
// Volume up
|
||||
volumeChangeHandler(Math.min(player.getVolume() + volumeIncrement, 1));
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
// Volume down
|
||||
volumeChangeHandler(Math.max(player.getVolume() - volumeIncrement, 0));
|
||||
break;
|
||||
default:
|
||||
targetKeyDownEventListener(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', keyDownEventListener);
|
||||
|
||||
export {
|
||||
videoElement,
|
||||
PlayerControlEvent,
|
||||
onPlay,
|
||||
playerCtrlStateUpdate,
|
||||
};
|
468
receivers/common/web/player/common.css
Normal file
468
receivers/common/web/player/common.css
Normal file
|
@ -0,0 +1,468 @@
|
|||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: black;
|
||||
color: white;
|
||||
width: 100vw;
|
||||
max-width: 100%;
|
||||
height: 100vh;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
#videoPlayer {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
|
||||
/* height: 100%; */
|
||||
height: 120px;
|
||||
width: 100%;
|
||||
/* background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%); */
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.0) 35%);
|
||||
|
||||
background-size: 100% 300px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom;
|
||||
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.volumeContainer {
|
||||
position: relative;
|
||||
height: 24px;
|
||||
width: 92px;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.volumeBar {
|
||||
position: absolute;
|
||||
/* left: 12px; */
|
||||
left: 8px;
|
||||
top: 10px;
|
||||
height: 4px;
|
||||
/* width: 72px; */
|
||||
width: 76px;
|
||||
background-color: #999999;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.volumeBarInteractiveArea {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
/* left: 8px; */
|
||||
top: 0px;
|
||||
height: 24px;
|
||||
width: 92px;
|
||||
/* width: 84px; */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volumeBarHandle {
|
||||
position: absolute;
|
||||
left: 84px;
|
||||
top: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
/* background-color: #ffffff; */
|
||||
background-color: #c9c9c9;
|
||||
box-shadow: 0px 32px 64px 0px rgba(0, 0, 0, 0.56), 0px 2px 21px 0px rgba(0, 0, 0, 0.55);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.volumeBarProgress {
|
||||
position: absolute;
|
||||
/* left: 12px; */
|
||||
left: 8px;
|
||||
top: 10px;
|
||||
height: 4px;
|
||||
width: 76px;
|
||||
/* background-color: #ffffff; */
|
||||
background-color: #c9c9c9;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progressBarContainer {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
height: 4px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.progressBarInteractiveArea {
|
||||
position: absolute;
|
||||
/* bottom: 60px; */
|
||||
/* left: 24px; */
|
||||
/* right: 24px; */
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
z-index: 999;
|
||||
}
|
||||
.progressBarChapterContainer {
|
||||
position: absolute;
|
||||
bottom: 73px;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
height: 4px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
/* position: absolute; */
|
||||
position: relative;
|
||||
/* bottom: 70px; */
|
||||
/* left: 24px; */
|
||||
/* right: 24px; */
|
||||
left: 8px;
|
||||
width: calc(100% - 16px);
|
||||
height: 4px;
|
||||
background-color: #99999945;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progressBarBuffer {
|
||||
/* position: absolute; */
|
||||
position: relative;
|
||||
/* bottom: 70px; */
|
||||
/* left: 24px; */
|
||||
left: 8px;
|
||||
bottom: 4px;
|
||||
height: 4px;
|
||||
background-color: #D9D9D945;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progressBarProgress {
|
||||
/* position: absolute; */
|
||||
position: relative;
|
||||
/* bottom: 70px; */
|
||||
/* left: 24px; */
|
||||
left: 8px;
|
||||
bottom: 8px;
|
||||
height: 4px;
|
||||
width: 0px;
|
||||
background-color: #019BE7;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progressBarPosition {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
padding: 2px 5px;
|
||||
|
||||
font-family: InterVariable;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.progressBarHandle {
|
||||
position: absolute;
|
||||
/* bottom: 70px; */
|
||||
bottom: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: -8px;
|
||||
margin-bottom: -8px;
|
||||
background-color: #019BE7;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.positionContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
|
||||
font-family: InterVariable;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
|
||||
.position {
|
||||
margin-right: 10px;
|
||||
vertical-align: bottom;
|
||||
|
||||
color: #c9c9c9;
|
||||
}
|
||||
|
||||
.duration {
|
||||
opacity: 0.6;
|
||||
|
||||
color: #c9c9c9;
|
||||
}
|
||||
|
||||
.liveBadge {
|
||||
background-color: red;
|
||||
/* margin-top: -2px; */
|
||||
/* padding: 5px 5px; */
|
||||
padding: 2px 5px;
|
||||
border-radius: 4px;
|
||||
/* margin-left: 10px; */
|
||||
margin-right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_play.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.play:hover {
|
||||
background-image: url("../assets/icons/player/icon24_play_active.svg");
|
||||
}
|
||||
|
||||
.pause {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_pause.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.pause:hover {
|
||||
background-image: url("../assets/icons/player/icon24_pause_active.svg");
|
||||
}
|
||||
|
||||
.volume_high {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_volume_more_50pct.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.volume_high:hover {
|
||||
background-image: url("../assets/icons/player/icon24_volume_more_50pct_active.svg");
|
||||
}
|
||||
|
||||
.volume_low {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_volume_less_50pct.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.volume_low:hover {
|
||||
background-image: url("../assets/icons/player/icon24_volume_less_50pct_active.svg");
|
||||
}
|
||||
|
||||
.mute {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_mute.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.mute:hover {
|
||||
background-image: url("../assets/icons/player/icon24_mute_active.svg");
|
||||
}
|
||||
|
||||
.speed {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_speed.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.speed:hover {
|
||||
background-image: url("../assets/icons/player/icon24_speed_active.svg");
|
||||
}
|
||||
|
||||
.captions_off {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_cc_off.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.captions_off:hover {
|
||||
background-image: url("../assets/icons/player/icon24_cc_off_active.svg");
|
||||
}
|
||||
|
||||
.captions_on {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_cc_on.svg");
|
||||
transition: background-image 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.captions_on:hover {
|
||||
background-image: url("../assets/icons/player/icon24_cc_on_active.svg");
|
||||
}
|
||||
|
||||
.leftButtonContainer {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
height: 24px;
|
||||
/* width: calc(50% - 24px); */
|
||||
right: 160px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
overflow: hidden;
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
height: 24px;
|
||||
/* width: calc(50% - 24px); */
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.captionsContainer {
|
||||
/* display: none; */
|
||||
position: relative;
|
||||
/* top: -200px; */
|
||||
bottom: 160px;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
|
||||
font-family: InterVariable;
|
||||
font-size: 28px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding: 0px 5px;
|
||||
|
||||
width: fit-content;
|
||||
|
||||
transition: bottom 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.speedMenu {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 60px;
|
||||
height: calc(55vh);
|
||||
max-height: 368px;
|
||||
|
||||
background-color: #141414;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #2E2E2E;
|
||||
scrollbar-width: thin;
|
||||
overflow: auto;
|
||||
|
||||
font-family: InterVariable;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
|
||||
box-shadow: 0px 1.852px 3.148px 0px rgba(0, 0, 0, 0.06), 0px 8.148px 6.519px 0px rgba(0, 0, 0, 0.10), 0px 20px 13px 0px rgba(0, 0, 0, 0.13), 0px 38.519px 25.481px 0px rgba(0, 0, 0, 0.15), 0px 64.815px 46.852px 0px rgba(0, 0, 0, 0.19), 0px 100px 80px 0px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.speedMenuTitle {
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.speedMenuEntry {
|
||||
display: flex;
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.speedMenuEntry:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.speedMenuSeparator {
|
||||
height: 1px;
|
||||
background: #2E2E2E;
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.speedMenuEntryEnabled {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
|
||||
background-image: url("../assets/icons/player/icon24_check_thin.svg");
|
||||
background-size: cover;
|
||||
opacity: 0;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue