Compare commits

..

6 Commits

Author SHA1 Message Date
lebaudantoine fc6898e5d3 ⬆️(frontend) upgrade livekit-client and @livekit/components-react
Bump livekit-client 2.20.0 -> 2.21.0 and
@livekit/components-react 2.9.21 -> 2.9.23. Patch/minor upgrade
with data streams v2, data channel flow control and reliability
fixes, iOS Safari double-playback fix, and a build-time type
resolution fix for `skipLibCheck: false`.

No breaking API changes.
2026-08-19 14:05:48 +02:00
lebaudantoine 61360d6023 ⬆️(frontend) upgrade posthog-js from 1.395.0 to 1.404.1
Minor version upgrade with bug fixes and internal improvements. No
breaking changes, API deprecations or required actions on our side.

Highlights:

* Better handling of `sendBeacon` quota rejections so events are no
  longer silently dropped.
* Improved error catching for synchronous throws from a
  monkey-patched `window.fetch`.
* Source map improvements for easier debugging.
2026-08-19 13:36:22 +02:00
lebaudantoine ab61c9c967 ⬆️(frontend) upgrade i18next and react-i18next patch versions
Bump i18next from 26.3.1 to 26.3.6 and react-i18next from 17.0.8 to
17.0.10. Both are patch upgrades with bug fixes and no breaking
changes, requiring no code modifications on our side.

i18next:

* 26.3.6: widen the optional TypeScript peer dependency range to
  include v7.
* 26.3.1: fix a regression in type definitions related to
  `keyPrefix` to prevent incorrect type pollution.

react-i18next:

* 17.0.10: improve warning messages for `useTranslation` and
  `Trans` to help debug common issues, especially in monorepos.
  Add a new development-only warning when `useTranslation`
  suspends while loading translations.
* 17.0.9: add TypeScript 7 support and fix related type-checking
  issues with the `<Trans>` component.
2026-08-19 13:32:41 +02:00
lebaudantoine 7c465f2148 ⬆️(backend) bump sqlparse from 0.5.5 to 0.6.0
Update sqlparse to 0.6.0 to pick up fixes for the following high
severity CVEs, all reported against versions prior to 0.6.0:

* CVE-2026-54284: https://avd.aquasec.com/nvd/cve-2026-54284
* CVE-2026-59893: https://avd.aquasec.com/nvd/cve-2026-59893
* CVE-2026-71491: https://avd.aquasec.com/nvd/cve-2026-71491
2026-08-19 13:30:25 +02:00
lebaudantoine d005f202c6 (backend) accept form-urlencoded on the user token endpoint
Accept `application/x-www-form-urlencoded` requests on the user
token endpoint, in addition to the existing JSON support.

