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:
parent
d4a900e902
commit
755db076e7
58 changed files with 2261 additions and 475 deletions
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
let deviceInfo;
|
||||
ipcRenderer.on("device-info", (_event, value) => {
|
|
@ -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.
|
@ -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>
|
||||
|
|
1
receivers/electron/src/main/qrcode.min.js
vendored
1
receivers/electron/src/main/qrcode.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -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 {
|
||||
|
|
1
receivers/electron/src/main/video-js.min.css
vendored
1
receivers/electron/src/main/video-js.min.css
vendored
File diff suppressed because one or more lines are too long
26
receivers/electron/src/main/video.min.js
vendored
26
receivers/electron/src/main/video.min.js
vendored
File diff suppressed because one or more lines are too long
223
receivers/electron/src/player/Player.ts
Normal file
223
receivers/electron/src/player/Player.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
24
receivers/electron/src/player/Preload.ts
Normal file
24
receivers/electron/src/player/Preload.ts
Normal 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)
|
||||
});
|
612
receivers/electron/src/player/Renderer.ts
Normal file
612
receivers/electron/src/player/Renderer.ts
Normal 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 = `/  ${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;
|
||||
}
|
||||
});
|
|
@ -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">/  00: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>
|
|
@ -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)
|
||||
});
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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
26
receivers/electron/src/player/video.min.js
vendored
26
receivers/electron/src/player/video.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue