1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-08-13 02:52:48 +00:00

Initial commit of new video player

This commit is contained in:
Michael Hollister 2024-11-04 09:17:20 -06:00
parent d4a900e902
commit 755db076e7
58 changed files with 2261 additions and 475 deletions

View file

@ -1,6 +1,6 @@
import * as mdns from 'mdns-js';
import * as cp from 'child_process';
import * as os from 'os';
import mdns = require('mdns-js');
const cp = require('child_process');
const os = require('os');
export class DiscoveryService {
private serviceTcp: any;

View file

@ -1,4 +1,4 @@
import * as net from 'net';
import net = require('net');
import { EventEmitter } from 'node:events';
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
import { WebSocket } from 'ws';

View file

@ -119,6 +119,8 @@ export default class Main {
Main.playerWindow = new BrowserWindow({
fullscreen: true,
autoHideMenuBar: true,
minWidth: 515,
minHeight: 290,
icon: path.join(__dirname, 'icon512.png'),
webPreferences: {
preload: path.join(__dirname, 'player/preload.js')
@ -166,6 +168,15 @@ export default class Main {
});
});
ipcMain.handle('is-full-screen', async () => {
const window = Main.playerWindow;
if (!window) {
return;
}
return window.isFullScreen();
});
ipcMain.on('toggle-full-screen', () => {
const window = Main.playerWindow;
if (!window) {
@ -330,7 +341,7 @@ export default class Main {
static main(app: Electron.App) {
Main.application = app;
const argv = yargs(hideBin(process.argv))
const argv = yargs(hideBin(process.argv))
.parserConfiguration({
'boolean-negation': false
})

View file

@ -1,4 +1,4 @@
import * as net from 'net';
import net = require('net');
import { FCastSession, Opcode } from './FCastSession';
import { EventEmitter } from 'node:events';
import { dialog } from 'electron';

View file

@ -1,4 +1,4 @@
const { contextBridge, ipcRenderer } = require('electron');
import { contextBridge, ipcRenderer } from 'electron';
let deviceInfo;
ipcRenderer.on("device-info", (_event, value) => {

View file

@ -1,21 +1,11 @@
const options = {
textTrackSettings: false,
autoplay: true,
loop: true,
controls: false
};
const player = videojs("video-player", options, function onPlayerReady() {
player.src({ type: "video/mp4", src: "./c.mp4" });
});
import QRCode from 'qrcode';
window.electronAPI.onDeviceInfo(renderIPsAndQRCode);
if(window.electronAPI.getDeviceInfo()) {
console.log("device info already present");
renderIPsAndQRCode();
}
}
function renderIPsAndQRCode() {
const value = window.electronAPI.getDeviceInfo();
@ -42,12 +32,16 @@ function renderIPsAndQRCode() {
console.log("qr", {json, url, base64});
const qrCodeElement = document.getElementById('qr-code');
new QRCode(qrCodeElement, {
text: url,
QRCode.toCanvas(qrCodeElement, url, {
margin: 0,
width: 256,
height: 256,
colorDark : "#000000",
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.M
color: {
dark : "#000000",
light : "#ffffff",
},
errorCorrectionLevel : "M",
},
(e) => {
console.log(`Error rendering QR Code: ${e}`)
});
}

Binary file not shown.

View file

@ -2,17 +2,13 @@
<html>
<head>
<meta charset="UTF-8">
<link href="./video-js.min.css" rel="stylesheet">
<link rel="stylesheet" href="./style.css" />
<title>FCast Receiver</title>
</head>
<body>
<div id="main-container">
<video id="video-player" class="video-js" controls preload="auto" data-setup='{}' style="object-fit: cover;">
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a>
</p>
<video id="video-player" class="video" autoplay loop>
<source src="../../assets/video/background.mp4" type="video/mp4">
</video>
<div id="overlay">
<div id="title">FCast</div>
@ -25,7 +21,7 @@
<div>Port<br>46899 (TCP), 46898 (WS)</div>
</div>
<div id="automatic-discovery">Automatic discovery is available via mDNS</div>
<div id="qr-code"></div>
<canvas id="qr-code"></canvas>
<div id="scan-to-connect" style="font-weight: bold;">Scan to connect</div>
</div>
<!--<div id="update-dialog">There is an update available. Do you wish to update?</div>
@ -38,9 +34,6 @@
<div id="window-can-be-closed" style="color: #666666; position: absolute; bottom: 0; margin-bottom: 20px;">App will continue to run as tray app when the window is closed</div>
</div>
</div>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
<script src="./video.min.js"></script>
<script src="./qrcode.min.js"></script>
<script src="./renderer.js"></script>
</body>
</html>

File diff suppressed because one or more lines are too long

View file

@ -6,11 +6,13 @@ body, html {
#main-container {
position: relative;
height: 100%;
overflow: hidden;
}
.video-js {
.video {
height: 100%;
width: 100%;
object-fit: cover;
}
#overlay {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,223 @@
import dashjs from 'dashjs';
import Hls from '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;
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;
}
}
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,24 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { contextBridge, ipcRenderer } from 'electron';
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from '../Packets';
declare global {
interface Window {
electronAPI: any;
}
}
contextBridge.exposeInMainWorld('electronAPI', {
isFullScreen: () => ipcRenderer.invoke('is-full-screen'),
toggleFullScreen: () => ipcRenderer.send('toggle-full-screen'),
exitFullScreen: () => ipcRenderer.send('exit-full-screen'),
sendPlaybackError: (error: PlaybackErrorMessage) => ipcRenderer.send('send-playback-error', error),
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => ipcRenderer.send('send-playback-update', update),
sendVolumeUpdate: (update: VolumeUpdateMessage) => ipcRenderer.send('send-volume-update', update),
onPlay: (callback: any) => ipcRenderer.on("play", callback),
onPause: (callback: any) => ipcRenderer.on("pause", callback),
onResume: (callback: any) => ipcRenderer.on("resume", callback),
onSeek: (callback: any) => ipcRenderer.on("seek", callback),
onSetVolume: (callback: any) => ipcRenderer.on("setvolume", callback),
onSetSpeed: (callback: any) => ipcRenderer.on("setspeed", callback)
});

View file

@ -0,0 +1,612 @@
import dashjs from 'dashjs';
import Hls from 'hls.js';
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from '../Packets';
import { Player, PlayerType } from './Player';
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) {
console.log(`Sending playback update: ${JSON.stringify(updateMessage)}`);
lastPlayerUpdateGenerationTime = updateMessage.generationTime;
window.electronAPI.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) {
player.setVolume(currentVolume);
}
playerCtrlStateUpdate(PlayerControlEvent.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 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 playerCtrlFullscreen = document.getElementById("fullscreen");
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;
let player: Player;
let playerPrevTime: number = 0;
let lastPlayerUpdateGenerationTime = 0;
window.electronAPI.onPlay((_event, value: PlayMessage) => {
console.log("Handle play message renderer", value);
const currentVolume = player ? player.getVolume() : null;
const currentPlaybackRate = player ? player.getPlaybackRate() : null;
playerPrevTime = 0;
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) });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => { sendPlaybackUpdate(2) });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => { sendPlaybackUpdate(0) });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => {
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
console.log(`CALC TIME: ${Math.abs(dashPlayer.time() - playerPrevTime)}`)
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, () => {
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
window.electronAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: dashPlayer.getVolume() });
});
dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (data) => { window.electronAPI.sendPlaybackError({
message: `DashJS ERROR: ${JSON.stringify(data)}`
})});
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (data) => { window.electronAPI.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) => {
// console.log("cueEnter", e);
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) => {
// console.log("cueExit ", e);
document.getElementById("subtitle-" + e.cueID).remove();
});
dashPlayer.updateSettings({
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 {
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, function(eventName, data) {
window.electronAPI.sendPlaybackError({
message: `HLS player error: ${JSON.stringify(data)}`
});
});
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) };
videoElement.onpause = () => { sendPlaybackUpdate(2) };
videoElement.onended = () => { sendPlaybackUpdate(0) };
videoElement.ontimeupdate = () => {
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
if (Math.abs(videoElement.currentTime - playerPrevTime) >= playbackUpdateInterval) {
sendPlaybackUpdate(videoElement.paused ? 2 : 1);
playerPrevTime = videoElement.currentTime;
}
};
videoElement.onratechange = () => { sendPlaybackUpdate(videoElement.paused ? 2 : 1) };
videoElement.onvolumechange = () => {
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
window.electronAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: videoElement.volume });
};
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.electronAPI.onPause(() => { playerCtrlStateUpdate(PlayerControlEvent.Pause); });
window.electronAPI.onResume(() => { playerCtrlStateUpdate(PlayerControlEvent.Play); });
window.electronAPI.onSeek((_event, value: SeekMessage) => { console.log("electron on seek " + value.time); player.setCurrentTime(value.time); });
window.electronAPI.onSetVolume((_event, value: SetVolumeMessage) => { player.setVolume(value.volume); playerCtrlStateUpdate(PlayerControlEvent.VolumeChange); });
window.electronAPI.onSetSpeed((_event, value: SetSpeedMessage) => { player.setPlaybackRate(value.speed); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); });
});
let scrubbing = false;
let volumeChanging = false;
enum PlayerControlEvent {
Load,
Pause,
Play,
ToggleMute,
VolumeChange,
TimeUpdate,
UiFadeOut,
UiFadeIn,
SetCaptions,
ToggleSpeedMenu,
SetPlaybackRate,
ToggleFullscreen,
ExitFullscreen,
}
// UI update handler
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`);
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
playerCtrlDuration.innerHTML = `/&nbsp&nbsp${formatDuration(player.getDuration())}`;
playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
break;
}
case PlayerControlEvent.Pause:
playerCtrlAction.setAttribute("class", "play");
stopUiHideTimer();
player.pause();
break;
case PlayerControlEvent.Play:
playerCtrlAction.setAttribute("class", "pause");
startUiHideTimer();
player.play();
break;
case PlayerControlEvent.ToggleMute:
player.setMute(!player.isMuted());
break;
case PlayerControlEvent.VolumeChange: {
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: {
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: ${progress + 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");
break;
case PlayerControlEvent.UiFadeIn:
document.body.style.cursor = "default";
playerControls.setAttribute("style", "opacity: 1");
break;
case PlayerControlEvent.SetCaptions:
console.log(player.isCaptionsEnabled());
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;
}
case PlayerControlEvent.ToggleFullscreen: {
window.electronAPI.toggleFullScreen();
window.electronAPI.isFullScreen().then((isFullScreen: boolean) => {
if (isFullScreen) {
playerCtrlFullscreen.setAttribute("class", "fullscreen_on");
} else {
playerCtrlFullscreen.setAttribute("class", "fullscreen_off");
}
});
break;
}
case PlayerControlEvent.ExitFullscreen:
window.electronAPI.exitFullScreen();
playerCtrlFullscreen.setAttribute("class", "fullscreen_off");
break;
default:
break;
}
}
// Receiver generated event handlers
playerCtrlAction.onclick = () => {
if (player.isPaused()) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
};
playerCtrlVolume.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleMute); };
PlayerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) };
PlayerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; };
PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
if (e.buttons === 0) {
volumeChanging = false;
}
};
PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
function scrubbingMouseHandler(e: MouseEvent) {
if (scrubbing && e.buttons === 1) {
const progressBarOffset = e.offsetX - 8;
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
const time = Math.round((progressBarOffset / progressBarWidth) * player.getDuration());
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
player.setCurrentTime(time);
}
}
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);
}
playerCtrlCaptions.onclick = () => { player.enableCaptions(!player.isCaptionsEnabled()); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); };
playerCtrlSpeed.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); };
playerCtrlFullscreen.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
playbackRates.forEach(r => {
const entry = document.getElementById(`speedMenuEntry_${r}`);
entry.onclick = () => { player.setPlaybackRate(parseFloat(r)); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); };
});
videoElement.onclick = () => {
if (player.isPaused()) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
};
videoElement.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
// Component hiding
let uiHideTimer = null;
let uiVisible = true;
function startUiHideTimer() {
uiHideTimer = window.setTimeout(() => {
uiHideTimer = null;
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}, 3000);
}
function stopUiHideTimer() {
if (uiHideTimer) {
window.clearTimeout(uiHideTimer);
}
if (!uiVisible) {
uiVisible = true;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
}
}
document.onmousemove = function() {
stopUiHideTimer();
if (player && !player.isPaused()) {
startUiHideTimer();
}
};
window.onresize = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); };
// Add the keydown event listener to the document
const skipInterval = 10;
const volumeIncrement = 0.1;
document.addEventListener('keydown', (event) => {
console.log("KeyDown", event);
switch (event.code) {
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
const duration = player.getDuration();
if (duration) {
player.setCurrentTime(Math.min(player.getCurrentTime() + skipInterval, duration));
} else {
player.setCurrentTime(player.getCurrentTime());
}
event.preventDefault();
break;
}
case 'Space':
case 'Enter':
// Pause/Continue
if (player.isPaused()) {
playerCtrlStateUpdate(PlayerControlEvent.Play);
} else {
playerCtrlStateUpdate(PlayerControlEvent.Pause);
}
event.preventDefault();
break;
case 'KeyM':
// Mute toggle
playerCtrlStateUpdate(PlayerControlEvent.ToggleMute);
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;
}
});

View file

@ -1,21 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="./video-js.min.css" rel="stylesheet">
<link rel="stylesheet" href="./style.css" />
<title>FCast Receiver</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../assets/fonts/inter/inter.css" />
<link rel="stylesheet" href="./style.css" />
<title>FCast Receiver</title>
</head>
<body>
<video id="video-player" class="video-js" controls preload="auto" data-setup='{}'>
<p class="vjs-no-js">
To view this video please enable JavaScript, and consider upgrading to a web browser that
<a href="https://videojs.com/html5-video-support/" target="_blank">supports HTML5 video
</a>
</p>
</video>
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
<script src="./video.min.js"></script>
<script src="./renderer.js"></script>
<video id="videoPlayer" autoplay preload="auto"></video>
<div id="videoCaptions" class="captionsContainer"></div>
<div id="controls" class="container">
<div class="progressBarContainer">
<div id="progressBar" ref="progressBar" class="progressBar" ></div>
<div id="progressBarBuffer" class="progressBarBuffer" ></div>
<div id="progressBarProgress" class="progressBarProgress" ></div>
<!-- <div class="progressBarChapterContainer"></div> -->
<div id="progressBarHandle" class="progressBarHandle" ></div>
<div id="progressBarInteractiveArea" class="progressBarInteractiveArea" ></div>
</div>
<div class="leftButtonContainer">
<div id="action" class="play"></div>
<div id="volume" class="volume_high"></div>
<div class="volumeContainer">
<div id="volumeBar" ref="volumeBar" class="volumeBar" ></div>
<div id="volumeBarProgress" class="volumeBarProgress" ></div>
<div id="volumeBarHandle" class="volumeBarHandle" ></div>
<div id="volumeBarInteractiveArea" class="volumeBarInteractiveArea" ></div>
</div>
<div class="positionContainer">
<div id="liveBadge" class="liveBadge" style="display: none">LIVE</div>
<div id="position" class="position">00:00</div>
<div id="duration" class="duration">/&nbsp&nbsp00:00</div>
</div>
</div>
<div class="buttonContainer">
<div id="fullscreen" class="fullscreen_on"></div>
<div id="speed" class="speed"></div>
<div id="captions" class="captions_off"></div>
</div>
<div id="speedMenu" class="speedMenu" style="display: none">
<div class="speedMenuTitle">Playback speed</div>
<div class="speedMenuSeparator"></div>
<div id="speedMenuEntry_0.25" class="speedMenuEntry">
<div id="speedMenuEntry_0.25_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">0.25</div>
</div>
<div id="speedMenuEntry_0.50" class="speedMenuEntry">
<div id="speedMenuEntry_0.50_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">0.5</div>
</div>
<div id="speedMenuEntry_0.75" class="speedMenuEntry">
<div id="speedMenuEntry_0.75_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">0.75</div>
</div>
<div id="speedMenuEntry_1.00" class="speedMenuEntry">
<div id="speedMenuEntry_1.00_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">1.0</div>
</div>
<div id="speedMenuEntry_1.25" class="speedMenuEntry">
<div id="speedMenuEntry_1.25_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">1.25</div>
</div>
<div id="speedMenuEntry_1.50" class="speedMenuEntry">
<div id="speedMenuEntry_1.50_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">1.5</div>
</div>
<div id="speedMenuEntry_1.75" class="speedMenuEntry">
<div id="speedMenuEntry_1.75_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">1.75</div>
</div>
<div id="speedMenuEntry_2.00" class="speedMenuEntry">
<div id="speedMenuEntry_2.00_enabled" class="speedMenuEntryEnabled"></div>
<div class="speedMenuEntryText">2.0</div>
</div>
</div>
</div>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -1,15 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
toggleFullScreen: () => ipcRenderer.send('toggle-full-screen'),
exitFullScreen: () => ipcRenderer.send('exit-full-screen'),
sendPlaybackError: (error) => ipcRenderer.send('send-playback-error', error),
sendPlaybackUpdate: (update) => ipcRenderer.send('send-playback-update', update),
sendVolumeUpdate: (update) => ipcRenderer.send('send-volume-update', update),
onPlay: (callback) => ipcRenderer.on("play", callback),
onPause: (callback) => ipcRenderer.on("pause", callback),
onResume: (callback) => ipcRenderer.on("resume", callback),
onSeek: (callback) => ipcRenderer.on("seek", callback),
onSetVolume: (callback) => ipcRenderer.on("setvolume", callback),
onSetSpeed: (callback) => ipcRenderer.on("setspeed", callback)
});

View file

@ -1,240 +0,0 @@
function toggleFullScreen(ev) {
window.electronAPI.toggleFullScreen();
}
const options = {
textTrackSettings: false
};
let customHeaders = null;
videojs.Vhs.xhr.beforeRequest = function(options) {
options.headers = { ... options.headers, ... customHeaders };
return options;
};
const player = videojs("video-player", options, function onPlayerReady() {
const fullScreenControls = document.getElementsByClassName("vjs-fullscreen-control");
for (let i = 0; i < fullScreenControls.length; i++) {
const node = fullScreenControls[i].cloneNode(true);
fullScreenControls[i].parentNode.replaceChild(node, fullScreenControls[i]);
fullScreenControls[i].onclick = toggleFullScreen;
fullScreenControls[i].ontap = toggleFullScreen;
}
});
sendPlaybackUpdate = (message) => {
const sanitizedMessage = {
generationTime: Date.now(),
time: message.time ? message.time : 0,
duration: message.duration && isFinite(message.duration) ? message.duration : 0,
state: message.state,
speed: message.speed ? message.speed : 1
};
window.electronAPI.sendPlaybackUpdate(sanitizedMessage);
};
player.on("pause", () => { sendPlaybackUpdate({
time: player.currentTime(),
duration: player.duration(),
state: 2,
speed: player.playbackRate()
})});
player.on("play", () => { sendPlaybackUpdate({
time: player.currentTime(),
duration: player.duration(),
state: 1,
speed: player.playbackRate()
})});
player.on("seeked", () => { sendPlaybackUpdate({
time: player.currentTime(),
duration: player.duration(),
state: player.paused() ? 2 : 1,
speed: player.playbackRate()
})});
player.on("ratechange", () => { sendPlaybackUpdate({
time: player.currentTime(),
duration: player.duration(),
state: player.paused() ? 2 : 1,
speed: player.playbackRate()
})});
player.on("volumechange", () => { window.electronAPI.sendVolumeUpdate({
generationTime: Date.now(),
volume: player.volume()
})});
player.on('error', () => { window.electronAPI.sendPlaybackError({
message: JSON.stringify(player.error())
})});
window.electronAPI.onPlay((_event, value) => {
console.log("Handle play message renderer", value);
customHeaders = value.headers;
if (value.content) {
player.src({ type: value.container, src: `data:${value.container};base64,` + window.btoa(value.content) });
} else {
player.src({ type: value.container, src: value.url });
}
const onLoadedMetadata = () => {
if (value.time) {
player.currentTime(value.time);
}
if (value.speed) {
player.playbackRate(value.speed);
} else {
player.playbackRate(1.0);
}
player.off('loadedmetadata', onLoadedMetadata);
};
player.on('loadedmetadata', onLoadedMetadata);
player.play();
});
window.electronAPI.onPause((_event) => {
console.log("Handle pause");
player.pause();
});
window.electronAPI.onResume((_event) => {
console.log("Handle resume");
player.play();
});
window.electronAPI.onSeek((_event, value) => {
console.log("Handle seek");
player.currentTime(value.time);
});
window.electronAPI.onSetVolume((_event, value) => {
console.log("Handle setVolume");
player.volume(Math.min(1.0, Math.max(0.0, value.volume)));
});
window.electronAPI.onSetSpeed((_event, value) => {
console.log("Handle setSpeed");
player.playbackRate(value.speed);
});
setInterval(() => {
sendPlaybackUpdate({
time: player.currentTime(),
duration: player.duration(),
state: player.paused() ? 2 : 1,
speed: player.playbackRate(),
});
}, 1000);
let mouseTimer = null;
let cursorVisible = true;
//Hide mouse cursor
function startMouseHideTimer() {
mouseTimer = window.setTimeout(() => {
mouseTimer = null;
document.body.style.cursor = "none";
cursorVisible = false;
}, 3000);
}
document.onmousemove = function() {
if (mouseTimer) {
window.clearTimeout(mouseTimer);
}
if (!cursorVisible) {
document.body.style.cursor = "default";
cursorVisible = true;
}
startMouseHideTimer();
};
startMouseHideTimer();
// Add the keydown event listener to the document
const skipInterval = 10;
const volumeIncrement = 0.1;
document.addEventListener('keydown', (event) => {
console.log("KeyDown", event);
switch (event.code) {
case 'F11':
window.electronAPI.toggleFullScreen();
event.preventDefault();
break;
case 'Escape':
window.electronAPI.exitFullScreen();
event.preventDefault();
break;
case 'ArrowLeft':
// Skip back
player.currentTime(Math.max(player.currentTime() - skipInterval, 0));
event.preventDefault();
break;
case 'ArrowRight':
// Skip forward
const duration = player.duration();
if (duration) {
player.currentTime(Math.min(player.currentTime() + skipInterval, duration));
} else {
player.currentTime(player.currentTime());
}
event.preventDefault();
break;
case 'Space':
case 'Enter':
// Pause/Continue
if (player.paused()) {
player.play();
} else {
player.pause();
}
event.preventDefault();
break;
case 'KeyM':
// Mute toggle
player.muted(!player.muted());
break;
case 'ArrowUp':
// Volume up
player.volume(Math.min(player.volume() + volumeIncrement, 1));
break;
case 'ArrowDown':
// Volume down
player.volume(Math.max(player.volume() - volumeIncrement, 0));
break;
}
});
//Select subtitle track by default
player.ready(() => {
const textTracks = player.textTracks();
textTracks.addEventListener("change", function () {
console.log("Text tracks changed", textTracks);
for (let i = 0; i < textTracks.length; i++) {
if (textTracks[i].language === "df" && textTracks[i].mode !== "showing") {
textTracks[i].mode = "showing";
}
}
});
player.on('loadedmetadata', function () {
console.log("Metadata loaded", textTracks);
for (let i = 0; i < textTracks.length; i++) {
if (textTracks[i].language === "df" && textTracks[i].mode !== "showing") {
textTracks[i].mode = "showing";
}
}
});
});

View file

@ -5,9 +5,9 @@ html {
}
body {
margin: 0;
margin: 0;
padding: 0;
background-color: black;
background-color: black;
color: white;
width: 100vw;
max-width: 100%;
@ -15,7 +15,7 @@ body {
max-height: 100%;
}
#video-player {
#videoPlayer {
object-fit: contain;
width: 100%;
height: 100%;
@ -24,4 +24,457 @@ body {
*: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;
background-color: #019BE7;
border-radius: 3px;
pointer-events: none;
}
.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;
border-radius: 4px;
margin-left: 10px;
}
.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");
}
.fullscreen_on {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_fullscreen_on.svg");
transition: background-image 0.1s ease-in-out;
}
.fullscreen_on:hover {
background-image: url("../../assets/icons/player/icon24_fullscreen_on_active.svg");
}
.fullscreen_off {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_fullscreen_off.svg");
transition: background-image 0.1s ease-in-out;
}
.fullscreen_off:hover {
background-image: url("../../assets/icons/player/icon24_fullscreen_off_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: 150px;
/* margin: auto; */
text-align: center;
font-family: InterVariable;
font-size: 24px;
font-style: normal;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
margin: 0px 40px 0px 40px;
/* display: flex; */
/* align-items: center; */
/* min-width: 100%; */
/* height: 100px; */
/* text-overflow: ellipsis; */
/* overflow: hidden; */
}
.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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long