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.
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.
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.
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.
`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.
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.
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.
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.
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.
Some characters span multiple UTF-16 code units. Taking a naive first
index for avatar initials can split them and show a broken glyph when
the camera is off.
Optically fix initials centering with a more complex approach.
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.
Introduce a new connection test page to allow users to verify
their device and network compatibility with the application.
The feature also supports generating and downloading a detailed report
of the test results.
site.webmanifest declared no start_url, so the page that linked
it became one and an install started inside a room reopened that
room on every launch. It now declares "/", moves out of public/
and takes VITE_APP_TITLE for name and short_name, which shipped
empty and leaned on the browser falling back to the title.
The frontend Dockerfile now declares that build argument, so the
value compose.yml passes stops being dropped.
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.
The SonarCloud gate fails on main, so every commit lands red, and
gh run list hides it: it lists only Actions workflows, and the
failure is an app check run.
Reliability rests on one bug, in test_file_service.py, which wrapped
an assertion in an except Exception re-raised through pytest.fail.
Removing it takes the rating from D to A.
Two pieces of debt ride along. The SDK callback id now comes from
crypto.getRandomValues, since it guards an endpoint with no auth. And
core/tasks gets the __init__.py that lets pylint see it, with the
debt that exposes, which is why #1533 fails lint-back.
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.
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.
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.
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.
Updating room access rewrote the entire metadata payload, removing information
about active recordings. This caused the frontend to lose track of ongoing
recordings and could trigger 409 errors when attempting to start a new
recording.
Consolidate the duplicated metadata update logic into
`RoomManagement.update_metadata()` and preserve merge behavior instead of
overwriting the full metadata object.