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.
This commit is contained in:
@@ -385,12 +385,12 @@ function decryptVideoPacket(
|
|||||||
encryptionMode?: string;
|
encryptionMode?: string;
|
||||||
nonceBuffer?: Buffer;
|
nonceBuffer?: Buffer;
|
||||||
secretKey?: Buffer | Uint8Array;
|
secretKey?: Buffer | Uint8Array;
|
||||||
dave?: { session?: Davey.DAVESession };
|
|
||||||
},
|
},
|
||||||
|
daveSession: { session?: Davey.DAVESession } | undefined,
|
||||||
userId: string,
|
userId: string,
|
||||||
): Buffer | null {
|
): Buffer | null {
|
||||||
const { encryptionMode, nonceBuffer, secretKey, dave } = connectionData;
|
const { encryptionMode, nonceBuffer, secretKey } = connectionData;
|
||||||
if (!encryptionMode || !nonceBuffer || !secretKey || !dave?.session)
|
if (!encryptionMode || !nonceBuffer || !secretKey || !daveSession?.session)
|
||||||
return null;
|
return null;
|
||||||
if (buffer.length < AUTH_TAG_LENGTH + UNPADDED_NONCE_LENGTH + 12) 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).
|
// DAVE layer — decrypt as VIDEO (audio wrapper hardcodes MediaType.AUDIO).
|
||||||
try {
|
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;
|
return decrypted && decrypted.length > 0 ? decrypted : null;
|
||||||
} catch {
|
} catch {
|
||||||
return null; // per-packet DAVE decrypt failures are transient (recovery on next keyframe)
|
return null; // per-packet DAVE decrypt failures are transient (recovery on next keyframe)
|
||||||
@@ -433,8 +437,8 @@ function handleUdpMessage(
|
|||||||
encryptionMode?: string;
|
encryptionMode?: string;
|
||||||
nonceBuffer?: Buffer;
|
nonceBuffer?: Buffer;
|
||||||
secretKey?: Buffer | Uint8Array;
|
secretKey?: Buffer | Uint8Array;
|
||||||
dave?: { session?: Davey.DAVESession };
|
|
||||||
};
|
};
|
||||||
|
dave?: { session?: Davey.DAVESession };
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
uid: string,
|
uid: string,
|
||||||
@@ -449,6 +453,10 @@ function handleUdpMessage(
|
|||||||
const decrypted = decryptVideoPacket(
|
const decrypted = decryptVideoPacket(
|
||||||
msg,
|
msg,
|
||||||
net.state.connectionData ?? {},
|
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,
|
uid,
|
||||||
);
|
);
|
||||||
if (!decrypted) return;
|
if (!decrypted) return;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js";
|
||||||
import {
|
import {
|
||||||
__resetVideoRecorderState,
|
__resetVideoRecorderState,
|
||||||
setVideoRecorderClient,
|
setVideoRecorderClient,
|
||||||
@@ -8,7 +9,6 @@ import {
|
|||||||
trackChannel,
|
trackChannel,
|
||||||
untrackChannel,
|
untrackChannel,
|
||||||
} from "../src/modules/voice-recording/videoRecorder.js";
|
} from "../src/modules/voice-recording/videoRecorder.js";
|
||||||
import * as streamWatch from "../src/modules/voice-recording/streamWatchReceiver.js";
|
|
||||||
|
|
||||||
// ─── mocks ─────────────────────────────────────────────────────────────
|
// ─── mocks ─────────────────────────────────────────────────────────────
|
||||||
function makeChannel(guildId = "g1", channelId = "c1") {
|
function makeChannel(guildId = "g1", channelId = "c1") {
|
||||||
@@ -52,7 +52,9 @@ describe("videoRecorder (stream-watch orchestration)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("startVideoRecording delegates to streamWatchReceiver.startStreamWatch", () => {
|
it("startVideoRecording delegates to streamWatchReceiver.startStreamWatch", () => {
|
||||||
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
const spy = vi
|
||||||
|
.spyOn(streamWatch, "startStreamWatch")
|
||||||
|
.mockImplementation(() => {});
|
||||||
const ch = makeChannel();
|
const ch = makeChannel();
|
||||||
startVideoRecording(ch, makeUser());
|
startVideoRecording(ch, makeUser());
|
||||||
expect(spy).toHaveBeenCalledWith(ch, expect.any(String));
|
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", () => {
|
it("refuses to record the bot's own video", () => {
|
||||||
setVideoRecorderClient(makeClient());
|
setVideoRecorderClient(makeClient());
|
||||||
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
const spy = vi
|
||||||
|
.spyOn(streamWatch, "startStreamWatch")
|
||||||
|
.mockImplementation(() => {});
|
||||||
startVideoRecording(makeChannel(), "bot1");
|
startVideoRecording(makeChannel(), "bot1");
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stopVideoRecording calls streamWatchReceiver.stopStreamWatch", () => {
|
it("stopVideoRecording calls streamWatchReceiver.stopStreamWatch", () => {
|
||||||
const spy = vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {});
|
const spy = vi
|
||||||
|
.spyOn(streamWatch, "stopStreamWatch")
|
||||||
|
.mockImplementation(() => {});
|
||||||
const u = makeUser();
|
const u = makeUser();
|
||||||
stopVideoRecording("g1", u);
|
stopVideoRecording("g1", u);
|
||||||
expect(spy).toHaveBeenCalledWith("g1", u);
|
expect(spy).toHaveBeenCalledWith("g1", u);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("untrackChannel tears down all stream watches for the guild", () => {
|
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");
|
untrackChannel("g1");
|
||||||
expect(spy).toHaveBeenCalledWith("g1");
|
expect(spy).toHaveBeenCalledWith("g1");
|
||||||
});
|
});
|
||||||
@@ -82,11 +90,18 @@ describe("videoRecorder (stream-watch orchestration)", () => {
|
|||||||
const client = makeClient() as any;
|
const client = makeClient() as any;
|
||||||
setVideoRecorderClient(client);
|
setVideoRecorderClient(client);
|
||||||
trackChannel("g1", makeChannel());
|
trackChannel("g1", makeChannel());
|
||||||
const startSpy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
const startSpy = vi
|
||||||
|
.spyOn(streamWatch, "startStreamWatch")
|
||||||
|
.mockImplementation(() => {});
|
||||||
const listener = client.on.mock.calls.find(
|
const listener = client.on.mock.calls.find(
|
||||||
(c: unknown[]) => c[0] === "voiceStateUpdate",
|
(c: unknown[]) => c[0] === "voiceStateUpdate",
|
||||||
)?.[1];
|
)?.[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();
|
expect(startSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,18 +109,27 @@ describe("videoRecorder (stream-watch orchestration)", () => {
|
|||||||
const client = makeClient() as any;
|
const client = makeClient() as any;
|
||||||
setVideoRecorderClient(client);
|
setVideoRecorderClient(client);
|
||||||
trackChannel("g1", makeChannel());
|
trackChannel("g1", makeChannel());
|
||||||
const stopSpy = vi.spyOn(streamWatch, "stopStreamWatch").mockImplementation(() => {});
|
const stopSpy = vi
|
||||||
|
.spyOn(streamWatch, "stopStreamWatch")
|
||||||
|
.mockImplementation(() => {});
|
||||||
const listener = client.on.mock.calls.find(
|
const listener = client.on.mock.calls.find(
|
||||||
(c: unknown[]) => c[0] === "voiceStateUpdate",
|
(c: unknown[]) => c[0] === "voiceStateUpdate",
|
||||||
)?.[1];
|
)?.[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");
|
expect(stopSpy).toHaveBeenCalledWith("g1", "u1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("trackChannel scans pre-existing streamers already in the channel (bot join case)", () => {
|
it("trackChannel scans pre-existing streamers already in the channel (bot join case)", () => {
|
||||||
const client = makeClient() as any;
|
const client = makeClient() as any;
|
||||||
setVideoRecorderClient(client);
|
setVideoRecorderClient(client);
|
||||||
const spy = vi.spyOn(streamWatch, "startStreamWatch").mockImplementation(() => {});
|
const spy = vi
|
||||||
|
.spyOn(streamWatch, "startStreamWatch")
|
||||||
|
.mockImplementation(() => {});
|
||||||
const ch = {
|
const ch = {
|
||||||
id: "c1",
|
id: "c1",
|
||||||
guild: { id: "g1" },
|
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)", () => {
|
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
|
trackChannel("g1", makeChannel()); // no members
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user