mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-06-24 21:25:23 +00:00
Electron: Initial support for most of protocol v3
This commit is contained in:
parent
a83f92d874
commit
72d5c10918
13 changed files with 764 additions and 216 deletions
|
@ -58,14 +58,18 @@ export class ConnectionMonitor {
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (ConnectionMonitor.backendConnections.size > 0) {
|
if (ConnectionMonitor.backendConnections.size > 0) {
|
||||||
for (const sessionId of ConnectionMonitor.backendConnections.keys()) {
|
for (const sessionId of ConnectionMonitor.backendConnections.keys()) {
|
||||||
if (ConnectionMonitor.heartbeatRetries.get(sessionId) > 3) {
|
const listener = ConnectionMonitor.backendConnections.get(sessionId);
|
||||||
ConnectionMonitor.logger.warn(`Could not ping device with connection id ${sessionId}. Disconnecting...`);
|
|
||||||
ConnectionMonitor.backendConnections.get(sessionId).disconnect(sessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectionMonitor.logger.debug(`Pinging session ${sessionId} with ${ConnectionMonitor.heartbeatRetries.get(sessionId)} retries left`);
|
if (listener.getSessionProtocolVersion(sessionId) >= 2) {
|
||||||
ConnectionMonitor.backendConnections.get(sessionId).send(Opcode.Ping, null, sessionId);
|
if (ConnectionMonitor.heartbeatRetries.get(sessionId) > 3) {
|
||||||
ConnectionMonitor.heartbeatRetries.set(sessionId, ConnectionMonitor.heartbeatRetries.get(sessionId) + 1);
|
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);
|
}, ConnectionMonitor.connectionPingTimeout);
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
import { EventEmitter } from 'events';
|
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 { Logger, LoggerType } from 'common/Logger';
|
||||||
|
import { getComputerName, getAppName, getAppVersion, getPlayMessage } from 'src/Main';
|
||||||
import { WebSocket } from 'modules/ws';
|
import { WebSocket } from 'modules/ws';
|
||||||
import { v4 as uuidv4 } from 'modules/uuid';
|
import { v4 as uuidv4 } from 'modules/uuid';
|
||||||
const logger = new Logger('FCastSession', LoggerType.BACKEND);
|
const logger = new Logger('FCastSession', LoggerType.BACKEND);
|
||||||
|
@ -18,6 +19,7 @@ const MAXIMUM_PACKET_LENGTH = 32000;
|
||||||
|
|
||||||
export class FCastSession {
|
export class FCastSession {
|
||||||
public sessionId: string;
|
public sessionId: string;
|
||||||
|
public protocolVersion: number;
|
||||||
buffer: Buffer = Buffer.alloc(MAXIMUM_PACKET_LENGTH);
|
buffer: Buffer = Buffer.alloc(MAXIMUM_PACKET_LENGTH);
|
||||||
bytesRead = 0;
|
bytesRead = 0;
|
||||||
packetLength = 0;
|
packetLength = 0;
|
||||||
|
@ -26,14 +28,25 @@ export class FCastSession {
|
||||||
state: SessionState;
|
state: SessionState;
|
||||||
emitter = new EventEmitter();
|
emitter = new EventEmitter();
|
||||||
|
|
||||||
|
private sentInitialMessage: boolean;
|
||||||
|
|
||||||
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
|
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
|
||||||
this.sessionId = uuidv4();
|
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.socket = socket;
|
||||||
this.writer = writer;
|
this.writer = writer;
|
||||||
this.state = SessionState.WaitingForLength;
|
this.state = SessionState.WaitingForLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
send(opcode: number, message = null) {
|
send(opcode: number, message = null) {
|
||||||
|
if (!this.isSupportedOpcode(opcode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
message = this.stripUnsupportedFields(opcode, message);
|
||||||
const json = message ? JSON.stringify(message) : null;
|
const json = message ? JSON.stringify(message) : null;
|
||||||
logger.info(`send: (session: ${this.sessionId}, opcode: ${opcode}, body: ${json})`);
|
logger.info(`send: (session: ${this.sessionId}, opcode: ${opcode}, body: ${json})`);
|
||||||
|
|
||||||
|
@ -189,9 +202,23 @@ export class FCastSession {
|
||||||
case Opcode.SetSpeed:
|
case Opcode.SetSpeed:
|
||||||
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
|
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
|
||||||
break;
|
break;
|
||||||
case Opcode.Version:
|
case Opcode.Version: {
|
||||||
this.emitter.emit("version", JSON.parse(body) as VersionMessage);
|
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;
|
break;
|
||||||
|
}
|
||||||
case Opcode.Ping:
|
case Opcode.Ping:
|
||||||
this.send(Opcode.Pong);
|
this.send(Opcode.Pong);
|
||||||
this.emitter.emit("ping");
|
this.emitter.emit("ping");
|
||||||
|
@ -199,6 +226,18 @@ export class FCastSession {
|
||||||
case Opcode.Pong:
|
case Opcode.Pong:
|
||||||
this.emitter.emit("pong");
|
this.emitter.emit("pong");
|
||||||
break;
|
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) {
|
} catch (e) {
|
||||||
logger.warn(`Error handling packet from.`, 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("version", (body: VersionMessage) => { emitter.emit("version", body) });
|
||||||
this.emitter.on("ping", () => { emitter.emit("ping", { sessionId: this.sessionId }) });
|
this.emitter.on("ping", () => { emitter.emit("ping", { sessionId: this.sessionId }) });
|
||||||
this.emitter.on("pong", () => { emitter.emit("pong", { 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
146
receivers/common/web/ListenerService.ts
Normal file
146
receivers/common/web/ListenerService.ts
Normal file
|
@ -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<string, FCastSession> = new Map();
|
||||||
|
private eventSubscribers: Map<string, EventSubscribeObject[]> = 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<string>, keyUp: Set<string> } {
|
||||||
|
let keyDown = new Set<string>();
|
||||||
|
let keyUp = new Set<string>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Protocol Documentation: https://gitlab.futo.org/videostreaming/fcast/-/wikis/Protocol-version-3
|
||||||
|
export const PROTOCOL_VERSION = 3;
|
||||||
|
|
||||||
export enum Opcode {
|
export enum Opcode {
|
||||||
None = 0,
|
None = 0,
|
||||||
Play = 1,
|
Play = 1,
|
||||||
|
@ -12,33 +15,101 @@ export enum Opcode {
|
||||||
SetSpeed = 10,
|
SetSpeed = 10,
|
||||||
Version = 11,
|
Version = 11,
|
||||||
Ping = 12,
|
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 {
|
export class PlayMessage {
|
||||||
constructor(
|
constructor(
|
||||||
public container: string,
|
public container: string, // The MIME type (video/mp4)
|
||||||
public url: string = null,
|
public url: string = null, // The URL to load (optional)
|
||||||
public content: string = null,
|
public content: string = null, // The content to load (i.e. a DASH manifest, json content, optional)
|
||||||
public time: number = null,
|
public time: number = null, // The time to start playing in seconds
|
||||||
public speed: number = null,
|
public volume: number = null, // The desired volume (0-1)
|
||||||
public headers: { [key: string]: string } = null
|
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<string, string>
|
||||||
|
public metadata: MetadataObject = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SeekMessage {
|
export class SeekMessage {
|
||||||
constructor(
|
constructor(
|
||||||
public time: number,
|
public time: number, // The time to seek to in seconds
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PlaybackUpdateMessage {
|
export class PlaybackUpdateMessage {
|
||||||
constructor(
|
constructor(
|
||||||
public generationTime: number,
|
public generationTime: number, // The time the packet was generated (unix time milliseconds)
|
||||||
public time: number,
|
public state: number, // The playback state
|
||||||
public duration: number,
|
public time: number = null, // The current time playing in seconds
|
||||||
public state: number,
|
public duration: number = null, // The duration in seconds
|
||||||
public speed: number
|
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 {
|
export class SetSpeedMessage {
|
||||||
constructor(
|
constructor(
|
||||||
public speed: number,
|
public speed: number, // The factor to multiply playback speed by
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VersionMessage {
|
export class VersionMessage {
|
||||||
constructor(
|
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<string, string>
|
||||||
|
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,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
import * as net from 'net';
|
import * as net from 'net';
|
||||||
|
import { ListenerService } from 'common/ListenerService';
|
||||||
import { FCastSession } from 'common/FCastSession';
|
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 { Logger, LoggerType } from 'common/Logger';
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { errorHandler } from 'src/Main';
|
|
||||||
const logger = new Logger('TcpListenerService', LoggerType.BACKEND);
|
const logger = new Logger('TcpListenerService', LoggerType.BACKEND);
|
||||||
|
|
||||||
export class TcpListenerService {
|
export class TcpListenerService extends ListenerService {
|
||||||
public static PORT = 46899;
|
public readonly PORT = 46899;
|
||||||
emitter = new EventEmitter();
|
|
||||||
|
|
||||||
private server: net.Server;
|
private server: net.Server;
|
||||||
private sessionMap = new Map();
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.server != null) {
|
if (this.server != null) {
|
||||||
|
@ -19,7 +15,7 @@ export class TcpListenerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.server = net.createServer()
|
this.server = net.createServer()
|
||||||
.listen(TcpListenerService.PORT)
|
.listen(this.PORT)
|
||||||
.on("connection", this.handleConnection.bind(this))
|
.on("connection", this.handleConnection.bind(this))
|
||||||
.on("error", this.handleServerError.bind(this));
|
.on("error", this.handleServerError.bind(this));
|
||||||
}
|
}
|
||||||
|
@ -35,29 +31,6 @@ export class TcpListenerService {
|
||||||
server.close();
|
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) {
|
disconnect(sessionId: string) {
|
||||||
this.sessionMap.get(sessionId)?.socket.destroy();
|
this.sessionMap.get(sessionId)?.socket.destroy();
|
||||||
this.sessionMap.delete(sessionId);
|
this.sessionMap.delete(sessionId);
|
||||||
|
@ -69,14 +42,6 @@ export class TcpListenerService {
|
||||||
return senders;
|
return senders;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSessions(): string[] {
|
|
||||||
return [...this.sessionMap.keys()];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleServerError(err: NodeJS.ErrnoException) {
|
|
||||||
errorHandler(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleConnection(socket: net.Socket) {
|
private handleConnection(socket: net.Socket) {
|
||||||
logger.info(`New connection from ${socket.remoteAddress}:${socket.remotePort}`);
|
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 }});
|
this.emitter.emit('connect', { sessionId: session.sessionId, type: 'tcp', data: { address: socket.remoteAddress, port: socket.remotePort }});
|
||||||
try {
|
try {
|
||||||
logger.info('Sending version');
|
logger.info('Sending version');
|
||||||
session.send(Opcode.Version, {version: 2});
|
session.send(Opcode.Version, new VersionMessage(PROTOCOL_VERSION));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info('Failed to send version', e);
|
logger.info('Failed to send version', e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
|
import { ListenerService } from 'common/ListenerService';
|
||||||
import { FCastSession } from 'common/FCastSession';
|
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 { Logger, LoggerType } from 'common/Logger';
|
||||||
import { EventEmitter } from 'events';
|
|
||||||
import { WebSocket, WebSocketServer } from 'modules/ws';
|
import { WebSocket, WebSocketServer } from 'modules/ws';
|
||||||
import { errorHandler } from 'src/Main';
|
|
||||||
const logger = new Logger('WebSocketListenerService', LoggerType.BACKEND);
|
const logger = new Logger('WebSocketListenerService', LoggerType.BACKEND);
|
||||||
|
|
||||||
export class WebSocketListenerService {
|
export class WebSocketListenerService extends ListenerService {
|
||||||
public static PORT = 46898;
|
public readonly PORT = 46898;
|
||||||
|
|
||||||
emitter = new EventEmitter();
|
|
||||||
|
|
||||||
private server: WebSocketServer;
|
private server: WebSocketServer;
|
||||||
private sessionMap = new Map();
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
if (this.server != null) {
|
if (this.server != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.server = new WebSocketServer({ port: WebSocketListenerService.PORT })
|
this.server = new WebSocketServer({ port: this.PORT })
|
||||||
.on("connection", this.handleConnection.bind(this))
|
.on("connection", this.handleConnection.bind(this))
|
||||||
.on("error", this.handleServerError.bind(this));
|
.on("error", this.handleServerError.bind(this));
|
||||||
}
|
}
|
||||||
|
@ -35,39 +30,10 @@ export class WebSocketListenerService {
|
||||||
server.close();
|
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) {
|
disconnect(sessionId: string) {
|
||||||
this.sessionMap.get(sessionId)?.close();
|
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) {
|
private handleConnection(socket: WebSocket, request: any) {
|
||||||
logger.info('New WebSocket connection');
|
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 }});
|
this.emitter.emit('connect', { sessionId: session.sessionId, type: 'ws', data: { url: socket.url }});
|
||||||
try {
|
try {
|
||||||
logger.info('Sending version');
|
logger.info('Sending version');
|
||||||
session.send(Opcode.Version, {version: 2});
|
session.send(Opcode.Version, new VersionMessage(PROTOCOL_VERSION));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.info('Failed to send version');
|
logger.info('Failed to send version', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Logger, LoggerType } from 'common/Logger';
|
import { Logger, LoggerType } from 'common/Logger';
|
||||||
|
import { EventMessage } from 'common/Packets';
|
||||||
const logger = new Logger('MainWindow', LoggerType.FRONTEND);
|
const logger = new Logger('MainWindow', LoggerType.FRONTEND);
|
||||||
|
|
||||||
// Cannot directly pass the object to the renderer for some reason...
|
// Cannot directly pass the object to the renderer for some reason...
|
||||||
|
@ -23,6 +24,10 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
let preloadData: Record<string, any> = {};
|
let preloadData: Record<string, any> = {};
|
||||||
|
preloadData.subscribedKeys = {
|
||||||
|
keyDown: new Set<string>(),
|
||||||
|
keyUp: new Set<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (TARGET === 'electron') {
|
if (TARGET === 'electron') {
|
||||||
|
@ -32,13 +37,20 @@ if (TARGET === 'electron') {
|
||||||
electronAPI.ipcRenderer.on("device-info", (_event, value: any) => {
|
electronAPI.ipcRenderer.on("device-info", (_event, value: any) => {
|
||||||
preloadData.deviceInfo = value;
|
preloadData.deviceInfo = value;
|
||||||
})
|
})
|
||||||
|
electronAPI.ipcRenderer.on("event-subscribed-keys-update", (_event, value: { keyDown: Set<string>, keyUp: Set<string> }) => {
|
||||||
|
logger.info('MAIN WINDOW Updated key subscriptions', value);
|
||||||
|
preloadData.subscribedKeys.keyDown = value.keyDown;
|
||||||
|
preloadData.subscribedKeys.keyUp = value.keyUp;
|
||||||
|
})
|
||||||
|
|
||||||
electronAPI.contextBridge.exposeInMainWorld('targetAPI', {
|
electronAPI.contextBridge.exposeInMainWorld('targetAPI', {
|
||||||
onDeviceInfo: (callback: any) => electronAPI.ipcRenderer.on('device-info', callback),
|
onDeviceInfo: (callback: any) => electronAPI.ipcRenderer.on('device-info', callback),
|
||||||
getDeviceInfo: () => preloadData.deviceInfo,
|
getDeviceInfo: () => preloadData.deviceInfo,
|
||||||
getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'),
|
getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'),
|
||||||
|
getSubscribedKeys: () => preloadData.subscribedKeys,
|
||||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
||||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback),
|
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', callback),
|
||||||
|
emitEvent: (message: EventMessage) => electronAPI.ipcRenderer.send('emit-event', message),
|
||||||
logger: loggerInterface,
|
logger: loggerInterface,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import QRCode from 'modules/qrcode';
|
||||||
import * as connectionMonitor from '../ConnectionMonitor';
|
import * as connectionMonitor from '../ConnectionMonitor';
|
||||||
import { onQRCodeRendered } from 'src/main/Renderer';
|
import { onQRCodeRendered } from 'src/main/Renderer';
|
||||||
import { toast, ToastIcon } from '../components/Toast';
|
import { toast, ToastIcon } from '../components/Toast';
|
||||||
|
import { EventMessage, EventType, KeyEvent } from 'common/Packets';
|
||||||
|
|
||||||
const connectionStatusText = document.getElementById('connection-status-text');
|
const connectionStatusText = document.getElementById('connection-status-text');
|
||||||
const connectionStatusSpinner = document.getElementById('connection-spinner');
|
const connectionStatusSpinner = document.getElementById('connection-spinner');
|
||||||
|
@ -199,3 +200,14 @@ function renderQRCode(url: string) {
|
||||||
|
|
||||||
onQRCodeRendered();
|
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)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* 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';
|
import { Logger, LoggerType } from 'common/Logger';
|
||||||
const logger = new Logger('PlayerWindow', LoggerType.FRONTEND);
|
const logger = new Logger('PlayerWindow', LoggerType.FRONTEND);
|
||||||
|
|
||||||
|
@ -25,12 +25,22 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
let preloadData: Record<string, any> = {};
|
let preloadData: Record<string, any> = {};
|
||||||
|
preloadData.subscribedKeys = {
|
||||||
|
keyDown: new Set<string>(),
|
||||||
|
keyUp: new Set<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (TARGET === 'electron') {
|
if (TARGET === 'electron') {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const electronAPI = __non_webpack_require__('electron');
|
const electronAPI = __non_webpack_require__('electron');
|
||||||
|
|
||||||
|
electronAPI.ipcRenderer.on("event-subscribed-keys-update", (_event, value: { keyDown: Set<string>, keyUp: Set<string> }) => {
|
||||||
|
logger.info('PLAYER Updated key subscriptions', value);
|
||||||
|
preloadData.subscribedKeys.keyDown = value.keyDown;
|
||||||
|
preloadData.subscribedKeys.keyUp = value.keyUp;
|
||||||
|
})
|
||||||
|
|
||||||
electronAPI.contextBridge.exposeInMainWorld('targetAPI', {
|
electronAPI.contextBridge.exposeInMainWorld('targetAPI', {
|
||||||
sendPlaybackError: (error: PlaybackErrorMessage) => electronAPI.ipcRenderer.send('send-playback-error', error),
|
sendPlaybackError: (error: PlaybackErrorMessage) => electronAPI.ipcRenderer.send('send-playback-error', error),
|
||||||
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => electronAPI.ipcRenderer.send('send-playback-update', update),
|
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),
|
onSeek: (callback: any) => electronAPI.ipcRenderer.on("seek", callback),
|
||||||
onSetVolume: (callback: any) => electronAPI.ipcRenderer.on("setvolume", callback),
|
onSetVolume: (callback: any) => electronAPI.ipcRenderer.on("setvolume", callback),
|
||||||
onSetSpeed: (callback: any) => electronAPI.ipcRenderer.on("setspeed", callback),
|
onSetSpeed: (callback: any) => electronAPI.ipcRenderer.on("setspeed", callback),
|
||||||
getSessions: () => electronAPI.ipcRenderer.invoke('get-sessions'),
|
|
||||||
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
onConnect: (callback: any) => electronAPI.ipcRenderer.on('connect', callback),
|
||||||
onDisconnect: (callback: any) => electronAPI.ipcRenderer.on('disconnect', 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,
|
logger: loggerInterface,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import dashjs from 'modules/dashjs';
|
import dashjs from 'modules/dashjs';
|
||||||
import Hls, { LevelLoadedData } from 'modules/hls.js';
|
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 { Player, PlayerType } from './Player';
|
||||||
import * as connectionMonitor from '../ConnectionMonitor';
|
import * as connectionMonitor from '../ConnectionMonitor';
|
||||||
import { toast, ToastIcon } from '../components/Toast';
|
import { toast, ToastIcon } from '../components/Toast';
|
||||||
|
@ -35,7 +35,7 @@ function formatDuration(duration: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendPlaybackUpdate(updateState: 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) {
|
if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) {
|
||||||
lastPlayerUpdateGenerationTime = updateMessage.generationTime;
|
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.sendVolumeUpdate({ generationTime: Date.now(), volume: 1.0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.targetAPI.emitEvent(new EventMessage(Date.now(), new MediaItemEvent(EventType.MediaItemStart, cachedPlayMediaItem)));
|
||||||
player.play();
|
player.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,11 +121,18 @@ let isLive = false;
|
||||||
let isLivePosition = false;
|
let isLivePosition = false;
|
||||||
let captionsBaseHeight = 0;
|
let captionsBaseHeight = 0;
|
||||||
let captionsContentHeight = 0;
|
let captionsContentHeight = 0;
|
||||||
|
let cachedPlayMediaItem: MediaItem = null;
|
||||||
|
|
||||||
function onPlay(_event, value: PlayMessage) {
|
function onPlay(_event, value: PlayMessage) {
|
||||||
logger.info("Handle play message renderer", JSON.stringify(value));
|
logger.info("Handle play message renderer", JSON.stringify(value));
|
||||||
const currentVolume = player ? player.getVolume() : null;
|
const currentVolume = player ? player.getVolume() : null;
|
||||||
const currentPlaybackRate = player ? player.getPlaybackRate() : 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) {
|
||||||
if ((player.getSource() === value.url) || (player.getSource() === value.content)) {
|
if ((player.getSource() === value.url) || (player.getSource() === value.content)) {
|
||||||
|
@ -167,7 +175,10 @@ function onPlay(_event, value: PlayMessage) {
|
||||||
// Player event handlers
|
// Player event handlers
|
||||||
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); });
|
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_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, () => {
|
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => {
|
||||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||||
|
|
||||||
|
@ -290,7 +301,10 @@ function onPlay(_event, value: PlayMessage) {
|
||||||
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
|
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
|
||||||
videoElement.onplay = () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); };
|
videoElement.onplay = () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); };
|
||||||
videoElement.onpause = () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); };
|
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 = () => {
|
videoElement.ontimeupdate = () => {
|
||||||
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
|
||||||
|
|
||||||
|
@ -737,69 +751,82 @@ document.addEventListener('click', (event: MouseEvent) => {
|
||||||
const skipInterval = 10;
|
const skipInterval = 10;
|
||||||
const volumeIncrement = 0.1;
|
const volumeIncrement = 0.1;
|
||||||
|
|
||||||
function keyDownEventListener(event: any) {
|
function keyDownEventListener(event: KeyboardEvent) {
|
||||||
// logger.info("KeyDown", event);
|
// logger.info("KeyDown", event);
|
||||||
const handledCase = targetKeyDownEventListener(event);
|
let handledCase = targetKeyDownEventListener(event);
|
||||||
if (handledCase) {
|
|
||||||
return;
|
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) {
|
if (window.targetAPI.getSubscribedKeys().keyDown.has(event.key)) {
|
||||||
case 'KeyF':
|
window.targetAPI.emitEvent(new EventMessage(Date.now(), new KeyEvent(EventType.KeyDown, event.key, event.repeat, handledCase)));
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -814,6 +841,11 @@ function skipForward() {
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', keyDownEventListener);
|
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 {
|
export {
|
||||||
PlayerControlEvent,
|
PlayerControlEvent,
|
||||||
|
|
|
@ -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 { supportedImageTypes } from 'common/MimeTypes';
|
||||||
import * as connectionMonitor from '../ConnectionMonitor';
|
import * as connectionMonitor from '../ConnectionMonitor';
|
||||||
import { toast, ToastIcon } from '../components/Toast';
|
import { toast, ToastIcon } from '../components/Toast';
|
||||||
|
@ -11,6 +11,12 @@ const genericViewer = document.getElementById('viewer-generic') as HTMLIFrameEle
|
||||||
|
|
||||||
function onPlay(_event, value: PlayMessage) {
|
function onPlay(_event, value: PlayMessage) {
|
||||||
logger.info("Handle play message renderer", JSON.stringify(value));
|
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;
|
const src = value.url ? value.url : value.content;
|
||||||
|
|
||||||
if (src && value.container && supportedImageTypes.find(v => v === value.container.toLocaleLowerCase()) && imageViewer) {
|
if (src && value.container && supportedImageTypes.find(v => v === value.container.toLocaleLowerCase()) && imageViewer) {
|
||||||
|
@ -59,3 +65,14 @@ connectionMonitor.setUiUpdateCallbacks({
|
||||||
});
|
});
|
||||||
|
|
||||||
window.targetAPI.onPlay(onPlay);
|
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)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog, shell } from 'electron';
|
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 { supportedPlayerTypes } from 'common/MimeTypes';
|
||||||
import { DiscoveryService } from 'common/DiscoveryService';
|
import { DiscoveryService } from 'common/DiscoveryService';
|
||||||
import { TcpListenerService } from 'common/TcpListenerService';
|
import { TcpListenerService } from 'common/TcpListenerService';
|
||||||
|
@ -8,6 +8,8 @@ import { NetworkService } from 'common/NetworkService';
|
||||||
import { ConnectionMonitor } from 'common/ConnectionMonitor';
|
import { ConnectionMonitor } from 'common/ConnectionMonitor';
|
||||||
import { Logger, LoggerType } from 'common/Logger';
|
import { Logger, LoggerType } from 'common/Logger';
|
||||||
import { Updater } from './Updater';
|
import { Updater } from './Updater';
|
||||||
|
import * as http from 'http';
|
||||||
|
import * as https from 'https';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import yargs from 'yargs';
|
import yargs from 'yargs';
|
||||||
|
@ -15,6 +17,15 @@ import { hideBin } from 'yargs/helpers';
|
||||||
const cp = require('child_process');
|
const cp = require('child_process');
|
||||||
let logger = null;
|
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<string>();
|
||||||
|
}
|
||||||
|
|
||||||
export class Main {
|
export class Main {
|
||||||
static shouldOpenMainWindow = true;
|
static shouldOpenMainWindow = true;
|
||||||
static startFullscreen = false;
|
static startFullscreen = false;
|
||||||
|
@ -26,8 +37,8 @@ export class Main {
|
||||||
static discoveryService: DiscoveryService;
|
static discoveryService: DiscoveryService;
|
||||||
static connectionMonitor: ConnectionMonitor;
|
static connectionMonitor: ConnectionMonitor;
|
||||||
static tray: Tray;
|
static tray: Tray;
|
||||||
|
static cache: AppCache = new AppCache();
|
||||||
|
|
||||||
private static cachedInterfaces = null;
|
|
||||||
private static playerWindowContentViewer = null;
|
private static playerWindowContentViewer = null;
|
||||||
|
|
||||||
private static toggleMainWindow() {
|
private static toggleMainWindow() {
|
||||||
|
@ -156,7 +167,31 @@ export class Main {
|
||||||
|
|
||||||
const listeners = [Main.tcpListenerService, Main.webSocketListenerService];
|
const listeners = [Main.tcpListenerService, Main.webSocketListenerService];
|
||||||
listeners.forEach(l => {
|
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';
|
const contentViewer = supportedPlayerTypes.find(v => v === message.container.toLocaleLowerCase()) ? 'player' : 'viewer';
|
||||||
|
|
||||||
if (!Main.playerWindow) {
|
if (!Main.playerWindow) {
|
||||||
|
@ -224,6 +259,26 @@ export class Main {
|
||||||
l.emitter.on('pong', (message) => {
|
l.emitter.on('pong', (message) => {
|
||||||
ConnectionMonitor.onPingPong(message, l instanceof WebSocketListenerService);
|
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();
|
l.start();
|
||||||
|
|
||||||
ipcMain.on('send-playback-error', (event: IpcMainEvent, value: PlaybackErrorMessage) => {
|
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) => {
|
ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => {
|
||||||
l.send(Opcode.VolumeUpdate, value);
|
l.send(Opcode.VolumeUpdate, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on('emit-event', (event: IpcMainEvent, value: EventMessage) => {
|
||||||
|
l.send(Opcode.Event, value);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('send-download-request', async () => {
|
ipcMain.on('send-download-request', async () => {
|
||||||
|
@ -299,7 +358,7 @@ export class Main {
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
ipcMain.on('network-changed', (event: IpcMainEvent, value: 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 });
|
Main.mainWindow?.webContents?.send("device-info", { name: os.hostname(), interfaces: value });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -364,8 +423,8 @@ export class Main {
|
||||||
Main.mainWindow.show();
|
Main.mainWindow.show();
|
||||||
|
|
||||||
Main.mainWindow.on('ready-to-show', () => {
|
Main.mainWindow.on('ready-to-show', () => {
|
||||||
if (Main.cachedInterfaces) {
|
if (Main.cache.interfaces) {
|
||||||
Main.mainWindow?.webContents?.send("device-info", { name: os.hostname(), interfaces: Main.cachedInterfaces });
|
Main.mainWindow?.webContents?.send("device-info", { name: os.hostname(), interfaces: Main.cache.interfaces });
|
||||||
}
|
}
|
||||||
|
|
||||||
networkWorker.loadFile(path.join(__dirname, 'main/worker.html'));
|
networkWorker.loadFile(path.join(__dirname, 'main/worker.html'));
|
||||||
|
@ -375,6 +434,8 @@ export class Main {
|
||||||
static async main(app: Electron.App) {
|
static async main(app: Electron.App) {
|
||||||
try {
|
try {
|
||||||
Main.application = app;
|
Main.application = app;
|
||||||
|
Main.cache.appName = app.name;
|
||||||
|
Main.cache.appVersion = app.getVersion();
|
||||||
|
|
||||||
const argv = yargs(hideBin(process.argv))
|
const argv = yargs(hideBin(process.argv))
|
||||||
.version(app.getVersion())
|
.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) {
|
export async function errorHandler(error: Error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
logger.shutdown();
|
logger.shutdown();
|
||||||
|
@ -497,3 +570,27 @@ export async function errorHandler(error: Error) {
|
||||||
Main.application.exit(0);
|
Main.application.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export async function fetchJSON(url: string): Promise<any> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { app } from 'electron';
|
||||||
import { Store } from './Store';
|
import { Store } from './Store';
|
||||||
import sudo from 'sudo-prompt';
|
import sudo from 'sudo-prompt';
|
||||||
import { Logger, LoggerType } from 'common/Logger';
|
import { Logger, LoggerType } from 'common/Logger';
|
||||||
|
import { fetchJSON } from './Main';
|
||||||
|
|
||||||
const cp = require('child_process');
|
const cp = require('child_process');
|
||||||
const extract = require('extract-zip');
|
const extract = require('extract-zip');
|
||||||
|
@ -91,28 +92,6 @@ export class Updater {
|
||||||
Store.set('updater', updaterSettings);
|
Store.set('updater', updaterSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
private static async fetchJSON(url: string): Promise<any> {
|
|
||||||
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<void> {
|
private static async downloadFile(url: string, destination: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = fs.createWriteStream(destination);
|
const file = fs.createWriteStream(destination);
|
||||||
|
@ -345,7 +324,7 @@ export class Updater {
|
||||||
logger.info('Checking for updates...');
|
logger.info('Checking for updates...');
|
||||||
|
|
||||||
try {
|
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 localChannelVersion: number = Updater.localPackageJson.channelVersion ? Updater.localPackageJson.channelVersion : 0;
|
||||||
const currentChannelVersion: number = Updater.releasesJson.channelCurrentVersions[Updater.updateChannel] ? Updater.releasesJson.channelCurrentVersions[Updater.updateChannel] : 0;
|
const currentChannelVersion: number = Updater.releasesJson.channelCurrentVersions[Updater.updateChannel] ? Updater.releasesJson.channelCurrentVersions[Updater.updateChannel] : 0;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue