Initial commit: mumh5, a modern Mumble client
Electron desktop app with a Svelte 5 interface for any Mumble server. - Mumble protocol core: TLS, handshake, channels, users, text, plugin data, client-side pacing of Murmur's rate limits - Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk, always-on, devices, per-user volume and local mute - Several servers at once, voice on one; server rail with icons and ordering - Chat: channels, direct messages, side chat, file sharing through f0ckm, inline images without it, click-to-play YouTube - Profiles with rich descriptions, registration, rename, nicknames, connection information and moderation menus - Identity wizard, multiple identities, PKCS#12 import/export, desktop Mumble certificate import, certificate pinning and viewer - Tray icon with voice state - Unit, server and end-to-end tests against a real Murmur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,482 @@
|
||||
import { Emitter } from './emitter.ts';
|
||||
import { FrameReader, frame, type Codec, type MessageName } from './proto.ts';
|
||||
import type { Transport } from './transport.ts';
|
||||
|
||||
export const CLIENT_RELEASE = 'mumh5 0.1.0';
|
||||
const VERSION = { major: 1, minor: 5, patch: 0 };
|
||||
const PING_INTERVAL_MS = 15000;
|
||||
|
||||
// Murmur drops these messages without telling the client when they come too fast
|
||||
// (leaky bucket, defaults messageburst=5 and messagelimit=1/s). Mirroring the bucket and
|
||||
// queueing keeps quick actions from getting lost. Slightly slower refill for safety.
|
||||
const RATE_BURST = 5;
|
||||
const RATE_REFILL_MS = 1100;
|
||||
const RATE_LIMITED = new Set<MessageName>(['TextMessage', 'ChannelState', 'ACL', 'Version']);
|
||||
|
||||
export interface Channel {
|
||||
id: number;
|
||||
parent: number | null;
|
||||
name: string;
|
||||
description: string;
|
||||
descriptionHash: Uint8Array | null;
|
||||
position: number;
|
||||
temporary: boolean;
|
||||
links: Set<number>;
|
||||
maxUsers: number;
|
||||
canEnter: boolean;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
session: number;
|
||||
name: string;
|
||||
userId: number | null;
|
||||
channelId: number;
|
||||
mute: boolean;
|
||||
deaf: boolean;
|
||||
suppress: boolean;
|
||||
selfMute: boolean;
|
||||
selfDeaf: boolean;
|
||||
prioritySpeaker: boolean;
|
||||
recording: boolean;
|
||||
comment: string;
|
||||
commentHash: Uint8Array | null;
|
||||
texture: Uint8Array | null;
|
||||
textureHash: Uint8Array | null;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export interface TextMessage {
|
||||
actor: number | null;
|
||||
sessions: number[];
|
||||
channels: number[];
|
||||
trees: number[];
|
||||
html: string;
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
allowHtml: boolean;
|
||||
messageLength: number;
|
||||
imageMessageLength: number;
|
||||
maxUsers: number;
|
||||
recordingAllowed: boolean;
|
||||
}
|
||||
|
||||
export interface Denial {
|
||||
type: number;
|
||||
permission: number;
|
||||
reason: string;
|
||||
channelId: number | null;
|
||||
session: number | null;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ACL permission bits, as used in PermissionDenied.permission
|
||||
export const PERMISSIONS: Record<number, string> = {
|
||||
0x1: 'Write ACL', 0x2: 'Traverse', 0x4: 'Enter', 0x8: 'Speak', 0x10: 'Mute/Deafen', 0x20: 'Move',
|
||||
0x40: 'Make channel', 0x80: 'Link channel', 0x100: 'Whisper', 0x200: 'Text message',
|
||||
0x400: 'Make temporary channel', 0x800: 'Listen', 0x10000: 'Kick', 0x20000: 'Ban',
|
||||
0x40000: 'Register', 0x80000: 'Register self', 0x100000: 'Reset user content'
|
||||
};
|
||||
|
||||
// Human-readable text for a PermissionDenied message
|
||||
export function describeDenial(d: Denial, channelName?: string): string {
|
||||
switch (d.type) {
|
||||
case 0: return d.reason || 'Denied by the server';
|
||||
case 1: {
|
||||
const what = PERMISSIONS[d.permission] ?? 'this action';
|
||||
return `You do not have permission for ${what}${channelName ? ` in ${channelName}` : ''}`;
|
||||
}
|
||||
case 2: return 'The SuperUser cannot be modified';
|
||||
case 3: return 'Invalid channel name';
|
||||
case 4: return 'Message too long for this server';
|
||||
case 6: return 'Not allowed in a temporary channel';
|
||||
case 7: return 'This needs a registered certificate';
|
||||
case 8: return `Invalid user name${d.name ? `: ${d.name}` : ''}`;
|
||||
case 9: return 'The channel is full';
|
||||
case 10: return 'Channels are nested too deeply';
|
||||
case 11: return 'The server has reached its channel limit';
|
||||
default: return d.reason || 'Denied by the server';
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConnectOptions {
|
||||
username: string;
|
||||
password?: string;
|
||||
tokens?: string[];
|
||||
os?: string;
|
||||
osVersion?: string;
|
||||
}
|
||||
|
||||
type ClientEvents = {
|
||||
synced: () => void;
|
||||
channel: (ch: Channel) => void;
|
||||
channelRemove: (id: number) => void;
|
||||
user: (user: User, changed: string[], actor: number | null, isNew: boolean) => void;
|
||||
userRemove: (user: User, actor: number | null, reason: string, ban: boolean) => void;
|
||||
text: (msg: TextMessage) => void;
|
||||
pluginData: (sender: number, dataId: string, data: Uint8Array) => void;
|
||||
permissionDenied: (msg: Denial) => void;
|
||||
reject: (type: number, reason: string) => void;
|
||||
voice: (packet: Uint8Array) => void;
|
||||
userStats: (stats: any) => void;
|
||||
ping: (rttMs: number) => void;
|
||||
close: (reason: string) => void;
|
||||
message: (name: MessageName, msg: any) => void;
|
||||
};
|
||||
|
||||
export class MumbleClient extends Emitter<ClientEvents> {
|
||||
readonly channels = new Map<number, Channel>();
|
||||
readonly users = new Map<number, User>();
|
||||
session: number | null = null;
|
||||
synced = false;
|
||||
welcomeText = '';
|
||||
maxBandwidth = 0;
|
||||
serverVersion = '';
|
||||
// Numeric server version (major << 16 | minor << 8 | patch), 0 until known
|
||||
serverVersionNum = 0;
|
||||
rtt = 0;
|
||||
config: ServerConfig = { allowHtml: true, messageLength: 5000, imageMessageLength: 131072, maxUsers: 0, recordingAllowed: true };
|
||||
|
||||
private transport: Transport | null = null;
|
||||
private reader = new FrameReader();
|
||||
private pingTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private closed = false;
|
||||
private tokens = RATE_BURST;
|
||||
private lastRefill = Date.now();
|
||||
private queue: Uint8Array[] = [];
|
||||
private queueTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
private codec: Codec;
|
||||
|
||||
constructor(codec: Codec) {
|
||||
super();
|
||||
this.codec = codec;
|
||||
}
|
||||
|
||||
get self(): User | null {
|
||||
return this.session == null ? null : this.users.get(this.session) ?? null;
|
||||
}
|
||||
|
||||
connect(transport: Transport, opts: ConnectOptions): void {
|
||||
this.transport = transport;
|
||||
transport.onData = chunk => {
|
||||
try {
|
||||
this.reader.push(chunk, (id, body) => this.handleFrame(id, body));
|
||||
} catch (e) {
|
||||
this.disconnect(`Protocol error: ${(e as Error).message}`);
|
||||
}
|
||||
};
|
||||
transport.onClose = reason => this.handleClose(reason);
|
||||
|
||||
const { major, minor, patch } = VERSION;
|
||||
this.send('Version', {
|
||||
version_v1: (major << 16) | (minor << 8) | patch,
|
||||
version_v2: major * 2 ** 48 + minor * 2 ** 32 + patch * 2 ** 16,
|
||||
release: CLIENT_RELEASE,
|
||||
os: opts.os ?? 'unknown',
|
||||
os_version: opts.osVersion ?? ''
|
||||
});
|
||||
this.send('Authenticate', {
|
||||
username: opts.username,
|
||||
password: opts.password ?? '',
|
||||
tokens: opts.tokens ?? [],
|
||||
opus: true,
|
||||
client_type: 0
|
||||
});
|
||||
this.pingTimer = setInterval(() => this.ping(), PING_INTERVAL_MS);
|
||||
}
|
||||
|
||||
disconnect(reason = 'Disconnected'): void {
|
||||
this.transport?.close();
|
||||
this.handleClose(reason);
|
||||
}
|
||||
|
||||
send(name: MessageName, payload: Record<string, unknown>): void {
|
||||
if (!this.transport || this.closed) return;
|
||||
const bytes = this.codec.encode(name, payload);
|
||||
// Only changes to our own user state count against the bucket
|
||||
const ownState = name === 'UserState' && (payload.session == null || payload.session === this.session);
|
||||
if (RATE_LIMITED.has(name) || ownState) this.sendPaced(bytes);
|
||||
else this.transport.send(bytes);
|
||||
}
|
||||
|
||||
// Messages still waiting for the rate limit
|
||||
get queued(): number {
|
||||
return this.queue.length;
|
||||
}
|
||||
|
||||
private refill(): void {
|
||||
const now = Date.now();
|
||||
const gained = Math.floor((now - this.lastRefill) / RATE_REFILL_MS);
|
||||
if (gained > 0) {
|
||||
this.tokens = Math.min(RATE_BURST, this.tokens + gained);
|
||||
this.lastRefill += gained * RATE_REFILL_MS;
|
||||
}
|
||||
if (this.tokens === RATE_BURST) this.lastRefill = now;
|
||||
}
|
||||
|
||||
private sendPaced(bytes: Uint8Array): void {
|
||||
this.queue.push(bytes);
|
||||
this.drain();
|
||||
}
|
||||
|
||||
private drain(): void {
|
||||
if (this.queueTimer || this.closed) return;
|
||||
this.refill();
|
||||
while (this.queue.length && this.tokens > 0) {
|
||||
this.tokens--;
|
||||
this.transport?.send(this.queue.shift()!);
|
||||
}
|
||||
if (this.queue.length) {
|
||||
this.queueTimer = setTimeout(() => { this.queueTimer = null; this.drain(); }, RATE_REFILL_MS - (Date.now() - this.lastRefill) + 5);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Actions ───────────────────────────────────────────────────────────────
|
||||
|
||||
sendText(target: { channels?: number[]; users?: number[]; trees?: number[] }, html: string): void {
|
||||
this.send('TextMessage', {
|
||||
session: target.users ?? [],
|
||||
channel_id: target.channels ?? [],
|
||||
tree_id: target.trees ?? [],
|
||||
message: html
|
||||
});
|
||||
}
|
||||
|
||||
joinChannel(channelId: number): void {
|
||||
if (this.session == null) return;
|
||||
this.send('UserState', { session: this.session, channel_id: channelId });
|
||||
}
|
||||
|
||||
setSelfMute(mute: boolean): void {
|
||||
// Unmuting also undeafens, matching the desktop client
|
||||
this.send('UserState', mute ? { self_mute: true } : { self_mute: false, self_deaf: false });
|
||||
}
|
||||
|
||||
setSelfDeaf(deaf: boolean): void {
|
||||
// Deafening implies muting
|
||||
this.send('UserState', deaf ? { self_mute: true, self_deaf: true } : { self_deaf: false });
|
||||
}
|
||||
|
||||
setComment(comment: string): void {
|
||||
if (this.session == null) return;
|
||||
this.send('UserState', { session: this.session, comment });
|
||||
}
|
||||
|
||||
kick(session: number, reason: string, ban = false): void {
|
||||
this.send('UserRemove', { session, reason, ban });
|
||||
}
|
||||
|
||||
// Server-side (admin) state of another user
|
||||
setUserState(session: number, state: { mute?: boolean; deaf?: boolean; priority_speaker?: boolean; channel_id?: number }): void {
|
||||
this.send('UserState', { session, ...state });
|
||||
}
|
||||
|
||||
// Connection details of a user; the server decides how much we may see
|
||||
requestUserStats(session: number, statsOnly = false): void {
|
||||
this.send('UserStats', { session, stats_only: statsOnly });
|
||||
}
|
||||
|
||||
// Fetch comments/descriptions/textures that the server only announced by hash.
|
||||
requestBlob(req: { textures?: number[]; comments?: number[]; descriptions?: number[] }): void {
|
||||
this.send('RequestBlob', {
|
||||
session_texture: req.textures ?? [],
|
||||
session_comment: req.comments ?? [],
|
||||
channel_description: req.descriptions ?? []
|
||||
});
|
||||
}
|
||||
|
||||
sendPluginData(receivers: number[], dataId: string, data: Uint8Array): void {
|
||||
if (this.session == null || !receivers.length) return;
|
||||
this.send('PluginDataTransmission', {
|
||||
senderSession: this.session,
|
||||
receiverSessions: receivers,
|
||||
data,
|
||||
dataID: dataId
|
||||
});
|
||||
}
|
||||
|
||||
// Voice over TCP: the UDPTunnel body is the raw voice packet, not a protobuf message
|
||||
sendVoiceTunnel(packet: Uint8Array): void {
|
||||
if (!this.transport || this.closed) return;
|
||||
this.transport.send(frame(1, packet));
|
||||
}
|
||||
|
||||
// 1.5+ servers talk to 1.5+ clients in the protobuf voice format
|
||||
get protobufVoice(): boolean {
|
||||
return this.serverVersionNum >= 0x010500;
|
||||
}
|
||||
|
||||
// Registers a user (ourselves or, with the Register permission, someone else) on the server
|
||||
register(session: number): void {
|
||||
this.send('UserState', { session, user_id: 0 });
|
||||
}
|
||||
|
||||
// ─── Incoming ──────────────────────────────────────────────────────────────
|
||||
|
||||
private ping(): void {
|
||||
this.send('Ping', { timestamp: Date.now() });
|
||||
}
|
||||
|
||||
private handleClose(reason: string): void {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
if (this.pingTimer) clearInterval(this.pingTimer);
|
||||
if (this.queueTimer) clearTimeout(this.queueTimer);
|
||||
this.pingTimer = null;
|
||||
this.queueTimer = null;
|
||||
this.queue = [];
|
||||
this.emit('close', reason);
|
||||
}
|
||||
|
||||
private handleFrame(typeId: number, body: Uint8Array): void {
|
||||
// UDPTunnel carries a raw voice packet, not a protobuf message
|
||||
if (typeId === 1) {
|
||||
this.emit('voice', body.slice());
|
||||
return;
|
||||
}
|
||||
const decoded = this.codec.decode(typeId, body);
|
||||
if (!decoded) return;
|
||||
const { name, msg } = decoded;
|
||||
|
||||
switch (name) {
|
||||
case 'Version':
|
||||
this.serverVersion = msg.release ?? '';
|
||||
if (msg.version_v2) {
|
||||
const v = Number(msg.version_v2);
|
||||
this.serverVersionNum = (Math.floor(v / 2 ** 48) << 16) | ((Math.floor(v / 2 ** 32) & 0xffff) << 8) | (Math.floor(v / 2 ** 16) & 0xffff);
|
||||
} else if (msg.version_v1) {
|
||||
this.serverVersionNum = msg.version_v1;
|
||||
}
|
||||
break;
|
||||
case 'Ping':
|
||||
if (msg.timestamp) {
|
||||
this.rtt = Date.now() - Number(msg.timestamp);
|
||||
this.emit('ping', this.rtt);
|
||||
}
|
||||
break;
|
||||
case 'Reject':
|
||||
this.emit('reject', msg.type ?? 0, msg.reason ?? 'Rejected');
|
||||
this.disconnect(msg.reason || 'Connection rejected');
|
||||
break;
|
||||
case 'ServerSync':
|
||||
this.session = msg.session;
|
||||
this.maxBandwidth = msg.max_bandwidth ?? 0;
|
||||
this.welcomeText = msg.welcome_text ?? '';
|
||||
this.synced = true;
|
||||
this.ping();
|
||||
this.emit('synced');
|
||||
break;
|
||||
case 'ServerConfig':
|
||||
if (msg.allow_html != null) this.config.allowHtml = msg.allow_html;
|
||||
if (msg.message_length != null) this.config.messageLength = msg.message_length;
|
||||
if (msg.image_message_length != null) this.config.imageMessageLength = msg.image_message_length;
|
||||
if (msg.max_users != null) this.config.maxUsers = msg.max_users;
|
||||
if (msg.recording_allowed != null) this.config.recordingAllowed = msg.recording_allowed;
|
||||
break;
|
||||
case 'ChannelState':
|
||||
this.applyChannelState(msg);
|
||||
break;
|
||||
case 'ChannelRemove':
|
||||
this.channels.delete(msg.channel_id);
|
||||
this.emit('channelRemove', msg.channel_id);
|
||||
break;
|
||||
case 'UserState':
|
||||
this.applyUserState(msg);
|
||||
break;
|
||||
case 'UserRemove': {
|
||||
const user = this.users.get(msg.session);
|
||||
if (user) {
|
||||
this.users.delete(msg.session);
|
||||
this.emit('userRemove', user, msg.actor ?? null, msg.reason ?? '', !!msg.ban);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'TextMessage':
|
||||
this.emit('text', {
|
||||
actor: msg.actor ?? null,
|
||||
sessions: msg.session ?? [],
|
||||
channels: msg.channel_id ?? [],
|
||||
trees: msg.tree_id ?? [],
|
||||
html: msg.message ?? '',
|
||||
time: Date.now()
|
||||
});
|
||||
break;
|
||||
case 'PermissionDenied':
|
||||
this.emit('permissionDenied', {
|
||||
type: msg.type ?? 0,
|
||||
permission: msg.permission ?? 0,
|
||||
reason: msg.reason ?? '',
|
||||
channelId: msg.channel_id ?? null,
|
||||
session: msg.session ?? null,
|
||||
name: msg.name ?? ''
|
||||
});
|
||||
break;
|
||||
case 'UserStats':
|
||||
this.emit('userStats', msg);
|
||||
break;
|
||||
case 'PluginDataTransmission':
|
||||
if (msg.senderSession != null && msg.dataID) {
|
||||
this.emit('pluginData', msg.senderSession, msg.dataID, msg.data ?? new Uint8Array(0));
|
||||
}
|
||||
break;
|
||||
}
|
||||
this.emit('message', name, msg);
|
||||
}
|
||||
|
||||
private applyChannelState(msg: any): void {
|
||||
const id: number = msg.channel_id;
|
||||
let ch = this.channels.get(id);
|
||||
if (!ch) {
|
||||
ch = {
|
||||
id, parent: null, name: '', description: '', descriptionHash: null, position: 0,
|
||||
temporary: false, links: new Set(), maxUsers: 0, canEnter: true
|
||||
};
|
||||
this.channels.set(id, ch);
|
||||
}
|
||||
if (msg.parent != null) ch.parent = msg.parent;
|
||||
if (msg.name != null) ch.name = msg.name;
|
||||
if (msg.description_hash != null && msg.description == null) ch.description = '';
|
||||
if (msg.description != null) ch.description = msg.description;
|
||||
if (msg.description_hash != null) ch.descriptionHash = msg.description_hash;
|
||||
if (msg.position != null) ch.position = msg.position;
|
||||
if (msg.temporary != null) ch.temporary = msg.temporary;
|
||||
if (msg.max_users != null) ch.maxUsers = msg.max_users;
|
||||
if (msg.can_enter != null) ch.canEnter = msg.can_enter;
|
||||
if (msg.links?.length) ch.links = new Set(msg.links);
|
||||
for (const l of msg.links_add ?? []) ch.links.add(l);
|
||||
for (const l of msg.links_remove ?? []) ch.links.delete(l);
|
||||
this.emit('channel', ch);
|
||||
}
|
||||
|
||||
private applyUserState(msg: any): void {
|
||||
const session: number = msg.session ?? this.session;
|
||||
let user = this.users.get(session);
|
||||
const isNew = !user;
|
||||
if (!user) {
|
||||
user = {
|
||||
session, name: '', userId: null, channelId: 0, mute: false, deaf: false, suppress: false,
|
||||
selfMute: false, selfDeaf: false, prioritySpeaker: false, recording: false,
|
||||
comment: '', commentHash: null, texture: null, textureHash: null, hash: ''
|
||||
};
|
||||
this.users.set(session, user);
|
||||
}
|
||||
const fields: [string, keyof User][] = [
|
||||
['name', 'name'], ['user_id', 'userId'], ['channel_id', 'channelId'], ['mute', 'mute'],
|
||||
['deaf', 'deaf'], ['suppress', 'suppress'], ['self_mute', 'selfMute'], ['self_deaf', 'selfDeaf'],
|
||||
['priority_speaker', 'prioritySpeaker'], ['recording', 'recording'], ['comment', 'comment'],
|
||||
['comment_hash', 'commentHash'], ['texture', 'texture'], ['texture_hash', 'textureHash'], ['hash', 'hash']
|
||||
];
|
||||
// A new hash without text means the text changed and must be fetched (RequestBlob)
|
||||
if (msg.comment_hash != null && msg.comment == null) user.comment = '';
|
||||
const changed: string[] = [];
|
||||
for (const [src, dst] of fields) {
|
||||
if (msg[src] != null) {
|
||||
(user as any)[dst] = msg[src];
|
||||
changed.push(dst);
|
||||
}
|
||||
}
|
||||
this.emit('user', user, changed, msg.actor ?? null, isNew);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Minimal typed event emitter that works in both browser and Node.
|
||||
export class Emitter<Events extends Record<string, (...args: any[]) => void>> {
|
||||
private handlers = new Map<keyof Events, Set<Function>>();
|
||||
|
||||
on<K extends keyof Events>(event: K, fn: Events[K]): () => void {
|
||||
let set = this.handlers.get(event);
|
||||
if (!set) this.handlers.set(event, (set = new Set()));
|
||||
set.add(fn);
|
||||
return () => set!.delete(fn);
|
||||
}
|
||||
|
||||
emit<K extends keyof Events>(event: K, ...args: Parameters<Events[K]>): void {
|
||||
for (const fn of this.handlers.get(event) ?? []) fn(...args);
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
// Types for the generated static module; the codec in proto.ts adds the real shape.
|
||||
export const MumbleProto: Record<string, unknown>;
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+1
@@ -0,0 +1 @@
|
||||
export const MumbleUDP: Record<string, any>;
|
||||
@@ -0,0 +1,639 @@
|
||||
/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-mixed-operators, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars, default-case, jsdoc/require-param*/
|
||||
import $protobuf from "protobufjs/minimal.js";
|
||||
|
||||
const $Reader = $protobuf.Reader, $Writer = $protobuf.Writer, $util = $protobuf.util;
|
||||
const $Object = $util.global.Object, $undefined = $util.global.undefined, $Error = $util.global.Error, $RangeError = $util.global.RangeError, $Array = $util.global.Array, $TypeError = $util.global.TypeError, $Number = $util.global.Number, $parseInt = $util.global.parseInt, $Boolean = $util.global.Boolean, $String = $util.global.String, $BigInt = $util.global.BigInt, $isFinite = $util.global.isFinite;
|
||||
|
||||
const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});
|
||||
|
||||
export const MumbleUDP = $root.MumbleUDP = (() => {
|
||||
|
||||
const MumbleUDP = {};
|
||||
|
||||
MumbleUDP.Audio = (function() {
|
||||
|
||||
const Audio = function (properties) {
|
||||
this.positional_data = [];
|
||||
if (properties)
|
||||
for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i)
|
||||
if (properties[keys[i]] != null && keys[i] !== "__proto__")
|
||||
this[keys[i]] = properties[keys[i]];
|
||||
};
|
||||
|
||||
Audio.prototype.target = null;
|
||||
Audio.prototype.context = null;
|
||||
Audio.prototype.sender_session = 0;
|
||||
Audio.prototype.frame_number = $util.Long ? $util.Long.fromBits(0,0,true) : 0;
|
||||
Audio.prototype.opus_data = $util.newBuffer([]);
|
||||
Audio.prototype.positional_data = $util.emptyArray;
|
||||
Audio.prototype.volume_adjustment = 0;
|
||||
Audio.prototype.is_terminator = false;
|
||||
|
||||
let $oneOfFields;
|
||||
|
||||
$Object.defineProperty(Audio.prototype, "Header", {
|
||||
get: $util.oneOfGetter($oneOfFields = ["target", "context"]),
|
||||
set: $util.oneOfSetter($oneOfFields)
|
||||
});
|
||||
|
||||
Audio.create = function(properties) {
|
||||
return new Audio(properties);
|
||||
};
|
||||
|
||||
Audio.encode = function (message, writer, _depth) {
|
||||
if (!writer)
|
||||
writer = $Writer.create();
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
if (message.target != null && $Object.hasOwnProperty.call(message, "target"))
|
||||
writer.uint32(8).uint32(message.target);
|
||||
if (message.context != null && $Object.hasOwnProperty.call(message, "context"))
|
||||
writer.uint32(16).uint32(message.context);
|
||||
if (message.sender_session != null && $Object.hasOwnProperty.call(message, "sender_session") && message.sender_session !== 0)
|
||||
writer.uint32(24).uint32(message.sender_session);
|
||||
if (message.frame_number != null && $Object.hasOwnProperty.call(message, "frame_number") && (typeof message.frame_number === "object" ? message.frame_number.low || message.frame_number.high : message.frame_number !== 0))
|
||||
writer.uint32(32).uint64(message.frame_number);
|
||||
if (message.opus_data != null && $Object.hasOwnProperty.call(message, "opus_data") && message.opus_data.length)
|
||||
writer.uint32(42).bytes(message.opus_data);
|
||||
if (message.positional_data != null && message.positional_data.length)
|
||||
writer.uint32(50).floats(message.positional_data);
|
||||
if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment") && !$Object.is(message.volume_adjustment, 0))
|
||||
writer.uint32(61).float(message.volume_adjustment);
|
||||
if (message.is_terminator != null && $Object.hasOwnProperty.call(message, "is_terminator") && message.is_terminator !== false)
|
||||
writer.uint32(128).bool(message.is_terminator);
|
||||
if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns"))
|
||||
for (let i = 0; i < message.$unknowns.length; ++i)
|
||||
writer.raw(message.$unknowns[i]);
|
||||
return writer;
|
||||
};
|
||||
|
||||
Audio.decode = function (reader, length, _end, _depth, _target) {
|
||||
if (!(reader instanceof $Reader))
|
||||
reader = $Reader.create(reader);
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $Reader.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
let end, message, value;
|
||||
if (length === $undefined)
|
||||
end = reader.len;
|
||||
else {
|
||||
end = reader.pos + length;
|
||||
if (end > reader.len)
|
||||
throw $RangeError("index out of range");
|
||||
length = reader.len;
|
||||
reader.len = end;
|
||||
}
|
||||
message = _target || new $root.MumbleUDP.Audio();
|
||||
while (reader.pos < end) {
|
||||
let start = reader.pos;
|
||||
let tag = reader.tag();
|
||||
if (tag === _end) {
|
||||
_end = $undefined;
|
||||
break;
|
||||
}
|
||||
let wireType = tag & 7;
|
||||
switch (tag >>>= 3) {
|
||||
case 1: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
message.target = reader.uint32();
|
||||
message.Header = "target";
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
message.context = reader.uint32();
|
||||
message.Header = "context";
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (value = reader.uint32())
|
||||
message.sender_session = value;
|
||||
else
|
||||
delete message.sender_session;
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (typeof (value = reader.uint64()) === "object" ? value.low || value.high : value !== 0)
|
||||
message.frame_number = value;
|
||||
else
|
||||
delete message.frame_number;
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (wireType !== 2)
|
||||
break;
|
||||
if ((value = reader.bytes()).length)
|
||||
message.opus_data = value;
|
||||
else
|
||||
delete message.opus_data;
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (wireType === 2) {
|
||||
if (!(message.positional_data && message.positional_data.length))
|
||||
message.positional_data = [];
|
||||
reader.floats(message.positional_data);
|
||||
continue;
|
||||
}
|
||||
if (wireType !== 5)
|
||||
break;
|
||||
if (!(message.positional_data && message.positional_data.length))
|
||||
message.positional_data = [];
|
||||
message.positional_data.push(reader.float());
|
||||
continue;
|
||||
}
|
||||
case 7: {
|
||||
if (wireType !== 5)
|
||||
break;
|
||||
if (!$Object.is(value = reader.float(), 0))
|
||||
message.volume_adjustment = value;
|
||||
else
|
||||
delete message.volume_adjustment;
|
||||
continue;
|
||||
}
|
||||
case 16: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (value = reader.bool())
|
||||
message.is_terminator = value;
|
||||
else
|
||||
delete message.is_terminator;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
reader.skipType(wireType, _depth, tag);
|
||||
if (!reader.discardUnknown) {
|
||||
$util.makeProp(message, "$unknowns", false);
|
||||
(message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos));
|
||||
}
|
||||
}
|
||||
if (length !== $undefined) {
|
||||
if (reader.pos !== end)
|
||||
throw $RangeError("index out of range");
|
||||
reader.len = length;
|
||||
}
|
||||
if (_end !== $undefined)
|
||||
throw $Error("missing end group");
|
||||
return message;
|
||||
};
|
||||
|
||||
Audio.verify = function (message, _depth) {
|
||||
if (typeof message !== "object" || message === null)
|
||||
return "object expected";
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
return "max depth exceeded";
|
||||
let properties = {};
|
||||
if (message.target != null && $Object.hasOwnProperty.call(message, "target")) {
|
||||
properties.Header = 1;
|
||||
if (!$util.isInteger(message.target))
|
||||
return "target: integer expected";
|
||||
}
|
||||
if (message.context != null && $Object.hasOwnProperty.call(message, "context")) {
|
||||
if (properties.Header === 1)
|
||||
return "Header: multiple values";
|
||||
properties.Header = 1;
|
||||
if (!$util.isInteger(message.context))
|
||||
return "context: integer expected";
|
||||
}
|
||||
if (message.sender_session != null && $Object.hasOwnProperty.call(message, "sender_session"))
|
||||
if (!$util.isInteger(message.sender_session))
|
||||
return "sender_session: integer expected";
|
||||
if (message.frame_number != null && $Object.hasOwnProperty.call(message, "frame_number"))
|
||||
if (!$util.isInteger(message.frame_number) && !(message.frame_number && $util.isInteger(message.frame_number.low) && $util.isInteger(message.frame_number.high)))
|
||||
return "frame_number: integer|Long expected";
|
||||
if (message.opus_data != null && $Object.hasOwnProperty.call(message, "opus_data"))
|
||||
if (!(message.opus_data && typeof message.opus_data.length === "number" || $util.isString(message.opus_data)))
|
||||
return "opus_data: buffer expected";
|
||||
if (message.positional_data != null && $Object.hasOwnProperty.call(message, "positional_data")) {
|
||||
if (!$Array.isArray(message.positional_data))
|
||||
return "positional_data: array expected";
|
||||
for (let i = 0; i < message.positional_data.length; ++i)
|
||||
if (typeof message.positional_data[i] !== "number")
|
||||
return "positional_data: number[] expected";
|
||||
}
|
||||
if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment"))
|
||||
if (typeof message.volume_adjustment !== "number")
|
||||
return "volume_adjustment: number expected";
|
||||
if (message.is_terminator != null && $Object.hasOwnProperty.call(message, "is_terminator"))
|
||||
if (typeof message.is_terminator !== "boolean")
|
||||
return "is_terminator: boolean expected";
|
||||
return null;
|
||||
};
|
||||
|
||||
Audio.fromObject = function (object, _depth) {
|
||||
if (object instanceof $root.MumbleUDP.Audio)
|
||||
return object;
|
||||
if (!$util.isObject(object))
|
||||
throw $TypeError(".MumbleUDP.Audio: object expected");
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
let message = new $root.MumbleUDP.Audio();
|
||||
if (object.target != null)
|
||||
message.target = object.target >>> 0;
|
||||
if (object.context != null)
|
||||
message.context = object.context >>> 0;
|
||||
if (object.sender_session != null)
|
||||
if ($Number(object.sender_session) !== 0)
|
||||
message.sender_session = object.sender_session >>> 0;
|
||||
if (object.frame_number != null)
|
||||
if (typeof object.frame_number === "object" ? object.frame_number.low || object.frame_number.high : $Number(object.frame_number) !== 0)
|
||||
if ($util.Long)
|
||||
message.frame_number = $util.Long.fromValue(object.frame_number, true);
|
||||
else if (typeof object.frame_number === "string")
|
||||
message.frame_number = $parseInt(object.frame_number, 10);
|
||||
else if (typeof object.frame_number === "number")
|
||||
message.frame_number = object.frame_number;
|
||||
else if (typeof object.frame_number === "object")
|
||||
message.frame_number = new $util.LongBits(object.frame_number.low >>> 0, object.frame_number.high >>> 0).toNumber(true);
|
||||
if (object.opus_data != null)
|
||||
if (object.opus_data.length)
|
||||
if (typeof object.opus_data === "string")
|
||||
$util.base64.decode(object.opus_data, message.opus_data = $util.newBuffer($util.base64.length(object.opus_data)), 0);
|
||||
else if (object.opus_data.length >= 0)
|
||||
message.opus_data = object.opus_data;
|
||||
if (object.positional_data) {
|
||||
if (!$Array.isArray(object.positional_data))
|
||||
throw $TypeError(".MumbleUDP.Audio.positional_data: array expected");
|
||||
message.positional_data = $Array(object.positional_data.length);
|
||||
for (let i = 0; i < object.positional_data.length; ++i)
|
||||
message.positional_data[i] = $Number(object.positional_data[i]);
|
||||
}
|
||||
if (object.volume_adjustment != null)
|
||||
if (!$Object.is($Number(object.volume_adjustment), 0))
|
||||
message.volume_adjustment = $Number(object.volume_adjustment);
|
||||
if (object.is_terminator != null)
|
||||
if (object.is_terminator)
|
||||
message.is_terminator = $Boolean(object.is_terminator);
|
||||
return message;
|
||||
};
|
||||
|
||||
Audio.toObject = function (message, options, _depth) {
|
||||
if (!options)
|
||||
options = {};
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
let object = {};
|
||||
if (options.arrays || options.defaults)
|
||||
object.positional_data = [];
|
||||
if (options.defaults) {
|
||||
object.sender_session = 0;
|
||||
if ($util.Long) {
|
||||
let long = new $util.Long(0, 0, true);
|
||||
object.frame_number = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long;
|
||||
} else
|
||||
object.frame_number = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0;
|
||||
if (options.bytes === $String)
|
||||
object.opus_data = "";
|
||||
else {
|
||||
object.opus_data = [];
|
||||
if (options.bytes !== $Array)
|
||||
object.opus_data = $util.newBuffer(object.opus_data);
|
||||
}
|
||||
object.volume_adjustment = 0;
|
||||
object.is_terminator = false;
|
||||
}
|
||||
if (message.target != null && $Object.hasOwnProperty.call(message, "target")) {
|
||||
object.target = message.target;
|
||||
if (options.oneofs)
|
||||
object.Header = "target";
|
||||
}
|
||||
if (message.context != null && $Object.hasOwnProperty.call(message, "context")) {
|
||||
object.context = message.context;
|
||||
if (options.oneofs)
|
||||
object.Header = "context";
|
||||
}
|
||||
if (message.sender_session != null && $Object.hasOwnProperty.call(message, "sender_session"))
|
||||
object.sender_session = message.sender_session;
|
||||
if (message.frame_number != null && $Object.hasOwnProperty.call(message, "frame_number"))
|
||||
if (typeof $BigInt !== "undefined" && options.longs === $BigInt)
|
||||
object.frame_number = typeof message.frame_number === "number" ? $BigInt(message.frame_number) : $util.Long.fromBits(message.frame_number.low >>> 0, message.frame_number.high >>> 0, true).toBigInt();
|
||||
else if (typeof message.frame_number === "number")
|
||||
object.frame_number = options.longs === $String ? $String(message.frame_number) : message.frame_number;
|
||||
else
|
||||
object.frame_number = options.longs === $String ? $util.Long.prototype.toString.call(message.frame_number) : options.longs === $Number ? new $util.LongBits(message.frame_number.low >>> 0, message.frame_number.high >>> 0).toNumber(true) : message.frame_number;
|
||||
if (message.opus_data != null && $Object.hasOwnProperty.call(message, "opus_data"))
|
||||
object.opus_data = options.bytes === $String ? $util.base64.encode(message.opus_data, 0, message.opus_data.length) : options.bytes === $Array ? $Array.prototype.slice.call(message.opus_data) : message.opus_data;
|
||||
if (message.positional_data && message.positional_data.length) {
|
||||
object.positional_data = $Array(message.positional_data.length);
|
||||
for (let j = 0; j < message.positional_data.length; ++j)
|
||||
object.positional_data[j] = options.json && !$isFinite(message.positional_data[j]) ? $String(message.positional_data[j]) : message.positional_data[j];
|
||||
}
|
||||
if (message.volume_adjustment != null && $Object.hasOwnProperty.call(message, "volume_adjustment"))
|
||||
object.volume_adjustment = options.json && !$isFinite(message.volume_adjustment) ? $String(message.volume_adjustment) : message.volume_adjustment;
|
||||
if (message.is_terminator != null && $Object.hasOwnProperty.call(message, "is_terminator"))
|
||||
object.is_terminator = message.is_terminator;
|
||||
return object;
|
||||
};
|
||||
|
||||
Audio.prototype.toJSON = function() {
|
||||
return Audio.toObject(this, $protobuf.util.toJSONOptions);
|
||||
};
|
||||
|
||||
Audio.getTypeUrl = function(prefix) {
|
||||
if (prefix === $undefined)
|
||||
prefix = "type.googleapis.com";
|
||||
return prefix + "/MumbleUDP.Audio";
|
||||
};
|
||||
|
||||
return Audio;
|
||||
})();
|
||||
|
||||
MumbleUDP.Ping = (function() {
|
||||
|
||||
const Ping = function (properties) {
|
||||
if (properties)
|
||||
for (let keys = $Object.keys(properties), i = 0; i < keys.length; ++i)
|
||||
if (properties[keys[i]] != null && keys[i] !== "__proto__")
|
||||
this[keys[i]] = properties[keys[i]];
|
||||
};
|
||||
|
||||
Ping.prototype.timestamp = $util.Long ? $util.Long.fromBits(0,0,true) : 0;
|
||||
Ping.prototype.request_extended_information = false;
|
||||
Ping.prototype.server_version_v2 = $util.Long ? $util.Long.fromBits(0,0,true) : 0;
|
||||
Ping.prototype.user_count = 0;
|
||||
Ping.prototype.max_user_count = 0;
|
||||
Ping.prototype.max_bandwidth_per_user = 0;
|
||||
|
||||
Ping.create = function(properties) {
|
||||
return new Ping(properties);
|
||||
};
|
||||
|
||||
Ping.encode = function (message, writer, _depth) {
|
||||
if (!writer)
|
||||
writer = $Writer.create();
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp") && (typeof message.timestamp === "object" ? message.timestamp.low || message.timestamp.high : message.timestamp !== 0))
|
||||
writer.uint32(8).uint64(message.timestamp);
|
||||
if (message.request_extended_information != null && $Object.hasOwnProperty.call(message, "request_extended_information") && message.request_extended_information !== false)
|
||||
writer.uint32(16).bool(message.request_extended_information);
|
||||
if (message.server_version_v2 != null && $Object.hasOwnProperty.call(message, "server_version_v2") && (typeof message.server_version_v2 === "object" ? message.server_version_v2.low || message.server_version_v2.high : message.server_version_v2 !== 0))
|
||||
writer.uint32(24).uint64(message.server_version_v2);
|
||||
if (message.user_count != null && $Object.hasOwnProperty.call(message, "user_count") && message.user_count !== 0)
|
||||
writer.uint32(32).uint32(message.user_count);
|
||||
if (message.max_user_count != null && $Object.hasOwnProperty.call(message, "max_user_count") && message.max_user_count !== 0)
|
||||
writer.uint32(40).uint32(message.max_user_count);
|
||||
if (message.max_bandwidth_per_user != null && $Object.hasOwnProperty.call(message, "max_bandwidth_per_user") && message.max_bandwidth_per_user !== 0)
|
||||
writer.uint32(48).uint32(message.max_bandwidth_per_user);
|
||||
if (message.$unknowns != null && $Object.hasOwnProperty.call(message, "$unknowns"))
|
||||
for (let i = 0; i < message.$unknowns.length; ++i)
|
||||
writer.raw(message.$unknowns[i]);
|
||||
return writer;
|
||||
};
|
||||
|
||||
Ping.decode = function (reader, length, _end, _depth, _target) {
|
||||
if (!(reader instanceof $Reader))
|
||||
reader = $Reader.create(reader);
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $Reader.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
let end, message, value;
|
||||
if (length === $undefined)
|
||||
end = reader.len;
|
||||
else {
|
||||
end = reader.pos + length;
|
||||
if (end > reader.len)
|
||||
throw $RangeError("index out of range");
|
||||
length = reader.len;
|
||||
reader.len = end;
|
||||
}
|
||||
message = _target || new $root.MumbleUDP.Ping();
|
||||
while (reader.pos < end) {
|
||||
let start = reader.pos;
|
||||
let tag = reader.tag();
|
||||
if (tag === _end) {
|
||||
_end = $undefined;
|
||||
break;
|
||||
}
|
||||
let wireType = tag & 7;
|
||||
switch (tag >>>= 3) {
|
||||
case 1: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (typeof (value = reader.uint64()) === "object" ? value.low || value.high : value !== 0)
|
||||
message.timestamp = value;
|
||||
else
|
||||
delete message.timestamp;
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (value = reader.bool())
|
||||
message.request_extended_information = value;
|
||||
else
|
||||
delete message.request_extended_information;
|
||||
continue;
|
||||
}
|
||||
case 3: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (typeof (value = reader.uint64()) === "object" ? value.low || value.high : value !== 0)
|
||||
message.server_version_v2 = value;
|
||||
else
|
||||
delete message.server_version_v2;
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (value = reader.uint32())
|
||||
message.user_count = value;
|
||||
else
|
||||
delete message.user_count;
|
||||
continue;
|
||||
}
|
||||
case 5: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (value = reader.uint32())
|
||||
message.max_user_count = value;
|
||||
else
|
||||
delete message.max_user_count;
|
||||
continue;
|
||||
}
|
||||
case 6: {
|
||||
if (wireType !== 0)
|
||||
break;
|
||||
if (value = reader.uint32())
|
||||
message.max_bandwidth_per_user = value;
|
||||
else
|
||||
delete message.max_bandwidth_per_user;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
reader.skipType(wireType, _depth, tag);
|
||||
if (!reader.discardUnknown) {
|
||||
$util.makeProp(message, "$unknowns", false);
|
||||
(message.$unknowns || (message.$unknowns = [])).push(reader.raw(start, reader.pos));
|
||||
}
|
||||
}
|
||||
if (length !== $undefined) {
|
||||
if (reader.pos !== end)
|
||||
throw $RangeError("index out of range");
|
||||
reader.len = length;
|
||||
}
|
||||
if (_end !== $undefined)
|
||||
throw $Error("missing end group");
|
||||
return message;
|
||||
};
|
||||
|
||||
Ping.verify = function (message, _depth) {
|
||||
if (typeof message !== "object" || message === null)
|
||||
return "object expected";
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
return "max depth exceeded";
|
||||
if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp"))
|
||||
if (!$util.isInteger(message.timestamp) && !(message.timestamp && $util.isInteger(message.timestamp.low) && $util.isInteger(message.timestamp.high)))
|
||||
return "timestamp: integer|Long expected";
|
||||
if (message.request_extended_information != null && $Object.hasOwnProperty.call(message, "request_extended_information"))
|
||||
if (typeof message.request_extended_information !== "boolean")
|
||||
return "request_extended_information: boolean expected";
|
||||
if (message.server_version_v2 != null && $Object.hasOwnProperty.call(message, "server_version_v2"))
|
||||
if (!$util.isInteger(message.server_version_v2) && !(message.server_version_v2 && $util.isInteger(message.server_version_v2.low) && $util.isInteger(message.server_version_v2.high)))
|
||||
return "server_version_v2: integer|Long expected";
|
||||
if (message.user_count != null && $Object.hasOwnProperty.call(message, "user_count"))
|
||||
if (!$util.isInteger(message.user_count))
|
||||
return "user_count: integer expected";
|
||||
if (message.max_user_count != null && $Object.hasOwnProperty.call(message, "max_user_count"))
|
||||
if (!$util.isInteger(message.max_user_count))
|
||||
return "max_user_count: integer expected";
|
||||
if (message.max_bandwidth_per_user != null && $Object.hasOwnProperty.call(message, "max_bandwidth_per_user"))
|
||||
if (!$util.isInteger(message.max_bandwidth_per_user))
|
||||
return "max_bandwidth_per_user: integer expected";
|
||||
return null;
|
||||
};
|
||||
|
||||
Ping.fromObject = function (object, _depth) {
|
||||
if (object instanceof $root.MumbleUDP.Ping)
|
||||
return object;
|
||||
if (!$util.isObject(object))
|
||||
throw $TypeError(".MumbleUDP.Ping: object expected");
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
let message = new $root.MumbleUDP.Ping();
|
||||
if (object.timestamp != null)
|
||||
if (typeof object.timestamp === "object" ? object.timestamp.low || object.timestamp.high : $Number(object.timestamp) !== 0)
|
||||
if ($util.Long)
|
||||
message.timestamp = $util.Long.fromValue(object.timestamp, true);
|
||||
else if (typeof object.timestamp === "string")
|
||||
message.timestamp = $parseInt(object.timestamp, 10);
|
||||
else if (typeof object.timestamp === "number")
|
||||
message.timestamp = object.timestamp;
|
||||
else if (typeof object.timestamp === "object")
|
||||
message.timestamp = new $util.LongBits(object.timestamp.low >>> 0, object.timestamp.high >>> 0).toNumber(true);
|
||||
if (object.request_extended_information != null)
|
||||
if (object.request_extended_information)
|
||||
message.request_extended_information = $Boolean(object.request_extended_information);
|
||||
if (object.server_version_v2 != null)
|
||||
if (typeof object.server_version_v2 === "object" ? object.server_version_v2.low || object.server_version_v2.high : $Number(object.server_version_v2) !== 0)
|
||||
if ($util.Long)
|
||||
message.server_version_v2 = $util.Long.fromValue(object.server_version_v2, true);
|
||||
else if (typeof object.server_version_v2 === "string")
|
||||
message.server_version_v2 = $parseInt(object.server_version_v2, 10);
|
||||
else if (typeof object.server_version_v2 === "number")
|
||||
message.server_version_v2 = object.server_version_v2;
|
||||
else if (typeof object.server_version_v2 === "object")
|
||||
message.server_version_v2 = new $util.LongBits(object.server_version_v2.low >>> 0, object.server_version_v2.high >>> 0).toNumber(true);
|
||||
if (object.user_count != null)
|
||||
if ($Number(object.user_count) !== 0)
|
||||
message.user_count = object.user_count >>> 0;
|
||||
if (object.max_user_count != null)
|
||||
if ($Number(object.max_user_count) !== 0)
|
||||
message.max_user_count = object.max_user_count >>> 0;
|
||||
if (object.max_bandwidth_per_user != null)
|
||||
if ($Number(object.max_bandwidth_per_user) !== 0)
|
||||
message.max_bandwidth_per_user = object.max_bandwidth_per_user >>> 0;
|
||||
return message;
|
||||
};
|
||||
|
||||
Ping.toObject = function (message, options, _depth) {
|
||||
if (!options)
|
||||
options = {};
|
||||
if (_depth === $undefined)
|
||||
_depth = 0;
|
||||
if (_depth > $util.recursionLimit)
|
||||
throw $Error("max depth exceeded");
|
||||
let object = {};
|
||||
if (options.defaults) {
|
||||
if ($util.Long) {
|
||||
let long = new $util.Long(0, 0, true);
|
||||
object.timestamp = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long;
|
||||
} else
|
||||
object.timestamp = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0;
|
||||
object.request_extended_information = false;
|
||||
if ($util.Long) {
|
||||
let long = new $util.Long(0, 0, true);
|
||||
object.server_version_v2 = options.longs === $String ? long.toString() : options.longs === $Number ? long.toNumber() : typeof $BigInt !== "undefined" && options.longs === $BigInt ? long.toBigInt() : long;
|
||||
} else
|
||||
object.server_version_v2 = options.longs === $String ? "0" : typeof $BigInt !== "undefined" && options.longs === $BigInt ? $BigInt("0") : 0;
|
||||
object.user_count = 0;
|
||||
object.max_user_count = 0;
|
||||
object.max_bandwidth_per_user = 0;
|
||||
}
|
||||
if (message.timestamp != null && $Object.hasOwnProperty.call(message, "timestamp"))
|
||||
if (typeof $BigInt !== "undefined" && options.longs === $BigInt)
|
||||
object.timestamp = typeof message.timestamp === "number" ? $BigInt(message.timestamp) : $util.Long.fromBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0, true).toBigInt();
|
||||
else if (typeof message.timestamp === "number")
|
||||
object.timestamp = options.longs === $String ? $String(message.timestamp) : message.timestamp;
|
||||
else
|
||||
object.timestamp = options.longs === $String ? $util.Long.prototype.toString.call(message.timestamp) : options.longs === $Number ? new $util.LongBits(message.timestamp.low >>> 0, message.timestamp.high >>> 0).toNumber(true) : message.timestamp;
|
||||
if (message.request_extended_information != null && $Object.hasOwnProperty.call(message, "request_extended_information"))
|
||||
object.request_extended_information = message.request_extended_information;
|
||||
if (message.server_version_v2 != null && $Object.hasOwnProperty.call(message, "server_version_v2"))
|
||||
if (typeof $BigInt !== "undefined" && options.longs === $BigInt)
|
||||
object.server_version_v2 = typeof message.server_version_v2 === "number" ? $BigInt(message.server_version_v2) : $util.Long.fromBits(message.server_version_v2.low >>> 0, message.server_version_v2.high >>> 0, true).toBigInt();
|
||||
else if (typeof message.server_version_v2 === "number")
|
||||
object.server_version_v2 = options.longs === $String ? $String(message.server_version_v2) : message.server_version_v2;
|
||||
else
|
||||
object.server_version_v2 = options.longs === $String ? $util.Long.prototype.toString.call(message.server_version_v2) : options.longs === $Number ? new $util.LongBits(message.server_version_v2.low >>> 0, message.server_version_v2.high >>> 0).toNumber(true) : message.server_version_v2;
|
||||
if (message.user_count != null && $Object.hasOwnProperty.call(message, "user_count"))
|
||||
object.user_count = message.user_count;
|
||||
if (message.max_user_count != null && $Object.hasOwnProperty.call(message, "max_user_count"))
|
||||
object.max_user_count = message.max_user_count;
|
||||
if (message.max_bandwidth_per_user != null && $Object.hasOwnProperty.call(message, "max_bandwidth_per_user"))
|
||||
object.max_bandwidth_per_user = message.max_bandwidth_per_user;
|
||||
return object;
|
||||
};
|
||||
|
||||
Ping.prototype.toJSON = function() {
|
||||
return Ping.toObject(this, $protobuf.util.toJSONOptions);
|
||||
};
|
||||
|
||||
Ping.getTypeUrl = function(prefix) {
|
||||
if (prefix === $undefined)
|
||||
prefix = "type.googleapis.com";
|
||||
return prefix + "/MumbleUDP.Ping";
|
||||
};
|
||||
|
||||
return Ping;
|
||||
})();
|
||||
|
||||
return MumbleUDP;
|
||||
})();
|
||||
|
||||
export {
|
||||
$root as default
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// Generated from proto/Mumble.proto by `npm run proto` (static code, no eval, so it runs under a strict CSP)
|
||||
import { MumbleProto } from './mumble-pb.js';
|
||||
|
||||
// TCP control message type ids, in wire order (index = type id).
|
||||
export const MESSAGE_TYPES = [
|
||||
'Version', 'UDPTunnel', 'Authenticate', 'Ping', 'Reject', 'ServerSync',
|
||||
'ChannelRemove', 'ChannelState', 'UserRemove', 'UserState', 'BanList',
|
||||
'TextMessage', 'PermissionDenied', 'ACL', 'QueryUsers', 'CryptSetup',
|
||||
'ContextActionModify', 'ContextAction', 'UserList', 'VoiceTarget',
|
||||
'PermissionQuery', 'CodecVersion', 'UserStats', 'RequestBlob',
|
||||
'ServerConfig', 'SuggestConfig', 'PluginDataTransmission'
|
||||
] as const;
|
||||
|
||||
export type MessageName = (typeof MESSAGE_TYPES)[number];
|
||||
|
||||
export interface Codec {
|
||||
encode(name: MessageName, payload: Record<string, unknown>): Uint8Array;
|
||||
decode(typeId: number, body: Uint8Array): { name: MessageName; msg: any } | null;
|
||||
}
|
||||
|
||||
interface GeneratedType {
|
||||
verify(obj: Record<string, unknown>): string | null;
|
||||
fromObject(obj: Record<string, unknown>): unknown;
|
||||
encode(msg: unknown): { finish(): Uint8Array };
|
||||
decode(body: Uint8Array): unknown;
|
||||
toObject(msg: unknown, opts: { longs: NumberConstructor }): any;
|
||||
}
|
||||
|
||||
// Field names keep their snake_case spelling from the .proto file; 64-bit integers become numbers.
|
||||
export function createCodec(): Codec {
|
||||
const types = MESSAGE_TYPES.map(n => (MumbleProto as unknown as Record<string, GeneratedType>)[n]);
|
||||
|
||||
return {
|
||||
encode(name, payload) {
|
||||
const id = MESSAGE_TYPES.indexOf(name);
|
||||
const type = types[id];
|
||||
const err = type.verify(payload);
|
||||
if (err) throw new Error(`${name}: ${err}`);
|
||||
const body = type.encode(type.fromObject(payload)).finish();
|
||||
return frame(id, body);
|
||||
},
|
||||
decode(typeId, body) {
|
||||
const type = types[typeId];
|
||||
if (!type) return null;
|
||||
const msg = type.toObject(type.decode(body), { longs: Number });
|
||||
return { name: MESSAGE_TYPES[typeId], msg };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 6-byte header: uint16 type, uint32 length (big endian), then the body.
|
||||
export function frame(typeId: number, body: Uint8Array): Uint8Array {
|
||||
const out = new Uint8Array(6 + body.length);
|
||||
const view = new DataView(out.buffer);
|
||||
view.setUint16(0, typeId);
|
||||
view.setUint32(2, body.length);
|
||||
out.set(body, 6);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Reassembles framed messages from an arbitrary chunked byte stream.
|
||||
export class FrameReader {
|
||||
private buf = new Uint8Array(0);
|
||||
|
||||
push(chunk: Uint8Array, onFrame: (typeId: number, body: Uint8Array) => void): void {
|
||||
const merged = new Uint8Array(this.buf.length + chunk.length);
|
||||
merged.set(this.buf);
|
||||
merged.set(chunk, this.buf.length);
|
||||
let off = 0;
|
||||
while (merged.length - off >= 6) {
|
||||
const view = new DataView(merged.buffer, merged.byteOffset + off);
|
||||
const typeId = view.getUint16(0);
|
||||
const len = view.getUint32(2);
|
||||
if (len > 8 * 1024 * 1024) throw new Error(`Frame too large: ${len}`);
|
||||
if (merged.length - off - 6 < len) break;
|
||||
onFrame(typeId, merged.subarray(off + 6, off + 6 + len));
|
||||
off += 6 + len;
|
||||
}
|
||||
this.buf = merged.slice(off);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// A reliable, ordered byte stream to a Mumble server (TLS in Electron, WebSocket proxy on the web).
|
||||
export interface Transport {
|
||||
send(bytes: Uint8Array): void;
|
||||
close(): void;
|
||||
onData: ((chunk: Uint8Array) => void) | null;
|
||||
onClose: ((reason: string) => void) | null;
|
||||
}
|
||||
|
||||
export interface ConnectTarget {
|
||||
host: string;
|
||||
port: number;
|
||||
}
|
||||
|
||||
// Identity certificate the client presents; Mumble uses it to recognize registered users.
|
||||
export interface Identity {
|
||||
certPem: string;
|
||||
keyPem: string;
|
||||
fingerprint: string;
|
||||
}
|
||||
|
||||
export interface CertDetails {
|
||||
subject: string;
|
||||
issuer: string;
|
||||
validFrom: string;
|
||||
validTo: string;
|
||||
serialNumber: string;
|
||||
fingerprint: string; // SHA-1, the hash Mumble uses for users
|
||||
fingerprint256: string;
|
||||
subjectAltName: string;
|
||||
keyType: string;
|
||||
keyBits: number | null;
|
||||
ca: boolean;
|
||||
selfSigned: boolean;
|
||||
pem: string;
|
||||
}
|
||||
|
||||
export interface ServerCertInfo {
|
||||
fingerprint: string; // sha256 of the server certificate, colon-separated hex
|
||||
authorized: boolean; // true if it chains to a trusted CA
|
||||
authError: string | null; // why it is not trusted, e.g. DEPTH_ZERO_SELF_SIGNED_CERT
|
||||
chain: CertDetails[]; // leaf first
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Mumble voice packets, as sent over UDP or tunneled through TCP (UDPTunnel).
|
||||
// Two wire formats exist: the legacy one (header byte + Mumble varints) and, since 1.5,
|
||||
// a protobuf one (type byte 0 + MumbleUDP.Audio). Servers pick per client version.
|
||||
import { MumbleUDP } from './mumble-udp-pb.js';
|
||||
|
||||
export interface VoicePacket {
|
||||
session: number; // sender (only in packets from the server)
|
||||
target: number; // 0 normal talking, 1-30 whisper targets, 31 server loopback; context when received
|
||||
frame: number; // sequence number, in 10 ms audio frames
|
||||
opus: Uint8Array;
|
||||
last: boolean; // end of this transmission
|
||||
}
|
||||
|
||||
const LEGACY_OPUS = 4;
|
||||
const PROTOBUF_AUDIO = 0;
|
||||
|
||||
// ─── Mumble varint (PacketDataStream) ─────────────────────────────────────────
|
||||
|
||||
export function writeVarint(out: number[], value: number): void {
|
||||
if (!Number.isInteger(value) || value < 0) throw new Error(`Unsupported varint ${value}`);
|
||||
if (value < 0x80) out.push(value);
|
||||
else if (value < 0x4000) out.push(0x80 | (value >> 8), value & 0xff);
|
||||
else if (value < 0x200000) out.push(0xc0 | (value >> 16), (value >> 8) & 0xff, value & 0xff);
|
||||
else if (value < 0x10000000) out.push(0xe0 | (value >> 24), (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff);
|
||||
else if (value < 0x100000000) out.push(0xf0, (value >>> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff);
|
||||
else {
|
||||
const hi = Math.floor(value / 0x100000000), lo = value >>> 0;
|
||||
out.push(0xf4, (hi >>> 24) & 0xff, (hi >> 16) & 0xff, (hi >> 8) & 0xff, hi & 0xff,
|
||||
(lo >>> 24) & 0xff, (lo >> 16) & 0xff, (lo >> 8) & 0xff, lo & 0xff);
|
||||
}
|
||||
}
|
||||
|
||||
export function readVarint(buf: Uint8Array, pos: number): [value: number, next: number] {
|
||||
const need = (n: number) => { if (pos + n > buf.length) throw new Error('Truncated varint'); };
|
||||
need(1);
|
||||
const b = buf[pos];
|
||||
if ((b & 0x80) === 0) return [b, pos + 1];
|
||||
if ((b & 0xc0) === 0x80) { need(2); return [((b & 0x3f) << 8) | buf[pos + 1], pos + 2]; }
|
||||
if ((b & 0xe0) === 0xc0) { need(3); return [((b & 0x1f) << 16) | (buf[pos + 1] << 8) | buf[pos + 2], pos + 3]; }
|
||||
if ((b & 0xf0) === 0xe0) { need(4); return [((b & 0x0f) << 24 | (buf[pos + 1] << 16) | (buf[pos + 2] << 8) | buf[pos + 3]) >>> 0, pos + 4]; }
|
||||
const be32 = (p: number) => ((buf[p] << 24) | (buf[p + 1] << 16) | (buf[p + 2] << 8) | buf[p + 3]) >>> 0;
|
||||
if ((b & 0xfc) === 0xf0) { need(5); return [be32(pos + 1), pos + 5]; }
|
||||
if ((b & 0xfc) === 0xf4) { need(9); return [be32(pos + 1) * 0x100000000 + be32(pos + 5), pos + 9]; }
|
||||
// Negative numbers (0xf8, 0xfc prefixes) never occur in audio headers
|
||||
throw new Error('Unsupported varint prefix');
|
||||
}
|
||||
|
||||
// ─── Encoding (client to server) ──────────────────────────────────────────────
|
||||
|
||||
export function encodeVoice(p: Omit<VoicePacket, 'session'>, protobuf: boolean): Uint8Array {
|
||||
if (protobuf) {
|
||||
const msg = MumbleUDP.Audio.fromObject({
|
||||
target: p.target,
|
||||
frame_number: p.frame,
|
||||
opus_data: p.opus,
|
||||
is_terminator: p.last
|
||||
});
|
||||
const body: Uint8Array = MumbleUDP.Audio.encode(msg).finish();
|
||||
const out = new Uint8Array(body.length + 1);
|
||||
out[0] = PROTOBUF_AUDIO;
|
||||
out.set(body, 1);
|
||||
return out;
|
||||
}
|
||||
if (p.opus.length >= 0x2000) throw new Error('Opus frame too large');
|
||||
const head: number[] = [(LEGACY_OPUS << 5) | (p.target & 0x1f)];
|
||||
writeVarint(head, p.frame);
|
||||
writeVarint(head, p.opus.length | (p.last ? 0x2000 : 0));
|
||||
const out = new Uint8Array(head.length + p.opus.length);
|
||||
out.set(head);
|
||||
out.set(p.opus, head.length);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Decoding (server to client) ──────────────────────────────────────────────
|
||||
|
||||
// Returns null for pings, other codecs and malformed packets.
|
||||
export function decodeVoice(buf: Uint8Array): VoicePacket | null {
|
||||
if (!buf.length) return null;
|
||||
try {
|
||||
if (buf[0] === PROTOBUF_AUDIO) {
|
||||
const m = MumbleUDP.Audio.toObject(MumbleUDP.Audio.decode(buf.subarray(1)), { longs: Number, defaults: true });
|
||||
return {
|
||||
session: m.sender_session,
|
||||
target: m.context ?? m.target ?? 0,
|
||||
frame: m.frame_number,
|
||||
opus: m.opus_data instanceof Uint8Array ? m.opus_data : new Uint8Array(m.opus_data ?? []),
|
||||
last: !!m.is_terminator
|
||||
};
|
||||
}
|
||||
if (buf[0] >> 5 !== LEGACY_OPUS) return null;
|
||||
const target = buf[0] & 0x1f;
|
||||
let pos = 1, session, frame, size;
|
||||
[session, pos] = readVarint(buf, pos);
|
||||
[frame, pos] = readVarint(buf, pos);
|
||||
[size, pos] = readVarint(buf, pos);
|
||||
const len = size & 0x1fff;
|
||||
if (pos + len > buf.length) return null;
|
||||
return { session, target, frame, opus: buf.slice(pos, pos + len), last: (size & 0x2000) !== 0 };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// For tests and the server-side view: a client packet as the server would relay it
|
||||
export function relayAsServer(clientPacket: Uint8Array, session: number, protobuf: boolean): Uint8Array {
|
||||
if (protobuf) {
|
||||
const m = MumbleUDP.Audio.decode(clientPacket.subarray(1));
|
||||
m.sender_session = session;
|
||||
m.context = 0;
|
||||
const body: Uint8Array = MumbleUDP.Audio.encode(m).finish();
|
||||
const out = new Uint8Array(body.length + 1);
|
||||
out.set(body, 1);
|
||||
return out;
|
||||
}
|
||||
const head: number[] = [clientPacket[0] & 0xe0];
|
||||
writeVarint(head, session);
|
||||
const out = new Uint8Array(head.length + clientPacket.length - 1);
|
||||
out.set(head);
|
||||
out.set(clientPacket.subarray(1), head.length);
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user