mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-19 23:07:29 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc6898e5d3 | |||
| 61360d6023 | |||
| ab61c9c967 | |||
| 7c465f2148 | |||
| d005f202c6 | |||
| a82023f8b0 |
+10
-2
@@ -8,9 +8,17 @@ and this project adheres to
|
||||
|
||||
## [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
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -50,10 +50,24 @@ paths:
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
requestBody:
|
||||
required: true
|
||||
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:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRequest'
|
||||
@@ -117,6 +131,19 @@ paths:
|
||||
summary: Domain not authorized
|
||||
value:
|
||||
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:
|
||||
get:
|
||||
|
||||
@@ -12,6 +12,9 @@ from rest_framework import decorators, mixins, viewsets
|
||||
from rest_framework import (
|
||||
exceptions as drf_exceptions,
|
||||
)
|
||||
from rest_framework import (
|
||||
parsers as drf_parsers,
|
||||
)
|
||||
from rest_framework import (
|
||||
response as drf_response,
|
||||
)
|
||||
@@ -41,6 +44,7 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
methods=["post"],
|
||||
url_path="token",
|
||||
url_name="token",
|
||||
parser_classes=[drf_parsers.FormParser, drf_parsers.JSONParser],
|
||||
)
|
||||
@FeatureFlag.require("application")
|
||||
def generate_jwt_access_token(self, request, *args, **kwargs):
|
||||
|
||||
@@ -5,6 +5,7 @@ Tests for external API /token endpoint
|
||||
# pylint: disable=W0621
|
||||
|
||||
from unittest import mock
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import jwt
|
||||
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():
|
||||
"""Invalid client_id should return 401."""
|
||||
user = UserFactory(email="user@example.com")
|
||||
|
||||
Generated
+3
-3
@@ -2252,11 +2252,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.5.5"
|
||||
version = "0.6.0"
|
||||
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 = [
|
||||
{ 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]]
|
||||
|
||||
Generated
+68
-65
@@ -12,7 +12,7 @@
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@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/track-processors": "0.7.2",
|
||||
"@mediapipe/tasks-vision": "0.10.14",
|
||||
@@ -24,17 +24,17 @@
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "26.3.1",
|
||||
"i18next": "26.3.6",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.20.0",
|
||||
"posthog-js": "1.395.0",
|
||||
"livekit-client": "2.21.0",
|
||||
"posthog-js": "1.404.1",
|
||||
"react": "18.3.1",
|
||||
"react-aria": "3.50.0",
|
||||
"react-aria-components": "1.19.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "17.0.8",
|
||||
"react-i18next": "17.0.10",
|
||||
"react-stately": "3.48.0",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
@@ -742,28 +742,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/core": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
"@floating-ui/utils": "^0.2.12"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/dom": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
|
||||
"integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
|
||||
"version": "1.7.6",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/core": "^1.7.3",
|
||||
"@floating-ui/utils": "^0.2.10"
|
||||
"@floating-ui/core": "^1.7.5",
|
||||
"@floating-ui/utils": "^0.2.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@floating-ui/utils": {
|
||||
"version": "0.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fontsource-variable/atkinson-hyperlegible-next": {
|
||||
@@ -966,12 +966,12 @@
|
||||
"license": "GPL-3.0+"
|
||||
},
|
||||
"node_modules/@livekit/components-core": {
|
||||
"version": "0.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
|
||||
"integrity": "sha512-DQmi84afHoHjZ62wm8y+XPNIDHTwFHAltjd3lmyXj8UZHOY7wcza4vFt1xnghJOD5wLRY58L1dkAgAw59MgWvw==",
|
||||
"version": "0.12.14",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.14.tgz",
|
||||
"integrity": "sha512-6OKP/1Ok2fCZewDLKd3SzaTb7KvfZl/6hjggI+TgUdhp0Y7HBg3+tHA7YmhZRc0BO8wIyVy4VgTPn7qkLHt2nQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.7.4",
|
||||
"@floating-ui/dom": "1.7.6",
|
||||
"loglevel": "1.9.1",
|
||||
"rxjs": "7.8.2"
|
||||
},
|
||||
@@ -979,17 +979,17 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"livekit-client": "^2.17.2",
|
||||
"livekit-client": "^2.20.1",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@livekit/components-react": {
|
||||
"version": "2.9.21",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.21.tgz",
|
||||
"integrity": "sha512-6hU9VucJJL+gAhilNGe4MBCDCZVk64qyjP9Ck86krvOIdVU76WeWksddg1MYUP10AlUwwrfD7davz41pJTcMJw==",
|
||||
"version": "2.9.23",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.23.tgz",
|
||||
"integrity": "sha512-clO+0g/u3YBpuOvnAjUSAJBH/o7w+RpUAseswjiSML9rHrXlTUhwBNm8LPk+qN5GOU3A6lzsNnFYVpUHUBVOkg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@livekit/components-core": "0.12.13",
|
||||
"@livekit/components-core": "0.12.14",
|
||||
"clsx": "2.1.1",
|
||||
"events": "^3.3.0",
|
||||
"jose": "^6.0.12",
|
||||
@@ -999,8 +999,8 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0",
|
||||
"livekit-client": "^2.18.2",
|
||||
"@livekit/krisp-noise-filter": "^0.2.12 || ^0.3.0 || ^0.4.0",
|
||||
"livekit-client": "^2.20.1",
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18",
|
||||
"tslib": "^2.6.2"
|
||||
@@ -1027,9 +1027,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@livekit/protocol": {
|
||||
"version": "1.46.6",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.46.6.tgz",
|
||||
"integrity": "sha512-upzlHP1vi/kZ/QqALZTFskQ0ifqc2f15RKucHYOsIHJsaXvEYanG75mAb7o+Yomfs4XhQ4BaRsdY+TFHXpaqrg==",
|
||||
"version": "1.50.4",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.50.4.tgz",
|
||||
"integrity": "sha512-L1uggNQAqyY21smQY8AllyOYbcv9Me9TaxwuLytL1R8ck9nbYPmQLNwEDi3pOFGAMa5F8I2nUi2Jc59W5awxlA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.10.0"
|
||||
@@ -1720,18 +1720,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.39.5",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.39.5.tgz",
|
||||
"integrity": "sha512-M8Imv7ZmmrT6derMOFXhX3c5VKKO2oG6OX70ozLnNpKJV73sNIhYmRX/raa90saF7OA0YmWWJlpKje3irGAyQQ==",
|
||||
"version": "1.48.3",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.48.3.tgz",
|
||||
"integrity": "sha512-kwVDVvwtCTXctApA2tpnwDjDDan8LrkwCW1Wv6PABaVs/s5ahbQ9W3pvdXcqr71HCuqukSA1jp7MySczebubGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/types": "^1.392.0"
|
||||
"@posthog/types": "^1.405.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/types": {
|
||||
"version": "1.392.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.392.0.tgz",
|
||||
"integrity": "sha512-nctNujXL3FC1v99FktaTMSugSD9ZOZekEpahUSafkU2TSvW+XGKNkQZbokuJtiWvPBK208dwMJva8UfBkChqpw==",
|
||||
"version": "1.405.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.405.0.tgz",
|
||||
"integrity": "sha512-4rZ/taVXKQxs9Jrf7ZjlCRgrOSL69oKAgIWJQa5kRNJ6wll1UANbrJTSY+Su1e88LIG4zZVjKKKjyQHCkHdHcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@react-aria/overlays": {
|
||||
@@ -4393,11 +4393,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.39.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz",
|
||||
"integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==",
|
||||
"version": "3.50.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.50.0.tgz",
|
||||
"integrity": "sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
@@ -6400,9 +6403,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
|
||||
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
|
||||
"version": "26.3.6",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
|
||||
"integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -6419,7 +6422,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6"
|
||||
"typescript": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
@@ -8109,20 +8112,20 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/livekit-client": {
|
||||
"version": "2.20.0",
|
||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.20.0.tgz",
|
||||
"integrity": "sha512-RIJcpvBmOmwz3jTj3rmdY6Dzr55HrhcaJjMgY+HSmoEM+yIRyA40m7r8UKv0hnZWM3z/AYhP1q8C8ciz5UWFKQ==",
|
||||
"version": "2.21.0",
|
||||
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.21.0.tgz",
|
||||
"integrity": "sha512-RBUhPkV/sl1nzl8lokVlK5uATPwn0AlsudCBZXissw/kDl9yz8ac4pNJ43iPpMVoHOeBYH/BZ3vUC1adqa/zFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@livekit/mutex": "1.1.1",
|
||||
"@livekit/protocol": "1.46.6",
|
||||
"@livekit/protocol": "1.50.4",
|
||||
"events": "^3.3.0",
|
||||
"jose": "^6.1.0",
|
||||
"loglevel": "^1.9.2",
|
||||
"sdp-transform": "^2.15.0",
|
||||
"tslib": "2.8.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"webrtc-adapter": "9.0.5"
|
||||
"webrtc-adapter": "9.0.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/dom-mediacapture-record": "^1"
|
||||
@@ -9120,17 +9123,17 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.395.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.395.0.tgz",
|
||||
"integrity": "sha512-5iTb00CGt2eQUUiBQysQiX89RAbCN6wK2sDNzvs9zv0alaY8mJ0ZySrUD3LQ+XyLhgM5pCpacBuUwChqiYDLDw==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"version": "1.404.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.404.1.tgz",
|
||||
"integrity": "sha512-ck/HOMKuZ6yefUk5OX+h2s9mUWBsnu0mGDUAWjIwAmQQrloZPShzOhOWuEuZQuMnCKORBFRVGsBAuzsl66cBJA==",
|
||||
"license": "(Apache-2.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@posthog/core": "^1.38.0",
|
||||
"@posthog/types": "^1.391.1",
|
||||
"core-js": "^3.38.1",
|
||||
"@posthog/core": "^1.43.1",
|
||||
"@posthog/types": "^1.397.0",
|
||||
"core-js": "^3.49.0",
|
||||
"dompurify": "^3.3.2",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.29.2",
|
||||
"preact": "^10.29.3",
|
||||
"query-selector-shadow-dom": "^1.0.1",
|
||||
"web-vitals": "^5.3.0"
|
||||
}
|
||||
@@ -9422,9 +9425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
|
||||
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
|
||||
"version": "17.0.10",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz",
|
||||
"integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.29.2",
|
||||
@@ -9434,7 +9437,7 @@
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 26.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5 || ^6"
|
||||
"typescript": "^5 || ^6 || ^7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
@@ -11645,9 +11648,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webrtc-adapter": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.5.tgz",
|
||||
"integrity": "sha512-U9vjByy/sK2OMXu5mmfuZFKTMIUQe34c0JXRO+oDrxJTsntdYT2iIFwYMOV7HhMTuktcZLGf2W1N/OcSf9ssWg==",
|
||||
"version": "9.0.6",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-9.0.6.tgz",
|
||||
"integrity": "sha512-CHbl2ZQbxx164IgWRgzJno4hWtM4tFbRam1QfI3Yxhs3w/DvqluVxVWeXs3oL5/fbGkSNLKo0Ty5MgUWceNhog==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"sdp": "^3.2.0"
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@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/track-processors": "0.7.2",
|
||||
"@mediapipe/tasks-vision": "0.10.14",
|
||||
@@ -31,17 +31,17 @@
|
||||
"crisp-sdk-web": "1.1.2",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "26.3.1",
|
||||
"i18next": "26.3.6",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.20.0",
|
||||
"posthog-js": "1.395.0",
|
||||
"livekit-client": "2.21.0",
|
||||
"posthog-js": "1.404.1",
|
||||
"react": "18.3.1",
|
||||
"react-aria": "3.50.0",
|
||||
"react-aria-components": "1.19.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "17.0.8",
|
||||
"react-i18next": "17.0.10",
|
||||
"react-stately": "3.48.0",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
|
||||
@@ -137,7 +137,6 @@ export const captureMediaEvent = async (
|
||||
| 'media-device-topology'
|
||||
| 'media-device-success'
|
||||
| 'device-not-found'
|
||||
| 'device-in-use'
|
||||
| 'permissions-denied'
|
||||
| 'screen-share-permission-denied'
|
||||
| 'silent-mic-detected'
|
||||
|
||||
@@ -23,7 +23,7 @@ export const ChatProvider = () => {
|
||||
resetChatStore()
|
||||
}, [])
|
||||
|
||||
// Tigger the message notification (temporary)
|
||||
// Trigger the message notification (temporary)
|
||||
useEffect(() => {
|
||||
// TEMPORARY: This is a brittle workaround that relies on message count tracking
|
||||
// due to recent LiveKit useChat changes breaking the previous implementation
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
userChoicesStore,
|
||||
} from '@/stores/userChoices'
|
||||
import { useCannotUseDevice } from '../livekit/hooks/useCannotUseDevice'
|
||||
import { useDeviceInUse } from '../livekit/hooks/useDeviceInUse'
|
||||
import { useDeviceMissing } from '../livekit/hooks/useDeviceMissing'
|
||||
import { useJoinTracks } from '../livekit/hooks/useJoinTracks'
|
||||
import { SilentMicDetector } from './SilentMicDetector'
|
||||
@@ -219,14 +218,12 @@ const switchTrackDevice =
|
||||
function getPreviewMessages({
|
||||
cameraFound,
|
||||
cameraDenied,
|
||||
cameraInUse,
|
||||
micDenied,
|
||||
videoEnabled,
|
||||
videoStarted,
|
||||
}: {
|
||||
cameraFound: boolean
|
||||
cameraDenied: boolean
|
||||
cameraInUse: boolean
|
||||
micDenied: boolean
|
||||
videoEnabled: boolean
|
||||
videoStarted: boolean
|
||||
@@ -238,9 +235,6 @@ function getPreviewMessages({
|
||||
const key = micDenied ? 'cameraAndMicNotGranted' : 'cameraNotGranted'
|
||||
return { hint: key, permissionsButtonLabel: key }
|
||||
}
|
||||
if (cameraInUse) {
|
||||
return { hint: 'cameraInUse', permissionsButtonLabel: null }
|
||||
}
|
||||
if (!videoEnabled) {
|
||||
return { hint: 'cameraDisabled', permissionsButtonLabel: null }
|
||||
}
|
||||
@@ -334,20 +328,18 @@ const VideoPreview = ({
|
||||
const cameraDenied = useCannotUseDevice('videoinput')
|
||||
const micDenied = useCannotUseDevice('audioinput')
|
||||
const cameraMissing = useDeviceMissing('videoinput')
|
||||
const cameraInUse = useDeviceInUse('videoinput')
|
||||
|
||||
const { videoEl, videoStarted } = useAttachedVideo(videoTrack, videoEnabled)
|
||||
|
||||
const { hint, permissionsButtonLabel } = getPreviewMessages({
|
||||
cameraFound: !cameraMissing,
|
||||
cameraDenied,
|
||||
cameraInUse,
|
||||
micDenied,
|
||||
videoEnabled,
|
||||
videoStarted,
|
||||
})
|
||||
|
||||
const isError = cameraMissing || cameraDenied || cameraInUse
|
||||
const isError = cameraMissing || cameraDenied
|
||||
|
||||
return (
|
||||
<div className={styles.previewFrame}>
|
||||
|
||||
+2
-2
@@ -44,7 +44,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
videoElement?: HTMLVideoElement
|
||||
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
|
||||
outputCanvasCtx?: CanvasRenderingContext2D
|
||||
|
||||
@@ -55,7 +55,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
segmentationMaskCanvas?: HTMLCanvasElement
|
||||
segmentationMaskCanvasCtx?: CanvasRenderingContext2D
|
||||
|
||||
// Mask containg the inference result.
|
||||
// Mask containing the inference result.
|
||||
segmentationMask?: ImageData
|
||||
|
||||
// The resized image of the video source.
|
||||
|
||||
+9
-22
@@ -20,7 +20,6 @@ import { openPermissionsDialog } from '@/stores/permissions'
|
||||
import { openSilentMicDialog, silentMicStore } from '@/stores/silentMic'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice'
|
||||
import { useDeviceInUse } from '../../../hooks/useDeviceInUse'
|
||||
import { useDeviceMissing } from '../../../hooks/useDeviceMissing'
|
||||
import { requestDevicePermission } from '../../../hooks/useJoinTracks'
|
||||
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||
@@ -98,23 +97,21 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
const deviceIcons = useDeviceIcons(kind)
|
||||
const cannotUseDevice = useCannotUseDevice(kind)
|
||||
const deviceMissing = useDeviceMissing(kind)
|
||||
const deviceInUse = useDeviceInUse(kind)
|
||||
const { status: silentMicStatus } = useSnapshot(silentMicStore)
|
||||
const silentMicWarning =
|
||||
kind === 'audioinput' &&
|
||||
silentMicStatus === 'silent' &&
|
||||
!cannotUseDevice &&
|
||||
!deviceMissing &&
|
||||
!deviceInUse
|
||||
!deviceMissing
|
||||
const deviceShortcut = useDeviceShortcut(kind)
|
||||
const announce = useScreenReaderAnnounce()
|
||||
|
||||
const isRequestingPermission = useRef(false)
|
||||
const [alertError, setAlertError] = useState<MediaDeviceFailure | null>(null)
|
||||
const [showDeviceNotFound, setShowDeviceNotFound] = useState(false)
|
||||
|
||||
const onPress = async () => {
|
||||
if (!enabled && deviceMissing) {
|
||||
setAlertError(MediaDeviceFailure.NotFound)
|
||||
setShowDeviceNotFound(true)
|
||||
return
|
||||
}
|
||||
if (!cannotUseDevice) {
|
||||
@@ -188,18 +185,10 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
<PermissionNeededButton
|
||||
tooltip={deviceMissing ? t(`deviceNotFound.${kind}`) : undefined}
|
||||
onPress={
|
||||
deviceMissing
|
||||
? () => setAlertError(MediaDeviceFailure.NotFound)
|
||||
: undefined
|
||||
deviceMissing ? () => setShowDeviceNotFound(true) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{deviceInUse && (
|
||||
<PermissionNeededButton
|
||||
tooltip={t(`deviceInUse.${kind}`)}
|
||||
onPress={() => setAlertError(MediaDeviceFailure.DeviceInUse)}
|
||||
/>
|
||||
)}
|
||||
{silentMicWarning && (
|
||||
<PermissionNeededButton
|
||||
tooltip={t('tooltip', { keyPrefix: 'silentMic' })}
|
||||
@@ -218,11 +207,9 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
tooltip={
|
||||
deviceMissing
|
||||
? t(`deviceNotFound.${kind}`)
|
||||
: deviceInUse
|
||||
? t(`deviceInUse.${kind}`)
|
||||
: cannotUseDevice
|
||||
? t('tooltip', { keyPrefix: 'permissionsButton' })
|
||||
: toggleLabel
|
||||
: cannotUseDevice
|
||||
? t('tooltip', { keyPrefix: 'permissionsButton' })
|
||||
: toggleLabel
|
||||
}
|
||||
{...computedToggleButtonProps}
|
||||
{...overrideToggleButtonProps}
|
||||
@@ -230,9 +217,9 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
<Icon />
|
||||
</ToggleButton>
|
||||
<MediaDeviceErrorAlert
|
||||
error={alertError}
|
||||
error={showDeviceNotFound ? MediaDeviceFailure.NotFound : null}
|
||||
kind={kind}
|
||||
onClose={() => setAlertError(null)}
|
||||
onClose={() => setShowDeviceNotFound(false)}
|
||||
/>
|
||||
</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,
|
||||
type PermissionKind,
|
||||
} from '@/stores/permissions'
|
||||
import { clearDeviceInUse, noteDeviceInUse } from '@/stores/deviceInUse'
|
||||
import { getOS } from '@/utils/os'
|
||||
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
||||
import {
|
||||
@@ -90,15 +89,7 @@ const onMediaPermissionError = (
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.DeviceInUse &&
|
||||
path === 'join_preview'
|
||||
) {
|
||||
noteDeviceInUse(kind)
|
||||
void captureMediaEvent('device-in-use', { path, kind, os: getOS() })
|
||||
return
|
||||
}
|
||||
|
||||
// "Other" and "Device in use" are still reported as errors, as they are not handled on the join screen.
|
||||
reportError(
|
||||
path === 'room' ? 'room_media_failure' : 'join_preview_failure',
|
||||
e,
|
||||
@@ -106,11 +97,6 @@ const onMediaPermissionError = (
|
||||
)
|
||||
}
|
||||
|
||||
const noteDeviceReady = (kind?: PermissionKind) => {
|
||||
noteGumSuccess(kind)
|
||||
clearDeviceInUse(kind)
|
||||
}
|
||||
|
||||
// Module-level: effect dependencies, must be referentially stable.
|
||||
const disableAudio = () => saveAudioInputEnabled(false)
|
||||
const disableVideo = () => saveVideoInputEnabled(false)
|
||||
@@ -128,7 +114,7 @@ export const requestDevicePermission = async (
|
||||
? await createLocalAudioTrack()
|
||||
: await createLocalVideoTrack()
|
||||
track.stop()
|
||||
noteDeviceReady(PERMISSION_KIND[kind])
|
||||
noteGumSuccess(PERMISSION_KIND[kind])
|
||||
return true
|
||||
} catch (error) {
|
||||
onMediaPermissionError(error as Error, PERMISSION_KIND[kind], path)
|
||||
@@ -172,7 +158,7 @@ function useWarmupPermissions(): WarmupState {
|
||||
video: true,
|
||||
})
|
||||
)
|
||||
noteDeviceReady()
|
||||
noteGumSuccess()
|
||||
bothReady()
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -194,7 +180,7 @@ function useWarmupPermissions(): WarmupState {
|
||||
.getUserMedia({ audio: true })
|
||||
.then((stream) => {
|
||||
stopAll(stream)
|
||||
noteDeviceReady('microphone')
|
||||
noteGumSuccess('microphone')
|
||||
})
|
||||
.catch((e) => onMediaPermissionError(e as Error, 'microphone'))
|
||||
.finally(() =>
|
||||
@@ -204,7 +190,7 @@ function useWarmupPermissions(): WarmupState {
|
||||
.getUserMedia({ video: true })
|
||||
.then((stream) => {
|
||||
stopAll(stream)
|
||||
noteDeviceReady('camera')
|
||||
noteGumSuccess('camera')
|
||||
})
|
||||
.catch((e) => onMediaPermissionError(e as Error, 'camera'))
|
||||
.finally(() =>
|
||||
@@ -241,7 +227,7 @@ function useLocalTrack<T extends LocalAudioTrack | LocalVideoTrack>({
|
||||
let cancelled = false
|
||||
create()
|
||||
.then((newTrack) => {
|
||||
noteDeviceReady(permissionKind)
|
||||
noteGumSuccess(permissionKind)
|
||||
if (cancelled) {
|
||||
newTrack.stop()
|
||||
return
|
||||
@@ -305,8 +291,6 @@ export function useJoinTracks(): {
|
||||
|
||||
const { audioReady, videoReady } = useWarmupPermissions()
|
||||
|
||||
useEffect(() => () => clearDeviceInUse(), [])
|
||||
|
||||
const createAudio = useCallback(
|
||||
() =>
|
||||
createLocalAudioTrack({
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
"videoinput": "Keine Kamera erkannt. Prüfe, ob sie 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": {
|
||||
"audio": "Audioeinstellungen",
|
||||
"video": "Videoeinstellungen"
|
||||
@@ -71,7 +67,6 @@
|
||||
},
|
||||
"cameraDisabled": "Kamera ist deaktiviert.",
|
||||
"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…",
|
||||
"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?",
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
"videoinput": "No camera 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": {
|
||||
"audio": "Audio settings",
|
||||
"video": "Video settings"
|
||||
@@ -71,7 +67,6 @@
|
||||
},
|
||||
"cameraDisabled": "Camera is disabled.",
|
||||
"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…",
|
||||
"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?",
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
"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é."
|
||||
},
|
||||
"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": {
|
||||
"audio": "Paramètres audio",
|
||||
"video": "Paramètres video"
|
||||
@@ -71,7 +67,6 @@
|
||||
},
|
||||
"cameraDisabled": "La caméra est désactivé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…",
|
||||
"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 ?",
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
"videoinput": "Geen camera 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": {
|
||||
"audio": "Audio-instellingen",
|
||||
"video": "Video-instellingen"
|
||||
@@ -71,7 +67,6 @@
|
||||
},
|
||||
"cameraDisabled": "Camera is uitgeschakeld.",
|
||||
"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…",
|
||||
"cameraNotGranted": "Wilt u dat anderen u tijdens de vergadering kunnen zien?",
|
||||
"cameraAndMicNotGranted": "Wilt u dat anderen u tijdens de vergadering kunnen zien en horen?",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class RecordingMetadata(BaseModel):
|
||||
|
||||
cloud_storage_url: Url = Field(
|
||||
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")
|
||||
ended_at: AwareDatetime = Field(title="End time of the recording to transcribe")
|
||||
|
||||
Reference in New Issue
Block a user