mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-20 20:49:20 +00:00
Merge branch 'livekit' into more-more-performance
This commit is contained in:
40
README.md
40
README.md
@@ -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
|
||||
[**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
|
||||
setup as outlined in the [Self-Hosting Guide](./docs/self_hosting.md). A typical
|
||||
federated site deployment for three different sites A, B and C is depicted below.
|
||||
For proper operation of Element Call, each deployment needs to set up a
|
||||
MatrixRTC transport in the form of a LiveKit server as outlined in the
|
||||
[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">
|
||||
<img src="./docs/Federated_Setup.drawio.png" alt="Element Call federated setup">
|
||||
</p>
|
||||
|
||||
### Backend Discovery
|
||||
### Transport Discovery
|
||||
|
||||
The MatrixRTC backend (according to
|
||||
[MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143)) is
|
||||
announced by the Matrix site's `.well-known/matrix/client` file and discovered
|
||||
via the `org.matrix.msc4143.rtc_foci` key, e.g.:
|
||||
Element Call discovers the available MatrixRTC transports (as defined by
|
||||
[MSC4519](https://github.com/matrix-org/matrix-spec-proposals/pull/4519)) by
|
||||
hitting the `GET /_matrix/client/unstable/org.matrix.msc4143/rtc/transports`
|
||||
endpoint of the Client-Server API. An example response:
|
||||
|
||||
```json
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"rtc_transports": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt"
|
||||
},
|
||||
]
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
where the format for MatrixRTC using LiveKit backend is defined in
|
||||
@@ -149,7 +152,7 @@ via `livekit_service_url`.
|
||||
|
||||
### 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.
|
||||
- For the **LiveKit** MatrixRTC backend
|
||||
([MSC4195](https://github.com/hughns/matrix-spec-proposals/blob/hughns/matrixrtc-livekit/proposals/4195-matrixrtc-livekit.md)),
|
||||
@@ -244,10 +247,9 @@ pnpm backend
|
||||
|
||||
> [!NOTE]
|
||||
> To ensure your local development frontend functions properly, you’ll need to
|
||||
> add certificate exceptions in your browser for `https://localhost:3000`,
|
||||
> `https://matrix-rtc.m.localhost/livekit/jwt/healthz` and
|
||||
> `https://synapse.m.localhost/.well-known/matrix/client`. This can be either
|
||||
> done by adding the minimum localhost CA
|
||||
> add certificate exceptions in your browser for `https://localhost:3000` and
|
||||
> `https://matrix-rtc.m.localhost/livekit/jwt/healthz`. This can be done either
|
||||
> by adding the minimum localhost CA
|
||||
> ([./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
|
||||
> your browser’s address bar and follow the prompts to add the exception.
|
||||
|
||||
@@ -40,6 +40,8 @@ experimental_features:
|
||||
msc4222_enabled: true
|
||||
# sticky events for MatrixRTC user state
|
||||
msc4354_enabled: true
|
||||
# MatrixRTC
|
||||
msc4143_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# 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"
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# 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
|
||||
per_second: 0.5
|
||||
burst_count: 30
|
||||
|
||||
matrix_rtc:
|
||||
transports:
|
||||
- type: livekit
|
||||
livekit_service_url: https://matrix-rtc.othersite.m.localhost/livekit/jwt
|
||||
|
||||
@@ -40,6 +40,8 @@ experimental_features:
|
||||
msc4222_enabled: true
|
||||
# sticky events for MatrixRTC user state
|
||||
msc4354_enabled: true
|
||||
# MatrixRTC
|
||||
msc4143_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# 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"
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# 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
|
||||
per_second: 0.5
|
||||
burst_count: 30
|
||||
|
||||
matrix_rtc:
|
||||
transports:
|
||||
- type: livekit
|
||||
livekit_service_url: https://matrix-rtc.m.localhost/livekit/jwt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Synapse reverse proxy including .well-known/matrix/client
|
||||
# Synapse reverse proxy
|
||||
# domain synapse.m.localhost
|
||||
server {
|
||||
listen 80;
|
||||
@@ -11,18 +11,6 @@ server {
|
||||
ssl_certificate /root/ssl/cert.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
|
||||
# This is also required for development environment.
|
||||
# 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
|
||||
server {
|
||||
listen 80;
|
||||
@@ -57,18 +45,6 @@ server {
|
||||
ssl_certificate /root/ssl/cert.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
|
||||
# This is also required for development environment.
|
||||
# Reason: the lk-jwt-service uses the federation API for the openid token
|
||||
|
||||
@@ -40,6 +40,8 @@ experimental_features:
|
||||
msc4222_enabled: true
|
||||
# sticky events for MatrixRTC user state
|
||||
msc4354_enabled: true
|
||||
# MatrixRTC
|
||||
msc4143_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# 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"
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||
#
|
||||
@@ -84,3 +85,8 @@ rc_login:
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
matrix_rtc:
|
||||
transports:
|
||||
- type: livekit
|
||||
livekit_service_url: https://matrix-rtc.othersite.m.localhost/livekit/jwt
|
||||
|
||||
@@ -40,6 +40,8 @@ experimental_features:
|
||||
msc4222_enabled: true
|
||||
# sticky events for MatrixRTC user state
|
||||
msc4354_enabled: true
|
||||
# MatrixRTC
|
||||
msc4143_enabled: true
|
||||
|
||||
# The maximum allowed duration by which sent events can be delayed, as
|
||||
# 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"
|
||||
|
||||
report_stats: false
|
||||
serve_server_wellknown: true
|
||||
|
||||
# Ratelimiting settings for client actions (registration, login, messaging).
|
||||
#
|
||||
@@ -84,3 +85,8 @@ rc_login:
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
matrix_rtc:
|
||||
transports:
|
||||
- type: livekit
|
||||
livekit_service_url: https://matrix-rtc.m.localhost/livekit/jwt
|
||||
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||
element-web:
|
||||
# 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:
|
||||
# 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
|
||||
|
||||
@@ -68,10 +68,10 @@ As a prerequisite for the
|
||||
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).
|
||||
|
||||
### MatrixRTC Backend
|
||||
### LiveKit backend
|
||||
|
||||
In order to **guarantee smooth operation** of Element Call, a MatrixRTC backend is
|
||||
required for each site deployment.
|
||||
In order to **guarantee smooth operation** of Element Call, a dedicated LiveKit
|
||||
backend is required for each site deployment.
|
||||
|
||||

|
||||
|
||||
@@ -165,7 +165,7 @@ Using Haproxy, you can achieve this by:
|
||||
use_backend mxrtc_auth_backend if is_mxrtc_auth matrixrtc_domain
|
||||
|
||||
# Backend
|
||||
## MatrixRTC backend
|
||||
## LiveKit backend
|
||||
backend sfu_backend
|
||||
server livekit 127.0.0.1:7880
|
||||
http-request set-path %[path,regsub(^/livekit/sfu/,/)]
|
||||
@@ -187,43 +187,22 @@ backend mxrtc_auth_backend
|
||||
|
||||
```
|
||||
|
||||
#### MatrixRTC backend announcement
|
||||
#### MatrixRTC transport announcement
|
||||
|
||||
> [!IMPORTANT]
|
||||
> As defined in
|
||||
> [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143),
|
||||
> 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:
|
||||
Enable the unstable feature flag `msc4143_enabled`, and update the
|
||||
[`matrix_rtc` section](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#matrix_rtc)
|
||||
of your Synapse config file:
|
||||
|
||||
```json
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://matrix-rtc.example.com/livekit/jwt"
|
||||
},
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://matrix-rtc-2.example.com/livekit/jwt"
|
||||
}
|
||||
]
|
||||
```yaml
|
||||
matrix_rtc:
|
||||
transports:
|
||||
- type: livekit
|
||||
livekit_service_url: https://matrix-rtc.example.com/livekit/jwt
|
||||
```
|
||||
|
||||
Make sure this file is served with the correct MIME type (`application/json`).
|
||||
Additionally, ensure the appropriate CORS headers are set to allow web clients
|
||||
to access it across origins. For more details, refer to the
|
||||
[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.
|
||||
The transport you specify will be made available to clients over the
|
||||
`/_matrix/client/unstable/org.matrix.msc4143/rtc/transports` endpoint as defined
|
||||
in [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143).
|
||||
|
||||
## 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).
|
||||
|
||||
> [!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
|
||||
> it might break Element Call going forward!
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"crypto_version": "Crypto version: {{version}}",
|
||||
"custom_livekit_url": {
|
||||
"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",
|
||||
"reset": "Reset overwrite",
|
||||
"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>",
|
||||
"screenshare_button_label": "Share screen",
|
||||
"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": {
|
||||
"effect_volume_description": "Adjust the volume at which reactions and hand raised effects play.",
|
||||
"effect_volume_label": "Sound effect volume"
|
||||
},
|
||||
"auto_gain_control_label": "Automatic gain control",
|
||||
"background_blur_header": "Background",
|
||||
"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.)",
|
||||
"camera_header": "Camera quality",
|
||||
"codec_label": "Codec",
|
||||
"developer_tab_title": "Developer",
|
||||
"devices": {
|
||||
"activating": "Activating…",
|
||||
@@ -218,12 +228,15 @@
|
||||
"speaker": "Speaker",
|
||||
"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_description_label": "Your feedback",
|
||||
"feedback_tab_h4": "Submit feedback",
|
||||
"feedback_tab_send_logs_label": "Include debug logs",
|
||||
"feedback_tab_thank_you": "Thanks, we received your 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.",
|
||||
"preferences_tab": {
|
||||
"developer_mode_label": "Developer mode",
|
||||
@@ -235,7 +248,9 @@
|
||||
"reactions_show_label": "Show reactions",
|
||||
"show_hand_raised_timer_description": "Show a timer when a participant raises their hand",
|
||||
"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_other": "{{count}} stars",
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"@typescript-eslint/utils": "^8.61.0",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
"@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",
|
||||
"@vitest/browser-playwright": "^4.1.5",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
@@ -97,7 +97,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"loglevel": "^1.9.1",
|
||||
"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",
|
||||
"normalize.css": "^8.0.1",
|
||||
"observable-hooks": "^4.2.3",
|
||||
@@ -131,5 +131,5 @@
|
||||
"vitest": "^4.1.5",
|
||||
"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
368
pnpm-lock.yaml
generated
@@ -39,10 +39,10 @@ importers:
|
||||
version: 11.7.12
|
||||
'@livekit/components-core':
|
||||
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':
|
||||
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':
|
||||
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))
|
||||
@@ -66,13 +66,13 @@ importers:
|
||||
version: 10.1.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@rolldown/plugin-babel':
|
||||
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':
|
||||
specifier: ^8.0.0
|
||||
version: 8.55.2(react@19.2.8)
|
||||
'@sentry/vite-plugin':
|
||||
specifier: ^3.0.0
|
||||
version: 3.6.1
|
||||
version: 3.6.1(supports-color@7.2.0)
|
||||
'@storybook/addon-docs':
|
||||
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))
|
||||
@@ -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)
|
||||
'@storybook/react-vite':
|
||||
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':
|
||||
specifier: ^10.1.0
|
||||
version: 10.4.1
|
||||
@@ -126,7 +126,7 @@ importers:
|
||||
version: 2.15.0
|
||||
'@typescript-eslint/utils':
|
||||
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':
|
||||
specifier: ^10.2.11
|
||||
version: 10.3.1(react@19.2.8)
|
||||
@@ -134,11 +134,11 @@ importers:
|
||||
specifier: ^10.0.0
|
||||
version: 10.2.3(@types/react@19.2.17)(react@19.2.8)
|
||||
'@vector-im/compound-web':
|
||||
specifier: ^9.3.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)
|
||||
specifier: ^10.0.0
|
||||
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':
|
||||
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':
|
||||
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)
|
||||
@@ -162,10 +162,10 @@ importers:
|
||||
version: link:eslint
|
||||
eslint-plugin-storybook:
|
||||
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:
|
||||
specifier: 11.1.5
|
||||
version: 11.1.5
|
||||
version: 11.1.5(node-fetch@2.7.0)
|
||||
i18next:
|
||||
specifier: ^26.0.0
|
||||
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)
|
||||
jsdom:
|
||||
specifier: ^26.0.0
|
||||
version: 26.1.0
|
||||
version: 26.1.0(supports-color@7.2.0)
|
||||
knip:
|
||||
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)
|
||||
@@ -192,9 +192,9 @@ importers:
|
||||
version: 1.9.2
|
||||
matrix-js-sdk:
|
||||
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:
|
||||
specifier: ^1.16.1
|
||||
specifier: ^1.18.0
|
||||
version: 1.18.0
|
||||
node-stdlib-browser:
|
||||
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))
|
||||
vite-plugin-svgr:
|
||||
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:
|
||||
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))
|
||||
vitest:
|
||||
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:
|
||||
specifier: ^1.0.0-pre.3
|
||||
version: 1.0.0-pre.5(vitest@4.1.10)
|
||||
@@ -332,10 +332,6 @@ packages:
|
||||
resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -391,14 +387,13 @@ packages:
|
||||
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@8.0.0':
|
||||
resolution: {integrity: sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==}
|
||||
|
||||
'@babel/template@7.29.7':
|
||||
resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==}
|
||||
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':
|
||||
resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -1164,15 +1159,15 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@livekit/components-core@0.12.14':
|
||||
resolution: {integrity: sha512-6OKP/1Ok2fCZewDLKd3SzaTb7KvfZl/6hjggI+TgUdhp0Y7HBg3+tHA7YmhZRc0BO8wIyVy4VgTPn7qkLHt2nQ==}
|
||||
'@livekit/components-core@0.12.15':
|
||||
resolution: {integrity: sha512-bDkK+jkPMgyT4a5lZmdcuc/hZ8fZUrXW+qQPNHVeSpXCBMzk5q9QWzx+HPtTsPJ8hnncWGfoT81RnXHBsy+Z8g==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
livekit-client: ^2.20.1
|
||||
tslib: ^2.6.2
|
||||
|
||||
'@livekit/components-react@2.9.23':
|
||||
resolution: {integrity: sha512-clO+0g/u3YBpuOvnAjUSAJBH/o7w+RpUAseswjiSML9rHrXlTUhwBNm8LPk+qN5GOU3A6lzsNnFYVpUHUBVOkg==}
|
||||
'@livekit/components-react@2.9.24':
|
||||
resolution: {integrity: sha512-qw5Oy1EfPg1f/xrKYrFielSrmtxY8BZ9mDt6299G5aikotQIztmldBfTNosrVS7VSNit7qeYfOFSyKaML20W6Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@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
|
||||
livekit-client: ^1.12.0 || ^2.1.0
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.3.1':
|
||||
resolution: {integrity: sha512-VRjWhE1UgHnPpJ3b9B5+8z71ZC/HICFngPPFIN6ktzmUBKI5RusPujzbAQUoB3CgZ0yU58L99AfSQS4YTztSWw==}
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.4.0':
|
||||
resolution: {integrity: sha512-osxkU1DQ+05+anGHapjWyvZqdHUb94Id37gy54mCKn1Cq/D7iGT5oEUEhjp4oTnCLo4TOtI6ULJ/LHsapaIptQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@mdx-js/react@3.1.1':
|
||||
@@ -3256,8 +3251,8 @@ packages:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
'@vector-im/compound-web@9.9.0':
|
||||
resolution: {integrity: sha512-GjAEhzB/mt9MA7Y1ySe6/AlW26vodDu4hvTi1ThB5rTrt4KAiHMpvk+T56KV5Lwo0VvBxAyHMtm/IY5tkyH12A==}
|
||||
'@vector-im/compound-web@10.0.1':
|
||||
resolution: {integrity: sha512-CT3aGMalPSf53o3JUbBF7EA10Rfp+auftIuBwOUSna+hfZ/zPxGLTFcuCuOJ7L8hrzijM+V4I8L0PUkfyOczpg==}
|
||||
peerDependencies:
|
||||
'@fontsource/inconsolata': ^5
|
||||
'@fontsource/inter': ^5
|
||||
@@ -4381,8 +4376,8 @@ packages:
|
||||
resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-network-error@1.3.1:
|
||||
resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==}
|
||||
is-network-error@1.3.2:
|
||||
resolution: {integrity: sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
is-number@7.0.0:
|
||||
@@ -4467,6 +4462,9 @@ packages:
|
||||
jose@6.2.4:
|
||||
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:
|
||||
resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==}
|
||||
|
||||
@@ -4518,10 +4516,6 @@ packages:
|
||||
jsonfile@6.2.0:
|
||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||
|
||||
jwt-decode@4.0.0:
|
||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -4685,9 +4679,9 @@ packages:
|
||||
matrix-events-sdk@0.0.1:
|
||||
resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==}
|
||||
|
||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7:
|
||||
resolution: {gitHosted: true, integrity: sha512-ThGe61xx0d1L1weA8hvodzZOcATv2jR+LXd/WKwSsVT2GcR8AADY4pPxgaNvhFfl9tLMi2qMHeNay36RjDa6Zw==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8c95727b6278fe7942c20d0b9485f984dd0694b7}
|
||||
version: 41.7.0
|
||||
matrix-js-sdk@https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0:
|
||||
resolution: {gitHosted: true, integrity: sha512-UmlkoXs9VMZvNaDeQqBb2k+gLifnIqEZc5cxrLeEwdYEDakQLbRqrFtmvIp4ipUCjdIPkCgtzb7QLlpPBLAwqA==, tarball: https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/178dc6528ebc6a15e2f6fab38410127f16b500d0}
|
||||
version: 42.1.0
|
||||
engines: {node: '>=22.0.0'}
|
||||
|
||||
matrix-widget-api@1.18.0:
|
||||
@@ -4829,10 +4823,6 @@ packages:
|
||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
oidc-client-ts@3.5.0:
|
||||
resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
@@ -6254,34 +6244,26 @@ snapshots:
|
||||
|
||||
'@babel/compat-data@7.29.7': {}
|
||||
|
||||
'@babel/core@7.29.7':
|
||||
'@babel/core@7.29.7(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@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-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/parser': 7.29.8
|
||||
'@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
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
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
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
transitivePeerDependencies:
|
||||
- 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':
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.8
|
||||
@@ -6300,19 +6282,19 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.8(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.8
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/helper-module-imports': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
'@babel/traverse': 7.29.7
|
||||
'@babel/traverse': 7.29.8(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6337,25 +6319,15 @@ snapshots:
|
||||
|
||||
'@babel/runtime@7.29.7': {}
|
||||
|
||||
'@babel/runtime@8.0.0': {}
|
||||
|
||||
'@babel/template@7.29.7':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/parser': 7.29.8
|
||||
'@babel/types': 7.29.8
|
||||
|
||||
'@babel/traverse@7.29.7':
|
||||
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':
|
||||
'@babel/traverse@7.29.8(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.7
|
||||
'@babel/generator': 7.29.8
|
||||
@@ -6363,7 +6335,7 @@ snapshots:
|
||||
'@babel/parser': 7.29.8
|
||||
'@babel/template': 7.29.7
|
||||
'@babel/types': 7.29.8
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -6829,17 +6801,17 @@ snapshots:
|
||||
'@esbuild/win32-x64@0.28.1':
|
||||
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:
|
||||
eslint: 8.57.1
|
||||
eslint: 8.57.1(supports-color@7.2.0)
|
||||
eslint-visitor-keys: 3.4.3
|
||||
|
||||
'@eslint-community/regexpp@4.12.2': {}
|
||||
|
||||
'@eslint/eslintrc@2.1.4':
|
||||
'@eslint/eslintrc@2.1.4(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
ajv: 6.15.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
@@ -6920,10 +6892,10 @@ snapshots:
|
||||
'@formatjs/intl-localematcher': 0.6.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@humanwhocodes/config-array@0.13.0':
|
||||
'@humanwhocodes/config-array@0.13.0(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
minimatch: 10.2.6
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -7092,7 +7064,7 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@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:
|
||||
'@floating-ui/dom': 1.7.6
|
||||
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
|
||||
@@ -7100,12 +7072,12 @@ snapshots:
|
||||
rxjs: 7.8.2
|
||||
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:
|
||||
'@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
|
||||
events: 3.3.0
|
||||
jose: 6.2.4
|
||||
jose: 6.2.9
|
||||
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
|
||||
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
|
||||
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)':
|
||||
dependencies:
|
||||
@@ -8075,9 +8047,9 @@ snapshots:
|
||||
'@rolldown/binding-win32-x64-msvc@1.1.5':
|
||||
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:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
picomatch: 4.0.4
|
||||
rolldown: 1.1.5
|
||||
optionalDependencies:
|
||||
@@ -8220,11 +8192,11 @@ snapshots:
|
||||
'@sentry-internal/replay-canvas': 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:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@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
|
||||
find-up: 5.0.0
|
||||
glob: 10.5.0
|
||||
@@ -8258,9 +8230,9 @@ snapshots:
|
||||
'@sentry/cli-win32-x64@2.58.5':
|
||||
optional: true
|
||||
|
||||
'@sentry/cli@2.58.5':
|
||||
'@sentry/cli@2.58.5(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
https-proxy-agent: 5.0.1
|
||||
https-proxy-agent: 5.0.1(supports-color@7.2.0)
|
||||
node-fetch: 2.7.0
|
||||
progress: 2.0.3
|
||||
proxy-from-env: 1.1.0
|
||||
@@ -8287,9 +8259,9 @@ snapshots:
|
||||
hoist-non-react-statics: 3.3.2
|
||||
react: 19.2.8
|
||||
|
||||
'@sentry/vite-plugin@3.6.1':
|
||||
'@sentry/vite-plugin@3.6.1(supports-color@7.2.0)':
|
||||
dependencies:
|
||||
'@sentry/bundler-plugin-core': 3.6.1
|
||||
'@sentry/bundler-plugin-core': 3.6.1(supports-color@7.2.0)
|
||||
unplugin: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
- 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-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: 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:
|
||||
- react
|
||||
|
||||
@@ -8366,16 +8338,16 @@ snapshots:
|
||||
'@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:
|
||||
'@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)
|
||||
'@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
|
||||
magic-string: 0.30.21
|
||||
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)
|
||||
resolve: 1.22.12
|
||||
storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8)
|
||||
@@ -8391,12 +8363,12 @@ snapshots:
|
||||
- supports-color
|
||||
- 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:
|
||||
'@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))
|
||||
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-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)
|
||||
@@ -8407,54 +8379,54 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@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:
|
||||
'@babel/core': 7.29.7
|
||||
'@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.7)
|
||||
'@svgr/babel-plugin-transform-svg-component': 8.0.0(@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(supports-color@7.2.0))
|
||||
'@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(supports-color@7.2.0))
|
||||
'@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(supports-color@7.2.0))
|
||||
'@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(supports-color@7.2.0))
|
||||
'@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:
|
||||
'@babel/core': 7.29.7
|
||||
'@svgr/babel-preset': 8.1.0(@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(supports-color@7.2.0))
|
||||
camelcase: 6.3.0
|
||||
cosmiconfig: 8.3.6(typescript@5.9.3)
|
||||
snake-case: 3.0.4
|
||||
@@ -8467,11 +8439,11 @@ snapshots:
|
||||
'@babel/types': 7.29.8
|
||||
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:
|
||||
'@babel/core': 7.29.7
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.29.7)
|
||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@svgr/babel-preset': 8.1.0(@babel/core@7.29.7(supports-color@7.2.0))
|
||||
'@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@5.9.3)
|
||||
'@svgr/hast-util-to-babel-ast': 8.0.0
|
||||
svg-parser: 2.0.4
|
||||
transitivePeerDependencies:
|
||||
@@ -8679,11 +8651,11 @@ snapshots:
|
||||
'@types/trusted-types@2.0.7':
|
||||
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:
|
||||
'@typescript-eslint/tsconfig-utils': 8.65.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.65.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8699,13 +8671,13 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@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/types': 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
|
||||
semver: 7.8.5
|
||||
tinyglobby: 0.2.17
|
||||
@@ -8714,13 +8686,13 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@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/types': 8.65.0
|
||||
'@typescript-eslint/typescript-estree': 8.65.0(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
'@typescript-eslint/typescript-estree': 8.65.0(supports-color@7.2.0)(typescript@5.9.3)
|
||||
eslint: 8.57.1(supports-color@7.2.0)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -8744,7 +8716,7 @@ snapshots:
|
||||
'@types/react': 19.2.17
|
||||
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:
|
||||
'@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)
|
||||
@@ -8766,12 +8738,12 @@ snapshots:
|
||||
- '@types/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:
|
||||
'@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)
|
||||
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
|
||||
|
||||
'@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))
|
||||
playwright: 1.62.0
|
||||
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:
|
||||
- bufferutil
|
||||
- msw
|
||||
@@ -8796,7 +8768,7 @@ snapshots:
|
||||
pngjs: 7.0.0
|
||||
sirv: 3.0.2
|
||||
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
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -8816,7 +8788,7 @@ snapshots:
|
||||
obug: 2.1.4
|
||||
std-env: 4.2.0
|
||||
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:
|
||||
'@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
|
||||
tinyglobby: 0.2.17
|
||||
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':
|
||||
dependencies:
|
||||
@@ -8904,9 +8876,9 @@ snapshots:
|
||||
|
||||
acorn@8.18.0: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
agent-base@6.0.2(supports-color@7.2.0):
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -9234,7 +9206,7 @@ snapshots:
|
||||
cosmiconfig@8.3.6(typescript@5.9.3):
|
||||
dependencies:
|
||||
import-fresh: 3.3.1
|
||||
js-yaml: 4.3.0
|
||||
js-yaml: 4.3.1
|
||||
parse-json: 5.2.0
|
||||
path-type: 4.0.0
|
||||
optionalDependencies:
|
||||
@@ -9329,9 +9301,11 @@ snapshots:
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
|
||||
debug@4.4.3:
|
||||
debug@4.4.3(supports-color@7.2.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 7.2.0
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
@@ -9507,11 +9481,11 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@typescript-eslint/types': 8.65.0
|
||||
'@typescript-eslint/utils': 8.65.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
eslint: 8.57.1
|
||||
'@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(supports-color@7.2.0)
|
||||
storybook: 10.5.5(@types/react@19.2.17)(prettier@3.8.3)(react@19.2.8)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -9526,20 +9500,20 @@ snapshots:
|
||||
|
||||
eslint-visitor-keys@5.0.1: {}
|
||||
|
||||
eslint@8.57.1:
|
||||
eslint@8.57.1(supports-color@7.2.0):
|
||||
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/eslintrc': 2.1.4
|
||||
'@eslint/eslintrc': 2.1.4(supports-color@7.2.0)
|
||||
'@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
|
||||
'@nodelib/fs.walk': 1.2.8
|
||||
'@ungap/structured-clone': 1.3.3
|
||||
ajv: 6.15.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
@@ -9655,13 +9629,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.5
|
||||
|
||||
fetch-mock@11.1.5:
|
||||
fetch-mock@11.1.5(node-fetch@2.7.0):
|
||||
dependencies:
|
||||
'@types/glob-to-regexp': 0.4.4
|
||||
dequal: 2.0.3
|
||||
glob-to-regexp: 0.4.1
|
||||
is-subset: 0.1.1
|
||||
regexparam: 3.0.0
|
||||
optionalDependencies:
|
||||
node-fetch: 2.7.0
|
||||
|
||||
fflate@0.4.8: {}
|
||||
|
||||
@@ -9859,26 +9835,26 @@ snapshots:
|
||||
|
||||
html-parse-stringify@4.0.1: {}
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
http-proxy-agent@7.0.2(supports-color@7.2.0):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-browserify@1.0.0: {}
|
||||
|
||||
https-proxy-agent@5.0.1:
|
||||
https-proxy-agent@5.0.1(supports-color@7.2.0):
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.4.3
|
||||
agent-base: 6.0.2(supports-color@7.2.0)
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
https-proxy-agent@7.0.6(supports-color@7.2.0):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@7.2.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -10009,7 +9985,7 @@ snapshots:
|
||||
call-bind: 1.0.9
|
||||
define-properties: 1.2.1
|
||||
|
||||
is-network-error@1.3.1: {}
|
||||
is-network-error@1.3.2: {}
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
@@ -10079,6 +10055,8 @@ snapshots:
|
||||
|
||||
jose@6.2.4: {}
|
||||
|
||||
jose@6.2.9: {}
|
||||
|
||||
js-tokens@10.0.0: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
@@ -10091,14 +10069,14 @@ snapshots:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
jsdom@26.1.0:
|
||||
jsdom@26.1.0(supports-color@7.2.0):
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
http-proxy-agent: 7.0.2(supports-color@7.2.0)
|
||||
https-proxy-agent: 7.0.6(supports-color@7.2.0)
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.23
|
||||
parse5: 7.3.0
|
||||
@@ -10138,8 +10116,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jwt-decode@4.0.0: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -10293,18 +10269,16 @@ snapshots:
|
||||
|
||||
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:
|
||||
'@babel/runtime': 7.29.2
|
||||
'@matrix-org/matrix-sdk-crypto-wasm': 18.3.1
|
||||
'@babel/runtime': 8.0.0
|
||||
'@matrix-org/matrix-sdk-crypto-wasm': 18.4.0
|
||||
another-json: 0.2.0
|
||||
bs58: 6.0.0
|
||||
content-type: 2.0.0
|
||||
jwt-decode: 4.0.0
|
||||
loglevel: 1.9.2
|
||||
matrix-events-sdk: 0.0.1
|
||||
matrix-widget-api: 1.18.0
|
||||
oidc-client-ts: 3.5.0
|
||||
p-retry: 8.0.0
|
||||
sdp-transform: 3.0.0
|
||||
unhomoglyph: 1.0.6
|
||||
@@ -10454,10 +10428,6 @@ snapshots:
|
||||
|
||||
obug@2.1.4: {}
|
||||
|
||||
oidc-client-ts@3.5.0:
|
||||
dependencies:
|
||||
jwt-decode: 4.0.0
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
@@ -10642,7 +10612,7 @@ snapshots:
|
||||
|
||||
p-retry@8.0.0:
|
||||
dependencies:
|
||||
is-network-error: 1.3.1
|
||||
is-network-error: 1.3.2
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
@@ -11060,10 +11030,10 @@ snapshots:
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
react-docgen@8.0.3:
|
||||
react-docgen@8.0.3(supports-color@7.2.0):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@babel/traverse': 7.29.8
|
||||
'@babel/core': 7.29.7(supports-color@7.2.0)
|
||||
'@babel/traverse': 7.29.8(supports-color@7.2.0)
|
||||
'@babel/types': 7.29.8
|
||||
'@types/babel__core': 7.20.5
|
||||
'@types/babel__traverse': 7.28.0
|
||||
@@ -11516,7 +11486,7 @@ snapshots:
|
||||
terser@5.46.1:
|
||||
dependencies:
|
||||
'@jridgewell/source-map': 0.3.11
|
||||
acorn: 8.16.0
|
||||
acorn: 8.18.0
|
||||
commander: 2.20.3
|
||||
source-map-support: 0.5.21
|
||||
|
||||
@@ -11745,11 +11715,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||
'@svgr/plugin-jsx': 8.1.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/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)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
@@ -11782,9 +11752,9 @@ snapshots:
|
||||
axe-core: 4.11.3
|
||||
chalk: 5.6.2
|
||||
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:
|
||||
'@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))
|
||||
@@ -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/coverage-v8': 4.1.10(@vitest/browser@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:
|
||||
- msw
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ allowBuilds:
|
||||
"@swc/core": true
|
||||
"core-js": 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
|
||||
overrides:
|
||||
# We need compatible versions of RxJS in our dependencies and LiveKit's dependencies, but
|
||||
|
||||
@@ -31,6 +31,11 @@ interface Props {
|
||||
max: number;
|
||||
step: number;
|
||||
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,
|
||||
step,
|
||||
disabled,
|
||||
tooltipFormatter,
|
||||
}) => {
|
||||
const onValueChange = useCallback(
|
||||
([v]: number[]) => onValueChangeProp(v),
|
||||
@@ -71,7 +77,14 @@ export const Slider: FC<Props> = ({
|
||||
<Range className={styles.highlight} />
|
||||
</Track>
|
||||
{/* 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} />
|
||||
</Tooltip>
|
||||
</Root>
|
||||
|
||||
@@ -74,10 +74,7 @@ export interface ConfigOptions {
|
||||
livekit?: {
|
||||
// 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
|
||||
// a livekit service url in the client well-known.
|
||||
// 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"
|
||||
// a livekit service url over the transports endpoint.
|
||||
livekit_service_url: string;
|
||||
};
|
||||
|
||||
@@ -105,6 +102,61 @@ export interface ConfigOptions {
|
||||
*/
|
||||
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?: {
|
||||
/**
|
||||
* Defines whether participants should start with audio enabled by default.
|
||||
@@ -185,6 +237,24 @@ export interface ConfigOptions {
|
||||
export interface ResolvedConfigOptions extends ConfigOptions {
|
||||
sync_disconnect_grace_period_ms: number;
|
||||
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: {
|
||||
wait_for_key_rotation_ms?: number;
|
||||
delayed_leave_event_delay_ms: number;
|
||||
@@ -201,6 +271,19 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
},
|
||||
sync_disconnect_grace_period_ms: 10000,
|
||||
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: {
|
||||
delayed_leave_event_delay_ms: 10000,
|
||||
network_error_retry_ms: 1000,
|
||||
|
||||
@@ -41,6 +41,8 @@ import { TileWrapper } from "./TileWrapper";
|
||||
import { usePrefersReducedMotion } from "../usePrefersReducedMotion";
|
||||
import { useInitial } from "../useInitial";
|
||||
|
||||
const MAX_ANIMATED_TILES = 50; // Capped for performance reasons
|
||||
|
||||
interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -285,7 +287,6 @@ export function Grid<
|
||||
const [visibleTilesCallback, setVisibleTilesCallback] =
|
||||
useState<VisibleTilesCallback | null>(null);
|
||||
const tiles = useInitial(() => new Map<string, Tile<TileModel>>());
|
||||
const prefersReducedMotion = usePrefersReducedMotion();
|
||||
|
||||
const Slot: FC<SlotProps<TileModel>> = useMemo(
|
||||
() =>
|
||||
@@ -372,6 +373,10 @@ export function Grid<
|
||||
// react-spring's imperative API during gestures to improve responsiveness
|
||||
const dragState = useRef<DragState | null>(null);
|
||||
|
||||
// If true, disables animations
|
||||
const immediate =
|
||||
usePrefersReducedMotion() || placedTiles.length > MAX_ANIMATED_TILES;
|
||||
|
||||
const [tileTransitions, springRef] = useTransition(
|
||||
placedTiles,
|
||||
() => ({
|
||||
@@ -389,9 +394,9 @@ export function Grid<
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
immediate: prefersReducedMotion,
|
||||
immediate,
|
||||
}),
|
||||
enter: { opacity: 1, scale: 1, immediate: prefersReducedMotion },
|
||||
enter: { opacity: 1, scale: 1, immediate },
|
||||
update: ({
|
||||
id,
|
||||
x,
|
||||
@@ -406,9 +411,9 @@ export function Grid<
|
||||
y,
|
||||
width,
|
||||
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 },
|
||||
}),
|
||||
// react-spring's types are bugged and can't infer the spring type
|
||||
@@ -441,8 +446,7 @@ export function Grid<
|
||||
y: tile.y,
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
immediate:
|
||||
prefersReducedMotion || ((key): boolean => key === "zIndex"),
|
||||
immediate: immediate || ((key): boolean => key === "zIndex"),
|
||||
// Allow the tile's position to settle before pushing its
|
||||
// z-index back down
|
||||
delay: (key): number => (key === "zIndex" ? 500 : 0),
|
||||
@@ -453,7 +457,7 @@ export function Grid<
|
||||
x: tileX,
|
||||
y: tileY,
|
||||
immediate:
|
||||
prefersReducedMotion ||
|
||||
immediate ||
|
||||
((key): boolean =>
|
||||
key === "zIndex" || key === "x" || key === "y"),
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { type FC, type ReactNode } from "react";
|
||||
import { useObservableEagerState } from "observable-hooks";
|
||||
import classNames from "classnames";
|
||||
|
||||
@@ -13,6 +13,9 @@ import { type CallLayout } from "./CallLayout";
|
||||
import { type SpotlightLandscapeLayout as SpotlightLandscapeLayoutModel } from "../state/layout-types.ts";
|
||||
import styles from "./SpotlightLandscapeLayout.module.css";
|
||||
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
|
||||
@@ -54,16 +57,10 @@ export const makeSpotlightLandscapeLayout: CallLayout<
|
||||
useUpdateLayout();
|
||||
useVisibleTiles(model.setVisibleTiles);
|
||||
useObservableEagerState(minBounds$);
|
||||
const withIndicators =
|
||||
useObservableEagerState(model.spotlight.media$).length > 1;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.layer}>
|
||||
<div
|
||||
className={classNames(styles.spotlight, {
|
||||
[styles.withIndicators]: withIndicators,
|
||||
})}
|
||||
/>
|
||||
<SpotlightSlot media$={model.spotlight.media$} />
|
||||
<div className={styles.grid}>
|
||||
{model.grid.map((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,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { Config } from "./config/Config";
|
||||
import { seedSettingsFromConfig } from "./settings/settings";
|
||||
import { platform } from "./Platform";
|
||||
import { isFailure } from "./utils/fetch";
|
||||
import { initializeWidget } from "./widget";
|
||||
@@ -237,6 +238,7 @@ export class Initializer {
|
||||
this.loadStates.config = LoadState.Loading;
|
||||
Config.init().then(
|
||||
() => {
|
||||
seedSettingsFromConfig(Config.get().media_quality);
|
||||
this.loadStates.config = LoadState.Loaded;
|
||||
this.initStep(resolve);
|
||||
},
|
||||
|
||||
213
src/livekit/options.test.ts
Normal file
213
src/livekit/options.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -9,46 +9,144 @@ import {
|
||||
AudioPresets,
|
||||
DefaultReconnectPolicy,
|
||||
type RoomOptions,
|
||||
ScreenSharePresets,
|
||||
type TrackPublishDefaults,
|
||||
type VideoPreset,
|
||||
VideoPresets,
|
||||
VideoPreset as VideoPresetClass,
|
||||
} from "livekit-client";
|
||||
|
||||
const defaultLiveKitPublishOptions: TrackPublishDefaults = {
|
||||
audioPreset: AudioPresets.music,
|
||||
dtx: true,
|
||||
// disable red because the livekit server strips out red packets for clients
|
||||
// that don't support it (firefox) but of course that doesn't work with e2ee.
|
||||
red: false,
|
||||
forceStereo: false,
|
||||
simulcast: true,
|
||||
videoSimulcastLayers: [VideoPresets.h180, VideoPresets.h360] as VideoPreset[],
|
||||
screenShareEncoding: ScreenSharePresets.h1080fps30.encoding,
|
||||
stopMicTrackOnMute: false,
|
||||
videoCodec: "vp8",
|
||||
videoEncoding: VideoPresets.h720.encoding,
|
||||
backupCodec: { codec: "vp8", encoding: VideoPresets.h720.encoding },
|
||||
} as const;
|
||||
import { Config } from "../config/Config";
|
||||
import { DEFAULT_CONFIG, type ConfigOptions } from "../config/ConfigOptions";
|
||||
|
||||
export const defaultLiveKitOptions: RoomOptions = {
|
||||
// automatically manage subscribed video quality
|
||||
adaptiveStream: true,
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// optimize publishing bandwidth and CPU for published tracks
|
||||
dynacast: true,
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// capture settings
|
||||
videoCaptureDefaults: {
|
||||
resolution: VideoPresets.h720.resolution,
|
||||
},
|
||||
// Camera video encoding
|
||||
const videoEncoding = {
|
||||
maxBitrate: videoConf?.max_bitrate ?? defaults.video.max_bitrate,
|
||||
maxFramerate: videoConf?.max_framerate ?? defaults.video.max_framerate,
|
||||
};
|
||||
|
||||
// publish settings
|
||||
publishDefaults: defaultLiveKitPublishOptions,
|
||||
// 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];
|
||||
}
|
||||
|
||||
// default LiveKit options that seem to be sane
|
||||
stopLocalTrackOnUnpublish: true,
|
||||
reconnectPolicy: new DefaultReconnectPolicy(),
|
||||
disconnectOnPageLeave: true,
|
||||
webAudioMix: false,
|
||||
};
|
||||
// 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,
|
||||
dtx: true,
|
||||
// disable red because the livekit server strips out red packets for clients
|
||||
// that don't support it (firefox) but of course that doesn't work with e2ee.
|
||||
red: false,
|
||||
forceStereo: false,
|
||||
simulcast: true,
|
||||
videoSimulcastLayers: videoSimulcastLayers as VideoPreset[],
|
||||
screenShareEncoding,
|
||||
...(screenShareSimulcastLayers && {
|
||||
screenShareSimulcastLayers: screenShareSimulcastLayers as VideoPreset[],
|
||||
}),
|
||||
stopMicTrackOnMute: false,
|
||||
videoCodec: codec,
|
||||
videoEncoding,
|
||||
backupCodec: {
|
||||
codec: "vp8",
|
||||
encoding: VideoPresets.h720.encoding,
|
||||
},
|
||||
} as TrackPublishDefaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
adaptiveStream: true,
|
||||
|
||||
// optimize publishing bandwidth and CPU for published tracks
|
||||
dynacast: true,
|
||||
|
||||
// capture settings
|
||||
videoCaptureDefaults: {
|
||||
resolution: basePreset.resolution,
|
||||
},
|
||||
|
||||
// publish settings
|
||||
publishDefaults: buildPublishOptions(mediaQuality),
|
||||
|
||||
// default LiveKit options that seem to be sane
|
||||
stopLocalTrackOnUnpublish: true,
|
||||
reconnectPolicy: new DefaultReconnectPolicy(),
|
||||
disconnectOnPageLeave: true,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
|
||||
import {
|
||||
type ChangeEvent,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -29,12 +30,14 @@ import {
|
||||
InlineField,
|
||||
Label,
|
||||
RadioControl,
|
||||
Separator,
|
||||
} from "@vector-im/compound-web";
|
||||
import { type Room as LivekitRoom } from "livekit-client";
|
||||
|
||||
import { FieldRow, InputField } from "../input/Input";
|
||||
import { Config } from "../config/Config";
|
||||
import {
|
||||
type Setting,
|
||||
useSetting,
|
||||
duplicateTiles as duplicateTilesSetting,
|
||||
debugTileLayout as debugTileLayoutSetting,
|
||||
@@ -43,10 +46,26 @@ import {
|
||||
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
||||
matrixRTCMode as matrixRTCModeSetting,
|
||||
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,
|
||||
} from "./settings";
|
||||
import { MatrixRTCMode } from "../config/ConfigOptions";
|
||||
import styles from "./DeveloperSettingsTab.module.css";
|
||||
import settingsStyles from "./SettingsModal.module.css";
|
||||
import { Slider } from "../Slider";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import { getSFUConfigWithOpenID } from "../livekit/openIDSFU";
|
||||
|
||||
@@ -138,6 +157,185 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
return null;
|
||||
}, [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 (
|
||||
<>
|
||||
<p>
|
||||
@@ -166,6 +364,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
id: client.getDeviceId() || "unknown",
|
||||
})}
|
||||
</p>
|
||||
<Separator />
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="duplicateTiles"
|
||||
@@ -315,6 +514,7 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
<ErrorMessage>{customLivekitUrlUpdateError}</ErrorMessage>
|
||||
)}
|
||||
</EditInPlace>
|
||||
<Separator />
|
||||
<Heading as="h3" type="body" weight="semibold" size="lg">
|
||||
{t("developer_mode.matrixRTCMode.title")}
|
||||
</Heading>
|
||||
@@ -403,6 +603,61 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
</ul>
|
||||
</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>
|
||||
<pre>{JSON.stringify(env, null, 2)}</pre>
|
||||
<p>{t("developer_mode.url_params")}</p>
|
||||
|
||||
@@ -33,3 +33,27 @@ Please see LICENSE in the repository root for full details.
|
||||
.volumeSlider > p {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,13 @@ export const SettingsModal: FC<Props> = ({
|
||||
/>
|
||||
|
||||
<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>
|
||||
<Slider
|
||||
label={t("video_tile.volume")}
|
||||
|
||||
@@ -17,6 +17,12 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
<p>
|
||||
Device ID: DEVICE123
|
||||
</p>
|
||||
<div
|
||||
class="_separator_13qwf_8"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<div
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
@@ -251,10 +257,16 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
||||
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>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
class="_separator_13qwf_8"
|
||||
data-kind="primary"
|
||||
data-orientation="horizontal"
|
||||
role="separator"
|
||||
/>
|
||||
<h3
|
||||
class="_typography_6v6n8_153 _font-body-lg-semibold_6v6n8_74"
|
||||
>
|
||||
@@ -463,6 +475,242 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</p>
|
||||
<ul />
|
||||
</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>
|
||||
Environment variables
|
||||
</p>
|
||||
|
||||
111
src/settings/settings.test.ts
Normal file
111
src/settings/settings.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export class Setting<T> {
|
||||
this.key = `matrix-setting-${key}`;
|
||||
|
||||
const storedValue = localStorage.getItem(this.key);
|
||||
this.hasStoredValue = storedValue !== null;
|
||||
let initialValue = defaultValue;
|
||||
if (storedValue !== null) {
|
||||
try {
|
||||
@@ -40,6 +41,7 @@ export class Setting<T> {
|
||||
}
|
||||
|
||||
private readonly key: string;
|
||||
private readonly hasStoredValue: boolean;
|
||||
|
||||
private readonly _value$: BehaviorSubject<T>;
|
||||
private readonly _lastUpdateReason$: BehaviorSubject<string | null>;
|
||||
@@ -54,6 +56,17 @@ export class Setting<T> {
|
||||
public readonly getValue = (): T => {
|
||||
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",
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,10 @@ import { useClient } from "../ClientContext";
|
||||
import { Config } from "../config/Config";
|
||||
import { type RageshakeRequestModal } from "../room/RageshakeRequestModal";
|
||||
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> => {
|
||||
// pako is relatively large (200KB), so we only import it when needed
|
||||
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) {
|
||||
try {
|
||||
const estimate: {
|
||||
|
||||
@@ -22,6 +22,8 @@ import { useSubmitRageshake, getRageshakeSubmitUrl } from "./submit-rageshake";
|
||||
import { ClientContextProvider } from "../ClientContext";
|
||||
import { getUrlParams } from "../UrlParams";
|
||||
import { mockConfig } from "../utils/test";
|
||||
import { DEFAULT_CONFIG } from "../config/ConfigOptions";
|
||||
import { advancedCamera, advancedScreenShare } from "./settings";
|
||||
|
||||
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", () => {
|
||||
it("starts unsent", () => {
|
||||
renderWithMockClient(() => undefined, false);
|
||||
|
||||
@@ -513,8 +513,6 @@ export function createCallViewModel$(
|
||||
livekitKeyProvider,
|
||||
getUrlParams().controlledAudioDevices,
|
||||
options.livekitRoomFactory,
|
||||
getUrlParams().echoCancellation,
|
||||
getUrlParams().noiseSuppression,
|
||||
);
|
||||
|
||||
const connectionManager = createConnectionManager$({
|
||||
|
||||
@@ -138,9 +138,6 @@ export function withCallViewModel(mode: MatrixRTCMode) {
|
||||
public getSyncState(): SyncState {
|
||||
return syncState;
|
||||
}
|
||||
public getAccessToken(): string | null {
|
||||
return "a-token";
|
||||
}
|
||||
})() as Partial<MatrixClient> as MatrixClient,
|
||||
getMembers: () => roomMembers,
|
||||
getMembersWithMembership: () => roomMembers,
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
afterAll,
|
||||
beforeEach,
|
||||
} from "vitest";
|
||||
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
|
||||
import { BehaviorSubject, map, of } from "rxjs";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type LocalParticipant, type LocalTrack } from "livekit-client";
|
||||
@@ -78,34 +77,10 @@ describe("LocalMembership", () => {
|
||||
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({
|
||||
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({
|
||||
room: {
|
||||
roomId: "roomId",
|
||||
@@ -132,7 +107,7 @@ describe("LocalMembership", () => {
|
||||
ownMemberMock,
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-well-known-service-url.com",
|
||||
livekit_service_url: "http://my-livekit-service-url.com",
|
||||
type: "livekit",
|
||||
},
|
||||
{
|
||||
@@ -150,7 +125,7 @@ describe("LocalMembership", () => {
|
||||
[
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-well-known-service-url.com",
|
||||
livekit_service_url: "http://my-livekit-service-url.com",
|
||||
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 = {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ParticipantEvent,
|
||||
type LocalParticipant,
|
||||
type ScreenShareCaptureOptions,
|
||||
type TrackPublishOptions,
|
||||
RoomEvent,
|
||||
MediaDeviceFailure,
|
||||
} from "livekit-client";
|
||||
@@ -53,6 +54,14 @@ import {
|
||||
import { ElementWidgetActions, widget } from "../../../widget.ts";
|
||||
import { getUrlParams } from "../../../UrlParams.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 { Config } from "../../../config/Config.ts";
|
||||
import {
|
||||
@@ -108,7 +117,6 @@ export type LocalMemberState =
|
||||
};
|
||||
|
||||
/*
|
||||
* - get well known
|
||||
* - get oldest membership
|
||||
* - get transport to use
|
||||
* - get openId + jwt token
|
||||
@@ -719,6 +727,43 @@ export const createLocalMembership$ = ({
|
||||
surfaceSwitching: "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;
|
||||
logger.info(
|
||||
`toggleScreenSharing called. Switching ${
|
||||
@@ -734,7 +779,11 @@ export const createLocalMembership$ = ({
|
||||
// is still initializing or publishing tracks, because there's no
|
||||
// technical reason to disallow this. LiveKit will publish if it can.
|
||||
participant$.value
|
||||
?.setScreenShareEnabled(targetScreenshareState, screenshareSettings)
|
||||
?.setScreenShareEnabled(
|
||||
targetScreenshareState,
|
||||
screenshareSettings,
|
||||
publishOptions,
|
||||
)
|
||||
.catch(logger.error);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ describe("LocalTransport", () => {
|
||||
client: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
getDomain: () => "",
|
||||
getDomain: () => "example.org",
|
||||
baseUrl: "example.org",
|
||||
// These won't be called in this error path but satisfy the type
|
||||
getOpenIdToken: vi.fn(),
|
||||
@@ -77,9 +76,11 @@ describe("LocalTransport", () => {
|
||||
await flushPromises();
|
||||
|
||||
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 () => {
|
||||
@@ -103,10 +104,8 @@ describe("LocalTransport", () => {
|
||||
useOldestMember: false,
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
baseUrl: "https://lk.example.org",
|
||||
// Use empty domain to skip .well-known and use config directly
|
||||
getDomain: () => "",
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
baseUrl: "https://example.org",
|
||||
getDomain: () => "example.org",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
getOpenIdToken: vi.fn(),
|
||||
@@ -150,11 +149,10 @@ describe("LocalTransport", () => {
|
||||
client: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
getDomain: () => "",
|
||||
getDomain: () => "example.org",
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
baseUrl: "https://lk.example.org",
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
baseUrl: "https://example.org",
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
@@ -221,13 +219,12 @@ describe("LocalTransport", () => {
|
||||
useOldestMember: true,
|
||||
memberships$: scope.behavior(memberships$.pipe(trackEpoch())),
|
||||
client: {
|
||||
getDomain: () => "",
|
||||
getDomain: () => "example.org",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
baseUrl: "https://lk.example.org",
|
||||
baseUrl: "https://example.org",
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
@@ -278,14 +275,13 @@ describe("LocalTransport", () => {
|
||||
useOldestMember: true,
|
||||
memberships$: scope.behavior(memberships$.pipe(trackEpoch())),
|
||||
client: {
|
||||
getDomain: () => "",
|
||||
getDomain: () => "example.org",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () =>
|
||||
Promise.resolve([aliceTransport]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
baseUrl: "https://lk.example.org",
|
||||
baseUrl: "https://example.org",
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
@@ -330,10 +326,9 @@ describe("LocalTransport", () => {
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
baseUrl: "https://example.org",
|
||||
getDomain: vi.fn().mockReturnValue(""),
|
||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
getOpenIdToken: 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 () => {
|
||||
localTransportOpts.client._unstable_getRTCTransports.mockResolvedValue([
|
||||
{ type: "livekit", livekit_service_url: "https://lk.example.org" },
|
||||
@@ -469,51 +428,6 @@ describe("LocalTransport", () => {
|
||||
).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 () => {
|
||||
const { advertised$, active$ } = createLocalTransport$({
|
||||
scope: testScope(),
|
||||
@@ -524,11 +438,10 @@ describe("LocalTransport", () => {
|
||||
delayId$: constant(null),
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
getDomain: () => "",
|
||||
getDomain: () => "example.org",
|
||||
baseUrl: "https://example.org",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
// These won't be called in this error path but satisfy the type
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
@@ -537,10 +450,10 @@ describe("LocalTransport", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(() => advertised$.value).toThrow(
|
||||
new MatrixRTCTransportMissingError(""),
|
||||
new MatrixRTCTransportMissingError("example.org"),
|
||||
);
|
||||
expect(() => active$.value).toThrow(
|
||||
new MatrixRTCTransportMissingError(""),
|
||||
new MatrixRTCTransportMissingError("example.org"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -565,11 +478,10 @@ describe("LocalTransport", () => {
|
||||
delayId$: delayId$,
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
getDomain: () => "",
|
||||
getDomain: () => "example.org",
|
||||
baseUrl: "https://example.org",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
// These won't be called in this error path but satisfy the type
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
tap,
|
||||
} from "rxjs";
|
||||
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 Behavior } from "../../Behavior.ts";
|
||||
@@ -57,7 +56,7 @@ interface Props {
|
||||
memberships$: Behavior<Epoch<CallMembership[]>>;
|
||||
client: Pick<
|
||||
MatrixClient,
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken"
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
||||
> &
|
||||
OpenIDClientParts;
|
||||
// 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({
|
||||
client: client,
|
||||
resolvedConfig: Config.get(),
|
||||
wellKnownFetcher: AutoDiscovery.getRawClientConfig.bind(AutoDiscovery),
|
||||
logger: logger,
|
||||
});
|
||||
|
||||
@@ -309,7 +307,7 @@ async function doOpenIdAndJWTFromUrl(
|
||||
roomId: string,
|
||||
client: Pick<
|
||||
MatrixClient,
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken"
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
||||
> &
|
||||
OpenIDClientParts,
|
||||
delayId?: string,
|
||||
@@ -339,7 +337,7 @@ function observeLocalTransportForOldestMembership(
|
||||
preferredTransport$: Observable<LocalTransportWithSFUConfig>,
|
||||
client: Pick<
|
||||
MatrixClient,
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports" | "getAccessToken"
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
||||
> &
|
||||
OpenIDClientParts,
|
||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type MockedObject,
|
||||
vi,
|
||||
} 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 {
|
||||
type LivekitTransportConfig,
|
||||
@@ -33,9 +33,9 @@ const backendTransport: LivekitTransportConfig = {
|
||||
livekit_service_url: "https://backend.example.org",
|
||||
};
|
||||
|
||||
const wellKnownTransport: LivekitTransportConfig = {
|
||||
const configTransport: LivekitTransportConfig = {
|
||||
type: "livekit",
|
||||
livekit_service_url: "https://well-known.example.org",
|
||||
livekit_service_url: "https://config.example.org",
|
||||
};
|
||||
|
||||
function makeClient(): MockedObject<DiscoveryClient> {
|
||||
@@ -43,7 +43,6 @@ function makeClient(): MockedObject<DiscoveryClient> {
|
||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||
baseUrl: "https://matrix.example.org",
|
||||
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
|
||||
getAccessToken: vi.fn().mockReturnValue("access_token"),
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
} as unknown as MockedObject<DiscoveryClient>;
|
||||
@@ -59,12 +58,6 @@ function makeResolvedConfig(livekitServiceUrl?: string): ResolvedConfigOptions {
|
||||
} as ResolvedConfigOptions;
|
||||
}
|
||||
|
||||
function makeWellKnown(rtcFoci?: Transport[]): IClientWellKnown {
|
||||
return {
|
||||
"org.matrix.msc4143.rtc_foci": rtcFoci,
|
||||
} as unknown as IClientWellKnown;
|
||||
}
|
||||
|
||||
describe("RtcTransportAutoDiscovery", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -75,33 +68,27 @@ describe("RtcTransportAutoDiscovery", () => {
|
||||
{ transports: [{ type: "not_livekit" }, backendTransport] },
|
||||
];
|
||||
it.each(VALID_TEST_CASES)(
|
||||
"prefers backend transport over well-known and app config $transports",
|
||||
"prefers backend transport other app config $transports",
|
||||
async ({ transports }) => {
|
||||
// it("prefers backend transport over well-known and app config", async () => {
|
||||
const client = makeClient();
|
||||
client._unstable_getRTCTransports.mockResolvedValue(transports);
|
||||
|
||||
const wellKnownFetcher = vi
|
||||
.fn<(domain: string) => Promise<IClientWellKnown>>()
|
||||
.mockResolvedValue(makeWellKnown([wellKnownTransport]));
|
||||
|
||||
const discovery = new RtcTransportAutoDiscovery({
|
||||
client,
|
||||
resolvedConfig: makeResolvedConfig("https://config.example.org"),
|
||||
wellKnownFetcher,
|
||||
resolvedConfig: makeResolvedConfig(configTransport.livekit_service_url),
|
||||
logger: rootLogger,
|
||||
});
|
||||
|
||||
await expect(
|
||||
discovery.discoverPreferredTransport(),
|
||||
).resolves.toStrictEqual(backendTransport);
|
||||
const discoveredTransport = await discovery.discoverPreferredTransport();
|
||||
|
||||
expect(discoveredTransport).toStrictEqual(backendTransport);
|
||||
expect(discoveredTransport).not.toStrictEqual(configTransport);
|
||||
|
||||
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();
|
||||
client._unstable_getRTCTransports
|
||||
.mockRejectedValueOnce(
|
||||
@@ -116,14 +103,9 @@ describe("RtcTransportAutoDiscovery", () => {
|
||||
)
|
||||
.mockResolvedValue([backendTransport]);
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -132,7 +114,6 @@ describe("RtcTransportAutoDiscovery", () => {
|
||||
);
|
||||
|
||||
expect(client._unstable_getRTCTransports).toHaveBeenCalledTimes(2);
|
||||
expect(wellKnownFetcher).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const INVALID_TEST_CASES: Array<{ transports: Transport[] }> = [
|
||||
@@ -140,91 +121,30 @@ describe("RtcTransportAutoDiscovery", () => {
|
||||
{ transports: [{ type: "not_livekit" }] },
|
||||
];
|
||||
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 }) => {
|
||||
const client = makeClient();
|
||||
client._unstable_getRTCTransports.mockResolvedValue(transports);
|
||||
|
||||
const wellKnownFetcher = vi
|
||||
.fn<(domain: string) => Promise<IClientWellKnown>>()
|
||||
.mockResolvedValue(makeWellKnown([wellKnownTransport]));
|
||||
|
||||
const discovery = new RtcTransportAutoDiscovery({
|
||||
client,
|
||||
resolvedConfig: makeResolvedConfig("https://config.example.org"),
|
||||
wellKnownFetcher,
|
||||
resolvedConfig: makeResolvedConfig(configTransport.livekit_service_url),
|
||||
logger: rootLogger,
|
||||
});
|
||||
|
||||
await expect(
|
||||
discovery.discoverPreferredTransport(),
|
||||
).resolves.toStrictEqual(wellKnownTransport);
|
||||
|
||||
expect(wellKnownFetcher).toHaveBeenCalledWith("example.org");
|
||||
const discoveredTransport = await discovery.discoverPreferredTransport();
|
||||
expect(discoveredTransport).not.toStrictEqual(backendTransport);
|
||||
expect(discoveredTransport).toStrictEqual(configTransport);
|
||||
},
|
||||
);
|
||||
|
||||
it("skips backend discovery in widget mode and uses well-known", 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 () => {
|
||||
it("returns null when backend and config are all unavailable", async () => {
|
||||
const client = makeClient();
|
||||
client._unstable_getRTCTransports.mockResolvedValue([]);
|
||||
|
||||
const wellKnownFetcher = vi
|
||||
.fn<(domain: string) => Promise<IClientWellKnown>>()
|
||||
.mockResolvedValue({} as IClientWellKnown);
|
||||
|
||||
const discovery = new RtcTransportAutoDiscovery({
|
||||
client,
|
||||
resolvedConfig: makeResolvedConfig(undefined),
|
||||
wellKnownFetcher,
|
||||
logger: rootLogger,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
isLivekitTransportConfig,
|
||||
type LivekitTransportConfig,
|
||||
} 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 { ResolvedConfigOptions } from "../../../config/ConfigOptions.ts";
|
||||
@@ -16,33 +16,27 @@ import { doNetworkOperationWithRetry } from "../../../utils/matrix.ts";
|
||||
|
||||
type TransportDiscoveryClient = Pick<
|
||||
MatrixClient,
|
||||
"getDomain" | "_unstable_getRTCTransports" | "getAccessToken"
|
||||
"getDomain" | "_unstable_getRTCTransports"
|
||||
>;
|
||||
|
||||
export interface RtcTransportAutoDiscoveryProps {
|
||||
client: TransportDiscoveryClient;
|
||||
resolvedConfig: ResolvedConfigOptions;
|
||||
wellKnownFetcher: (domain: string) => Promise<IClientWellKnown>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export class RtcTransportAutoDiscovery {
|
||||
private readonly client: TransportDiscoveryClient;
|
||||
private readonly resolvedConfig: ResolvedConfigOptions;
|
||||
private readonly wellKnownFetcher: (
|
||||
domain: string,
|
||||
) => Promise<IClientWellKnown>;
|
||||
private readonly logger: Logger;
|
||||
|
||||
public constructor({
|
||||
client,
|
||||
resolvedConfig,
|
||||
wellKnownFetcher,
|
||||
logger,
|
||||
}: RtcTransportAutoDiscoveryProps) {
|
||||
this.client = client;
|
||||
this.resolvedConfig = resolvedConfig;
|
||||
this.wellKnownFetcher = wellKnownFetcher;
|
||||
this.logger = logger.getChild("[RtcTransportAutoDiscovery]");
|
||||
}
|
||||
|
||||
@@ -56,21 +50,7 @@ export class RtcTransportAutoDiscovery {
|
||||
return backendTransport;
|
||||
}
|
||||
|
||||
this.logger.info("No backend transport found, falling back to well-known");
|
||||
// 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
|
||||
// 2) app config URL
|
||||
const configTransport = this.tryConfigTransport();
|
||||
if (configTransport) {
|
||||
this.logger.info(
|
||||
@@ -90,72 +70,23 @@ export class RtcTransportAutoDiscovery {
|
||||
private async tryBackendTransports(): Promise<LivekitTransportConfig | null> {
|
||||
const client = this.client;
|
||||
// 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 ...");
|
||||
try {
|
||||
const transportList = await doNetworkOperationWithRetry(async () =>
|
||||
client._unstable_getRTCTransports(),
|
||||
);
|
||||
const first = transportList.find(isLivekitTransportConfig);
|
||||
if (first) {
|
||||
return first;
|
||||
} else {
|
||||
this.logger.info(
|
||||
`No livekit transport found in getRTCTransports end point`,
|
||||
transportList,
|
||||
);
|
||||
}
|
||||
} catch (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];
|
||||
}
|
||||
this.logger.info("First try to use getRTCTransports end point ...");
|
||||
try {
|
||||
const transportList = await doNetworkOperationWithRetry(async () =>
|
||||
client._unstable_getRTCTransports(),
|
||||
);
|
||||
const first = transportList.find(isLivekitTransportConfig);
|
||||
if (first) {
|
||||
return first;
|
||||
} else {
|
||||
this.logger.info(
|
||||
`No .well-known "org.matrix.msc4143.rtc_foci" found for ${domain}`,
|
||||
wellKnownFoci,
|
||||
`No livekit transport found in getRTCTransports end point`,
|
||||
transportList,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Should never happen, but just in case
|
||||
this.logger.warn(`No domain configured for client`);
|
||||
} catch (ex) {
|
||||
this.logger.info(`Failed to use getRTCTransports end point: ${ex}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
type E2EEManagerOptions,
|
||||
type BaseE2EEManager,
|
||||
} 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)
|
||||
import E2EEWorker from "livekit-client/e2ee-worker?worker&inline";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
@@ -27,7 +27,18 @@ import type {
|
||||
import type { MediaDevices } from "../../MediaDevices.ts";
|
||||
import type { Behavior } from "../../Behavior.ts";
|
||||
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
|
||||
export interface ConnectionFactory {
|
||||
@@ -53,8 +64,6 @@ export class ECConnectionFactory implements ConnectionFactory {
|
||||
* @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 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(
|
||||
private client: OpenIDClientParts,
|
||||
@@ -64,25 +73,22 @@ export class ECConnectionFactory implements ConnectionFactory {
|
||||
livekitKeyProvider: BaseKeyProvider | undefined,
|
||||
private controlledAudioDevices: boolean,
|
||||
livekitRoomFactory?: () => LivekitRoom,
|
||||
echoCancellation: boolean = true,
|
||||
noiseSuppression: boolean = true,
|
||||
) {
|
||||
const defaultFactory = (): LivekitRoom =>
|
||||
new LivekitRoom(
|
||||
generateRoomOption({
|
||||
devices: this.devices,
|
||||
processorState: this.processorState$.value,
|
||||
e2eeLivekitOptions: livekitKeyProvider && {
|
||||
keyProvider: livekitKeyProvider,
|
||||
// It's important that every room use a separate E2EE worker.
|
||||
// They get confused if given streams from multiple rooms.
|
||||
worker: new E2EEWorker(),
|
||||
},
|
||||
controlledAudioDevices: this.controlledAudioDevices,
|
||||
echoCancellation,
|
||||
noiseSuppression,
|
||||
}),
|
||||
);
|
||||
const defaultFactory = (): LivekitRoom => {
|
||||
const roomOptions = generateRoomOption({
|
||||
devices: this.devices,
|
||||
processorState: this.processorState$.value,
|
||||
e2eeLivekitOptions: livekitKeyProvider && {
|
||||
keyProvider: livekitKeyProvider,
|
||||
// It's important that every room use a separate E2EE worker.
|
||||
// They get confused if given streams from multiple rooms.
|
||||
worker: new E2EEWorker(),
|
||||
},
|
||||
controlledAudioDevices: this.controlledAudioDevices,
|
||||
});
|
||||
logger.info("[ECConnectionFactory] livekit room options: ", roomOptions);
|
||||
return new LivekitRoom(roomOptions);
|
||||
};
|
||||
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.
|
||||
* Reads audio processing and camera quality settings directly from Settings.
|
||||
*/
|
||||
function generateRoomOption({
|
||||
devices,
|
||||
processorState,
|
||||
e2eeLivekitOptions,
|
||||
controlledAudioDevices,
|
||||
echoCancellation,
|
||||
noiseSuppression,
|
||||
}: {
|
||||
devices: MediaDevices;
|
||||
processorState: ProcessorState;
|
||||
@@ -135,21 +140,44 @@ function generateRoomOption({
|
||||
| { e2eeManager: BaseE2EEManager }
|
||||
| undefined;
|
||||
controlledAudioDevices: boolean;
|
||||
echoCancellation: boolean;
|
||||
noiseSuppression: boolean;
|
||||
}): RoomOptions {
|
||||
const liveKitOptions = getLiveKitOptions();
|
||||
|
||||
// Apply advanced camera settings if enabled
|
||||
let videoCaptureDefaults = {
|
||||
...liveKitOptions.videoCaptureDefaults,
|
||||
deviceId: devices.videoInput.selected$.value?.id,
|
||||
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 {
|
||||
...defaultLiveKitOptions,
|
||||
videoCaptureDefaults: {
|
||||
...defaultLiveKitOptions.videoCaptureDefaults,
|
||||
deviceId: devices.videoInput.selected$.value?.id,
|
||||
processor: processorState.processor,
|
||||
},
|
||||
...liveKitOptions,
|
||||
videoCaptureDefaults,
|
||||
publishDefaults,
|
||||
audioCaptureDefaults: {
|
||||
...defaultLiveKitOptions.audioCaptureDefaults,
|
||||
...liveKitOptions.audioCaptureDefaults,
|
||||
deviceId: devices.audioInput.selected$.value?.id,
|
||||
echoCancellation,
|
||||
noiseSuppression,
|
||||
echoCancellation: echoCancellationSetting.getValue(),
|
||||
noiseSuppression: noiseSuppressionSetting.getValue(),
|
||||
autoGainControl: autoGainControlSetting.getValue(),
|
||||
},
|
||||
audioOutput: {
|
||||
// When using controlled audio devices, we don't want to set the
|
||||
|
||||
@@ -22,6 +22,16 @@ import {
|
||||
} from "../../../utils/test.ts";
|
||||
import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
|
||||
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
|
||||
vi.mock("livekit-client", async (importOriginal) => {
|
||||
@@ -58,11 +68,14 @@ describe("ECConnectionFactory - Audio inputs options", () => {
|
||||
{ echo: false, noise: true },
|
||||
{ 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 }) => {
|
||||
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
|
||||
const RoomConstructor = vi.mocked(LivekitRoom);
|
||||
|
||||
// Set audio processing settings
|
||||
echoCancellationSetting.setValue(echo);
|
||||
noiseSuppressionSetting.setValue(noise);
|
||||
|
||||
const ecConnectionFactory = new ECConnectionFactory(
|
||||
mockClient,
|
||||
"!roomid:example.org",
|
||||
@@ -73,9 +86,6 @@ describe("ECConnectionFactory - Audio inputs options", () => {
|
||||
}),
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
echo,
|
||||
noise,
|
||||
);
|
||||
ecConnectionFactory.createConnection(
|
||||
testScope,
|
||||
@@ -101,9 +111,13 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
||||
test.each([{ controlled: true }, { controlled: false }])(
|
||||
"it sets controlledAudioDevice=$controlled then uses deviceId accordingly",
|
||||
({ controlled }) => {
|
||||
// test("it sets echoCancellation and noiseSuppression based on constructor parameters", () => {
|
||||
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(
|
||||
mockClient,
|
||||
"!roomid:example.org",
|
||||
@@ -120,9 +134,6 @@ describe("ECConnectionFactory - ControlledAudioDevice", () => {
|
||||
}),
|
||||
undefined,
|
||||
controlled,
|
||||
undefined,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
ecConnectionFactory.createConnection(
|
||||
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(() => {
|
||||
testScope.end();
|
||||
fetchMock.reset();
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
import { type RemoteTrackPublication } from "livekit-client";
|
||||
import {
|
||||
type LocalTrackPublication,
|
||||
type RemoteTrackPublication,
|
||||
} from "livekit-client";
|
||||
import { test, expect } from "vitest";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { axe } from "vitest-axe";
|
||||
@@ -17,6 +20,9 @@ import {
|
||||
mockRtcMembership,
|
||||
mockRemoteMedia,
|
||||
mockRemoteParticipant,
|
||||
mockLocalMedia,
|
||||
mockLocalParticipant,
|
||||
mockMediaDevices,
|
||||
} from "../utils/test";
|
||||
import { GridTileViewModel } from "../state/TileViewModel";
|
||||
import { ReactionsSenderProvider } from "../reactions/useReactionsSender";
|
||||
@@ -54,7 +60,7 @@ const callVm = {
|
||||
handsRaised$: constant({}),
|
||||
} as Partial<CallViewModel> as CallViewModel;
|
||||
|
||||
test("GridTile is accessible", async () => {
|
||||
test("GridTile displays remote media", async () => {
|
||||
const vm = mockRemoteMedia(
|
||||
mockRtcMembership("@alice:example.org", "AAAA"),
|
||||
{
|
||||
@@ -88,6 +94,40 @@ test("GridTile is accessible", async () => {
|
||||
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 () => {
|
||||
const pickupState$ = new BehaviorSubject<
|
||||
RingingMediaViewModel["pickupState$"]["value"]
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { type animated } from "@react-spring/web";
|
||||
import classNames from "classnames";
|
||||
@@ -103,20 +104,22 @@ interface UserMediaTileProps extends TileProps {
|
||||
playbackMuted: boolean;
|
||||
waitingForMedia?: boolean;
|
||||
primaryButton?: ReactNode;
|
||||
menuStart?: ReactNode;
|
||||
menuEnd?: ReactNode;
|
||||
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,
|
||||
vm,
|
||||
showSpeakingIndicators,
|
||||
playbackMuted,
|
||||
waitingForMedia,
|
||||
primaryButton,
|
||||
menuStart,
|
||||
menuEnd,
|
||||
menu,
|
||||
className,
|
||||
focusUrl,
|
||||
displayName,
|
||||
@@ -156,24 +159,26 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
: t("microphone_off");
|
||||
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
const menu = (
|
||||
<>
|
||||
{menuStart}
|
||||
{/*
|
||||
No additional menu item (used to be the manual fit to frame.
|
||||
Placeholder for future menu items that should be placed here.
|
||||
*/}
|
||||
{menuEnd}
|
||||
</>
|
||||
const menuTrigger = useMemo(
|
||||
() => (
|
||||
<button
|
||||
aria-label={t("common.options")}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<OverflowHorizontalIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
),
|
||||
[t, focusable],
|
||||
);
|
||||
|
||||
const raisedHandOnClick = vm.local
|
||||
? (): void => void toggleRaisedHand()
|
||||
: undefined;
|
||||
const raisedHandOnClick = useMemo(
|
||||
() => (vm.local ? (): void => void toggleRaisedHand() : undefined),
|
||||
[vm.local, toggleRaisedHand],
|
||||
);
|
||||
|
||||
const showSpeaking = showSpeakingIndicators && speaking;
|
||||
|
||||
const tile = (
|
||||
return (
|
||||
<MediaView
|
||||
ref={ref}
|
||||
video={video}
|
||||
@@ -202,14 +207,7 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
open={menuOpen}
|
||||
onOpenChange={setMenuOpen}
|
||||
title={displayName}
|
||||
trigger={
|
||||
<button
|
||||
aria-label={t("common.options")}
|
||||
tabIndex={focusable ? undefined : -1}
|
||||
>
|
||||
<OverflowHorizontalIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
}
|
||||
trigger={menuTrigger}
|
||||
side="left"
|
||||
align="start"
|
||||
>
|
||||
@@ -231,9 +229,37 @@ const UserMediaTile: FC<UserMediaTileProps> = ({
|
||||
{...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 (
|
||||
<ContextMenu title={displayName} trigger={tile} hasAccessibleAlternative>
|
||||
<ContextMenu
|
||||
title={props.displayName}
|
||||
trigger={<UserMediaTileInner {...props} menu={menu} />}
|
||||
hasAccessibleAlternative
|
||||
>
|
||||
{menu}
|
||||
</ContextMenu>
|
||||
);
|
||||
@@ -269,6 +295,29 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
[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 (
|
||||
<UserMediaTile
|
||||
ref={ref}
|
||||
@@ -287,23 +336,8 @@ const LocalUserMediaTile: FC<LocalUserMediaTileProps> = ({
|
||||
</button>
|
||||
)
|
||||
}
|
||||
menuStart={
|
||||
<ToggleMenuItem
|
||||
Icon={VisibilityOnIcon}
|
||||
label={t("video_tile.always_show")}
|
||||
checked={alwaysShow}
|
||||
onSelect={onSelectAlwaysShow}
|
||||
/>
|
||||
}
|
||||
menuEnd={
|
||||
onOpenProfile && (
|
||||
<MenuItem
|
||||
Icon={UserProfileIcon}
|
||||
label={t("common.profile")}
|
||||
onSelect={onOpenProfile}
|
||||
/>
|
||||
)
|
||||
}
|
||||
menuStart={menuStart}
|
||||
menuEnd={menuEnd}
|
||||
focusable={focusable}
|
||||
focusUrl={focusUrl}
|
||||
{...props}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { i18nKey } from "./i18n";
|
||||
|
||||
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",
|
||||
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 {
|
||||
public domain: string;
|
||||
|
||||
@@ -19,6 +19,7 @@ const createRoomWidgetClientSpy = vi.mocked(createRoomWidgetClient);
|
||||
vi.mock("./config/Config", () => ({
|
||||
Config: {
|
||||
init: vi.fn().mockImplementation(async () => Promise.resolve()),
|
||||
get: vi.fn().mockReturnValue({}),
|
||||
},
|
||||
}));
|
||||
const configInitSpy = vi.mocked(Config.init);
|
||||
@@ -109,6 +110,7 @@ describe("widget", () => {
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: false,
|
||||
rtcTransports: true,
|
||||
sendDelayedEvents: true,
|
||||
updateDelayedEvents: true,
|
||||
sendSticky: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { LazyEventEmitter } from "./LazyEventEmitter";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { Config } from "./config/Config";
|
||||
import { seedSettingsFromConfig } from "./settings/settings";
|
||||
import { ElementCallReactionEventType } from "./reactions";
|
||||
|
||||
// Subset of the actions in element-web
|
||||
@@ -173,6 +174,7 @@ export const initializeWidget = (
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: false,
|
||||
rtcTransports: true,
|
||||
sendDelayedEvents: true,
|
||||
updateDelayedEvents: 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
|
||||
// be otherwise)
|
||||
await Config.init();
|
||||
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
|
||||
seedSettingsFromConfig(Config.get().media_quality);
|
||||
await client.startClient();
|
||||
return client;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user