From 47243e52ec6cabd673e17663a34a45631f055301 Mon Sep 17 00:00:00 2001 From: Adam Lamers Date: Tue, 5 May 2026 15:31:55 -0400 Subject: [PATCH] replace eager signal deletion with scheduled backend cleanup; add Firefox audio fix, SDP quality tuning, and negotiation hardening --- package.json | 3 +- pnpm-lock.yaml | 16 ++ spacetimedb/src/lib.rs | 9 ++ spacetimedb/src/reducers.rs | 45 ++++++ spacetimedb/src/tables.rs | 3 + spacetimedb/src/utils.rs | 14 +- src/SpacetimeProvider.svelte | 143 ++++++++++++------ src/chat/ChatContainer.svelte | 2 +- .../components/settings/RoleSettings.svelte | 4 +- .../webrtc/channel-audio-webrtc.svelte.ts | 118 +++++++++------ .../services/webrtc/local-media.svelte.ts | 67 ++++++-- .../services/webrtc/peer-manager.svelte.ts | 65 +++++++- .../webrtc/screen-sharing-webrtc.svelte.ts | 113 ++++++++------ src/chat/services/webrtc/webrtc.svelte.ts | 2 + src/main.ts | 1 + 15 files changed, 441 insertions(+), 164 deletions(-) diff --git a/package.json b/package.json index 3fc7f94..92d4fdd 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "openpgp": "^6.3.0", "spacetimedb": "^2.1.0", "svelte": "^5.55.1", - "svelte-check": "^4.4.6" + "svelte-check": "^4.4.6", + "webrtc-adapter": "^9.0.5" }, "devDependencies": { "@cloudflare/vite-plugin": "^1.31.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e1014b..b7e6e0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: svelte-check: specifier: ^4.4.6 version: 4.4.6(picomatch@4.0.4)(svelte@5.55.1)(typescript@5.6.3) + webrtc-adapter: + specifier: ^9.0.5 + version: 9.0.5 devDependencies: '@cloudflare/vite-plugin': specifier: ^1.31.0 @@ -1924,6 +1927,9 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} + sdp@3.2.2: + resolution: {integrity: sha512-xZocWwfyp4hkbN4hLWxMjmv2Q8aNa9MhmOZ7L9aCZPT+dZsgRr6wZRrSYE3HTdyk/2pZKPSgqI7ns7Een1xMSA==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -2234,6 +2240,10 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webrtc-adapter@9.0.5: + resolution: {integrity: sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==} + engines: {node: '>=6.0.0', npm: '>=3.10.0'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -3841,6 +3851,8 @@ snapshots: dependencies: xmlchars: 2.2.0 + sdp@3.2.2: {} + semver@7.7.4: {} sharp@0.34.5: @@ -4138,6 +4150,10 @@ snapshots: webidl-conversions@7.0.0: {} + webrtc-adapter@9.0.5: + dependencies: + sdp: 3.2.2 + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 diff --git a/spacetimedb/src/lib.rs b/spacetimedb/src/lib.rs index b806a81..d842248 100644 --- a/spacetimedb/src/lib.rs +++ b/spacetimedb/src/lib.rs @@ -86,6 +86,15 @@ pub fn init(ctx: &ReducerContext) { // Grant access to system user sync_server_access(&ctx.db, system_identity, s.id); } + + // Seed the recurring WebRTC signal cleanup job if it doesn't exist yet + if ctx.db.webrtc_signal_cleanup().iter().next().is_none() { + let first_run = ctx.timestamp + spacetimedb::TimeDuration::from_micros(30_000_000); + ctx.db.webrtc_signal_cleanup().insert(WebRTCSignalCleanup { + scheduled_id: 0, + scheduled_at: spacetimedb::ScheduleAt::Time(first_run), + }); + } } #[spacetimedb::reducer(client_connected)] diff --git a/spacetimedb/src/reducers.rs b/spacetimedb/src/reducers.rs index 175a080..bf87e8c 100644 --- a/spacetimedb/src/reducers.rs +++ b/spacetimedb/src/reducers.rs @@ -1052,6 +1052,51 @@ pub fn send_webrtc_signal( media_type, data, channel_id, + sent: ctx.timestamp, + }); +} + +#[spacetimedb::reducer] +pub fn delete_webrtc_signal(ctx: &ReducerContext, signal_id: u64) { + if let Some(signal) = ctx.db.webrtc_signal().id().find(signal_id) { + // Only allow sender or receiver to delete the signal + if signal.sender == ctx.sender() || signal.receiver == ctx.sender() { + ctx.db.webrtc_signal().id().delete(signal_id); + } + } +} + +#[spacetimedb::table(accessor = webrtc_signal_cleanup, scheduled(run_webrtc_signal_cleanup))] +#[derive(Clone)] +pub struct WebRTCSignalCleanup { + #[primary_key] + #[auto_inc] + pub scheduled_id: u64, + pub scheduled_at: spacetimedb::ScheduleAt, +} + +#[spacetimedb::reducer] +pub fn run_webrtc_signal_cleanup(ctx: &ReducerContext, _cleanup: WebRTCSignalCleanup) { + const CLEANUP_INTERVAL_MICROS: i64 = 30_000_000; // 30 seconds + const SIGNAL_TTL_MICROS: i64 = 60_000_000; // 60 seconds + + let cutoff = ctx.timestamp - spacetimedb::TimeDuration::from_micros(SIGNAL_TTL_MICROS); + + let stale: Vec = ctx.db.webrtc_signal() + .iter() + .filter(|s| s.sent < cutoff) + .map(|s| s.id) + .collect(); + + for id in stale { + ctx.db.webrtc_signal().id().delete(id); + } + + // Re-schedule next cleanup + let next_run = ctx.timestamp + spacetimedb::TimeDuration::from_micros(CLEANUP_INTERVAL_MICROS); + ctx.db.webrtc_signal_cleanup().insert(WebRTCSignalCleanup { + scheduled_id: 0, + scheduled_at: spacetimedb::ScheduleAt::Time(next_run), }); } diff --git a/spacetimedb/src/tables.rs b/spacetimedb/src/tables.rs index 0b7fc8f..220dbb0 100644 --- a/spacetimedb/src/tables.rs +++ b/spacetimedb/src/tables.rs @@ -152,8 +152,11 @@ pub struct WebRTCSignal { pub data: String, #[index(btree)] pub channel_id: u64, + pub sent: Timestamp, } + + #[spacetimedb::table(accessor = channel_subscription)] #[derive(Clone)] pub struct ChannelSubscription { diff --git a/spacetimedb/src/utils.rs b/spacetimedb/src/utils.rs index 59ed4a3..ab7b552 100644 --- a/spacetimedb/src/utils.rs +++ b/spacetimedb/src/utils.rs @@ -415,13 +415,23 @@ pub fn clear_user_presence(db: &Local, identity: Identity) { } pub fn clear_signaling_for_user(db: &Local, identity: Identity) { - let signals: Vec<_> = db + let signals_as_sender: Vec<_> = db .webrtc_signal() .sender() .filter(identity) .map(|s| s.id) .collect(); - for id in signals { + for id in signals_as_sender { + db.webrtc_signal().id().delete(id); + } + + let signals_as_receiver: Vec<_> = db + .webrtc_signal() + .receiver() + .filter(identity) + .map(|s| s.id) + .collect(); + for id in signals_as_receiver { db.webrtc_signal().id().delete(id); } } diff --git a/src/SpacetimeProvider.svelte b/src/SpacetimeProvider.svelte index aacf804..50169fc 100644 --- a/src/SpacetimeProvider.svelte +++ b/src/SpacetimeProvider.svelte @@ -17,53 +17,19 @@ let providerKey = $state(0); $effect(() => { - // Hold off until OIDC is settled for the first time + // Hold off until OIDC is settled if (auth.isLoading) return; const currentToken = auth.user?.id_token; - // 1. Initial creation - if (!builder) { - console.log(`[SpacetimeProvider] Initializing connection builder. OIDC present: ${!!currentToken}`); + // Only (re)create the builder if we don't have one, or if the token changed. + if (!builder || currentToken !== lastUsedOidcToken) { + console.log(`[SpacetimeProvider] Initializing connection (Auth Settled). OIDC present: ${!!currentToken}`); + untrack(() => { builder = connectionBuilder(currentToken); lastUsedOidcToken = currentToken; - providerKey += 1; - }); - return; - } - - // 2. Identity transition (Logged out -> Logged in) - // If we were a guest (or null) and now have a token, we SHOULD remount - // to ensure the OIDC credentials take over completely. - if (currentToken && !lastUsedOidcToken) { - console.log("[SpacetimeProvider] Transitioning from Guest/None to OIDC session. Remounting..."); - untrack(() => { - builder = connectionBuilder(currentToken); - lastUsedOidcToken = currentToken; - providerKey += 1; - }); - return; - } - - // 3. Background Refresh (Token -> New Token) - // If it's just a refresh, we DON'T remount. We let InnerSpacetimeDBProvider - // handle the in-place upgrade via withToken. - if (currentToken && currentToken !== lastUsedOidcToken) { - console.log("[SpacetimeProvider] Background token refresh detected. Upgrading in-place."); - untrack(() => { - lastUsedOidcToken = currentToken; - // Notice we DON'T increment providerKey here - }); - } - - // 4. Logout (Token -> Null) - if (!currentToken && lastUsedOidcToken) { - console.log("[SpacetimeProvider] User logged out. Remounting for Guest mode."); - untrack(() => { - builder = connectionBuilder(undefined); - lastUsedOidcToken = undefined; - providerKey += 1; + providerKey += 1; // Force re-mount of InnerSpacetimeDBProvider for a clean handshake }); } }); @@ -71,17 +37,36 @@ // Reactive labels for the loading screen const host = getStdbHost(); const dbName = getStdbDbName(); + + // Unified status message logic + const statusInfo = $derived.by(() => { + if (auth.isLoading) { + return { + title: "Authenticating...", + icon: "fa-id-card", + message: "Verifying your identity with the provider." + }; + } + + if (!builder) { + return { + title: "Preparing Handshake...", + icon: "fa-key", + message: "Finalizing security credentials." + }; + } + + // Default connecting state + return { + title: "Connecting to SpacetimeDB...", + icon: "fa-circle-notch", + spin: true, + message: "Establishing a secure connection to the chat server." + }; + }); -{#if !builder || (auth.isLoading && !auth.user)} - -{:else} +{#if builder && !auth.isLoading} {#key providerKey} {/key} +{:else} + {/if} diff --git a/src/chat/ChatContainer.svelte b/src/chat/ChatContainer.svelte index 7457685..e1fc0a6 100644 --- a/src/chat/ChatContainer.svelte +++ b/src/chat/ChatContainer.svelte @@ -1,7 +1,7 @@