Skip to content

feat(stream): parked - #1157

Closed
teetangh wants to merge 7 commits into
devfrom
parked/stream-call-filters
Closed

feat(stream): parked#1157
teetangh wants to merge 7 commits into
devfrom
parked/stream-call-filters

Conversation

@teetangh

Copy link
Copy Markdown
Contributor

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.

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 , 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

teetangh and others added 7 commits August 13, 2026 18:29
…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
@netlify

netlify Bot commented Aug 13, 2026

Copy link
Copy Markdown

Deploy Preview for familiarise failed. Why did it fail? →

Name Link
🔨 Latest commit c1e574a
🔍 Latest deploy log https://app.netlify.com/projects/familiarise/deploys/6a7df8ec8c997100087d2eb7

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0fb59550-2c35-4801-a8e6-ee15af6a3f23

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@teetangh

Copy link
Copy Markdown
Contributor Author

Closing — this branch is parked deliberately and must not merge as-is.

parked/stream-call-filters is the preserved tip of the old #1143 (c1e574a2). It exists so the noise-cancellation and background-blur work is not lost, not as a merge candidate. Merging it into dev would:

  • Reintroduce @stream-io/audio-filters-web to dependencies, which is what broke the Netlify deploy 18 times out of 18 — the server function is already near AWS's non-raisable 250 MB ceiling, and this package's dist/cjs/index.js is webpack-inlined into the SSR chunks.
  • Re-enable a paid, per-participant-minute add-on on a call type whose settings.audio.noise_cancellation.mode is "auto-on" — verified live. In that mode the client turns the filter on by itself once the call reaches JOINED, so registering the provider and starting to bill are the same act.
  • Carry two next.config.mjs / netlify.toml changes that are provable no-ops, which would read like fixes in review while changing nothing.

It is also based on an older dev and would need rebasing before it could be used for anything.

The fixes that were trapped inside #1143 already shipped separately in #1155, which is merged: connection states, setDisconnectionTimeout (the root cause of the 1,417 sessions that never closed), receive-side video quality, the bounded client wait, and the Leave-button fix.

#1158 carries the restoration plan for what remains here, including why the two attempted exclusions do not work, what would actually work (next/dynamic with ssr: false), and the recommendation to put a size budget on the server function first — because at roughly 245 MB, the next dependency of any size reproduces this failure regardless of which feature it belongs to.

The branch stays. Please do not open a PR from it until #1158 is worked.

@teetangh teetangh closed this Aug 13, 2026
@teetangh
teetangh deleted the parked/stream-call-filters branch August 26, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant