Merge branch 'livekit' into more-more-performance

This commit is contained in:
Robin
2026-08-17 11:12:17 +02:00
42 changed files with 2025 additions and 813 deletions

View File

@@ -115,30 +115,33 @@ For more details on the packages, see the
For operating and deploying Element Call on your own server, refer to the For operating and deploying Element Call on your own server, refer to the
[**Self-Hosting Guide**](./docs/self_hosting.md). [**Self-Hosting Guide**](./docs/self_hosting.md).
## 🧭 MatrixRTC Backend Discovery and Selection ## MatrixRTC Transports
For proper Element Call operation each site deployment needs a MatrixRTC backend For proper operation of Element Call, each deployment needs to set up a
setup as outlined in the [Self-Hosting Guide](./docs/self_hosting.md). A typical MatrixRTC transport in the form of a LiveKit server as outlined in the
federated site deployment for three different sites A, B and C is depicted below. [Self-Hosting Guide](./docs/self_hosting.md). A typical federated site
deployment for three different sites A, B and C is depicted below.
<p align="center"> <p align="center">
<img src="./docs/Federated_Setup.drawio.png" alt="Element Call federated setup"> <img src="./docs/Federated_Setup.drawio.png" alt="Element Call federated setup">
</p> </p>
### Backend Discovery ### Transport Discovery
The MatrixRTC backend (according to Element Call discovers the available MatrixRTC transports (as defined by
[MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)) is [MSC4519](https://github.com/matrix-org/matrix-spec-proposals/pull/4519)) by
announced by the Matrix site's `.well-known/matrix/client` file and discovered hitting the `GET /_matrix/client/unstable/org.matrix.msc4143/rtc/transports`
via the `org.matrix.msc4143.rtc_foci` key, e.g.: endpoint of the Client-Server API. An example response:
```json ```json
"org.matrix.msc4143.rtc_foci": [ {
"rtc_transports": [
{ {
"type": "livekit", "type": "livekit",
"livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt" "livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt"
}, }
] ]
}
``` ```
where the format for MatrixRTC using LiveKit backend is defined in where the format for MatrixRTC using LiveKit backend is defined in
@@ -149,7 +152,7 @@ via `livekit_service_url`.
### Backend Selection ### Backend Selection
- Each call participant proposes their discovered MatrixRTC backend from - Each call participant proposes their discovered MatrixRTC transport from
`org.matrix.msc4143.rtc_foci` in their `org.matrix.msc3401.call.member` state event. `org.matrix.msc4143.rtc_foci` in their `org.matrix.msc3401.call.member` state event.
- For the **LiveKit** MatrixRTC backend - For the **LiveKit** MatrixRTC backend
([MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)), ([MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)),
@@ -244,10 +247,9 @@ pnpm backend
> [!NOTE] > [!NOTE]
> To ensure your local development frontend functions properly, youll need to > To ensure your local development frontend functions properly, youll need to
> add certificate exceptions in your browser for `https://localhost:3000`, > add certificate exceptions in your browser for `https://localhost:3000` and
> `https://matrix-rtc.m.localhost/livekit/jwt/healthz` and > `https://matrix-rtc.m.localhost/livekit/jwt/healthz`. This can be done either
> `https://synapse.m.localhost/.well-known/matrix/client`. This can be either > by adding the minimum localhost CA
> done by adding the minimum localhost CA
> ([./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt)) to your web > ([./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt)) to your web
> browser's trusted certificates or by simply copying and pasting each URL into > browser's trusted certificates or by simply copying and pasting each URL into
> your browsers address bar and follow the prompts to add the exception. > your browsers address bar and follow the prompts to add the exception.

View File

@@ -40,6 +40,8 @@ experimental_features:
msc4222_enabled: true msc4222_enabled: true
# sticky events for MatrixRTC user state # sticky events for MatrixRTC user state
msc4354_enabled: true msc4354_enabled: true
# MatrixRTC
msc4143_enabled: true
# The maximum allowed duration by which sent events can be delayed, as # The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no # per MSC4140. Must be a positive value if set. Defaults to no
@@ -54,7 +56,6 @@ enable_registration_without_verification: true
registration_shared_secret: "test_shared_secret_for_local_dev_only" registration_shared_secret: "test_shared_secret_for_local_dev_only"
report_stats: false report_stats: false
serve_server_wellknown: true
# Ratelimiting settings for client actions (registration, login, messaging). # Ratelimiting settings for client actions (registration, login, messaging).
# #
@@ -67,3 +68,8 @@ rc_message:
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
per_second: 0.5 per_second: 0.5
burst_count: 30 burst_count: 30
matrix_rtc:
transports:
- type: livekit
livekit_service_url: https://matrix-rtc.othersite.m.localhost/livekit/jwt

View File

@@ -40,6 +40,8 @@ experimental_features:
msc4222_enabled: true msc4222_enabled: true
# sticky events for MatrixRTC user state # sticky events for MatrixRTC user state
msc4354_enabled: true msc4354_enabled: true
# MatrixRTC
msc4143_enabled: true
# The maximum allowed duration by which sent events can be delayed, as # The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no # per MSC4140. Must be a positive value if set. Defaults to no
@@ -54,7 +56,6 @@ enable_registration_without_verification: true
registration_shared_secret: "test_shared_secret_for_local_dev_only" registration_shared_secret: "test_shared_secret_for_local_dev_only"
report_stats: false report_stats: false
serve_server_wellknown: true
# Ratelimiting settings for client actions (registration, login, messaging). # Ratelimiting settings for client actions (registration, login, messaging).
# #
@@ -67,3 +68,8 @@ rc_message:
# Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s # Currently the heart-beat is every 5 seconds which translates into a rate of 0.2s
per_second: 0.5 per_second: 0.5
burst_count: 30 burst_count: 30
matrix_rtc:
transports:
- type: livekit
livekit_service_url: https://matrix-rtc.m.localhost/livekit/jwt

View File

@@ -1,4 +1,4 @@
# Synapse reverse proxy including .well-known/matrix/client # Synapse reverse proxy
# domain synapse.m.localhost # domain synapse.m.localhost
server { server {
listen 80; listen 80;
@@ -11,18 +11,6 @@ server {
ssl_certificate /root/ssl/cert.pem; ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem; ssl_certificate_key /root/ssl/key.pem;
# well-known config adding rtc_foci backend
# Note well-known is currently not effective due to:
# https://spec.matrix.org/v1.12/client-server-api/#well-known-uri the spec
# says it must be at https://$server_name/... (implied port 443) Hence, we
# currently rely for local development environment on deprecated config.json
# setting for livekit_service_url
location /.well-known/matrix/client {
add_header Access-Control-Allow-Origin *;
return 200 '{"m.homeserver": {"base_url": "https://synapse.m.localhost"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrix-rtc.m.localhost/livekit/jwt"}]}';
default_type application/json;
}
# Reverse proxy for Matrix Synapse Homeserver # Reverse proxy for Matrix Synapse Homeserver
# This is also required for development environment. # This is also required for development environment.
# Reason: the lk-jwt-service uses the federation API for the openid token # Reason: the lk-jwt-service uses the federation API for the openid token
@@ -44,7 +32,7 @@ server {
} }
# Synapse reverse proxy including .well-known/matrix/client # Synapse reverse proxy
# domain synapse.othersite.m.localhost # domain synapse.othersite.m.localhost
server { server {
listen 80; listen 80;
@@ -57,18 +45,6 @@ server {
ssl_certificate /root/ssl/cert.pem; ssl_certificate /root/ssl/cert.pem;
ssl_certificate_key /root/ssl/key.pem; ssl_certificate_key /root/ssl/key.pem;
# well-known config adding rtc_foci backend
# Note well-known is currently not effective due to:
# https://spec.matrix.org/v1.12/client-server-api/#well-known-uri the spec
# says it must be at https://$server_name/... (implied port 443) Hence, we
# currently rely for local development environment on deprecated config.json
# setting for livekit_service_url
location /.well-known/matrix/client {
add_header Access-Control-Allow-Origin *;
return 200 '{"m.homeserver": {"base_url": "https://synapse.othersite.m.localhost"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrix-rtc.othersite.m.localhost/livekit/jwt"}]}';
default_type application/json;
}
# Reverse proxy for Matrix Synapse Homeserver # Reverse proxy for Matrix Synapse Homeserver
# This is also required for development environment. # This is also required for development environment.
# Reason: the lk-jwt-service uses the federation API for the openid token # Reason: the lk-jwt-service uses the federation API for the openid token

View File

@@ -40,6 +40,8 @@ experimental_features:
msc4222_enabled: true msc4222_enabled: true
# sticky events for MatrixRTC user state # sticky events for MatrixRTC user state
msc4354_enabled: true msc4354_enabled: true
# MatrixRTC
msc4143_enabled: true
# The maximum allowed duration by which sent events can be delayed, as # The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no # per MSC4140. Must be a positive value if set. Defaults to no
@@ -54,7 +56,6 @@ enable_registration_without_verification: true
registration_shared_secret: "test_shared_secret_for_local_dev_only" registration_shared_secret: "test_shared_secret_for_local_dev_only"
report_stats: false report_stats: false
serve_server_wellknown: true
# Ratelimiting settings for client actions (registration, login, messaging). # Ratelimiting settings for client actions (registration, login, messaging).
# #
@@ -84,3 +85,8 @@ rc_login:
rc_registration: rc_registration:
per_second: 10000 per_second: 10000
burst_count: 10000 burst_count: 10000
matrix_rtc:
transports:
- type: livekit
livekit_service_url: https://matrix-rtc.othersite.m.localhost/livekit/jwt

View File

@@ -40,6 +40,8 @@ experimental_features:
msc4222_enabled: true msc4222_enabled: true
# sticky events for MatrixRTC user state # sticky events for MatrixRTC user state
msc4354_enabled: true msc4354_enabled: true
# MatrixRTC
msc4143_enabled: true
# The maximum allowed duration by which sent events can be delayed, as # The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no # per MSC4140. Must be a positive value if set. Defaults to no
@@ -54,7 +56,6 @@ enable_registration_without_verification: true
registration_shared_secret: "test_shared_secret_for_local_dev_only" registration_shared_secret: "test_shared_secret_for_local_dev_only"
report_stats: false report_stats: false
serve_server_wellknown: true
# Ratelimiting settings for client actions (registration, login, messaging). # Ratelimiting settings for client actions (registration, login, messaging).
# #
@@ -84,3 +85,8 @@ rc_login:
rc_registration: rc_registration:
per_second: 10000 per_second: 10000
burst_count: 10000 burst_count: 10000
matrix_rtc:
transports:
- type: livekit
livekit_service_url: https://matrix-rtc.m.localhost/livekit/jwt

View File

@@ -13,7 +13,7 @@ services:
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z - ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
element-web: element-web:
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates. # Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
image: ghcr.io/element-hq/element-web:develop@sha256:b4e2d2d0919f26362755d4df29dbc7a55075880fdef329f53a7937241712c526 image: ghcr.io/element-hq/element-web:develop@sha256:af44e1e18bdd363a3332a577c3af66a9dde4ad352508a42ee5012c4a9528b30d
element-web-1: element-web-1:
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates. # Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
image: ghcr.io/element-hq/element-web:develop@sha256:b4e2d2d0919f26362755d4df29dbc7a55075880fdef329f53a7937241712c526 image: ghcr.io/element-hq/element-web:develop@sha256:af44e1e18bdd363a3332a577c3af66a9dde4ad352508a42ee5012c4a9528b30d

View File

@@ -68,10 +68,10 @@ As a prerequisite for the
make sure that your Synapse server has either a `federation` or `openid` make sure that your Synapse server has either a `federation` or `openid`
[listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners). [listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners).
### MatrixRTC Backend ### LiveKit backend
In order to **guarantee smooth operation** of Element Call, a MatrixRTC backend is In order to **guarantee smooth operation** of Element Call, a dedicated LiveKit
required for each site deployment. backend is required for each site deployment.
![MSC4195 compatible setup](MSC4195_setup.drawio.png) ![MSC4195 compatible setup](MSC4195_setup.drawio.png)
@@ -165,7 +165,7 @@ Using Haproxy, you can achieve this by:
use_backend mxrtc_auth_backend if is_mxrtc_auth matrixrtc_domain use_backend mxrtc_auth_backend if is_mxrtc_auth matrixrtc_domain
# Backend # Backend
## MatrixRTC backend ## LiveKit backend
backend sfu_backend backend sfu_backend
server livekit 127.0.0.1:7880 server livekit 127.0.0.1:7880
http-request set-path %[path,regsub(^/livekit/sfu/,/)] http-request set-path %[path,regsub(^/livekit/sfu/,/)]
@@ -187,43 +187,22 @@ backend mxrtc_auth_backend
``` ```
#### MatrixRTC backend announcement #### MatrixRTC transport announcement
> [!IMPORTANT] Enable the unstable feature flag `msc4143_enabled`, and update the
> As defined in [`matrix_rtc` section](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#matrix_rtc)
> [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143), of your Synapse config file:
> the MatrixRTC backend(s) must be announced to the client via your **Matrix site's
> `.well-known/matrix/client`** file (e.g.
> `example.com/.well-known/matrix/client` matching the site deployment example
> from above). The configuration is a list of Foci configs:
```json ```yaml
"org.matrix.msc4143.rtc_foci": [ matrix_rtc:
{ transports:
"type": "livekit", - type: livekit
"livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt" livekit_service_url: https://matrix-rtc.example.com/livekit/jwt
},
{
"type": "livekit",
"livekit_service_url": "https://matrix-rtc-2.example.com/livekit/jwt"
}
]
``` ```
Make sure this file is served with the correct MIME type (`application/json`). The transport you specify will be made available to clients over the
Additionally, ensure the appropriate CORS headers are set to allow web clients `/_matrix/client/unstable/org.matrix.msc4143/rtc/transports` endpoint as defined
to access it across origins. For more details, refer to the in [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143).
[Matrix Client-Server API: 2. Web Browser Clients](https://spec.matrix.org/latest/client-server-api/#web-browser-clients).
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: X-Requested-With, Content-Type, Authorization
```
> [!NOTE]
> Most `org.matrix.msc4143.rtc_foci` configurations will only have one entry in
> the array.
## Building Element Call ## Building Element Call
@@ -278,7 +257,7 @@ runtime. Documentation and default values for `public/config.json` can be found
in [ConfigOptions.ts](../src/config/ConfigOptions.ts). in [ConfigOptions.ts](../src/config/ConfigOptions.ts).
> [!CAUTION] > [!CAUTION]
> Please note configuring MatrixRTC backend via `config.json` of > Please note configuring LiveKit backend via `config.json` of
> Element Call is only available for developing and debug purposes. Relying on > Element Call is only available for developing and debug purposes. Relying on
> it might break Element Call going forward! > it might break Element Call going forward!

View File

@@ -61,7 +61,7 @@
"crypto_version": "Crypto version: {{version}}", "crypto_version": "Crypto version: {{version}}",
"custom_livekit_url": { "custom_livekit_url": {
"current_url": "Currently set to: ", "current_url": "Currently set to: ",
"from_config": "Currently, no overwrite is set. Url from well-known or config is used.", "from_config": "Currently, no overwrite is set. Url from config is used.",
"label": "Custom Livekit-url", "label": "Custom Livekit-url",
"reset": "Reset overwrite", "reset": "Reset overwrite",
"save": "Save", "save": "Save",
@@ -196,13 +196,23 @@
"room_auth_view_ssla_caption": "By clicking \"Join call now\", you agree to our <2>Software and Services License Agreement (SSLA)</2>", "room_auth_view_ssla_caption": "By clicking \"Join call now\", you agree to our <2>Software and Services License Agreement (SSLA)</2>",
"screenshare_button_label": "Share screen", "screenshare_button_label": "Share screen",
"settings": { "settings": {
"advanced_camera_description": "Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.",
"advanced_camera_label": "Advanced camera settings",
"advanced_screen_share_description": "Configure resolution, framerate, bitrate, and codec for screen sharing",
"advanced_screen_share_label": "Advanced screen share settings",
"audio_processing_description": "Changes apply on next call join.",
"audio_processing_header": "Audio processing",
"audio_tab": { "audio_tab": {
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play.", "effect_volume_description": "Adjust the volume at which reactions and hand raised effects play.",
"effect_volume_label": "Sound effect volume" "effect_volume_label": "Sound effect volume"
}, },
"auto_gain_control_label": "Automatic gain control",
"background_blur_header": "Background", "background_blur_header": "Background",
"background_blur_label": "Blur the background of the video", "background_blur_label": "Blur the background of the video",
"bitrate_label": "Bitrate",
"blur_not_supported_by_browser": "(Background blur is not supported by this device.)", "blur_not_supported_by_browser": "(Background blur is not supported by this device.)",
"camera_header": "Camera quality",
"codec_label": "Codec",
"developer_tab_title": "Developer", "developer_tab_title": "Developer",
"devices": { "devices": {
"activating": "Activating…", "activating": "Activating…",
@@ -218,12 +228,15 @@
"speaker": "Speaker", "speaker": "Speaker",
"speaker_numbered": "Speaker {{n}}" "speaker_numbered": "Speaker {{n}}"
}, },
"echo_cancellation_label": "Echo cancellation",
"feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.", "feedback_tab_body": "If you are experiencing issues or simply would like to provide some feedback, please send us a short description below.",
"feedback_tab_description_label": "Your feedback", "feedback_tab_description_label": "Your feedback",
"feedback_tab_h4": "Submit feedback", "feedback_tab_h4": "Submit feedback",
"feedback_tab_send_logs_label": "Include debug logs", "feedback_tab_send_logs_label": "Include debug logs",
"feedback_tab_thank_you": "Thanks, we received your feedback!", "feedback_tab_thank_you": "Thanks, we received your feedback!",
"feedback_tab_title": "Feedback", "feedback_tab_title": "Feedback",
"framerate_label": "Framerate",
"noise_suppression_label": "Noise suppression",
"opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.", "opt_in_description": "<0></0><1></1>You may withdraw consent by unchecking this box. If you are currently in a call, this setting will take effect at the end of the call.",
"preferences_tab": { "preferences_tab": {
"developer_mode_label": "Developer mode", "developer_mode_label": "Developer mode",
@@ -235,7 +248,9 @@
"reactions_show_label": "Show reactions", "reactions_show_label": "Show reactions",
"show_hand_raised_timer_description": "Show a timer when a participant raises their hand", "show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
"show_hand_raised_timer_label": "Show hand raise duration" "show_hand_raised_timer_label": "Show hand raise duration"
} },
"resolution_label": "Resolution",
"screen_share_header": "Screen sharing"
}, },
"star_rating_input_label_one": "{{count}} star", "star_rating_input_label_one": "{{count}} star",
"star_rating_input_label_other": "{{count}} stars", "star_rating_input_label_other": "{{count}} stars",

View File

@@ -77,7 +77,7 @@
"@typescript-eslint/utils": "^8.61.0", "@typescript-eslint/utils": "^8.61.0",
"@use-gesture/react": "^10.2.11", "@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^10.0.0", "@vector-im/compound-design-tokens": "^10.0.0",
"@vector-im/compound-web": "^9.3.0", "@vector-im/compound-web": "^10.0.0",
"@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-react": "^6.0.2",
"@vitest/browser-playwright": "^4.1.5", "@vitest/browser-playwright": "^4.1.5",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
@@ -97,7 +97,7 @@
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"loglevel": "^1.9.1", "loglevel": "^1.9.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^1.16.1", "matrix-widget-api": "^1.18.0",
"node-stdlib-browser": "^1.3.1", "node-stdlib-browser": "^1.3.1",
"normalize.css": "^8.0.1", "normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3", "observable-hooks": "^4.2.3",
@@ -131,5 +131,5 @@
"vitest": "^4.1.5", "vitest": "^4.1.5",
"vitest-axe": "^1.0.0-pre.3" "vitest-axe": "^1.0.0-pre.3"
}, },
"packageManager": "pnpm@11.6.0+sha512.9a36518224080c6fe5165afdcfe79bfa118c29be703f3f462b1e32efe1e98e47e8750b148e08286250aad4113cc7993ca413c4e2cd447752708c2ee5751bc95f" "packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1"
} }

368
pnpm-lock.yaml generated
View File

@@ -39,10 +39,10 @@ importers:
version: 11.7.12 version: 11.7.12
'@livekit/components-core': '@livekit/components-core':
specifier: ^0.12.0 specifier: ^0.12.0
version: 0.12.14(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) version: 0.12.15(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
'@livekit/components-react': '@livekit/components-react':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.9.23(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1) version: 2.9.24(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)
'@livekit/track-processors': '@livekit/track-processors':
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22)) version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))
@@ -66,13 +66,13 @@ importers:
version: 10.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) version: 10.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@rolldown/plugin-babel': '@rolldown/plugin-babel':
specifier: ^0.2.3 specifier: ^0.2.3
version: 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
'@sentry/react': '@sentry/react':
specifier: ^8.0.0 specifier: ^8.0.0
version: 8.55.2(react@19.2.8) version: 8.55.2(react@19.2.8)
'@sentry/vite-plugin': '@sentry/vite-plugin':
specifier: ^3.0.0 specifier: ^3.0.0
version: 3.6.1 version: 3.6.1(supports-color@7.2.0)
'@storybook/addon-docs': '@storybook/addon-docs':
specifier: ^10.3.6 specifier: ^10.3.6
version: 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
@@ -81,7 +81,7 @@ importers:
version: 10.5.5(@vitest/browser-playwright@4.1.10)(@vitest/browser@4.1.10)(@vitest/runner@4.1.10)(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(vitest@4.1.10) version: 10.5.5(@vitest/browser-playwright@4.1.10)(@vitest/browser@4.1.10)(@vitest/runner@4.1.10)(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(vitest@4.1.10)
'@storybook/react-vite': '@storybook/react-vite':
specifier: ^10.3.6 specifier: ^10.3.6
version: 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(supports-color@7.2.0)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
'@testing-library/dom': '@testing-library/dom':
specifier: ^10.1.0 specifier: ^10.1.0
version: 10.4.1 version: 10.4.1
@@ -126,7 +126,7 @@ importers:
version: 2.15.0 version: 2.15.0
'@typescript-eslint/utils': '@typescript-eslint/utils':
specifier: ^8.61.0 specifier: ^8.61.0
version: 8.65.0(eslint@8.57.1)(typescript@5.9.3) version: 8.65.0(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
'@use-gesture/react': '@use-gesture/react':
specifier: ^10.2.11 specifier: ^10.2.11
version: 10.3.1(react@19.2.8) version: 10.3.1(react@19.2.8)
@@ -134,11 +134,11 @@ importers:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.2.3(@types/react@19.2.17)(react@19.2.8) version: 10.2.3(@types/react@19.2.17)(react@19.2.8)
'@vector-im/compound-web': '@vector-im/compound-web':
specifier: ^9.3.0 specifier: ^10.0.0
version: 9.9.0(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) version: 10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: ^6.0.2 specifier: ^6.0.2
version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
'@vitest/browser-playwright': '@vitest/browser-playwright':
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10) version: 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)
@@ -162,10 +162,10 @@ importers:
version: link:eslint version: link:eslint
eslint-plugin-storybook: eslint-plugin-storybook:
specifier: ^10.3.6 specifier: ^10.3.6
version: 10.5.5(eslint@8.57.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(typescript@5.9.3) version: 10.5.5(eslint@8.57.1(supports-color@7.2.0))(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(supports-color@7.2.0)(typescript@5.9.3)
fetch-mock: fetch-mock:
specifier: 11.1.5 specifier: 11.1.5
version: 11.1.5 version: 11.1.5(node-fetch@2.7.0)
i18next: i18next:
specifier: ^26.0.0 specifier: ^26.0.0
version: 26.3.6(typescript@5.9.3) version: 26.3.6(typescript@5.9.3)
@@ -177,7 +177,7 @@ importers:
version: 1.67.3(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(typescript@5.9.3) version: 1.67.3(@types/node@24.13.3)(react-dom@19.2.8(react@19.2.8))(typescript@5.9.3)
jsdom: jsdom:
specifier: ^26.0.0 specifier: ^26.0.0
version: 26.1.0 version: 26.1.0(supports-color@7.2.0)
knip: knip:
specifier: ^5.86.0 specifier: ^5.86.0
version: 5.88.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3)(typescript@5.9.3) version: 5.88.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3)(typescript@5.9.3)
@@ -192,9 +192,9 @@ importers:
version: 1.9.2 version: 1.9.2
matrix-js-sdk: matrix-js-sdk:
specifier: github:matrix-org/matrix-js-sdk#develop specifier: github:matrix-org/matrix-js-sdk#develop
version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7 version: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0
matrix-widget-api: matrix-widget-api:
specifier: ^1.16.1 specifier: ^1.18.0
version: 1.18.0 version: 1.18.0
node-stdlib-browser: node-stdlib-browser:
specifier: ^1.3.1 specifier: ^1.3.1
@@ -282,13 +282,13 @@ importers:
version: 0.2.1(node-stdlib-browser@1.3.1)(rollup@4.60.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 0.2.1(node-stdlib-browser@1.3.1)(rollup@4.60.1)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
vite-plugin-svgr: vite-plugin-svgr:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.5.0(rollup@4.60.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 4.5.0(rollup@4.60.1)(supports-color@7.2.0)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
vite-plugin-wasm: vite-plugin-wasm:
specifier: ^3.6.0 specifier: ^3.6.0
version: 3.6.0(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 3.6.0(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
vitest: vitest:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
vitest-axe: vitest-axe:
specifier: ^1.0.0-pre.3 specifier: ^1.0.0-pre.3
version: 1.0.0-pre.5(vitest@4.1.10) version: 1.0.0-pre.5(vitest@4.1.10)
@@ -332,10 +332,6 @@ packages:
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/generator@7.29.7':
resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==}
engines: {node: '>=6.9.0'}
'@babel/generator@7.29.8': '@babel/generator@7.29.8':
resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -391,14 +387,13 @@ packages:
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/runtime@8.0.0':
resolution: {integrity: sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==}
'@babel/template@7.29.7': '@babel/template@7.29.7':
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.7':
resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==}
engines: {node: '>=6.9.0'}
'@babel/traverse@7.29.8': '@babel/traverse@7.29.8':
resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
engines: {node: '>=6.9.0'} engines: {node: '>=6.9.0'}
@@ -1164,15 +1159,15 @@ packages:
'@jridgewell/trace-mapping@0.3.31': '@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@livekit/components-core@0.12.14': '@livekit/components-core@0.12.15':
resolution: {integrity: sha512-6OKP/1Ok2fCZewDLKd3SzaTb7KvfZl/6hjggI+TgUdhp0Y7HBg3+tHA7YmhZRc0BO8wIyVy4VgTPn7qkLHt2nQ==} resolution: {integrity: sha512-bDkK+jkPMgyT4a5lZmdcuc/hZ8fZUrXW+qQPNHVeSpXCBMzk5q9QWzx+HPtTsPJ8hnncWGfoT81RnXHBsy+Z8g==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
livekit-client: ^2.20.1 livekit-client: ^2.20.1
tslib: ^2.6.2 tslib: ^2.6.2
'@livekit/components-react@2.9.23': '@livekit/components-react@2.9.24':
resolution: {integrity: sha512-clO+0g/u3YBpuOvnAjUSAJBH/o7w+RpUAseswjiSML9rHrXlTUhwBNm8LPk+qN5GOU3A6lzsNnFYVpUHUBVOkg==} resolution: {integrity: sha512-qw5Oy1EfPg1f/xrKYrFielSrmtxY8BZ9mDt6299G5aikotQIztmldBfTNosrVS7VSNit7qeYfOFSyKaML20W6Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
'@livekit/krisp-noise-filter': ^0.2.12 || ^0.3.0 || ^0.4.0 '@livekit/krisp-noise-filter': ^0.2.12 || ^0.3.0 || ^0.4.0
@@ -1196,8 +1191,8 @@ packages:
'@types/dom-mediacapture-transform': ^0.1.9 '@types/dom-mediacapture-transform': ^0.1.9
livekit-client: ^1.12.0 || ^2.1.0 livekit-client: ^1.12.0 || ^2.1.0
'@matrix-org/matrix-sdk-crypto-wasm@18.3.1': '@matrix-org/matrix-sdk-crypto-wasm@18.4.0':
resolution: {integrity: sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==} resolution: {integrity: sha512-osxkU1DQ+05+anGHapjWyvZqdHUb94Id37gy54mCKn1Cq/D7iGT5oEUEhjp4oTnCLo4TOtI6ULJ/LHsapaIptQ==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
'@mdx-js/react@3.1.1': '@mdx-js/react@3.1.1':
@@ -3256,8 +3251,8 @@ packages:
react: react:
optional: true optional: true
'@vector-im/compound-web@9.9.0': '@vector-im/compound-web@10.0.1':
resolution: {integrity: sha512-GjAEhzB/mt9MA7Y1ySe6/AlW26vodDu4hvTi1ThB5rTrt4KAiHMpvk+T56KV5Lwo0VvBxAyHMtm/IY5tkyH12A==} resolution: {integrity: sha512-CT3aGMalPSf53o3JUbBF7EA10Rfp+auftIuBwOUSna+hfZ/zPxGLTFcuCuOJ7L8hrzijM+V4I8L0PUkfyOczpg==}
peerDependencies: peerDependencies:
'@fontsource/inconsolata': ^5 '@fontsource/inconsolata': ^5
'@fontsource/inter': ^5 '@fontsource/inter': ^5
@@ -4381,8 +4376,8 @@ packages:
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
is-network-error@1.3.1: is-network-error@1.3.2:
resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==} resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==}
engines: {node: '>=16'} engines: {node: '>=16'}
is-number@7.0.0: is-number@7.0.0:
@@ -4467,6 +4462,9 @@ packages:
jose@6.2.4: jose@6.2.4:
resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==} resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==}
jose@6.2.9:
resolution: {integrity: sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==}
js-tokens@10.0.0: js-tokens@10.0.0:
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
@@ -4518,10 +4516,6 @@ packages:
jsonfile@6.2.0: jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jwt-decode@4.0.0:
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
engines: {node: '>=18'}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -4685,9 +4679,9 @@ packages:
matrix-events-sdk@0.0.1: matrix-events-sdk@0.0.1:
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7: matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0:
resolution: {gitHosted: true, integrity: sha512-ThGe61xx0d1L1weA8hvodzZOcATv2jR+LXd/WKwSsVT2GcR8AADY4pPxgaNvhFfl9tLMi2qMHeNay36RjDa6Zw==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7} resolution: {gitHosted: true, integrity: sha512-UmlkoXs9VMZvNaDeQqBb2k+gLifnIqEZc5cxrLeEwdYEDakQLbRqrFtmvIp4ipUCjdIPkCgtzb7QLlpPBLAwqA==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0}
version: 41.7.0 version: 42.1.0
engines: {node: '>=22.0.0'} engines: {node: '>=22.0.0'}
matrix-widget-api@1.18.0: matrix-widget-api@1.18.0:
@@ -4829,10 +4823,6 @@ packages:
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
engines: {node: '>=12.20.0'} engines: {node: '>=12.20.0'}
oidc-client-ts@3.5.0:
resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==}
engines: {node: '>=18'}
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
@@ -6254,34 +6244,26 @@ snapshots:
'@babel/compat-data@7.29.7': {} '@babel/compat-data@7.29.7': {}
'@babel/core@7.29.7': '@babel/core@7.29.7(supports-color@7.2.0)':
dependencies: dependencies:
'@babel/code-frame': 7.29.7 '@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7 '@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7 '@babel/helper-compilation-targets': 7.29.7
'@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
'@babel/helpers': 7.29.7 '@babel/helpers': 7.29.7
'@babel/parser': 7.29.8 '@babel/parser': 7.29.8
'@babel/template': 7.29.7 '@babel/template': 7.29.7
'@babel/traverse': 7.29.7 '@babel/traverse': 7.29.8(supports-color@7.2.0)
'@babel/types': 7.29.8 '@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5 '@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0 convert-source-map: 2.0.0
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
gensync: 1.0.0-beta.2 gensync: 1.0.0-beta.2
json5: 2.2.3 json5: 2.2.3
semver: 6.3.1 semver: 6.3.1
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@babel/generator@7.29.7':
dependencies:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
jsesc: 3.1.0
'@babel/generator@7.29.8': '@babel/generator@7.29.8':
dependencies: dependencies:
'@babel/parser': 7.29.8 '@babel/parser': 7.29.8
@@ -6300,19 +6282,19 @@ snapshots:
'@babel/helper-globals@7.29.7': {} '@babel/helper-globals@7.29.7': {}
'@babel/helper-module-imports@7.29.7': '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)':
dependencies: dependencies:
'@babel/traverse': 7.29.7 '@babel/traverse': 7.29.8(supports-color@7.2.0)
'@babel/types': 7.29.8 '@babel/types': 7.29.8
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/helper-module-imports': 7.29.7 '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
'@babel/helper-validator-identifier': 7.29.7 '@babel/helper-validator-identifier': 7.29.7
'@babel/traverse': 7.29.7 '@babel/traverse': 7.29.8(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6337,25 +6319,15 @@ snapshots:
'@babel/runtime@7.29.7': {} '@babel/runtime@7.29.7': {}
'@babel/runtime@8.0.0': {}
'@babel/template@7.29.7': '@babel/template@7.29.7':
dependencies: dependencies:
'@babel/code-frame': 7.29.7 '@babel/code-frame': 7.29.7
'@babel/parser': 7.29.8 '@babel/parser': 7.29.8
'@babel/types': 7.29.8 '@babel/types': 7.29.8
'@babel/traverse@7.29.7': '@babel/traverse@7.29.8(supports-color@7.2.0)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.7
'@babel/helper-globals': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/types': 7.29.8
debug: 4.4.3
transitivePeerDependencies:
- supports-color
'@babel/traverse@7.29.8':
dependencies: dependencies:
'@babel/code-frame': 7.29.7 '@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8 '@babel/generator': 7.29.8
@@ -6363,7 +6335,7 @@ snapshots:
'@babel/parser': 7.29.8 '@babel/parser': 7.29.8
'@babel/template': 7.29.7 '@babel/template': 7.29.7
'@babel/types': 7.29.8 '@babel/types': 7.29.8
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6829,17 +6801,17 @@ snapshots:
'@esbuild/win32-x64@0.28.1': '@esbuild/win32-x64@0.28.1':
optional: true optional: true
'@eslint-community/eslint-utils@4.10.1(eslint@8.57.1)': '@eslint-community/eslint-utils@4.10.1(eslint@8.57.1(supports-color@7.2.0))':
dependencies: dependencies:
eslint: 8.57.1 eslint: 8.57.1(supports-color@7.2.0)
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {} '@eslint-community/regexpp@4.12.2': {}
'@eslint/eslintrc@2.1.4': '@eslint/eslintrc@2.1.4(supports-color@7.2.0)':
dependencies: dependencies:
ajv: 6.15.0 ajv: 6.15.0
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
espree: 9.6.1 espree: 9.6.1
globals: 13.24.0 globals: 13.24.0
ignore: 5.3.2 ignore: 5.3.2
@@ -6920,10 +6892,10 @@ snapshots:
'@formatjs/intl-localematcher': 0.6.2 '@formatjs/intl-localematcher': 0.6.2
tslib: 2.8.1 tslib: 2.8.1
'@humanwhocodes/config-array@0.13.0': '@humanwhocodes/config-array@0.13.0(supports-color@7.2.0)':
dependencies: dependencies:
'@humanwhocodes/object-schema': 2.0.3 '@humanwhocodes/object-schema': 2.0.3
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6 minimatch: 10.2.6
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -7092,7 +7064,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@livekit/components-core@0.12.14(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)': '@livekit/components-core@0.12.15(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)':
dependencies: dependencies:
'@floating-ui/dom': 1.7.6 '@floating-ui/dom': 1.7.6
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22) livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
@@ -7100,12 +7072,12 @@ snapshots:
rxjs: 7.8.2 rxjs: 7.8.2
tslib: 2.8.1 tslib: 2.8.1
'@livekit/components-react@2.9.23(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)': '@livekit/components-react@2.9.24(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)':
dependencies: dependencies:
'@livekit/components-core': 0.12.14(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1) '@livekit/components-core': 0.12.15(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
clsx: 2.1.1 clsx: 2.1.1
events: 3.3.0 events: 3.3.0
jose: 6.2.4 jose: 6.2.9
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22) livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
react: 19.2.8 react: 19.2.8
react-dom: 19.2.8(react@19.2.8) react-dom: 19.2.8(react@19.2.8)
@@ -7124,7 +7096,7 @@ snapshots:
'@types/dom-mediacapture-transform': 0.1.11 '@types/dom-mediacapture-transform': 0.1.11
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22) livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
'@matrix-org/matrix-sdk-crypto-wasm@18.3.1': {} '@matrix-org/matrix-sdk-crypto-wasm@18.4.0': {}
'@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)': '@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.8)':
dependencies: dependencies:
@@ -8075,9 +8047,9 @@ snapshots:
'@rolldown/binding-win32-x64-msvc@1.1.5': '@rolldown/binding-win32-x64-msvc@1.1.5':
optional: true optional: true
'@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
picomatch: 4.0.4 picomatch: 4.0.4
rolldown: 1.1.5 rolldown: 1.1.5
optionalDependencies: optionalDependencies:
@@ -8220,11 +8192,11 @@ snapshots:
'@sentry-internal/replay-canvas': 8.55.2 '@sentry-internal/replay-canvas': 8.55.2
'@sentry/core': 8.55.2 '@sentry/core': 8.55.2
'@sentry/bundler-plugin-core@3.6.1': '@sentry/bundler-plugin-core@3.6.1(supports-color@7.2.0)':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@sentry/babel-plugin-component-annotate': 3.6.1 '@sentry/babel-plugin-component-annotate': 3.6.1
'@sentry/cli': 2.58.5 '@sentry/cli': 2.58.5(supports-color@7.2.0)
dotenv: 16.6.1 dotenv: 16.6.1
find-up: 5.0.0 find-up: 5.0.0
glob: 10.5.0 glob: 10.5.0
@@ -8258,9 +8230,9 @@ snapshots:
'@sentry/cli-win32-x64@2.58.5': '@sentry/cli-win32-x64@2.58.5':
optional: true optional: true
'@sentry/cli@2.58.5': '@sentry/cli@2.58.5(supports-color@7.2.0)':
dependencies: dependencies:
https-proxy-agent: 5.0.1 https-proxy-agent: 5.0.1(supports-color@7.2.0)
node-fetch: 2.7.0 node-fetch: 2.7.0
progress: 2.0.3 progress: 2.0.3
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
@@ -8287,9 +8259,9 @@ snapshots:
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
react: 19.2.8 react: 19.2.8
'@sentry/vite-plugin@3.6.1': '@sentry/vite-plugin@3.6.1(supports-color@7.2.0)':
dependencies: dependencies:
'@sentry/bundler-plugin-core': 3.6.1 '@sentry/bundler-plugin-core': 3.6.1(supports-color@7.2.0)
unplugin: 1.0.1 unplugin: 1.0.1
transitivePeerDependencies: transitivePeerDependencies:
- encoding - encoding
@@ -8327,7 +8299,7 @@ snapshots:
'@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10) '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/browser-playwright': 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10) '@vitest/browser-playwright': 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/runner': 4.1.10 '@vitest/runner': 4.1.10
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
transitivePeerDependencies: transitivePeerDependencies:
- react - react
@@ -8366,16 +8338,16 @@ snapshots:
'@types/react': 19.2.17 '@types/react': 19.2.17
'@types/react-dom': 19.2.3(@types/react@19.2.17) '@types/react-dom': 19.2.3(@types/react@19.2.17)
'@storybook/react-vite@10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': '@storybook/react-vite@10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(supports-color@7.2.0)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))':
dependencies: dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
'@rollup/pluginutils': 5.4.0(rollup@4.60.1) '@rollup/pluginutils': 5.4.0(rollup@4.60.1)
'@storybook/builder-vite': 10.5.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@storybook/builder-vite': 10.5.5(esbuild@0.28.1)(rollup@4.60.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
'@storybook/react': 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(typescript@5.9.3) '@storybook/react': 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(supports-color@7.2.0)(typescript@5.9.3)
empathic: 2.0.1 empathic: 2.0.1
magic-string: 0.30.21 magic-string: 0.30.21
react: 19.2.8 react: 19.2.8
react-docgen: 8.0.3 react-docgen: 8.0.3(supports-color@7.2.0)
react-dom: 19.2.8(react@19.2.8) react-dom: 19.2.8(react@19.2.8)
resolve: 1.22.12 resolve: 1.22.12
storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8) storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8)
@@ -8391,12 +8363,12 @@ snapshots:
- supports-color - supports-color
- webpack - webpack
'@storybook/react@10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(typescript@5.9.3)': '@storybook/react@10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(supports-color@7.2.0)(typescript@5.9.3)':
dependencies: dependencies:
'@storybook/global': 5.0.0 '@storybook/global': 5.0.0
'@storybook/react-dom-shim': 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8)) '@storybook/react-dom-shim': 10.5.5(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))
react: 19.2.8 react: 19.2.8
react-docgen: 8.0.3 react-docgen: 8.0.3(supports-color@7.2.0)
react-docgen-typescript: 2.4.0(typescript@5.9.3) react-docgen-typescript: 2.4.0(typescript@5.9.3)
react-dom: 19.2.8(react@19.2.8) react-dom: 19.2.8(react@19.2.8)
storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8) storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8)
@@ -8407,54 +8379,54 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7)': '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7)': '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-preset@8.1.0(@babel/core@7.29.7)': '@svgr/babel-preset@8.1.0(@babel/core@7.29.7(supports-color@7.2.0))':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7) '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7) '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/core@8.1.0(typescript@5.9.3)': '@svgr/core@8.1.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@7.2.0))
camelcase: 6.3.0 camelcase: 6.3.0
cosmiconfig: 8.3.6(typescript@5.9.3) cosmiconfig: 8.3.6(typescript@5.9.3)
snake-case: 3.0.4 snake-case: 3.0.4
@@ -8467,11 +8439,11 @@ snapshots:
'@babel/types': 7.29.8 '@babel/types': 7.29.8
entities: 4.5.0 entities: 4.5.0
'@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@5.9.3))(supports-color@7.2.0)':
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@svgr/babel-preset': 8.1.0(@babel/core@7.29.7) '@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@7.2.0))
'@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@5.9.3)
'@svgr/hast-util-to-babel-ast': 8.0.0 '@svgr/hast-util-to-babel-ast': 8.0.0
svg-parser: 2.0.4 svg-parser: 2.0.4
transitivePeerDependencies: transitivePeerDependencies:
@@ -8679,11 +8651,11 @@ snapshots:
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
optional: true optional: true
'@typescript-eslint/project-service@8.65.0(typescript@5.9.3)': '@typescript-eslint/project-service@8.65.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3)
'@typescript-eslint/types': 8.65.0 '@typescript-eslint/types': 8.65.0
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -8699,13 +8671,13 @@ snapshots:
'@typescript-eslint/types@8.65.0': {} '@typescript-eslint/types@8.65.0': {}
'@typescript-eslint/typescript-estree@8.65.0(typescript@5.9.3)': '@typescript-eslint/typescript-estree@8.65.0(supports-color@7.2.0)(typescript@5.9.3)':
dependencies: dependencies:
'@typescript-eslint/project-service': 8.65.0(typescript@5.9.3) '@typescript-eslint/project-service': 8.65.0(supports-color@7.2.0)(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3)
'@typescript-eslint/types': 8.65.0 '@typescript-eslint/types': 8.65.0
'@typescript-eslint/visitor-keys': 8.65.0 '@typescript-eslint/visitor-keys': 8.65.0
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6 minimatch: 10.2.6
semver: 7.8.5 semver: 7.8.5
tinyglobby: 0.2.17 tinyglobby: 0.2.17
@@ -8714,13 +8686,13 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@typescript-eslint/utils@8.65.0(eslint@8.57.1)(typescript@5.9.3)': '@typescript-eslint/utils@8.65.0(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1(supports-color@7.2.0))
'@typescript-eslint/scope-manager': 8.65.0 '@typescript-eslint/scope-manager': 8.65.0
'@typescript-eslint/types': 8.65.0 '@typescript-eslint/types': 8.65.0
'@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1(supports-color@7.2.0)
typescript: 5.9.3 typescript: 5.9.3
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -8744,7 +8716,7 @@ snapshots:
'@types/react': 19.2.17 '@types/react': 19.2.17
react: 19.2.8 react: 19.2.8
'@vector-im/compound-web@9.9.0(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': '@vector-im/compound-web@10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies: dependencies:
'@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
@@ -8766,12 +8738,12 @@ snapshots:
- '@types/react-dom' - '@types/react-dom'
- react-dom - react-dom
'@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))': '@vitejs/plugin-react@6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.1 '@rolldown/pluginutils': 1.0.1
vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0) vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)
optionalDependencies: optionalDependencies:
'@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
babel-plugin-react-compiler: 1.0.0 babel-plugin-react-compiler: 1.0.0
'@vitest/browser-playwright@4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)': '@vitest/browser-playwright@4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)':
@@ -8780,7 +8752,7 @@ snapshots:
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
playwright: 1.62.0 playwright: 1.62.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
- msw - msw
@@ -8796,7 +8768,7 @@ snapshots:
pngjs: 7.0.0 pngjs: 7.0.0
sirv: 3.0.2 sirv: 3.0.2
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
ws: 8.21.1 ws: 8.21.1
transitivePeerDependencies: transitivePeerDependencies:
- bufferutil - bufferutil
@@ -8816,7 +8788,7 @@ snapshots:
obug: 2.1.4 obug: 2.1.4
std-env: 4.2.0 std-env: 4.2.0
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
optionalDependencies: optionalDependencies:
'@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10) '@vitest/browser': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)
@@ -8880,7 +8852,7 @@ snapshots:
sirv: 3.0.2 sirv: 3.0.2
tinyglobby: 0.2.17 tinyglobby: 0.2.17
tinyrainbow: 3.1.1 tinyrainbow: 3.1.1
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
'@vitest/utils@3.2.4': '@vitest/utils@3.2.4':
dependencies: dependencies:
@@ -8904,9 +8876,9 @@ snapshots:
acorn@8.18.0: {} acorn@8.18.0: {}
agent-base@6.0.2: agent-base@6.0.2(supports-color@7.2.0):
dependencies: dependencies:
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9234,7 +9206,7 @@ snapshots:
cosmiconfig@8.3.6(typescript@5.9.3): cosmiconfig@8.3.6(typescript@5.9.3):
dependencies: dependencies:
import-fresh: 3.3.1 import-fresh: 3.3.1
js-yaml: 4.3.0 js-yaml: 4.3.1
parse-json: 5.2.0 parse-json: 5.2.0
path-type: 4.0.0 path-type: 4.0.0
optionalDependencies: optionalDependencies:
@@ -9329,9 +9301,11 @@ snapshots:
whatwg-mimetype: 4.0.0 whatwg-mimetype: 4.0.0
whatwg-url: 14.2.0 whatwg-url: 14.2.0
debug@4.4.3: debug@4.4.3(supports-color@7.2.0):
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
optionalDependencies:
supports-color: 7.2.0
decamelize@1.2.0: {} decamelize@1.2.0: {}
@@ -9507,11 +9481,11 @@ snapshots:
escape-string-regexp@4.0.0: {} escape-string-regexp@4.0.0: {}
eslint-plugin-storybook@10.5.5(eslint@8.57.1)(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(typescript@5.9.3): eslint-plugin-storybook@10.5.5(eslint@8.57.1(supports-color@7.2.0))(storybook@10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8))(supports-color@7.2.0)(typescript@5.9.3):
dependencies: dependencies:
'@typescript-eslint/types': 8.65.0 '@typescript-eslint/types': 8.65.0
'@typescript-eslint/utils': 8.65.0(eslint@8.57.1)(typescript@5.9.3) '@typescript-eslint/utils': 8.65.0(eslint@8.57.1(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
eslint: 8.57.1 eslint: 8.57.1(supports-color@7.2.0)
storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8) storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -9526,20 +9500,20 @@ snapshots:
eslint-visitor-keys@5.0.1: {} eslint-visitor-keys@5.0.1: {}
eslint@8.57.1: eslint@8.57.1(supports-color@7.2.0):
dependencies: dependencies:
'@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1) '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1(supports-color@7.2.0))
'@eslint-community/regexpp': 4.12.2 '@eslint-community/regexpp': 4.12.2
'@eslint/eslintrc': 2.1.4 '@eslint/eslintrc': 2.1.4(supports-color@7.2.0)
'@eslint/js': 8.57.1 '@eslint/js': 8.57.1
'@humanwhocodes/config-array': 0.13.0 '@humanwhocodes/config-array': 0.13.0(supports-color@7.2.0)
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8 '@nodelib/fs.walk': 1.2.8
'@ungap/structured-clone': 1.3.3 '@ungap/structured-clone': 1.3.3
ajv: 6.15.0 ajv: 6.15.0
chalk: 4.1.2 chalk: 4.1.2
cross-spawn: 7.0.6 cross-spawn: 7.0.6
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
doctrine: 3.0.0 doctrine: 3.0.0
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 7.2.2 eslint-scope: 7.2.2
@@ -9655,13 +9629,15 @@ snapshots:
optionalDependencies: optionalDependencies:
picomatch: 4.0.5 picomatch: 4.0.5
fetch-mock@11.1.5: fetch-mock@11.1.5(node-fetch@2.7.0):
dependencies: dependencies:
'@types/glob-to-regexp': 0.4.4 '@types/glob-to-regexp': 0.4.4
dequal: 2.0.3 dequal: 2.0.3
glob-to-regexp: 0.4.1 glob-to-regexp: 0.4.1
is-subset: 0.1.1 is-subset: 0.1.1
regexparam: 3.0.0 regexparam: 3.0.0
optionalDependencies:
node-fetch: 2.7.0
fflate@0.4.8: {} fflate@0.4.8: {}
@@ -9859,26 +9835,26 @@ snapshots:
html-parse-stringify@4.0.1: {} html-parse-stringify@4.0.1: {}
http-proxy-agent@7.0.2: http-proxy-agent@7.0.2(supports-color@7.2.0):
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
https-browserify@1.0.0: {} https-browserify@1.0.0: {}
https-proxy-agent@5.0.1: https-proxy-agent@5.0.1(supports-color@7.2.0):
dependencies: dependencies:
agent-base: 6.0.2 agent-base: 6.0.2(supports-color@7.2.0)
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
https-proxy-agent@7.0.6: https-proxy-agent@7.0.6(supports-color@7.2.0):
dependencies: dependencies:
agent-base: 7.1.4 agent-base: 7.1.4
debug: 4.4.3 debug: 4.4.3(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -10009,7 +9985,7 @@ snapshots:
call-bind: 1.0.9 call-bind: 1.0.9
define-properties: 1.2.1 define-properties: 1.2.1
is-network-error@1.3.1: {} is-network-error@1.3.2: {}
is-number@7.0.0: {} is-number@7.0.0: {}
@@ -10079,6 +10055,8 @@ snapshots:
jose@6.2.4: {} jose@6.2.4: {}
jose@6.2.9: {}
js-tokens@10.0.0: {} js-tokens@10.0.0: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -10091,14 +10069,14 @@ snapshots:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
jsdom@26.1.0: jsdom@26.1.0(supports-color@7.2.0):
dependencies: dependencies:
cssstyle: 4.6.0 cssstyle: 4.6.0
data-urls: 5.0.0 data-urls: 5.0.0
decimal.js: 10.6.0 decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0 html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2 http-proxy-agent: 7.0.2(supports-color@7.2.0)
https-proxy-agent: 7.0.6 https-proxy-agent: 7.0.6(supports-color@7.2.0)
is-potential-custom-element-name: 1.0.1 is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.23 nwsapi: 2.2.23
parse5: 7.3.0 parse5: 7.3.0
@@ -10138,8 +10116,6 @@ snapshots:
optionalDependencies: optionalDependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jwt-decode@4.0.0: {}
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
@@ -10293,18 +10269,16 @@ snapshots:
matrix-events-sdk@0.0.1: {} matrix-events-sdk@0.0.1: {}
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7: matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0:
dependencies: dependencies:
'@babel/runtime': 7.29.2 '@babel/runtime': 8.0.0
'@matrix-org/matrix-sdk-crypto-wasm': 18.3.1 '@matrix-org/matrix-sdk-crypto-wasm': 18.4.0
another-json: 0.2.0 another-json: 0.2.0
bs58: 6.0.0 bs58: 6.0.0
content-type: 2.0.0 content-type: 2.0.0
jwt-decode: 4.0.0
loglevel: 1.9.2 loglevel: 1.9.2
matrix-events-sdk: 0.0.1 matrix-events-sdk: 0.0.1
matrix-widget-api: 1.18.0 matrix-widget-api: 1.18.0
oidc-client-ts: 3.5.0
p-retry: 8.0.0 p-retry: 8.0.0
sdp-transform: 3.0.0 sdp-transform: 3.0.0
unhomoglyph: 1.0.6 unhomoglyph: 1.0.6
@@ -10454,10 +10428,6 @@ snapshots:
obug@2.1.4: {} obug@2.1.4: {}
oidc-client-ts@3.5.0:
dependencies:
jwt-decode: 4.0.0
once@1.4.0: once@1.4.0:
dependencies: dependencies:
wrappy: 1.0.2 wrappy: 1.0.2
@@ -10642,7 +10612,7 @@ snapshots:
p-retry@8.0.0: p-retry@8.0.0:
dependencies: dependencies:
is-network-error: 1.3.1 is-network-error: 1.3.2
p-try@2.2.0: {} p-try@2.2.0: {}
@@ -11060,10 +11030,10 @@ snapshots:
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
react-docgen@8.0.3: react-docgen@8.0.3(supports-color@7.2.0):
dependencies: dependencies:
'@babel/core': 7.29.7 '@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/traverse': 7.29.8 '@babel/traverse': 7.29.8(supports-color@7.2.0)
'@babel/types': 7.29.8 '@babel/types': 7.29.8
'@types/babel__core': 7.20.5 '@types/babel__core': 7.20.5
'@types/babel__traverse': 7.28.0 '@types/babel__traverse': 7.28.0
@@ -11516,7 +11486,7 @@ snapshots:
terser@5.46.1: terser@5.46.1:
dependencies: dependencies:
'@jridgewell/source-map': 0.3.11 '@jridgewell/source-map': 0.3.11
acorn: 8.16.0 acorn: 8.18.0
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
@@ -11745,11 +11715,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
vite-plugin-svgr@4.5.0(rollup@4.60.1)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)): vite-plugin-svgr@4.5.0(rollup@4.60.1)(supports-color@7.2.0)(typescript@5.9.3)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)):
dependencies: dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.60.1) '@rollup/pluginutils': 5.3.0(rollup@4.60.1)
'@svgr/core': 8.1.0(typescript@5.9.3) '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@5.9.3)
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@5.9.3))(supports-color@7.2.0)
vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0) vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
@@ -11782,9 +11752,9 @@ snapshots:
axe-core: 4.11.3 axe-core: 4.11.3
chalk: 5.6.2 chalk: 5.6.2
lodash-es: 4.18.1 lodash-es: 4.18.1
vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)): vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@vitest/ui@4.1.10)(jsdom@26.1.0(supports-color@7.2.0))(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)):
dependencies: dependencies:
'@vitest/expect': 4.1.10 '@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)) '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
@@ -11812,7 +11782,7 @@ snapshots:
'@vitest/browser-playwright': 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10) '@vitest/browser-playwright': 4.1.10(playwright@1.62.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))(vitest@4.1.10)
'@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)
'@vitest/ui': 4.1.10(vitest@4.1.10) '@vitest/ui': 4.1.10(vitest@4.1.10)
jsdom: 26.1.0 jsdom: 26.1.0(supports-color@7.2.0)
transitivePeerDependencies: transitivePeerDependencies:
- msw - msw

View File

@@ -9,7 +9,7 @@ allowBuilds:
"@swc/core": true "@swc/core": true
"core-js": true "core-js": true
"esbuild": true "esbuild": true
"matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7": true "matrix-js-sdk@git+https://github.com/matrix-org/matrix-js-sdk.git": true
"protobufjs": true "protobufjs": true
overrides: overrides:
# We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but # We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but

View File

@@ -31,6 +31,11 @@ interface Props {
max: number; max: number;
step: number; step: number;
disabled?: boolean; disabled?: boolean;
/**
* Custom formatter for the tooltip label. If not provided, the value is
* displayed as a percentage.
*/
tooltipFormatter?: (value: number) => string;
} }
/** /**
@@ -46,6 +51,7 @@ export const Slider: FC<Props> = ({
max, max,
step, step,
disabled, disabled,
tooltipFormatter,
}) => { }) => {
const onValueChange = useCallback( const onValueChange = useCallback(
([v]: number[]) => onValueChangeProp(v), ([v]: number[]) => onValueChangeProp(v),
@@ -71,7 +77,14 @@ export const Slider: FC<Props> = ({
<Range className={styles.highlight} /> <Range className={styles.highlight} />
</Track> </Track>
{/* Note: This is expected not to be visible on mobile.*/} {/* Note: This is expected not to be visible on mobile.*/}
<Tooltip placement="top" label={Math.round(value * 100).toString() + "%"}> <Tooltip
placement="top"
label={
tooltipFormatter
? tooltipFormatter(value)
: Math.round(value * 100).toString() + "%"
}
>
<Thumb className={styles.handle} aria-label={label} /> <Thumb className={styles.handle} aria-label={label} />
</Tooltip> </Tooltip>
</Root> </Root>

View File

@@ -74,10 +74,7 @@ export interface ConfigOptions {
livekit?: { livekit?: {
// The link to the service that returns a livekit url and token to use it. // The link to the service that returns a livekit url and token to use it.
// This is a fallback link in case the homeserver in use does not advertise // This is a fallback link in case the homeserver in use does not advertise
// a livekit service url in the client well-known. // a livekit service url over the transports endpoint.
// The well known needs to be formatted like so:
// {"type":"livekit", "livekit_service_url":"https://livekit.example.com"}
// and stored under the key: "org.matrix.msc4143.rtc_foci"
livekit_service_url: string; livekit_service_url: string;
}; };
@@ -105,6 +102,61 @@ export interface ConfigOptions {
*/ */
ssla?: string; ssla?: string;
/**
* Media quality settings for video and screen sharing.
* These override the hardcoded LiveKit defaults.
*/
media_quality?: {
/**
* Video codec preference. The server must also have the codec enabled.
* @default "vp8"
*/
video_codec?: "vp8" | "vp9" | "h264" | "av1";
/**
* Camera video settings.
*/
video?: {
/** Max resolution height in pixels (e.g. 720, 1080, 1440). @default 720 */
max_resolution?: number;
/** Max bitrate in bits per second. @default 1700000 */
max_bitrate?: number;
/** Max framerate. @default 30 */
max_framerate?: number;
/**
* Simulcast layers as an array of {height, bitrate} objects,
* ordered from lowest to highest quality.
* @default [{height: 180, bitrate: 160000}, {height: 360, bitrate: 450000}]
*/
simulcast_layers?: Array<{
height: number;
bitrate: number;
}>;
};
/**
* Screen share settings.
*/
screen_share?: {
/** Max resolution height in pixels. @default 1080 */
max_resolution?: number;
/** Max bitrate in bits per second. @default 5000000 */
max_bitrate?: number;
/** Max framerate. @default 30 */
max_framerate?: number;
/**
* Simulcast layers for screen sharing as an array of {height, bitrate, framerate} objects,
* ordered from lowest to highest quality. If omitted, LiveKit SDK defaults apply (1 extra
* layer at half resolution).
*/
simulcast_layers?: Array<{
height: number;
bitrate: number;
framerate?: number;
}>;
};
};
media_devices?: { media_devices?: {
/** /**
* Defines whether participants should start with audio enabled by default. * Defines whether participants should start with audio enabled by default.
@@ -185,6 +237,24 @@ export interface ConfigOptions {
export interface ResolvedConfigOptions extends ConfigOptions { export interface ResolvedConfigOptions extends ConfigOptions {
sync_disconnect_grace_period_ms: number; sync_disconnect_grace_period_ms: number;
ssla: string; ssla: string;
media_quality: Required<
Pick<NonNullable<ConfigOptions["media_quality"]>, "video_codec">
> & {
video: Required<
Pick<
NonNullable<NonNullable<ConfigOptions["media_quality"]>["video"]>,
"max_resolution" | "max_bitrate" | "max_framerate"
>
>;
screen_share: Required<
Pick<
NonNullable<
NonNullable<ConfigOptions["media_quality"]>["screen_share"]
>,
"max_resolution" | "max_bitrate" | "max_framerate"
>
>;
};
matrix_rtc_session: { matrix_rtc_session: {
wait_for_key_rotation_ms?: number; wait_for_key_rotation_ms?: number;
delayed_leave_event_delay_ms: number; delayed_leave_event_delay_ms: number;
@@ -201,6 +271,19 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
}, },
sync_disconnect_grace_period_ms: 10000, sync_disconnect_grace_period_ms: 10000,
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf", ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
media_quality: {
video_codec: "vp8",
video: {
max_resolution: 720,
max_bitrate: 1_700_000,
max_framerate: 30,
},
screen_share: {
max_resolution: 1080,
max_bitrate: 5_000_000,
max_framerate: 30,
},
},
matrix_rtc_session: { matrix_rtc_session: {
delayed_leave_event_delay_ms: 10000, delayed_leave_event_delay_ms: 10000,
network_error_retry_ms: 1000, network_error_retry_ms: 1000,

View File

@@ -41,6 +41,8 @@ import { TileWrapper } from "./TileWrapper";
import { usePrefersReducedMotion } from "../usePrefersReducedMotion"; import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
import { useInitial } from "../useInitial"; import { useInitial } from "../useInitial";
const MAX_ANIMATED_TILES = 50; // Capped for performance reasons
interface Rect { interface Rect {
x: number; x: number;
y: number; y: number;
@@ -285,7 +287,6 @@ export function Grid<
const [visibleTilesCallback, setVisibleTilesCallback] = const [visibleTilesCallback, setVisibleTilesCallback] =
useState<VisibleTilesCallback | null>(null); useState<VisibleTilesCallback | null>(null);
const tiles = useInitial(() => new Map<string, Tile<TileModel>>()); const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
const prefersReducedMotion = usePrefersReducedMotion();
const Slot: FC<SlotProps<TileModel>> = useMemo( const Slot: FC<SlotProps<TileModel>> = useMemo(
() => () =>
@@ -372,6 +373,10 @@ export function Grid<
// react-spring's imperative API during gestures to improve responsiveness // react-spring's imperative API during gestures to improve responsiveness
const dragState = useRef<DragState | null>(null); const dragState = useRef<DragState | null>(null);
// If true, disables animations
const immediate =
usePrefersReducedMotion() || placedTiles.length > MAX_ANIMATED_TILES;
const [tileTransitions, springRef] = useTransition( const [tileTransitions, springRef] = useTransition(
placedTiles, placedTiles,
() => ({ () => ({
@@ -389,9 +394,9 @@ export function Grid<
y, y,
width, width,
height, height,
immediate: prefersReducedMotion, immediate,
}), }),
enter: { opacity: 1, scale: 1, immediate: prefersReducedMotion }, enter: { opacity: 1, scale: 1, immediate },
update: ({ update: ({
id, id,
x, x,
@@ -406,9 +411,9 @@ export function Grid<
y, y,
width, width,
height, height,
immediate: prefersReducedMotion, immediate,
}, },
leave: { opacity: 0, scale: 0, immediate: prefersReducedMotion }, leave: { opacity: 0, scale: 0, immediate },
config: { mass: 0.7, tension: 252, friction: 25 }, config: { mass: 0.7, tension: 252, friction: 25 },
}), }),
// react-spring's types are bugged and can't infer the spring type // react-spring's types are bugged and can't infer the spring type
@@ -441,8 +446,7 @@ export function Grid<
y: tile.y, y: tile.y,
width: tile.width, width: tile.width,
height: tile.height, height: tile.height,
immediate: immediate: immediate || ((key): boolean => key === "zIndex"),
prefersReducedMotion || ((key): boolean => key === "zIndex"),
// Allow the tile's position to settle before pushing its // Allow the tile's position to settle before pushing its
// z-index back down // z-index back down
delay: (key): number => (key === "zIndex" ? 500 : 0), delay: (key): number => (key === "zIndex" ? 500 : 0),
@@ -453,7 +457,7 @@ export function Grid<
x: tileX, x: tileX,
y: tileY, y: tileY,
immediate: immediate:
prefersReducedMotion || immediate ||
((key): boolean => ((key): boolean =>
key === "zIndex" || key === "x" || key === "y"), key === "zIndex" || key === "x" || key === "y"),
}, },

View File

@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { type ReactNode } from "react"; import { type FC, type ReactNode } from "react";
import { useObservableEagerState } from "observable-hooks"; import { useObservableEagerState } from "observable-hooks";
import classNames from "classnames"; import classNames from "classnames";
@@ -13,6 +13,9 @@ import { type CallLayout } from "./CallLayout";
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/layout-types.ts"; import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/layout-types.ts";
import styles from "./SpotlightLandscapeLayout.module.css"; import styles from "./SpotlightLandscapeLayout.module.css";
import { useUpdateLayout, useVisibleTiles } from "./Grid"; import { useUpdateLayout, useVisibleTiles } from "./Grid";
import { type MediaViewModel } from "../state/media/MediaViewModel.ts";
import { type Behavior } from "../state/Behavior.ts";
import { useBehavior } from "../useBehavior.ts";
/** /**
* An implementation of the "spotlight landscape" layout, in which the spotlight * An implementation of the "spotlight landscape" layout, in which the spotlight
@@ -54,16 +57,10 @@ export const makeSpotlightLandscapeLayout: CallLayout<
useUpdateLayout(); useUpdateLayout();
useVisibleTiles(model.setVisibleTiles); useVisibleTiles(model.setVisibleTiles);
useObservableEagerState(minBounds$); useObservableEagerState(minBounds$);
const withIndicators =
useObservableEagerState(model.spotlight.media$).length > 1;
return ( return (
<div ref={ref} className={styles.layer}> <div ref={ref} className={styles.layer}>
<div <SpotlightSlot media$={model.spotlight.media$} />
className={classNames(styles.spotlight, {
[styles.withIndicators]: withIndicators,
})}
/>
<div className={styles.grid}> <div className={styles.grid}>
{model.grid.map((m) => ( {model.grid.map((m) => (
<Slot key={m.id} className={styles.slot} id={m.id} model={m} /> <Slot key={m.id} className={styles.slot} id={m.id} model={m} />
@@ -73,3 +70,20 @@ export const makeSpotlightLandscapeLayout: CallLayout<
); );
}, },
}); });
interface SpotlightSlotProps {
media$: Behavior<MediaViewModel[]>;
}
// This component isolates the subscription to the spotlight media so that it
// can change without causing the whole layout to re-render
const SpotlightSlot: FC<SpotlightSlotProps> = ({ media$ }) => {
const withIndicators = useBehavior(media$).length > 1;
return (
<div
className={classNames(styles.spotlight, {
[styles.withIndicators]: withIndicators,
})}
/>
);
};

View File

@@ -30,6 +30,7 @@ import {
import { getUrlParams } from "./UrlParams"; import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { seedSettingsFromConfig } from "./settings/settings";
import { platform } from "./Platform"; import { platform } from "./Platform";
import { isFailure } from "./utils/fetch"; import { isFailure } from "./utils/fetch";
import { initializeWidget } from "./widget"; import { initializeWidget } from "./widget";
@@ -237,6 +238,7 @@ export class Initializer {
this.loadStates.config = LoadState.Loading; this.loadStates.config = LoadState.Loading;
Config.init().then( Config.init().then(
() => { () => {
seedSettingsFromConfig(Config.get().media_quality);
this.loadStates.config = LoadState.Loaded; this.loadStates.config = LoadState.Loaded;
this.initStep(resolve); this.initStep(resolve);
}, },

213
src/livekit/options.test.ts Normal file
View File

@@ -0,0 +1,213 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { describe, expect, it, vi } from "vitest";
import { VideoPresets, type VideoPreset } from "livekit-client";
import { buildLiveKitOptions, getLiveKitOptions } from "./options";
import { Config } from "../config/Config";
vi.mock("../config/Config", () => ({
Config: {
get: vi.fn(),
},
}));
describe("buildLiveKitOptions", () => {
it("returns sensible defaults with no config", () => {
const opts = buildLiveKitOptions();
expect(opts.adaptiveStream).toBe(true);
expect(opts.dynacast).toBe(true);
expect(opts.videoCaptureDefaults?.resolution).toEqual(
VideoPresets.h720.resolution,
);
expect(opts.publishDefaults?.videoCodec).toBe("vp8");
expect(opts.publishDefaults?.videoEncoding).toEqual({
maxBitrate: 1_700_000,
maxFramerate: 30,
});
expect(opts.publishDefaults?.screenShareEncoding).toEqual({
maxBitrate: 5_000_000,
maxFramerate: 30,
});
expect(opts.publishDefaults?.videoSimulcastLayers).toEqual([
VideoPresets.h180,
VideoPresets.h360,
]);
});
it("applies video codec from config", () => {
const opts = buildLiveKitOptions({ video_codec: "vp9" });
expect(opts.publishDefaults?.videoCodec).toBe("vp9");
});
it("applies video resolution and encoding from config", () => {
const baseVideoConfig = {
max_resolution: 1080,
max_bitrate: 3_000_000,
max_framerate: 60,
};
const opts1080 = buildLiveKitOptions({
video: baseVideoConfig,
});
const opts1440 = buildLiveKitOptions({
video: { ...baseVideoConfig, max_resolution: 1440 },
});
const opts2160 = buildLiveKitOptions({
video: { ...baseVideoConfig, max_resolution: 2160 },
});
expect(opts1080.videoCaptureDefaults?.resolution).toEqual(
VideoPresets.h1080.resolution,
);
expect(opts1440.videoCaptureDefaults?.resolution).toEqual(
VideoPresets.h1440.resolution,
);
expect(opts2160.videoCaptureDefaults?.resolution).toEqual(
VideoPresets.h2160.resolution,
);
expect(opts1080.publishDefaults?.videoEncoding).toEqual({
maxBitrate: 3_000_000,
maxFramerate: 60,
});
});
it("applies screen share encoding from config", () => {
const opts = buildLiveKitOptions({
screen_share: {
max_bitrate: 8_000_000,
max_framerate: 15,
},
});
expect(opts.publishDefaults?.screenShareEncoding).toEqual({
maxBitrate: 8_000_000,
maxFramerate: 15,
});
});
it("uses DEFAULT_CONFIG defaults when only resolution is set", () => {
const opts = buildLiveKitOptions({
screen_share: {
max_resolution: 720,
},
});
// Bitrate and framerate fall back to DEFAULT_CONFIG, not the preset
expect(opts.publishDefaults?.screenShareEncoding).toEqual({
maxBitrate: 5_000_000,
maxFramerate: 30,
});
});
it("maps low resolutions to the closest preset, rounding up", () => {
const expectations: [number, VideoPreset][] = [
[180, VideoPresets.h180],
[360, VideoPresets.h360],
[480, VideoPresets.h540],
[540, VideoPresets.h540],
[720, VideoPresets.h720],
];
for (const [height, preset] of expectations) {
const opts = buildLiveKitOptions({ video: { max_resolution: height } });
expect(opts.videoCaptureDefaults?.resolution).toEqual(preset.resolution);
}
});
it("screen share layers fall back to max_framerate, then 30", () => {
const fromMax = buildLiveKitOptions({
screen_share: {
max_framerate: 15,
simulcast_layers: [{ height: 540, bitrate: 1_000_000 }],
},
});
expect(
fromMax.publishDefaults?.screenShareSimulcastLayers?.[0],
).toMatchObject({ encoding: { maxFramerate: 15 } });
const fromDefault = buildLiveKitOptions({
screen_share: {
simulcast_layers: [{ height: 540, bitrate: 1_000_000 }],
},
});
expect(
fromDefault.publishDefaults?.screenShareSimulcastLayers?.[0],
).toMatchObject({ encoding: { maxFramerate: 30 } });
});
it("applies custom video simulcast layers", () => {
const opts = buildLiveKitOptions({
video: {
simulcast_layers: [
{ height: 180, bitrate: 100_000 },
{ height: 360, bitrate: 300_000 },
{ height: 540, bitrate: 600_000 },
],
max_framerate: 24,
},
});
const layers = opts.publishDefaults?.videoSimulcastLayers;
expect(layers).toHaveLength(3);
expect(layers?.[0]).toMatchObject({
width: 320,
height: 180,
encoding: { maxBitrate: 100_000, maxFramerate: 24 },
});
expect(layers?.[2]).toMatchObject({
width: 960,
height: 540,
encoding: { maxBitrate: 600_000, maxFramerate: 24 },
});
});
it("applies custom screen share simulcast layers", () => {
const opts = buildLiveKitOptions({
screen_share: {
simulcast_layers: [{ height: 540, bitrate: 1_000_000, framerate: 5 }],
},
});
const layers = opts.publishDefaults?.screenShareSimulcastLayers;
expect(layers).toHaveLength(1);
expect(layers?.[0]).toMatchObject({
width: 960,
height: 540,
encoding: { maxBitrate: 1_000_000, maxFramerate: 5 },
});
});
it("does not include screenShareSimulcastLayers when not configured", () => {
const opts = buildLiveKitOptions();
expect(opts.publishDefaults?.screenShareSimulcastLayers).toBeUndefined();
});
it("backupCodec always uses stock VP8 720p encoding", () => {
const opts = buildLiveKitOptions({
video_codec: "av1",
video: { max_bitrate: 10_000_000, max_framerate: 60 },
});
const backup = opts.publishDefaults?.backupCodec as {
codec: string;
encoding: { maxBitrate: number; maxFramerate: number };
};
expect(backup.codec).toBe("vp8");
expect(backup.encoding).toEqual(VideoPresets.h720.encoding);
});
});
describe("getLiveKitOptions", () => {
it("reads from Config singleton", () => {
vi.mocked(Config.get).mockReturnValue({
media_quality: { video_codec: "h264" },
} as ReturnType<typeof Config.get>);
const opts = getLiveKitOptions();
expect(opts.publishDefaults?.videoCodec).toBe("h264");
});
it("throws when Config is not initialized", () => {
vi.mocked(Config.get).mockImplementation(() => {
throw new Error("Config not initialized");
});
expect(() => getLiveKitOptions()).toThrow("Config not initialized");
});
});

View File

@@ -9,13 +9,83 @@ import {
AudioPresets, AudioPresets,
DefaultReconnectPolicy, DefaultReconnectPolicy,
type RoomOptions, type RoomOptions,
ScreenSharePresets,
type TrackPublishDefaults, type TrackPublishDefaults,
type VideoPreset, type VideoPreset,
VideoPresets, VideoPresets,
VideoPreset as VideoPresetClass,
} from "livekit-client"; } from "livekit-client";
const defaultLiveKitPublishOptions: TrackPublishDefaults = { import { Config } from "../config/Config";
import { DEFAULT_CONFIG, type ConfigOptions } from "../config/ConfigOptions";
/**
* Find the closest matching VideoPreset for a given height.
*/
function videoPresetForHeight(height: number): VideoPreset {
if (height <= 180) return VideoPresets.h180;
if (height <= 360) return VideoPresets.h360;
if (height <= 540) return VideoPresets.h540;
if (height <= 720) return VideoPresets.h720;
if (height <= 1080) return VideoPresets.h1080;
if (height <= 1440) return VideoPresets.h1440;
return VideoPresets.h2160;
}
/**
* Build LiveKit publish options from config, falling back to sensible defaults.
*/
function buildPublishOptions(
mediaQuality: ConfigOptions["media_quality"],
): TrackPublishDefaults {
const defaults = DEFAULT_CONFIG.media_quality;
const videoConf = mediaQuality?.video;
const screenConf = mediaQuality?.screen_share;
const codec = mediaQuality?.video_codec ?? defaults.video_codec;
// Camera video encoding
const videoEncoding = {
maxBitrate: videoConf?.max_bitrate ?? defaults.video.max_bitrate,
maxFramerate: videoConf?.max_framerate ?? defaults.video.max_framerate,
};
// Camera simulcast layers
let videoSimulcastLayers: VideoPreset[];
if (videoConf?.simulcast_layers) {
videoSimulcastLayers = videoConf.simulcast_layers.map(
(layer) =>
new VideoPresetClass(
Math.round((layer.height * 16) / 9),
layer.height,
layer.bitrate,
videoConf?.max_framerate ?? defaults.video.max_framerate,
),
);
} else {
videoSimulcastLayers = [VideoPresets.h180, VideoPresets.h360];
}
// Screen share encoding
const screenShareEncoding = {
maxBitrate: screenConf?.max_bitrate ?? defaults.screen_share.max_bitrate,
maxFramerate:
screenConf?.max_framerate ?? defaults.screen_share.max_framerate,
};
// Screen share simulcast layers
let screenShareSimulcastLayers: VideoPreset[] | undefined;
if (screenConf?.simulcast_layers) {
screenShareSimulcastLayers = screenConf.simulcast_layers.map(
(layer) =>
new VideoPresetClass(
Math.round((layer.height * 16) / 9),
layer.height,
layer.bitrate,
layer.framerate ?? screenConf?.max_framerate ?? 30,
),
);
}
return {
audioPreset: AudioPresets.music, audioPreset: AudioPresets.music,
dtx: true, dtx: true,
// disable red because the livekit server strips out red packets for clients // disable red because the livekit server strips out red packets for clients
@@ -23,15 +93,34 @@ const defaultLiveKitPublishOptions: TrackPublishDefaults = {
red: false, red: false,
forceStereo: false, forceStereo: false,
simulcast: true, simulcast: true,
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360] as VideoPreset[], videoSimulcastLayers: videoSimulcastLayers as VideoPreset[],
screenShareEncoding: ScreenSharePresets.h1080fps30.encoding, screenShareEncoding,
...(screenShareSimulcastLayers && {
screenShareSimulcastLayers: screenShareSimulcastLayers as VideoPreset[],
}),
stopMicTrackOnMute: false, stopMicTrackOnMute: false,
videoCodec: "vp8", videoCodec: codec,
videoEncoding: VideoPresets.h720.encoding, videoEncoding,
backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding }, backupCodec: {
} as const; codec: "vp8",
encoding: VideoPresets.h720.encoding,
},
} as TrackPublishDefaults;
}
export const defaultLiveKitOptions: RoomOptions = { /**
* Build LiveKit RoomOptions from config.
* Call this after Config.init() has resolved.
*/
export function buildLiveKitOptions(
mediaQuality?: ConfigOptions["media_quality"],
): RoomOptions {
const videoHeight =
mediaQuality?.video?.max_resolution ??
DEFAULT_CONFIG.media_quality.video.max_resolution;
const basePreset = videoPresetForHeight(videoHeight);
return {
// automatically manage subscribed video quality // automatically manage subscribed video quality
adaptiveStream: true, adaptiveStream: true,
@@ -40,11 +129,11 @@ export const defaultLiveKitOptions: RoomOptions = {
// capture settings // capture settings
videoCaptureDefaults: { videoCaptureDefaults: {
resolution: VideoPresets.h720.resolution, resolution: basePreset.resolution,
}, },
// publish settings // publish settings
publishDefaults: defaultLiveKitPublishOptions, publishDefaults: buildPublishOptions(mediaQuality),
// default LiveKit options that seem to be sane // default LiveKit options that seem to be sane
stopLocalTrackOnUnpublish: true, stopLocalTrackOnUnpublish: true,
@@ -52,3 +141,12 @@ export const defaultLiveKitOptions: RoomOptions = {
disconnectOnPageLeave: true, disconnectOnPageLeave: true,
webAudioMix: false, webAudioMix: false,
}; };
}
/**
* Get LiveKit options, reading from the loaded Config singleton.
* Requires Config.init() to have resolved first.
*/
export function getLiveKitOptions(): RoomOptions {
return buildLiveKitOptions(Config.get().media_quality);
}

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { import {
type ChangeEvent, type ChangeEvent,
type FC, type FC,
type ReactNode,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -29,12 +30,14 @@ import {
InlineField, InlineField,
Label, Label,
RadioControl, RadioControl,
Separator,
} from "@vector-im/compound-web"; } from "@vector-im/compound-web";
import { type Room as LivekitRoom } from "livekit-client"; import { type Room as LivekitRoom } from "livekit-client";
import { FieldRow, InputField } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { import {
type Setting,
useSetting, useSetting,
duplicateTiles as duplicateTilesSetting, duplicateTiles as duplicateTilesSetting,
debugTileLayout as debugTileLayoutSetting, debugTileLayout as debugTileLayoutSetting,
@@ -43,10 +46,26 @@ import {
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
matrixRTCMode as matrixRTCModeSetting, matrixRTCMode as matrixRTCModeSetting,
customLivekitUrl as customLivekitUrlSetting, customLivekitUrl as customLivekitUrlSetting,
advancedScreenShare as advancedScreenShareSetting,
screenShareResolution as screenShareResolutionSetting,
screenShareFramerate as screenShareFramerateSetting,
screenShareBitrate as screenShareBitrateSetting,
screenShareCodec as screenShareCodecSetting,
advancedCamera as advancedCameraSetting,
cameraResolution as cameraResolutionSetting,
cameraFramerate as cameraFramerateSetting,
cameraBitrate as cameraBitrateSetting,
cameraCodec as cameraCodecSetting,
echoCancellationSetting,
noiseSuppressionSetting,
autoGainControlSetting,
type VideoCodec,
enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting, enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting,
} from "./settings"; } from "./settings";
import { MatrixRTCMode } from "../config/ConfigOptions"; import { MatrixRTCMode } from "../config/ConfigOptions";
import styles from "./DeveloperSettingsTab.module.css"; import styles from "./DeveloperSettingsTab.module.css";
import settingsStyles from "./SettingsModal.module.css";
import { Slider } from "../Slider";
import { useUrlParams } from "../UrlParams"; import { useUrlParams } from "../UrlParams";
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
@@ -138,6 +157,185 @@ export const DeveloperSettingsTab: FC<Props> = ({
return null; return null;
}, [livekitRooms]); }, [livekitRooms]);
const MediaQualitySettings: React.FC<{
id: string;
header: string;
toggleLabel: string;
description: string;
toggleSetting: Setting<boolean>;
resolutionSetting: Setting<string>;
framerateSetting: Setting<number>;
bitrateSetting: Setting<number>;
codecSetting: Setting<VideoCodec>;
resolutionOptions: { value: string; label: string }[];
bitrateRange: { min: number; max: number; step: number };
}> = ({
id,
header,
toggleLabel,
description,
toggleSetting,
resolutionSetting,
framerateSetting,
bitrateSetting,
codecSetting,
resolutionOptions,
bitrateRange,
}): ReactNode => {
const [advancedEnabled, setAdvancedEnabled] = useSetting(toggleSetting);
const [resolution, setResolution] = useSetting(resolutionSetting);
const [framerate, setFramerate] = useSetting(framerateSetting);
const [framerateRaw, setFramerateRaw] = useState(framerate);
const [bitrate, setBitrate] = useSetting(bitrateSetting);
const [bitrateRaw, setBitrateRaw] = useState(bitrate);
const [codec, setCodec] = useSetting(codecSetting);
return (
<>
<h4>{header}</h4>
<FieldRow>
<InputField
id={`${id}Toggle`}
label={toggleLabel}
description={description}
type="checkbox"
checked={advancedEnabled}
onChange={(e): void => setAdvancedEnabled(e.target.checked)}
/>
</FieldRow>
{advancedEnabled && (
<>
<div className={settingsStyles.volumeSlider}>
<label htmlFor={`${id}Resolution`}>
{t("settings.resolution_label", "Resolution")}
</label>
<select
id={`${id}Resolution`}
value={resolution}
onChange={(e): void => setResolution(e.target.value)}
>
{resolutionOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div className={settingsStyles.volumeSlider}>
<label>
{t("settings.framerate_label", "Framerate")}
{": "}
<span className={settingsStyles.settingValue}>
{framerateRaw} fps
</span>
</label>
<Slider
label={t("settings.framerate_label", "Framerate")}
value={framerateRaw}
onValueChange={setFramerateRaw}
onValueCommit={setFramerate}
min={5}
max={60}
step={5}
tooltipFormatter={(v): string => `${v} fps`}
/>
</div>
<div className={settingsStyles.volumeSlider}>
<label>
{t("settings.bitrate_label", "Bitrate")}
{": "}
<span className={settingsStyles.settingValue}>
{(bitrateRaw / 1_000_000).toFixed(1)} Mbps
</span>
</label>
<Slider
label={t("settings.bitrate_label", "Bitrate")}
value={bitrateRaw}
onValueChange={setBitrateRaw}
onValueCommit={setBitrate}
min={bitrateRange.min}
max={bitrateRange.max}
step={bitrateRange.step}
tooltipFormatter={(v): string =>
`${(v / 1_000_000).toFixed(1)} Mbps`
}
/>
</div>
<div className={settingsStyles.volumeSlider}>
<label htmlFor={`${id}Codec`}>
{t("settings.codec_label", "Codec")}
</label>
<select
id={`${id}Codec`}
value={codec}
onChange={(e): void => setCodec(e.target.value as VideoCodec)}
>
<option value="vp8">VP8</option>
<option value="vp9">VP9</option>
<option value="h264">H.264</option>
<option value="av1">AV1</option>
</select>
</div>
</>
)}
</>
);
};
const AudioProcessingSettings: React.FC = (): ReactNode => {
const [echoCancellation, setEchoCancellation] = useSetting(
echoCancellationSetting,
);
const [noiseSuppression, setNoiseSuppression] = useSetting(
noiseSuppressionSetting,
);
const [autoGainControl, setAutoGainControl] = useSetting(
autoGainControlSetting,
);
return (
<>
<h4>{t("settings.audio_processing_header", "Audio processing")}</h4>
<p>
{t(
"settings.audio_processing_description",
"Changes apply on next call join.",
)}
</p>
<FieldRow>
<InputField
id="echoCancellation"
label={t("settings.echo_cancellation_label", "Echo cancellation")}
type="checkbox"
checked={echoCancellation}
onChange={(e): void => setEchoCancellation(e.target.checked)}
/>
</FieldRow>
<FieldRow>
<InputField
id="noiseSuppression"
label={t("settings.noise_suppression_label", "Noise suppression")}
type="checkbox"
checked={noiseSuppression}
onChange={(e): void => setNoiseSuppression(e.target.checked)}
/>
</FieldRow>
<FieldRow>
<InputField
id="autoGainControl"
label={t(
"settings.auto_gain_control_label",
"Automatic gain control",
)}
type="checkbox"
checked={autoGainControl}
onChange={(e): void => setAutoGainControl(e.target.checked)}
/>
</FieldRow>
</>
);
};
return ( return (
<> <>
<p> <p>
@@ -166,6 +364,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
id: client.getDeviceId() || "unknown", id: client.getDeviceId() || "unknown",
})} })}
</p> </p>
<Separator />
<FieldRow> <FieldRow>
<InputField <InputField
id="duplicateTiles" id="duplicateTiles"
@@ -315,6 +514,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
<ErrorMessage>{customLivekitUrlUpdateError}</ErrorMessage> <ErrorMessage>{customLivekitUrlUpdateError}</ErrorMessage>
)} )}
</EditInPlace> </EditInPlace>
<Separator />
<Heading as="h3" type="body" weight="semibold" size="lg"> <Heading as="h3" type="body" weight="semibold" size="lg">
{t("developer_mode.matrixRTCMode.title")} {t("developer_mode.matrixRTCMode.title")}
</Heading> </Heading>
@@ -403,6 +603,61 @@ export const DeveloperSettingsTab: FC<Props> = ({
</ul> </ul>
</div> </div>
))} ))}
<Separator />
<MediaQualitySettings
id="camera"
header={t("settings.camera_header", "Camera quality")}
toggleLabel={t(
"settings.advanced_camera_label",
"Advanced camera settings",
)}
description={t(
"settings.advanced_camera_description",
"Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.",
)}
toggleSetting={advancedCameraSetting}
resolutionSetting={cameraResolutionSetting}
framerateSetting={cameraFramerateSetting}
bitrateSetting={cameraBitrateSetting}
codecSetting={cameraCodecSetting}
resolutionOptions={[
{ value: "640x360", label: "360p" },
{ value: "960x540", label: "540p" },
{ value: "1280x720", label: "720p" },
{ value: "1920x1080", label: "1080p" },
{ value: "2560x1440", label: "1440p" },
]}
bitrateRange={{ min: 200_000, max: 8_000_000, step: 100_000 }}
/>
<Separator />
<MediaQualitySettings
id="screenShare"
header={t("settings.screen_share_header", "Screen sharing")}
toggleLabel={t(
"settings.advanced_screen_share_label",
"Advanced screen share settings",
)}
description={t(
"settings.advanced_screen_share_description",
"Configure resolution, framerate, bitrate, and codec for screen sharing",
)}
toggleSetting={advancedScreenShareSetting}
resolutionSetting={screenShareResolutionSetting}
framerateSetting={screenShareFramerateSetting}
bitrateSetting={screenShareBitrateSetting}
codecSetting={screenShareCodecSetting}
resolutionOptions={[
{ value: "1024x576", label: "576p" },
{ value: "1280x720", label: "720p" },
{ value: "1920x1080", label: "1080p" },
{ value: "2560x1440", label: "1440p" },
{ value: "3840x2160", label: "4K" },
]}
bitrateRange={{ min: 500_000, max: 15_000_000, step: 500_000 }}
/>
<Separator />
<AudioProcessingSettings />
<Separator />
<p>{t("developer_mode.environment_variables")}</p> <p>{t("developer_mode.environment_variables")}</p>
<pre>{JSON.stringify(env, null, 2)}</pre> <pre>{JSON.stringify(env, null, 2)}</pre>
<p>{t("developer_mode.url_params")}</p> <p>{t("developer_mode.url_params")}</p>

View File

@@ -33,3 +33,27 @@ Please see LICENSE in the repository root for full details.
.volumeSlider > p { .volumeSlider > p {
color: var(--cpd-color-text-secondary); color: var(--cpd-color-text-secondary);
} }
.volumeSlider > select {
display: block;
width: 100%;
padding: 8px 12px;
margin-top: var(--cpd-space-1x);
border: 1px solid var(--cpd-color-border-interactive-primary);
border-radius: 4px;
background-color: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-text-primary);
font-size: var(--font-size-body);
font-family: inherit;
cursor: pointer;
}
.volumeSlider > select:focus {
outline: none;
border-color: var(--cpd-color-text-link-external);
}
.settingValue {
font-weight: normal;
color: var(--cpd-color-text-secondary);
}

View File

@@ -152,7 +152,13 @@ export const SettingsModal: FC<Props> = ({
/> />
<div className={styles.volumeSlider}> <div className={styles.volumeSlider}>
<label>{t("settings.audio_tab.effect_volume_label")}</label> <label>
{t("settings.audio_tab.effect_volume_label")}
{": "}
<span className={styles.settingValue}>
{Math.round(soundVolumeRaw * 100)}%
</span>
</label>
<p>{t("settings.audio_tab.effect_volume_description")}</p> <p>{t("settings.audio_tab.effect_volume_description")}</p>
<Slider <Slider
label={t("video_tile.volume")} label={t("video_tile.volume")}

View File

@@ -17,6 +17,12 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
<p> <p>
Device ID: DEVICE123 Device ID: DEVICE123
</p> </p>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<div <div
class="_fieldRow_1bd8c0" class="_fieldRow_1bd8c0"
> >
@@ -251,10 +257,16 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
class="_message_1o4d9_86 _help-message_1o4d9_92" class="_message_1o4d9_86 _help-message_1o4d9_92"
id="radix-_r_8_" id="radix-_r_8_"
> >
Currently, no overwrite is set. Url from well-known or config is used. Currently, no overwrite is set. Url from config is used.
</span> </span>
</div> </div>
</form> </form>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<h3 <h3
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74" class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74"
> >
@@ -463,6 +475,242 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
</p> </p>
<ul /> <ul />
</div> </div>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<h4>
Camera quality
</h4>
<div
class="_fieldRow_1bd8c0"
>
<div
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
<input
aria-describedby="_r_k_"
id="cameraToggle"
type="checkbox"
/>
<label
for="cameraToggle"
>
<div
class="_checkbox_1bd8c0"
>
<svg
fill="none"
height="24"
stroke="#000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20 6-11 11-5-5"
/>
</svg>
</div>
Advanced camera settings
</label>
<p
class="_description_1bd8c0"
id="_r_k_"
>
Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.
</p>
</div>
</div>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<h4>
Screen sharing
</h4>
<div
class="_fieldRow_1bd8c0"
>
<div
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
<input
aria-describedby="_r_l_"
id="screenShareToggle"
type="checkbox"
/>
<label
for="screenShareToggle"
>
<div
class="_checkbox_1bd8c0"
>
<svg
fill="none"
height="24"
stroke="#000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20 6-11 11-5-5"
/>
</svg>
</div>
Advanced screen share settings
</label>
<p
class="_description_1bd8c0"
id="_r_l_"
>
Configure resolution, framerate, bitrate, and codec for screen sharing
</p>
</div>
</div>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<h4>
Audio processing
</h4>
<p>
Changes apply on next call join.
</p>
<div
class="_fieldRow_1bd8c0"
>
<div
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
<input
aria-describedby="_r_m_"
checked=""
id="echoCancellation"
type="checkbox"
/>
<label
for="echoCancellation"
>
<div
class="_checkbox_1bd8c0"
>
<svg
fill="none"
height="24"
stroke="#000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20 6-11 11-5-5"
/>
</svg>
</div>
Echo cancellation
</label>
</div>
</div>
<div
class="_fieldRow_1bd8c0"
>
<div
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
<input
aria-describedby="_r_n_"
checked=""
id="noiseSuppression"
type="checkbox"
/>
<label
for="noiseSuppression"
>
<div
class="_checkbox_1bd8c0"
>
<svg
fill="none"
height="24"
stroke="#000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20 6-11 11-5-5"
/>
</svg>
</div>
Noise suppression
</label>
</div>
</div>
<div
class="_fieldRow_1bd8c0"
>
<div
class="_field_1bd8c0 _checkboxField_1bd8c0"
>
<input
aria-describedby="_r_o_"
checked=""
id="autoGainControl"
type="checkbox"
/>
<label
for="autoGainControl"
>
<div
class="_checkbox_1bd8c0"
>
<svg
fill="none"
height="24"
stroke="#000"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20 6-11 11-5-5"
/>
</svg>
</div>
Automatic gain control
</label>
</div>
</div>
<div
class="_separator_13qwf_8"
data-kind="primary"
data-orientation="horizontal"
role="separator"
/>
<p> <p>
Environment variables Environment variables
</p> </p>

View File

@@ -0,0 +1,111 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { describe, expect, it, beforeEach } from "vitest";
import {
Setting,
parseResolution,
seedSettingsFromConfig,
screenShareCodec,
screenShareResolution,
screenShareFramerate,
screenShareBitrate,
cameraCodec,
cameraResolution,
cameraFramerate,
cameraBitrate,
} from "./settings";
beforeEach(() => {
localStorage.clear();
});
describe("parseResolution", () => {
it("parses a WIDTHxHEIGHT string", () => {
expect(parseResolution("1920x1080")).toEqual({
width: 1920,
height: 1080,
});
});
it("parses non-standard resolutions", () => {
expect(parseResolution("640x360")).toEqual({ width: 640, height: 360 });
});
});
describe("Setting", () => {
describe("seedFromConfig", () => {
it("updates value when no localStorage value exists", () => {
const setting = new Setting<string>("test-seed", "default");
setting.seedFromConfig("from-config");
expect(setting.getValue()).toBe("from-config");
});
it("does not override an existing localStorage value", () => {
localStorage.setItem(
"matrix-setting-test-seed-existing",
JSON.stringify("user-set"),
);
const setting = new Setting<string>("test-seed-existing", "default");
expect(setting.getValue()).toBe("user-set");
setting.seedFromConfig("from-config");
expect(setting.getValue()).toBe("user-set");
});
});
});
describe("seedSettingsFromConfig", () => {
it("does nothing when config is undefined", () => {
const codecBefore = screenShareCodec.getValue();
seedSettingsFromConfig(undefined);
expect(screenShareCodec.getValue()).toBe(codecBefore);
});
it("seeds video codec to both camera and screen share", () => {
seedSettingsFromConfig({ video_codec: "av1" });
expect(screenShareCodec.getValue()).toBe("av1");
expect(cameraCodec.getValue()).toBe("av1");
});
it("seeds screen share settings from config", () => {
seedSettingsFromConfig({
screen_share: {
max_resolution: 720,
max_framerate: 15,
max_bitrate: 2_000_000,
},
});
expect(screenShareResolution.getValue()).toBe("1280x720");
expect(screenShareFramerate.getValue()).toBe(15);
expect(screenShareBitrate.getValue()).toBe(2_000_000);
});
it("seeds camera/video settings from config", () => {
seedSettingsFromConfig({
video: {
max_resolution: 1080,
max_framerate: 60,
max_bitrate: 4_000_000,
},
});
expect(cameraResolution.getValue()).toBe("1920x1080");
expect(cameraFramerate.getValue()).toBe(60);
expect(cameraBitrate.getValue()).toBe(4_000_000);
});
it("only seeds provided fields, leaves others at defaults", () => {
const defaultFramerate = screenShareFramerate.getValue();
seedSettingsFromConfig({
screen_share: {
max_bitrate: 3_000_000,
},
});
expect(screenShareBitrate.getValue()).toBe(3_000_000);
expect(screenShareFramerate.getValue()).toBe(defaultFramerate);
});
});

View File

@@ -21,6 +21,7 @@ export class Setting<T> {
this.key = `matrix-setting-${key}`; this.key = `matrix-setting-${key}`;
const storedValue = localStorage.getItem(this.key); const storedValue = localStorage.getItem(this.key);
this.hasStoredValue = storedValue !== null;
let initialValue = defaultValue; let initialValue = defaultValue;
if (storedValue !== null) { if (storedValue !== null) {
try { try {
@@ -40,6 +41,7 @@ export class Setting<T> {
} }
private readonly key: string; private readonly key: string;
private readonly hasStoredValue: boolean;
private readonly _value$: BehaviorSubject<T>; private readonly _value$: BehaviorSubject<T>;
private readonly _lastUpdateReason$: BehaviorSubject<string | null>; private readonly _lastUpdateReason$: BehaviorSubject<string | null>;
@@ -54,6 +56,17 @@ export class Setting<T> {
public readonly getValue = (): T => { public readonly getValue = (): T => {
return this._value$.getValue(); return this._value$.getValue();
}; };
/**
* Update the setting's value from a config source, but only if the user
* hasn't explicitly set a value in localStorage. This lets admins set
* org-wide defaults in config.json that users can override.
*/
public seedFromConfig(value: T): void {
if (!this.hasStoredValue) {
this._value$.next(value);
}
}
} }
/** /**
@@ -144,3 +157,130 @@ export const customLivekitUrl = new Setting<string | null>(
"custom-livekit-url", "custom-livekit-url",
null, null,
); );
export type VideoCodec = "vp8" | "vp9" | "h264" | "av1";
/**
* Parse a "WIDTHxHEIGHT" resolution string into numeric width and height.
*/
export function parseResolution(res: string): {
width: number;
height: number;
} {
const [w, h] = res.split("x").map(Number);
return { width: w, height: h };
}
export const advancedScreenShare = new Setting<boolean>(
"advanced-screen-share",
false,
);
export const screenShareResolution = new Setting<string>(
"screen-share-resolution",
"1920x1080",
);
export const screenShareFramerate = new Setting<number>(
"screen-share-framerate",
30,
);
export const screenShareBitrate = new Setting<number>(
"screen-share-bitrate",
5_000_000,
);
export const screenShareCodec = new Setting<VideoCodec>(
"screen-share-codec",
"vp9",
);
// Camera video quality settings
export const advancedCamera = new Setting<boolean>("advanced-camera", false);
export const cameraResolution = new Setting<string>(
"camera-resolution",
"1280x720",
);
export const cameraFramerate = new Setting<number>("camera-framerate", 30);
export const cameraBitrate = new Setting<number>("camera-bitrate", 1_700_000);
export const cameraCodec = new Setting<VideoCodec>("camera-codec", "vp8");
// Audio processing settings
export const echoCancellationSetting = new Setting<boolean>(
"echo-cancellation",
true,
);
export const noiseSuppressionSetting = new Setting<boolean>(
"noise-suppression",
true,
);
export const autoGainControlSetting = new Setting<boolean>(
"auto-gain-control",
true,
);
/**
* Seed setting defaults from config.json's media_quality section.
* Call this after Config.init() has resolved.
* Only updates settings that the user hasn't explicitly set in localStorage.
*/
export function seedSettingsFromConfig(
mediaQuality:
| {
video_codec?: VideoCodec;
video?: {
max_resolution?: number;
max_bitrate?: number;
max_framerate?: number;
};
screen_share?: {
max_resolution?: number;
max_bitrate?: number;
max_framerate?: number;
};
}
| undefined,
): void {
if (!mediaQuality) return;
const codec = mediaQuality.video_codec;
if (codec) {
screenShareCodec.seedFromConfig(codec);
cameraCodec.seedFromConfig(codec);
}
const screen = mediaQuality.screen_share;
if (screen) {
if (screen.max_resolution) {
const width = Math.round((screen.max_resolution * 16) / 9);
screenShareResolution.seedFromConfig(`${width}x${screen.max_resolution}`);
}
if (screen.max_framerate) {
screenShareFramerate.seedFromConfig(screen.max_framerate);
}
if (screen.max_bitrate) {
screenShareBitrate.seedFromConfig(screen.max_bitrate);
}
}
const video = mediaQuality.video;
if (video) {
if (video.max_resolution) {
const width = Math.round((video.max_resolution * 16) / 9);
cameraResolution.seedFromConfig(`${width}x${video.max_resolution}`);
}
if (video.max_framerate) {
cameraFramerate.seedFromConfig(video.max_framerate);
}
if (video.max_bitrate) {
cameraBitrate.seedFromConfig(video.max_bitrate);
}
}
}

View File

@@ -19,7 +19,10 @@ import { useClient } from "../ClientContext";
import { Config } from "../config/Config"; import { Config } from "../config/Config";
import { type RageshakeRequestModal } from "../room/RageshakeRequestModal"; import { type RageshakeRequestModal } from "../room/RageshakeRequestModal";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
import { deepCompare } from "matrix-js-sdk/lib/utils";
import { advancedCamera as advancedCameraSetting } from "./settings";
import { advancedScreenShare as advancedScreenShareSetting } from "./settings";
import { DEFAULT_CONFIG } from "../config/ConfigOptions";
const gzip = async (text: string): Promise<Blob> => { const gzip = async (text: string): Promise<Blob> => {
// pako is relatively large (200KB), so we only import it when needed // pako is relatively large (200KB), so we only import it when needed
const { gzip: pakoGzip } = await import("pako"); const { gzip: pakoGzip } = await import("pako");
@@ -245,6 +248,20 @@ export function useSubmitRageshake(
} }
} }
// Add custom media related information to the rageshake issue description.
// Used to quickly identify issues due to untested configurations.
if (
!deepCompare(Config.get().media_quality, DEFAULT_CONFIG.media_quality)
) {
body.append("custom_media_quality_in_config", "true");
}
if (advancedCameraSetting.getValue()) {
body.append("devTools_advancedCameraSettings", "true");
}
if (advancedScreenShareSetting.getValue()) {
body.append("devTools_advancedScreenShareSetting", "true");
}
if (navigator.storage && navigator.storage.estimate) { if (navigator.storage && navigator.storage.estimate) {
try { try {
const estimate: { const estimate: {

View File

@@ -22,6 +22,8 @@ import { useSubmitRageshake, getRageshakeSubmitUrl } from "./submit-rageshake";
import { ClientContextProvider } from "../ClientContext"; import { ClientContextProvider } from "../ClientContext";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../UrlParams";
import { mockConfig } from "../utils/test"; import { mockConfig } from "../utils/test";
import { DEFAULT_CONFIG } from "../config/ConfigOptions";
import { advancedCamera, advancedScreenShare } from "./settings";
vi.mock("../UrlParams", () => ({ getUrlParams: vi.fn() })); vi.mock("../UrlParams", () => ({ getUrlParams: vi.fn() }));
@@ -201,6 +203,60 @@ describe("useSubmitRageshake", () => {
}); });
}); });
describe("media quality metadata", () => {
const submitAndGetBody = async (): Promise<FormData> => {
const fetchFn = vi.fn().mockResolvedValue({
status: 200,
});
vi.stubGlobal("fetch", fetchFn);
renderWithMockClient(() => "https://rageshake.localhost/foo", false);
screen.getByTestId("submit").click();
await waitFor(() => {
expect(screen.getByTestId("sent").textContent).toBe("true");
});
return fetchFn.mock.calls[0][1].body as FormData;
};
beforeEach(() => {
vi.unstubAllGlobals();
});
afterEach(() => {
advancedCamera.setValue(advancedCamera.defaultValue);
advancedScreenShare.setValue(advancedScreenShare.defaultValue);
vi.clearAllMocks();
});
it("omits media quality fields when config and settings are default", async () => {
mockConfig({});
const body = await submitAndGetBody();
expect(body.get("custom_media_quality_in_config")).toBeNull();
expect(body.get("devTools_advancedCameraSettings")).toBeNull();
expect(body.get("devTools_advancedScreenShareSetting")).toBeNull();
});
it("includes custom_media_quality_in_config when media_quality differs from default", async () => {
mockConfig({
media_quality: {
...DEFAULT_CONFIG.media_quality,
video_codec: "h264",
},
});
const body = await submitAndGetBody();
expect(body.get("custom_media_quality_in_config")).toBe("true");
});
it("includes devTools flags when advanced media settings are enabled", async () => {
mockConfig({});
advancedCamera.setValue(true);
advancedScreenShare.setValue(true);
const body = await submitAndGetBody();
expect(body.get("devTools_advancedCameraSettings")).toBe("true");
expect(body.get("devTools_advancedScreenShareSetting")).toBe("true");
});
});
describe("when rageshake is not available", () => { describe("when rageshake is not available", () => {
it("starts unsent", () => { it("starts unsent", () => {
renderWithMockClient(() => undefined, false); renderWithMockClient(() => undefined, false);

View File

@@ -513,8 +513,6 @@ export function createCallViewModel$(
livekitKeyProvider, livekitKeyProvider,
getUrlParams().controlledAudioDevices, getUrlParams().controlledAudioDevices,
options.livekitRoomFactory, options.livekitRoomFactory,
getUrlParams().echoCancellation,
getUrlParams().noiseSuppression,
); );
const connectionManager = createConnectionManager$({ const connectionManager = createConnectionManager$({

View File

@@ -138,9 +138,6 @@ export function withCallViewModel(mode: MatrixRTCMode) {
public getSyncState(): SyncState { public getSyncState(): SyncState {
return syncState; return syncState;
} }
public getAccessToken(): string | null {
return "a-token";
}
})() as Partial<MatrixClient> as MatrixClient, })() as Partial<MatrixClient> as MatrixClient,
getMembers: () => roomMembers, getMembers: () => roomMembers,
getMembersWithMembership: () => roomMembers, getMembersWithMembership: () => roomMembers,

View File

@@ -20,7 +20,6 @@ import {
afterAll, afterAll,
beforeEach, beforeEach,
} from "vitest"; } from "vitest";
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
import { BehaviorSubject, map, of } from "rxjs"; import { BehaviorSubject, map, of } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type LocalParticipant, type LocalTrack } from "livekit-client"; import { type LocalParticipant, type LocalTrack } from "livekit-client";
@@ -78,34 +77,10 @@ describe("LocalMembership", () => {
livekit_alias: "my-oldest-member-service-alias", livekit_alias: "my-oldest-member-service-alias",
}; };
const focusConfigFromWellKnown = {
type: "livekit",
livekit_service_url: "http://my-well-known-service-url.com",
};
const focusConfigFromWellKnown2 = {
type: "livekit",
livekit_service_url: "http://my-well-known-service-url2.com",
};
const clientWellKnown = {
"org.matrix.msc4143.rtc_foci": [
focusConfigFromWellKnown,
focusConfigFromWellKnown2,
],
};
mockConfig({ mockConfig({
livekit: { livekit_service_url: "http://my-default-service-url.com" }, livekit: { livekit_service_url: "http://my-default-service-url.com" },
}); });
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockImplementation(
async (domain) => {
if (domain === "example.org") {
return Promise.resolve(clientWellKnown);
}
return Promise.resolve({});
},
);
const mockedSession = vi.mocked({ const mockedSession = vi.mocked({
room: { room: {
roomId: "roomId", roomId: "roomId",
@@ -132,7 +107,7 @@ describe("LocalMembership", () => {
ownMemberMock, ownMemberMock,
{ {
livekit_alias: "roomId", livekit_alias: "roomId",
livekit_service_url: "http://my-well-known-service-url.com", livekit_service_url: "http://my-livekit-service-url.com",
type: "livekit", type: "livekit",
}, },
{ {
@@ -150,7 +125,7 @@ describe("LocalMembership", () => {
[ [
{ {
livekit_alias: "roomId", livekit_alias: "roomId",
livekit_service_url: "http://my-well-known-service-url.com", livekit_service_url: "http://my-livekit-service-url.com",
type: "livekit", type: "livekit",
}, },
], ],
@@ -161,50 +136,6 @@ describe("LocalMembership", () => {
}), }),
); );
}); });
it("It should not fail with configuration error if homeserver config has livekit url but not fallback", () => {
mockConfig({});
vi.spyOn(AutoDiscovery, "getRawClientConfig").mockResolvedValue({
"org.matrix.msc4143.rtc_foci": [
{
type: "livekit",
livekit_service_url: "http://my-well-known-service-url.com",
},
],
});
const mockedSession = vi.mocked({
room: {
roomId: "roomId",
client: {
getDomain: vi.fn().mockReturnValue("example.org"),
getOpenIdToken: vi.fn().mockResolvedValue({
access_token: "ACCCESS_TOKEN",
token_type: "Bearer",
matrix_server_name: "localhost",
expires_in: 10000,
}),
},
},
memberships: [],
getFocusInUse: vi.fn(),
joinRTCSession: vi.fn(),
}) as unknown as MatrixRTCSession;
enterRTCSession(
mockedSession,
ownMemberMock,
{
livekit_alias: "roomId",
livekit_service_url: "http://my-well-known-service-url.com",
type: "livekit",
},
{
encryptMedia: true,
matrixRTCMode: MATRIX_RTC_MODE,
},
);
});
}); });
const defaultCreateLocalMemberValues = { const defaultCreateLocalMemberValues = {

View File

@@ -10,6 +10,7 @@ import {
ParticipantEvent, ParticipantEvent,
type LocalParticipant, type LocalParticipant,
type ScreenShareCaptureOptions, type ScreenShareCaptureOptions,
type TrackPublishOptions,
RoomEvent, RoomEvent,
MediaDeviceFailure, MediaDeviceFailure,
} from "livekit-client"; } from "livekit-client";
@@ -53,6 +54,14 @@ import {
import { ElementWidgetActions, widget } from "../../../widget.ts"; import { ElementWidgetActions, widget } from "../../../widget.ts";
import { getUrlParams } from "../../../UrlParams.ts"; import { getUrlParams } from "../../../UrlParams.ts";
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts"; import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts";
import {
advancedScreenShare,
screenShareResolution,
screenShareFramerate,
screenShareBitrate,
screenShareCodec,
parseResolution,
} from "../../../settings/settings.ts";
import { MatrixRTCMode } from "../../../config/ConfigOptions.ts"; import { MatrixRTCMode } from "../../../config/ConfigOptions.ts";
import { Config } from "../../../config/Config.ts"; import { Config } from "../../../config/Config.ts";
import { import {
@@ -108,7 +117,6 @@ export type LocalMemberState =
}; };
/* /*
* - get well known
* - get oldest membership * - get oldest membership
* - get transport to use * - get transport to use
* - get openId + jwt token * - get openId + jwt token
@@ -719,6 +727,43 @@ export const createLocalMembership$ = ({
surfaceSwitching: "include", surfaceSwitching: "include",
systemAudio: "include", systemAudio: "include",
}; };
let publishOptions: TrackPublishOptions | undefined;
if (advancedScreenShare.getValue()) {
// User has advanced screen share settings enabled
const { width, height } = parseResolution(
screenShareResolution.getValue(),
);
const fps = screenShareFramerate.getValue();
const bps = screenShareBitrate.getValue();
const codec = screenShareCodec.getValue();
screenshareSettings.resolution = {
width,
height,
frameRate: fps,
};
publishOptions = {
screenShareEncoding: {
maxBitrate: bps,
maxFramerate: fps,
},
videoCodec: codec,
};
} else {
// Fall back to config.json settings if available
const screenConf = Config.get().media_quality?.screen_share;
if (screenConf?.max_resolution) {
screenshareSettings.resolution = {
width: Math.round((screenConf.max_resolution * 16) / 9),
height: screenConf.max_resolution,
frameRate: screenConf.max_framerate ?? 30,
};
}
}
const targetScreenshareState = !sharingScreen$.value; const targetScreenshareState = !sharingScreen$.value;
logger.info( logger.info(
`toggleScreenSharing called. Switching ${ `toggleScreenSharing called. Switching ${
@@ -734,7 +779,11 @@ export const createLocalMembership$ = ({
// is still initializing or publishing tracks, because there's no // is still initializing or publishing tracks, because there's no
// technical reason to disallow this. LiveKit will publish if it can. // technical reason to disallow this. LiveKit will publish if it can.
participant$.value participant$.value
?.setScreenShareEnabled(targetScreenshareState, screenshareSettings) ?.setScreenShareEnabled(
targetScreenshareState,
screenshareSettings,
publishOptions,
)
.catch(logger.error); .catch(logger.error);
}; };
} }

View File

@@ -63,8 +63,7 @@ describe("LocalTransport", () => {
client: { client: {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => Promise.resolve([]), _unstable_getRTCTransports: async () => Promise.resolve([]),
getAccessToken: vi.fn().mockReturnValue("access_token"), getDomain: () => "example.org",
getDomain: () => "",
baseUrl: "example.org", baseUrl: "example.org",
// These won't be called in this error path but satisfy the type // These won't be called in this error path but satisfy the type
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
@@ -77,9 +76,11 @@ describe("LocalTransport", () => {
await flushPromises(); await flushPromises();
expect(() => advertised$.value).toThrow( expect(() => advertised$.value).toThrow(
new MatrixRTCTransportMissingError(""), new MatrixRTCTransportMissingError("example.org"),
);
expect(() => active$.value).toThrow(
new MatrixRTCTransportMissingError("example.org"),
); );
expect(() => active$.value).toThrow(new MatrixRTCTransportMissingError(""));
}); });
it("throws FailToGetOpenIdToken when OpenID fetch fails", async () => { it("throws FailToGetOpenIdToken when OpenID fetch fails", async () => {
@@ -103,10 +104,8 @@ describe("LocalTransport", () => {
useOldestMember: false, useOldestMember: false,
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
baseUrl: "https://lk.example.org", baseUrl: "https://example.org",
// Use empty domain to skip .well-known and use config directly getDomain: () => "example.org",
getDomain: () => "",
getAccessToken: vi.fn().mockReturnValue("access_token"),
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => Promise.resolve([]), _unstable_getRTCTransports: async () => Promise.resolve([]),
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
@@ -150,11 +149,10 @@ describe("LocalTransport", () => {
client: { client: {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => Promise.resolve([]), _unstable_getRTCTransports: async () => Promise.resolve([]),
getDomain: () => "", getDomain: () => "example.org",
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
baseUrl: "https://lk.example.org", baseUrl: "https://example.org",
getAccessToken: vi.fn().mockReturnValue("access_token"),
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
forceJwtEndpoint: JwtEndpointVersion.Legacy, forceJwtEndpoint: JwtEndpointVersion.Legacy,
@@ -221,13 +219,12 @@ describe("LocalTransport", () => {
useOldestMember: true, useOldestMember: true,
memberships$: scope.behavior(memberships$.pipe(trackEpoch())), memberships$: scope.behavior(memberships$.pipe(trackEpoch())),
client: { client: {
getDomain: () => "", getDomain: () => "example.org",
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => Promise.resolve([]), _unstable_getRTCTransports: async () => Promise.resolve([]),
getAccessToken: vi.fn().mockReturnValue("access_token"),
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
baseUrl: "https://lk.example.org", baseUrl: "https://example.org",
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
forceJwtEndpoint: JwtEndpointVersion.Legacy, forceJwtEndpoint: JwtEndpointVersion.Legacy,
@@ -278,14 +275,13 @@ describe("LocalTransport", () => {
useOldestMember: true, useOldestMember: true,
memberships$: scope.behavior(memberships$.pipe(trackEpoch())), memberships$: scope.behavior(memberships$.pipe(trackEpoch())),
client: { client: {
getDomain: () => "", getDomain: () => "example.org",
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => _unstable_getRTCTransports: async () =>
Promise.resolve([aliceTransport]), Promise.resolve([aliceTransport]),
getAccessToken: vi.fn().mockReturnValue("access_token"),
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
baseUrl: "https://lk.example.org", baseUrl: "https://example.org",
}, },
ownMembershipIdentity: ownMemberMock, ownMembershipIdentity: ownMemberMock,
forceJwtEndpoint: JwtEndpointVersion.Legacy, forceJwtEndpoint: JwtEndpointVersion.Legacy,
@@ -330,10 +326,9 @@ describe("LocalTransport", () => {
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
baseUrl: "https://example.org", baseUrl: "https://example.org",
getDomain: vi.fn().mockReturnValue(""), getDomain: vi.fn().mockReturnValue("example.org"),
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]), _unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
getAccessToken: vi.fn().mockReturnValue("access_token"),
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
}, },
@@ -421,42 +416,6 @@ describe("LocalTransport", () => {
}); });
}); });
it("Should not call _unstable_getRTCTransports in widget mode but use well-known", async () => {
mockConfig({
livekit: { livekit_service_url: "https://do-not-use.lk.example.org" },
});
localTransportOpts.client.getDomain.mockReturnValue("example.org");
fetchMock.getOnce("https://example.org/.well-known/matrix/client", {
"org.matrix.msc4143.rtc_foci": [
{
type: "livekit",
livekit_service_url: "https://use-me.jwt.call.example.org",
},
],
});
localTransportOpts.client.getAccessToken.mockReturnValue(null);
const { advertised$, active$ } =
createLocalTransport$(localTransportOpts);
openIdResolver.resolve?.(openIdResponse);
expect(advertised$.value).toBe(null);
expect(active$.value).toBe(null);
await flushPromises();
expect(
localTransportOpts.client._unstable_getRTCTransports,
).not.toHaveBeenCalled();
const expectedTransport = {
type: "livekit",
livekit_service_url: "https://use-me.jwt.call.example.org",
};
expect(advertised$.value).toStrictEqual(expectedTransport);
});
it("fails fast if the openID request fails for backend config", async () => { it("fails fast if the openID request fails for backend config", async () => {
localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([ localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([
{ type: "livekit", livekit_service_url: "https://lk.example.org" }, { type: "livekit", livekit_service_url: "https://lk.example.org" },
@@ -469,51 +428,6 @@ describe("LocalTransport", () => {
).rejects.toThrow(expect.any(FailToGetOpenIdToken)); ).rejects.toThrow(expect.any(FailToGetOpenIdToken));
}); });
it("supports getting transport via well-known", async () => {
localTransportOpts.client.getDomain.mockReturnValue("example.org");
fetchMock.getOnce("https://example.org/.well-known/matrix/client", {
"org.matrix.msc4143.rtc_foci": [
{ type: "livekit", livekit_service_url: "https://lk.example.org" },
],
});
const { advertised$, active$ } =
createLocalTransport$(localTransportOpts);
openIdResolver.resolve?.(openIdResponse);
expect(advertised$.value).toBe(null);
expect(active$.value).toBe(null);
await flushPromises();
const expectedTransport = {
livekit_service_url: "https://lk.example.org",
type: "livekit",
};
expect(advertised$.value).toStrictEqual(expectedTransport);
expect(active$.value).toStrictEqual({
transport: expectedTransport,
sfuConfig: {
jwt: "e30=.eyJzdWIiOiJAbWU6ZXhhbXBsZS5vcmc6QUJDREVGIiwidmlkZW8iOnsicm9vbSI6IiFleGFtcGxlX3Jvb21faWQifX0=.e30=",
livekitAlias: "Akph4alDMhen",
livekitIdentity: "@lk_user:ABCDEF",
url: "https://lk.example.org",
},
});
expect(fetchMock.done()).toEqual(true);
});
it("fails fast if the openId request fails for the well-known config", async () => {
localTransportOpts.client.getDomain.mockReturnValue("example.org");
fetchMock.getOnce("https://example.org/.well-known/matrix/client", {
"org.matrix.msc4143.rtc_foci": [
{ type: "livekit", livekit_service_url: "https://lk.example.org" },
],
});
openIdResolver.reject(
new FailToGetOpenIdToken(new Error("Test driven error")),
);
await expect(async () =>
lastValueFrom(createLocalTransport$(localTransportOpts).active$),
).rejects.toThrow(expect.any(FailToGetOpenIdToken));
});
it("throws if no options are available", async () => { it("throws if no options are available", async () => {
const { advertised$, active$ } = createLocalTransport$({ const { advertised$, active$ } = createLocalTransport$({
scope: testScope(), scope: testScope(),
@@ -524,11 +438,10 @@ describe("LocalTransport", () => {
delayId$: constant(null), delayId$: constant(null),
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
getDomain: () => "", getDomain: () => "example.org",
baseUrl: "https://example.org", baseUrl: "https://example.org",
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => Promise.resolve([]), _unstable_getRTCTransports: async () => Promise.resolve([]),
getAccessToken: vi.fn().mockReturnValue("access_token"),
// These won't be called in this error path but satisfy the type // These won't be called in this error path but satisfy the type
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
@@ -537,10 +450,10 @@ describe("LocalTransport", () => {
await flushPromises(); await flushPromises();
expect(() => advertised$.value).toThrow( expect(() => advertised$.value).toThrow(
new MatrixRTCTransportMissingError(""), new MatrixRTCTransportMissingError("example.org"),
); );
expect(() => active$.value).toThrow( expect(() => active$.value).toThrow(
new MatrixRTCTransportMissingError(""), new MatrixRTCTransportMissingError("example.org"),
); );
}); });
}); });
@@ -565,11 +478,10 @@ describe("LocalTransport", () => {
delayId$: delayId$, delayId$: delayId$,
memberships$: constant(new Epoch<CallMembership[]>([])), memberships$: constant(new Epoch<CallMembership[]>([])),
client: { client: {
getDomain: () => "", getDomain: () => "example.org",
baseUrl: "https://example.org", baseUrl: "https://example.org",
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
_unstable_getRTCTransports: async () => Promise.resolve([]), _unstable_getRTCTransports: async () => Promise.resolve([]),
getAccessToken: vi.fn().mockReturnValue("access_token"),
// These won't be called in this error path but satisfy the type // These won't be called in this error path but satisfy the type
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),

View File

@@ -26,7 +26,6 @@ import {
tap, tap,
} from "rxjs"; } from "rxjs";
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import { type Behavior } from "../../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
@@ -57,7 +56,7 @@ interface Props {
memberships$: Behavior<Epoch<CallMembership[]>>; memberships$: Behavior<Epoch<CallMembership[]>>;
client: Pick< client: Pick<
MatrixClient, MatrixClient,
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" "getDomain" | "baseUrl" | "_unstable_getRTCTransports"
> & > &
OpenIDClientParts; OpenIDClientParts;
// Used by the jwt service to create the livekit room and compute the livekit alias. // Used by the jwt service to create the livekit room and compute the livekit alias.
@@ -150,7 +149,6 @@ export const createLocalTransport$ = ({
const transportDiscovery = new RtcTransportAutoDiscovery({ const transportDiscovery = new RtcTransportAutoDiscovery({
client: client, client: client,
resolvedConfig: Config.get(), resolvedConfig: Config.get(),
wellKnownFetcher: AutoDiscovery.getRawClientConfig.bind(AutoDiscovery),
logger: logger, logger: logger,
}); });
@@ -309,7 +307,7 @@ async function doOpenIdAndJWTFromUrl(
roomId: string, roomId: string,
client: Pick< client: Pick<
MatrixClient, MatrixClient,
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" "getDomain" | "baseUrl" | "_unstable_getRTCTransports"
> & > &
OpenIDClientParts, OpenIDClientParts,
delayId?: string, delayId?: string,
@@ -339,7 +337,7 @@ function observeLocalTransportForOldestMembership(
preferredTransport$: Observable<LocalTransportWithSFUConfig>, preferredTransport$: Observable<LocalTransportWithSFUConfig>,
client: Pick< client: Pick<
MatrixClient, MatrixClient,
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken" "getDomain" | "baseUrl" | "_unstable_getRTCTransports"
> & > &
OpenIDClientParts, OpenIDClientParts,
ownMembershipIdentity: CallMembershipIdentityParts, ownMembershipIdentity: CallMembershipIdentityParts,

View File

@@ -13,7 +13,7 @@ import {
type MockedObject, type MockedObject,
vi, vi,
} from "vitest"; } from "vitest";
import { type IClientWellKnown, MatrixError } from "matrix-js-sdk"; import { MatrixError } from "matrix-js-sdk";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { import {
type LivekitTransportConfig, type LivekitTransportConfig,
@@ -33,9 +33,9 @@ const backendTransport: LivekitTransportConfig = {
livekit_service_url: "https://backend.example.org", livekit_service_url: "https://backend.example.org",
}; };
const wellKnownTransport: LivekitTransportConfig = { const configTransport: LivekitTransportConfig = {
type: "livekit", type: "livekit",
livekit_service_url: "https://well-known.example.org", livekit_service_url: "https://config.example.org",
}; };
function makeClient(): MockedObject<DiscoveryClient> { function makeClient(): MockedObject<DiscoveryClient> {
@@ -43,7 +43,6 @@ function makeClient(): MockedObject<DiscoveryClient> {
getDomain: vi.fn().mockReturnValue("example.org"), getDomain: vi.fn().mockReturnValue("example.org"),
baseUrl: "https://matrix.example.org", baseUrl: "https://matrix.example.org",
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]), _unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
getAccessToken: vi.fn().mockReturnValue("access_token"),
getOpenIdToken: vi.fn(), getOpenIdToken: vi.fn(),
getDeviceId: vi.fn(), getDeviceId: vi.fn(),
} as unknown as MockedObject<DiscoveryClient>; } as unknown as MockedObject<DiscoveryClient>;
@@ -59,12 +58,6 @@ function makeResolvedConfig(livekitServiceUrl?: string): ResolvedConfigOptions {
} as ResolvedConfigOptions; } as ResolvedConfigOptions;
} }
function makeWellKnown(rtcFoci?: Transport[]): IClientWellKnown {
return {
"org.matrix.msc4143.rtc_foci": rtcFoci,
} as unknown as IClientWellKnown;
}
describe("RtcTransportAutoDiscovery", () => { describe("RtcTransportAutoDiscovery", () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
@@ -75,33 +68,27 @@ describe("RtcTransportAutoDiscovery", () => {
{ transports: [{ type: "not_livekit" }, backendTransport] }, { transports: [{ type: "not_livekit" }, backendTransport] },
]; ];
it.each(VALID_TEST_CASES)( it.each(VALID_TEST_CASES)(
"prefers backend transport over well-known and app config $transports", "prefers backend transport other app config $transports",
async ({ transports }) => { async ({ transports }) => {
// it("prefers backend transport over well-known and app config", async () => {
const client = makeClient(); const client = makeClient();
client._unstable_getRTCTransports.mockResolvedValue(transports); client._unstable_getRTCTransports.mockResolvedValue(transports);
const wellKnownFetcher = vi
.fn<(domain: string) => Promise<IClientWellKnown>>()
.mockResolvedValue(makeWellKnown([wellKnownTransport]));
const discovery = new RtcTransportAutoDiscovery({ const discovery = new RtcTransportAutoDiscovery({
client, client,
resolvedConfig: makeResolvedConfig("https://config.example.org"), resolvedConfig: makeResolvedConfig(configTransport.livekit_service_url),
wellKnownFetcher,
logger: rootLogger, logger: rootLogger,
}); });
await expect( const discoveredTransport = await discovery.discoverPreferredTransport();
discovery.discoverPreferredTransport(),
).resolves.toStrictEqual(backendTransport); expect(discoveredTransport).toStrictEqual(backendTransport);
expect(discoveredTransport).not.toStrictEqual(configTransport);
expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(1); expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(1);
expect(wellKnownFetcher).not.toHaveBeenCalled();
}, },
); );
it("Retries limit_exceeded backend transport over well-known", async () => { it("Retries limit_exceeded backend transport", async () => {
const client = makeClient(); const client = makeClient();
client._unstable_getRTCTransports client._unstable_getRTCTransports
.mockRejectedValueOnce( .mockRejectedValueOnce(
@@ -116,14 +103,9 @@ describe("RtcTransportAutoDiscovery", () => {
) )
.mockResolvedValue([backendTransport]); .mockResolvedValue([backendTransport]);
const wellKnownFetcher = vi
.fn<(domain: string) => Promise<IClientWellKnown>>()
.mockResolvedValue(makeWellKnown([wellKnownTransport]));
const discovery = new RtcTransportAutoDiscovery({ const discovery = new RtcTransportAutoDiscovery({
client, client,
resolvedConfig: makeResolvedConfig("https://config.example.org"), resolvedConfig: makeResolvedConfig("https://config.example.org"),
wellKnownFetcher,
logger: rootLogger, logger: rootLogger,
}); });
@@ -132,7 +114,6 @@ describe("RtcTransportAutoDiscovery", () => {
); );
expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(2); expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(2);
expect(wellKnownFetcher).not.toHaveBeenCalled();
}); });
const INVALID_TEST_CASES: Array<{ transports: Transport[] }> = [ const INVALID_TEST_CASES: Array<{ transports: Transport[] }> = [
@@ -140,91 +121,30 @@ describe("RtcTransportAutoDiscovery", () => {
{ transports: [{ type: "not_livekit" }] }, { transports: [{ type: "not_livekit" }] },
]; ];
it.each(INVALID_TEST_CASES)( it.each(INVALID_TEST_CASES)(
"falls back to well-known when backend has no (valid) livekit transports $transports", "falls back to config when backend has no (valid) livekit transports $transports",
async ({ transports }) => { async ({ transports }) => {
const client = makeClient(); const client = makeClient();
client._unstable_getRTCTransports.mockResolvedValue(transports); client._unstable_getRTCTransports.mockResolvedValue(transports);
const wellKnownFetcher = vi
.fn<(domain: string) => Promise<IClientWellKnown>>()
.mockResolvedValue(makeWellKnown([wellKnownTransport]));
const discovery = new RtcTransportAutoDiscovery({ const discovery = new RtcTransportAutoDiscovery({
client, client,
resolvedConfig: makeResolvedConfig("https://config.example.org"), resolvedConfig: makeResolvedConfig(configTransport.livekit_service_url),
wellKnownFetcher,
logger: rootLogger, logger: rootLogger,
}); });
await expect( const discoveredTransport = await discovery.discoverPreferredTransport();
discovery.discoverPreferredTransport(), expect(discoveredTransport).not.toStrictEqual(backendTransport);
).resolves.toStrictEqual(wellKnownTransport); expect(discoveredTransport).toStrictEqual(configTransport);
expect(wellKnownFetcher).toHaveBeenCalledWith("example.org");
}, },
); );
it("skips backend discovery in widget mode and uses well-known", async () => { it("returns null when backend and config are all unavailable", async () => {
const client = makeClient();
// widget mode is detected by the absence of an access token
client.getAccessToken.mockReturnValue(null);
const wellKnownFetcher = vi
.fn<(domain: string) => Promise<IClientWellKnown>>()
.mockResolvedValue(makeWellKnown([wellKnownTransport]));
const discovery = new RtcTransportAutoDiscovery({
client,
resolvedConfig: makeResolvedConfig("https://config.example.org"),
wellKnownFetcher,
logger: rootLogger,
});
await expect(discovery.discoverPreferredTransport()).resolves.toStrictEqual(
wellKnownTransport,
);
expect(client._unstable_getRTCTransports).not.toHaveBeenCalled();
expect(wellKnownFetcher).toHaveBeenCalledWith("example.org");
});
it("falls back to app config when backend fails and well-known has no rtc_foci", async () => {
const client = makeClient();
client._unstable_getRTCTransports.mockRejectedValue(
new MatrixError({ errcode: "M_UNKNOWN" }, 404),
);
const wellKnownFetcher = vi
.fn<(domain: string) => Promise<IClientWellKnown>>()
.mockResolvedValue({} as IClientWellKnown);
const discovery = new RtcTransportAutoDiscovery({
client,
resolvedConfig: makeResolvedConfig("https://config.example.org"),
wellKnownFetcher,
logger: rootLogger,
});
await expect(discovery.discoverPreferredTransport()).resolves.toStrictEqual(
{
type: "livekit",
livekit_service_url: "https://config.example.org",
},
);
});
it("returns null when backend, well-known and config are all unavailable", async () => {
const client = makeClient(); const client = makeClient();
client._unstable_getRTCTransports.mockResolvedValue([]); client._unstable_getRTCTransports.mockResolvedValue([]);
const wellKnownFetcher = vi
.fn<(domain: string) => Promise<IClientWellKnown>>()
.mockResolvedValue({} as IClientWellKnown);
const discovery = new RtcTransportAutoDiscovery({ const discovery = new RtcTransportAutoDiscovery({
client, client,
resolvedConfig: makeResolvedConfig(undefined), resolvedConfig: makeResolvedConfig(undefined),
wellKnownFetcher,
logger: rootLogger, logger: rootLogger,
}); });

View File

@@ -8,7 +8,7 @@ import {
isLivekitTransportConfig, isLivekitTransportConfig,
type LivekitTransportConfig, type LivekitTransportConfig,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { type IClientWellKnown, type MatrixClient } from "matrix-js-sdk"; import { type MatrixClient } from "matrix-js-sdk";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { type Logger } from "matrix-js-sdk/lib/logger";
import type { ResolvedConfigOptions } from "../../../config/ConfigOptions.ts"; import type { ResolvedConfigOptions } from "../../../config/ConfigOptions.ts";
@@ -16,33 +16,27 @@ import { doNetworkOperationWithRetry } from "../../../utils/matrix.ts";
type TransportDiscoveryClient = Pick< type TransportDiscoveryClient = Pick<
MatrixClient, MatrixClient,
"getDomain" | "_unstable_getRTCTransports" | "getAccessToken" "getDomain" | "_unstable_getRTCTransports"
>; >;
export interface RtcTransportAutoDiscoveryProps { export interface RtcTransportAutoDiscoveryProps {
client: TransportDiscoveryClient; client: TransportDiscoveryClient;
resolvedConfig: ResolvedConfigOptions; resolvedConfig: ResolvedConfigOptions;
wellKnownFetcher: (domain: string) => Promise<IClientWellKnown>;
logger: Logger; logger: Logger;
} }
export class RtcTransportAutoDiscovery { export class RtcTransportAutoDiscovery {
private readonly client: TransportDiscoveryClient; private readonly client: TransportDiscoveryClient;
private readonly resolvedConfig: ResolvedConfigOptions; private readonly resolvedConfig: ResolvedConfigOptions;
private readonly wellKnownFetcher: (
domain: string,
) => Promise<IClientWellKnown>;
private readonly logger: Logger; private readonly logger: Logger;
public constructor({ public constructor({
client, client,
resolvedConfig, resolvedConfig,
wellKnownFetcher,
logger, logger,
}: RtcTransportAutoDiscoveryProps) { }: RtcTransportAutoDiscoveryProps) {
this.client = client; this.client = client;
this.resolvedConfig = resolvedConfig; this.resolvedConfig = resolvedConfig;
this.wellKnownFetcher = wellKnownFetcher;
this.logger = logger.getChild("[RtcTransportAutoDiscovery]"); this.logger = logger.getChild("[RtcTransportAutoDiscovery]");
} }
@@ -56,21 +50,7 @@ export class RtcTransportAutoDiscovery {
return backendTransport; return backendTransport;
} }
this.logger.info("No backend transport found, falling back to well-known"); // 2) app config URL
// 2) .well-known transports
const wellKnownTransport = await this.tryWellKnownTransports();
if (wellKnownTransport) {
this.logger.info(
`Found .well-known transport: ${wellKnownTransport.livekit_service_url}`,
);
return wellKnownTransport;
}
this.logger.info(
"No .well-known transport found, falling back to app config",
);
// 3) app config URL
const configTransport = this.tryConfigTransport(); const configTransport = this.tryConfigTransport();
if (configTransport) { if (configTransport) {
this.logger.info( this.logger.info(
@@ -90,13 +70,6 @@ export class RtcTransportAutoDiscovery {
private async tryBackendTransports(): Promise<LivekitTransportConfig | null> { private async tryBackendTransports(): Promise<LivekitTransportConfig | null> {
const client = this.client; const client = this.client;
// MSC4143: Attempt to fetch transports from backend. // MSC4143: Attempt to fetch transports from backend.
// TODO: Workaround for an issue in the js-sdk RoomWidgetClient that
// is not yet implementing _unstable_getRTCTransports properly (via widget API new action).
// For now we just skip this call if we are in a widget.
// In widget mode the client is a `RoomWidgetClient` which has no access token (it is using the widget API).
// Could be removed once the js-sdk is fixed (https://github.com/matrix-org/matrix-js-sdk/issues/5245)
const isSPA = !!client.getAccessToken();
if (isSPA && "_unstable_getRTCTransports" in client) {
this.logger.info("First try to use getRTCTransports end point ..."); this.logger.info("First try to use getRTCTransports end point ...");
try { try {
const transportList = await doNetworkOperationWithRetry(async () => const transportList = await doNetworkOperationWithRetry(async () =>
@@ -114,48 +87,6 @@ export class RtcTransportAutoDiscovery {
} catch (ex) { } catch (ex) {
this.logger.info(`Failed to use getRTCTransports end point: ${ex}`); this.logger.info(`Failed to use getRTCTransports end point: ${ex}`);
} }
} else {
this.logger.debug(`getRTCTransports end point not available`);
}
return null;
}
/**
* Fetches the first rtc_foci from the .well-known/matrix/client.
* This will not throw errors, but instead just log them and return null if the expected config is not found or malformed.
* @private
*/
private async tryWellKnownTransports(): Promise<LivekitTransportConfig | null> {
// Legacy MSC4143 (to be removed) WELL_KNOWN: Prioritize the .well-known/matrix/client, if available.
const client = this.client;
const domain = client.getDomain();
if (domain) {
// we use AutoDiscovery instead of relying on the MatrixClient having already
// been fully configured and started
const wellKnownFoci = await this.wellKnownFetcher(domain);
const fociConfig = wellKnownFoci["org.matrix.msc4143.rtc_foci"];
if (fociConfig) {
if (!Array.isArray(fociConfig)) {
this.logger.warn(
`org.matrix.msc4143.rtc_foci is not an array in .well-known`,
);
} else {
return fociConfig[0];
}
} else {
this.logger.info(
`No .well-known "org.matrix.msc4143.rtc_foci" found for ${domain}`,
wellKnownFoci,
);
}
} else {
// Should never happen, but just in case
this.logger.warn(`No domain configured for client`);
}
return null; return null;
} }

View File

@@ -12,7 +12,7 @@ import {
type E2EEManagerOptions, type E2EEManagerOptions,
type BaseE2EEManager, type BaseE2EEManager,
} from "livekit-client"; } from "livekit-client";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { logger, type Logger } from "matrix-js-sdk/lib/logger";
// imported as inline to support worker when loaded from a cdn (cross domain) // imported as inline to support worker when loaded from a cdn (cross domain)
import E2EEWorker from "livekit-client/e2ee-worker?worker&inline"; import E2EEWorker from "livekit-client/e2ee-worker?worker&inline";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager"; import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
@@ -27,7 +27,18 @@ import type {
import type { MediaDevices } from "../../MediaDevices.ts"; import type { MediaDevices } from "../../MediaDevices.ts";
import type { Behavior } from "../../Behavior.ts"; import type { Behavior } from "../../Behavior.ts";
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { defaultLiveKitOptions } from "../../../livekit/options.ts"; import { getLiveKitOptions } from "../../../livekit/options.ts";
import {
advancedCamera,
cameraResolution,
cameraFramerate,
cameraBitrate,
cameraCodec,
parseResolution,
echoCancellationSetting,
noiseSuppressionSetting,
autoGainControlSetting,
} from "../../../settings/settings.ts";
// TODO evaluate if this should be done like the Publisher Factory // TODO evaluate if this should be done like the Publisher Factory
export interface ConnectionFactory { export interface ConnectionFactory {
@@ -53,8 +64,6 @@ export class ECConnectionFactory implements ConnectionFactory {
* @param livekitKeyProvider - Optional key provider for end-to-end encryption. * @param livekitKeyProvider - Optional key provider for end-to-end encryption.
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app). * @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used. * @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
* @param echoCancellation - Whether to enable echo cancellation for audio capture.
* @param noiseSuppression - Whether to enable noise suppression for audio capture.
*/ */
public constructor( public constructor(
private client: OpenIDClientParts, private client: OpenIDClientParts,
@@ -64,12 +73,9 @@ export class ECConnectionFactory implements ConnectionFactory {
livekitKeyProvider: BaseKeyProvider | undefined, livekitKeyProvider: BaseKeyProvider | undefined,
private controlledAudioDevices: boolean, private controlledAudioDevices: boolean,
livekitRoomFactory?: () => LivekitRoom, livekitRoomFactory?: () => LivekitRoom,
echoCancellation: boolean = true,
noiseSuppression: boolean = true,
) { ) {
const defaultFactory = (): LivekitRoom => const defaultFactory = (): LivekitRoom => {
new LivekitRoom( const roomOptions = generateRoomOption({
generateRoomOption({
devices: this.devices, devices: this.devices,
processorState: this.processorState$.value, processorState: this.processorState$.value,
e2eeLivekitOptions: livekitKeyProvider && { e2eeLivekitOptions: livekitKeyProvider && {
@@ -79,10 +85,10 @@ export class ECConnectionFactory implements ConnectionFactory {
worker: new E2EEWorker(), worker: new E2EEWorker(),
}, },
controlledAudioDevices: this.controlledAudioDevices, controlledAudioDevices: this.controlledAudioDevices,
echoCancellation, });
noiseSuppression, logger.info("[ECConnectionFactory] livekit room options: ", roomOptions);
}), return new LivekitRoom(roomOptions);
); };
this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory; this.livekitRoomFactory = livekitRoomFactory ?? defaultFactory;
} }
@@ -119,14 +125,13 @@ export class ECConnectionFactory implements ConnectionFactory {
/** /**
* Generate the initial LiveKit RoomOptions based on the current media devices and processor state. * Generate the initial LiveKit RoomOptions based on the current media devices and processor state.
* Reads audio processing and camera quality settings directly from Settings.
*/ */
function generateRoomOption({ function generateRoomOption({
devices, devices,
processorState, processorState,
e2eeLivekitOptions, e2eeLivekitOptions,
controlledAudioDevices, controlledAudioDevices,
echoCancellation,
noiseSuppression,
}: { }: {
devices: MediaDevices; devices: MediaDevices;
processorState: ProcessorState; processorState: ProcessorState;
@@ -135,21 +140,44 @@ function generateRoomOption({
| { e2eeManager: BaseE2EEManager } | { e2eeManager: BaseE2EEManager }
| undefined; | undefined;
controlledAudioDevices: boolean; controlledAudioDevices: boolean;
echoCancellation: boolean;
noiseSuppression: boolean;
}): RoomOptions { }): RoomOptions {
return { const liveKitOptions = getLiveKitOptions();
...defaultLiveKitOptions,
videoCaptureDefaults: { // Apply advanced camera settings if enabled
...defaultLiveKitOptions.videoCaptureDefaults, let videoCaptureDefaults = {
...liveKitOptions.videoCaptureDefaults,
deviceId: devices.videoInput.selected$.value?.id, deviceId: devices.videoInput.selected$.value?.id,
processor: processorState.processor, processor: processorState.processor,
}, };
let publishDefaults = liveKitOptions.publishDefaults;
if (advancedCamera.getValue()) {
const { width, height } = parseResolution(cameraResolution.getValue());
const fps = cameraFramerate.getValue();
const bps = cameraBitrate.getValue();
const codec = cameraCodec.getValue();
videoCaptureDefaults = {
...videoCaptureDefaults,
resolution: { width, height, frameRate: fps },
};
publishDefaults = {
...publishDefaults,
videoEncoding: { maxBitrate: bps, maxFramerate: fps },
videoCodec: codec,
};
}
return {
...liveKitOptions,
videoCaptureDefaults,
publishDefaults,
audioCaptureDefaults: { audioCaptureDefaults: {
...defaultLiveKitOptions.audioCaptureDefaults, ...liveKitOptions.audioCaptureDefaults,
deviceId: devices.audioInput.selected$.value?.id, deviceId: devices.audioInput.selected$.value?.id,
echoCancellation, echoCancellation: echoCancellationSetting.getValue(),
noiseSuppression, noiseSuppression: noiseSuppressionSetting.getValue(),
autoGainControl: autoGainControlSetting.getValue(),
}, },
audioOutput: { audioOutput: {
// When using controlled audio devices, we don't want to set the // When using controlled audio devices, we don't want to set the

View File

@@ -22,6 +22,16 @@ import {
} from "../../../utils/test.ts"; } from "../../../utils/test.ts";
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { constant } from "../../Behavior"; import { constant } from "../../Behavior";
import {
echoCancellationSetting,
noiseSuppressionSetting,
autoGainControlSetting,
advancedCamera,
cameraResolution,
cameraFramerate,
cameraBitrate,
cameraCodec,
} from "../../../settings/settings.ts";
// At the top of your test file, after imports // At the top of your test file, after imports
vi.mock("livekit-client", async (importOriginal) => { vi.mock("livekit-client", async (importOriginal) => {
@@ -58,11 +68,14 @@ describe("ECConnectionFactory - Audio inputs options", () => {
{ echo: false, noise: true }, { echo: false, noise: true },
{ echo: false, noise: false }, { echo: false, noise: false },
])( ])(
"it sets echoCancellation=$echo and noiseSuppression=$noise based on constructor parameters", "it sets echoCancellation=$echo and noiseSuppression=$noise based on settings",
({ echo, noise }) => { ({ echo, noise }) => {
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
const RoomConstructor = vi.mocked(LivekitRoom); const RoomConstructor = vi.mocked(LivekitRoom);
// Set audio processing settings
echoCancellationSetting.setValue(echo);
noiseSuppressionSetting.setValue(noise);
const ecConnectionFactory = new ECConnectionFactory( const ecConnectionFactory = new ECConnectionFactory(
mockClient, mockClient,
"!roomid:example.org", "!roomid:example.org",
@@ -73,9 +86,6 @@ describe("ECConnectionFactory - Audio inputs options", () => {
}), }),
undefined, undefined,
false, false,
undefined,
echo,
noise,
); );
ecConnectionFactory.createConnection( ecConnectionFactory.createConnection(
testScope, testScope,
@@ -101,9 +111,13 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
test.each([{ controlled: true }, { controlled: false }])( test.each([{ controlled: true }, { controlled: false }])(
"it sets controlledAudioDevice=$controlled then uses deviceId accordingly", "it sets controlledAudioDevice=$controlled then uses deviceId accordingly",
({ controlled }) => { ({ controlled }) => {
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
const RoomConstructor = vi.mocked(LivekitRoom); const RoomConstructor = vi.mocked(LivekitRoom);
// Explicitly set audio settings so the test doesn't depend on defaults
echoCancellationSetting.setValue(true);
noiseSuppressionSetting.setValue(true);
autoGainControlSetting.setValue(true);
const ecConnectionFactory = new ECConnectionFactory( const ecConnectionFactory = new ECConnectionFactory(
mockClient, mockClient,
"!roomid:example.org", "!roomid:example.org",
@@ -120,9 +134,6 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
}), }),
undefined, undefined,
controlled, controlled,
undefined,
false,
false,
); );
ecConnectionFactory.createConnection( ecConnectionFactory.createConnection(
testScope, testScope,
@@ -143,6 +154,114 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
); );
}); });
describe("ECConnectionFactory - Camera quality settings", () => {
test("it uses default video options when advancedCamera is disabled", () => {
const RoomConstructor = vi.mocked(LivekitRoom);
advancedCamera.setValue(false);
const ecConnectionFactory = new ECConnectionFactory(
mockClient,
"!roomid:example.org",
mockMediaDevices({}),
new BehaviorSubject<ProcessorState>({
supported: true,
processor: undefined,
}),
undefined,
false,
);
ecConnectionFactory.createConnection(
testScope,
exampleTransport,
ownMemberMock,
logger,
);
// publishDefaults should use config defaults (vp8), not custom settings
expect(RoomConstructor).toHaveBeenCalledWith(
expect.objectContaining({
publishDefaults: expect.objectContaining({
videoCodec: "vp8",
}),
}),
);
});
test("it applies custom camera resolution, encoding, and codec when advancedCamera is enabled", () => {
const RoomConstructor = vi.mocked(LivekitRoom);
advancedCamera.setValue(true);
cameraResolution.setValue("1920x1080");
cameraFramerate.setValue(60);
cameraBitrate.setValue(4_000_000);
cameraCodec.setValue("vp9");
const ecConnectionFactory = new ECConnectionFactory(
mockClient,
"!roomid:example.org",
mockMediaDevices({}),
new BehaviorSubject<ProcessorState>({
supported: true,
processor: undefined,
}),
undefined,
false,
);
ecConnectionFactory.createConnection(
testScope,
exampleTransport,
ownMemberMock,
logger,
);
expect(RoomConstructor).toHaveBeenCalledWith(
expect.objectContaining({
videoCaptureDefaults: expect.objectContaining({
resolution: { width: 1920, height: 1080, frameRate: 60 },
}),
publishDefaults: expect.objectContaining({
videoEncoding: { maxBitrate: 4_000_000, maxFramerate: 60 },
videoCodec: "vp9",
}),
}),
);
});
test("it applies autoGainControl from settings", () => {
const RoomConstructor = vi.mocked(LivekitRoom);
autoGainControlSetting.setValue(false);
echoCancellationSetting.setValue(true);
noiseSuppressionSetting.setValue(true);
const ecConnectionFactory = new ECConnectionFactory(
mockClient,
"!roomid:example.org",
mockMediaDevices({}),
new BehaviorSubject<ProcessorState>({
supported: true,
processor: undefined,
}),
undefined,
false,
);
ecConnectionFactory.createConnection(
testScope,
exampleTransport,
ownMemberMock,
logger,
);
expect(RoomConstructor).toHaveBeenCalledWith(
expect.objectContaining({
audioCaptureDefaults: expect.objectContaining({
autoGainControl: false,
}),
}),
);
});
});
afterEach(() => { afterEach(() => {
testScope.end(); testScope.end();
fetchMock.reset(); fetchMock.reset();

View File

@@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { type RemoteTrackPublication } from "livekit-client"; import {
type LocalTrackPublication,
type RemoteTrackPublication,
} from "livekit-client";
import { test, expect } from "vitest"; import { test, expect } from "vitest";
import { act, render, screen } from "@testing-library/react"; import { act, render, screen } from "@testing-library/react";
import { axe } from "vitest-axe"; import { axe } from "vitest-axe";
@@ -17,6 +20,9 @@ import {
mockRtcMembership, mockRtcMembership,
mockRemoteMedia, mockRemoteMedia,
mockRemoteParticipant, mockRemoteParticipant,
mockLocalMedia,
mockLocalParticipant,
mockMediaDevices,
} from "../utils/test"; } from "../utils/test";
import { GridTileViewModel } from "../state/TileViewModel"; import { GridTileViewModel } from "../state/TileViewModel";
import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
@@ -54,7 +60,7 @@ const callVm = {
handsRaised$: constant({}), handsRaised$: constant({}),
} as Partial<CallViewModel> as CallViewModel; } as Partial<CallViewModel> as CallViewModel;
test("GridTile is accessible", async () => { test("GridTile displays remote media", async () => {
const vm = mockRemoteMedia( const vm = mockRemoteMedia(
mockRtcMembership("@alice:example.org", "AAAA"), mockRtcMembership("@alice:example.org", "AAAA"),
{ {
@@ -88,6 +94,40 @@ test("GridTile is accessible", async () => {
screen.getByText("Alice"); screen.getByText("Alice");
}); });
test("GridTile displays local media", async () => {
const vm = mockLocalMedia(
mockRtcMembership("@alice:example.org", "AAAA"),
{
rawDisplayName: "Alice",
getMxcAvatarUrl: () => "mxc://adfsg",
},
mockLocalParticipant({
getTrackPublication: () =>
({}) as Partial<LocalTrackPublication> as LocalTrackPublication,
}),
mockMediaDevices({}),
);
const { container } = render(
<ReactionsSenderProvider vm={callVm} rtcSession={fakeRtcSession}>
<GridTile
vm={new GridTileViewModel(constant(vm))}
onOpenProfile={() => {}}
targetWidth={300}
targetHeight={200}
showSpeakingIndicators
showNameTags
showRingingStatus
showOutline
focusable
/>
</ReactionsSenderProvider>,
);
expect(await axe(container)).toHaveNoViolations();
// Name should be visible
screen.getByText("Alice");
});
test("GridTile displays ringing media", async () => { test("GridTile displays ringing media", async () => {
const pickupState$ = new BehaviorSubject< const pickupState$ = new BehaviorSubject<
RingingMediaViewModel["pickupState$"]["value"] RingingMediaViewModel["pickupState$"]["value"]

View File

@@ -13,6 +13,7 @@ import {
useCallback, useCallback,
useRef, useRef,
useState, useState,
useMemo,
} from "react"; } from "react";
import { type animated } from "@react-spring/web"; import { type animated } from "@react-spring/web";
import classNames from "classnames"; import classNames from "classnames";
@@ -103,20 +104,22 @@ interface UserMediaTileProps extends TileProps {
playbackMuted: boolean; playbackMuted: boolean;
waitingForMedia?: boolean; waitingForMedia?: boolean;
primaryButton?: ReactNode; primaryButton?: ReactNode;
menuStart?: ReactNode;
menuEnd?: ReactNode;
focusUrl: string | undefined; focusUrl: string | undefined;
} }
const UserMediaTile: FC<UserMediaTileProps> = ({ /**
* A user media tile without a context menu.
*/
// The context menu is kept separate from this component for performance
// reasons (c.f. UserMediaTile)
const UserMediaTileInner: FC<UserMediaTileProps & { menu: ReactNode }> = ({
ref, ref,
vm, vm,
showSpeakingIndicators, showSpeakingIndicators,
playbackMuted, playbackMuted,
waitingForMedia, waitingForMedia,
primaryButton, primaryButton,
menuStart, menu,
menuEnd,
className, className,
focusUrl, focusUrl,
displayName, displayName,
@@ -156,24 +159,26 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
: t("microphone_off"); : t("microphone_off");
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const menu = ( const menuTrigger = useMemo(
<> () => (
{menuStart} <button
{/* aria-label={t("common.options")}
No additional menu item (used to be the manual fit to frame. tabIndex={focusable ? undefined : -1}
Placeholder for future menu items that should be placed here. >
*/} <OverflowHorizontalIcon aria-hidden width={20} height={20} />
{menuEnd} </button>
</> ),
[t, focusable],
); );
const raisedHandOnClick = vm.local const raisedHandOnClick = useMemo(
? (): void => void toggleRaisedHand() () => (vm.local ? (): void => void toggleRaisedHand() : undefined),
: undefined; [vm.local, toggleRaisedHand],
);
const showSpeaking = showSpeakingIndicators && speaking; const showSpeaking = showSpeakingIndicators && speaking;
const tile = ( return (
<MediaView <MediaView
ref={ref} ref={ref}
video={video} video={video}
@@ -202,14 +207,7 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
open={menuOpen} open={menuOpen}
onOpenChange={setMenuOpen} onOpenChange={setMenuOpen}
title={displayName} title={displayName}
trigger={ trigger={menuTrigger}
<button
aria-label={t("common.options")}
tabIndex={focusable ? undefined : -1}
>
<OverflowHorizontalIcon aria-hidden width={20} height={20} />
</button>
}
side="left" side="left"
align="start" align="start"
> >
@@ -231,9 +229,37 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
{...props} {...props}
/> />
); );
};
/**
* A user media tile enhanced with a context menu.
*/
const UserMediaTile: FC<
UserMediaTileProps & { menuStart?: ReactNode; menuEnd?: ReactNode }
> = ({ menuStart, menuEnd, ...props }) => {
const menu = useMemo(
() => (
<>
{menuStart}
{/*
No additional menu item (used to be the manual fit to frame.
Placeholder for future menu items that should be placed here.
*/}
{menuEnd}
</>
),
[menuStart, menuEnd],
);
// ContextMenu is expensive to render, so we avoid subscribing to any
// frequently-changing behaviors here and instead keep them isolated in the
// UserMediaTileInner component
return ( return (
<ContextMenu title={displayName} trigger={tile} hasAccessibleAlternative> <ContextMenu
title={props.displayName}
trigger={<UserMediaTileInner {...props} menu={menu} />}
hasAccessibleAlternative
>
{menu} {menu}
</ContextMenu> </ContextMenu>
); );
@@ -269,6 +295,29 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
[vm, latestAlwaysShow], [vm, latestAlwaysShow],
); );
const menuStart = useMemo(
() => (
<ToggleMenuItem
Icon={VisibilityOnIcon}
label={t("video_tile.always_show")}
checked={alwaysShow}
onSelect={onSelectAlwaysShow}
/>
),
[t, alwaysShow, onSelectAlwaysShow],
);
const menuEnd = useMemo(
() =>
onOpenProfile && (
<MenuItem
Icon={UserProfileIcon}
label={t("common.profile")}
onSelect={onOpenProfile}
/>
),
[t, onOpenProfile],
);
return ( return (
<UserMediaTile <UserMediaTile
ref={ref} ref={ref}
@@ -287,23 +336,8 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
</button> </button>
) )
} }
menuStart={ menuStart={menuStart}
<ToggleMenuItem menuEnd={menuEnd}
Icon={VisibilityOnIcon}
label={t("video_tile.always_show")}
checked={alwaysShow}
onSelect={onSelectAlwaysShow}
/>
}
menuEnd={
onOpenProfile && (
<MenuItem
Icon={UserProfileIcon}
label={t("common.profile")}
onSelect={onOpenProfile}
/>
)
}
focusable={focusable} focusable={focusable}
focusUrl={focusUrl} focusUrl={focusUrl}
{...props} {...props}

View File

@@ -12,7 +12,7 @@ import { i18nKey } from "./i18n";
export enum ErrorCode { export enum ErrorCode {
/** /**
* Configuration problem due to no MatrixRTC backend/SFU is exposed via .well-known and no fallback configured. * Configuration problem due to no MatrixRTC transport provided by homeserver and no fallback configured.
*/ */
MISSING_MATRIX_RTC_TRANSPORT = "MISSING_MATRIX_RTC_TRANSPORT", MISSING_MATRIX_RTC_TRANSPORT = "MISSING_MATRIX_RTC_TRANSPORT",
CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR", CONNECTION_LOST_ERROR = "CONNECTION_LOST_ERROR",
@@ -67,7 +67,7 @@ export class ElementCallError extends Error {
} }
/** /**
* Configuration problem due to no MatrixRTC backend/SFU is exposed via .well-known and no fallback configured. * Configuration problem due to no MatrixRTC transport provided by homeserver and no fallback configured.
*/ */
export class MatrixRTCTransportMissingError extends ElementCallError { export class MatrixRTCTransportMissingError extends ElementCallError {
public domain: string; public domain: string;

View File

@@ -19,6 +19,7 @@ const createRoomWidgetClientSpy = vi.mocked(createRoomWidgetClient);
vi.mock("./config/Config", () => ({ vi.mock("./config/Config", () => ({
Config: { Config: {
init: vi.fn().mockImplementation(async () => Promise.resolve()), init: vi.fn().mockImplementation(async () => Promise.resolve()),
get: vi.fn().mockReturnValue({}),
}, },
})); }));
const configInitSpy = vi.mocked(Config.init); const configInitSpy = vi.mocked(Config.init);
@@ -109,6 +110,7 @@ describe("widget", () => {
sendToDevice: sendRecvToDevice, sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice, receiveToDevice: sendRecvToDevice,
turnServers: false, turnServers: false,
rtcTransports: true,
sendDelayedEvents: true, sendDelayedEvents: true,
updateDelayedEvents: true, updateDelayedEvents: true,
sendSticky: true, sendSticky: true,

View File

@@ -21,6 +21,7 @@ import type { IWidgetApiRequest } from "matrix-widget-api";
import { LazyEventEmitter } from "./LazyEventEmitter"; import { LazyEventEmitter } from "./LazyEventEmitter";
import { getUrlParams } from "./UrlParams"; import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config"; import { Config } from "./config/Config";
import { seedSettingsFromConfig } from "./settings/settings";
import { ElementCallReactionEventType } from "./reactions"; import { ElementCallReactionEventType } from "./reactions";
// Subset of the actions in element-web // Subset of the actions in element-web
@@ -173,6 +174,7 @@ export const initializeWidget = (
sendToDevice: sendRecvToDevice, sendToDevice: sendRecvToDevice,
receiveToDevice: sendRecvToDevice, receiveToDevice: sendRecvToDevice,
turnServers: false, turnServers: false,
rtcTransports: true,
sendDelayedEvents: true, sendDelayedEvents: true,
updateDelayedEvents: true, updateDelayedEvents: true,
sendSticky: true, sendSticky: true,
@@ -195,7 +197,8 @@ export const initializeWidget = (
// Wait for the config file to be ready (we load very early on so it might not // Wait for the config file to be ready (we load very early on so it might not
// be otherwise) // be otherwise)
await Config.init(); await Config.init();
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 }); seedSettingsFromConfig(Config.get().media_quality);
await client.startClient();
return client; return client;
}; };