This aligns the endpoint with the OAuth 2.0 specification for token
endpoint requests (RFC 6749, sections 3.2 and 4.4.2), so standard
OAuth 2.0 client libraries can call it without any customization,
while keeping backward compatibility with existing JSON clients.
2026-08-19 13:30:25 +02:00
chaitanyaphatak a82023f8b0 📝(docs) fix minor typos in comments and docstrings 2026-08-17 10:30:16 +02:00
21 changed files with 288 additions and 190 deletions
+10 -2
View File
@@ -8,9 +8,17 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Added ### Changed
- 🚸(frontend) explain camera-in-use failures on the join screen - ✨(backend) accept form-urlencoded on the user token endpoint
- ⬆️(frontend) upgrade i18next and react-i18next patch versions
- ⬆️(frontend) upgrade posthog-js from 1.395.0 to 1.404.1
- ⬆️(frontend) upgrade livekit-client and @livekit/components-react
### Fixed
- 📝(docs) fix minor typos in comments and docstrings
- ⬆️(backend) bump sqlparse from 0.5.5 to 0.6.0
## [1.27.0] - 2026-08-14 ## [1.27.0] - 2026-08-14
+1 -1
View File
@@ -4,7 +4,7 @@
Security is very important to us. Security is very important to us.
If you have any issue regarding security, please disclose the information responsibly submiting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at visio@numerique.gouv.fr If you have any issue regarding security, please disclose the information responsibly by submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at visio@numerique.gouv.fr
We appreciate your effort to make Visio more secure. We appreciate your effort to make Visio more secure.
+27
View File
@@ -50,10 +50,24 @@ paths:
The application must be authorized for the user's email domain. The application must be authorized for the user's email domain.
The returned token expires after a configured duration and must be refreshed by calling this endpoint again. The returned token expires after a configured duration and must be refreshed by calling this endpoint again.
Request parameters may be sent either as "application/x-www-form-urlencoded"
(as specified by RFC 6749 for OAuth 2.0 token endpoints) or as "application/json".
operationId: generateToken operationId: generateToken
requestBody: requestBody:
required: true required: true
content: content:
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/TokenRequest'
examples:
tokenRequest:
summary: Request token for user delegation
value:
client_id: "550e8400-e29b-41d4-a716-446655440000"
client_secret: "1234567890abcdefghijklmnopqrstuvwxyz"
grant_type: "client_credentials"
scope: "user@example.com"
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/TokenRequest' $ref: '#/components/schemas/TokenRequest'
@@ -117,6 +131,19 @@ paths:
summary: Domain not authorized summary: Domain not authorized
value: value:
error: "This application is not authorized for this email domain." error: "This application is not authorized for this email domain."
'415':
description: |
Unsupported media type. The request body must be sent as
"application/x-www-form-urlencoded" or "application/json".
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
unsupportedMediaType:
summary: Unsupported request content type
value:
detail: 'Unsupported media type "text/plain" in request.'
/rooms: /rooms:
get: get:
@@ -12,6 +12,9 @@ from rest_framework import decorators, mixins, viewsets
from rest_framework import ( from rest_framework import (
exceptions as drf_exceptions, exceptions as drf_exceptions,
) )
from rest_framework import (
parsers as drf_parsers,
)
from rest_framework import ( from rest_framework import (
response as drf_response, response as drf_response,
) )
@@ -41,6 +44,7 @@ class ApplicationViewSet(viewsets.ViewSet):
methods=["post"], methods=["post"],
url_path="token", url_path="token",
url_name="token", url_name="token",
parser_classes=[drf_parsers.FormParser, drf_parsers.JSONParser],
) )
@FeatureFlag.require("application") @FeatureFlag.require("application")
def generate_jwt_access_token(self, request, *args, **kwargs): def generate_jwt_access_token(self, request, *args, **kwargs):
@@ -5,6 +5,7 @@ Tests for external API /token endpoint
# pylint: disable=W0621 # pylint: disable=W0621
from unittest import mock from unittest import mock
from urllib.parse import urlencode
import jwt import jwt
import pytest import pytest
@@ -88,6 +89,155 @@ def test_api_applications_generate_token_success(settings):
} }
def test_api_applications_generate_token_form_urlencoded(settings):
"""The token endpoint should accept "application/x-www-form-urlencoded"
requests, as mandated by RFC 6749 (sections 3.2 and 4.4.2) for OAuth 2.0
token endpoints, so that standard OAuth 2.0 client libraries work
out of the box."""
UserFactory(email="user@example.com")
application = ApplicationFactory(
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
)
plain_secret = "test-secret-123"
application.client_secret = plain_secret
application.save()
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
(
f"client_id={application.client_id}"
f"&client_secret={plain_secret}"
"&grant_type=client_credentials"
"&scope=user%40example.com"
),
content_type="application/x-www-form-urlencoded",
)
assert response.status_code == 200
assert "access_token" in response.data
response.data.pop("access_token")
assert response.data == {
"token_type": "Bearer",
"expires_in": settings.APPLICATION_JWT_EXPIRATION_SECONDS,
"scope": "rooms:list rooms:create",
}
def test_api_applications_generate_token_form_urlencoded_invalid_credentials():
"""Invalid credentials sent as form-urlencoded should be parsed and
rejected with 401, proving the request body is properly decoded."""
user = UserFactory(email="user@example.com")
application = ApplicationFactory(is_active=True)
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
urlencode(
{
"client_id": application.client_id,
"client_secret": "wrong-secret",
"grant_type": "client_credentials",
"scope": user.email,
}
),
content_type="application/x-www-form-urlencoded",
)
assert response.status_code == 401
assert "Invalid credentials" in str(response.data)
def test_api_applications_generate_token_form_urlencoded_missing_fields():
"""Missing required fields in a form-urlencoded request should return
a 400 validation error, like for JSON requests."""
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
urlencode({"grant_type": "client_credentials"}),
content_type="application/x-www-form-urlencoded",
)
assert response.status_code == 400
for field in ("client_id", "client_secret", "scope"):
assert field in response.data
def test_api_applications_generate_token_form_urlencoded_invalid_grant_type():
"""An unsupported grant_type sent as form-urlencoded should return 400."""
user = UserFactory(email="user@example.com")
application = ApplicationFactory(is_active=True)
plain_secret = "test-secret-123"
application.client_secret = plain_secret
application.save()
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
urlencode(
{
"client_id": application.client_id,
"client_secret": plain_secret,
"grant_type": "authorization_code",
"scope": user.email,
}
),
content_type="application/x-www-form-urlencoded",
)
assert response.status_code == 400
assert "grant_type" in response.data
def test_api_applications_generate_token_form_urlencoded_special_characters():
"""Percent-encoded reserved characters ("&", "=", "+", "%") in the
client_secret should survive form-urlencoded decoding."""
UserFactory(email="user@example.com")
application = ApplicationFactory(
is_active=True,
scopes=[ApplicationScope.ROOMS_LIST],
)
plain_secret = "s3cr3t&with=special+chars%42"
application.client_secret = plain_secret
application.save()
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
urlencode(
{
"client_id": application.client_id,
"client_secret": plain_secret,
"grant_type": "client_credentials",
"scope": "user@example.com",
}
),
content_type="application/x-www-form-urlencoded",
)
assert response.status_code == 200
assert "access_token" in response.data
def test_api_applications_generate_token_unsupported_media_type():
"""Content types other than JSON and form-urlencoded should still be
rejected with 415 Unsupported Media Type."""
client = APIClient()
response = client.post(
"/external-api/v1.0/application/token/",
"client_id=x&client_secret=y&grant_type=client_credentials&scope=a@b.co",
content_type="text/plain",
)
assert response.status_code == 415
def test_api_applications_generate_token_invalid_client_id(): def test_api_applications_generate_token_invalid_client_id():
"""Invalid client_id should return 401.""" """Invalid client_id should return 401."""
user = UserFactory(email="user@example.com") user = UserFactory(email="user@example.com")
+3 -3
View File
@@ -2252,11 +2252,11 @@ wheels = [
[[package]] [[package]]
name = "sqlparse" name = "sqlparse"
version = "0.5.5" version = "0.6.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } sdist = { url = "https://files.pythonhosted.org/packages/5f/d3/3f06a1006f2261d1342aefb3c71eed02f5d4ca5bdbecd86ebc12ad38306e/sqlparse-0.6.0.tar.gz", hash = "sha256:113c35c75365ab9cc9c7231d68c6428fb11c085fc8e9eb1ad659b7ddbf6cd2b9", size = 178477, upload-time = "2026-08-13T19:16:06.396Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, { url = "https://files.pythonhosted.org/packages/d9/50/f00935da0ec7cbf325f8dc4f772ae46fbc7b672dd62876e73f0a94adda57/sqlparse-0.6.0-py3-none-any.whl", hash = "sha256:b861c0288ce2fa56209a9a6412d2e066ac664b3873b89c26c9d8415e8e32996f", size = 50070, upload-time = "2026-08-13T19:16:04.062Z" },
] ]
[[package]] [[package]]
+68 -65
View File
@@ -12,7 +12,7 @@
"@fontsource-variable/lexend": "5.2.11", "@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5", "@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4", "@libreaudio/la-call": "0.1.4",
"@livekit/components-react": "2.9.21", "@livekit/components-react": "2.9.23",
"@livekit/components-styles": "1.2.0", "@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2", "@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.14", "@mediapipe/tasks-vision": "0.10.14",
@@ -24,17 +24,17 @@
"crisp-sdk-web": "1.1.2", "crisp-sdk-web": "1.1.2",
"hoofd": "1.7.3", "hoofd": "1.7.3",
"humanize-duration": "3.33.2", "humanize-duration": "3.33.2",
"i18next": "26.3.1", "i18next": "26.3.6",
"i18next-browser-languagedetector": "8.2.1", "i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0", "i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1", "i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.20.0", "livekit-client": "2.21.0",
"posthog-js": "1.395.0", "posthog-js": "1.404.1",
"react": "18.3.1", "react": "18.3.1",
"react-aria": "3.50.0", "react-aria": "3.50.0",
"react-aria-components": "1.19.0", "react-aria-components": "1.19.0",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-i18next": "17.0.8", "react-i18next": "17.0.10",
"react-stately": "3.48.0", "react-stately": "3.48.0",
"use-sound": "5.0.0", "use-sound": "5.0.0",
"valtio": "2.3.2", "valtio": "2.3.2",
@@ -742,28 +742,28 @@
} }
}, },
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.7.5", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/utils": "^0.2.11" "@floating-ui/utils": "^0.2.12"
} }
}, },
"node_modules/@floating-ui/dom": { "node_modules/@floating-ui/dom": {
"version": "1.7.4", "version": "1.7.6",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@floating-ui/core": "^1.7.3", "@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.10" "@floating-ui/utils": "^0.2.11"
} }
}, },
"node_modules/@floating-ui/utils": { "node_modules/@floating-ui/utils": {
"version": "0.2.11", "version": "0.2.12",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@fontsource-variable/atkinson-hyperlegible-next": { "node_modules/@fontsource-variable/atkinson-hyperlegible-next": {
@@ -966,12 +966,12 @@
"license": "GPL-3.0+" "license": "GPL-3.0+"
}, },
"node_modules/@livekit/components-core": { "node_modules/@livekit/components-core": {
"version": "0.12.13", "version": "0.12.14",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz", "resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.14.tgz",
"integrity": "sha512-DQmi84afHoHjZ62wm8y+XPNIDHTwFHAltjd3lmyXj8UZHOY7wcza4vFt1xnghJOD5wLRY58L1dkAgAw59MgWvw==", "integrity": "sha512-6OKP/1Ok2fCZewDLKd3SzaTb7KvfZl/6hjggI+TgUdhp0Y7HBg3+tHA7YmhZRc0BO8wIyVy4VgTPn7qkLHt2nQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@floating-ui/dom": "1.7.4", "@floating-ui/dom": "1.7.6",
"loglevel": "1.9.1", "loglevel": "1.9.1",
"rxjs": "7.8.2" "rxjs": "7.8.2"
}, },
@@ -979,17 +979,17 @@
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"livekit-client": "^2.17.2", "livekit-client": "^2.20.1",
"tslib": "^2.6.2" "tslib": "^2.6.2"
} }
}, },
"node_modules/@livekit/components-react": { "node_modules/@livekit/components-react": {
"version": "2.9.21", "version": "2.9.23",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.21.tgz", "resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.23.tgz",
"integrity": "sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==", "integrity": "sha512-clO+0g/u3YBpuOvnAjUSAJBH/o7w+RpUAseswjiSML9rHrXlTUhwBNm8LPk+qN5GOU3A6lzsNnFYVpUHUBVOkg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@livekit/components-core": "0.12.13", "@livekit/components-core": "0.12.14",
"clsx": "2.1.1", "clsx": "2.1.1",
"events": "^3.3.0", "events": "^3.3.0",
"jose": "^6.0.12", "jose": "^6.0.12",
@@ -999,8 +999,8 @@
"node": ">=18" "node": ">=18"
}, },
"peerDependencies": { "peerDependencies": {
"@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0", "@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0 || ^0.4.0",
"livekit-client": "^2.18.2", "livekit-client": "^2.20.1",
"react": ">=18", "react": ">=18",
"react-dom": ">=18", "react-dom": ">=18",
"tslib": "^2.6.2" "tslib": "^2.6.2"
@@ -1027,9 +1027,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@livekit/protocol": { "node_modules/@livekit/protocol": {
"version": "1.46.6", "version": "1.50.4",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.46.6.tgz", "resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.50.4.tgz",
"integrity": "sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==", "integrity": "sha512-L1uggNQAqyY21smQY8AllyOYbcv9Me9TaxwuLytL1R8ck9nbYPmQLNwEDi3pOFGAMa5F8I2nUi2Jc59W5awxlA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@bufbuild/protobuf": "^1.10.0" "@bufbuild/protobuf": "^1.10.0"
@@ -1720,18 +1720,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@posthog/core": { "node_modules/@posthog/core": {
"version": "1.39.5", "version": "1.48.3",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.39.5.tgz", "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.48.3.tgz",
"integrity": "sha512-M8Imv7ZmmrT6derMOFXhX3c5VKKO2oG6OX70ozLnNpKJV73sNIhYmRX/raa90saF7OA0YmWWJlpKje3irGAyQQ==", "integrity": "sha512-kwVDVvwtCTXctApA2tpnwDjDDan8LrkwCW1Wv6PABaVs/s5ahbQ9W3pvdXcqr71HCuqukSA1jp7MySczebubGg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@posthog/types": "^1.392.0" "@posthog/types": "^1.405.0"
} }
}, },
"node_modules/@posthog/types": { "node_modules/@posthog/types": {
"version": "1.392.0", "version": "1.405.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.392.0.tgz", "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.405.0.tgz",
"integrity": "sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==", "integrity": "sha512-4rZ/taVXKQxs9Jrf7ZjlCRgrOSL69oKAgIWJQa5kRNJ6wll1UANbrJTSY+Su1e88LIG4zZVjKKKjyQHCkHdHcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@react-aria/overlays": { "node_modules/@react-aria/overlays": {
@@ -4393,11 +4393,14 @@
} }
}, },
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.39.0", "version": "3.50.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", "integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"engines": {
"node": "*"
},
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
@@ -6400,9 +6403,9 @@
} }
}, },
"node_modules/i18next": { "node_modules/i18next": {
"version": "26.3.1", "version": "26.3.6",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==", "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@@ -6419,7 +6422,7 @@
], ],
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"typescript": "^5 || ^6" "typescript": "^5 || ^6 || ^7"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"typescript": { "typescript": {
@@ -8109,20 +8112,20 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/livekit-client": { "node_modules/livekit-client": {
"version": "2.20.0", "version": "2.21.0",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.20.0.tgz", "resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.21.0.tgz",
"integrity": "sha512-RIJcpvBmOmwz3jTj3rmdY6Dzr55HrhcaJjMgY+HSmoEM+yIRyA40m7r8UKv0hnZWM3z/AYhP1q8C8ciz5UWFKQ==", "integrity": "sha512-RBUhPkV/sl1nzl8lokVlK5uATPwn0AlsudCBZXissw/kDl9yz8ac4pNJ43iPpMVoHOeBYH/BZ3vUC1adqa/zFQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@livekit/mutex": "1.1.1", "@livekit/mutex": "1.1.1",
"@livekit/protocol": "1.46.6", "@livekit/protocol": "1.50.4",
"events": "^3.3.0", "events": "^3.3.0",
"jose": "^6.1.0", "jose": "^6.1.0",
"loglevel": "^1.9.2", "loglevel": "^1.9.2",
"sdp-transform": "^2.15.0", "sdp-transform": "^2.15.0",
"tslib": "2.8.1", "tslib": "2.8.1",
"typed-emitter": "^2.1.0", "typed-emitter": "^2.1.0",
"webrtc-adapter": "9.0.5" "webrtc-adapter": "9.0.6"
}, },
"peerDependencies": { "peerDependencies": {
"@types/dom-mediacapture-record": "^1" "@types/dom-mediacapture-record": "^1"
@@ -9120,17 +9123,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/posthog-js": { "node_modules/posthog-js": {
"version": "1.395.0", "version": "1.404.1",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.395.0.tgz", "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.404.1.tgz",
"integrity": "sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==", "integrity": "sha512-ck/HOMKuZ6yefUk5OX+h2s9mUWBsnu0mGDUAWjIwAmQQrloZPShzOhOWuEuZQuMnCKORBFRVGsBAuzsl66cBJA==",
"license": "SEE LICENSE IN LICENSE", "license": "(Apache-2.0 AND MIT)",
"dependencies": { "dependencies": {
"@posthog/core": "^1.38.0", "@posthog/core": "^1.43.1",
"@posthog/types": "^1.391.1", "@posthog/types": "^1.397.0",
"core-js": "^3.38.1", "core-js": "^3.49.0",
"dompurify": "^3.3.2", "dompurify": "^3.3.2",
"fflate": "^0.4.8", "fflate": "^0.4.8",
"preact": "^10.29.2", "preact": "^10.29.3",
"query-selector-shadow-dom": "^1.0.1", "query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^5.3.0" "web-vitals": "^5.3.0"
} }
@@ -9422,9 +9425,9 @@
} }
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "17.0.8", "version": "17.0.10",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz",
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==", "integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.29.2", "@babel/runtime": "^7.29.2",
@@ -9434,7 +9437,7 @@
"peerDependencies": { "peerDependencies": {
"i18next": ">= 26.2.0", "i18next": ">= 26.2.0",
"react": ">= 16.8.0", "react": ">= 16.8.0",
"typescript": "^5 || ^6" "typescript": "^5 || ^6 || ^7"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"react-dom": { "react-dom": {
@@ -11645,9 +11648,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/webrtc-adapter": { "node_modules/webrtc-adapter": {
"version": "9.0.5", "version": "9.0.6",
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz", "resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.6.tgz",
"integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==", "integrity": "sha512-CHbl2ZQbxx164IgWRgzJno4hWtM4tFbRam1QfI3Yxhs3w/DvqluVxVWeXs3oL5/fbGkSNLKo0Ty5MgUWceNhog==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"sdp": "^3.2.0" "sdp": "^3.2.0"
+5 -5
View File
@@ -19,7 +19,7 @@
"@fontsource-variable/lexend": "5.2.11", "@fontsource-variable/lexend": "5.2.11",
"@fontsource/opendyslexic": "5.2.5", "@fontsource/opendyslexic": "5.2.5",
"@libreaudio/la-call": "0.1.4", "@libreaudio/la-call": "0.1.4",
"@livekit/components-react": "2.9.21", "@livekit/components-react": "2.9.23",
"@livekit/components-styles": "1.2.0", "@livekit/components-styles": "1.2.0",
"@livekit/track-processors": "0.7.2", "@livekit/track-processors": "0.7.2",
"@mediapipe/tasks-vision": "0.10.14", "@mediapipe/tasks-vision": "0.10.14",
@@ -31,17 +31,17 @@
"crisp-sdk-web": "1.1.2", "crisp-sdk-web": "1.1.2",
"hoofd": "1.7.3", "hoofd": "1.7.3",
"humanize-duration": "3.33.2", "humanize-duration": "3.33.2",
"i18next": "26.3.1", "i18next": "26.3.6",
"i18next-browser-languagedetector": "8.2.1", "i18next-browser-languagedetector": "8.2.1",
"i18next-parser": "9.4.0", "i18next-parser": "9.4.0",
"i18next-resources-to-backend": "1.2.1", "i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.20.0", "livekit-client": "2.21.0",
"posthog-js": "1.395.0", "posthog-js": "1.404.1",
"react": "18.3.1", "react": "18.3.1",
"react-aria": "3.50.0", "react-aria": "3.50.0",
"react-aria-components": "1.19.0", "react-aria-components": "1.19.0",
"react-dom": "18.3.1", "react-dom": "18.3.1",
"react-i18next": "17.0.8", "react-i18next": "17.0.10",
"react-stately": "3.48.0", "react-stately": "3.48.0",
"use-sound": "5.0.0", "use-sound": "5.0.0",
"valtio": "2.3.2", "valtio": "2.3.2",
@@ -137,7 +137,6 @@ export const captureMediaEvent = async (
| 'media-device-topology' | 'media-device-topology'
| 'media-device-success' | 'media-device-success'
| 'device-not-found' | 'device-not-found'
| 'device-in-use'
| 'permissions-denied' | 'permissions-denied'
| 'screen-share-permission-denied' | 'screen-share-permission-denied'
| 'silent-mic-detected' | 'silent-mic-detected'
@@ -23,7 +23,7 @@ export const ChatProvider = () => {
resetChatStore() resetChatStore()
}, []) }, [])
// Tigger the message notification (temporary) // Trigger the message notification (temporary)
useEffect(() => { useEffect(() => {
// TEMPORARY: This is a brittle workaround that relies on message count tracking // TEMPORARY: This is a brittle workaround that relies on message count tracking
// due to recent LiveKit useChat changes breaking the previous implementation // due to recent LiveKit useChat changes breaking the previous implementation
@@ -28,7 +28,6 @@ import {
userChoicesStore, userChoicesStore,
} from '@/stores/userChoices' } from '@/stores/userChoices'
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice' import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
import { useDeviceInUse } from '../livekit/hooks/useDeviceInUse'
import { useDeviceMissing } from '../livekit/hooks/useDeviceMissing' import { useDeviceMissing } from '../livekit/hooks/useDeviceMissing'
import { useJoinTracks } from '../livekit/hooks/useJoinTracks' import { useJoinTracks } from '../livekit/hooks/useJoinTracks'
import { SilentMicDetector } from './SilentMicDetector' import { SilentMicDetector } from './SilentMicDetector'
@@ -219,14 +218,12 @@ const switchTrackDevice =
function getPreviewMessages({ function getPreviewMessages({
cameraFound, cameraFound,
cameraDenied, cameraDenied,
cameraInUse,
micDenied, micDenied,
videoEnabled, videoEnabled,
videoStarted, videoStarted,
}: { }: {
cameraFound: boolean cameraFound: boolean
cameraDenied: boolean cameraDenied: boolean
cameraInUse: boolean
micDenied: boolean micDenied: boolean
videoEnabled: boolean videoEnabled: boolean
videoStarted: boolean videoStarted: boolean
@@ -238,9 +235,6 @@ function getPreviewMessages({
const key = micDenied ? 'cameraAndMicNotGranted' : 'cameraNotGranted' const key = micDenied ? 'cameraAndMicNotGranted' : 'cameraNotGranted'
return { hint: key, permissionsButtonLabel: key } return { hint: key, permissionsButtonLabel: key }
} }
if (cameraInUse) {
return { hint: 'cameraInUse', permissionsButtonLabel: null }
}
if (!videoEnabled) { if (!videoEnabled) {
return { hint: 'cameraDisabled', permissionsButtonLabel: null } return { hint: 'cameraDisabled', permissionsButtonLabel: null }
} }
@@ -334,20 +328,18 @@ const VideoPreview = ({
const cameraDenied = useCannotUseDevice('videoinput') const cameraDenied = useCannotUseDevice('videoinput')
const micDenied = useCannotUseDevice('audioinput') const micDenied = useCannotUseDevice('audioinput')
const cameraMissing = useDeviceMissing('videoinput') const cameraMissing = useDeviceMissing('videoinput')
const cameraInUse = useDeviceInUse('videoinput')
const { videoEl, videoStarted } = useAttachedVideo(videoTrack, videoEnabled) const { videoEl, videoStarted } = useAttachedVideo(videoTrack, videoEnabled)
const { hint, permissionsButtonLabel } = getPreviewMessages({ const { hint, permissionsButtonLabel } = getPreviewMessages({
cameraFound: !cameraMissing, cameraFound: !cameraMissing,
cameraDenied, cameraDenied,
cameraInUse,
micDenied, micDenied,
videoEnabled, videoEnabled,
videoStarted, videoStarted,
}) })
const isError = cameraMissing || cameraDenied || cameraInUse const isError = cameraMissing || cameraDenied
return ( return (
<div className={styles.previewFrame}> <div className={styles.previewFrame}>
@@ -44,7 +44,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
videoElement?: HTMLVideoElement videoElement?: HTMLVideoElement
videoElementLoaded?: boolean videoElementLoaded?: boolean
// Canvas containg the video processing result, of which we extract as stream. // Canvas containing the video processing result, of which we extract as stream.
outputCanvas?: HTMLCanvasElement outputCanvas?: HTMLCanvasElement
outputCanvasCtx?: CanvasRenderingContext2D outputCanvasCtx?: CanvasRenderingContext2D
@@ -55,7 +55,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
segmentationMaskCanvas?: HTMLCanvasElement segmentationMaskCanvas?: HTMLCanvasElement
segmentationMaskCanvasCtx?: CanvasRenderingContext2D segmentationMaskCanvasCtx?: CanvasRenderingContext2D
// Mask containg the inference result. // Mask containing the inference result.
segmentationMask?: ImageData segmentationMask?: ImageData
// The resized image of the video source. // The resized image of the video source.
@@ -20,7 +20,6 @@ import { openPermissionsDialog } from '@/stores/permissions'
import { openSilentMicDialog, silentMicStore } from '@/stores/silentMic' import { openSilentMicDialog, silentMicStore } from '@/stores/silentMic'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice' import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
import { useDeviceInUse } from '../../../hooks/useDeviceInUse'
import { useDeviceMissing } from '../../../hooks/useDeviceMissing' import { useDeviceMissing } from '../../../hooks/useDeviceMissing'
import { requestDevicePermission } from '../../../hooks/useJoinTracks' import { requestDevicePermission } from '../../../hooks/useJoinTracks'
import { useDeviceIcons } from '../../../hooks/useDeviceIcons' import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
@@ -98,23 +97,21 @@ export const ToggleDevice = <T extends ToggleSource>({
const deviceIcons = useDeviceIcons(kind) const deviceIcons = useDeviceIcons(kind)
const cannotUseDevice = useCannotUseDevice(kind) const cannotUseDevice = useCannotUseDevice(kind)
const deviceMissing = useDeviceMissing(kind) const deviceMissing = useDeviceMissing(kind)
const deviceInUse = useDeviceInUse(kind)
const { status: silentMicStatus } = useSnapshot(silentMicStore) const { status: silentMicStatus } = useSnapshot(silentMicStore)
const silentMicWarning = const silentMicWarning =
kind === 'audioinput' && kind === 'audioinput' &&
silentMicStatus === 'silent' && silentMicStatus === 'silent' &&
!cannotUseDevice && !cannotUseDevice &&
!deviceMissing && !deviceMissing
!deviceInUse
const deviceShortcut = useDeviceShortcut(kind) const deviceShortcut = useDeviceShortcut(kind)
const announce = useScreenReaderAnnounce() const announce = useScreenReaderAnnounce()
const isRequestingPermission = useRef(false) const isRequestingPermission = useRef(false)
const [alertError, setAlertError] = useState<MediaDeviceFailure | null>(null) const [showDeviceNotFound, setShowDeviceNotFound] = useState(false)
const onPress = async () => { const onPress = async () => {
if (!enabled && deviceMissing) { if (!enabled && deviceMissing) {
setAlertError(MediaDeviceFailure.NotFound) setShowDeviceNotFound(true)
return return
} }
if (!cannotUseDevice) { if (!cannotUseDevice) {
@@ -188,18 +185,10 @@ export const ToggleDevice = <T extends ToggleSource>({
<PermissionNeededButton <PermissionNeededButton
tooltip={deviceMissing ? t(`deviceNotFound.${kind}`) : undefined} tooltip={deviceMissing ? t(`deviceNotFound.${kind}`) : undefined}
onPress={ onPress={
deviceMissing deviceMissing ? () => setShowDeviceNotFound(true) : undefined
? () => setAlertError(MediaDeviceFailure.NotFound)
: undefined
} }
/> />
)} )}
{deviceInUse && (
<PermissionNeededButton
tooltip={t(`deviceInUse.${kind}`)}
onPress={() => setAlertError(MediaDeviceFailure.DeviceInUse)}
/>
)}
{silentMicWarning && ( {silentMicWarning && (
<PermissionNeededButton <PermissionNeededButton
tooltip={t('tooltip', { keyPrefix: 'silentMic' })} tooltip={t('tooltip', { keyPrefix: 'silentMic' })}
@@ -218,11 +207,9 @@ export const ToggleDevice = <T extends ToggleSource>({
tooltip={ tooltip={
deviceMissing deviceMissing
? t(`deviceNotFound.${kind}`) ? t(`deviceNotFound.${kind}`)
: deviceInUse : cannotUseDevice
? t(`deviceInUse.${kind}`) ? t('tooltip', { keyPrefix: 'permissionsButton' })
: cannotUseDevice : toggleLabel
? t('tooltip', { keyPrefix: 'permissionsButton' })
: toggleLabel
} }
{...computedToggleButtonProps} {...computedToggleButtonProps}
{...overrideToggleButtonProps} {...overrideToggleButtonProps}
@@ -230,9 +217,9 @@ export const ToggleDevice = <T extends ToggleSource>({
<Icon /> <Icon />
</ToggleButton> </ToggleButton>
<MediaDeviceErrorAlert <MediaDeviceErrorAlert
error={alertError} error={showDeviceNotFound ? MediaDeviceFailure.NotFound : null}
kind={kind} kind={kind}
onClose={() => setAlertError(null)} onClose={() => setShowDeviceNotFound(false)}
/> />
</div> </div>
) )
@@ -1,15 +0,0 @@
import { useSnapshot } from 'valtio'
import { deviceInUseStore } from '@/stores/deviceInUse'
import { PERMISSION_BY_DEVICE_KIND } from '@/stores/permissions'
import { useCannotUseDevice } from './useCannotUseDevice'
import { useDeviceMissing } from './useDeviceMissing'
export const useDeviceInUse = (kind: MediaDeviceKind): boolean => {
const inUse = useSnapshot(deviceInUseStore)
const cannotUseDevice = useCannotUseDevice(kind)
const deviceMissing = useDeviceMissing(kind)
const permissionKind = PERMISSION_BY_DEVICE_KIND[kind]
if (!permissionKind || cannotUseDevice || deviceMissing) return false
return inUse[permissionKind]
}
@@ -18,7 +18,6 @@ import {
noteSystemPermissionDenied, noteSystemPermissionDenied,
type PermissionKind, type PermissionKind,
} from '@/stores/permissions' } from '@/stores/permissions'
import { clearDeviceInUse, noteDeviceInUse } from '@/stores/deviceInUse'
import { getOS } from '@/utils/os' import { getOS } from '@/utils/os'
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry' import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
import { import {
@@ -90,15 +89,7 @@ const onMediaPermissionError = (
return return
} }
if ( // "Other" and "Device in use" are still reported as errors, as they are not handled on the join screen.
MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.DeviceInUse &&
path === 'join_preview'
) {
noteDeviceInUse(kind)
void captureMediaEvent('device-in-use', { path, kind, os: getOS() })
return
}
reportError( reportError(
path === 'room' ? 'room_media_failure' : 'join_preview_failure', path === 'room' ? 'room_media_failure' : 'join_preview_failure',
e, e,
@@ -106,11 +97,6 @@ const onMediaPermissionError = (
) )
} }
const noteDeviceReady = (kind?: PermissionKind) => {
noteGumSuccess(kind)
clearDeviceInUse(kind)
}
// Module-level: effect dependencies, must be referentially stable. // Module-level: effect dependencies, must be referentially stable.
const disableAudio = () => saveAudioInputEnabled(false) const disableAudio = () => saveAudioInputEnabled(false)
const disableVideo = () => saveVideoInputEnabled(false) const disableVideo = () => saveVideoInputEnabled(false)
@@ -128,7 +114,7 @@ export const requestDevicePermission = async (
? await createLocalAudioTrack() ? await createLocalAudioTrack()
: await createLocalVideoTrack() : await createLocalVideoTrack()
track.stop() track.stop()
noteDeviceReady(PERMISSION_KIND[kind]) noteGumSuccess(PERMISSION_KIND[kind])
return true return true
} catch (error) { } catch (error) {
onMediaPermissionError(error as Error, PERMISSION_KIND[kind], path) onMediaPermissionError(error as Error, PERMISSION_KIND[kind], path)
@@ -172,7 +158,7 @@ function useWarmupPermissions(): WarmupState {
video: true, video: true,
}) })
) )
noteDeviceReady() noteGumSuccess()
bothReady() bothReady()
} catch (error) { } catch (error) {
if ( if (
@@ -194,7 +180,7 @@ function useWarmupPermissions(): WarmupState {
.getUserMedia({ audio: true }) .getUserMedia({ audio: true })
.then((stream) => { .then((stream) => {
stopAll(stream) stopAll(stream)
noteDeviceReady('microphone') noteGumSuccess('microphone')
}) })
.catch((e) => onMediaPermissionError(e as Error, 'microphone')) .catch((e) => onMediaPermissionError(e as Error, 'microphone'))
.finally(() => .finally(() =>
@@ -204,7 +190,7 @@ function useWarmupPermissions(): WarmupState {
.getUserMedia({ video: true }) .getUserMedia({ video: true })
.then((stream) => { .then((stream) => {
stopAll(stream) stopAll(stream)
noteDeviceReady('camera') noteGumSuccess('camera')
}) })
.catch((e) => onMediaPermissionError(e as Error, 'camera')) .catch((e) => onMediaPermissionError(e as Error, 'camera'))
.finally(() => .finally(() =>
@@ -241,7 +227,7 @@ function useLocalTrack<T extends LocalAudioTrack | LocalVideoTrack>({
let cancelled = false let cancelled = false
create() create()
.then((newTrack) => { .then((newTrack) => {
noteDeviceReady(permissionKind) noteGumSuccess(permissionKind)
if (cancelled) { if (cancelled) {
newTrack.stop() newTrack.stop()
return return
@@ -305,8 +291,6 @@ export function useJoinTracks(): {
const { audioReady, videoReady } = useWarmupPermissions() const { audioReady, videoReady } = useWarmupPermissions()
useEffect(() => () => clearDeviceInUse(), [])
const createAudio = useCallback( const createAudio = useCallback(
() => () =>
createLocalAudioTrack({ createLocalAudioTrack({
-5
View File
@@ -16,10 +16,6 @@
"videoinput": "Keine Kamera erkannt. Prüfe, ob sie richtig angeschlossen ist.", "videoinput": "Keine Kamera erkannt. Prüfe, ob sie richtig angeschlossen ist.",
"audioinput": "Kein Mikrofon erkannt. Prüfe, ob es richtig angeschlossen ist." "audioinput": "Kein Mikrofon erkannt. Prüfe, ob es richtig angeschlossen ist."
}, },
"deviceInUse": {
"videoinput": "Kamera nicht verfügbar: Sie wird wahrscheinlich von einer anderen App oder einem anderen Tab verwendet.",
"audioinput": "Mikrofon nicht verfügbar: Es wird wahrscheinlich von einer anderen App oder einem anderen Tab verwendet."
},
"settings": { "settings": {
"audio": "Audioeinstellungen", "audio": "Audioeinstellungen",
"video": "Videoeinstellungen" "video": "Videoeinstellungen"
@@ -71,7 +67,6 @@
}, },
"cameraDisabled": "Kamera ist deaktiviert.", "cameraDisabled": "Kamera ist deaktiviert.",
"cameraNotFound": "Keine Kamera erkannt. Prüfe, ob sie richtig angeschlossen ist.", "cameraNotFound": "Keine Kamera erkannt. Prüfe, ob sie richtig angeschlossen ist.",
"cameraInUse": "Deine Kamera ist nicht verfügbar. Sie wird wahrscheinlich von einer anderen App oder einem anderen Tab verwendet.",
"cameraStarting": "Kamera wird gestartet…", "cameraStarting": "Kamera wird gestartet…",
"cameraNotGranted": "Möchtest du, dass andere dich während des Meetings sehen können?", "cameraNotGranted": "Möchtest du, dass andere dich während des Meetings sehen können?",
"cameraAndMicNotGranted": "Möchtest du, dass andere dich während des Meetings sehen und hören können?", "cameraAndMicNotGranted": "Möchtest du, dass andere dich während des Meetings sehen und hören können?",
-5
View File
@@ -16,10 +16,6 @@
"videoinput": "No camera detected. Check that it is properly wired.", "videoinput": "No camera detected. Check that it is properly wired.",
"audioinput": "No microphone detected. Check that it is properly wired." "audioinput": "No microphone detected. Check that it is properly wired."
}, },
"deviceInUse": {
"videoinput": "Camera unavailable: it is probably in use by another application or browser tab.",
"audioinput": "Microphone unavailable: it is probably in use by another application or browser tab."
},
"settings": { "settings": {
"audio": "Audio settings", "audio": "Audio settings",
"video": "Video settings" "video": "Video settings"
@@ -71,7 +67,6 @@
}, },
"cameraDisabled": "Camera is disabled.", "cameraDisabled": "Camera is disabled.",
"cameraNotFound": "No camera detected. Check that it is properly plugged in.", "cameraNotFound": "No camera detected. Check that it is properly plugged in.",
"cameraInUse": "Your camera is unavailable. It is probably being used by another application or browser tab.",
"cameraStarting": "Camera is starting…", "cameraStarting": "Camera is starting…",
"cameraNotGranted": "Would you like others to be able to see you during the meeting?", "cameraNotGranted": "Would you like others to be able to see you during the meeting?",
"cameraAndMicNotGranted": "Would you like others to be able to see and hear you during the meeting?", "cameraAndMicNotGranted": "Would you like others to be able to see and hear you during the meeting?",
-5
View File
@@ -16,10 +16,6 @@
"videoinput": "Aucune caméra détectée. Vérifiez qu'elle est bien branchée.", "videoinput": "Aucune caméra détectée. Vérifiez qu'elle est bien branchée.",
"audioinput": "Aucun microphone détecté. Vérifiez qu'il est bien branché." "audioinput": "Aucun microphone détecté. Vérifiez qu'il est bien branché."
}, },
"deviceInUse": {
"videoinput": "Caméra indisponible : elle est probablement utilisée par une autre application ou un autre onglet.",
"audioinput": "Microphone indisponible : il est probablement utilisé par une autre application ou un autre onglet."
},
"settings": { "settings": {
"audio": "Paramètres audio", "audio": "Paramètres audio",
"video": "Paramètres video" "video": "Paramètres video"
@@ -71,7 +67,6 @@
}, },
"cameraDisabled": "La caméra est désactivée.", "cameraDisabled": "La caméra est désactivée.",
"cameraNotFound": "Aucune caméra détectée. Vérifiez qu'elle est bien branchée.", "cameraNotFound": "Aucune caméra détectée. Vérifiez qu'elle est bien branchée.",
"cameraInUse": "Votre caméra n'est pas disponible. Elle est probablement utilisée par une autre application ou un autre onglet.",
"cameraStarting": "La caméra va démarrer…", "cameraStarting": "La caméra va démarrer…",
"cameraNotGranted": "Souhaitez-vous que les autres puissent vous voir pendant la réunion ?", "cameraNotGranted": "Souhaitez-vous que les autres puissent vous voir pendant la réunion ?",
"cameraAndMicNotGranted": "Souhaitez-vous que les autres puissent vous voir et vous entendre pendant la réunion ?", "cameraAndMicNotGranted": "Souhaitez-vous que les autres puissent vous voir et vous entendre pendant la réunion ?",
-5
View File
@@ -16,10 +16,6 @@
"videoinput": "Geen camera gedetecteerd. Controleer of deze goed is aangesloten.", "videoinput": "Geen camera gedetecteerd. Controleer of deze goed is aangesloten.",
"audioinput": "Geen microfoon gedetecteerd. Controleer of deze goed is aangesloten." "audioinput": "Geen microfoon gedetecteerd. Controleer of deze goed is aangesloten."
}, },
"deviceInUse": {
"videoinput": "Camera niet beschikbaar: deze wordt waarschijnlijk gebruikt door een andere toepassing of een ander tabblad.",
"audioinput": "Microfoon niet beschikbaar: deze wordt waarschijnlijk gebruikt door een andere toepassing of een ander tabblad."
},
"settings": { "settings": {
"audio": "Audio-instellingen", "audio": "Audio-instellingen",
"video": "Video-instellingen" "video": "Video-instellingen"
@@ -71,7 +67,6 @@
}, },
"cameraDisabled": "Camera is uitgeschakeld.", "cameraDisabled": "Camera is uitgeschakeld.",
"cameraNotFound": "Geen camera gedetecteerd. Controleer of deze goed is aangesloten.", "cameraNotFound": "Geen camera gedetecteerd. Controleer of deze goed is aangesloten.",
"cameraInUse": "Je camera is niet beschikbaar. Deze wordt waarschijnlijk gebruikt door een andere toepassing of een ander tabblad.",
"cameraStarting": "Camera wordt ingeschakeld…", "cameraStarting": "Camera wordt ingeschakeld…",
"cameraNotGranted": "Wilt u dat anderen u tijdens de vergadering kunnen zien?", "cameraNotGranted": "Wilt u dat anderen u tijdens de vergadering kunnen zien?",
"cameraAndMicNotGranted": "Wilt u dat anderen u tijdens de vergadering kunnen zien en horen?", "cameraAndMicNotGranted": "Wilt u dat anderen u tijdens de vergadering kunnen zien en horen?",
-21
View File
@@ -1,21 +0,0 @@
import { proxy } from 'valtio'
import type { PermissionKind } from './permissions'
export const deviceInUseStore = proxy<Record<PermissionKind, boolean>>({
camera: false,
microphone: false,
})
const ALL_KINDS: PermissionKind[] = ['camera', 'microphone']
export const noteDeviceInUse = (kind?: PermissionKind) => {
for (const k of kind ? [kind] : ALL_KINDS) {
deviceInUseStore[k] = true
}
}
export const clearDeviceInUse = (kind?: PermissionKind) => {
for (const k of kind ? [kind] : ALL_KINDS) {
deviceInUseStore[k] = false
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ class RecordingMetadata(BaseModel):
cloud_storage_url: Url = Field( cloud_storage_url: Url = Field(
title="Cloud Storage URL", title="Cloud Storage URL",
description="The URL of the metadata file for speaker assignement.", description="The URL of the metadata file for speaker assignment.",
) )
started_at: AwareDatetime = Field(title="Start time of the recording to transcribe") started_at: AwareDatetime = Field(title="Start time of the recording to transcribe")
ended_at: AwareDatetime = Field(title="End time of the recording to transcribe") ended_at: AwareDatetime = Field(title="End time of the recording to transcribe")