1
0
Fork 0
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:
Michael Hollister 2025-04-25 14:04:59 -05:00
parent a7cd81aa34
commit c54ce74dfd
11 changed files with 268 additions and 57 deletions

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

View file

@ -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);

View file

@ -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);

View file

@ -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

View file

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

View file

@ -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);

View file

@ -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

View file

@ -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;

View file

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

View file

@ -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 () => {

View file

@ -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>