mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
Receivers: Added connect/disconenct notifications to player window
This commit is contained in:
parent
a7cd81aa34
commit
c54ce74dfd
11 changed files with 268 additions and 57 deletions
52
receivers/common/web/ConnectionMonitor.ts
Normal file
52
receivers/common/web/ConnectionMonitor.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { Opcode } from 'common/Packets';
|
||||
|
||||
const connectionPingTimeout = 2500;
|
||||
const connections = [];
|
||||
const heartbeatRetries = {};
|
||||
let uiUpdateCallbacks = {
|
||||
onConnect: null,
|
||||
onDisconnect: null,
|
||||
}
|
||||
|
||||
export function setUiUpdateCallbacks(callbacks: any) {
|
||||
uiUpdateCallbacks = callbacks;
|
||||
}
|
||||
|
||||
// Window might be re-created while devices are still connected
|
||||
window.targetAPI.onPing((_event, value: any) => {
|
||||
if (value) {
|
||||
heartbeatRetries[value.id] = 0;
|
||||
|
||||
if (!connections.includes(value.id)) {
|
||||
connections.push(value.id);
|
||||
uiUpdateCallbacks.onConnect(connections, value.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.targetAPI.onConnect((_event, value: any) => {
|
||||
connections.push(value.id);
|
||||
uiUpdateCallbacks.onConnect(connections, value);
|
||||
});
|
||||
window.targetAPI.onDisconnect((_event, value: any) => {
|
||||
console.log(`Device disconnected: ${JSON.stringify(value)}`);
|
||||
const index = connections.indexOf(value.id);
|
||||
if (index != -1) {
|
||||
connections.splice(index, 1);
|
||||
uiUpdateCallbacks.onDisconnect(connections, value.id);
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
if (connections.length > 0) {
|
||||
window.targetAPI.sendSessionMessage(Opcode.Ping, null);
|
||||
|
||||
for (const session of connections) {
|
||||
if (heartbeatRetries[session] > 3) {
|
||||
console.warn(`Could not ping device with connection id ${session}. Disconnecting...`);
|
||||
window.targetAPI.disconnectDevice(session);
|
||||
}
|
||||
|
||||
heartbeatRetries[session] = heartbeatRetries[session] === undefined ? 1 : heartbeatRetries[session] + 1;
|
||||
}
|
||||
}
|
||||
}, connectionPingTimeout);
|
|
@ -7,12 +7,11 @@ import { v4 as uuidv4 } from 'modules/uuid';
|
|||
|
||||
export class TcpListenerService {
|
||||
public static PORT = 46899;
|
||||
private static TIMEOUT = 2500;
|
||||
|
||||
emitter = new EventEmitter();
|
||||
|
||||
private server: net.Server;
|
||||
private sessions: FCastSession[] = [];
|
||||
private sessionMap = {};
|
||||
|
||||
start() {
|
||||
if (this.server != null) {
|
||||
|
@ -48,6 +47,10 @@ export class TcpListenerService {
|
|||
});
|
||||
}
|
||||
|
||||
disconnect(connectionId: string) {
|
||||
this.sessionMap[connectionId].socket.destroy();
|
||||
}
|
||||
|
||||
private async handleServerError(err: NodeJS.ErrnoException) {
|
||||
errorHandler(err);
|
||||
}
|
||||
|
@ -60,22 +63,7 @@ export class TcpListenerService {
|
|||
this.sessions.push(session);
|
||||
|
||||
const connectionId = uuidv4();
|
||||
let heartbeatRetries = 0;
|
||||
socket.setTimeout(TcpListenerService.TIMEOUT);
|
||||
socket.on('timeout', () => {
|
||||
try {
|
||||
if (heartbeatRetries > 3) {
|
||||
Main.logger.warn(`Could not ping device ${socket.remoteAddress}:${socket.remotePort}. Disconnecting...`);
|
||||
socket.destroy();
|
||||
}
|
||||
|
||||
heartbeatRetries += 1;
|
||||
session.send(Opcode.Ping);
|
||||
} catch (e) {
|
||||
Main.logger.warn(`Error while pinging sender device ${socket.remoteAddress}:${socket.remotePort}.`, e);
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
this.sessionMap[connectionId] = session;
|
||||
|
||||
socket.on("error", (err) => {
|
||||
Main.logger.warn(`Error from ${socket.remoteAddress}:${socket.remotePort}.`, err);
|
||||
|
@ -84,7 +72,6 @@ export class TcpListenerService {
|
|||
|
||||
socket.on("data", buffer => {
|
||||
try {
|
||||
heartbeatRetries = 0;
|
||||
session.processBytes(buffer);
|
||||
} catch (e) {
|
||||
Main.logger.warn(`Error while handling packet from ${socket.remoteAddress}:${socket.remotePort}.`, e);
|
||||
|
|
|
@ -12,6 +12,7 @@ export class WebSocketListenerService {
|
|||
|
||||
private server: WebSocketServer;
|
||||
private sessions: FCastSession[] = [];
|
||||
private sessionMap = {};
|
||||
|
||||
start() {
|
||||
if (this.server != null) {
|
||||
|
@ -45,6 +46,10 @@ export class WebSocketListenerService {
|
|||
});
|
||||
}
|
||||
|
||||
disconnect(connectionId: string) {
|
||||
this.sessionMap[connectionId].close();
|
||||
}
|
||||
|
||||
private async handleServerError(err: NodeJS.ErrnoException) {
|
||||
errorHandler(err);
|
||||
}
|
||||
|
@ -54,6 +59,8 @@ export class WebSocketListenerService {
|
|||
|
||||
const session = new FCastSession(socket, (data) => socket.send(data));
|
||||
const connectionId = uuidv4();
|
||||
this.sessionMap[connectionId] = session;
|
||||
|
||||
session.bindEvents(this.emitter);
|
||||
this.sessions.push(session);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Opcode } from 'common/Packets';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -22,11 +23,13 @@ if (TARGET === 'electron') {
|
|||
})
|
||||
|
||||
electronAPI.contextBridge.exposeInMainWorld('targetAPI', {
|
||||
onDeviceInfo: (callback: any) => electronAPI.ipcRenderer.on("device-info", callback),
|
||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on("connect", callback),
|
||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on("disconnect", callback),
|
||||
onPing: (callback: any) => electronAPI.ipcRenderer.on("ping", callback),
|
||||
onDeviceInfo: (callback: any) => electronAPI.ipcRenderer.on('device-info', callback),
|
||||
getDeviceInfo: () => preloadData.deviceInfo,
|
||||
sendSessionMessage: (opcode: Opcode, message: any) => electronAPI.ipcRenderer.send('send-session-message', { opcode: opcode, message: message }),
|
||||
disconnectDevice: (session: string) => electronAPI.ipcRenderer.send('disconnect-device', session),
|
||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback),
|
||||
onPing: (callback: any) => electronAPI.ipcRenderer.on('ping', callback),
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
|
||||
import QRCode from 'modules/qrcode';
|
||||
import * as connectionMonitor from '../ConnectionMonitor';
|
||||
import { onQRCodeRendered } from 'src/main/Renderer';
|
||||
import { toast, ToastIcon } from '../components/Toast';
|
||||
|
||||
const connectionStatusText = document.getElementById('connection-status-text');
|
||||
const connectionStatusSpinner = document.getElementById('connection-spinner');
|
||||
const connectionStatusCheck = document.getElementById('connection-check');
|
||||
let connections = [];
|
||||
let renderedConnectionInfo = false;
|
||||
let renderedAddresses = null;
|
||||
let qrCodeUrl = null;
|
||||
|
@ -14,25 +14,15 @@ let qrWidth = null;
|
|||
|
||||
window.addEventListener('resize', (event) => calculateQRCodeWidth());
|
||||
|
||||
// Window might be re-created while devices are still connected
|
||||
window.targetAPI.onPing((_event, value: any) => {
|
||||
if (value && !connections.includes(value.id)) {
|
||||
connections.push(value.id);
|
||||
onConnect(value.id);
|
||||
}
|
||||
});
|
||||
|
||||
window.targetAPI.onDeviceInfo(renderIPsAndQRCode);
|
||||
window.targetAPI.onConnect((_event, value: any) => {
|
||||
connections.push(value.id);
|
||||
onConnect(value);
|
||||
});
|
||||
window.targetAPI.onDisconnect((_event, value: any) => {
|
||||
console.log(`Device disconnected: ${JSON.stringify(value)}`);
|
||||
const index = connections.indexOf(value.id);
|
||||
if (index != -1) {
|
||||
connections.splice(index, 1);
|
||||
|
||||
connectionMonitor.setUiUpdateCallbacks({
|
||||
onConnect: (connections: string[], connectionInfo: any) => {
|
||||
console.log(`Device connected: ${JSON.stringify(connectionInfo)}`);
|
||||
connectionStatusText.textContent = connections.length > 1 ? 'Multiple devices connected:\r\n Ready to cast' : 'Connected: Ready to cast';
|
||||
connectionStatusSpinner.style.display = 'none';
|
||||
connectionStatusCheck.style.display = 'inline-block';
|
||||
},
|
||||
onDisconnect: (connections: string[], connectionInfo: any) => {
|
||||
console.log(`Device disconnected: ${JSON.stringify(connectionInfo)}`);
|
||||
if (connections.length === 0) {
|
||||
connectionStatusText.textContent = 'Waiting for a connection';
|
||||
connectionStatusSpinner.style.display = 'inline-block';
|
||||
|
@ -43,21 +33,16 @@ window.targetAPI.onDisconnect((_event, value: any) => {
|
|||
connectionStatusText.textContent = connections.length > 1 ? 'Multiple devices connected:\r\n Ready to cast' : 'Connected: Ready to cast';
|
||||
toast('A device has disconnected', ToastIcon.INFO);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
window.targetAPI.onDeviceInfo(renderIPsAndQRCode);
|
||||
|
||||
if(window.targetAPI.getDeviceInfo()) {
|
||||
console.log('device info already present');
|
||||
renderIPsAndQRCode();
|
||||
}
|
||||
|
||||
function onConnect(value: any) {
|
||||
console.log(`Device connected: ${JSON.stringify(value)}`);
|
||||
connectionStatusText.textContent = connections.length > 1 ? 'Multiple devices connected:\r\n Ready to cast' : 'Connected: Ready to cast';
|
||||
connectionStatusSpinner.style.display = 'none';
|
||||
connectionStatusCheck.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function renderIPsAndQRCode() {
|
||||
const value = window.targetAPI.getDeviceInfo();
|
||||
console.log(`Network Interface Info: ${value}`);
|
||||
|
|
|
@ -293,6 +293,7 @@ body, html {
|
|||
position: relative;
|
||||
top: -200px;
|
||||
max-width: 70%;
|
||||
width: fit-content;
|
||||
|
||||
background: #F0F0F0;
|
||||
border: 3px solid rgba(0, 0, 0, 0.08);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage, Opcode } from 'common/Packets';
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
|
@ -28,8 +28,11 @@ if (TARGET === 'electron') {
|
|||
onPause: (callback: any) => electronAPI.ipcRenderer.on("pause", callback),
|
||||
onResume: (callback: any) => electronAPI.ipcRenderer.on("resume", callback),
|
||||
onSeek: (callback: any) => electronAPI.ipcRenderer.on("seek", callback),
|
||||
onSetVolume: (callback: any) => electronAPI.ipcRenderer.on("setvolume", callback),
|
||||
onSetSpeed: (callback: any) => electronAPI.ipcRenderer.on("setspeed", callback)
|
||||
sendSessionMessage: (opcode: Opcode, message: any) => electronAPI.ipcRenderer.send('send-session-message', { opcode: opcode, message: message }),
|
||||
disconnectDevice: (session: string) => electronAPI.ipcRenderer.send('disconnect-device', session),
|
||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback),
|
||||
onPing: (callback: any) => electronAPI.ipcRenderer.on('ping', callback),
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
|
|
|
@ -2,6 +2,8 @@ import dashjs from 'modules/dashjs';
|
|||
import Hls, { LevelLoadedData } from 'modules/hls.js';
|
||||
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets';
|
||||
import { Player, PlayerType } from './Player';
|
||||
import * as connectionMonitor from '../ConnectionMonitor';
|
||||
import { toast, ToastIcon } from '../components/Toast';
|
||||
import {
|
||||
targetPlayerCtrlStateUpdate,
|
||||
targetKeyDownEventListener,
|
||||
|
@ -330,6 +332,17 @@ function onPlay(_event, value: PlayMessage) {
|
|||
window.targetAPI.onSetSpeed((_event, value: SetSpeedMessage) => { player.setPlaybackRate(value.speed); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); });
|
||||
};
|
||||
|
||||
connectionMonitor.setUiUpdateCallbacks({
|
||||
onConnect: (connections: string[], connectionInfo: any) => {
|
||||
console.log(`Device connected: ${JSON.stringify(connectionInfo)}`);
|
||||
toast('Device connected', ToastIcon.INFO);
|
||||
},
|
||||
onDisconnect: (connections: string[], connectionInfo: any) => {
|
||||
console.log(`Device disconnected: ${JSON.stringify(connectionInfo)}`);
|
||||
toast('Device disconnected. If you experience playback issues, please reconnect.', ToastIcon.INFO);
|
||||
},
|
||||
});
|
||||
|
||||
window.targetAPI.onPlay(onPlay);
|
||||
|
||||
let scrubbing = false;
|
||||
|
|
|
@ -456,3 +456,140 @@ body {
|
|||
background-size: cover;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#toast-notification {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
|
||||
position: relative;
|
||||
top: calc(-100% + 20px);
|
||||
margin: auto;
|
||||
max-width: 25%;
|
||||
width: fit-content;
|
||||
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 3px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0px 100px 80px rgba(0, 0, 0, 0.33), 0px 64.8148px 46.8519px rgba(0, 0, 0, 0.250556), 0px 38.5185px 25.4815px rgba(0, 0, 0, 0.200444), 0px 20px 13px rgba(0, 0, 0, 0.165), 0px 8.14815px 6.51852px rgba(0, 0, 0, 0.129556), 0px 1.85185px 3.14815px rgba(0, 0, 0, 0.0794444);
|
||||
border-radius: 12px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
background-image: url(../assets/icons/app/info.svg);
|
||||
background-size: cover;
|
||||
filter: grayscale(0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
margin-right: 5px;
|
||||
|
||||
font-family: InterVariable;
|
||||
font-size: 28px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.toast-fade-in {
|
||||
animation: toast-fade-in 1.0s cubic-bezier(0.5, 0, 0.5, 1) 1;
|
||||
}
|
||||
|
||||
.toast-fade-out {
|
||||
animation: toast-fade-out 1.0s cubic-bezier(0.5, 0, 0.5, 1) 1;
|
||||
}
|
||||
|
||||
@keyframes toast-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes toast-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Display scaling (Minimum supported resolution is 960x540) */
|
||||
@media only screen and ((min-width: 2560px) or (min-height: 1440px)) {
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
margin: 5px 10px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and ((max-width: 2559px) or (max-height: 1439px)) {
|
||||
#toast-notification {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 5px 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and ((max-width: 1919px) or (max-height: 1079px)) {
|
||||
#toast-notification {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin: 5px 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and ((max-width: 1279px) or (max-height: 719px)) {
|
||||
#toast-notification {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
#toast-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 5px 5px;
|
||||
}
|
||||
|
||||
#toast-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,9 +188,18 @@ export class Main {
|
|||
l.emitter.on("setvolume", (message) => Main.playerWindow?.webContents?.send("setvolume", message));
|
||||
l.emitter.on("setspeed", (message) => Main.playerWindow?.webContents?.send("setspeed", message));
|
||||
|
||||
l.emitter.on('connect', (message) => Main.mainWindow?.webContents?.send('connect', message));
|
||||
l.emitter.on('disconnect', (message) => Main.mainWindow?.webContents?.send('disconnect', message));
|
||||
l.emitter.on('ping', (message) => Main.mainWindow?.webContents?.send('ping', message));
|
||||
l.emitter.on('connect', (message) => {
|
||||
Main.mainWindow?.webContents?.send('connect', message);
|
||||
Main.playerWindow?.webContents?.send('connect', message);
|
||||
});
|
||||
l.emitter.on('disconnect', (message) => {
|
||||
Main.mainWindow?.webContents?.send('disconnect', message);
|
||||
Main.playerWindow?.webContents?.send('disconnect', message);
|
||||
});
|
||||
l.emitter.on('ping', (message) => {
|
||||
Main.mainWindow?.webContents?.send('ping', message);
|
||||
Main.playerWindow?.webContents?.send('ping', message);
|
||||
});
|
||||
l.start();
|
||||
|
||||
ipcMain.on('send-playback-error', (event: IpcMainEvent, value: PlaybackErrorMessage) => {
|
||||
|
@ -204,6 +213,15 @@ export class Main {
|
|||
ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => {
|
||||
l.send(Opcode.VolumeUpdate, value);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ipcMain.on('send-session-message', (event: IpcMainEvent, value: any) => {
|
||||
l.send(value.opcode, value.message);
|
||||
});
|
||||
|
||||
ipcMain.on('disconnect-device', (event: IpcMainEvent, value: string) => {
|
||||
l.disconnect(value);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('send-download-request', async () => {
|
||||
|
|
|
@ -86,6 +86,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-notification">
|
||||
<div id="toast-icon"></div>
|
||||
<div id="toast-text"></div>
|
||||
</div>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue