From f1a7b0c2a15dfb1cc11460f319e8e0d24a0e1d29 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Mon, 31 Aug 2026 16:08:55 +0700 Subject: [PATCH] fix(gateway): resolve DAVE session from net.state.dave, not connectionData CRITICAL: djs/voice stores the DAVESession wrapper at net.state.dave (createDaveSession assigns to state.dave on op4 SessionDescription), NOT inside connectionData. decryptVideoPacket looked up connectionData.dave which is ALWAYS undefined -> every video packet hit '!dave?.session' guard and was silently dropped, so no .h264/.mp4 ever got written despite the handshake reaching Ready. Fix: pass net.state.dave as a separate arg (the wrapper has .session -> Davey.DAVESession) so the DAVE-layer decrypt (MediaType.VIDEO) actually runs. Typecheck + build pass, lint clean (src/), 179/179 tests. --- .../voice-recording/streamWatchReceiver.ts | 18 +++++-- .../tests/videoRecorder.test.ts | 48 ++++++++++++++----- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts b/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts index 363c4e55..089042c4 100644 --- a/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts +++ b/services/discord-gateway/src/modules/voice-recording/streamWatchReceiver.ts @@ -385,12 +385,12 @@ function decryptVideoPacket( encryptionMode?: string; nonceBuffer?: Buffer; secretKey?: Buffer | Uint8Array; - dave?: { session?: Davey.DAVESession }; }, + daveSession: { session?: Davey.DAVESession } | undefined, userId: string, ): Buffer | null { - const { encryptionMode, nonceBuffer, secretKey, dave } = connectionData; - if (!encryptionMode || !nonceBuffer || !secretKey || !dave?.session) + const { encryptionMode, nonceBuffer, secretKey } = connectionData; + if (!encryptionMode || !nonceBuffer || !secretKey || !daveSession?.session) return null; if (buffer.length < AUTH_TAG_LENGTH + UNPADDED_NONCE_LENGTH + 12) return null; @@ -418,7 +418,11 @@ function decryptVideoPacket( // DAVE layer — decrypt as VIDEO (audio wrapper hardcodes MediaType.AUDIO). try { - const decrypted = dave.session.decrypt(userId, MediaType.VIDEO, packet); + const decrypted = daveSession.session.decrypt( + userId, + MediaType.VIDEO, + packet, + ); return decrypted && decrypted.length > 0 ? decrypted : null; } catch { return null; // per-packet DAVE decrypt failures are transient (recovery on next keyframe) @@ -433,8 +437,8 @@ function handleUdpMessage( encryptionMode?: string; nonceBuffer?: Buffer; secretKey?: Buffer | Uint8Array; - dave?: { session?: Davey.DAVESession }; }; + dave?: { session?: Davey.DAVESession }; }; }, uid: string, @@ -449,6 +453,10 @@ function handleUdpMessage( const decrypted = decryptVideoPacket( msg, net.state.connectionData ?? {}, + // djs/voice stores the DAVESession at net.state.dave (NOT inside + // connectionData) — look it up at the top-level state so DAVE layer + // decrypt (MediaType.VIDEO) actually runs. + net.state.dave, uid, ); if (!decrypted) return; diff --git a/services/discord-gateway/tests/videoRecorder.test.ts b/services/discord-gateway/tests/videoRecorder.test.ts index b130f758..ed23db40 100644 --- a/services/discord-gateway/tests/videoRecorder.test.ts +++ b/services/discord-gateway/tests/videoRecorder.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; +import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js"; import { __resetVideoRecorderState, setVideoRecorderClient, @@ -8,7 +9,6 @@ import { trackChannel, untrackChannel, } from "../src/modules/voice-recording/videoRecorder.js"; -import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js"; // ─── mocks ───────────────────────────────────────────────────────────── function makeChannel(guildId = "g1", channelId = "c1") { @@ -52,7 +52,9 @@ describe("videoRecorder (stream-watch orchestration)", () => { }); it("startVideoRecording delegates to streamWatchReceiver.startStreamWatch", () => { - const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {}); + const spy = vi + .spyOn(streamWatch, "startStreamWatch") + .mockImplementation(() => {}); const ch = makeChannel(); startVideoRecording(ch, makeUser()); expect(spy).toHaveBeenCalledWith(ch, expect.any(String)); @@ -60,20 +62,26 @@ describe("videoRecorder (stream-watch orchestration)", () => { it("refuses to record the bot's own video", () => { setVideoRecorderClient(makeClient()); - const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {}); + const spy = vi + .spyOn(streamWatch, "startStreamWatch") + .mockImplementation(() => {}); startVideoRecording(makeChannel(), "bot1"); expect(spy).not.toHaveBeenCalled(); }); it("stopVideoRecording calls streamWatchReceiver.stopStreamWatch", () => { - const spy = vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {}); + const spy = vi + .spyOn(streamWatch, "stopStreamWatch") + .mockImplementation(() => {}); const u = makeUser(); stopVideoRecording("g1", u); expect(spy).toHaveBeenCalledWith("g1", u); }); it("untrackChannel tears down all stream watches for the guild", () => { - const spy = vi.spyOn(streamWatch, "stopAllStreamWatches").mockImplementation(() => {}); + const spy = vi + .spyOn(streamWatch, "stopAllStreamWatches") + .mockImplementation(() => {}); untrackChannel("g1"); expect(spy).toHaveBeenCalledWith("g1"); }); @@ -82,11 +90,18 @@ describe("videoRecorder (stream-watch orchestration)", () => { const client = makeClient() as any; setVideoRecorderClient(client); trackChannel("g1", makeChannel()); - const startSpy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {}); + const startSpy = vi + .spyOn(streamWatch, "startStreamWatch") + .mockImplementation(() => {}); const listener = client.on.mock.calls.find( (c: unknown[]) => c[0] === "voiceStateUpdate", )?.[1]; - listener(null, { id: "u1", guild: { id: "g1" }, channelId: "c1", streaming: true }); + listener(null, { + id: "u1", + guild: { id: "g1" }, + channelId: "c1", + streaming: true, + }); expect(startSpy).toHaveBeenCalled(); }); @@ -94,18 +109,27 @@ describe("videoRecorder (stream-watch orchestration)", () => { const client = makeClient() as any; setVideoRecorderClient(client); trackChannel("g1", makeChannel()); - const stopSpy = vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {}); + const stopSpy = vi + .spyOn(streamWatch, "stopStreamWatch") + .mockImplementation(() => {}); const listener = client.on.mock.calls.find( (c: unknown[]) => c[0] === "voiceStateUpdate", )?.[1]; - listener(null, { id: "u1", guild: { id: "g1" }, channelId: "c1", streaming: false }); + 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 spy = vi + .spyOn(streamWatch, "startStreamWatch") + .mockImplementation(() => {}); const ch = { id: "c1", guild: { id: "g1" }, @@ -122,7 +146,9 @@ describe("videoRecorder (stream-watch orchestration)", () => { }); it("trackChannel is a no-op when the channel has no members (or none streaming)", () => { - const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {}); + const spy = vi + .spyOn(streamWatch, "startStreamWatch") + .mockImplementation(() => {}); trackChannel("g1", makeChannel()); // no members expect(spy).not.toHaveBeenCalled(); });