feat(stream): parked - #1157
Conversation
…pay for less video The meeting room collapsed every calling state that was not JOINED into one bare spinner. A network blip, an SFU migration, a dead network and a connection the SDK had permanently given up on were the same screen, and the terminal one had no way out of it. Each now says what it is, and only the two states a Call instance cannot recover from — RECONNECTING_FAILED and LEFT — offer a rejoin. That rejoin re-creates the call through /api/meetings/[id]/join, because that route is what grants Stream membership, and restores the mic and camera to what the person had chosen rather than the call type's defaults. `setDisconnectionTimeout` is set to 90s. Stream's default is 0 — "remain in the call until their connection restores or the call is ended" — so a participant who shuts their laptop never emits call.session_participant_left. That is where the 1,417 MeetingSession rows that never closed came from. Receive-side video quality is now a control. Stream bills the aggregated RECEIVED resolution per 1,000 participant-minutes (1080p $3.00 / 720p $1.50 / 480p $0.75 / audio-only $0.30), so this is a 2-5x cost lever as well as the only thing a participant can do about their own downlink. Poor connections are surfaced from participant.connectionQuality — not the CPU-expensive polled stats report — for the local participant only and only at POOR, and pausedTracks finally explains why a tile went black on its own. Background blur needed no install: @stream-io/video-filters-web is already a hard dependency of the video SDK. Noise cancellation adds @stream-io/audio-filters-web and is a PAID per-participant-minute add-on, so it is defaulted OFF by not mounting its provider at all — the call type is `auto-on`, and an always-mounted provider would have billed every consultation on the platform from the day it shipped. Both packages fetch their model and WASM from unpkg.com at runtime unless given a basePath, so both are self-hosted out of node_modules into public/ by the postinstall chain (~32MB, gitignored, version-matched by construction). The CSP comment claiming worker-src was the blocker was wrong on every count: there is no `new Worker` in either bundle, Krisp uses an AudioWorklet (checked under script-src), worker-src falls back to script-src rather than default-src, and 'unsafe-eval' already permits WASM. connect-src was the real blocker and needs nothing now. 'wasm-unsafe-eval' is added as future-proofing and worker-src is pinned explicitly, which is tighter than inheriting. Also restores SpeakingWhileMutedNotification, lost when CallControls was replaced by hand-rolled buttons, and removes four pieces of dead code: an unreachable <RecordCallButton />, two console.log-only effects, and an `isPersonalRoom` flag read from a query parameter nothing in the app ever sets. Part of #1134 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019pbBn6yWAr2DXjfACyocUv
`useGetCallById` returns early while the client is undefined, deliberately — the provider mounts it lazily, so `undefined` is the normal cold-load state and erroring there produced a "Video client not available" flash on every open. But the effect re-runs only on `[client, callId, rejoinKey]`. If the client never arrives — Stream unconfigured, a token fetch that keeps failing, a provider that errored out — none of those change, so `isCallLoading` stayed true and page.tsx rendered MeetingRoomSkeleton with no error, no message and no exit. Someone waiting to be let into a session they paid for watched a placeholder animate. Bounded rather than removed. 45s is sized against the provider, not picked round: StreamProviderImpl retries five times with `min(1000 * 2^n, 30_000)` backoff, a 30-second ladder plus the connect attempts themselves. A shorter bound would fire while it was still legitimately retrying and turn a slow connect into a reported failure. Clears `isCallLoading` as well as setting the error, because page.tsx gates on loading first — an error underneath a true loading flag renders the same skeleton. `__tests__/stream/client-wait-timeout.test.ts` derives the provider's ladder from its source rather than hardcoding 30s, so the two cannot drift apart. Confirmed the assertion fails at a 5s bound rather than passing vacuously. Part of #1134
…1134) The deploy preview on this branch failed seven consecutive times while every required check stayed green, and `next build` reproduced nothing locally. That is because **the build was never the problem** — the failure is at the deploy stage: Failed to create function: invalid parameter for function creation: Invalid AWS Lambda parameters used in this request. Failed to upload file: ___netlify-server-handler `postinstall` copies ~32MB of MediaPipe WASM and Krisp models into `public/` so the browser fetches them from us rather than unpkg. Netlify builds its server handler from Next's standalone output, which copies `public/` wholesale — so all 32MB rode into a Lambda that never serves them, and AWS rejected the upload on size. Excluded on both sides rather than moved out of `public/`: the CDN still serves them at `/mediapipe` and `/nc-models`, which is what the SDK's `basePath` expects, and self-hosting is the point — it keeps a third party off the call path and out of `connect-src`. Netlify has no `excluded_files` key; exclusion is a `!` entry in `included_files`. Checked against their docs rather than assumed. Confirmed this is specific to this branch and not the known Netlify rollout issue: of the last 40 deploys on this site, 25 deploy previews succeeded and 7 failed, and all 7 failures are this branch. Part of #1134
…ublic/ (#1134) The first attempt excluded `public/` and did not work. The resolved Netlify config in the failing deploy showed the exclusion applied, and AWS still rejected the function — so the static assets were never the weight. The weight is node_modules: `@stream-io/audio-filters-web` is 44MB (33MB of it Krisp model data) and `@stream-io/video-filters-web` is 30MB. Next traces both into the server output because `NoiseCancellationGate.tsx` and `CallFiltersProvider.tsx` import them — but both are `"use client"`, and nothing under app/api, lib, actions, jobs or scripts imports either. The server was carrying 74MB it can never execute, and the Lambda is capped at 50MB zipped. Their WASM and models still reach the browser: `postinstall` copies them into `public/` and the CDN serves them, which is the whole point of self-hosting — it keeps unpkg off the call path and out of `connect-src`. The `public/` exclusion stays. 32MB of assets have no business in a Lambda either, and the comments now say plainly that it was not the fix, so the next reader does not mistake it for one. Part of #1134
❌ Deploy Preview for familiarise failed. Why did it fail? →
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
Closing — this branch is parked deliberately and must not merge as-is.
It is also based on an older The fixes that were trapped inside #1143 already shipped separately in #1155, which is merged: connection states, #1158 carries the restoration plan for what remains here, including why the two attempted exclusions do not work, what would actually work ( The branch stays. Please do not open a PR from it until #1158 is worked. |



tell people what the connection is doing, and let them pay for less video
The meeting room collapsed every calling state that was not JOINED into one
bare spinner. A network blip, an SFU migration, a dead network and a connection
the SDK had permanently given up on were the same screen, and the terminal one
had no way out of it. Each now says what it is, and only the two states a Call
instance cannot recover from — RECONNECTING_FAILED and LEFT — offer a rejoin.
That rejoin re-creates the call through /api/meetings/[id]/join, because that
route is what grants Stream membership, and restores the mic and camera to what
the person had chosen rather than the call type's defaults.
setDisconnectionTimeoutis set to 90s. Stream's default is 0 — "remain in thecall until their connection restores or the call is ended" — so a participant
who shuts their laptop never emits call.session_participant_left. That is where
the 1,417 MeetingSession rows that never closed came from.
Receive-side video quality is now a control. Stream bills the aggregated
RECEIVED resolution per 1,000 participant-minutes (1080p $3.00 / 720p $1.50 /
480p $0.75 / audio-only $0.30), so this is a 2-5x cost lever as well as the only
thing a participant can do about their own downlink. Poor connections are
surfaced from participant.connectionQuality — not the CPU-expensive polled
stats report — for the local participant only and only at POOR, and
pausedTracks finally explains why a tile went black on its own.
Background blur needed no install: @stream-io/video-filters-web is already a
hard dependency of the video SDK. Noise cancellation adds
@stream-io/audio-filters-web and is a PAID per-participant-minute add-on, so it
is defaulted OFF by not mounting its provider at all — the call type is
auto-on, and an always-mounted provider would have billed every consultationon the platform from the day it shipped.
Both packages fetch their model and WASM from unpkg.com at runtime unless given
a basePath, so both are self-hosted out of node_modules into public/ by the
postinstall chain (~32MB, gitignored, version-matched by construction). The CSP
comment claiming worker-src was the blocker was wrong on every count: there is
no
new Workerin either bundle, Krisp uses an AudioWorklet (checked underscript-src), worker-src falls back to script-src rather than default-src, and
'unsafe-eval' already permits WASM. connect-src was the real blocker and needs
nothing now. 'wasm-unsafe-eval' is added as future-proofing and worker-src is
pinned explicitly, which is tighter than inheriting.
Also restores SpeakingWhileMutedNotification, lost when CallControls was
replaced by hand-rolled buttons, and removes four pieces of dead code: an
unreachable , two console.log-only effects, and an
isPersonalRoomflag read from a query parameter nothing in the app ever sets.Part of #1134
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_019pbBn6yWAr2DXjfACyocUv