1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-07-24 10:56:59 +00:00

Implemented WebSocket for electron version.

This commit is contained in:
Koen 2023-12-06 11:50:26 +01:00
parent ad8f3985a3
commit 85530ca218
10 changed files with 310 additions and 66 deletions

View file

@ -1,6 +1,7 @@
import net = require('net');
import { EventEmitter } from 'node:events';
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
import { WebSocket } from 'ws';
enum SessionState {
Idle = 0,
@ -28,12 +29,14 @@ export class FCastSession {
buffer: Buffer = Buffer.alloc(MAXIMUM_PACKET_LENGTH);
bytesRead = 0;
packetLength = 0;
socket: net.Socket;
socket: net.Socket | WebSocket;
writer: (data: Buffer) => void;
state: SessionState;
emitter = new EventEmitter();
constructor(socket: net.Socket) {
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
this.socket = socket;
this.writer = writer;
this.state = SessionState.WaitingForLength;
}
@ -67,7 +70,15 @@ export class FCastSession {
packet = header;
}
this.socket.write(packet);
this.writer(packet);
}
close() {
if (this.socket instanceof WebSocket) {
this.socket.close();
} else if (this.socket instanceof net.Socket) {
this.socket.end();
}
}
processBytes(receivedBytes: Buffer) {
@ -77,7 +88,7 @@ export class FCastSession {
return;
}
console.log(`${receivedBytes.length} bytes received from ${this.socket.remoteAddress}:${this.socket.remotePort}`);
console.log(`${receivedBytes.length} bytes received`);
switch (this.state) {
case SessionState.WaitingForLength:
@ -104,17 +115,14 @@ export class FCastSession {
this.state = SessionState.WaitingForData;
this.packetLength = this.buffer.readUInt32LE(0);
this.bytesRead = 0;
console.log(`Packet length header received from ${this.socket.remoteAddress}:${this.socket.remotePort}: ${this.packetLength}`);
console.log(`Packet length header received from: ${this.packetLength}`);
if (this.packetLength > MAXIMUM_PACKET_LENGTH) {
console.log(`Maximum packet length is 32kB, killing socket ${this.socket.remoteAddress}:${this.socket.remotePort}: ${this.packetLength}`);
this.socket.end();
this.state = SessionState.Disconnected;
return;
throw new Error(`Maximum packet length is 32kB: ${this.packetLength}`);
}
if (bytesRemaining > 0) {
console.log(`${bytesRemaining} remaining bytes ${this.socket.remoteAddress}:${this.socket.remotePort} pushed to handlePacketBytes`);
console.log(`${bytesRemaining} remaining bytes pushed to handlePacketBytes`);
this.handlePacketBytes(receivedBytes.slice(bytesToRead));
}
}
@ -129,7 +137,7 @@ export class FCastSession {
console.log(`handlePacketBytes: Read ${bytesToRead} bytes from packet`);
if (this.bytesRead >= this.packetLength) {
console.log(`Packet finished receiving from ${this.socket.remoteAddress}:${this.socket.remotePort} of ${this.packetLength} bytes.`);
console.log(`Packet finished receiving from of ${this.packetLength} bytes.`);
this.handlePacket();
this.state = SessionState.WaitingForLength;
@ -137,14 +145,14 @@ export class FCastSession {
this.bytesRead = 0;
if (bytesRemaining > 0) {
console.log(`${bytesRemaining} remaining bytes ${this.socket.remoteAddress}:${this.socket.remotePort} pushed to handleLengthBytes`);
console.log(`${bytesRemaining} remaining bytes pushed to handleLengthBytes`);
this.handleLengthBytes(receivedBytes.slice(bytesToRead));
}
}
}
private handlePacket() {
console.log(`Processing packet of ${this.bytesRead} bytes from ${this.socket.remoteAddress}:${this.socket.remotePort}`);
console.log(`Processing packet of ${this.bytesRead} bytes from`);
const opcode = this.buffer[0];
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
@ -172,7 +180,7 @@ export class FCastSession {
break;
}
} catch (e) {
console.warn(`Error handling packet from ${this.socket.remoteAddress}:${this.socket.remotePort}.`, e);
console.warn(`Error handling packet from.`, e);
}
}
}

View file

@ -1,14 +1,16 @@
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog } from 'electron';
import path = require('path');
import { FCastService } from './FCastService';
import { TcpListenerService } from './TcpListenerService';
import { PlaybackUpdateMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
import { DiscoveryService } from './DiscoveryService';
import { Updater } from './Updater';
import { WebSocketListenerService } from './WebSocketListenerService';
export default class Main {
static mainWindow: Electron.BrowserWindow;
static application: Electron.App;
static service: FCastService;
static tcpListenerService: TcpListenerService;
static webSocketListenerService: WebSocketListenerService;
static discoveryService: DiscoveryService;
static tray: Tray;
@ -92,42 +94,55 @@ export default class Main {
Main.discoveryService = new DiscoveryService();
Main.discoveryService.start();
Main.service = new FCastService();
Main.service.emitter.on("play", (message) => {
if (Main.mainWindow == null) {
Main.mainWindow = new BrowserWindow({
fullscreen: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
Main.tcpListenerService = new TcpListenerService();
Main.webSocketListenerService = new WebSocketListenerService();
const listeners = [Main.tcpListenerService, Main.webSocketListenerService];
Main.mainWindow.setAlwaysOnTop(false, 'pop-up-menu');
Main.mainWindow.show();
Main.mainWindow.loadFile(path.join(__dirname, 'index.html'));
Main.mainWindow.on('ready-to-show', () => {
listeners.forEach(l => {
l.emitter.on("play", (message) => {
if (Main.mainWindow == null) {
Main.mainWindow = new BrowserWindow({
fullscreen: true,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
Main.mainWindow.setAlwaysOnTop(false, 'pop-up-menu');
Main.mainWindow.show();
Main.mainWindow.loadFile(path.join(__dirname, 'index.html'));
Main.mainWindow.on('ready-to-show', () => {
Main.mainWindow?.webContents?.send("play", message);
});
Main.mainWindow.on('closed', Main.onClose);
} else {
Main.mainWindow?.webContents?.send("play", message);
});
Main.mainWindow.on('closed', Main.onClose);
} else {
Main.mainWindow?.webContents?.send("play", message);
}
});
Main.service.emitter.on("pause", () => Main.mainWindow?.webContents?.send("pause"));
Main.service.emitter.on("resume", () => Main.mainWindow?.webContents?.send("resume"));
}
});
l.emitter.on("pause", () => Main.mainWindow?.webContents?.send("pause"));
l.emitter.on("resume", () => Main.mainWindow?.webContents?.send("resume"));
l.emitter.on("stop", () => {
Main.mainWindow.close();
Main.mainWindow = null;
});
l.emitter.on("seek", (message) => Main.mainWindow?.webContents?.send("seek", message));
l.emitter.on("setvolume", (message) => Main.mainWindow?.webContents?.send("setvolume", message));
l.start();
Main.service.emitter.on("stop", () => {
Main.mainWindow.close();
Main.mainWindow = null;
ipcMain.on('send-playback-update', (event: IpcMainEvent, value: PlaybackUpdateMessage) => {
l.sendPlaybackUpdate(value);
});
ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => {
l.sendVolumeUpdate(value);
});
});
Main.service.emitter.on("seek", (message) => Main.mainWindow?.webContents?.send("seek", message));
Main.service.emitter.on("setvolume", (message) => Main.mainWindow?.webContents?.send("setvolume", message));
Main.service.start();
ipcMain.on('toggle-full-screen', () => {
const window = Main.mainWindow;
if (!window) {
@ -145,14 +160,6 @@ export default class Main {
window.setFullScreen(false);
});
ipcMain.on('send-playback-update', (event: IpcMainEvent, value: PlaybackUpdateMessage) => {
Main.service.sendPlaybackUpdate(value);
});
ipcMain.on('send-volume-update', (event: IpcMainEvent, value: VolumeUpdateMessage) => {
Main.service.sendVolumeUpdate(value);
});
}
static main(app: Electron.App) {

View file

@ -15,13 +15,16 @@ export class SeekMessage {
export class PlaybackUpdateMessage {
constructor(
public generationTime: number,
public time: number,
public duration: number,
public state: number
) {}
}
export class VolumeUpdateMessage {
constructor(
public generationTime: number,
public volume: number
) {}
}

View file

@ -5,7 +5,7 @@ import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetVolumeMessage, Volu
import { dialog } from 'electron';
import Main from './Main';
export class FCastService {
export class TcpListenerService {
emitter = new EventEmitter();
private server: net.Server;
@ -41,7 +41,7 @@ export class FCastService {
session.sendPlaybackUpdate(value);
} catch (e) {
console.warn("Failed to send update.", e);
session.socket.end();
session.close();
}
});
}
@ -54,7 +54,7 @@ export class FCastService {
session.sendVolumeUpdate(value);
} catch (e) {
console.warn("Failed to send update.", e);
session.socket.end();
session.close();
}
});
}
@ -82,7 +82,7 @@ export class FCastService {
private handleConnection(socket: net.Socket) {
console.log(`new connection from ${socket.remoteAddress}:${socket.remotePort}`);
const session = new FCastSession(socket);
const session = new FCastSession(socket, (data) => socket.write(data));
session.emitter.on("play", (body: PlayMessage) => { this.emitter.emit("play", body) });
session.emitter.on("pause", () => { this.emitter.emit("pause") });
session.emitter.on("resume", () => { this.emitter.emit("resume") });

View file

@ -0,0 +1,121 @@
import net = require('net');
import { FCastSession } from './FCastSession';
import { EventEmitter } from 'node:events';
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
import { dialog } from 'electron';
import Main from './Main';
import { WebSocket, WebSocketServer } from 'ws';
export class WebSocketListenerService {
emitter = new EventEmitter();
private server: WebSocketServer;
private sessions: FCastSession[] = [];
start() {
if (this.server != null) {
return;
}
this.server = new WebSocketServer({ port: 46898 })
.on("connection", this.handleConnection.bind(this))
.on("error", this.handleServerError.bind(this));
}
stop() {
if (this.server == null) {
return;
}
const server = this.server;
this.server = null;
server.close();
}
sendPlaybackUpdate(value: PlaybackUpdateMessage) {
console.info("Sending playback update.", value);
this.sessions.forEach(session => {
try {
session.sendPlaybackUpdate(value);
} catch (e) {
console.warn("Failed to send update.", e);
session.close();
}
});
}
sendVolumeUpdate(value: VolumeUpdateMessage) {
console.info("Sending volume update.", value);
this.sessions.forEach(session => {
try {
session.sendVolumeUpdate(value);
} catch (e) {
console.warn("Failed to send update.", e);
session.close();
}
});
}
private async handleServerError(err: NodeJS.ErrnoException) {
console.error("Server error:", err);
const restartPrompt = await dialog.showMessageBox({
type: 'error',
title: 'Failed to start',
message: 'The application failed to start properly.',
buttons: ['Restart', 'Close'],
defaultId: 0,
cancelId: 1
});
if (restartPrompt.response === 0) {
Main.application.relaunch();
Main.application.exit(0);
} else {
Main.application.exit(0);
}
}
private handleConnection(socket: WebSocket) {
console.log('New WebSocket connection');
const session = new FCastSession(socket, (data) => socket.send(data));
session.emitter.on("play", (body: PlayMessage) => { this.emitter.emit("play", body) });
session.emitter.on("pause", () => { this.emitter.emit("pause") });
session.emitter.on("resume", () => { this.emitter.emit("resume") });
session.emitter.on("stop", () => { this.emitter.emit("stop") });
session.emitter.on("seek", (body: SeekMessage) => { this.emitter.emit("seek", body) });
session.emitter.on("setvolume", (body: SetVolumeMessage) => { this.emitter.emit("setvolume", body) });
this.sessions.push(session);
socket.on("error", (err) => {
console.warn(`Error.`, err);
session.close();
});
socket.on('message', data => {
try {
if (data instanceof Buffer) {
session.processBytes(data);
} else {
console.warn("Received unhandled string message", data);
}
} catch (e) {
console.warn(`Error while handling packet.`, e);
session.close();
}
});
socket.on("close", () => {
console.log('WebSocket connection closed');
const index = this.sessions.indexOf(session);
if (index != -1) {
this.sessions.splice(index, 1);
}
});
}
}

View file

@ -15,10 +15,31 @@ const player = videojs("video-player", options, function onPlayerReady() {
}
});
player.on("pause", () => { window.electronAPI.sendPlaybackUpdate({ time: Math.round(player.currentTime()), state: 2 }) });
player.on("play", () => { window.electronAPI.sendPlaybackUpdate({ time: Math.round(player.currentTime()), state: 1 }) });
player.on("seeked", () => { window.electronAPI.sendPlaybackUpdate({ time: Math.round(player.currentTime()), state: player.paused() ? 2 : 1 }) });
player.on("volumechange", () => { window.electronAPI.sendVolumeUpdate({ volume: player.volume() }); });
player.on("pause", () => { window.electronAPI.sendPlaybackUpdate({
generationTime: Date.now(),
time: Math.round(player.currentTime()),
duration: Math.round(player.duration()),
state: 2
})});
player.on("play", () => { window.electronAPI.sendPlaybackUpdate({
generationTime: Date.now(),
time: Math.round(player.currentTime()),
duration: Math.round(player.duration()),
state: 1
})});
player.on("seeked", () => { window.electronAPI.sendPlaybackUpdate({
generationTime: Date.now(),
time: Math.round(player.currentTime()),
duration: Math.round(player.duration()),
state: player.paused() ? 2 : 1 })
});
player.on("volumechange", () => { window.electronAPI.sendVolumeUpdate({
generationTime: Date.now(),
volume: player.volume()
})});
window.electronAPI.onPlay((_event, value) => {
console.log("Handle play message renderer", value);
@ -57,7 +78,12 @@ window.electronAPI.onSetVolume((_event, value) => {
});
setInterval(() => {
window.electronAPI.sendPlaybackUpdate({ time: Math.round(player.currentTime()), state: player.paused() ? 2 : 1 });
window.electronAPI.sendPlaybackUpdate({
generationTime: Date.now(),
time: Math.round(player.currentTime()),
duration: Math.round(player.duration()),
state: player.paused() ? 2 : 1
});
}, 1000);
let mouseTimer = null;