@fluxerjs/voice 1.1.7 → 1.1.9
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.js
CHANGED
|
@@ -3122,11 +3122,21 @@ function floatToInt16(float32) {
|
|
|
3122
3122
|
}
|
|
3123
3123
|
return int16;
|
|
3124
3124
|
}
|
|
3125
|
+
function applyVolumeToInt16(samples, volumePercent) {
|
|
3126
|
+
const vol = (volumePercent ?? 100) / 100;
|
|
3127
|
+
if (vol === 1) return samples;
|
|
3128
|
+
const out = new Int16Array(samples.length);
|
|
3129
|
+
for (let i = 0; i < samples.length; i++) {
|
|
3130
|
+
out[i] = Math.max(-32768, Math.min(32767, Math.round(samples[i] * vol)));
|
|
3131
|
+
}
|
|
3132
|
+
return out;
|
|
3133
|
+
}
|
|
3125
3134
|
var VOICE_DEBUG = process.env.VOICE_DEBUG === "1" || process.env.VOICE_DEBUG === "true";
|
|
3126
3135
|
var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
3127
3136
|
client;
|
|
3128
3137
|
channel;
|
|
3129
3138
|
guildId;
|
|
3139
|
+
_volume = 100;
|
|
3130
3140
|
_playing = false;
|
|
3131
3141
|
_playingVideo = false;
|
|
3132
3142
|
_destroyed = false;
|
|
@@ -3183,6 +3193,14 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3183
3193
|
const ep = (endpoint ?? "").trim();
|
|
3184
3194
|
return ep === (this.lastServerEndpoint ?? "") && token === (this.lastServerToken ?? "");
|
|
3185
3195
|
}
|
|
3196
|
+
/** Set playback volume (0-200, 100 = normal). Affects current and future playback. */
|
|
3197
|
+
setVolume(volumePercent) {
|
|
3198
|
+
this._volume = Math.max(0, Math.min(200, volumePercent ?? 100));
|
|
3199
|
+
}
|
|
3200
|
+
/** Get current volume (0-200). */
|
|
3201
|
+
getVolume() {
|
|
3202
|
+
return this._volume ?? 100;
|
|
3203
|
+
}
|
|
3186
3204
|
playOpus(_stream) {
|
|
3187
3205
|
this.emit(
|
|
3188
3206
|
"error",
|
|
@@ -3259,7 +3277,8 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3259
3277
|
this.emit("error", new Error("LiveKit: not connected"));
|
|
3260
3278
|
return;
|
|
3261
3279
|
}
|
|
3262
|
-
|
|
3280
|
+
let useFFmpeg = options?.useFFmpeg ?? process.env.FLUXER_VIDEO_FFMPEG === "1";
|
|
3281
|
+
if (options?.resolution) useFFmpeg = true;
|
|
3263
3282
|
if (useFFmpeg && typeof urlOrBuffer === "string") {
|
|
3264
3283
|
await this.playVideoFFmpeg(urlOrBuffer, options);
|
|
3265
3284
|
return;
|
|
@@ -3656,8 +3675,9 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3656
3675
|
newBuffer.set(int16, sampleBuffer.length);
|
|
3657
3676
|
sampleBuffer = newBuffer;
|
|
3658
3677
|
while (sampleBuffer.length >= FRAME_SAMPLES && this._playingVideo && audioSource) {
|
|
3659
|
-
const
|
|
3678
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
3660
3679
|
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
3680
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
3661
3681
|
const audioFrame = new import_rtc_node.AudioFrame(
|
|
3662
3682
|
outSamples,
|
|
3663
3683
|
SAMPLE_RATE,
|
|
@@ -3724,6 +3744,7 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3724
3744
|
const loop = options?.loop ?? true;
|
|
3725
3745
|
let width = 640;
|
|
3726
3746
|
let height = 480;
|
|
3747
|
+
let hasAudio = false;
|
|
3727
3748
|
try {
|
|
3728
3749
|
const { execFile } = await import("child_process");
|
|
3729
3750
|
const { promisify } = await import("util");
|
|
@@ -3733,10 +3754,9 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3733
3754
|
[
|
|
3734
3755
|
"-v",
|
|
3735
3756
|
"error",
|
|
3736
|
-
"-
|
|
3737
|
-
"v:0",
|
|
3757
|
+
"-show_streams",
|
|
3738
3758
|
"-show_entries",
|
|
3739
|
-
"stream=width,height",
|
|
3759
|
+
"stream=codec_type,width,height",
|
|
3740
3760
|
"-of",
|
|
3741
3761
|
"json",
|
|
3742
3762
|
url
|
|
@@ -3744,10 +3764,19 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3744
3764
|
{ encoding: "utf8", timeout: 1e4 }
|
|
3745
3765
|
);
|
|
3746
3766
|
const parsed = JSON.parse(stdout);
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
width
|
|
3750
|
-
|
|
3767
|
+
const streams = parsed?.streams ?? [];
|
|
3768
|
+
for (const s of streams) {
|
|
3769
|
+
if (s.codec_type === "video" && s.width != null && s.height != null) {
|
|
3770
|
+
width = s.width;
|
|
3771
|
+
height = s.height;
|
|
3772
|
+
break;
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
for (const s of streams) {
|
|
3776
|
+
if (s.codec_type === "audio") {
|
|
3777
|
+
hasAudio = true;
|
|
3778
|
+
break;
|
|
3779
|
+
}
|
|
3751
3780
|
}
|
|
3752
3781
|
} catch (probeErr) {
|
|
3753
3782
|
this.emit(
|
|
@@ -3758,7 +3787,29 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3758
3787
|
);
|
|
3759
3788
|
return;
|
|
3760
3789
|
}
|
|
3761
|
-
|
|
3790
|
+
let maxFps = options?.maxFramerate ?? 60;
|
|
3791
|
+
const res = options?.resolution;
|
|
3792
|
+
if (res === "480p") {
|
|
3793
|
+
width = 854;
|
|
3794
|
+
height = 480;
|
|
3795
|
+
maxFps = 60;
|
|
3796
|
+
} else if (res === "720p") {
|
|
3797
|
+
width = 1280;
|
|
3798
|
+
height = 720;
|
|
3799
|
+
maxFps = 60;
|
|
3800
|
+
} else if (res === "1080p") {
|
|
3801
|
+
width = 1920;
|
|
3802
|
+
height = 1080;
|
|
3803
|
+
maxFps = 60;
|
|
3804
|
+
} else if (res === "1440p") {
|
|
3805
|
+
width = 2560;
|
|
3806
|
+
height = 1440;
|
|
3807
|
+
maxFps = 60;
|
|
3808
|
+
} else if (res === "4k") {
|
|
3809
|
+
width = 3840;
|
|
3810
|
+
height = 2160;
|
|
3811
|
+
maxFps = 60;
|
|
3812
|
+
} else if (options?.width != null && options?.height != null) {
|
|
3762
3813
|
width = options.width;
|
|
3763
3814
|
height = options.height;
|
|
3764
3815
|
}
|
|
@@ -3770,7 +3821,7 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3770
3821
|
source: sourceOption === "screenshare" ? import_rtc_node.TrackSource.SOURCE_SCREENSHARE : import_rtc_node.TrackSource.SOURCE_CAMERA,
|
|
3771
3822
|
videoEncoding: {
|
|
3772
3823
|
maxBitrate: BigInt(options?.videoBitrate ?? 25e5),
|
|
3773
|
-
maxFramerate:
|
|
3824
|
+
maxFramerate: maxFps
|
|
3774
3825
|
}
|
|
3775
3826
|
});
|
|
3776
3827
|
const participant = this.room?.localParticipant;
|
|
@@ -3781,24 +3832,29 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3781
3832
|
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
3782
3833
|
return;
|
|
3783
3834
|
}
|
|
3784
|
-
let
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
audioSource
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3835
|
+
let audioSource = null;
|
|
3836
|
+
let audioReady = false;
|
|
3837
|
+
if (hasAudio) {
|
|
3838
|
+
const src = new import_rtc_node.AudioSource(SAMPLE_RATE, CHANNELS2);
|
|
3839
|
+
audioSource = src;
|
|
3840
|
+
this.audioSource = src;
|
|
3841
|
+
const track2 = import_rtc_node.LocalAudioTrack.createAudioTrack("audio", src);
|
|
3842
|
+
this.audioTrack = track2;
|
|
3843
|
+
try {
|
|
3844
|
+
await participant.publishTrack(
|
|
3845
|
+
track2,
|
|
3846
|
+
new import_rtc_node.TrackPublishOptions({ source: import_rtc_node.TrackSource.SOURCE_MICROPHONE })
|
|
3847
|
+
);
|
|
3848
|
+
audioReady = true;
|
|
3849
|
+
} catch {
|
|
3850
|
+
track2.close().catch(() => {
|
|
3851
|
+
});
|
|
3852
|
+
this.audioTrack = null;
|
|
3853
|
+
this.audioSource = null;
|
|
3854
|
+
}
|
|
3855
|
+
} else {
|
|
3801
3856
|
this.audioSource = null;
|
|
3857
|
+
this.audioTrack = null;
|
|
3802
3858
|
}
|
|
3803
3859
|
this._playingVideo = true;
|
|
3804
3860
|
this.emit("requestVoiceStateSync", {
|
|
@@ -3806,7 +3862,6 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3806
3862
|
self_video: sourceOption === "camera"
|
|
3807
3863
|
});
|
|
3808
3864
|
const frameSize = Math.ceil(width * height * 3 / 2);
|
|
3809
|
-
const maxFps = options?.maxFramerate ?? 60;
|
|
3810
3865
|
const FRAME_INTERVAL_MS = Math.round(1e3 / maxFps);
|
|
3811
3866
|
let pacingTimeout = null;
|
|
3812
3867
|
let ffmpegProc = null;
|
|
@@ -3824,10 +3879,6 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3824
3879
|
ffmpegProc.kill("SIGKILL");
|
|
3825
3880
|
ffmpegProc = null;
|
|
3826
3881
|
}
|
|
3827
|
-
if (audioFfmpegProc && !audioFfmpegProc.killed) {
|
|
3828
|
-
audioFfmpegProc.kill("SIGKILL");
|
|
3829
|
-
audioFfmpegProc = null;
|
|
3830
|
-
}
|
|
3831
3882
|
this.emit("requestVoiceStateSync", { self_stream: false, self_video: false });
|
|
3832
3883
|
this.currentVideoStream = null;
|
|
3833
3884
|
if (this.audioTrack) {
|
|
@@ -3912,32 +3963,38 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3912
3963
|
pacingTimeout = setTimeout(scheduleNextPacing, FRAME_INTERVAL_MS);
|
|
3913
3964
|
};
|
|
3914
3965
|
scheduleNextPacing();
|
|
3915
|
-
const runFFmpeg = () => {
|
|
3966
|
+
const runFFmpeg = async () => {
|
|
3916
3967
|
const ffmpegArgs = [
|
|
3917
3968
|
"-loglevel",
|
|
3918
3969
|
"warning",
|
|
3919
3970
|
"-re",
|
|
3971
|
+
...loop ? ["-stream_loop", "-1"] : [],
|
|
3920
3972
|
"-i",
|
|
3921
3973
|
url,
|
|
3974
|
+
"-map",
|
|
3975
|
+
"0:v",
|
|
3976
|
+
"-vf",
|
|
3977
|
+
`scale=${width}:${height}`,
|
|
3978
|
+
"-r",
|
|
3979
|
+
String(maxFps),
|
|
3922
3980
|
"-f",
|
|
3923
3981
|
"rawvideo",
|
|
3924
3982
|
"-pix_fmt",
|
|
3925
3983
|
"yuv420p",
|
|
3926
|
-
"-
|
|
3927
|
-
|
|
3984
|
+
"-an",
|
|
3985
|
+
"pipe:1",
|
|
3986
|
+
...hasAudio ? ["-map", "0:a", "-c:a", "libopus", "-f", "webm", "-vn", "pipe:3"] : []
|
|
3928
3987
|
];
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
ffmpegArgs.push("-");
|
|
3933
|
-
ffmpegProc = (0, import_node_child_process.spawn)("ffmpeg", ffmpegArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3988
|
+
const stdioOpts = hasAudio ? ["ignore", "pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"];
|
|
3989
|
+
const proc = (0, import_node_child_process.spawn)("ffmpeg", ffmpegArgs, { stdio: stdioOpts });
|
|
3990
|
+
ffmpegProc = proc;
|
|
3934
3991
|
this.currentVideoStream = {
|
|
3935
3992
|
destroy: () => {
|
|
3936
|
-
if (
|
|
3993
|
+
if (proc && !proc.killed) proc.kill("SIGKILL");
|
|
3937
3994
|
}
|
|
3938
3995
|
};
|
|
3939
|
-
const stdout =
|
|
3940
|
-
const stderr =
|
|
3996
|
+
const stdout = proc.stdout;
|
|
3997
|
+
const stderr = proc.stderr;
|
|
3941
3998
|
if (stdout) {
|
|
3942
3999
|
stdout.on("data", (chunk) => {
|
|
3943
4000
|
if (!this._playingVideo || cleanupCalled) return;
|
|
@@ -3951,105 +4008,79 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
3951
4008
|
if (line && VOICE_DEBUG) this.audioDebug("ffmpeg stderr", { line: line.slice(0, 200) });
|
|
3952
4009
|
});
|
|
3953
4010
|
}
|
|
3954
|
-
|
|
4011
|
+
if (hasAudio && audioReady && audioSource && proc.stdio[3]) {
|
|
4012
|
+
const audioPipe = proc.stdio[3];
|
|
4013
|
+
const { opus: prismOpus } = await import("prism-media");
|
|
4014
|
+
const { OpusDecoder } = await import("opus-decoder");
|
|
4015
|
+
const demuxer = new prismOpus.WebmDemuxer();
|
|
4016
|
+
audioPipe.pipe(demuxer);
|
|
4017
|
+
const decoder = new OpusDecoder({ sampleRate: SAMPLE_RATE, channels: CHANNELS2 });
|
|
4018
|
+
await decoder.ready;
|
|
4019
|
+
let sampleBuffer = new Int16Array(0);
|
|
4020
|
+
let opusBuffer = new Uint8Array(0);
|
|
4021
|
+
let processing = false;
|
|
4022
|
+
const opusFrameQueue = [];
|
|
4023
|
+
const processOneOpusFrame = async (frame) => {
|
|
4024
|
+
if (frame.length < 2 || !audioSource || !this._playingVideo) return;
|
|
4025
|
+
try {
|
|
4026
|
+
const result = decoder.decodeFrame(frame);
|
|
4027
|
+
if (!result?.channelData?.[0]?.length) return;
|
|
4028
|
+
const int16 = floatToInt16(result.channelData[0]);
|
|
4029
|
+
const newBuffer = new Int16Array(sampleBuffer.length + int16.length);
|
|
4030
|
+
newBuffer.set(sampleBuffer);
|
|
4031
|
+
newBuffer.set(int16, sampleBuffer.length);
|
|
4032
|
+
sampleBuffer = newBuffer;
|
|
4033
|
+
while (sampleBuffer.length >= FRAME_SAMPLES && this._playingVideo && audioSource) {
|
|
4034
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
4035
|
+
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
4036
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
4037
|
+
const audioFrame = new import_rtc_node.AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
4038
|
+
if (audioSource.queuedDuration > 500) await audioSource.waitForPlayout();
|
|
4039
|
+
await audioSource.captureFrame(audioFrame);
|
|
4040
|
+
}
|
|
4041
|
+
} catch {
|
|
4042
|
+
}
|
|
4043
|
+
};
|
|
4044
|
+
const drainQueue = async () => {
|
|
4045
|
+
if (processing || opusFrameQueue.length === 0) return;
|
|
4046
|
+
processing = true;
|
|
4047
|
+
while (opusFrameQueue.length > 0 && this._playingVideo && audioSource) {
|
|
4048
|
+
const f = opusFrameQueue.shift();
|
|
4049
|
+
await processOneOpusFrame(f);
|
|
4050
|
+
}
|
|
4051
|
+
processing = false;
|
|
4052
|
+
};
|
|
4053
|
+
demuxer.on("data", (chunk) => {
|
|
4054
|
+
if (!this._playingVideo) return;
|
|
4055
|
+
opusBuffer = new Uint8Array(concatUint8Arrays(opusBuffer, new Uint8Array(chunk)));
|
|
4056
|
+
while (opusBuffer.length > 0) {
|
|
4057
|
+
const parsed = parseOpusPacketBoundaries(opusBuffer);
|
|
4058
|
+
if (!parsed) break;
|
|
4059
|
+
opusBuffer = new Uint8Array(opusBuffer.subarray(parsed.consumed));
|
|
4060
|
+
for (const frame of parsed.frames) opusFrameQueue.push(frame);
|
|
4061
|
+
}
|
|
4062
|
+
drainQueue().catch(() => {
|
|
4063
|
+
});
|
|
4064
|
+
});
|
|
4065
|
+
}
|
|
4066
|
+
proc.on("error", (err) => {
|
|
3955
4067
|
this.emit("error", err);
|
|
3956
4068
|
doCleanup();
|
|
3957
4069
|
});
|
|
3958
|
-
|
|
4070
|
+
proc.on("exit", (code) => {
|
|
3959
4071
|
ffmpegProc = null;
|
|
3960
4072
|
if (cleanupCalled || !this._playingVideo) return;
|
|
3961
4073
|
if (loop && (code === 0 || code === null)) {
|
|
3962
4074
|
frameBuffer.length = 0;
|
|
3963
4075
|
frameBufferBytes = 0;
|
|
3964
4076
|
frameIndex = 0n;
|
|
3965
|
-
setImmediate(runFFmpeg);
|
|
4077
|
+
setImmediate(() => runFFmpeg());
|
|
3966
4078
|
} else {
|
|
3967
4079
|
doCleanup();
|
|
3968
4080
|
}
|
|
3969
4081
|
});
|
|
3970
4082
|
};
|
|
3971
|
-
runFFmpeg();
|
|
3972
|
-
const runAudioFfmpeg = async () => {
|
|
3973
|
-
if (!this._playingVideo || cleanupCalled || !audioSource) return;
|
|
3974
|
-
const audioProc = (0, import_node_child_process.spawn)(
|
|
3975
|
-
"ffmpeg",
|
|
3976
|
-
[
|
|
3977
|
-
"-loglevel",
|
|
3978
|
-
"warning",
|
|
3979
|
-
"-re",
|
|
3980
|
-
"-i",
|
|
3981
|
-
url,
|
|
3982
|
-
"-vn",
|
|
3983
|
-
"-c:a",
|
|
3984
|
-
"libopus",
|
|
3985
|
-
"-f",
|
|
3986
|
-
"webm",
|
|
3987
|
-
...loop ? ["-stream_loop", "-1"] : [],
|
|
3988
|
-
"pipe:1"
|
|
3989
|
-
],
|
|
3990
|
-
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
3991
|
-
);
|
|
3992
|
-
audioFfmpegProc = audioProc;
|
|
3993
|
-
const { opus: prismOpus } = await import("prism-media");
|
|
3994
|
-
const { OpusDecoder } = await import("opus-decoder");
|
|
3995
|
-
const demuxer = new prismOpus.WebmDemuxer();
|
|
3996
|
-
if (audioProc.stdout) audioProc.stdout.pipe(demuxer);
|
|
3997
|
-
const decoder = new OpusDecoder({ sampleRate: SAMPLE_RATE, channels: CHANNELS2 });
|
|
3998
|
-
await decoder.ready;
|
|
3999
|
-
let sampleBuffer = new Int16Array(0);
|
|
4000
|
-
let opusBuffer = new Uint8Array(0);
|
|
4001
|
-
let processing = false;
|
|
4002
|
-
const opusFrameQueue = [];
|
|
4003
|
-
const processOneOpusFrame = async (frame) => {
|
|
4004
|
-
if (frame.length < 2 || !audioSource || !this._playingVideo) return;
|
|
4005
|
-
try {
|
|
4006
|
-
const result = decoder.decodeFrame(frame);
|
|
4007
|
-
if (!result?.channelData?.[0]?.length) return;
|
|
4008
|
-
const int16 = floatToInt16(result.channelData[0]);
|
|
4009
|
-
const newBuffer = new Int16Array(sampleBuffer.length + int16.length);
|
|
4010
|
-
newBuffer.set(sampleBuffer);
|
|
4011
|
-
newBuffer.set(int16, sampleBuffer.length);
|
|
4012
|
-
sampleBuffer = newBuffer;
|
|
4013
|
-
while (sampleBuffer.length >= FRAME_SAMPLES && this._playingVideo && audioSource) {
|
|
4014
|
-
const outSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
4015
|
-
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
4016
|
-
const audioFrame = new import_rtc_node.AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
4017
|
-
if (audioSource.queuedDuration > 500) await audioSource.waitForPlayout();
|
|
4018
|
-
await audioSource.captureFrame(audioFrame);
|
|
4019
|
-
}
|
|
4020
|
-
} catch {
|
|
4021
|
-
}
|
|
4022
|
-
};
|
|
4023
|
-
const drainQueue = async () => {
|
|
4024
|
-
if (processing || opusFrameQueue.length === 0) return;
|
|
4025
|
-
processing = true;
|
|
4026
|
-
while (opusFrameQueue.length > 0 && this._playingVideo && audioSource) {
|
|
4027
|
-
const f = opusFrameQueue.shift();
|
|
4028
|
-
await processOneOpusFrame(f);
|
|
4029
|
-
}
|
|
4030
|
-
processing = false;
|
|
4031
|
-
};
|
|
4032
|
-
demuxer.on("data", (chunk) => {
|
|
4033
|
-
if (!this._playingVideo) return;
|
|
4034
|
-
opusBuffer = new Uint8Array(concatUint8Arrays(opusBuffer, new Uint8Array(chunk)));
|
|
4035
|
-
while (opusBuffer.length > 0) {
|
|
4036
|
-
const parsed = parseOpusPacketBoundaries(opusBuffer);
|
|
4037
|
-
if (!parsed) break;
|
|
4038
|
-
opusBuffer = new Uint8Array(opusBuffer.subarray(parsed.consumed));
|
|
4039
|
-
for (const frame of parsed.frames) opusFrameQueue.push(frame);
|
|
4040
|
-
}
|
|
4041
|
-
drainQueue().catch(() => {
|
|
4042
|
-
});
|
|
4043
|
-
});
|
|
4044
|
-
audioProc.on("exit", (code) => {
|
|
4045
|
-
if (audioFfmpegProc === audioProc) audioFfmpegProc = null;
|
|
4046
|
-
if (loop && this._playingVideo && !cleanupCalled && (code === 0 || code === null)) {
|
|
4047
|
-
setImmediate(() => runAudioFfmpeg());
|
|
4048
|
-
}
|
|
4049
|
-
});
|
|
4050
|
-
};
|
|
4051
|
-
runAudioFfmpeg().catch(() => {
|
|
4052
|
-
});
|
|
4083
|
+
runFFmpeg().catch((e) => this.audioDebug("ffmpeg error", { error: String(e) }));
|
|
4053
4084
|
}
|
|
4054
4085
|
/**
|
|
4055
4086
|
* Play audio from a WebM/Opus URL or readable stream. Publishes to the LiveKit room as an audio track.
|
|
@@ -4108,8 +4139,9 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
4108
4139
|
newBuffer.set(int16, sampleBuffer.length);
|
|
4109
4140
|
sampleBuffer = newBuffer;
|
|
4110
4141
|
while (sampleBuffer.length >= FRAME_SAMPLES && this._playing && source) {
|
|
4111
|
-
const
|
|
4142
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
4112
4143
|
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
4144
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
4113
4145
|
const audioFrame = new import_rtc_node.AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
4114
4146
|
if (source.queuedDuration > 500) {
|
|
4115
4147
|
await source.waitForPlayout();
|
|
@@ -4164,8 +4196,9 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
4164
4196
|
await new Promise((r) => setImmediate(r));
|
|
4165
4197
|
}
|
|
4166
4198
|
while (sampleBuffer.length >= FRAME_SAMPLES && this._playing && source) {
|
|
4167
|
-
const
|
|
4199
|
+
const rawSamples = sampleBuffer.subarray(0, FRAME_SAMPLES);
|
|
4168
4200
|
sampleBuffer = sampleBuffer.subarray(FRAME_SAMPLES).slice();
|
|
4201
|
+
const outSamples = applyVolumeToInt16(rawSamples, this._volume);
|
|
4169
4202
|
const audioFrame = new import_rtc_node.AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
4170
4203
|
await source.captureFrame(audioFrame);
|
|
4171
4204
|
framesCaptured++;
|
|
@@ -4173,7 +4206,8 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
4173
4206
|
if (sampleBuffer.length > 0 && this._playing && source) {
|
|
4174
4207
|
const padded = new Int16Array(FRAME_SAMPLES);
|
|
4175
4208
|
padded.set(sampleBuffer);
|
|
4176
|
-
const
|
|
4209
|
+
const outSamples = applyVolumeToInt16(padded, this._volume);
|
|
4210
|
+
const audioFrame = new import_rtc_node.AudioFrame(outSamples, SAMPLE_RATE, CHANNELS2, FRAME_SAMPLES);
|
|
4177
4211
|
await source.captureFrame(audioFrame);
|
|
4178
4212
|
framesCaptured++;
|
|
4179
4213
|
}
|
|
@@ -4263,11 +4297,13 @@ var LiveKitRtcConnection = class extends import_events2.EventEmitter {
|
|
|
4263
4297
|
var import_collection = require("@fluxerjs/collection");
|
|
4264
4298
|
var VoiceManager = class extends import_events3.EventEmitter {
|
|
4265
4299
|
client;
|
|
4300
|
+
/** channel_id -> connection (Fluxer multi-channel: allows multiple connections per guild) */
|
|
4266
4301
|
connections = new import_collection.Collection();
|
|
4267
|
-
/**
|
|
4302
|
+
/** channel_id -> connection_id (from VoiceServerUpdate; required for voice state updates) */
|
|
4268
4303
|
connectionIds = /* @__PURE__ */ new Map();
|
|
4269
4304
|
/** guild_id -> user_id -> channel_id */
|
|
4270
4305
|
voiceStates = /* @__PURE__ */ new Map();
|
|
4306
|
+
/** channel_id -> pending join */
|
|
4271
4307
|
pending = /* @__PURE__ */ new Map();
|
|
4272
4308
|
shardId;
|
|
4273
4309
|
constructor(client, options = {}) {
|
|
@@ -4320,31 +4356,43 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4320
4356
|
this.voiceStates.set(guildId, guildMap);
|
|
4321
4357
|
}
|
|
4322
4358
|
guildMap.set(data.user_id, data.channel_id);
|
|
4323
|
-
const
|
|
4359
|
+
const channelKey = data.channel_id ?? guildId;
|
|
4360
|
+
const pendingByChannel = this.pending.get(channelKey);
|
|
4361
|
+
const pendingByGuild = this.pending.get(guildId);
|
|
4362
|
+
const pending = pendingByChannel ?? pendingByGuild;
|
|
4324
4363
|
const isBot = String(data.user_id) === String(this.client.user?.id);
|
|
4325
4364
|
if (isBot && data.connection_id) {
|
|
4326
|
-
this.storeConnectionId(
|
|
4365
|
+
this.storeConnectionId(channelKey, data.connection_id);
|
|
4327
4366
|
}
|
|
4328
4367
|
if (pending && isBot) {
|
|
4329
4368
|
this.client.emit?.(
|
|
4330
4369
|
"debug",
|
|
4331
|
-
`[VoiceManager] VoiceStateUpdate for bot - completing pending
|
|
4370
|
+
`[VoiceManager] VoiceStateUpdate for bot - completing pending channel ${channelKey}`
|
|
4332
4371
|
);
|
|
4333
4372
|
pending.state = data;
|
|
4334
|
-
this.tryCompletePending(guildId);
|
|
4373
|
+
this.tryCompletePending(pendingByChannel ? channelKey : guildId, pending);
|
|
4335
4374
|
}
|
|
4336
4375
|
}
|
|
4337
4376
|
handleVoiceServerUpdate(data) {
|
|
4338
4377
|
const guildId = data.guild_id;
|
|
4339
|
-
|
|
4378
|
+
let pending = this.pending.get(guildId);
|
|
4379
|
+
if (!pending) {
|
|
4380
|
+
for (const [, p] of this.pending) {
|
|
4381
|
+
if (p.channel?.guildId === guildId) {
|
|
4382
|
+
pending = p;
|
|
4383
|
+
break;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4340
4387
|
if (pending) {
|
|
4388
|
+
const channelKey = pending.channel?.id ?? guildId;
|
|
4341
4389
|
const hasToken = !!(data.token && data.token.length > 0);
|
|
4342
4390
|
this.client.emit?.(
|
|
4343
4391
|
"debug",
|
|
4344
|
-
`[VoiceManager] VoiceServerUpdate guild=${guildId} endpoint=${data.endpoint ?? "null"} token=${hasToken ? "yes" : "NO"}`
|
|
4392
|
+
`[VoiceManager] VoiceServerUpdate guild=${guildId} channel=${channelKey} endpoint=${data.endpoint ?? "null"} token=${hasToken ? "yes" : "NO"}`
|
|
4345
4393
|
);
|
|
4346
4394
|
pending.server = data;
|
|
4347
|
-
this.tryCompletePending(
|
|
4395
|
+
this.tryCompletePending(channelKey, pending);
|
|
4348
4396
|
return;
|
|
4349
4397
|
}
|
|
4350
4398
|
const userId = this.client.user?.id;
|
|
@@ -4355,15 +4403,21 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4355
4403
|
);
|
|
4356
4404
|
return;
|
|
4357
4405
|
}
|
|
4358
|
-
|
|
4406
|
+
let conn;
|
|
4407
|
+
for (const [, c] of this.connections) {
|
|
4408
|
+
if (c?.channel?.guildId === guildId) {
|
|
4409
|
+
conn = c;
|
|
4410
|
+
break;
|
|
4411
|
+
}
|
|
4412
|
+
}
|
|
4359
4413
|
if (!conn) return;
|
|
4360
4414
|
if (!data.endpoint || !data.token) {
|
|
4361
4415
|
this.client.emit?.(
|
|
4362
4416
|
"debug",
|
|
4363
|
-
`[VoiceManager] Voice server endpoint null for guild ${guildId}; disconnecting
|
|
4417
|
+
`[VoiceManager] Voice server endpoint null for guild ${guildId}; disconnecting`
|
|
4364
4418
|
);
|
|
4365
4419
|
conn.destroy();
|
|
4366
|
-
this.connections.delete(
|
|
4420
|
+
this.connections.delete(conn.channel.id);
|
|
4367
4421
|
return;
|
|
4368
4422
|
}
|
|
4369
4423
|
if (!isLiveKitEndpoint(data.endpoint, data.token)) return;
|
|
@@ -4373,14 +4427,15 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4373
4427
|
const channel = conn.channel;
|
|
4374
4428
|
this.client.emit?.(
|
|
4375
4429
|
"debug",
|
|
4376
|
-
`[VoiceManager] Voice server migration for guild ${guildId}; reconnecting`
|
|
4430
|
+
`[VoiceManager] Voice server migration for guild ${guildId} channel ${channel.id}; reconnecting`
|
|
4377
4431
|
);
|
|
4378
4432
|
conn.destroy();
|
|
4379
|
-
this.connections.delete(
|
|
4380
|
-
this.
|
|
4433
|
+
this.connections.delete(channel.id);
|
|
4434
|
+
this.connectionIds.delete(channel.id);
|
|
4435
|
+
this.storeConnectionId(channel.id, data.connection_id);
|
|
4381
4436
|
const ConnClass = LiveKitRtcConnection;
|
|
4382
4437
|
const newConn = new ConnClass(this.client, channel, userId);
|
|
4383
|
-
this.registerConnection(
|
|
4438
|
+
this.registerConnection(channel.id, newConn);
|
|
4384
4439
|
const state = {
|
|
4385
4440
|
guild_id: guildId,
|
|
4386
4441
|
channel_id: channel.id,
|
|
@@ -4388,42 +4443,43 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4388
4443
|
session_id: ""
|
|
4389
4444
|
};
|
|
4390
4445
|
newConn.connect(data, state).catch((e) => {
|
|
4391
|
-
this.connections.delete(
|
|
4446
|
+
this.connections.delete(channel.id);
|
|
4392
4447
|
newConn.emit("error", e instanceof Error ? e : new Error(String(e)));
|
|
4393
4448
|
});
|
|
4394
4449
|
}
|
|
4395
|
-
storeConnectionId(
|
|
4450
|
+
storeConnectionId(channelId, connectionId) {
|
|
4396
4451
|
const id = connectionId != null ? String(connectionId) : null;
|
|
4397
|
-
if (id) this.connectionIds.set(
|
|
4398
|
-
else this.connectionIds.delete(
|
|
4452
|
+
if (id) this.connectionIds.set(channelId, id);
|
|
4453
|
+
else this.connectionIds.delete(channelId);
|
|
4399
4454
|
}
|
|
4400
|
-
registerConnection(
|
|
4401
|
-
|
|
4455
|
+
registerConnection(channelId, conn) {
|
|
4456
|
+
const cid = conn.channel?.id ?? channelId;
|
|
4457
|
+
this.connections.set(cid, conn);
|
|
4402
4458
|
conn.once("disconnect", () => {
|
|
4403
|
-
this.connections.delete(
|
|
4404
|
-
this.connectionIds.delete(
|
|
4459
|
+
this.connections.delete(cid);
|
|
4460
|
+
this.connectionIds.delete(cid);
|
|
4405
4461
|
});
|
|
4406
4462
|
conn.on("requestVoiceStateSync", (p) => {
|
|
4407
|
-
this.updateVoiceState(
|
|
4463
|
+
this.updateVoiceState(cid, p);
|
|
4408
4464
|
if (p.self_stream) {
|
|
4409
|
-
this.uploadStreamPreview(
|
|
4465
|
+
this.uploadStreamPreview(cid, conn).catch(
|
|
4410
4466
|
(e) => this.client.emit?.("debug", `[VoiceManager] Stream preview upload failed: ${String(e)}`)
|
|
4411
4467
|
);
|
|
4412
4468
|
}
|
|
4413
4469
|
});
|
|
4414
4470
|
}
|
|
4415
4471
|
/** Upload a placeholder stream preview so the preview URL returns 200 instead of 404. */
|
|
4416
|
-
async uploadStreamPreview(
|
|
4417
|
-
const
|
|
4472
|
+
async uploadStreamPreview(channelId, conn) {
|
|
4473
|
+
const cid = conn.channel?.id ?? channelId;
|
|
4474
|
+
const connectionId = this.connectionIds.get(cid);
|
|
4418
4475
|
if (!connectionId) return;
|
|
4419
|
-
const streamKey = `${guildId}:${conn.channel.id}:${connectionId}`;
|
|
4476
|
+
const streamKey = `${conn.channel.guildId}:${conn.channel.id}:${connectionId}`;
|
|
4420
4477
|
const route = import_types.Routes.streamPreview(streamKey);
|
|
4421
4478
|
const body = { channel_id: conn.channel.id, thumbnail, content_type: "image/png" };
|
|
4422
4479
|
await this.client.rest.post(route, { body, auth: true });
|
|
4423
4480
|
this.client.emit?.("debug", `[VoiceManager] Uploaded stream preview for ${streamKey}`);
|
|
4424
4481
|
}
|
|
4425
|
-
tryCompletePending(
|
|
4426
|
-
const pending = this.pending.get(guildId);
|
|
4482
|
+
tryCompletePending(channelId, pending) {
|
|
4427
4483
|
if (!pending?.server) return;
|
|
4428
4484
|
const useLiveKit = isLiveKitEndpoint(pending.server.endpoint, pending.server.token);
|
|
4429
4485
|
const hasState = !!pending.state;
|
|
@@ -4442,6 +4498,7 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4442
4498
|
);
|
|
4443
4499
|
return;
|
|
4444
4500
|
}
|
|
4501
|
+
const guildId = pending.channel?.guildId ?? "";
|
|
4445
4502
|
const state = pending.state ?? {
|
|
4446
4503
|
guild_id: guildId,
|
|
4447
4504
|
channel_id: pending.channel.id,
|
|
@@ -4449,13 +4506,13 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4449
4506
|
session_id: ""
|
|
4450
4507
|
};
|
|
4451
4508
|
this.storeConnectionId(
|
|
4452
|
-
|
|
4509
|
+
channelId,
|
|
4453
4510
|
pending.server.connection_id ?? state.connection_id
|
|
4454
4511
|
);
|
|
4455
|
-
this.pending.delete(
|
|
4512
|
+
this.pending.delete(channelId);
|
|
4456
4513
|
const ConnClass = useLiveKit ? LiveKitRtcConnection : VoiceConnection;
|
|
4457
4514
|
const conn = new ConnClass(this.client, pending.channel, userId);
|
|
4458
|
-
this.registerConnection(
|
|
4515
|
+
this.registerConnection(channelId, conn);
|
|
4459
4516
|
conn.connect(pending.server, state).then(
|
|
4460
4517
|
() => pending.resolve(conn),
|
|
4461
4518
|
(e) => pending.reject(e)
|
|
@@ -4463,25 +4520,27 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4463
4520
|
}
|
|
4464
4521
|
/**
|
|
4465
4522
|
* Join a voice channel. Resolves when the connection is ready.
|
|
4523
|
+
* Supports multiple connections per guild (Fluxer multi-channel).
|
|
4466
4524
|
* @param channel - The voice channel to join
|
|
4467
4525
|
* @returns The voice connection (LiveKitRtcConnection when Fluxer uses LiveKit)
|
|
4468
4526
|
*/
|
|
4469
4527
|
async join(channel) {
|
|
4470
|
-
const
|
|
4528
|
+
const channelId = channel.id;
|
|
4529
|
+
const existing = this.connections.get(channelId);
|
|
4471
4530
|
if (existing) {
|
|
4472
|
-
const isReusable = existing
|
|
4531
|
+
const isReusable = existing instanceof LiveKitRtcConnection ? existing.isConnected() : true;
|
|
4473
4532
|
if (isReusable) return existing;
|
|
4474
4533
|
existing.destroy();
|
|
4475
|
-
this.connections.delete(
|
|
4534
|
+
this.connections.delete(channelId);
|
|
4476
4535
|
}
|
|
4477
4536
|
return new Promise((resolve, reject) => {
|
|
4478
4537
|
this.client.emit?.(
|
|
4479
4538
|
"debug",
|
|
4480
|
-
`[VoiceManager] Requesting voice join guild=${channel.guildId} channel=${
|
|
4539
|
+
`[VoiceManager] Requesting voice join guild=${channel.guildId} channel=${channelId}`
|
|
4481
4540
|
);
|
|
4482
4541
|
const timeout = setTimeout(() => {
|
|
4483
|
-
if (this.pending.has(
|
|
4484
|
-
this.pending.delete(
|
|
4542
|
+
if (this.pending.has(channelId)) {
|
|
4543
|
+
this.pending.delete(channelId);
|
|
4485
4544
|
reject(
|
|
4486
4545
|
new Error(
|
|
4487
4546
|
"Voice connection timeout. Ensure the server has voice enabled and the bot has Connect permissions. The gateway must send VoiceServerUpdate and VoiceStateUpdate in response."
|
|
@@ -4489,7 +4548,7 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4489
4548
|
);
|
|
4490
4549
|
}
|
|
4491
4550
|
}, 2e4);
|
|
4492
|
-
this.pending.set(
|
|
4551
|
+
this.pending.set(channelId, {
|
|
4493
4552
|
channel,
|
|
4494
4553
|
resolve: (c) => {
|
|
4495
4554
|
clearTimeout(timeout);
|
|
@@ -4512,56 +4571,92 @@ var VoiceManager = class extends import_events3.EventEmitter {
|
|
|
4512
4571
|
});
|
|
4513
4572
|
}
|
|
4514
4573
|
/**
|
|
4515
|
-
* Leave
|
|
4574
|
+
* Leave all voice channels in a guild.
|
|
4575
|
+
* With multi-channel support, disconnects from every channel in the guild.
|
|
4516
4576
|
* @param guildId - Guild ID to leave
|
|
4517
4577
|
*/
|
|
4518
4578
|
leave(guildId) {
|
|
4519
|
-
const
|
|
4520
|
-
|
|
4579
|
+
const toLeave = [];
|
|
4580
|
+
for (const [cid, c] of this.connections) {
|
|
4581
|
+
if (c?.channel?.guildId === guildId) toLeave.push({ channelId: cid, conn: c });
|
|
4582
|
+
}
|
|
4583
|
+
for (const { channelId, conn } of toLeave) {
|
|
4521
4584
|
conn.destroy();
|
|
4522
|
-
this.connections.delete(
|
|
4523
|
-
this.connectionIds.delete(
|
|
4585
|
+
this.connections.delete(channelId);
|
|
4586
|
+
this.connectionIds.delete(channelId);
|
|
4524
4587
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4588
|
+
if (toLeave.length > 0) {
|
|
4589
|
+
this.client.sendToGateway(this.shardId, {
|
|
4590
|
+
op: import_types.GatewayOpcodes.VoiceStateUpdate,
|
|
4591
|
+
d: {
|
|
4592
|
+
guild_id: guildId,
|
|
4593
|
+
channel_id: null,
|
|
4594
|
+
self_mute: false,
|
|
4595
|
+
self_deaf: false
|
|
4596
|
+
}
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
/**
|
|
4601
|
+
* Leave a specific voice channel by channel ID.
|
|
4602
|
+
* @param channelId - Channel ID to leave
|
|
4603
|
+
*/
|
|
4604
|
+
leaveChannel(channelId) {
|
|
4605
|
+
const conn = this.connections.get(channelId);
|
|
4606
|
+
if (conn) {
|
|
4607
|
+
const guildId = conn.channel?.guildId;
|
|
4608
|
+
conn.destroy();
|
|
4609
|
+
this.connections.delete(channelId);
|
|
4610
|
+
this.connectionIds.delete(channelId);
|
|
4611
|
+
if (guildId) {
|
|
4612
|
+
this.client.sendToGateway(this.shardId, {
|
|
4613
|
+
op: import_types.GatewayOpcodes.VoiceStateUpdate,
|
|
4614
|
+
d: {
|
|
4615
|
+
guild_id: guildId,
|
|
4616
|
+
channel_id: null,
|
|
4617
|
+
self_mute: false,
|
|
4618
|
+
self_deaf: false
|
|
4619
|
+
}
|
|
4620
|
+
});
|
|
4532
4621
|
}
|
|
4533
|
-
}
|
|
4622
|
+
}
|
|
4534
4623
|
}
|
|
4535
4624
|
/**
|
|
4536
|
-
* Get the active voice connection for a
|
|
4537
|
-
* @param
|
|
4625
|
+
* Get the active voice connection for a channel or guild.
|
|
4626
|
+
* @param channelOrGuildId - Channel ID (primary) or guild ID (returns first connection in that guild)
|
|
4538
4627
|
*/
|
|
4539
|
-
getConnection(
|
|
4540
|
-
|
|
4628
|
+
getConnection(channelOrGuildId) {
|
|
4629
|
+
const byChannel = this.connections.get(channelOrGuildId);
|
|
4630
|
+
if (byChannel) return byChannel;
|
|
4631
|
+
for (const [, c] of this.connections) {
|
|
4632
|
+
if (c?.channel?.guildId === channelOrGuildId) return c;
|
|
4633
|
+
}
|
|
4634
|
+
return void 0;
|
|
4541
4635
|
}
|
|
4542
4636
|
/**
|
|
4543
4637
|
* Update voice state (e.g. self_stream, self_video) while in a channel.
|
|
4544
4638
|
* Sends a VoiceStateUpdate to the gateway so the server and clients see the change.
|
|
4545
4639
|
* Requires connection_id (from VoiceServerUpdate); without it, the gateway would treat
|
|
4546
4640
|
* the update as a new join and trigger a new VoiceServerUpdate, causing connection loops.
|
|
4547
|
-
* @param
|
|
4641
|
+
* @param channelId - Channel ID (connection key)
|
|
4548
4642
|
* @param partial - Partial voice state to update (self_stream, self_video, self_mute, self_deaf)
|
|
4549
4643
|
*/
|
|
4550
|
-
updateVoiceState(
|
|
4551
|
-
const conn = this.connections.get(
|
|
4644
|
+
updateVoiceState(channelId, partial) {
|
|
4645
|
+
const conn = this.connections.get(channelId);
|
|
4552
4646
|
if (!conn) return;
|
|
4553
|
-
const connectionId = this.connectionIds.get(
|
|
4647
|
+
const connectionId = this.connectionIds.get(channelId);
|
|
4648
|
+
const guildId = conn.channel?.guildId;
|
|
4554
4649
|
if (!connectionId) {
|
|
4555
4650
|
this.client.emit?.(
|
|
4556
4651
|
"debug",
|
|
4557
|
-
`[VoiceManager] Skipping voice state sync: no connection_id for
|
|
4652
|
+
`[VoiceManager] Skipping voice state sync: no connection_id for channel ${channelId}`
|
|
4558
4653
|
);
|
|
4559
4654
|
return;
|
|
4560
4655
|
}
|
|
4561
4656
|
this.client.sendToGateway(this.shardId, {
|
|
4562
4657
|
op: import_types.GatewayOpcodes.VoiceStateUpdate,
|
|
4563
4658
|
d: {
|
|
4564
|
-
guild_id: guildId,
|
|
4659
|
+
guild_id: guildId ?? "",
|
|
4565
4660
|
channel_id: conn.channel.id,
|
|
4566
4661
|
connection_id: connectionId,
|
|
4567
4662
|
self_mute: partial.self_mute ?? false,
|