@fluxerjs/voice 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +25 -7
- package/dist/index.d.ts +25 -7
- package/dist/index.js +290 -195
- package/dist/index.mjs +290 -195
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -545,11 +545,21 @@ function floatToInt16(float32) {
|
|
|
545
545
|
}
|
|
546
546
|
return int16;
|
|
547
547
|
}
|
|
548
|
+
function applyVolumeToInt16(samples, volumePercent) {
|
|
549
|
+
const vol = (volumePercent ?? 100) / 100;
|
|
550
|
+
if (vol === 1) return samples;
|
|
551
|
+
const out = new Int16Array(samples.length);
|
|
552
|
+
for (let i = 0; i < samples.length; i++) {
|
|
553
|
+
out[i] = Math.max(-32768, Math.min(32767, Math.round(samples[i] * vol)));
|
|
554
|
+
}
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
548
557
|
var VOICE_DEBUG = process.env.VOICE_DEBUG === "1" || process.env.VOICE_DEBUG === "true";
|
|
549
558
|
var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
550
559
|
client;
|
|
551
560
|
channel;
|
|
552
561
|
guildId;
|
|
562
|
+
_volume = 100;
|
|
553
563
|
_playing = false;
|
|
554
564
|
_playingVideo = false;
|
|
555
565
|
_destroyed = false;
|
|
@@ -606,6 +616,14 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
606
616
|
const ep = (endpoint ?? "").trim();
|
|
607
617
|
return ep === (this.lastServerEndpoint ?? "") && token === (this.lastServerToken ?? "");
|
|
608
618
|
}
|
|
619
|
+
/** Set playback volume (0-200, 100 = normal). Affects current and future playback. */
|
|
620
|
+
setVolume(volumePercent) {
|
|
621
|
+
this._volume = Math.max(0, Math.min(200, volumePercent ?? 100));
|
|
622
|
+
}
|
|
623
|
+
/** Get current volume (0-200). */
|
|
624
|
+
getVolume() {
|
|
625
|
+
return this._volume ?? 100;
|
|
626
|
+
}
|
|
609
627
|
playOpus(_stream) {
|
|
610
628
|
this.emit(
|
|
611
629
|
"error",
|
|
@@ -682,7 +700,8 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
682
700
|
this.emit("error", new Error("LiveKit: not connected"));
|
|
683
701
|
return;
|
|
684
702
|
}
|
|
685
|
-
|
|
703
|
+
let useFFmpeg = options?.useFFmpeg ?? process.env.FLUXER_VIDEO_FFMPEG === "1";
|
|
704
|
+
if (options?.resolution) useFFmpeg = true;
|
|
686
705
|
if (useFFmpeg && typeof urlOrBuffer === "string") {
|
|
687
706
|
await this.playVideoFFmpeg(urlOrBuffer, options);
|
|
688
707
|
return;
|
|
@@ -1079,8 +1098,9 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1079
1098
|
newBuffer.set(int16, sampleBuffer.length);
|
|
1080
1099
|
sampleBuffer = newBuffer;
|
|
1081
1100
|
while (sampleBuffer.length >= FRAME_SAMPLES && this._playingVideo && audioSource) {
|
|
1082
|
-
const
|
|
1101
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
1083
1102
|
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
1103
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
1084
1104
|
const audioFrame = new AudioFrame(
|
|
1085
1105
|
outSamples,
|
|
1086
1106
|
SAMPLE_RATE,
|
|
@@ -1147,6 +1167,7 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1147
1167
|
const loop = options?.loop ?? true;
|
|
1148
1168
|
let width = 640;
|
|
1149
1169
|
let height = 480;
|
|
1170
|
+
let hasAudio = false;
|
|
1150
1171
|
try {
|
|
1151
1172
|
const { execFile } = await import("child_process");
|
|
1152
1173
|
const { promisify } = await import("util");
|
|
@@ -1156,10 +1177,9 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1156
1177
|
[
|
|
1157
1178
|
"-v",
|
|
1158
1179
|
"error",
|
|
1159
|
-
"-
|
|
1160
|
-
"v:0",
|
|
1180
|
+
"-show_streams",
|
|
1161
1181
|
"-show_entries",
|
|
1162
|
-
"stream=width,height",
|
|
1182
|
+
"stream=codec_type,width,height",
|
|
1163
1183
|
"-of",
|
|
1164
1184
|
"json",
|
|
1165
1185
|
url
|
|
@@ -1167,10 +1187,19 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1167
1187
|
{ encoding: "utf8", timeout: 1e4 }
|
|
1168
1188
|
);
|
|
1169
1189
|
const parsed = JSON.parse(stdout);
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
width
|
|
1173
|
-
|
|
1190
|
+
const streams = parsed?.streams ?? [];
|
|
1191
|
+
for (const s of streams) {
|
|
1192
|
+
if (s.codec_type === "video" && s.width != null && s.height != null) {
|
|
1193
|
+
width = s.width;
|
|
1194
|
+
height = s.height;
|
|
1195
|
+
break;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
for (const s of streams) {
|
|
1199
|
+
if (s.codec_type === "audio") {
|
|
1200
|
+
hasAudio = true;
|
|
1201
|
+
break;
|
|
1202
|
+
}
|
|
1174
1203
|
}
|
|
1175
1204
|
} catch (probeErr) {
|
|
1176
1205
|
this.emit(
|
|
@@ -1181,7 +1210,29 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1181
1210
|
);
|
|
1182
1211
|
return;
|
|
1183
1212
|
}
|
|
1184
|
-
|
|
1213
|
+
let maxFps = options?.maxFramerate ?? 60;
|
|
1214
|
+
const res = options?.resolution;
|
|
1215
|
+
if (res === "480p") {
|
|
1216
|
+
width = 854;
|
|
1217
|
+
height = 480;
|
|
1218
|
+
maxFps = 60;
|
|
1219
|
+
} else if (res === "720p") {
|
|
1220
|
+
width = 1280;
|
|
1221
|
+
height = 720;
|
|
1222
|
+
maxFps = 60;
|
|
1223
|
+
} else if (res === "1080p") {
|
|
1224
|
+
width = 1920;
|
|
1225
|
+
height = 1080;
|
|
1226
|
+
maxFps = 60;
|
|
1227
|
+
} else if (res === "1440p") {
|
|
1228
|
+
width = 2560;
|
|
1229
|
+
height = 1440;
|
|
1230
|
+
maxFps = 60;
|
|
1231
|
+
} else if (res === "4k") {
|
|
1232
|
+
width = 3840;
|
|
1233
|
+
height = 2160;
|
|
1234
|
+
maxFps = 60;
|
|
1235
|
+
} else if (options?.width != null && options?.height != null) {
|
|
1185
1236
|
width = options.width;
|
|
1186
1237
|
height = options.height;
|
|
1187
1238
|
}
|
|
@@ -1193,7 +1244,7 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1193
1244
|
source: sourceOption === "screenshare" ? TrackSource.SOURCE_SCREENSHARE : TrackSource.SOURCE_CAMERA,
|
|
1194
1245
|
videoEncoding: {
|
|
1195
1246
|
maxBitrate: BigInt(options?.videoBitrate ?? 25e5),
|
|
1196
|
-
maxFramerate:
|
|
1247
|
+
maxFramerate: maxFps
|
|
1197
1248
|
}
|
|
1198
1249
|
});
|
|
1199
1250
|
const participant = this.room?.localParticipant;
|
|
@@ -1204,24 +1255,29 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1204
1255
|
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
1205
1256
|
return;
|
|
1206
1257
|
}
|
|
1207
|
-
let
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
audioSource
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1258
|
+
let audioSource = null;
|
|
1259
|
+
let audioReady = false;
|
|
1260
|
+
if (hasAudio) {
|
|
1261
|
+
const src = new AudioSource(SAMPLE_RATE, CHANNELS2);
|
|
1262
|
+
audioSource = src;
|
|
1263
|
+
this.audioSource = src;
|
|
1264
|
+
const track2 = LocalAudioTrack.createAudioTrack("audio", src);
|
|
1265
|
+
this.audioTrack = track2;
|
|
1266
|
+
try {
|
|
1267
|
+
await participant.publishTrack(
|
|
1268
|
+
track2,
|
|
1269
|
+
new TrackPublishOptions({ source: TrackSource.SOURCE_MICROPHONE })
|
|
1270
|
+
);
|
|
1271
|
+
audioReady = true;
|
|
1272
|
+
} catch {
|
|
1273
|
+
track2.close().catch(() => {
|
|
1274
|
+
});
|
|
1275
|
+
this.audioTrack = null;
|
|
1276
|
+
this.audioSource = null;
|
|
1277
|
+
}
|
|
1278
|
+
} else {
|
|
1224
1279
|
this.audioSource = null;
|
|
1280
|
+
this.audioTrack = null;
|
|
1225
1281
|
}
|
|
1226
1282
|
this._playingVideo = true;
|
|
1227
1283
|
this.emit("requestVoiceStateSync", {
|
|
@@ -1229,7 +1285,6 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1229
1285
|
self_video: sourceOption === "camera"
|
|
1230
1286
|
});
|
|
1231
1287
|
const frameSize = Math.ceil(width * height * 3 / 2);
|
|
1232
|
-
const maxFps = options?.maxFramerate ?? 60;
|
|
1233
1288
|
const FRAME_INTERVAL_MS = Math.round(1e3 / maxFps);
|
|
1234
1289
|
let pacingTimeout = null;
|
|
1235
1290
|
let ffmpegProc = null;
|
|
@@ -1247,10 +1302,6 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1247
1302
|
ffmpegProc.kill("SIGKILL");
|
|
1248
1303
|
ffmpegProc = null;
|
|
1249
1304
|
}
|
|
1250
|
-
if (audioFfmpegProc && !audioFfmpegProc.killed) {
|
|
1251
|
-
audioFfmpegProc.kill("SIGKILL");
|
|
1252
|
-
audioFfmpegProc = null;
|
|
1253
|
-
}
|
|
1254
1305
|
this.emit("requestVoiceStateSync", { self_stream: false, self_video: false });
|
|
1255
1306
|
this.currentVideoStream = null;
|
|
1256
1307
|
if (this.audioTrack) {
|
|
@@ -1335,32 +1386,38 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1335
1386
|
pacingTimeout = setTimeout(scheduleNextPacing, FRAME_INTERVAL_MS);
|
|
1336
1387
|
};
|
|
1337
1388
|
scheduleNextPacing();
|
|
1338
|
-
const runFFmpeg = () => {
|
|
1389
|
+
const runFFmpeg = async () => {
|
|
1339
1390
|
const ffmpegArgs = [
|
|
1340
1391
|
"-loglevel",
|
|
1341
1392
|
"warning",
|
|
1342
1393
|
"-re",
|
|
1394
|
+
...loop ? ["-stream_loop", "-1"] : [],
|
|
1343
1395
|
"-i",
|
|
1344
1396
|
url,
|
|
1397
|
+
"-map",
|
|
1398
|
+
"0:v",
|
|
1399
|
+
"-vf",
|
|
1400
|
+
`scale=${width}:${height}`,
|
|
1401
|
+
"-r",
|
|
1402
|
+
String(maxFps),
|
|
1345
1403
|
"-f",
|
|
1346
1404
|
"rawvideo",
|
|
1347
1405
|
"-pix_fmt",
|
|
1348
1406
|
"yuv420p",
|
|
1349
|
-
"-
|
|
1350
|
-
|
|
1407
|
+
"-an",
|
|
1408
|
+
"pipe:1",
|
|
1409
|
+
...hasAudio ? ["-map", "0:a", "-c:a", "libopus", "-f", "webm", "-vn", "pipe:3"] : []
|
|
1351
1410
|
];
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
ffmpegArgs.push("-");
|
|
1356
|
-
ffmpegProc = spawn("ffmpeg", ffmpegArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1411
|
+
const stdioOpts = hasAudio ? ["ignore", "pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
|
|
1412
|
+
const proc = spawn("ffmpeg", ffmpegArgs, { stdio: stdioOpts });
|
|
1413
|
+
ffmpegProc = proc;
|
|
1357
1414
|
this.currentVideoStream = {
|
|
1358
1415
|
destroy: () => {
|
|
1359
|
-
if (
|
|
1416
|
+
if (proc && !proc.killed) proc.kill("SIGKILL");
|
|
1360
1417
|
}
|
|
1361
1418
|
};
|
|
1362
|
-
const stdout =
|
|
1363
|
-
const stderr =
|
|
1419
|
+
const stdout = proc.stdout;
|
|
1420
|
+
const stderr = proc.stderr;
|
|
1364
1421
|
if (stdout) {
|
|
1365
1422
|
stdout.on("data", (chunk) => {
|
|
1366
1423
|
if (!this._playingVideo || cleanupCalled) return;
|
|
@@ -1374,105 +1431,79 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1374
1431
|
if (line && VOICE_DEBUG) this.audioDebug("ffmpeg stderr", { line: line.slice(0, 200) });
|
|
1375
1432
|
});
|
|
1376
1433
|
}
|
|
1377
|
-
|
|
1434
|
+
if (hasAudio && audioReady && audioSource && proc.stdio[3]) {
|
|
1435
|
+
const audioPipe = proc.stdio[3];
|
|
1436
|
+
const { opus: prismOpus } = await import("prism-media");
|
|
1437
|
+
const { OpusDecoder } = await import("opus-decoder");
|
|
1438
|
+
const demuxer = new prismOpus.WebmDemuxer();
|
|
1439
|
+
audioPipe.pipe(demuxer);
|
|
1440
|
+
const decoder = new OpusDecoder({ sampleRate: SAMPLE_RATE, channels: CHANNELS2 });
|
|
1441
|
+
await decoder.ready;
|
|
1442
|
+
let sampleBuffer = new Int16Array(0);
|
|
1443
|
+
let opusBuffer = new Uint8Array(0);
|
|
1444
|
+
let processing = false;
|
|
1445
|
+
const opusFrameQueue = [];
|
|
1446
|
+
const processOneOpusFrame = async (frame) => {
|
|
1447
|
+
if (frame.length < 2 || !audioSource || !this._playingVideo) return;
|
|
1448
|
+
try {
|
|
1449
|
+
const result = decoder.decodeFrame(frame);
|
|
1450
|
+
if (!result?.channelData?.[0]?.length) return;
|
|
1451
|
+
const int16 = floatToInt16(result.channelData[0]);
|
|
1452
|
+
const newBuffer = new Int16Array(sampleBuffer.length + int16.length);
|
|
1453
|
+
newBuffer.set(sampleBuffer);
|
|
1454
|
+
newBuffer.set(int16, sampleBuffer.length);
|
|
1455
|
+
sampleBuffer = newBuffer;
|
|
1456
|
+
while (sampleBuffer.length >= FRAME_SAMPLES && this._playingVideo && audioSource) {
|
|
1457
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
1458
|
+
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
1459
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
1460
|
+
const audioFrame = new AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
1461
|
+
if (audioSource.queuedDuration > 500) await audioSource.waitForPlayout();
|
|
1462
|
+
await audioSource.captureFrame(audioFrame);
|
|
1463
|
+
}
|
|
1464
|
+
} catch {
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
const drainQueue = async () => {
|
|
1468
|
+
if (processing || opusFrameQueue.length === 0) return;
|
|
1469
|
+
processing = true;
|
|
1470
|
+
while (opusFrameQueue.length > 0 && this._playingVideo && audioSource) {
|
|
1471
|
+
const f = opusFrameQueue.shift();
|
|
1472
|
+
await processOneOpusFrame(f);
|
|
1473
|
+
}
|
|
1474
|
+
processing = false;
|
|
1475
|
+
};
|
|
1476
|
+
demuxer.on("data", (chunk) => {
|
|
1477
|
+
if (!this._playingVideo) return;
|
|
1478
|
+
opusBuffer = new Uint8Array(concatUint8Arrays(opusBuffer, new Uint8Array(chunk)));
|
|
1479
|
+
while (opusBuffer.length > 0) {
|
|
1480
|
+
const parsed = parseOpusPacketBoundaries(opusBuffer);
|
|
1481
|
+
if (!parsed) break;
|
|
1482
|
+
opusBuffer = new Uint8Array(opusBuffer.subarray(parsed.consumed));
|
|
1483
|
+
for (const frame of parsed.frames) opusFrameQueue.push(frame);
|
|
1484
|
+
}
|
|
1485
|
+
drainQueue().catch(() => {
|
|
1486
|
+
});
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
proc.on("error", (err) => {
|
|
1378
1490
|
this.emit("error", err);
|
|
1379
1491
|
doCleanup();
|
|
1380
1492
|
});
|
|
1381
|
-
|
|
1493
|
+
proc.on("exit", (code) => {
|
|
1382
1494
|
ffmpegProc = null;
|
|
1383
1495
|
if (cleanupCalled || !this._playingVideo) return;
|
|
1384
1496
|
if (loop && (code === 0 || code === null)) {
|
|
1385
1497
|
frameBuffer.length = 0;
|
|
1386
1498
|
frameBufferBytes = 0;
|
|
1387
1499
|
frameIndex = 0n;
|
|
1388
|
-
setImmediate(runFFmpeg);
|
|
1500
|
+
setImmediate(() => runFFmpeg());
|
|
1389
1501
|
} else {
|
|
1390
1502
|
doCleanup();
|
|
1391
1503
|
}
|
|
1392
1504
|
});
|
|
1393
1505
|
};
|
|
1394
|
-
runFFmpeg();
|
|
1395
|
-
const runAudioFfmpeg = async () => {
|
|
1396
|
-
if (!this._playingVideo || cleanupCalled || !audioSource) return;
|
|
1397
|
-
const audioProc = spawn(
|
|
1398
|
-
"ffmpeg",
|
|
1399
|
-
[
|
|
1400
|
-
"-loglevel",
|
|
1401
|
-
"warning",
|
|
1402
|
-
"-re",
|
|
1403
|
-
"-i",
|
|
1404
|
-
url,
|
|
1405
|
-
"-vn",
|
|
1406
|
-
"-c:a",
|
|
1407
|
-
"libopus",
|
|
1408
|
-
"-f",
|
|
1409
|
-
"webm",
|
|
1410
|
-
...loop ? ["-stream_loop", "-1"] : [],
|
|
1411
|
-
"pipe:1"
|
|
1412
|
-
],
|
|
1413
|
-
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
1414
|
-
);
|
|
1415
|
-
audioFfmpegProc = audioProc;
|
|
1416
|
-
const { opus: prismOpus } = await import("prism-media");
|
|
1417
|
-
const { OpusDecoder } = await import("opus-decoder");
|
|
1418
|
-
const demuxer = new prismOpus.WebmDemuxer();
|
|
1419
|
-
if (audioProc.stdout) audioProc.stdout.pipe(demuxer);
|
|
1420
|
-
const decoder = new OpusDecoder({ sampleRate: SAMPLE_RATE, channels: CHANNELS2 });
|
|
1421
|
-
await decoder.ready;
|
|
1422
|
-
let sampleBuffer = new Int16Array(0);
|
|
1423
|
-
let opusBuffer = new Uint8Array(0);
|
|
1424
|
-
let processing = false;
|
|
1425
|
-
const opusFrameQueue = [];
|
|
1426
|
-
const processOneOpusFrame = async (frame) => {
|
|
1427
|
-
if (frame.length < 2 || !audioSource || !this._playingVideo) return;
|
|
1428
|
-
try {
|
|
1429
|
-
const result = decoder.decodeFrame(frame);
|
|
1430
|
-
if (!result?.channelData?.[0]?.length) return;
|
|
1431
|
-
const int16 = floatToInt16(result.channelData[0]);
|
|
1432
|
-
const newBuffer = new Int16Array(sampleBuffer.length + int16.length);
|
|
1433
|
-
newBuffer.set(sampleBuffer);
|
|
1434
|
-
newBuffer.set(int16, sampleBuffer.length);
|
|
1435
|
-
sampleBuffer = newBuffer;
|
|
1436
|
-
while (sampleBuffer.length >= FRAME_SAMPLES && this._playingVideo && audioSource) {
|
|
1437
|
-
const outSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
1438
|
-
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
1439
|
-
const audioFrame = new AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
1440
|
-
if (audioSource.queuedDuration > 500) await audioSource.waitForPlayout();
|
|
1441
|
-
await audioSource.captureFrame(audioFrame);
|
|
1442
|
-
}
|
|
1443
|
-
} catch {
|
|
1444
|
-
}
|
|
1445
|
-
};
|
|
1446
|
-
const drainQueue = async () => {
|
|
1447
|
-
if (processing || opusFrameQueue.length === 0) return;
|
|
1448
|
-
processing = true;
|
|
1449
|
-
while (opusFrameQueue.length > 0 && this._playingVideo && audioSource) {
|
|
1450
|
-
const f = opusFrameQueue.shift();
|
|
1451
|
-
await processOneOpusFrame(f);
|
|
1452
|
-
}
|
|
1453
|
-
processing = false;
|
|
1454
|
-
};
|
|
1455
|
-
demuxer.on("data", (chunk) => {
|
|
1456
|
-
if (!this._playingVideo) return;
|
|
1457
|
-
opusBuffer = new Uint8Array(concatUint8Arrays(opusBuffer, new Uint8Array(chunk)));
|
|
1458
|
-
while (opusBuffer.length > 0) {
|
|
1459
|
-
const parsed = parseOpusPacketBoundaries(opusBuffer);
|
|
1460
|
-
if (!parsed) break;
|
|
1461
|
-
opusBuffer = new Uint8Array(opusBuffer.subarray(parsed.consumed));
|
|
1462
|
-
for (const frame of parsed.frames) opusFrameQueue.push(frame);
|
|
1463
|
-
}
|
|
1464
|
-
drainQueue().catch(() => {
|
|
1465
|
-
});
|
|
1466
|
-
});
|
|
1467
|
-
audioProc.on("exit", (code) => {
|
|
1468
|
-
if (audioFfmpegProc === audioProc) audioFfmpegProc = null;
|
|
1469
|
-
if (loop && this._playingVideo && !cleanupCalled && (code === 0 || code === null)) {
|
|
1470
|
-
setImmediate(() => runAudioFfmpeg());
|
|
1471
|
-
}
|
|
1472
|
-
});
|
|
1473
|
-
};
|
|
1474
|
-
runAudioFfmpeg().catch(() => {
|
|
1475
|
-
});
|
|
1506
|
+
runFFmpeg().catch((e) => this.audioDebug("ffmpeg error", { error: String(e) }));
|
|
1476
1507
|
}
|
|
1477
1508
|
/**
|
|
1478
1509
|
* Play audio from a WebM/Opus URL or readable stream. Publishes to the LiveKit room as an audio track.
|
|
@@ -1531,8 +1562,9 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1531
1562
|
newBuffer.set(int16, sampleBuffer.length);
|
|
1532
1563
|
sampleBuffer = newBuffer;
|
|
1533
1564
|
while (sampleBuffer.length >= FRAME_SAMPLES && this._playing && source) {
|
|
1534
|
-
const
|
|
1565
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
1535
1566
|
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
1567
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
1536
1568
|
const audioFrame = new AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
1537
1569
|
if (source.queuedDuration > 500) {
|
|
1538
1570
|
await source.waitForPlayout();
|
|
@@ -1587,8 +1619,9 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1587
1619
|
await new Promise((r) => setImmediate(r));
|
|
1588
1620
|
}
|
|
1589
1621
|
while (sampleBuffer.length >= FRAME_SAMPLES && this._playing && source) {
|
|
1590
|
-
const
|
|
1622
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
1591
1623
|
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
1624
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
1592
1625
|
const audioFrame = new AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
1593
1626
|
await source.captureFrame(audioFrame);
|
|
1594
1627
|
framesCaptured++;
|
|
@@ -1596,7 +1629,8 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1596
1629
|
if (sampleBuffer.length > 0 && this._playing && source) {
|
|
1597
1630
|
const padded = new Int16Array(FRAME_SAMPLES);
|
|
1598
1631
|
padded.set(sampleBuffer);
|
|
1599
|
-
const
|
|
1632
|
+
const outSamples = applyVolumeToInt16(padded, this._volume);
|
|
1633
|
+
const audioFrame = new AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
1600
1634
|
await source.captureFrame(audioFrame);
|
|
1601
1635
|
framesCaptured++;
|
|
1602
1636
|
}
|
|
@@ -1686,11 +1720,13 @@ var LiveKitRtcConnection = class extends EventEmitter2 {
|
|
|
1686
1720
|
import { Collection } from "@fluxerjs/collection";
|
|
1687
1721
|
var VoiceManager = class extends EventEmitter3 {
|
|
1688
1722
|
client;
|
|
1723
|
+
/** channel_id -> connection (Fluxer multi-channel: allows multiple connections per guild) */
|
|
1689
1724
|
connections = new Collection();
|
|
1690
|
-
/**
|
|
1725
|
+
/** channel_id -> connection_id (from VoiceServerUpdate; required for voice state updates) */
|
|
1691
1726
|
connectionIds = /* @__PURE__ */ new Map();
|
|
1692
1727
|
/** guild_id -> user_id -> channel_id */
|
|
1693
1728
|
voiceStates = /* @__PURE__ */ new Map();
|
|
1729
|
+
/** channel_id -> pending join */
|
|
1694
1730
|
pending = /* @__PURE__ */ new Map();
|
|
1695
1731
|
shardId;
|
|
1696
1732
|
constructor(client, options = {}) {
|
|
@@ -1743,31 +1779,43 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1743
1779
|
this.voiceStates.set(guildId, guildMap);
|
|
1744
1780
|
}
|
|
1745
1781
|
guildMap.set(data.user_id, data.channel_id);
|
|
1746
|
-
const
|
|
1782
|
+
const channelKey = data.channel_id ?? guildId;
|
|
1783
|
+
const pendingByChannel = this.pending.get(channelKey);
|
|
1784
|
+
const pendingByGuild = this.pending.get(guildId);
|
|
1785
|
+
const pending = pendingByChannel ?? pendingByGuild;
|
|
1747
1786
|
const isBot = String(data.user_id) === String(this.client.user?.id);
|
|
1748
1787
|
if (isBot && data.connection_id) {
|
|
1749
|
-
this.storeConnectionId(
|
|
1788
|
+
this.storeConnectionId(channelKey, data.connection_id);
|
|
1750
1789
|
}
|
|
1751
1790
|
if (pending && isBot) {
|
|
1752
1791
|
this.client.emit?.(
|
|
1753
1792
|
"debug",
|
|
1754
|
-
`[VoiceManager] VoiceStateUpdate for bot - completing pending
|
|
1793
|
+
`[VoiceManager] VoiceStateUpdate for bot - completing pending channel ${channelKey}`
|
|
1755
1794
|
);
|
|
1756
1795
|
pending.state = data;
|
|
1757
|
-
this.tryCompletePending(guildId);
|
|
1796
|
+
this.tryCompletePending(pendingByChannel ? channelKey : guildId, pending);
|
|
1758
1797
|
}
|
|
1759
1798
|
}
|
|
1760
1799
|
handleVoiceServerUpdate(data) {
|
|
1761
1800
|
const guildId = data.guild_id;
|
|
1762
|
-
|
|
1801
|
+
let pending = this.pending.get(guildId);
|
|
1802
|
+
if (!pending) {
|
|
1803
|
+
for (const [, p] of this.pending) {
|
|
1804
|
+
if (p.channel?.guildId === guildId) {
|
|
1805
|
+
pending = p;
|
|
1806
|
+
break;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1763
1810
|
if (pending) {
|
|
1811
|
+
const channelKey = pending.channel?.id ?? guildId;
|
|
1764
1812
|
const hasToken = !!(data.token && data.token.length > 0);
|
|
1765
1813
|
this.client.emit?.(
|
|
1766
1814
|
"debug",
|
|
1767
|
-
`[VoiceManager] VoiceServerUpdate guild=${guildId} endpoint=${data.endpoint ?? "null"} token=${hasToken ? "yes" : "NO"}`
|
|
1815
|
+
`[VoiceManager] VoiceServerUpdate guild=${guildId} channel=${channelKey} endpoint=${data.endpoint ?? "null"} token=${hasToken ? "yes" : "NO"}`
|
|
1768
1816
|
);
|
|
1769
1817
|
pending.server = data;
|
|
1770
|
-
this.tryCompletePending(
|
|
1818
|
+
this.tryCompletePending(channelKey, pending);
|
|
1771
1819
|
return;
|
|
1772
1820
|
}
|
|
1773
1821
|
const userId = this.client.user?.id;
|
|
@@ -1778,15 +1826,21 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1778
1826
|
);
|
|
1779
1827
|
return;
|
|
1780
1828
|
}
|
|
1781
|
-
|
|
1829
|
+
let conn;
|
|
1830
|
+
for (const [, c] of this.connections) {
|
|
1831
|
+
if (c?.channel?.guildId === guildId) {
|
|
1832
|
+
conn = c;
|
|
1833
|
+
break;
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1782
1836
|
if (!conn) return;
|
|
1783
1837
|
if (!data.endpoint || !data.token) {
|
|
1784
1838
|
this.client.emit?.(
|
|
1785
1839
|
"debug",
|
|
1786
|
-
`[VoiceManager] Voice server endpoint null for guild ${guildId}; disconnecting
|
|
1840
|
+
`[VoiceManager] Voice server endpoint null for guild ${guildId}; disconnecting`
|
|
1787
1841
|
);
|
|
1788
1842
|
conn.destroy();
|
|
1789
|
-
this.connections.delete(
|
|
1843
|
+
this.connections.delete(conn.channel.id);
|
|
1790
1844
|
return;
|
|
1791
1845
|
}
|
|
1792
1846
|
if (!isLiveKitEndpoint(data.endpoint, data.token)) return;
|
|
@@ -1796,14 +1850,15 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1796
1850
|
const channel = conn.channel;
|
|
1797
1851
|
this.client.emit?.(
|
|
1798
1852
|
"debug",
|
|
1799
|
-
`[VoiceManager] Voice server migration for guild ${guildId}; reconnecting`
|
|
1853
|
+
`[VoiceManager] Voice server migration for guild ${guildId} channel ${channel.id}; reconnecting`
|
|
1800
1854
|
);
|
|
1801
1855
|
conn.destroy();
|
|
1802
|
-
this.connections.delete(
|
|
1803
|
-
this.
|
|
1856
|
+
this.connections.delete(channel.id);
|
|
1857
|
+
this.connectionIds.delete(channel.id);
|
|
1858
|
+
this.storeConnectionId(channel.id, data.connection_id);
|
|
1804
1859
|
const ConnClass = LiveKitRtcConnection;
|
|
1805
1860
|
const newConn = new ConnClass(this.client, channel, userId);
|
|
1806
|
-
this.registerConnection(
|
|
1861
|
+
this.registerConnection(channel.id, newConn);
|
|
1807
1862
|
const state = {
|
|
1808
1863
|
guild_id: guildId,
|
|
1809
1864
|
channel_id: channel.id,
|
|
@@ -1811,42 +1866,43 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1811
1866
|
session_id: ""
|
|
1812
1867
|
};
|
|
1813
1868
|
newConn.connect(data, state).catch((e) => {
|
|
1814
|
-
this.connections.delete(
|
|
1869
|
+
this.connections.delete(channel.id);
|
|
1815
1870
|
newConn.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
1816
1871
|
});
|
|
1817
1872
|
}
|
|
1818
|
-
storeConnectionId(
|
|
1873
|
+
storeConnectionId(channelId, connectionId) {
|
|
1819
1874
|
const id = connectionId != null ? String(connectionId) : null;
|
|
1820
|
-
if (id) this.connectionIds.set(
|
|
1821
|
-
else this.connectionIds.delete(
|
|
1875
|
+
if (id) this.connectionIds.set(channelId, id);
|
|
1876
|
+
else this.connectionIds.delete(channelId);
|
|
1822
1877
|
}
|
|
1823
|
-
registerConnection(
|
|
1824
|
-
|
|
1878
|
+
registerConnection(channelId, conn) {
|
|
1879
|
+
const cid = conn.channel?.id ?? channelId;
|
|
1880
|
+
this.connections.set(cid, conn);
|
|
1825
1881
|
conn.once("disconnect", () => {
|
|
1826
|
-
this.connections.delete(
|
|
1827
|
-
this.connectionIds.delete(
|
|
1882
|
+
this.connections.delete(cid);
|
|
1883
|
+
this.connectionIds.delete(cid);
|
|
1828
1884
|
});
|
|
1829
1885
|
conn.on("requestVoiceStateSync", (p) => {
|
|
1830
|
-
this.updateVoiceState(
|
|
1886
|
+
this.updateVoiceState(cid, p);
|
|
1831
1887
|
if (p.self_stream) {
|
|
1832
|
-
this.uploadStreamPreview(
|
|
1888
|
+
this.uploadStreamPreview(cid, conn).catch(
|
|
1833
1889
|
(e) => this.client.emit?.("debug", `[VoiceManager] Stream preview upload failed: ${String(e)}`)
|
|
1834
1890
|
);
|
|
1835
1891
|
}
|
|
1836
1892
|
});
|
|
1837
1893
|
}
|
|
1838
1894
|
/** Upload a placeholder stream preview so the preview URL returns 200 instead of 404. */
|
|
1839
|
-
async uploadStreamPreview(
|
|
1840
|
-
const
|
|
1895
|
+
async uploadStreamPreview(channelId, conn) {
|
|
1896
|
+
const cid = conn.channel?.id ?? channelId;
|
|
1897
|
+
const connectionId = this.connectionIds.get(cid);
|
|
1841
1898
|
if (!connectionId) return;
|
|
1842
|
-
const streamKey = `${guildId}:${conn.channel.id}:${connectionId}`;
|
|
1899
|
+
const streamKey = `${conn.channel.guildId}:${conn.channel.id}:${connectionId}`;
|
|
1843
1900
|
const route = Routes.streamPreview(streamKey);
|
|
1844
1901
|
const body = { channel_id: conn.channel.id, thumbnail, content_type: "image/png" };
|
|
1845
1902
|
await this.client.rest.post(route, { body, auth: true });
|
|
1846
1903
|
this.client.emit?.("debug", `[VoiceManager] Uploaded stream preview for ${streamKey}`);
|
|
1847
1904
|
}
|
|
1848
|
-
tryCompletePending(
|
|
1849
|
-
const pending = this.pending.get(guildId);
|
|
1905
|
+
tryCompletePending(channelId, pending) {
|
|
1850
1906
|
if (!pending?.server) return;
|
|
1851
1907
|
const useLiveKit = isLiveKitEndpoint(pending.server.endpoint, pending.server.token);
|
|
1852
1908
|
const hasState = !!pending.state;
|
|
@@ -1865,6 +1921,7 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1865
1921
|
);
|
|
1866
1922
|
return;
|
|
1867
1923
|
}
|
|
1924
|
+
const guildId = pending.channel?.guildId ?? "";
|
|
1868
1925
|
const state = pending.state ?? {
|
|
1869
1926
|
guild_id: guildId,
|
|
1870
1927
|
channel_id: pending.channel.id,
|
|
@@ -1872,13 +1929,13 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1872
1929
|
session_id: ""
|
|
1873
1930
|
};
|
|
1874
1931
|
this.storeConnectionId(
|
|
1875
|
-
|
|
1932
|
+
channelId,
|
|
1876
1933
|
pending.server.connection_id ?? state.connection_id
|
|
1877
1934
|
);
|
|
1878
|
-
this.pending.delete(
|
|
1935
|
+
this.pending.delete(channelId);
|
|
1879
1936
|
const ConnClass = useLiveKit ? LiveKitRtcConnection : VoiceConnection;
|
|
1880
1937
|
const conn = new ConnClass(this.client, pending.channel, userId);
|
|
1881
|
-
this.registerConnection(
|
|
1938
|
+
this.registerConnection(channelId, conn);
|
|
1882
1939
|
conn.connect(pending.server, state).then(
|
|
1883
1940
|
() => pending.resolve(conn),
|
|
1884
1941
|
(e) => pending.reject(e)
|
|
@@ -1886,25 +1943,27 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1886
1943
|
}
|
|
1887
1944
|
/**
|
|
1888
1945
|
* Join a voice channel. Resolves when the connection is ready.
|
|
1946
|
+
* Supports multiple connections per guild (Fluxer multi-channel).
|
|
1889
1947
|
* @param channel - The voice channel to join
|
|
1890
1948
|
* @returns The voice connection (LiveKitRtcConnection when Fluxer uses LiveKit)
|
|
1891
1949
|
*/
|
|
1892
1950
|
async join(channel) {
|
|
1893
|
-
const
|
|
1951
|
+
const channelId = channel.id;
|
|
1952
|
+
const existing = this.connections.get(channelId);
|
|
1894
1953
|
if (existing) {
|
|
1895
|
-
const isReusable = existing
|
|
1954
|
+
const isReusable = existing instanceof LiveKitRtcConnection ? existing.isConnected() : true;
|
|
1896
1955
|
if (isReusable) return existing;
|
|
1897
1956
|
existing.destroy();
|
|
1898
|
-
this.connections.delete(
|
|
1957
|
+
this.connections.delete(channelId);
|
|
1899
1958
|
}
|
|
1900
1959
|
return new Promise((resolve, reject) => {
|
|
1901
1960
|
this.client.emit?.(
|
|
1902
1961
|
"debug",
|
|
1903
|
-
`[VoiceManager] Requesting voice join guild=${channel.guildId} channel=${
|
|
1962
|
+
`[VoiceManager] Requesting voice join guild=${channel.guildId} channel=${channelId}`
|
|
1904
1963
|
);
|
|
1905
1964
|
const timeout = setTimeout(() => {
|
|
1906
|
-
if (this.pending.has(
|
|
1907
|
-
this.pending.delete(
|
|
1965
|
+
if (this.pending.has(channelId)) {
|
|
1966
|
+
this.pending.delete(channelId);
|
|
1908
1967
|
reject(
|
|
1909
1968
|
new Error(
|
|
1910
1969
|
"Voice connection timeout. Ensure the server has voice enabled and the bot has Connect permissions. The gateway must send VoiceServerUpdate and VoiceStateUpdate in response."
|
|
@@ -1912,7 +1971,7 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1912
1971
|
);
|
|
1913
1972
|
}
|
|
1914
1973
|
}, 2e4);
|
|
1915
|
-
this.pending.set(
|
|
1974
|
+
this.pending.set(channelId, {
|
|
1916
1975
|
channel,
|
|
1917
1976
|
resolve: (c) => {
|
|
1918
1977
|
clearTimeout(timeout);
|
|
@@ -1935,56 +1994,92 @@ var VoiceManager = class extends EventEmitter3 {
|
|
|
1935
1994
|
});
|
|
1936
1995
|
}
|
|
1937
1996
|
/**
|
|
1938
|
-
* Leave
|
|
1997
|
+
* Leave all voice channels in a guild.
|
|
1998
|
+
* With multi-channel support, disconnects from every channel in the guild.
|
|
1939
1999
|
* @param guildId - Guild ID to leave
|
|
1940
2000
|
*/
|
|
1941
2001
|
leave(guildId) {
|
|
1942
|
-
const
|
|
1943
|
-
|
|
2002
|
+
const toLeave = [];
|
|
2003
|
+
for (const [cid, c] of this.connections) {
|
|
2004
|
+
if (c?.channel?.guildId === guildId) toLeave.push({ channelId: cid, conn: c });
|
|
2005
|
+
}
|
|
2006
|
+
for (const { channelId, conn } of toLeave) {
|
|
1944
2007
|
conn.destroy();
|
|
1945
|
-
this.connections.delete(
|
|
1946
|
-
this.connectionIds.delete(
|
|
2008
|
+
this.connections.delete(channelId);
|
|
2009
|
+
this.connectionIds.delete(channelId);
|
|
1947
2010
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
2011
|
+
if (toLeave.length > 0) {
|
|
2012
|
+
this.client.sendToGateway(this.shardId, {
|
|
2013
|
+
op: GatewayOpcodes.VoiceStateUpdate,
|
|
2014
|
+
d: {
|
|
2015
|
+
guild_id: guildId,
|
|
2016
|
+
channel_id: null,
|
|
2017
|
+
self_mute: false,
|
|
2018
|
+
self_deaf: false
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Leave a specific voice channel by channel ID.
|
|
2025
|
+
* @param channelId - Channel ID to leave
|
|
2026
|
+
*/
|
|
2027
|
+
leaveChannel(channelId) {
|
|
2028
|
+
const conn = this.connections.get(channelId);
|
|
2029
|
+
if (conn) {
|
|
2030
|
+
const guildId = conn.channel?.guildId;
|
|
2031
|
+
conn.destroy();
|
|
2032
|
+
this.connections.delete(channelId);
|
|
2033
|
+
this.connectionIds.delete(channelId);
|
|
2034
|
+
if (guildId) {
|
|
2035
|
+
this.client.sendToGateway(this.shardId, {
|
|
2036
|
+
op: GatewayOpcodes.VoiceStateUpdate,
|
|
2037
|
+
d: {
|
|
2038
|
+
guild_id: guildId,
|
|
2039
|
+
channel_id: null,
|
|
2040
|
+
self_mute: false,
|
|
2041
|
+
self_deaf: false
|
|
2042
|
+
}
|
|
2043
|
+
});
|
|
1955
2044
|
}
|
|
1956
|
-
}
|
|
2045
|
+
}
|
|
1957
2046
|
}
|
|
1958
2047
|
/**
|
|
1959
|
-
* Get the active voice connection for a
|
|
1960
|
-
* @param
|
|
2048
|
+
* Get the active voice connection for a channel or guild.
|
|
2049
|
+
* @param channelOrGuildId - Channel ID (primary) or guild ID (returns first connection in that guild)
|
|
1961
2050
|
*/
|
|
1962
|
-
getConnection(
|
|
1963
|
-
|
|
2051
|
+
getConnection(channelOrGuildId) {
|
|
2052
|
+
const byChannel = this.connections.get(channelOrGuildId);
|
|
2053
|
+
if (byChannel) return byChannel;
|
|
2054
|
+
for (const [, c] of this.connections) {
|
|
2055
|
+
if (c?.channel?.guildId === channelOrGuildId) return c;
|
|
2056
|
+
}
|
|
2057
|
+
return void 0;
|
|
1964
2058
|
}
|
|
1965
2059
|
/**
|
|
1966
2060
|
* Update voice state (e.g. self_stream, self_video) while in a channel.
|
|
1967
2061
|
* Sends a VoiceStateUpdate to the gateway so the server and clients see the change.
|
|
1968
2062
|
* Requires connection_id (from VoiceServerUpdate); without it, the gateway would treat
|
|
1969
2063
|
* the update as a new join and trigger a new VoiceServerUpdate, causing connection loops.
|
|
1970
|
-
* @param
|
|
2064
|
+
* @param channelId - Channel ID (connection key)
|
|
1971
2065
|
* @param partial - Partial voice state to update (self_stream, self_video, self_mute, self_deaf)
|
|
1972
2066
|
*/
|
|
1973
|
-
updateVoiceState(
|
|
1974
|
-
const conn = this.connections.get(
|
|
2067
|
+
updateVoiceState(channelId, partial) {
|
|
2068
|
+
const conn = this.connections.get(channelId);
|
|
1975
2069
|
if (!conn) return;
|
|
1976
|
-
const connectionId = this.connectionIds.get(
|
|
2070
|
+
const connectionId = this.connectionIds.get(channelId);
|
|
2071
|
+
const guildId = conn.channel?.guildId;
|
|
1977
2072
|
if (!connectionId) {
|
|
1978
2073
|
this.client.emit?.(
|
|
1979
2074
|
"debug",
|
|
1980
|
-
`[VoiceManager] Skipping voice state sync: no connection_id for
|
|
2075
|
+
`[VoiceManager] Skipping voice state sync: no connection_id for channel ${channelId}`
|
|
1981
2076
|
);
|
|
1982
2077
|
return;
|
|
1983
2078
|
}
|
|
1984
2079
|
this.client.sendToGateway(this.shardId, {
|
|
1985
2080
|
op: GatewayOpcodes.VoiceStateUpdate,
|
|
1986
2081
|
d: {
|
|
1987
|
-
guild_id: guildId,
|
|
2082
|
+
guild_id: guildId ?? "",
|
|
1988
2083
|
channel_id: conn.channel.id,
|
|
1989
2084
|
connection_id: connectionId,
|
|
1990
2085
|
self_mute: partial.self_mute ?? false,
|