Only report `Other` `MediaDeviceFailure` cases as they genuinely
need investigation.
Make sure we do not report the same situation both as a media event
and as a media exception when it is already handled.
The screen-share error modal was tailored to macOS and did not work
correctly on other operating systems.
Make it OS-aware so it also handles Windows properly, showing the
right guidance for each platform.
Also open the OS settings link in a new tab, so the user is not
disconnected from the ongoing meeting when following it.
Add a small helper that classifies a `getDisplayMedia` failure as a
user, browser, or OS permission denial, or returns null when it is
a genuine error.
Chromium reports denials with explicit, non-localized messages:
* "Permission denied by user" when the user cancels or dismisses
the source picker.
* "Permission denied by system" when the OS blocks capture (e.g.
the macOS Screen Recording privacy setting).
* Plain "Permission denied" for browser-level blocks (site
settings, enterprise policy, permissions-policy).
Firefox and Safari use generic `NotAllowedError` messages, which
fall into the "browser" bucket.
Firefox additionally does not map macOS Screen Recording (TCC)
blocks to `NotAllowedError`: the OS silently returns no capturable
sources, so `getDisplayMedia` rejects with `NotFoundError` ("The
object can not be found here."). Same quirk as the mic/cam OS blocks
handled in `useWatchMediaDeviceErrors` via `isLikelySystemNotFound`.
Behavior on a denied screen-share permission:
* Denials are expected outcomes (picker cancelled by the user, OS
privacy settings, enterprise policy…) and no longer surface as
exceptions in error tracking; capture an analytics event instead.
* Only OS-level blocks get the modal, since it explains how to
unblock them.
Filter out harmless `ResizeObserver loop limit exceeded` and
`ResizeObserver loop completed with undelivered notifications`
errors via `beforeSend`.
Why this is safe:
* These are W3C spec-mandated browser guards that defer notification
delivery to the next frame when callbacks alter layout during
render. They do not cause JS runtime exceptions or break the UX.
Why we actually need to filter them:
* Telemetry platforms like PostHog do not stack/group these well,
frequently generating distinct error events per browser engine
and version.
* The unique variants flood reporting dashboards and trigger
false-positive alerts that clutter real issue triage.
* Switch toolbar horizontal alignment from `marginRight` to
`transform: translateX()`, so it no longer triggers layout reflows
during ResizeObserver cycles and stops the "ResizeObserver loop"
error.
* Replace the unstable `shift * 2` margin heuristic with a direct
1:1 positional delta (`offsetX + shift`).
* Decouple CSS transitions: use the individual CSS `translate`
property for the slide-up/down animations, leaving `transform`
free for dynamic horizontal positioning.
Copy the `formatChatMessageLinks` function locally so we can iterate
on it without patching the upstream dependency.
Use the local copy to trim `\n` characters at the beginning and end
of chat messages, which were leaking into the rendered output.
Introduce dual thresholds (1100px wide, 1050px narrow) for switching
the control bar between the expanded inline controls and the
collapsed menu.
The 50px deadband absorbs the width changes caused by rendering
5 buttons vs. 1 button, preventing an infinite layout oscillation
and the resulting `ResizeObserver loop` errors.
The vendored ConnectionObserver collected connection data that never
turned out to be useful for debugging.
Remove it to reduce dead code, and re-add a targeted observer later
if a concrete debugging need shows up.
- Only call `setSinkId` when supported and the device is actually
enumerated: LiveKit can fall back to a stale id on browsers (e.g.
WebKit) that expose no such device, making `setSinkId` throw
`NotFoundError`.
- Await `audio.play()` and reset the playing state on failure, to
avoid a stuck button and an unhandled rejection.
- Use an absolute `/sounds/uprise.mp3` URL so the asset resolves
regardless of the current SPA route.
401 responses were not handled by the user preferences sync, which
could leave the app in an inconsistent state when the session had
expired.
Handle the 401 case explicitly and report the error through the
telemetry module so it stays visible without crashing the flow.
The "unreachable external home URL" check was reporting failures as
errors. In practice, it fired a lot for users behind corporate
networks that cannot reach our public landing page, which is
expected behavior and not something to investigate.
Capture it as a regular telemetry event instead of an error, so it
still gives us visibility on the frequency of the case without
polluting error dashboards.
Refactor the hint paragraph markup and semantics to resolve an
accessibility issue flagged on it, so assistive technologies expose
it correctly to users.
Since `ToggleDevice` renders on both the join screen and in the
room, `requestDevicePermission` was reporting in-room denials
through the join-preview handler, inflating the `join_preview_failure`
funnel.
Rename `onJoinPreviewError` to `onMediaPermissionError` and thread
a `path` parameter through, derived from `ToggleDevice`'s existing
`context` prop. In-room failures are now reported under a new
`room_media_failure` code, keeping `join_preview_failure` intact
for existing dashboards.
The Picture-in-Picture error handler was reporting every error to
PostHog, including the ones triggered when the user intentionally
closes or cancels the PiP window.
Only report unexpected errors, so PostHog no longer receives noise
from normal user interactions.
Introduce a watcher that listens to the microphone stream and detects
when it stays silent, which is often a sign of an underlying issue:
missing OS permissions, a faulty device, or a hardware lock (e.g. a
physical mute switch).
Wire the watcher on both the join and room screens, so users get a
signal that something is off before it turns into an actual meeting
problem.
Introduce a new handling flow for the case where the operating
system itself is blocking browser access to the microphone or
camera, rather than the browser's own permission.
Detect the situation and surface guidance to the user, so they know
they need to allow the browser to access their microphone/camera in
the OS settings.
Only a minority of users are impacted, but the failure mode is very
confusing when it happens. Hopefully this reduces the amount of
support requests around it.
Fix a minor issue on the join screen: the page title was missing the
meeting id, even though the hook's documentation stated it should be
included.
Align the actual behavior with the documented one so the meeting id
now shows up in the browser tab title.
Also snapshot the state of media devices when the user successfully
joins a meeting, not only when something goes wrong. This gives us
the baseline needed to compute meaningful ratios — for example, the
share of users who join a meeting without granting permissions, or
without a microphone or camera available.
Without a happy-path measurement, the current error-only data has no
denominator to compare against.
Switch calls to `reportError` over to `captureMediaEvent` when the
underlying situation is not an engineering issue to investigate but
rather a media-related event worth tracking (e.g. no camera or
microphone available on the user's device).
`reportError` stays reserved for actual errors that warrant an
engineer's attention.
Handle the "requested device not found" error surfaced in production
when users arrive without a microphone or camera available on their
computer. Some devices also have a hardware button that physically
locks the microphone and makes it invisible to the browser.
Instead of failing loudly, surface a clearer state to the user so
they can still proceed with whatever device is actually available.
Forward `console.error` calls to PostHog on top of the existing
exception capture.
This is experimental: the goal is to gather more information about
buggy situations that do not surface as thrown exceptions today.
May be reverted or filtered depending on the signal-to-noise ratio.
When a media exception is raised on the join screen, include the
kind of media involved (microphone or camera) in the tracking event,
so we can tell which device is actually failing without having to
correlate other signals.
Pageviews were being counted twice in PostHog. Refactor the way
pageviews are computed to follow PostHog's documented recommended
pattern.
Verified locally by connecting PostHog to localhost and confirming
that only a single pageview event is emitted per navigation.
Hide the ProConnect button (only used by the Dinum frontend) when
the device viewport is not wide enough to display it cleanly, so it
does not overflow or break the layout on smaller screens.
Guard the effect button so it only renders when the track exists.
This prevents the frontend build from failing when TypeScript
rightly flagged the possibility of an undefined track being passed
to the effect logic.
When a user clicks the microphone or camera toggle while the
corresponding permission is denied, trigger a permission prompt via
`getUserMedia` instead of silently doing nothing.
This gives users a clear path back to granting access without having
to dig into the browser settings themselves.
Vendor `usePreviewTracks` from LiveKit. The only reason we kept the
upstream hook was to trigger a single combined permission prompt for
both microphone and camera at once, but it also tied the lifecycle
of the two tracks together, which made preview handling harder than
it needed to be.
Simplify the track lifecycle: instantiate each preview track once,
and drop the dynamic fallback that came with the shared hook.
To still get a single combined prompt, trigger a dedicated
`getUserMedia` call for mic + camera on entry, and release the
resulting tracks as soon as the user answers the prompt.
Known limitation: if the user denies both mic and camera at that
first prompt, the app will prompt again per device type on later
attempts, instead of asking once again for both. Acceptable trade-off
for now.
Restructure the code inside the Join component to factorize related
pieces and group them more consistently.
This does not change behavior; it just makes the component easier to
read and maintain.
Extract all the lobby-related logic from the Join component into a
dedicated component.
This makes the Join component easier to maintain and pushes the
lobby state down closer to where it is actually used, avoiding
unnecessary re-renders higher up.
Add a sound tester next to the selected output device in the speaker
select menu, so users can play a test sound and confirm they picked
the right speaker.
Inspired by the microphone gauge added previously, and requested by
users.
Add an audio level gauge next to the selected microphone in the mic
select menu, so users can see at a glance whether their microphone
is actually picking up sound.
Inspired by Google Meet's mic picker, and requested by users.
Now that the exact deviceId constraint has been dropped, the browser
can pick a different device than the one persisted in localStorage
(for example when the persisted device is no longer available).
Sync the persisted ids in localStorage with the device id that was
actually selected on the started track, so the local cache stays
consistent with what the app is really using.
Revert the old hotfix that allowed users to toggle their microphone
or camera while permissions were not granted, which then triggered
a `getUserMedia` call to prompt for them.
Now that the permission store is properly kept in sync with the
browser, this workaround is no longer needed as-is. The intended
behavior will be reimplemented cleanly in a later commit.
`derive-valtio` was broken by a recent update, which cascaded into
various regressions in the permission store.
Take the opportunity to also refactor how permissions are handled.
The store is now a pure cache with a single writer: every signal
re-reads the browser via `syncPermissions()`, and the browser stays
the only source of truth.
Re-sync triggers, all event-driven (no polling):
* `devicechange`: granting permission reveals device labels/ids, so
it fires on grant in every browser, including Safari. This
replaces the previous 500ms Safari polling. Denials are still
caught by the concurrent `getUserMedia` rejection through
`notePermissionDeniedFromGum`.
* Window focus: covers the return from the browser or system
permission UI.
* Permissions API `change` events, where the query is supported.
Remove the current device-id resolution code that was buggy and
failed to resolve the device id correctly.
A replacement will be introduced in upcoming commits.
Attach a media diagnostics snapshot to the room event handler for
media exceptions. The snapshot captures the state of the user's
setup at the moment of the error (available devices, permission
state, active tracks, etc.), so support has enough context to
troubleshoot user issues without asking them to reproduce.
Dynamic track creation used an exact deviceId constraint based on
the device id persisted in localStorage. If that device was no
longer available on reconnect, the browser raised a DOMException
instead of falling back to another device.
Drop the exact constraint so the browser can pick any available
device when the persisted one is gone.
Move the remaining direct `posthog.capture` calls behind the
telemetry module, so PostHog is only referenced from a single place.
Call sites now use the telemetry API instead of touching PostHog
directly, making it easier to swap the backend later without
changing every call site.
Introduce a telemetry module that exposes a `reportError` helper.
Under the hood it forwards errors to PostHog, but the module is the
only place that knows about PostHog.
Replace `console.error` calls used for error reporting with
`reportError`, so the codebase now goes through a single, consistent
API for telemetry.
This normalizes how errors are reported and makes it straightforward
to swap PostHog for another backend later on, without touching every
call site.
`isMobileBrowser()` only reads `navigator.userAgent`, which does
not change during the lifetime of the document, so the previous
`resize` listener never had anything meaningful to update.
It did, however, dispatch `setIsMobile` on components rendered into
a Document Picture-in-Picture window (e.g. the reactions toolbar).
When the PiP window had already been closed, Firefox threw
"can't access dead object".
Compute the value once and skip the listener entirely.
Fix 019cb315-d827-73f2-b1cc-74e4dd71e982
`ProcessorWrapper.isSupported` reports pipeline support but not
whether the WebGL2 transformer is available. On browsers where it
is not (e.g. Chrome/Edge on Windows with WebGL2 disabled by a GPU
blocklist), toggling blur throws at runtime.
Update `supportsBackgroundProcessors()` to check both, so the UI
only exposes blur when it can actually run.
fix 019f8e3b-f035-73e2-9d6a-d0dd2d0a1163
InviteDialog.tsx and Info.tsx were the last call sites calling
getRouteUrl('room', slug) without a slug guard, unlike every other
caller (e.g. useCopyRoomToClipboard).
Compute roomUrl only when the slug exists (undefined in
InviteDialog, '' in Info to keep its unguarded .replace safe).
Guarding at the call site preserves the "no room data yet" state
instead of returning a bogus "/" URL from room.to.
Fix 019fd616-f158-7771-8cff-bac3090b8449
When the PiP window closes, the browser destroys its document right
after `pagehide`. If the portal unmount is left to React's async
scheduling, it commits against a dead document and `removeChild`
throws "NotFoundError", crashing the app.
Subscribe `PictureInPicturePortal` to the Valtio store with
`sync: true`, and use `flushSync` in `usePictureInPicture` on
teardown so React unmounts the portal while the PiP document is
still alive.
Fix 019f42cf-86a9-7ad2-8e64-81b004ddc5de
LiveKit can surface raw DOM events (for example WebSocket "error"
events, whose only enumerable key is `isTrusted`) instead of Error
instances.
When such a value ends up being captured, our error reporting logs
it as "Event: Event captured as exception with keys: isTrusted",
which is unhelpful and hides the real cause.
Add a small helper that normalizes any unknown thrown or emitted
value into a proper Error, preserving the original payload as
context.
Fixes 01997b9a-db63-7fc2-8fe4-f21dd7fd608d.
The wasm and js files shipped by MediaPipe were served with
different cache policies, which could leave the two out of sync on
the client (fresh js with stale wasm, or vice versa).
Align the cache configuration across the MediaPipe assets so they
are always cached and invalidated together.
The MediaPipe assets were served under /assets, where the cache
behavior differs between wasm and js files. As a result, clients
could end up with a fresh js loader paired with a stale wasm binary
(or vice versa), leaving MediaPipe out of sync.
Copy the assets under a versioned route so the URL changes whenever
the dependency version bumps. Clients then reload both the js and
the wasm together, keeping them in sync.
The fetch-room URL was missing its trailing slash, which caused the
backend to issue a 301 redirect. Query parameters were being dropped
in the process, leading to incorrect requests.
Append the trailing slash so the request hits the correct endpoint
directly, without going through a redirect.
Rapid toggles could persist a stale configuration: each PATCH
replaces the full room config, and every call site built it from a
render-time snapshot. A toggle issued before the previous one
resolved therefore overwrote the newer value with an older one.
Handle the cache centrally in usePatchRoom so the next toggle always
reads an up-to-date configuration.
Skip adding the username query parameter when its value is
undefined, so the request URL no longer ends up with an
`?username=undefined` (or similar) that the backend has to handle.
Introduce a room configuration popup opened from the SDK's
CreateMeetingButton, laid out like the Google Meet "call options"
dialog: logo header, grey section bands, and a footer bar with the
close action.
Like CreatePopup, it runs in a dedicated popup window so it can
access session cookies, which would be blocked in an iframe. If the
user is not authenticated, they are redirected to login and come
back to this popup afterwards.
Permissions are enforced server-side. The room is fetched with the
user's session, and settings are only shown when the room is
administrable by this user. Since #1482 removed the
is_administrable flag from the room serializer (roles now live in
the LiveKit participant attributes, only available in-meeting),
administrability is detected here through the presence of the
`accesses` field, which the backend only serializes for
administrators and owners. The PATCH endpoint enforces the same
permissions server-side regardless.
The settings mirror the in-room Admin panel. Unlike the Admin panel,
there is no LiveKit connection here, so changes are only persisted
in the room configuration (and applied when a session starts):
participants of an ongoing session are not live-synced or notified.
Refactor CONNECTION_TEST_ROOM_MAX_AGE_SECONDS so it is no longer an
independent setting but a quantity derived from (or added on top of)
the token TTL.
This prevents a misconfiguration where the token would outlive the
delete-room callback. In that case, an attacker holding a valid
token could recreate the room after the callback fired and escape
the intended cleanup.
Icons inside the Switch primitive were not properly centered.
Use relative sizes for the icons and switch to a grid-based
placement strategy so they stay centered regardless of the switch
size.
_generate_title returned a lazy gettext_lazy proxy in the
recording_datetime is None branch, which json.dumps cannot
serialize.
This crashed requests.post(json=payload) with "Object of type
__proxy__ is not JSON serializable" whenever the LiveKit egress
lookup failed (started_at=None).
Force evaluation with a non-lazy method.
Add a regression test asserting the v2 payload is a real str and
is JSON-serializable when timestamps are unavailable.
The existing without_metadata test missed this: mocked
requests.post never serialized, and a lazy proxy compares equal
to its string.
Add a custom diagnostic step that reports which ICE candidate pair
was selected on the WebRTC connection, as well as all working pairs
observed during the check.
Experimental and vibe-coded for now; the output is meant to help
debugging and will likely be revisited.
Out of precaution, also revert the previous GridLayout re-render
optimization to avoid any layout regression alongside the
CarouselLayout revert.
The useSize-based re-render optimization will be reintroduced in a
dedicated small PR and release. That will also be a good occasion to
polish the layout code along the way.
The previous optimization of the CarouselLayout was broken: the
approach did not hold in practice, and strict-mode rendering was
hiding the issue during development.
Revert the change for now and revisit the optimization later with a
sounder approach.
Add a hidden div in the DOM that reflects the current state of the
microphone and camera, so that external SIP media gateways (e.g. the
Renater one) can observe it and keep an accurate view of the media
state.
Also emit a custom event from the page whenever the microphone or
camera state changes, so external consumers can subscribe to updates
instead of polling the DOM.
Update the API so that, when a user creates a new meeting without
passing an explicit configuration, the user's persisted preferences
are applied as defaults.
This allows a user to, for example, enable the waiting room by
default on every meeting they create.
Add attributes on the User model to persist per-user preferences for
the default link access level and the default room configuration.
The frontend will let users update these preferences and then reuse
them when generating a link through the webapp.
Persisting them on the backend (rather than in application memory
only) ensures the preferences survive across sessions and devices.
Expose the default access level for rooms in the backend settings
response, so the frontend can initialize the global room preferences
UI with the current default value.
The roomkit can now create a SIP dispatch rule before the LiveKit
webhook that used to trigger this creation is fired. In practice,
when the roomkit connects to the room, it also triggers the
webhook, leading to a duplicated dispatch rule.
Switch from "create dispatch rule" to "ensure dispatch rule exists"
semantics, so subsequent calls are idempotent and no duplicate rule
is created.
Rename the telephony service to a more descriptive name,
SIPManagementService, which clearly states what the service is used
for.
It is no longer used only by the telephony feature; the roomkit
feature also relies on it now.
Introduce a new viewset that lets the roomkit start a room even when
no WebRTC participant has joined yet.
This is a first entry point that will be extended over time with
more actions a roomkit needs to be able to trigger.
Known limitations:
* The responsibility around SIP rules is currently split between
the telephony feature and the roomkit one. This may need a
refactor later on to consolidate ownership in a single place.
* The default throttle might be too low for production usage and
will likely need to be revisited.
LiveKit was declared as an app-dev dependency, which caused it
(along with its egress) to be started whenever we ran unrelated
commands such as tests, migrate or makemigrations.
Drop that dependency and start LiveKit explicitly only when it is
actually needed, i.e. when calling run-backend.
The tests were failing when the Django settings did not disable
recording events, which was the case by default.
We do not rely on these events anymore by default, so set the
corresponding environment variable to false in the env file to make
the tests pass out of the box.
Display two initials in the Avatar whenever the participant's name
allows it, instead of a single letter.
A single initial makes it too hard to distinguish participants when
their cameras are off, especially in larger
Rework how the participant name is displayed in the participant
list to show as much of the name as possible before truncating.
When the name has to be truncated, add a tooltip so users can hover
to see the full name.
Requested by users.
Add a visual badge on participants who are not authenticated, so it
is immediately clear who could be an anonymous participant. This is
a small but explicit security signal in the participant list.
Beyond that, the badge also plays a functional role: since only
authenticated participants can be promoted or demoted, the badge
helps users see at a glance who is eligible for a role change.
Since the username refactoring, the username in the store could be
undefined when the join input was pre-filled from user.full_name,
because no keystroke was needed to populate the store.
This led to a 400 error on the request-entry endpoint whenever the
user joined without editing the pre-filled name.
Fall back to user.full_name when the store username is missing, so
the endpoint always receives a value.
Acknowledged as a somewhat wobbly fix, but ships as-is until the
underlying flow is reworked.
Listen to role changes in the admin panel and close the side panel
if the current user is demoted while it is open. Without this,
unprivileged users could still see the admin side panel until they
closed it manually.
I checked the other features that could be affected by hot role
changes; this was the only one still exposing admin-only UI after a
demotion. Everything else already handles live permission updates
correctly.
Introduce a new feature that lets a user promote one of the
authenticated participants of the meeting to a role with additional
privileges.
Known limitations:
* Only authenticated participants can be promoted, but there is no
visual indicator yet distinguishing authenticated from anonymous
participants. This will be added in a follow-up commit.
* The resource_access data fetched in the initial API call becomes
stale after a promotion. It is not currently used in the product,
so this is not visible, but it should either be refreshed later
or removed from the initial fetch.
* Demoting a promoted user turns them into a member, which is still
a privileged role. This is a deliberate choice until we introduce
finer-grained tuning of participant roles.
Extract the logic that closes the side panel into a utility function
declared at the store module level, as recommended by Valtio.
This avoids re-creating the function on every render and prevents
extra re-renders in components that use it.
The API serializer was too restrictive on the `sub` field, expecting
a UUID. This worked in our development and production setups because
our Keycloak is configured to emit UUID subs, but it broke for other
providers.
Per the OIDC spec and the DB model, `sub` can be any string. Align
the serializer with this and accept arbitrary string values.
Fixes#1525.
Uppercase the initials rendered in the Avatar so their vertical
centering stays consistent.
With lowercase letters, the initials were slightly shifted toward
the bottom of the Avatar, which broke the alignment.
The is_administrable flag was previously read from the room API
response through the room serializer, giving the frontend static
information about the user's rights.
Refactor the frontend so it derives this flag from the participant
role carried in the participant metadata instead.
Two benefits:
* The flag now updates live along with the participant
attributes/metadata, so role changes are reflected immediately.
* It removes the duplication between the API response and the
metadata, which both used to determine the user's capabilities.
Include the is_authenticated flag on the user in the LiveKit token
and participant metadata.
The frontend needs this information (used in the next commit) to
know whether it can offer to promote a user with access to the room
admin.
The backend previously passed an abstract is_admin_or_owner boolean
flag in the LiveKit token. That kept the frontend minimalistic and
saved it from having to handle role comparisons.
As we introduce more features that need to distinguish between the
room owner and admins, refactor the token to carry the role
directly. The frontend can then derive the relevant flags from a
richer piece of information.
Add an endpoint that allows updating a user's role while in a
meeting. The goal is to let users promote other connected
participants to admin or moderator, so the burden of administrating
a meeting can be shared.
Introduce a new permission class that verifies the caller making a
request is both authenticated and actually present in the call.
It will be used to gate actions that require the user to be live in
the room, for example:
* allowing someone in from the waiting room
* promoting another participant to a different role
More generally, this covers every action where, for security
reasons, we need to make sure the user is truly present in the call
and that someone is not reusing their cookie as an API key.
Reset the chat state on the first render of the ChatProvider, to
make sure no chat messages from a previous room leak into the new
one.
This covers SPA navigations where the user switches from one room
to another without a full page reload.
Introduce a tripwire component that listens to
RoomEvent.ActiveSpeakersChanged imperatively and forces a single
re-render of its host only when an active speaker has none of their
tiles within the visible span (maxVisibleTiles). That re-render
re-runs useVisualStableUpdate, which reads live isSpeaking state
and performs the actual tile swap.
Speakers already visible are ignored, so this costs zero React work
in the common case.
This lets us drop the ActiveSpeakersChanged subscription from
useTracks in the StageLayout upstream (updateOnlyOn: []), which was
re-rendering the whole stage on every speaker change.