1
0
Fork 0
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:
Michael Hollister 2024-12-09 00:56:55 -06:00
parent b7e304b987
commit 90e1f4de1a
118 changed files with 18279 additions and 1746 deletions

View 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
}
}

View 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}`);
}

View 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 = `/&nbsp&nbsp${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,
};

View 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;
}