2023-06-20 08:45:01 +02:00
|
|
|
import net = require('net');
|
|
|
|
import { EventEmitter } from 'node:events';
|
2023-12-07 16:10:18 +01:00
|
|
|
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VolumeUpdateMessage } from './Packets';
|
2023-12-06 11:50:26 +01:00
|
|
|
import { WebSocket } from 'ws';
|
2023-06-20 08:45:01 +02:00
|
|
|
|
|
|
|
enum SessionState {
|
|
|
|
Idle = 0,
|
|
|
|
WaitingForLength,
|
|
|
|
WaitingForData,
|
|
|
|
Disconnected,
|
|
|
|
};
|
|
|
|
|
|
|
|
enum Opcode {
|
|
|
|
None = 0,
|
|
|
|
Play = 1,
|
|
|
|
Pause = 2,
|
|
|
|
Resume = 3,
|
|
|
|
Stop = 4,
|
|
|
|
Seek = 5,
|
|
|
|
PlaybackUpdate = 6,
|
|
|
|
VolumeUpdate = 7,
|
2023-12-07 16:10:18 +01:00
|
|
|
SetVolume = 8,
|
|
|
|
PlaybackError = 9,
|
|
|
|
SetSpeed = 10
|
2023-06-20 08:45:01 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const LENGTH_BYTES = 4;
|
|
|
|
const MAXIMUM_PACKET_LENGTH = 32000;
|
|
|
|
|
|
|
|
export class FCastSession {
|
|
|
|
buffer: Buffer = Buffer.alloc(MAXIMUM_PACKET_LENGTH);
|
|
|
|
bytesRead = 0;
|
|
|
|
packetLength = 0;
|
2023-12-06 11:50:26 +01:00
|
|
|
socket: net.Socket | WebSocket;
|
|
|
|
writer: (data: Buffer) => void;
|
2023-06-20 08:45:01 +02:00
|
|
|
state: SessionState;
|
|
|
|
emitter = new EventEmitter();
|
|
|
|
|
2023-12-06 11:50:26 +01:00
|
|
|
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
|
2023-06-20 08:45:01 +02:00
|
|
|
this.socket = socket;
|
2023-12-06 11:50:26 +01:00
|
|
|
this.writer = writer;
|
2023-06-20 08:45:01 +02:00
|
|
|
this.state = SessionState.WaitingForLength;
|
|
|
|
}
|
|
|
|
|
2023-12-07 16:10:18 +01:00
|
|
|
sendPlaybackError(value: PlaybackErrorMessage) {
|
|
|
|
this.send(Opcode.PlaybackError, value);
|
|
|
|
}
|
|
|
|
|
2023-06-20 08:45:01 +02:00
|
|
|
sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
|
|
|
this.send(Opcode.PlaybackUpdate, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
sendVolumeUpdate(value: VolumeUpdateMessage) {
|
|
|
|
this.send(Opcode.VolumeUpdate, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
private send(opcode: number, message = null) {
|
|
|
|
const json = message ? JSON.stringify(message) : null;
|
|
|
|
let data: Uint8Array;
|
|
|
|
if (json) {
|
|
|
|
const utf8Encode = new TextEncoder();
|
|
|
|
data = utf8Encode.encode(json);
|
|
|
|
} else {
|
|
|
|
data = new Uint8Array(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
const size = 1 + data.length;
|
|
|
|
const header = Buffer.alloc(4 + 1);
|
|
|
|
header.writeUint32LE(size, 0);
|
|
|
|
header[4] = opcode;
|
|
|
|
|
|
|
|
let packet: Buffer;
|
|
|
|
if (data.length > 0) {
|
|
|
|
packet = Buffer.concat([ header, data ]);
|
|
|
|
} else {
|
|
|
|
packet = header;
|
|
|
|
}
|
|
|
|
|
2023-12-06 11:50:26 +01:00
|
|
|
this.writer(packet);
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
if (this.socket instanceof WebSocket) {
|
|
|
|
this.socket.close();
|
|
|
|
} else if (this.socket instanceof net.Socket) {
|
|
|
|
this.socket.end();
|
|
|
|
}
|
2023-06-20 08:45:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
processBytes(receivedBytes: Buffer) {
|
|
|
|
//TODO: Multithreading?
|
|
|
|
|
|
|
|
if (receivedBytes.length == 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-12-06 11:50:26 +01:00
|
|
|
console.log(`${receivedBytes.length} bytes received`);
|
2023-06-20 08:45:01 +02:00
|
|
|
|
|
|
|
switch (this.state) {
|
|
|
|
case SessionState.WaitingForLength:
|
|
|
|
this.handleLengthBytes(receivedBytes);
|
|
|
|
break;
|
|
|
|
case SessionState.WaitingForData:
|
|
|
|
this.handlePacketBytes(receivedBytes);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.log(`Data received is unhandled in current session state ${this.state}.`);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handleLengthBytes(receivedBytes: Buffer) {
|
|
|
|
const bytesToRead = Math.min(LENGTH_BYTES, receivedBytes.length);
|
|
|
|
const bytesRemaining = receivedBytes.length - bytesToRead;
|
|
|
|
receivedBytes.copy(this.buffer, this.bytesRead, 0, bytesToRead);
|
|
|
|
this.bytesRead += bytesToRead;
|
|
|
|
|
|
|
|
console.log(`handleLengthBytes: Read ${bytesToRead} bytes from packet`);
|
|
|
|
|
|
|
|
if (this.bytesRead >= LENGTH_BYTES) {
|
|
|
|
this.state = SessionState.WaitingForData;
|
|
|
|
this.packetLength = this.buffer.readUInt32LE(0);
|
|
|
|
this.bytesRead = 0;
|
2023-12-06 11:50:26 +01:00
|
|
|
console.log(`Packet length header received from: ${this.packetLength}`);
|
2023-06-20 08:45:01 +02:00
|
|
|
|
|
|
|
if (this.packetLength > MAXIMUM_PACKET_LENGTH) {
|
2023-12-06 11:50:26 +01:00
|
|
|
throw new Error(`Maximum packet length is 32kB: ${this.packetLength}`);
|
2023-06-20 08:45:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (bytesRemaining > 0) {
|
2023-12-06 11:50:26 +01:00
|
|
|
console.log(`${bytesRemaining} remaining bytes pushed to handlePacketBytes`);
|
2023-06-20 08:45:01 +02:00
|
|
|
this.handlePacketBytes(receivedBytes.slice(bytesToRead));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handlePacketBytes(receivedBytes: Buffer) {
|
|
|
|
const bytesToRead = Math.min(this.packetLength, receivedBytes.length);
|
|
|
|
const bytesRemaining = receivedBytes.length - bytesToRead;
|
|
|
|
receivedBytes.copy(this.buffer, this.bytesRead, 0, bytesToRead);
|
|
|
|
this.bytesRead += bytesToRead;
|
|
|
|
|
|
|
|
console.log(`handlePacketBytes: Read ${bytesToRead} bytes from packet`);
|
|
|
|
|
|
|
|
if (this.bytesRead >= this.packetLength) {
|
2023-12-06 11:50:26 +01:00
|
|
|
console.log(`Packet finished receiving from of ${this.packetLength} bytes.`);
|
2023-06-20 08:45:01 +02:00
|
|
|
this.handlePacket();
|
|
|
|
|
|
|
|
this.state = SessionState.WaitingForLength;
|
|
|
|
this.packetLength = 0;
|
|
|
|
this.bytesRead = 0;
|
|
|
|
|
|
|
|
if (bytesRemaining > 0) {
|
2023-12-06 11:50:26 +01:00
|
|
|
console.log(`${bytesRemaining} remaining bytes pushed to handleLengthBytes`);
|
2023-06-20 08:45:01 +02:00
|
|
|
this.handleLengthBytes(receivedBytes.slice(bytesToRead));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private handlePacket() {
|
2023-12-06 11:50:26 +01:00
|
|
|
console.log(`Processing packet of ${this.bytesRead} bytes from`);
|
2023-06-20 08:45:01 +02:00
|
|
|
|
|
|
|
const opcode = this.buffer[0];
|
|
|
|
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
|
|
|
|
console.log('body', body);
|
|
|
|
|
|
|
|
try {
|
|
|
|
switch (opcode) {
|
|
|
|
case Opcode.Play:
|
|
|
|
this.emitter.emit("play", JSON.parse(body) as PlayMessage);
|
|
|
|
break;
|
|
|
|
case Opcode.Pause:
|
|
|
|
this.emitter.emit("pause");
|
|
|
|
break;
|
|
|
|
case Opcode.Resume:
|
|
|
|
this.emitter.emit("resume");
|
|
|
|
break;
|
|
|
|
case Opcode.Stop:
|
|
|
|
this.emitter.emit("stop");
|
|
|
|
break;
|
|
|
|
case Opcode.Seek:
|
|
|
|
this.emitter.emit("seek", JSON.parse(body) as SeekMessage);
|
|
|
|
break;
|
|
|
|
case Opcode.SetVolume:
|
|
|
|
this.emitter.emit("setvolume", JSON.parse(body) as SetVolumeMessage);
|
|
|
|
break;
|
2023-12-07 16:10:18 +01:00
|
|
|
case Opcode.SetSpeed:
|
|
|
|
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
|
|
|
|
break;
|
2023-06-20 08:45:01 +02:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2023-12-06 11:50:26 +01:00
|
|
|
console.warn(`Error handling packet from.`, e);
|
2023-06-20 08:45:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|