Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export default tseslint.config(
'src/polls/poll.ts',
'src/routes/uploadStorage.ts',
'src/services/mlsWire.ts',
'src/socket/utils/screenShares.ts',
'src/utils/activityText.ts',
'src/utils/linkResolvers.ts',
'src/utils/profanityFilter.ts',
Expand Down
12 changes: 10 additions & 2 deletions src/socket/handlers/voice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import consola from "consola";
import type { HandlerContext, EventHandlerMap } from "./types";
import { requireAuth, requireOutranks } from "../middleware/auth";
import { syncAllClients, broadcastMemberList } from "../utils/clients";
import { screenSharesFrom } from "../utils/screenShares";
import { checkRateLimit, type RateLimitRule } from "../../utils/rateLimiter";
import { getVoiceSeatLimit } from "../../utils/voiceSeats";
import { insertServerAudit } from "../../db";
Expand Down Expand Up @@ -138,9 +139,11 @@ export function registerVoiceHandlers(ctx: HandlerContext): EventHandlerMap {
return;
}
consola.info(`[ScreenShare] voice:screen:state from=${clientId} enabled=${enabled} videoStreamId=${videoStreamId} audioStreamId=${audioStreamId}`);
const shares = enabled ? screenSharesFrom(payload) : [];
clientsInfo[clientId].screenShareEnabled = enabled;
clientsInfo[clientId].screenShareVideoStreamID = enabled ? videoStreamId : "";
clientsInfo[clientId].screenShareAudioStreamID = enabled ? audioStreamId : "";
clientsInfo[clientId].screenShares = shares;
clientsInfo[clientId].screenShareVideoStreamID = enabled ? (shares[0]?.videoStreamID ?? videoStreamId) : "";
clientsInfo[clientId].screenShareAudioStreamID = enabled ? (shares[0]?.audioStreamID ?? audioStreamId) : "";
syncAllClients(io, clientsInfo);
},

Expand Down Expand Up @@ -232,6 +235,7 @@ export function registerVoiceHandlers(ctx: HandlerContext): EventHandlerMap {
clientsInfo[existingClientId].screenShareEnabled = false;
clientsInfo[existingClientId].screenShareVideoStreamID = "";
clientsInfo[existingClientId].screenShareAudioStreamID = "";
clientsInfo[existingClientId].screenShares = [];
if (sfuClient) sfuClient.untrackUserConnection(serverUserId);
}
}
Expand All @@ -258,6 +262,7 @@ export function registerVoiceHandlers(ctx: HandlerContext): EventHandlerMap {
clientsInfo[clientId].screenShareEnabled = false;
clientsInfo[clientId].screenShareVideoStreamID = "";
clientsInfo[clientId].screenShareAudioStreamID = "";
clientsInfo[clientId].screenShares = [];
if (sfuClient && serverUserId) sfuClient.untrackUserConnection(serverUserId);
}

Expand Down Expand Up @@ -461,6 +466,7 @@ export function registerVoiceHandlers(ctx: HandlerContext): EventHandlerMap {
carried.screenShareEnabled = false;
carried.screenShareVideoStreamID = "";
carried.screenShareAudioStreamID = "";
carried.screenShares = [];
}

consola.success(`[Voice:Step 5] Granting room access: client=${clientId} room=${uniqueRoomId} sfu_urls=${sfuPublicUrls.join(", ")}`);
Expand Down Expand Up @@ -547,6 +553,7 @@ export function registerVoiceHandlers(ctx: HandlerContext): EventHandlerMap {
clientsInfo[clientId].screenShareEnabled = false;
clientsInfo[clientId].screenShareVideoStreamID = "";
clientsInfo[clientId].screenShareAudioStreamID = "";
clientsInfo[clientId].screenShares = [];
}

syncAllClients(io, clientsInfo);
Expand Down Expand Up @@ -676,6 +683,7 @@ export function registerVoiceHandlers(ctx: HandlerContext): EventHandlerMap {
targetClient.screenShareEnabled = false;
targetClient.screenShareVideoStreamID = "";
targetClient.screenShareAudioStreamID = "";
targetClient.screenShares = [];
if (sfuClient) sfuClient.untrackUserConnection(targetUserId);

syncAllClients(io, clientsInfo);
Expand Down
1 change: 1 addition & 0 deletions src/socket/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sfuRoomId, voiceRoomName } from "./utils/voiceRooms";

Check warning on line 1 in src/socket/index.ts

View workflow job for this annotation

GitHub Actions / verify

'sfuRoomId' is defined but never used
import consola from "consola";

import { addressLabel } from "../utils/addressLabel";
Expand Down Expand Up @@ -223,6 +223,7 @@
ci.screenShareEnabled = false;
ci.screenShareVideoStreamID = "";
ci.screenShareAudioStreamID = "";
ci.screenShares = [];
sfuClient.untrackUserConnection(ci.serverUserId);

