feat(gateway): detect pre-existing streamers on bot voice join

If someone is ALREADY sharing screen / camera on when the bot joins the
channel, no voiceStateUpdate with streaming:true fires for them, so the
bot never sent STREAM_WATCH and missed their video entirely. trackChannel
now scans channel.members and starts a watch for anyone already streaming
(ignoring the bot itself and non-streamers). Idempotent: startStreamWatch
no-ops if a watch already exists. +2 tests (9/9 in videoRecorder).
This commit is contained in:
asepharyana
2026-08-31 15:48:20 +07:00
parent 4405647b33
commit 87f1f8be8d
2 changed files with 55 additions and 0 deletions
@@ -78,6 +78,36 @@ export function setVideoRecorderClient(client: Client | undefined) {
/** Register a channel whose audio recording is active (video should follow). */
export function trackChannel(guildId: string, channel: VoiceChannel) {
watchedChannels.set(guildId, channel);
// When the bot joins a channel that ALREADY has people streaming (screen
// share / camera on BEFORE the bot joined), no voiceStateUpdate with
// streaming:true fires for them. Scan every member now and start watching
// anyone already streaming, so we don't miss a live stream in progress.
scanExistingStreamers(channel);
}
/**
* Scan all members currently in `channel` and start watching anyone whose
* voice state has `streaming` set (screen share / camera already on when the
* bot joined). Fire-and-forget per member — idempotent (startStreamWatch is
* a no-op if a watch already exists for that user).
*/
export function scanExistingStreamers(channel: VoiceChannel): void {
const members = channel.members;
if (!members || members.size === 0) return;
let watched = 0;
for (const [, member] of members) {
const vs = member.voice?.streaming;
if (vs && member.id !== _client?.user?.id) {
startVideoRecording(channel, member.id);
watched++;
}
}
if (watched > 0) {
logger.info(
{ guildId: channel.guild.id, watched },
"Scanned pre-existing streamers on join",
);
}
}
/** Unregister a channel (voice stopped). Tear down any video watches. */
@@ -101,4 +101,29 @@ describe("videoRecorder (stream-watch orchestration)", () => {
listener(null, { id: "u1", guild: { id: "g1" }, channelId: "c1", streaming: false });
expect(stopSpy).toHaveBeenCalledWith("g1", "u1");
});
it("trackChannel scans pre-existing streamers already in the channel (bot join case)", () => {
const client = makeClient() as any;
setVideoRecorderClient(client);
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
const ch = {
id: "c1",
guild: { id: "g1" },
members: new Map([
["u1", { id: "u1", voice: { streaming: true } }], // already streaming
["u2", { id: "u2", voice: { streaming: false } }], // not streaming
["bot1", { id: "bot1", voice: { streaming: true } }], // bot itself
]),
};
trackChannel("g1", ch as any);
// only the already-streaming non-bot member gets watched
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(ch, "u1");
});
it("trackChannel is a no-op when the channel has no members (or none streaming)", () => {
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
trackChannel("g1", makeChannel()); // no members
expect(spy).not.toHaveBeenCalled();
});
});