From 87f1f8be8d269d61423a56be54268a847091f118 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 31 Aug 2026 15:48:20 +0700 Subject: [PATCH] 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). --- .../modules/voice-recording/videoRecorder.ts | 30 +++++++++++++++++++ .../tests/videoRecorder.test.ts | 25 ++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts b/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts index bb94b49b..3f310d83 100644 --- a/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts +++ b/services/discord-gateway/src/modules/voice-recording/videoRecorder.ts @@ -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. */ diff --git a/services/discord-gateway/tests/videoRecorder.test.ts b/services/discord-gateway/tests/videoRecorder.test.ts index 6ec0d3ce..b130f758 100644 --- a/services/discord-gateway/tests/videoRecorder.test.ts +++ b/services/discord-gateway/tests/videoRecorder.test.ts @@ -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(); + }); });