A tag and a branch both move, so actions/checkout@v6 and
numerique-gouv/action-trivy-cache@main ran whatever the owner had last
pushed. Each of the 65 uses now names a 40-character commit, with the
version it resolved to in a trailing comment.
dependabot.yml keeps the hash and that comment moving together.
gitlint-core keeps the linter at 0.19.1, which the wheels-only pin was
resolving down to 0.18.0 through an sdist-only sh. --ignore-scripts
stops three npm ci, one yarn install and one npm install -g running
the scripts of what they fetch, and yarn is pinned to 1.22.22. curl
holds the dockerize download and its redirects to https.
A package could run arbitrary code on the runner while installing,
and the versions were resolved rather than taken from the pins.
The two pip calls now take wheels only, and uv run no longer
resolves an environment of its own.
The component exposes the local microphone and camera state so external
tools automating the frontend can read it. It does not expose whether
publication is allowed, so such a tool cannot tell a muted microphone from
one it is not permitted to unmute, and ends up offering a control that
silently does nothing.
Expose canPublishMicrophone and canPublishCamera in the event detail and as
data attributes, reusing the useCanPublishTrack hook that already gates the
web client controls. The permissions are part of the effect dependencies so
a mid-meeting permission change emits the event.
The make lint target did not cover the summary and agents components. Update
the linting workflow to include both services and harmonize Makefile target
names. Harmonize Docker compose user declarations.
Add Spanish to the language selection system on both trees, with 872
frontend phrases and 145 backend ones translated from the French source.
The frontend to backend language map gains it too, so a Spanish browser
does not write an empty language to the account on every load.
The Kyutai open-source model turned out not to be production-ready:
it caused disruptions in the production environment, especially on
long-running meeting sessions.
Switch to the Voxtral realtime model, which looks like a
credible competitor and behaves much better in our setup.
For now, the code handling the Voxtral realtime API lives directly
in the project. It could be extracted into an open-source package
later.
See PR #1277 for the full details of the implementation, proposed
by @cameldev.
Recordings used to be finalized by an inbound notifications sent by
S3. This tied the recording lifecycle to bucket notifications, adding
complexity and dependency to limited S3 services.
The LiveKit egress_ended webhook, added as a fallback in aee1847, does
the same job without any object-storage dependency. Make it the only
mechanism: RecordingEventsService.handle_complete is now called on
EGRESS_COMPLETE / EGRESS_LIMIT_REACHED unconditionally, instead of only
when RECORDING_STORAGE_EVENT_ENABLE is False. Remove the storage-event
path entirely.
`cache.keys()` runs Redis `KEYS`, a full-keyspace scan on Redis's
single thread that blocks everything else, including session reads
in the same cache. Its cost scales with total keys, not matches,
and some managed providers disable `KEYS` entirely.
The trusted-lobby feature made this urgent: the waiting-list
endpoint scanned on every poll, and its polling audience grows from
a few admins to potentially every authenticated participant.
Switch to cursor-based `SCAN` via two `core.utils` helpers, deleting
in bounded batches so cleanup of a large room cannot block either.
A single seam also lets us forbid raw `cache.keys()` going forward.
`SCAN` still iterates the keyspace incrementally on the polled
path. If monitoring flags it, the follow-up is a per-room set
index — out of scope here since it changes the lobby storage model.
Frontend counterpart of the trusted-lobby backend feature: on
`trusted` rooms, any authenticated participant sees the waiting
notification and can accept or deny entry requests, not only admins
and owners.
Gating moves from role to capability: `useCanManageLobby` mirrors
the backend permission and derives from `useRoomData()`. Room
metadata is already synced into the query cache, so an access-level
change mid-meeting recomputes the capability on every client with no
new sync mechanism. It is only a UI gate; the backend re-checks
everything per request and fails closed.
Fetching moves into a single room-level `LobbyProvider`: the hook
was previously instantiated by two components and only worked
because React Query deduplicated their queries. The provider owns
one query and an explicit state machine - ways in (connection
established, ParticipantWaiting broadcast, panel opened, rights
regained while the panel is open) all arm and fetch; ways out
(rights lost, server 401/403) disarm and clear the cached list so
nothing stale can render.
Polling is tiered by audience since managers grow from a few admins
to potentially the whole room: 1s when acting (panel open),
10s when the notification is shown, and zero when the list is empty -
decided in the refetchInterval callback because structural sharing
suppresses data-keyed effects on identical empty responses. A quiet
room costs nothing; fetches are triggered by uncorrelated human
events, never synchronized across the room (the rights-regained
trigger is panel-gated for this reason).
On rooms with the `trusted` access level, any authenticated user
connected to the meeting can now manage the lobby. Requested by
several organizations, and a step toward generalized lobby
management once hubs and groups land (same organization only).
Being authenticated is not enough to grant the capability: a
`trusted` room means "trusted to join", not "trusted to decide who
else joins from outside the call". The new `CanManageLobby`
permission therefore also requires the requester to be currently
connected to the meeting, verified against LiveKit and failing
closed, like `IsPresentInMeeting`. The access level itself is never
cached and always read fresh, so an owner switching the room back to
`restricted` revokes the capability on the very next request - the
one guarantee we did not want to trade for performance.
Performance is traded elsewhere: the waiting list is polled by every
lobby manager, and on a trusted room that audience grows from a few
admins to potentially the whole meeting. Hitting LiveKit once per
poll per participant would not survive that fan-out, so presence is
memoized in Redis (`PresenceCache`, `PRESENCE_CACHE_TIMEOUT`, 1h).
Entries are created lazily because only the minority of participants
who actually manage a lobby ever need one, and only positive answers
are cached because a sticky negative would lock out someone joining
right after a miss for the whole TTL. Eager invalidation on
`participant_left`, `room_finished` and admin kick keeps the cache
honest; the TTL is the safety net when an event is lost, and its
value bounds how long a departed participant could still act.
Trade-offs in this v0:
* `PRESENCE_CLEAR_ON_PARTICIPANT_LEFT` gates the eager invalidation
on `participant_left`: its cost is one Redis DELETE per departure,
for every departure, so we want to be able to measure it in
production and turn it off independently of the feature. When
disabled, invalidation relies on `room_finished` and the TTL only,
widening the stale window above.
* This can put non-trivial pressure on the cache at scale; the
rollout will need to be monitored closely.
* The `participant_left` webhook must be enabled in the LiveKit
deployment, otherwise eager invalidation silently degrades to the
TTL-only behavior.
Apply the `VOICE_AUDIO_CONSTRAINTS` to the audio track used during
the active meeting to reduce bandwidth usage.
* Originally proposed by trummerschlunk to reduce bandwidth, but
previously restricted to the join screen preview.
* Backport the standard voice constraints (48 kHz sample rate,
mono channel, 16-bit sample size) to the active call session, as
requested by the BBBA team.
Tighten the feedback screen layout on mobile viewports so it fits
comfortably on small phone screens and reads naturally without
horizontal scroll or cramped controls.
Wrap the buttons in the idle modal so they stack vertically on
mobile viewports.
The horizontal layout was cramped on small screens; a column layout
gives each button a comfortable touch target and reads more
naturally on narrow displays.
Apply the same approach as the PiP screen to the mobile control
bar: make sure it never overflows on small phone screens by
collapsing some controls into the overflow menu when there is not
enough horizontal room.
livekit-client leaks the raw WebSocket error Event as an unhandled
rejection when the signal WebSocket errors after connect. This is
mostly seen on Firefox and coincides with signal reconnects.
The event carries zero diagnostic content — the close reason is
already logged by the SDK — so it only adds noise to error
tracking.
Filter it out from our reporting.
Upstream issue: https://github.com/livekit/client-sdk-js/issues/2062
The device-in-use logic could race when multiple detection events
fired close together (e.g. rapid mic/cam toggles or several devices
becoming busy at once), leading to inconsistent store state.
Refactor the flow so all updates go through a serialized path,
removing the race conditions and keeping the in-use state
consistent regardless of event ordering.
Refactor the device-in-use handling to key state by device id
instead of only by kind (microphone or camera).
On computers with several devices of the same kind, keying by kind
meant that one device being in use marked the whole kind as busy,
even when the user could still use another device. Tracking per id
lets the UI and logic distinguish between devices correctly.
The store is updated accordingly so that per-device state is stored
and read consistently across the app.
Also consider `AbortError` with a message like "Timeout starting
video/audio source" as an "already in use" device error.
This can happen for other reasons in theory, but in practice most
occurrences are caused by another application still holding the
camera or microphone.
Route it through the existing device-in-use handling so users get
the same clear explanation as with the standard error, instead of a
generic failure.
Some versions of Firefox on Windows do not raise the
`NotReadableError` that LiveKit expects when the camera or
microphone fails to start. Instead, they surface an `AbortError`
with a message explaining the browser could not start the video or
microphone input.
This case was observed in PostHog error tracking and was not
handled, so users hit an unhelpful failure state.
Detect that specific `AbortError` and route it through the same
device-in-use / not-readable handling as the standard error, so
users get a clear explanation.
On Chrome and Windows 10, when the camera or microphone is already
in use by another application, the browser rejects
`getUserMedia`. Without dedicated handling, users just saw their
camera or microphone not turning on, without any explanation.
Detect this specific failure and surface a clear message to the
user, so they understand another application is holding the device
and know what to do about it.
The toast is enqueued on `ParticipantConnected`, which livekit-client
fires before the participant's track publications exist. At that
point `getTrackPublication(Camera)` is `undefined`, and the track
ref used by the toast is frozen that way — the toast never
re-subscribes to the participant.
Downstream effects:
* LiveKit's `useIsMuted` initializes to `false` when there is no
publication, so the tile first paints with
`data-lk-video-muted="false"`, which the stock CSS maps to
placeholder `opacity: 0` — a blank box.
* Its effect then subscribes to `mutedObserver`, which defaults to
`true` for the same situation, triggering a second render and
only then starting the 200 ms fade-in. That render-effect-render
transition is the visible lag behind the text.
* Because the track ref never updates, no `VideoTrack` ever mounts.
When the camera is later subscribed, the placeholder fades out
again, leaving the tile empty.
Re-derive the track ref inside the toast when the participant's
publications become available, so `useIsMuted` sees the real state
from the start and the tile renders the video (or a stable
placeholder) instead of a blank box.
`MuteButton` owned the confirmation dialog state locally. On the
participant tile, the button lives inside `FadeOverlay`, which
conditionally mounts its children based on tile mouse activity
(3 s idle timeout). Once the user moved the pointer onto the
react-aria portal, mouse events on the tile stopped, the idle timer
expired, `FadeOverlay` unmounted `MuteButton`, and the dialog was
destroyed with it — hence the "closes randomly" behavior.
Render the dialog once at the VideoConference level via a shared
`MuteAlertDialogProvider`, so its lifetime is independent from any
button that opens it.
A regression introduced with the custom virtual background feature
(16daf7b8, March 26): every blur init on Firefox, and every update
call, was going through `init_virtual_background`, which raised an
error because there is no virtual background to initialize in that
code path.
The existing guard was incorrect. Replace it with an early return,
so `init_virtual_background` is only executed when there is actually
a virtual background to initialize.
This does not fix the other virtual background issues that currently
prevent some users from activating their camera; those will be
addressed separately.
Send an analytics event whenever starting or stopping a recording
fails, so we can monitor how often it happens.
It also gives support the context needed to understand what went
wrong when inspecting a user's session.
After the recent refactoring, `MoreControls` only renders once. On
that first render, the container ref is `null`, and when it later
gets a reference to the div, the ref update does not trigger a
re-render.
As a result, the additional controls were never showing up.
Switch from a ref to a state to track the container element. State
updates do trigger a re-render, so the controls now show as
expected once the container is available.
`connect()` calls can be aborted by a `disconnect()` before the
join completes — typically when the user leaves, refreshes,
navigates away early, or when the component remounts. This is
expected behavior, not a failure.
Stop reporting these as errors. Track them as analytics events
instead, so we can still monitor their volume without polluting
error dashboards.
Volume is relatively low, but keeping visibility helps troubleshoot
users who complain about difficulty connecting to a room, and could
also flag a component that is re-rendering and killing the
connection.
Add a build-time warning in the Vite config that fires when the
MediaPipe assets copied by `vite-plugin-static-copy` fall out of
sync with the version used by `@livekit/track-processors`.
This catches at build time the kind of drift that would otherwise
only surface at runtime as broken virtual backgrounds.
Node 18 is now legacy, and running `make mails-install` was failing
with a "bad engine" error against the current image.
Bump the Node image used for `mails-install` to a more recent
version, aligned with the one used by the frontend, so the make
target runs cleanly again.
Note: the Alpine variant cannot be used here, as `bash` is required
to build the mail templates.