@tf2pickup-org/mumble-client
Version:
A simple bot for managing mumble servers
217 lines • 7.68 kB
JavaScript
import { Subject } from 'rxjs';
import { packetForType, packetType } from './packet-type-registry.js';
import { UDPTunnel } from '@tf2pickup-org/mumble-protocol';
import { readVarint } from './read-varint.js';
import { writeVarint } from './write-varint.js';
export var AudioCodec;
(function (AudioCodec) {
AudioCodec[AudioCodec["CELTAlpha"] = 0] = "CELTAlpha";
AudioCodec[AudioCodec["Ping"] = 1] = "Ping";
AudioCodec[AudioCodec["Speex"] = 2] = "Speex";
AudioCodec[AudioCodec["CELTBeta"] = 3] = "CELTBeta";
AudioCodec[AudioCodec["Opus"] = 4] = "Opus";
})(AudioCodec || (AudioCodec = {}));
export class MumbleSocket {
socket;
_packet = new Subject();
_audioPacket = new Subject();
_fullAudioPacket = new Subject();
buffers = [];
length = 0;
readers = [];
audioSequence = 0;
constructor(socket) {
this.socket = socket;
this.socket.on('data', (data) => {
this.receiveData(data);
});
this.readPrefix();
}
get packet() {
return this._packet.asObservable();
}
get audioPacket() {
return this._audioPacket.asObservable();
}
get fullAudioPacket() {
return this._fullAudioPacket.asObservable();
}
read(length, callback) {
this.readers.push({ length, callback });
if (this.readers.length === 1) {
this.flushReaders();
}
}
async send(message, payload) {
const typeNumber = packetType(message);
if (typeNumber === undefined) {
throw new Error(`unknown message type (${message.typeName})`);
}
const encoded = message.toBinary(payload);
const prefix = Buffer.alloc(6);
prefix.writeUint16BE(typeNumber, 0);
prefix.writeUint32BE(encoded.length, 2);
await this.write(Buffer.concat([prefix, encoded]));
}
write(buffer) {
return new Promise((resolve, reject) => {
if (this.socket.writable) {
this.socket.write(buffer, err => {
if (err) {
reject(err);
}
else {
resolve();
}
});
}
else {
reject(new Error('socket not writable'));
}
});
}
end() {
this.socket.end();
}
async sendAudio({ data, codec = AudioCodec.Opus, target = 0, isTerminator = false, }) {
const header = Buffer.alloc(1);
header[0] = ((codec & 0b111) << 5) | (target & 0b00011111);
const sequence = writeVarint(this.audioSequence++);
let frameHeader;
if (codec === AudioCodec.Opus) {
const frameHeaderValue = (data.length & 0x1fff) | (isTerminator ? 0x2000 : 0);
frameHeader = writeVarint(frameHeaderValue);
}
else {
const continuationBit = isTerminator ? 0 : 0x80;
frameHeader = Buffer.from([(data.length & 0x7f) | continuationBit]);
}
const packet = Buffer.concat([header, sequence, frameHeader, data]);
const typeNumber = packetType(UDPTunnel);
if (typeNumber === undefined) {
throw new Error('UDPTunnel packet type not found');
}
const prefix = Buffer.alloc(6);
prefix.writeUint16BE(typeNumber, 0);
prefix.writeUint32BE(packet.length, 2);
await this.write(Buffer.concat([prefix, packet]));
}
receiveData(data) {
this.buffers.push(data);
this.length += data.length;
this.flushReaders();
}
flushReaders() {
if (this.readers.length === 0) {
return;
}
const reader = this.readers[0];
if (this.length < reader.length) {
return;
}
const buffer = Buffer.alloc(reader.length);
let written = 0;
while (written < reader.length) {
const received = this.buffers[0];
const remaining = reader.length - written;
if (received.length <= remaining) {
received.copy(buffer, written);
written += received.length;
this.buffers.splice(0, 1);
this.length -= received.length;
}
else {
received.copy(buffer, written, 0, remaining);
written += remaining;
this.buffers[0] = received.subarray(remaining);
this.length -= remaining;
}
}
this.readers.splice(0, 1);
reader.callback(buffer);
}
readPrefix() {
this.read(6, prefix => {
const type = prefix.readUint16BE(0);
const length = prefix.readUint32BE(2);
this.readPacket(type, length);
});
}
readPacket(type, length) {
this.read(length, data => {
const message = packetForType(type);
if (message) {
switch (message.typeName) {
case UDPTunnel.typeName:
this.decodeAudio(data);
break;
default:
this._packet.next({
type,
typeName: message.typeName,
payload: message.fromBinary(data),
});
}
}
else {
console.error(`Unrecognized packet type (${type})`);
}
this.readPrefix();
});
}
decodeAudio(packet) {
if (packet.length < 1) {
return;
}
const header = packet[0];
const codec = (header >> 5) & 0b111;
const target = header & 0b00011111;
if (codec === AudioCodec.Ping) {
return;
}
let offset = 1;
const sessionResult = readVarint(packet.subarray(offset));
const source = sessionResult.value;
offset += sessionResult.length;
this._audioPacket.next({ source });
if (target !== 0) {
return;
}
const seqResult = readVarint(packet.subarray(offset));
const sequence = seqResult.value;
offset += seqResult.length;
let audioData;
let hasTerminator = false;
if (codec === AudioCodec.Opus) {
const frameHeaderResult = readVarint(packet.subarray(offset));
const frameHeader = frameHeaderResult.value;
offset += frameHeaderResult.length;
const audioLength = frameHeader & 0x1fff;
hasTerminator = (frameHeader & 0x2000) !== 0;
if (offset + audioLength > packet.length) {
return;
}
audioData = packet.subarray(offset, offset + audioLength);
}
else {
const frames = [];
while (offset < packet.length) {
const frameHeader = packet[offset];
offset++;
const frameLength = frameHeader & 0x7f;
const continuation = (frameHeader & 0x80) !== 0;
if (frameLength > 0 && offset + frameLength <= packet.length) {
frames.push(packet.subarray(offset, offset + frameLength));
offset += frameLength;
}
if (!continuation) {
hasTerminator = true;
break;
}
}
audioData = Buffer.concat(frames);
}
this._fullAudioPacket.next({ source, target, codec, sequence, audioData, hasTerminator });
}
}
//# sourceMappingURL=mumble-socket.js.map