From 1622a9b752ccec5ce347eadd6bad0422845a5fe4 Mon Sep 17 00:00:00 2001 From: Michael Hollister Date: Mon, 4 Aug 2025 18:09:06 -0500 Subject: [PATCH] Receivers: Module refractoring for portability --- receivers/common/web/ConnectionMonitor.ts | 65 ++++++++----------- receivers/common/web/DiscoveryService.ts | 8 ++- receivers/common/web/FCastSession.ts | 4 +- receivers/common/web/ListenerService.ts | 2 +- receivers/common/web/MediaCache.ts | 4 +- receivers/common/web/MimeTypes.ts | 4 +- receivers/common/web/TcpListenerService.ts | 4 +- .../common/web/WebSocketListenerService.ts | 4 +- receivers/electron/src/Main.ts | 10 +-- .../webos/fcast-receiver-service/src/Main.ts | 10 +-- 10 files changed, 54 insertions(+), 61 deletions(-) diff --git a/receivers/common/web/ConnectionMonitor.ts b/receivers/common/web/ConnectionMonitor.ts index 9858ff1..24049fe 100644 --- a/receivers/common/web/ConnectionMonitor.ts +++ b/receivers/common/web/ConnectionMonitor.ts @@ -43,52 +43,47 @@ export function setUiUpdateCallbacks(callbacks: any) { export class ConnectionMonitor { private static logger: Logger; - private static initialized = false; private static connectionPingTimeout = 2500; - private static heartbeatRetries = new Map(); - private static backendConnections = new Map(); + private static heartbeatRetries = new Map(); + private static backendConnections = new Map(); // is `ListenerService`, but cant import backend module in frontend private static uiConnectUpdateTimeout = 100; private static uiDisconnectUpdateTimeout = 2000; // Senders may reconnect, but generally need more time - private static uiUpdateMap = new Map(); + private static uiUpdateMap = new Map(); // { event: string, uiUpdateCallback: () => void } constructor() { - if (!ConnectionMonitor.initialized) { - ConnectionMonitor.logger = new Logger('ConnectionMonitor', LoggerType.BACKEND); + ConnectionMonitor.logger = new Logger('ConnectionMonitor', LoggerType.BACKEND); - setInterval(() => { - if (ConnectionMonitor.backendConnections.size > 0) { - for (const sessionId of ConnectionMonitor.backendConnections.keys()) { - const listener = ConnectionMonitor.backendConnections.get(sessionId); + setInterval(() => { + if (ConnectionMonitor.backendConnections.size > 0) { + for (const sessionId of ConnectionMonitor.backendConnections.keys()) { + const listener = ConnectionMonitor.backendConnections.get(sessionId); - if (listener.getSessionProtocolVersion(sessionId) >= 2) { - if (ConnectionMonitor.heartbeatRetries.get(sessionId) > 3) { - ConnectionMonitor.logger.warn(`Could not ping device with connection id ${sessionId}. Disconnecting...`); - listener.disconnect(sessionId); - } - - ConnectionMonitor.logger.debug(`Pinging session ${sessionId} with ${ConnectionMonitor.heartbeatRetries.get(sessionId)} retries left`); - listener.send(Opcode.Ping, null, sessionId); - ConnectionMonitor.heartbeatRetries.set(sessionId, ConnectionMonitor.heartbeatRetries.get(sessionId) + 1); - } - else if (listener.getSessionProtocolVersion(sessionId) === undefined) { - ConnectionMonitor.logger.warn(`Session ${sessionId} was not found in the list of active sessions. Removing...`); - ConnectionMonitor.backendConnections.delete(sessionId); - ConnectionMonitor.heartbeatRetries.delete(sessionId); + if (listener.getSessionProtocolVersion(sessionId) >= 2) { + if (ConnectionMonitor.heartbeatRetries.get(sessionId) > 3) { + ConnectionMonitor.logger.warn(`Could not ping device with connection id ${sessionId}. Disconnecting...`); + listener.disconnect(sessionId); } + + ConnectionMonitor.logger.debug(`Pinging session ${sessionId} with ${ConnectionMonitor.heartbeatRetries.get(sessionId)} retries left`); + listener.send(Opcode.Ping, null, sessionId); + ConnectionMonitor.heartbeatRetries.set(sessionId, ConnectionMonitor.heartbeatRetries.get(sessionId) + 1); + } + else if (listener.getSessionProtocolVersion(sessionId) === undefined) { + ConnectionMonitor.logger.warn(`Session ${sessionId} was not found in the list of active sessions. Removing...`); + ConnectionMonitor.backendConnections.delete(sessionId); + ConnectionMonitor.heartbeatRetries.delete(sessionId); } } - }, ConnectionMonitor.connectionPingTimeout); - - ConnectionMonitor.initialized = true; - } + } + }, ConnectionMonitor.connectionPingTimeout); } - public static onPingPong(value: any, isWebsockets: boolean) { - ConnectionMonitor.logger.debug(`Received response from ${value.sessionId}`); + public static onPingPong(sessionId: string, isWebsockets: boolean) { + ConnectionMonitor.logger.debug(`Received response from ${sessionId}`); // Websocket clients currently don't support ping-pong commands if (!isWebsockets) { - ConnectionMonitor.heartbeatRetries.set(value.sessionId, 0); + ConnectionMonitor.heartbeatRetries.set(sessionId, 0); } } @@ -96,17 +91,13 @@ export class ConnectionMonitor { ConnectionMonitor.logger.info(`Device connected: ${JSON.stringify(value)}`); const idMapping = isWebsockets ? value.sessionId : value.data.address; - if (!ConnectionMonitor.uiUpdateMap.has(idMapping)) { - ConnectionMonitor.uiUpdateMap.set(idMapping, []); - } - if (!isWebsockets) { ConnectionMonitor.backendConnections.set(value.sessionId, listener); ConnectionMonitor.heartbeatRetries.set(value.sessionId, 0); } // Occasionally senders seem to instantaneously disconnect and reconnect, so suppress those ui updates - const senderUpdateQueue = ConnectionMonitor.uiUpdateMap.get(idMapping); + const senderUpdateQueue = ConnectionMonitor.uiUpdateMap.get(idMapping) ?? []; senderUpdateQueue.push({ event: 'connect', uiUpdateCallback: uiUpdateCallback }); ConnectionMonitor.uiUpdateMap.set(idMapping, senderUpdateQueue); @@ -115,7 +106,7 @@ export class ConnectionMonitor { } } - public static onDisconnect(listener: any, value: any, isWebsockets: boolean, uiUpdateCallback: any) { + public static onDisconnect(value: any, isWebsockets: boolean, uiUpdateCallback: any) { ConnectionMonitor.logger.info(`Device disconnected: ${JSON.stringify(value)}`); if (!isWebsockets) { diff --git a/receivers/common/web/DiscoveryService.ts b/receivers/common/web/DiscoveryService.ts index 10cecb1..ad39aea 100644 --- a/receivers/common/web/DiscoveryService.ts +++ b/receivers/common/web/DiscoveryService.ts @@ -2,6 +2,8 @@ import mdns from 'modules/@futo/mdns-js'; import { Logger, LoggerType } from 'common/Logger'; import { getAppName, getAppVersion, getComputerName } from 'src/Main'; import { PROTOCOL_VERSION } from 'common/Packets'; +import { TcpListenerService } from './TcpListenerService'; +import { WebSocketListenerService } from './WebSocketListenerService'; const logger = new Logger('DiscoveryService', LoggerType.BACKEND); export class DiscoveryService { @@ -19,13 +21,15 @@ export class DiscoveryService { // Note that txt field must be populated, otherwise certain mdns stacks have undefined behavior/issues // when connecting to the receiver. Also mdns-js internally gets a lot of parsing errors when txt records // are not defined. - this.serviceTcp = mdns.createAdvertisement(mdns.tcp('_fcast'), 46899, { name: name, txt: { + this.serviceTcp = mdns.createAdvertisement(mdns.tcp('_fcast'), TcpListenerService.PORT, + { name: name, txt: { version: PROTOCOL_VERSION, appName: getAppName(), appVersion: getAppVersion(), } }); this.serviceTcp.start(); - this.serviceWs = mdns.createAdvertisement(mdns.tcp('_fcast-ws'), 46898, { name: name, txt: { + this.serviceWs = mdns.createAdvertisement(mdns.tcp('_fcast-ws'), WebSocketListenerService.PORT, + { name: name, txt: { version: PROTOCOL_VERSION, appName: getAppName(), appVersion: getAppVersion(), diff --git a/receivers/common/web/FCastSession.ts b/receivers/common/web/FCastSession.ts index 57aa757..d69c2c9 100644 --- a/receivers/common/web/FCastSession.ts +++ b/receivers/common/web/FCastSession.ts @@ -244,8 +244,8 @@ export class FCastSession { this.emitter.on("setvolume", (body: SetVolumeMessage) => { emitter.emit("setvolume", body) }); this.emitter.on("setspeed", (body: SetSpeedMessage) => { emitter.emit("setspeed", body) }); this.emitter.on("version", (body: VersionMessage) => { emitter.emit("version", body) }); - this.emitter.on("ping", () => { emitter.emit("ping", { sessionId: this.sessionId }) }); - this.emitter.on("pong", () => { emitter.emit("pong", { sessionId: this.sessionId }) }); + this.emitter.on("ping", () => { emitter.emit("ping", this.sessionId) }); + this.emitter.on("pong", () => { emitter.emit("pong", this.sessionId) }); this.emitter.on("initial", (body: InitialSenderMessage) => { emitter.emit("initial", body) }); this.emitter.on("setplaylistitem", (body: SetPlaylistItemMessage) => { emitter.emit("setplaylistitem", body) }); this.emitter.on("subscribeevent", (body: SubscribeEventMessage) => { emitter.emit("subscribeevent", { sessionId: this.sessionId, body: body }) }); diff --git a/receivers/common/web/ListenerService.ts b/receivers/common/web/ListenerService.ts index 6300e24..f72e163 100644 --- a/receivers/common/web/ListenerService.ts +++ b/receivers/common/web/ListenerService.ts @@ -7,13 +7,13 @@ import { errorHandler } from 'src/Main'; const logger = new Logger('ListenerService', LoggerType.BACKEND); export abstract class ListenerService { - public readonly PORT: number; public emitter: EventEmitter = new EventEmitter(); protected sessionMap: Map = new Map(); private eventSubscribers: Map = new Map(); public abstract start(): void; public abstract stop(): void; + public abstract disconnect(sessionId: string): void; public send(opcode: number, message = null, sessionId = null) { // logger.info(`Sending message ${JSON.stringify(message)}`); diff --git a/receivers/common/web/MediaCache.ts b/receivers/common/web/MediaCache.ts index 1da2bd9..18eaf16 100644 --- a/receivers/common/web/MediaCache.ts +++ b/receivers/common/web/MediaCache.ts @@ -24,7 +24,7 @@ class CacheObject { export class MediaCache { private static instance: MediaCache = null; private cache: Map; - private cacheUrlMap: Map; + private cacheUrlMap: Map; private playlist: PlaylistContent; private playlistIndex: number; private quota: number; @@ -72,9 +72,7 @@ export class MediaCache { MediaCache.instance = null; this.cache.clear(); - this.cache = null; this.cacheUrlMap.clear(); - this.cacheUrlMap = null; this.playlist = null; this.quota = 0; this.cacheSize = 0; diff --git a/receivers/common/web/MimeTypes.ts b/receivers/common/web/MimeTypes.ts index bc9e577..8d7861c 100644 --- a/receivers/common/web/MimeTypes.ts +++ b/receivers/common/web/MimeTypes.ts @@ -45,7 +45,7 @@ export const supportedImageTypes = [ 'image/png', 'image/svg+xml', 'image/vnd.microsoft.icon', - 'image/webp' + 'image/webp', ]; export const supportedImageExtensions = [ @@ -57,7 +57,7 @@ export const supportedImageExtensions = [ '.jpeg', '.jpg', '.jpe', '.jif', '.jfif', '.jfi', '.png', '.svg', - '.webp' + '.webp', ]; export const supportedPlayerTypes = streamingMediaTypes.concat( diff --git a/receivers/common/web/TcpListenerService.ts b/receivers/common/web/TcpListenerService.ts index ab458ce..ebd01d5 100644 --- a/receivers/common/web/TcpListenerService.ts +++ b/receivers/common/web/TcpListenerService.ts @@ -6,7 +6,7 @@ import { Logger, LoggerType } from 'common/Logger'; const logger = new Logger('TcpListenerService', LoggerType.BACKEND); export class TcpListenerService extends ListenerService { - public readonly PORT = 46899; + public static readonly PORT = 46899; private server: net.Server; start() { @@ -15,7 +15,7 @@ export class TcpListenerService extends ListenerService { } this.server = net.createServer() - .listen(this.PORT) + .listen(TcpListenerService.PORT) .on("connection", this.handleConnection.bind(this)) .on("error", this.handleServerError.bind(this)); } diff --git a/receivers/common/web/WebSocketListenerService.ts b/receivers/common/web/WebSocketListenerService.ts index e3faad0..ed4f190 100644 --- a/receivers/common/web/WebSocketListenerService.ts +++ b/receivers/common/web/WebSocketListenerService.ts @@ -6,7 +6,7 @@ import { WebSocket, WebSocketServer } from 'modules/ws'; const logger = new Logger('WebSocketListenerService', LoggerType.BACKEND); export class WebSocketListenerService extends ListenerService { - public readonly PORT = 46898; + public static readonly PORT = 46898; private server: WebSocketServer; start() { @@ -14,7 +14,7 @@ export class WebSocketListenerService extends ListenerService { return; } - this.server = new WebSocketServer({ port: this.PORT }) + this.server = new WebSocketServer({ port: WebSocketListenerService.PORT }) .on("connection", this.handleConnection.bind(this)) .on("error", this.handleServerError.bind(this)); } diff --git a/receivers/electron/src/Main.ts b/receivers/electron/src/Main.ts index 1d1367a..dc41513 100644 --- a/receivers/electron/src/Main.ts +++ b/receivers/electron/src/Main.ts @@ -249,16 +249,16 @@ export class Main { }); }); l.emitter.on('disconnect', (message) => { - ConnectionMonitor.onDisconnect(l, message, l instanceof WebSocketListenerService, () => { + ConnectionMonitor.onDisconnect(message, l instanceof WebSocketListenerService, () => { Main.mainWindow?.webContents?.send('disconnect', message); Main.playerWindow?.webContents?.send('disconnect', message); }); }); - l.emitter.on('ping', (message) => { - ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService); + l.emitter.on('ping', (sessionId: string) => { + ConnectionMonitor.onPingPong(sessionId, l instanceof WebSocketListenerService); }); - l.emitter.on('pong', (message) => { - ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService); + l.emitter.on('pong', (sessionId: string) => { + ConnectionMonitor.onPingPong(sessionId, l instanceof WebSocketListenerService); }); l.emitter.on('initial', (message) => { logger.info(`Received 'Initial' message from sender: ${message}`); diff --git a/receivers/webos/fcast-receiver-service/src/Main.ts b/receivers/webos/fcast-receiver-service/src/Main.ts index bae5f8c..843b095 100644 --- a/receivers/webos/fcast-receiver-service/src/Main.ts +++ b/receivers/webos/fcast-receiver-service/src/Main.ts @@ -241,15 +241,15 @@ export class Main { }); }); l.emitter.on('disconnect', (message) => { - ConnectionMonitor.onDisconnect(l, message, l instanceof WebSocketListenerService, () => { + ConnectionMonitor.onDisconnect(message, l instanceof WebSocketListenerService, () => { Main.emitter.emit('disconnect', message); }); }); - l.emitter.on('ping', (message) => { - ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService); + l.emitter.on('ping', (sessionId: string) => { + ConnectionMonitor.onPingPong(sessionId, l instanceof WebSocketListenerService); }); - l.emitter.on('pong', (message) => { - ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService); + l.emitter.on('pong', (sessionId: string) => { + ConnectionMonitor.onPingPong(sessionId, l instanceof WebSocketListenerService); }); l.emitter.on('initial', (message) => { logger.info(`Received 'Initial' message from sender: ${message}`);