diff --git a/receivers/common/web/ConnectionMonitor.ts b/receivers/common/web/ConnectionMonitor.ts index 4a14d9b..26e09e4 100644 --- a/receivers/common/web/ConnectionMonitor.ts +++ b/receivers/common/web/ConnectionMonitor.ts @@ -58,14 +58,18 @@ export class ConnectionMonitor { setInterval(() => { if (ConnectionMonitor.backendConnections.size > 0) { for (const sessionId of ConnectionMonitor.backendConnections.keys()) { - if (ConnectionMonitor.heartbeatRetries.get(sessionId) > 3) { - ConnectionMonitor.logger.warn(`Could not ping device with connection id ${sessionId}. Disconnecting...`); - ConnectionMonitor.backendConnections.get(sessionId).disconnect(sessionId); - } + const listener = ConnectionMonitor.backendConnections.get(sessionId); - ConnectionMonitor.logger.debug(`Pinging session ${sessionId} with ${ConnectionMonitor.heartbeatRetries.get(sessionId)} retries left`); - ConnectionMonitor.backendConnections.get(sessionId).send(Opcode.Ping, null, sessionId); - ConnectionMonitor.heartbeatRetries.set(sessionId, ConnectionMonitor.heartbeatRetries.get(sessionId) + 1); + 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); + } } } }, ConnectionMonitor.connectionPingTimeout); diff --git a/receivers/common/web/FCastSession.ts b/receivers/common/web/FCastSession.ts index 98ddf44..5b74ea5 100644 --- a/receivers/common/web/FCastSession.ts +++ b/receivers/common/web/FCastSession.ts @@ -1,7 +1,8 @@ import * as net from 'net'; import { EventEmitter } from 'events'; -import { Opcode, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from 'common/Packets'; +import { Opcode, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, InitialSenderMessage, SetPlaylistItemMessage, SubscribeEventMessage, UnsubscribeEventMessage, PROTOCOL_VERSION, InitialReceiverMessage } from 'common/Packets'; import { Logger, LoggerType } from 'common/Logger'; +import { getComputerName, getAppName, getAppVersion, getPlayMessage } from 'src/Main'; import { WebSocket } from 'modules/ws'; import { v4 as uuidv4 } from 'modules/uuid'; const logger = new Logger('FCastSession', LoggerType.BACKEND); @@ -18,6 +19,7 @@ const MAXIMUM_PACKET_LENGTH = 32000; export class FCastSession { public sessionId: string; + public protocolVersion: number; buffer: Buffer = Buffer.alloc(MAXIMUM_PACKET_LENGTH); bytesRead = 0; packetLength = 0; @@ -26,14 +28,25 @@ export class FCastSession { state: SessionState; emitter = new EventEmitter(); + private sentInitialMessage: boolean; + constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) { this.sessionId = uuidv4(); + // Not all senders send a version message to the receiver on connection. Choosing version 2 + // as the base version since most/all current senders support this version. + this.protocolVersion = 2; + this.sentInitialMessage = false; this.socket = socket; this.writer = writer; this.state = SessionState.WaitingForLength; } send(opcode: number, message = null) { + if (!this.isSupportedOpcode(opcode)) { + return; + } + + message = this.stripUnsupportedFields(opcode, message); const json = message ? JSON.stringify(message) : null; logger.info(`send: (session: ${this.sessionId}, opcode: ${opcode}, body: ${json})`); @@ -189,9 +202,23 @@ export class FCastSession { case Opcode.SetSpeed: this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage); break; - case Opcode.Version: - this.emitter.emit("version", JSON.parse(body) as VersionMessage); + case Opcode.Version: { + const versionMessage = JSON.parse(body) as VersionMessage; + this.protocolVersion = (versionMessage.version > 0 && versionMessage.version <= PROTOCOL_VERSION) ? versionMessage.version : this.protocolVersion; + if (!this.sentInitialMessage && this.protocolVersion >= 3) { + this.send(Opcode.Initial, new InitialReceiverMessage( + getComputerName(), + getAppName(), + getAppVersion(), + getPlayMessage(), + )); + + this.sentInitialMessage = true; + } + + this.emitter.emit("version", versionMessage); break; + } case Opcode.Ping: this.send(Opcode.Pong); this.emitter.emit("ping"); @@ -199,6 +226,18 @@ export class FCastSession { case Opcode.Pong: this.emitter.emit("pong"); break; + case Opcode.Initial: + this.emitter.emit("initial", JSON.parse(body) as InitialSenderMessage); + break; + case Opcode.SetPlaylistItem: + this.emitter.emit("setplaylistitem", JSON.parse(body) as SetPlaylistItemMessage); + break; + case Opcode.SubscribeEvent: + this.emitter.emit("subscribeevent", JSON.parse(body) as SubscribeEventMessage); + break; + case Opcode.UnsubscribeEvent: + this.emitter.emit("unsubscribeevent", JSON.parse(body) as UnsubscribeEventMessage); + break; } } catch (e) { logger.warn(`Error handling packet from.`, e); @@ -222,5 +261,72 @@ export class FCastSession { 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("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 }) }); + this.emitter.on("unsubscribeevent", (body: UnsubscribeEventMessage) => { emitter.emit("unsubscribeevent", { sessionId: this.sessionId, body: body }) }); + } + + private isSupportedOpcode(opcode: number) { + switch (this.protocolVersion) { + case 1: + return opcode <= 8; + + case 2: + return opcode <= 13; + + case 3: + return opcode <= 19; + + default: + return false; + } + } + + private stripUnsupportedFields(opcode: number, message: any = null): any { + switch (this.protocolVersion) { + case 1: { + switch (opcode) { + case Opcode.Play: + delete message.speed; + delete message.headers; + break; + case Opcode.PlaybackUpdate: + delete message.generationTime; + delete message.duration; + delete message.speed; + break; + case Opcode.VolumeUpdate: + delete message.generationTime; + break; + default: + break; + } + + // fallthrough + } + case 2: { + switch (opcode) { + case Opcode.Play: + delete message.volume; + delete message.metadata; + break; + case Opcode.PlaybackUpdate: + delete message.itemIndex; + break; + default: + break; + } + + // fallthrough + } + case 3: + break; + + default: + break; + } + + return message; } } diff --git a/receivers/common/web/ListenerService.ts b/receivers/common/web/ListenerService.ts new file mode 100644 index 0000000..a2afd9b --- /dev/null +++ b/receivers/common/web/ListenerService.ts @@ -0,0 +1,146 @@ +import { FCastSession } from 'common/FCastSession'; +import { Opcode, EventSubscribeObject, EventType, KeyEvent, KeyDownEvent, KeyUpEvent } from 'common/Packets'; +import { Logger, LoggerType } from 'common/Logger'; +import { EventEmitter } from 'events'; +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 send(opcode: number, message = null, sessionId = null) { + // logger.info(`Sending message ${JSON.stringify(message)}`); + + if (sessionId) { + if (opcode === Opcode.Event.valueOf() && !this.isSubscribedToEvent(sessionId, message.event)) { + return; + } + + try { + this.sessionMap.get(sessionId)?.send(opcode, message); + } catch (e) { + logger.warn("Failed to send error.", e); + this.sessionMap.get(sessionId)?.close(); + } + } + else { + for (const session of this.sessionMap.values()) { + if (opcode === Opcode.Event.valueOf() && !this.isSubscribedToEvent(session.sessionId, message.event)) { + continue; + } + + try { + session.send(opcode, message); + } catch (e) { + logger.warn("Failed to send error.", e); + session.close(); + } + } + } + } + + public subscribeEvent(sessionId: string, event: EventSubscribeObject): any { + if (!this.eventSubscribers.has(sessionId)) { + this.eventSubscribers.set(sessionId, []); + } + + let sessionSubscriptions = this.eventSubscribers.get(sessionId); + sessionSubscriptions.push(event); + this.eventSubscribers.set(sessionId, sessionSubscriptions); + + if (event.type === EventType.KeyDown.valueOf() || event.type === EventType.KeyUp.valueOf()) { + return this.getAllSubscribedKeys(); + } + } + + public unsubscribeEvent(sessionId: string, event: EventSubscribeObject): any { + if (this.eventSubscribers.has(sessionId)) { + let sessionSubscriptions = this.eventSubscribers.get(sessionId); + + const index = sessionSubscriptions.findIndex((obj) => ListenerService.deepEqual(obj, event)); + if (index != -1) { + sessionSubscriptions.splice(index, 1); + } + + this.eventSubscribers.set(sessionId, sessionSubscriptions); + } + + if (event.type === EventType.KeyDown.valueOf() || event.type === EventType.KeyUp.valueOf()) { + return this.getAllSubscribedKeys(); + } + } + + public getSessions(): string[] { + return [...this.sessionMap.keys()]; + } + + public getSessionProtocolVersion(sessionId: string) { + return this.sessionMap.get(sessionId).protocolVersion; + } + + private isSubscribedToEvent(sessionId: string, event: EventSubscribeObject) { + let isSubscribed = false; + + if (this.eventSubscribers.has(sessionId)) { + for (const e of this.eventSubscribers.get(sessionId).values()) { + if (e.type === event.type) { + if (e.type === EventType.KeyDown.valueOf() || e.type === EventType.KeyUp.valueOf()) { + const subscribeEvent = e.type === EventType.KeyDown.valueOf() ? e as KeyDownEvent : e as KeyUpEvent; + const keyEvent = event as KeyEvent; + + if (!subscribeEvent.keys.includes(keyEvent.key)) { + continue; + } + } + + isSubscribed = true; + break; + } + } + } + + return isSubscribed; + } + + private getAllSubscribedKeys(): { keyDown: Set, keyUp: Set } { + let keyDown = new Set(); + let keyUp = new Set(); + + for (const session of this.eventSubscribers.values()) { + for (const event of session) { + switch (event.type) { + case EventType.KeyDown: + keyDown = new Set([...keyDown, ...(event as KeyDownEvent).keys]); + break; + + case EventType.KeyUp: + keyUp = new Set([...keyUp, ...(event as KeyUpEvent).keys]); + break; + + default: + break; + } + } + } + + return { keyDown: keyDown, keyUp: keyUp }; + } + + protected async handleServerError(err: NodeJS.ErrnoException) { + errorHandler(err); + } + + private static deepEqual(x, y) { + const ok = Object.keys, tx = typeof x, ty = typeof y; + return x && y && tx === 'object' && tx === ty ? ( + ok(x).length === ok(y).length && + ok(x).every(key => this.deepEqual(x[key], y[key])) + ) : (x === y); + } +} diff --git a/receivers/common/web/Packets.ts b/receivers/common/web/Packets.ts index a9c1a02..10394f7 100644 --- a/receivers/common/web/Packets.ts +++ b/receivers/common/web/Packets.ts @@ -1,3 +1,6 @@ +// Protocol Documentation: https://gitlab.futo.org/videostreaming/fcast/-/wikis/Protocol-version-3 +export const PROTOCOL_VERSION = 3; + export enum Opcode { None = 0, Play = 1, @@ -12,33 +15,101 @@ export enum Opcode { SetSpeed = 10, Version = 11, Ping = 12, - Pong = 13 + Pong = 13, + Initial = 14, + PlayUpdate = 15, + SetPlaylistItem = 16, + SubscribeEvent = 17, + UnsubscribeEvent = 18, + Event = 19, }; +export enum PlaybackState { + Idle = 0, + Playing = 1, + Paused = 2, +} + +export enum ContentType { + Playlist = 0, +} + +export enum MetadataType { + Generic = 0, +} + +export enum EventType { + MediaItemStart = 0, + MediaItemEnd = 1, + MediaItemChange = 2, + KeyDown = 3, + KeyUp = 4, +} + +// Required supported keys for listener events defined below. +// Optionally supported key values list: https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values +export enum KeyNames { + Left = 'ArrowLeft', + Right = 'ArrowRight', + Up = 'ArrowUp', + Down = 'ArrowDown', + Ok = 'Enter', +} + +export interface MetadataObject { + type: MetadataType; +} + +export class GenericMediaMetadata implements MetadataObject { + readonly type = MetadataType.Generic; + + constructor( + public title: string = null, + public thumbnailUrl: string = null, + public custom: any = null, + ) {} +} + export class PlayMessage { constructor( - public container: string, - public url: string = null, - public content: string = null, - public time: number = null, - public speed: number = null, - public headers: { [key: string]: string } = null + public container: string, // The MIME type (video/mp4) + public url: string = null, // The URL to load (optional) + public content: string = null, // The content to load (i.e. a DASH manifest, json content, optional) + public time: number = null, // The time to start playing in seconds + public volume: number = null, // The desired volume (0-1) + public speed: number = null, // The factor to multiply playback speed by (defaults to 1.0) + public headers: { [key: string]: string } = null, // HTTP request headers to add to the play request Map + public metadata: MetadataObject = null, ) {} } export class SeekMessage { constructor( - public time: number, + public time: number, // The time to seek to in seconds ) {} } export class PlaybackUpdateMessage { constructor( - public generationTime: number, - public time: number, - public duration: number, - public state: number, - public speed: number + public generationTime: number, // The time the packet was generated (unix time milliseconds) + public state: number, // The playback state + public time: number = null, // The current time playing in seconds + public duration: number = null, // The duration in seconds + public speed: number = null, // The playback speed factor + public itemIndex: number = null, // The playlist item index currently being played on receiver + ) {} +} + +export class VolumeUpdateMessage { + constructor( + public generationTime: number, // The time the packet was generated (unix time milliseconds) + public volume: number, // The current volume (0-1) + ) {} +} + +export class SetVolumeMessage { + constructor( + public volume: number, // The desired volume (0-1) ) {} } @@ -48,27 +119,154 @@ export class PlaybackErrorMessage { ) {} } -export class VolumeUpdateMessage { - constructor( - public generationTime: number, - public volume: number - ) {} -} - -export class SetVolumeMessage { - constructor( - public volume: number, - ) {} -} - export class SetSpeedMessage { constructor( - public speed: number, + public speed: number, // The factor to multiply playback speed by ) {} } export class VersionMessage { constructor( - public version: number, + public version: number, // Protocol version number (integer) + ) {} +} + +export interface ContentObject { + contentType: ContentType; +} + +export class MediaItem { + constructor( + public container: string, // The MIME type (video/mp4) + public url: string = null, // The URL to load (optional) + public content: string = null, // The content to load (i.e. a DASH manifest, json content, optional) + public time: number = null, // The time to start playing in seconds + public volume: number = null, // The desired volume (0-1) + public speed: number = null, // The factor to multiply playback speed by (defaults to 1.0) + public cache: boolean = null, // Indicates if the receiver should preload the media item + public showDuration: number = null, // Indicates how long the item content is presented on screen in seconds + public headers: { [key: string]: string } = null, // HTTP request headers to add to the play request Map + public metadata: MetadataObject = null, + ) {} +} + +export class PlaylistContent implements ContentObject { + readonly contentType = ContentType.Playlist; + + constructor( + public items: MediaItem[], + public offset: number = null, // Start position of the first item to play from the playlist + public volume: number = null, // The desired volume (0-1) + public speed: number = null, // The factor to multiply playback speed by (defaults to 1.0) + public forwardCache: number = null, // Count of media items should be pre-loaded forward from the current view index + public backwardCache: number = null, // Count of media items should be pre-loaded backward from the current view index + public metadata: MetadataObject = null, + ) {} +} + +export class InitialSenderMessage { + constructor( + public displayName: string = null, + public appName: string = null, + public appVersion: string = null, + ) {} +} + +export class InitialReceiverMessage { + constructor( + public displayName: string = null, + public appName: string = null, + public appVersion: string = null, + public playData: PlayMessage = null, + ) {} +} + +export class PlayUpdateMessage { + constructor( + public generationTime: number, + public playData: PlayMessage = null, + ) {} +} + +export class SetPlaylistItemMessage { + constructor( + public itemIndex: number, // The playlist item index to play on receiver + ) {} +} + +export interface EventSubscribeObject { + type: EventType; +} + +export interface EventObject { + type: EventType; +} + +export class MediaItemStartEvent implements EventSubscribeObject { + readonly type = EventType.MediaItemStart; + + constructor() {} +} + +export class MediaItemEndEvent implements EventSubscribeObject { + readonly type = EventType.MediaItemEnd; + + constructor() {} +} + +export class MediaItemChangeEvent implements EventSubscribeObject { + readonly type = EventType.MediaItemChange; + + constructor() {} +} + +export class KeyDownEvent implements EventSubscribeObject { + readonly type = EventType.KeyDown; + + constructor( + public keys: string[], + ) {} +} + +export class KeyUpEvent implements EventSubscribeObject { + readonly type = EventType.KeyUp; + + constructor( + public keys: string[], + ) {} +} + +export class SubscribeEventMessage { + constructor( + public event: EventSubscribeObject, + ) {} +} + +export class UnsubscribeEventMessage { + constructor( + public event: EventSubscribeObject, + ) {} +} + +export class MediaItemEvent implements EventObject { + constructor( + public type: EventType, + public mediaItem: MediaItem, + ) {} +} + +export class KeyEvent implements EventObject { + constructor( + public type: EventType, + public key: string, + public repeat: boolean, + public handled: boolean, + ) {} +} + +export class EventMessage { + constructor( + public generationTime: number, + public event: EventObject, ) {} } diff --git a/receivers/common/web/TcpListenerService.ts b/receivers/common/web/TcpListenerService.ts index 268c7ce..ab458ce 100644 --- a/receivers/common/web/TcpListenerService.ts +++ b/receivers/common/web/TcpListenerService.ts @@ -1,17 +1,13 @@ import * as net from 'net'; +import { ListenerService } from 'common/ListenerService'; import { FCastSession } from 'common/FCastSession'; -import { Opcode } from 'common/Packets'; +import { Opcode, PROTOCOL_VERSION, VersionMessage } from 'common/Packets'; import { Logger, LoggerType } from 'common/Logger'; -import { EventEmitter } from 'events'; -import { errorHandler } from 'src/Main'; const logger = new Logger('TcpListenerService', LoggerType.BACKEND); -export class TcpListenerService { - public static PORT = 46899; - emitter = new EventEmitter(); - +export class TcpListenerService extends ListenerService { + public readonly PORT = 46899; private server: net.Server; - private sessionMap = new Map(); start() { if (this.server != null) { @@ -19,7 +15,7 @@ export class TcpListenerService { } this.server = net.createServer() - .listen(TcpListenerService.PORT) + .listen(this.PORT) .on("connection", this.handleConnection.bind(this)) .on("error", this.handleServerError.bind(this)); } @@ -35,29 +31,6 @@ export class TcpListenerService { server.close(); } - send(opcode: number, message = null, sessionId = null) { - // logger.info(`Sending message ${JSON.stringify(message)}`); - - if (sessionId) { - try { - this.sessionMap.get(sessionId)?.send(opcode, message); - } catch (e) { - logger.warn("Failed to send error.", e); - this.sessionMap.get(sessionId)?.close(); - } - } - else { - for (const session of this.sessionMap.values()) { - try { - session.send(opcode, message); - } catch (e) { - logger.warn("Failed to send error.", e); - session.close(); - } - } - } - } - disconnect(sessionId: string) { this.sessionMap.get(sessionId)?.socket.destroy(); this.sessionMap.delete(sessionId); @@ -69,14 +42,6 @@ export class TcpListenerService { return senders; } - public getSessions(): string[] { - return [...this.sessionMap.keys()]; - } - - private async handleServerError(err: NodeJS.ErrnoException) { - errorHandler(err); - } - private handleConnection(socket: net.Socket) { logger.info(`New connection from ${socket.remoteAddress}:${socket.remotePort}`); @@ -106,7 +71,7 @@ export class TcpListenerService { this.emitter.emit('connect', { sessionId: session.sessionId, type: 'tcp', data: { address: socket.remoteAddress, port: socket.remotePort }}); try { logger.info('Sending version'); - session.send(Opcode.Version, {version: 2}); + session.send(Opcode.Version, new VersionMessage(PROTOCOL_VERSION)); } catch (e) { logger.info('Failed to send version', e); } diff --git a/receivers/common/web/WebSocketListenerService.ts b/receivers/common/web/WebSocketListenerService.ts index 4be5118..e3faad0 100644 --- a/receivers/common/web/WebSocketListenerService.ts +++ b/receivers/common/web/WebSocketListenerService.ts @@ -1,25 +1,20 @@ +import { ListenerService } from 'common/ListenerService'; import { FCastSession } from 'common/FCastSession'; -import { Opcode } from 'common/Packets'; +import { Opcode, PROTOCOL_VERSION, VersionMessage } from 'common/Packets'; import { Logger, LoggerType } from 'common/Logger'; -import { EventEmitter } from 'events'; import { WebSocket, WebSocketServer } from 'modules/ws'; -import { errorHandler } from 'src/Main'; const logger = new Logger('WebSocketListenerService', LoggerType.BACKEND); -export class WebSocketListenerService { - public static PORT = 46898; - - emitter = new EventEmitter(); - +export class WebSocketListenerService extends ListenerService { + public readonly PORT = 46898; private server: WebSocketServer; - private sessionMap = new Map(); start() { if (this.server != null) { return; } - this.server = new WebSocketServer({ port: WebSocketListenerService.PORT }) + this.server = new WebSocketServer({ port: this.PORT }) .on("connection", this.handleConnection.bind(this)) .on("error", this.handleServerError.bind(this)); } @@ -35,39 +30,10 @@ export class WebSocketListenerService { server.close(); } - send(opcode: number, message = null, sessionId = null) { - if (sessionId) { - try { - this.sessionMap.get(sessionId)?.send(opcode, message); - } catch (e) { - logger.warn("Failed to send error.", e); - this.sessionMap.get(sessionId)?.close(); - } - } - else { - for (const session of this.sessionMap.values()) { - try { - session.send(opcode, message); - } catch (e) { - logger.warn("Failed to send error.", e); - session.close(); - } - } - } - } - disconnect(sessionId: string) { this.sessionMap.get(sessionId)?.close(); } - public getSessions(): string[] { - return [...this.sessionMap.keys()]; - } - - private async handleServerError(err: NodeJS.ErrnoException) { - errorHandler(err); - } - private handleConnection(socket: WebSocket, request: any) { logger.info('New WebSocket connection'); @@ -101,9 +67,9 @@ export class WebSocketListenerService { this.emitter.emit('connect', { sessionId: session.sessionId, type: 'ws', data: { url: socket.url }}); try { logger.info('Sending version'); - session.send(Opcode.Version, {version: 2}); + session.send(Opcode.Version, new VersionMessage(PROTOCOL_VERSION)); } catch (e) { - logger.info('Failed to send version'); + logger.info('Failed to send version', e); } } } diff --git a/receivers/common/web/main/Preload.ts b/receivers/common/web/main/Preload.ts index dfa8f76..88e2c3d 100644 --- a/receivers/common/web/main/Preload.ts +++ b/receivers/common/web/main/Preload.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Logger, LoggerType } from 'common/Logger'; +import { EventMessage } from 'common/Packets'; const logger = new Logger('MainWindow', LoggerType.FRONTEND); // Cannot directly pass the object to the renderer for some reason... @@ -23,6 +24,10 @@ declare global { } let preloadData: Record = {}; +preloadData.subscribedKeys = { + keyDown: new Set(), + keyUp: new Set(), +}; // @ts-ignore if (TARGET === 'electron') { @@ -32,13 +37,20 @@ if (TARGET === 'electron') { electronAPI.ipcRenderer.on("device-info", (_event, value: any) => { preloadData.deviceInfo = value; }) + electronAPI.ipcRenderer.on("event-subscribed-keys-update", (_event, value: { keyDown: Set, keyUp: Set }) => { + logger.info('MAIN WINDOW Updated key subscriptions', value); + preloadData.subscribedKeys.keyDown = value.keyDown; + preloadData.subscribedKeys.keyUp = value.keyUp; + }) electronAPI.contextBridge.exposeInMainWorld('targetAPI', { onDeviceInfo: (callback: any) => electronAPI.ipcRenderer.on('device-info', callback), getDeviceInfo: () => preloadData.deviceInfo, getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'), + getSubscribedKeys: () => preloadData.subscribedKeys, onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback), onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback), + emitEvent: (message: EventMessage) => electronAPI.ipcRenderer.send('emit-event', message), logger: loggerInterface, }); diff --git a/receivers/common/web/main/Renderer.ts b/receivers/common/web/main/Renderer.ts index 74fbac5..987cf65 100644 --- a/receivers/common/web/main/Renderer.ts +++ b/receivers/common/web/main/Renderer.ts @@ -3,6 +3,7 @@ import QRCode from 'modules/qrcode'; import * as connectionMonitor from '../ConnectionMonitor'; import { onQRCodeRendered } from 'src/main/Renderer'; import { toast, ToastIcon } from '../components/Toast'; +import { EventMessage, EventType, KeyEvent } from 'common/Packets'; const connectionStatusText = document.getElementById('connection-status-text'); const connectionStatusSpinner = document.getElementById('connection-spinner'); @@ -199,3 +200,14 @@ function renderQRCode(url: string) { onQRCodeRendered(); } + +document.addEventListener('keydown', (event: KeyboardEvent) => { + if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) { + window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, false))); + } +}); +document.addEventListener('keyup', (event: KeyboardEvent) => { + if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { + window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); + } +}); diff --git a/receivers/common/web/player/Preload.ts b/receivers/common/web/player/Preload.ts index 8bf5dc8..ab757cc 100644 --- a/receivers/common/web/player/Preload.ts +++ b/receivers/common/web/player/Preload.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage, Opcode } from 'common/Packets'; +import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage, Opcode, EventMessage } from 'common/Packets'; import { Logger, LoggerType } from 'common/Logger'; const logger = new Logger('PlayerWindow', LoggerType.FRONTEND); @@ -25,12 +25,22 @@ declare global { } let preloadData: Record = {}; +preloadData.subscribedKeys = { + keyDown: new Set(), + keyUp: new Set(), +}; // @ts-ignore if (TARGET === 'electron') { // @ts-ignore const electronAPI = __non_webpack_require__('electron'); + electronAPI.ipcRenderer.on("event-subscribed-keys-update", (_event, value: { keyDown: Set, keyUp: Set }) => { + logger.info('PLAYER Updated key subscriptions', value); + preloadData.subscribedKeys.keyDown = value.keyDown; + preloadData.subscribedKeys.keyUp = value.keyUp; + }) + electronAPI.contextBridge.exposeInMainWorld('targetAPI', { sendPlaybackError: (error: PlaybackErrorMessage) => electronAPI.ipcRenderer.send('send-playback-error', error), sendPlaybackUpdate: (update: PlaybackUpdateMessage) => electronAPI.ipcRenderer.send('send-playback-update', update), @@ -41,9 +51,13 @@ if (TARGET === 'electron') { onSeek: (callback: any) => electronAPI.ipcRenderer.on("seek", callback), onSetVolume: (callback: any) => electronAPI.ipcRenderer.on("setvolume", callback), onSetSpeed: (callback: any) => electronAPI.ipcRenderer.on("setspeed", callback), - getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'), onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback), onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback), + onSetPlaylistItem: (callback: any) => electronAPI.ipcRenderer.on("setplaylistitem", callback), + emitEvent: (message: EventMessage) => electronAPI.ipcRenderer.send('emit-event', message), + + getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'), + getSubscribedKeys: () => preloadData.subscribedKeys, logger: loggerInterface, }); diff --git a/receivers/common/web/player/Renderer.ts b/receivers/common/web/player/Renderer.ts index 0e492fc..51ebd3f 100644 --- a/receivers/common/web/player/Renderer.ts +++ b/receivers/common/web/player/Renderer.ts @@ -1,6 +1,6 @@ import dashjs from 'modules/dashjs'; import Hls, { LevelLoadedData } from 'modules/hls.js'; -import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets'; +import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets'; import { Player, PlayerType } from './Player'; import * as connectionMonitor from '../ConnectionMonitor'; import { toast, ToastIcon } from '../components/Toast'; @@ -35,7 +35,7 @@ function formatDuration(duration: number) { } function sendPlaybackUpdate(updateState: number) { - const updateMessage = new PlaybackUpdateMessage(Date.now(), player.getCurrentTime(), player.getDuration(), updateState, player.getPlaybackRate()); + const updateMessage = new PlaybackUpdateMessage(Date.now(), updateState, player.getCurrentTime(), player.getDuration(), player.getPlaybackRate()); if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) { lastPlayerUpdateGenerationTime = updateMessage.generationTime; @@ -73,6 +73,7 @@ function onPlayerLoad(value: PlayMessage, currentPlaybackRate?: number, currentV window.targetAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: 1.0 }); } + window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem))); player.play(); } @@ -120,11 +121,18 @@ let isLive = false; let isLivePosition = false; let captionsBaseHeight = 0; let captionsContentHeight = 0; +let cachedPlayMediaItem: MediaItem = null; function onPlay(_event, value: PlayMessage) { logger.info("Handle play message renderer", JSON.stringify(value)); const currentVolume = player ? player.getVolume() : null; const currentPlaybackRate = player ? player.getPlaybackRate() : null; + cachedPlayMediaItem = new MediaItem( + value.container, value.url, value.content, + value.time, value.volume, value.speed, + null, null, value.headers, value.metadata + ); + window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, cachedPlayMediaItem))); if (player) { if ((player.getSource() === value.url) || (player.getSource() === value.content)) { @@ -167,7 +175,10 @@ function onPlay(_event, value: PlayMessage) { // Player event handlers dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); }); dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); }); - dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => { sendPlaybackUpdate(0) }); + dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => { + sendPlaybackUpdate(0); + window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem))); + }); dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); @@ -290,7 +301,10 @@ function onPlay(_event, value: PlayMessage) { if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) { videoElement.onplay = () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); }; videoElement.onpause = () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); }; - videoElement.onended = () => { sendPlaybackUpdate(0) }; + videoElement.onended = () => { + sendPlaybackUpdate(0); + window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemEnd, cachedPlayMediaItem))); + }; videoElement.ontimeupdate = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); @@ -737,69 +751,82 @@ document.addEventListener('click', (event: MouseEvent) => { const skipInterval = 10; const volumeIncrement = 0.1; -function keyDownEventListener(event: any) { +function keyDownEventListener(event: KeyboardEvent) { // logger.info("KeyDown", event); - const handledCase = targetKeyDownEventListener(event); - if (handledCase) { - return; + let handledCase = targetKeyDownEventListener(event); + + if (!handledCase) { + switch (event.code) { + case 'KeyF': + case 'F11': + playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); + event.preventDefault(); + handledCase = true; + break; + case 'Escape': + playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen); + event.preventDefault(); + handledCase = true; + break; + case 'ArrowLeft': + skipBack(); + event.preventDefault(); + handledCase = true; + break; + case 'ArrowRight': + skipForward(); + event.preventDefault(); + handledCase = true; + break; + case "Home": + player?.setCurrentTime(0); + event.preventDefault(); + handledCase = true; + break; + case "End": + if (isLive) { + setLivePosition(); + } + else { + player?.setCurrentTime(player?.getDuration()); + } + event.preventDefault(); + handledCase = true; + break; + case 'KeyK': + case 'Space': + case 'Enter': + // Play/pause toggle + if (player?.isPaused()) { + player?.play(); + } else { + player?.pause(); + } + event.preventDefault(); + handledCase = true; + break; + case 'KeyM': + // Mute toggle + player?.setMute(!player?.isMuted()); + handledCase = true; + break; + case 'ArrowUp': + // Volume up + volumeChangeHandler(Math.min(player?.getVolume() + volumeIncrement, 1)); + handledCase = true; + break; + case 'ArrowDown': + // Volume down + volumeChangeHandler(Math.max(player?.getVolume() - volumeIncrement, 0)); + handledCase = true; + break; + default: + break; + } } - switch (event.code) { - case 'KeyF': - case 'F11': - playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); - event.preventDefault(); - break; - case 'Escape': - playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen); - event.preventDefault(); - break; - case 'ArrowLeft': - skipBack(); - event.preventDefault(); - break; - case 'ArrowRight': - skipForward(); - event.preventDefault(); - break; - case "Home": - player?.setCurrentTime(0); - event.preventDefault(); - break; - case "End": - if (isLive) { - setLivePosition(); - } - else { - player?.setCurrentTime(player?.getDuration()); - } - event.preventDefault(); - break; - case 'KeyK': - case 'Space': - case 'Enter': - // Play/pause toggle - if (player?.isPaused()) { - player?.play(); - } else { - player?.pause(); - } - event.preventDefault(); - break; - case 'KeyM': - // Mute toggle - player?.setMute(!player?.isMuted()); - 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; - default: - break; + if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) { + window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase))); } } @@ -814,6 +841,11 @@ function skipForward() { } document.addEventListener('keydown', keyDownEventListener); +document.addEventListener('keyup', (event: KeyboardEvent) => { + if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { + window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); + } +}); export { PlayerControlEvent, diff --git a/receivers/common/web/viewer/Renderer.ts b/receivers/common/web/viewer/Renderer.ts index cb0e55a..e4664d6 100644 --- a/receivers/common/web/viewer/Renderer.ts +++ b/receivers/common/web/viewer/Renderer.ts @@ -1,4 +1,4 @@ -import { PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets'; +import { EventMessage, EventType, KeyEvent, MediaItem, MediaItemEvent, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from 'common/Packets'; import { supportedImageTypes } from 'common/MimeTypes'; import * as connectionMonitor from '../ConnectionMonitor'; import { toast, ToastIcon } from '../components/Toast'; @@ -11,6 +11,12 @@ const genericViewer = document.getElementById('viewer-generic') as HTMLIFrameEle function onPlay(_event, value: PlayMessage) { logger.info("Handle play message renderer", JSON.stringify(value)); + const playMediaItem = new MediaItem( + value.container, value.url, value.content, + value.time, value.volume, value.speed, + null, null, value.headers, value.metadata + ); + window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemChange, playMediaItem))); const src = value.url ? value.url : value.content; if (src && value.container && supportedImageTypes.find(v => v === value.container.toLocaleLowerCase()) && imageViewer) { @@ -59,3 +65,14 @@ connectionMonitor.setUiUpdateCallbacks({ }); window.targetAPI.onPlay(onPlay); + +document.addEventListener('keydown', (event: KeyboardEvent) => { + if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) { + window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, false))); + } +}); +document.addEventListener('keyup', (event: KeyboardEvent) => { + if (window.targetAPI.getSubscribedKeys().keyUp.has(event.key)) { + window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyUp, event.key, event.repeat, false))); + } +}); diff --git a/receivers/electron/src/Main.ts b/receivers/electron/src/Main.ts index 1b7d9ba..c8e2397 100644 --- a/receivers/electron/src/Main.ts +++ b/receivers/electron/src/Main.ts @@ -1,5 +1,5 @@ import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog, shell } from 'electron'; -import { Opcode, PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets'; +import { Opcode, PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage, PlayMessage, PlayUpdateMessage, EventMessage, EventType, ContentObject, ContentType, PlaylistContent } from 'common/Packets'; import { supportedPlayerTypes } from 'common/MimeTypes'; import { DiscoveryService } from 'common/DiscoveryService'; import { TcpListenerService } from 'common/TcpListenerService'; @@ -8,6 +8,8 @@ import { NetworkService } from 'common/NetworkService'; import { ConnectionMonitor } from 'common/ConnectionMonitor'; import { Logger, LoggerType } from 'common/Logger'; import { Updater } from './Updater'; +import * as http from 'http'; +import * as https from 'https'; import * as os from 'os'; import * as path from 'path'; import yargs from 'yargs'; @@ -15,6 +17,15 @@ import { hideBin } from 'yargs/helpers'; const cp = require('child_process'); let logger = null; +class AppCache { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public interfaces: any = null; + public appName: string = null; + public appVersion: string = null; + public playMessage: PlayMessage = null; + public subscribedKeys = new Set(); +} + export class Main { static shouldOpenMainWindow = true; static startFullscreen = false; @@ -26,8 +37,8 @@ export class Main { static discoveryService: DiscoveryService; static connectionMonitor: ConnectionMonitor; static tray: Tray; + static cache: AppCache = new AppCache(); - private static cachedInterfaces = null; private static playerWindowContentViewer = null; private static toggleMainWindow() { @@ -156,7 +167,31 @@ export class Main { const listeners = [Main.tcpListenerService, Main.webSocketListenerService]; listeners.forEach(l => { - l.emitter.on("play", async (message) => { + l.emitter.on("play", async (message: PlayMessage) => { + Main.cache.playMessage = message; + l.send(Opcode.PlayUpdate, new PlayUpdateMessage(Date.now(), message)); + + // todo: finish implementation (player window playlist context, main process media caching) + if (message.container === 'application/json') { + const json: ContentObject = message.url ? await fetchJSON(message.url) : JSON.parse(message.content); + + if (json && json.contentType !== undefined) { + switch (json.contentType) { + case ContentType.Playlist: { + const playlist = json as PlaylistContent; + const offset = playlist.offset ? playlist.offset : 0; + + message = new PlayMessage(playlist.items[offset].container, playlist.items[offset].url, playlist.items[offset].content, + playlist.items[offset].time, playlist.items[offset].volume, playlist.items[offset].speed, + playlist.items[offset].headers, playlist.items[offset].metadata); + break; + } + + default: + break; + } + } + } const contentViewer = supportedPlayerTypes.find(v => v === message.container.toLocaleLowerCase()) ? 'player' : 'viewer'; if (!Main.playerWindow) { @@ -224,6 +259,26 @@ export class Main { l.emitter.on('pong', (message) => { ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService); }); + l.emitter.on('initial', (message) => { + logger.info(`Received 'Initial' message from sender: ${message}`); + }); + l.emitter.on("setplaylistitem", (message) => Main.playerWindow?.webContents?.send("setplaylistitem", message)); + l.emitter.on('subscribeevent', (message) => { + const subscribeData = l.subscribeEvent(message.sessionId, message.body.event); + + if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) { + Main.mainWindow?.webContents?.send("event-subscribed-keys-update", subscribeData); + Main.playerWindow?.webContents?.send("event-subscribed-keys-update", subscribeData); + } + }); + l.emitter.on('unsubscribeevent', (message) => { + const unsubscribeData = l.unsubscribeEvent(message.sessionId, message.body.event); + + if (message.body.event.type === EventType.KeyDown.valueOf() || message.body.event.type === EventType.KeyUp.valueOf()) { + Main.mainWindow?.webContents?.send("event-subscribed-keys-update", unsubscribeData); + Main.playerWindow?.webContents?.send("event-subscribed-keys-update", unsubscribeData); + } + }); l.start(); ipcMain.on('send-playback-error', (event: IpcMainEvent, value: PlaybackErrorMessage) => { @@ -237,6 +292,10 @@ export class Main { ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => { l.send(Opcode.VolumeUpdate, value); }); + + ipcMain.on('emit-event', (event: IpcMainEvent, value: EventMessage) => { + l.send(Opcode.Event, value); + }); }); ipcMain.on('send-download-request', async () => { @@ -299,7 +358,7 @@ export class Main { // eslint-disable-next-line @typescript-eslint/no-explicit-any ipcMain.on('network-changed', (event: IpcMainEvent, value: any) => { - Main.cachedInterfaces = value; + Main.cache.interfaces = value; Main.mainWindow?.webContents?.send("device-info", { name: os.hostname(), interfaces: value }); }); @@ -364,8 +423,8 @@ export class Main { Main.mainWindow.show(); Main.mainWindow.on('ready-to-show', () => { - if (Main.cachedInterfaces) { - Main.mainWindow?.webContents?.send("device-info", { name: os.hostname(), interfaces: Main.cachedInterfaces }); + if (Main.cache.interfaces) { + Main.mainWindow?.webContents?.send("device-info", { name: os.hostname(), interfaces: Main.cache.interfaces }); } networkWorker.loadFile(path.join(__dirname, 'main/worker.html')); @@ -375,6 +434,8 @@ export class Main { static async main(app: Electron.App) { try { Main.application = app; + Main.cache.appName = app.name; + Main.cache.appVersion = app.getVersion(); const argv = yargs(hideBin(process.argv)) .version(app.getVersion()) @@ -477,6 +538,18 @@ export function getComputerName() { } } +export function getAppName() { + return Main.cache.appName; +} + +export function getAppVersion() { + return Main.cache.appVersion; +} + +export function getPlayMessage() { + return Main.cache.playMessage; +} + export async function errorHandler(error: Error) { logger.error(error); logger.shutdown(); @@ -497,3 +570,27 @@ export async function errorHandler(error: Error) { Main.application.exit(0); } } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function fetchJSON(url: string): Promise { + const protocol = url.startsWith('https') ? https : http; + + return new Promise((resolve, reject) => { + protocol.get(url, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (err) { + reject(err); + } + }); + }).on('error', (err) => { + reject(err); + }); + }); +} diff --git a/receivers/electron/src/Updater.ts b/receivers/electron/src/Updater.ts index 0cdbf27..4dedf26 100644 --- a/receivers/electron/src/Updater.ts +++ b/receivers/electron/src/Updater.ts @@ -6,6 +6,7 @@ import { app } from 'electron'; import { Store } from './Store'; import sudo from 'sudo-prompt'; import { Logger, LoggerType } from 'common/Logger'; +import { fetchJSON } from './Main'; const cp = require('child_process'); const extract = require('extract-zip'); @@ -91,28 +92,6 @@ export class Updater { Store.set('updater', updaterSettings); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private static async fetchJSON(url: string): Promise { - return new Promise((resolve, reject) => { - https.get(url, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - - res.on('end', () => { - try { - resolve(JSON.parse(data)); - } catch (err) { - reject(err); - } - }); - }).on('error', (err) => { - reject(err); - }); - }); - } - private static async downloadFile(url: string, destination: string): Promise { return new Promise((resolve, reject) => { const file = fs.createWriteStream(destination); @@ -345,7 +324,7 @@ export class Updater { logger.info('Checking for updates...'); try { - Updater.releasesJson = await Updater.fetchJSON(`${Updater.baseUrl}/releases_v${Updater.supportedReleasesJsonVersion}.json`.toString()) as ReleaseInfo; + Updater.releasesJson = await fetchJSON(`${Updater.baseUrl}/releases_v${Updater.supportedReleasesJsonVersion}.json`.toString()) as ReleaseInfo; const localChannelVersion: number = Updater.localPackageJson.channelVersion ? Updater.localPackageJson.channelVersion : 0; const currentChannelVersion: number = Updater.releasesJson.channelCurrentVersions[Updater.updateChannel] ? Updater.releasesJson.channelCurrentVersions[Updater.updateChannel] : 0;