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.
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.
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.
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.
`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.
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.
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 InviteDialog open state was managed very high in the React tree,
which triggered a re-render of the whole conference tree whenever
the dialog was opened or closed.
Push the state down to a narrower component to limit the scope of
re-renders.
Part of a broader effort to isolate as much as possible what should
re-render, so we can then focus on tackling the real performance
issues.
Create a new dedicated store for user choices, starting with the
username, to decouple this part from any LiveKit elements. This is
better for tree-shaking, and depending on LiveKit for storing the
username brings no real value.
The refactoring will be continued later for the other user choices
that can be persisted.
When joining a meeting that already has many participants, new
participants are now muted by default.
This is an empirical change, not directly requested by users but
informed by user experience: a lot of people joining large meetings
arrive with their microphone and/or camera open, which can be
painful for the host, who otherwise has to mute everyone or ask
everyone to mute. Many of these participants are inattentive but
still noisy.
If you are joining a room that is already at a certain size, you are
probably not the one expected to speak; presenters typically join
among the very first participants.
Manually upgrade the frontend codebase to ESLint 9.
Update the linting configuration and related code where required to
maintain compatibility with the new major version.
Mark JS imports as type-only when applicable so they are stripped at
build time and ignored during chunk splitting, ensuring we take full
advantage of Rollup's code-splitting optimizations.
The PiP control bar shares state with the main window's control
bar — most importantly, the reaction toolbar and the mic/camera
toggles, which are fully stateful.
Sharing state works naturally here because the PiP content is
rendered through a portal into the same React tree, so the
controls in both windows read from and write to the same stores.
Co-authored-by: Cyril <c.gromoff@gmail.com>
Add a hook that manages the logic needed to open a document
picture-in-picture window, stores a ref to the window in a global
store, and—once ready—mounts a portal to duplicate content into
the PiP document's container.
Mounting the PiP content via a portal lets us share the same React
tree as the main app, and therefore share application state. This
comes with some trade-offs, particularly around components that
rely on the DOM hierarchy: react-aria popovers and overlays may
not behave correctly across documents.
The logic is kept to a minimum here. This first commit does nothing
more than open a window containing a loading spinner. The topic is
new to us, so plenty is likely to be refined in follow-ups.
Co-authored-by: Cyril <c.gromoff@gmail.com>
Updating connectionObserverSnapshot triggered page re-renders,
causing participants to reconnect due to a race condition.
Read directly from the underlying store instead of using the
snapshot to avoid unnecessary re-renders.
Capture selected ICE candidates for both subscriber and publisher
peer connections.
This enables correlation between survey feedback and connectivity
setup, helping identify problematic network configurations.
Replace costly PostHog surveys with basic surveys, which better
fit our headless usage and avoid short data retention limits.
Enhance emitted events with additional metadata, including a
unique session ID and room ID, to correlate survey responses with
specific sessions.
This lays the groundwork for further enrichment with participant
connection data.
Add a custom virtual background feature.
If the backend supports uploading files, backgrounds are stored
in the backend for the user.
Otherwise, only one background image can be selected.
Refactor the duplicateIdentity boolean URL parameter into an extensible string
reason parameter, making it easier to customize the disconnection message
shown to users.
Avoid passing this value via URL parameters, which are easy to manipulate.
Instead, use Wouter’s built-in navigation state to pass data across pages.
This was initially missed because navigateTo is a wrapper around Wouter’s
official navigation function, and its arguments were easy to overlook. This is
now fixed.
This prepares the ground for supporting additional disconnection reasons in
upcoming commits.
While creating a meeting on mobile, the dialog was opening,
and when its width exceeds the mobile width, users are unable
to close the dialog.
Prevent the dialog opening on mobile as a hot fix.
Revert recent changes to dynacast and adaptive streaming functionality
to isolate potential causes of regression issues. Changes will be
reintroduced in future commits with improved error handling and
thorough investigation of root causes.
Implement user choice persistence for video publishing resolution
configuration to maintain user preferences across sessions.
Stores selected video resolution in user settings, ensuring consistent
video quality preferences without requiring reconfiguration on each
visit.
Introduce speaker selection component requested by users to allow audio
output device configuration before entering calls.
Enables users to test and configure their preferred audio output device
during prejoin setup, ensuring proper audio routing before call begins.
Improves user experience by preventing audio issues during meetings.
Remove incorrect public room warning that was always displayed
regardless of room privacy settings.
Warning should only appear for genuinely public rooms since the lobby
system introduction changed room privacy behavior.
Prevents user confusion about room privacy settings.
We've introduced simplifications that improve performance, enhance ux,
and contribute to the overall perception of experience quality.
Previously, our processor handling was overly complex. LiveKit allows us to set
a processor before starting the local video preview track, which eliminates
the black blink glitch that appeared when loading the join component
with a default processor.
This change prevents the unnecessary stopping and restarting
of the local video track.
I'm glad this issue is now resolved.
We also simplified component communication by avoiding props drilling.
Now, we use a single flag to indicate when the user is ready to enter the room.
This significantly reduces the complexity of props passed through components.
The LiveKit API URL is necessary to interact with the API. It uses https
protocol.
Eplicit wss protocol is necessary in Websocket constructor for some
older browsers.
This resolves critical compatibility issues with legacy browsers
(notably Firefox <124, Chrome <125, Edge <125) that lack support
for HTTPS URLs in the WebSocket() constructor. Without explicit WSS
URLs, WebSocket signaling connections may fail, crash, or be blocked
entirely in these environments.
The setting is optional and defaults to the current behavior when
not specified, ensuring zero breaking changes for existing deployments.
Replace hardcoded true values with supportsAdaptiveStream() and
supportsDynacast() checks. LiveKit SDK supports broad browser range but
requires specific APIs - modern features need explicit compatibility checks.
Prevents enabling unsupported WebRTC features on incompatible browsers,
which could led to a poor user experience.
One alternative solution could be to install polyfills.
Connection warmup wasn't working properly - only works when trying to
establish WebSocket first, then workaround kicks in. Call WebSocket
endpoint without auth info expecting 401 error, but enough to initiate
cache for subsequent WebSocket functionality.
Scope this **dirty** trick to Firefox users only. Haven't figured out
how to detect proxy from JS code simply.
Tested in staging and works on our constrained WiFi.
Implement HTTPS prefetch before joining rooms to resolve WebSocket
handshake failures where Firefox+proxy returns HTTP 200 instead of 101.
Reproduced locally with Squid container. No proxy configuration fixes
found - HTTPS warmup is only working workaround. Issue doesn't occur
when signaling server shares webapp domain, making warmup unnecessary.
Use HEAD request to minimize bandwidth.
Test tracking signaling failures to determine root causes when connection
fails. Will remove if it spams analytics - goal is understanding failure
patterns and reasons.
Show explicit warning when microphone/camera are occupied by other
applications. Helps users understand permission failures and reminds
them to close other video conferencing apps.
Spotted issue through Crisp support - users often forget to quit other
webconfs that don't auto-disconnect when alone.
Add context to feedback page when user is disconnected for joining with
same identity, which is forbidden by LiveKit. Improves user understanding
of disconnection reasons.
Centralize disconnect handling to ensure all client-initiated disconnects
trigger feedback page navigation. More extensible for future disconnect
event routing needs, especially when errors happen.
I may have introduced a misusage of the usePersistentUserChoice hook.
I ended using it while expecting it to be a global state, it wasn't.
Fix broken global state that caused user choice desync. Use LiveKit default
persistence functions similar to notification store approach. Carefully
handles existing localStorage data to prevent regressions.
Note: Audio output persistence will be added in future commits.
The participant's name in the query key prevented proper cache invalidation
when renamed. Since name changes don't affect query data, removed this
dependency.
BackgroundBlurCustomProcessor is renamed to BackgroundCustomProcessor in
order to reflect the fact that is now handles virtual backgrounds too.
BackgroundBlurFactory is also renamed to BackgroundProcessorFactory.
The processor serialization handling has also been updated in order
to support various options, also if persisted in local storage.
According to the documentation:
Dynacast dynamically pauses video layers that are
not being consumed by any subscribers, significantly
reducing publishing CPU and bandwidth usage.
Dynacast will be enabled if SVC codecs (VP9/AV1) are used.
Multi-codec simulcast requires dynacast
My goal is to reduce CPU and bandwidth usage for clients.
Dynacast is enabled both in OpenTalk and LiveKit demo app!
Adaptive stream is a key optimization in large room.
Enabled adaptive stream to automatically manage
the quality of subscribed video tracks, optimizing
for bandwidth and CPU usage.
When video elements are visible, it adjusts
the resolution based on the size of the largest visible
element. If no video elements are visible, it
temporarily pauses the track until they are visible again.
Additionally, introduced support for custom pixel density,
which defaults to `2` for high-density screens
(devicePixelRatio ≥ 3) or `1` for standard screens.
Setting it to `screen` allows the pixel density to match
the actual screen's devicePixelRatio, optimizing video
clarity on ultra-high-definition displays.
This ensures a balance between streaming quality
and resource consumption.
This might also significantly increase the bandwidth
consumed by people streaming on high definition screens.
It needs to be battle tested.
OpenTalk uses a adaptiveStream equals true, while LiveKit
demo app uses 'screen' value. I followed OpenTalk choice,
I was scared 'screen' value creates performance issues
for user with high resolution screen in poor network conditions.
As a mandatory codec in WebRTC specifications, VP8 serves as
the baseline for compatibility, making it the default choice
in LiveKit client configuration.
There is room for optimization.
Newer codecs like VP9 offer significant efficiency gains
compared to VP8, with a 23-33% improvement in compression
efficiency. This translates to better video quality at
the same bitrate or reduced bandwidth usage for the same quality.
VP9 is supported in Safari starting from version 15.0+,
and Firefox offers partial support. However, Firefox lacks
support for VP9's Scalable Video Coding (SVC).
With SVC, participants can send a single VP9 stream
with multiple spatial or temporal layers. This allows receivers
to dynamically adjust video quality by using lower layers when
resolution or bandwidth needs to be reduced, improving
adaptability in heterogeneous network conditions.
Simulcast, by contrast, sends multiple separate streams
at different resolutions. While widely supported in VP8 and VP9,
it consumes more bandwidth compared to SVC.
The configuration added here is based on the LiveKit demo app,
which defaults to VP9 when supported. OpenTalk’s configuration
also recommends VP9.
If a browser does not support VP9, LiveKit falls back to VP8 or
other codecs as needed. Notably, LiveKit disables VP9 encoding for
Firefox due to longstanding issues, but it can still decode VP9
streams and encode VP8 for outgoing streams. This ensures
compatibility with other participants, even in mixed environments
where some browsers use VP9 and others fallback to VP8.
In theory, participants do not all need to switch to a single codec,
as both LiveKit and browsers intelligently handle codec negotiation
on a per-participant basis. This dynamic adaptation ensures seamless
communication without manual intervention.
A similar challenge with codec compatibility was raised
in Jitsi two years ago, check issue #10657.
Before any release, this needs to be battle tested
with Firefox 115 browsers.
DINUM users face connection issues in bandwidth-constrained environments.
They cannot reach their room, the peer connection timeouts while negotiating
TURN/TLS handshake (from our current understanding).
I am not sure this issue is linked to those parameters, at least try something.
Properly integrate chat into the sidepanel to improve UX and avoid disruptions.
Implement initial styling based on Google Meet's design, with plans for future
enhancements. Some details remain to be refined, such as preserving newline
characters in the message formatter.
This substantial commit refactors and cleans up a significant legacy component.
Chat notifications will be addressed in a separate PR.
Note: While this is a large commit, it represents a major improvement in user
experience (in my opinion).