feat: Add TURN server support
All checks were successful
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 5m35s
Queue Release Build / build-linux (push) Successful in 24m45s
Queue Release Build / build-windows (push) Successful in 13m52s
Queue Release Build / finalize (push) Successful in 23s

This commit is contained in:
2026-04-18 21:27:04 +02:00
parent 167c45ba8d
commit 44588e8789
60 changed files with 2404 additions and 365 deletions

View File

@@ -1,9 +1,6 @@
import { expect, type Page } from '@playwright/test';
import { test, type Client } from '../../fixtures/multi-client';
import {
installTestServerEndpoints,
type SeededEndpointInput
} from '../../helpers/seed-test-endpoint';
import { installTestServerEndpoints, type SeededEndpointInput } from '../../helpers/seed-test-endpoint';
import { startTestServer } from '../../helpers/test-server';
import {
dumpRtcDiagnostics,
@@ -28,11 +25,11 @@ const USER_COUNT = 8;
const EXPECTED_REMOTE_PEERS = USER_COUNT - 1;
const STABILITY_WINDOW_MS = 20_000;
type TestUser = {
interface TestUser {
username: string;
displayName: string;
password: string;
};
}
type TestClient = Client & {
user: TestUser;
@@ -64,7 +61,6 @@ test.describe('Dual-signal multi-user voice', () => {
status: 'online'
}
];
const users = buildUsers();
const clients = await createTrackedClients(createClient, users, endpoints);
@@ -86,12 +82,14 @@ test.describe('Dual-signal multi-user voice', () => {
description: 'Primary signal room for 8-user voice mesh',
sourceId: PRIMARY_SIGNAL_ID
});
await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });
await searchPage.createServer(SECONDARY_ROOM_NAME, {
description: 'Secondary signal room for dual-socket coverage',
sourceId: SECONDARY_SIGNAL_ID
});
await expect(clients[0].page).toHaveURL(/\/room\//, { timeout: 20_000 });
});
@@ -141,7 +139,7 @@ test.describe('Dual-signal multi-user voice', () => {
waitForAudioStatsPresent(client.page, 30_000)
));
// Allow the mesh to settle voice routing, allowed-peer-id
// Allow the mesh to settle - voice routing, allowed-peer-id
// propagation and renegotiation all need time after the last
// user joins.
await clients[0].page.waitForTimeout(5_000);
@@ -173,6 +171,7 @@ test.describe('Dual-signal multi-user voice', () => {
timeout: 10_000,
intervals: [500, 1_000]
}).toBe(EXPECTED_REMOTE_PEERS);
await expect.poll(async () => await getConnectedSignalManagerCount(client.page), {
timeout: 10_000,
intervals: [500, 1_000]
@@ -236,7 +235,7 @@ test.describe('Dual-signal multi-user voice', () => {
await room.deafenButton.click();
await client.page.waitForTimeout(500);
// Un-deafen does NOT restore mute the user stays muted
// Un-deafen does NOT restore mute - the user stays muted
await waitForVoiceStateAcrossPages(clients, client.user.displayName, {
isMuted: true,
isDeafened: false
@@ -245,7 +244,7 @@ test.describe('Dual-signal multi-user voice', () => {
});
await test.step('Unmute all users and verify audio flows end-to-end', async () => {
// Every user is left muted after deafen cycling unmute them all
// Every user is left muted after deafen cycling - unmute them all
for (const client of clients) {
const room = new ChatRoomPage(client.page);
@@ -256,7 +255,7 @@ test.describe('Dual-signal multi-user voice', () => {
});
}
// Final audio flow check on every peer confirms the full
// Final audio flow check on every peer - confirms the full
// send/receive pipeline still works after mute+deafen cycling
for (const client of clients) {
try {
@@ -284,7 +283,7 @@ function buildUsers(): TestUser[] {
async function createTrackedClients(
createClient: () => Promise<Client>,
users: TestUser[],
endpoints: ReadonlyArray<SeededEndpointInput>
endpoints: readonly SeededEndpointInput[]
): Promise<TestClient[]> {
const clients: TestClient[] = [];
@@ -384,9 +383,11 @@ async function waitForCurrentRoomName(page: Page, roomName: string, timeout = 20
}
async function openVoiceWorkspace(page: Page): Promise<void> {
const viewButton = page.locator('app-rooms-side-panel').getByRole('button', { name: /view|open/i }).first();
const viewButton = page.locator('app-rooms-side-panel').getByRole('button', { name: /view|open/i })
.first();
if (await page.locator('app-voice-workspace').isVisible().catch(() => false)) {
if (await page.locator('app-voice-workspace').isVisible()
.catch(() => false)) {
return;
}
@@ -396,6 +397,7 @@ async function openVoiceWorkspace(page: Page): Promise<void> {
async function joinVoiceChannelUntilConnected(page: Page, channelName: string, attempts = 3): Promise<void> {
const room = new ChatRoomPage(page);
let lastError: unknown;
for (let attempt = 1; attempt <= attempts; attempt++) {
@@ -559,7 +561,7 @@ async function getVoiceJoinDiagnostics(page: Page, channelName: string): Promise
const realtime = component['realtime'] as {
connectionErrorMessage?: () => string | null;
signalingTransportHandler?: {
getConnectedSignalingManagers?: () => Array<{ signalUrl: string }>;
getConnectedSignalingManagers?: () => { signalUrl: string }[];
};
} | undefined;
@@ -596,7 +598,7 @@ async function waitForConnectedSignalManagerCount(page: Page, expectedCount: num
const component = debugApi.getComponent(host);
const realtime = component['realtime'] as {
signalingTransportHandler?: {
getConnectedSignalingManagers?: () => Array<{ signalUrl: string }>;
getConnectedSignalingManagers?: () => { signalUrl: string }[];
};
} | undefined;
const countValue = realtime?.signalingTransportHandler?.getConnectedSignalingManagers?.().length ?? 0;
@@ -624,7 +626,7 @@ async function getConnectedSignalManagerCount(page: Page): Promise<number> {
const component = debugApi.getComponent(host);
const realtime = component['realtime'] as {
signalingTransportHandler?: {
getConnectedSignalingManagers?: () => Array<{ signalUrl: string }>;
getConnectedSignalingManagers?: () => { signalUrl: string }[];
};
} | undefined;
@@ -647,7 +649,7 @@ async function waitForVoiceWorkspaceUserCount(page: Page, expectedCount: number)
}
const component = debugApi.getComponent(host);
const connectedUsers = (component['connectedVoiceUsers'] as (() => Array<unknown>) | undefined)?.() ?? [];
const connectedUsers = (component['connectedVoiceUsers'] as (() => unknown[]) | undefined)?.() ?? [];
return connectedUsers.length === count;
},
@@ -688,7 +690,7 @@ async function waitForVoiceRosterCount(page: Page, channelName: string, expected
return false;
}
const roster = (component['voiceUsersInRoom'] as ((roomId: string) => Array<unknown>) | undefined)?.(channelId) ?? [];
const roster = (component['voiceUsersInRoom'] as ((roomId: string) => unknown[]) | undefined)?.(channelId) ?? [];
return roster.length === expected;
},
@@ -698,7 +700,7 @@ async function waitForVoiceRosterCount(page: Page, channelName: string, expected
}
async function waitForVoiceStateAcrossPages(
clients: ReadonlyArray<TestClient>,
clients: readonly TestClient[],
displayName: string,
expectedState: { isMuted: boolean; isDeafened: boolean }
): Promise<void> {