const sock = io.sockets.sockets.get(sid);
Expand Down
2 changes: 2 additions & 0 deletions src/socket/utils/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function publicClientRecord(client: Clients[string], voiceChannelId: stri
screenShareEnabled: client.screenShareEnabled,
screenShareVideoStreamID: client.screenShareVideoStreamID,
screenShareAudioStreamID: client.screenShareAudioStreamID,
screenShares: client.screenShares ?? [],
};
}

Expand Down Expand Up @@ -161,6 +162,7 @@ export function syncAllClients(io: Server, clientsInfo: Clients) {
screenShareEnabled: client.screenShareEnabled,
screenShareVideoStreamID: client.screenShareVideoStreamID,
screenShareAudioStreamID: client.screenShareAudioStreamID,
screenShares: client.screenShares ?? [],
isServerMuted: client.isServerMuted,
isServerDeafened: client.isServerDeafened,
}))
Expand Down
3 changes: 2 additions & 1 deletion src/socket/utils/publicClientList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { publicClientList, publicClientRecord } from "./clients";
const PUBLIC_FIELDS = [
"cameraEnabled", "cameraStreamID", "hasJoinedChannel", "isAFK", "isConnectedToVoice",
"isDeafened", "isMuted", "isServerDeafened", "isServerMuted", "nickname",
"screenShareAudioStreamID", "screenShareEnabled", "screenShareVideoStreamID",
"screenShareAudioStreamID", "screenShareEnabled", "screenShareVideoStreamID", "screenShares",
"serverUserId", "streamID", "voiceChannelId",
];

Expand All @@ -40,6 +40,7 @@ const everything: Required<Clients[string]> = {
screenShareEnabled: true,
screenShareVideoStreamID: "share-video-a",
screenShareAudioStreamID: "share-audio-a",
screenShares: [{ videoStreamID: "share-video-a", audioStreamID: "share-audio-a" }],
isServerMuted: false,
isServerDeafened: true,
activity: "Playing something",
Expand Down
27 changes: 27 additions & 0 deletions src/socket/utils/screenShares.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";

import { MAX_SCREEN_SHARES, screenSharesFrom } from "./screenShares";

describe("screenSharesFrom", () => {
it("reads the single pair an older app sends", () => {
assert.deepEqual(screenSharesFrom({ enabled: true, videoStreamId: "v", audioStreamId: "a" }), [{ videoStreamID: "v", audioStreamID: "a" }]);
});

it("reads every window from shares", () => {
const shares = [{ videoStreamId: "v1", audioStreamId: "a1" }, { videoStreamId: "v2" }];
assert.deepEqual(screenSharesFrom({ enabled: true, shares }), [
{ videoStreamID: "v1", audioStreamID: "a1" },
{ videoStreamID: "v2", audioStreamID: "" },
]);
});

it("drops what is malformed and stops at the ceiling", () => {
const shares = [null, 5, { videoStreamId: 7 }, { videoStreamId: "x".repeat(200) }, ...Array.from({ length: 20 }, (_, i) => ({ videoStreamId: `v${i}` }))];
const out = screenSharesFrom({ enabled: true, shares });
assert.ok(out.length <= MAX_SCREEN_SHARES);
assert.ok(out.every((s) => s.videoStreamID.startsWith("v")));
assert.deepEqual(screenSharesFrom("yes"), []);
assert.deepEqual(screenSharesFrom({ enabled: true }), []);
});
});
29 changes: 29 additions & 0 deletions src/socket/utils/screenShares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/** Ceiling on windows one client can announce, matching the SFU's `maxScreenShares`. */
export const MAX_SCREEN_SHARES = 8;
const MAX_ID_LENGTH = 128;

export interface ScreenShare {
videoStreamID: string;
audioStreamID: string;
}

const id = (v: unknown): string | null => (typeof v === "string" && v.length <= MAX_ID_LENGTH ? v : null);

/**
* The windows a `voice:screen:state` announces. `shares` from an app that shares several,
* otherwise the single pair older apps send. Anything malformed is dropped, not trusted.
*/
export function screenSharesFrom(payload: unknown): ScreenShare[] {
if (typeof payload !== "object" || payload === null) return [];
const p = payload as { shares?: unknown; videoStreamId?: unknown; audioStreamId?: unknown };
const raw = Array.isArray(p.shares) ? p.shares : [{ videoStreamId: p.videoStreamId, audioStreamId: p.audioStreamId }];
const out: ScreenShare[] = [];
for (const entry of raw.slice(0, MAX_SCREEN_SHARES)) {
if (typeof entry !== "object" || entry === null) continue;
const e = entry as { videoStreamId?: unknown; audioStreamId?: unknown };
const video = id(e.videoStreamId);
if (!video) continue;
out.push({ videoStreamID: video, audioStreamID: id(e.audioStreamId) ?? "" });
}
return out;
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface Clients {
screenShareEnabled: boolean;
screenShareVideoStreamID: string;
screenShareAudioStreamID: string;
/** Every shared window, first one also in the two fields above for older apps (GRYT-1905). */
screenShares?: { videoStreamID: string; audioStreamID: string }[];
isServerMuted: boolean;
isServerDeafened: boolean;
/** On the connection rather than in `users`, like `isAFK`: a stored one would
Expand Down
Loading