mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-21 23:57:00 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06494371cc | |||
| 4f5d5d1b9a | |||
| e5f0b1c202 | |||
| 9e0d57a8c6 | |||
| 7ba0803b71 | |||
| beacfc3d3f | |||
| 52e5d99e83 | |||
| 15ca2b41b4 | |||
| e34f3dd219 | |||
| feb573551f | |||
| 1d0a0cc637 | |||
| eae93f771f | |||
| 45175a2a54 | |||
| 8b22059b18 | |||
| 87dbd8069d | |||
| c7420c59a3 | |||
| f46babfcbd |
@@ -8,14 +8,30 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- 📈(frontend) track errors when starting or stopping a recording
|
||||
- 🚸(frontend) explain camera-in-use failures on the join screen
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(backend) accept form-urlencoded on the user token endpoint
|
||||
- ✨(summary) configurable s3 region
|
||||
- ⬆️(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
|
||||
- 💄(frontend) increase the blur intensity
|
||||
|
||||
### Fixed
|
||||
|
||||
- 📝(docs) fix minor typos in comments and docstrings
|
||||
- ⬆️(backend) bump sqlparse from 0.5.5 to 0.6.0
|
||||
- ⬆️(mail) bump @html-to/text-cli from 0.6.0 to 0.6.1
|
||||
- 🐛(frontend) treat client-initiated connect aborts as events
|
||||
- 🐛(frontend) use state instead of a ref for MoreControls container
|
||||
- 🐛(frontend) stop init_virtual_background from firing on blur updates
|
||||
- 🐛(frontend) hoist mute confirmation dialog to VideoConference level
|
||||
- 🐛(frontend) fix joined notification tile no longer rendering properly
|
||||
|
||||
## [1.27.0] - 2026-08-14
|
||||
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ services:
|
||||
working_dir: /app
|
||||
|
||||
node:
|
||||
image: node:18
|
||||
image: node:22
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
|
||||
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,6 +137,7 @@ 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'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useToast } from 'react-aria'
|
||||
import { useRef } from 'react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { Button as RACButton } from 'react-aria-components'
|
||||
import { Track } from 'livekit-client'
|
||||
import Source = Track.Source
|
||||
@@ -11,6 +11,7 @@ import { Div } from '@/primitives'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { StyledToastContainer } from './StyledToastContainer'
|
||||
import { setPinnedTrack } from '@/stores/layout'
|
||||
import { useParticipantTracks } from '@livekit/components-react'
|
||||
|
||||
const ClickableToast = styled(RACButton, {
|
||||
base: {
|
||||
@@ -30,13 +31,17 @@ export function ToastJoined({ state, ...props }: Readonly<ToastProps>) {
|
||||
)
|
||||
const participant = props.toast.content.participant
|
||||
|
||||
if (!participant) return
|
||||
const [cameraTrack] = useParticipantTracks(
|
||||
[Source.Camera],
|
||||
participant?.identity
|
||||
)
|
||||
|
||||
const trackReference = {
|
||||
participant,
|
||||
publication: participant.getTrackPublication(Source.Camera),
|
||||
source: Source.Camera,
|
||||
}
|
||||
const trackReference = useMemo(
|
||||
() => cameraTrack ?? { participant, source: Source.Camera },
|
||||
[cameraTrack, participant]
|
||||
)
|
||||
|
||||
if (!participant) return
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
|
||||
+11
-26
@@ -1,11 +1,9 @@
|
||||
import { Participant, Track } from 'livekit-client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTrackMutedIndicator } from '@livekit/components-react'
|
||||
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/primitives'
|
||||
import { RiMicLine, RiMicOffLine } from '@remixicon/react'
|
||||
import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog'
|
||||
import { openMuteDialog } from '@/stores/muteDialog'
|
||||
|
||||
export const MuteButton = ({ participant }: { participant: Participant }) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
||||
@@ -15,31 +13,18 @@ export const MuteButton = ({ participant }: { participant: Participant }) => {
|
||||
source: Track.Source.Microphone,
|
||||
})
|
||||
|
||||
const { muteParticipant } = useMuteParticipant()
|
||||
const [isAlertOpen, setIsAlertOpen] = useState(false)
|
||||
|
||||
const name = participant.name || participant.identity
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={isMuted}
|
||||
size={'sm'}
|
||||
variant={'primaryTextDark'}
|
||||
square
|
||||
onPress={() => setIsAlertOpen(true)}
|
||||
tooltip={t('muteParticipant', { name })}
|
||||
>
|
||||
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
|
||||
</Button>
|
||||
<MuteAlertDialog
|
||||
isOpen={isAlertOpen}
|
||||
onSubmit={() =>
|
||||
muteParticipant(participant).then(() => setIsAlertOpen(false))
|
||||
}
|
||||
onClose={() => setIsAlertOpen(false)}
|
||||
name={name}
|
||||
/>
|
||||
</>
|
||||
<Button
|
||||
isDisabled={isMuted}
|
||||
size={'sm'}
|
||||
variant={'primaryTextDark'}
|
||||
square
|
||||
onPress={() => openMuteDialog(participant)}
|
||||
tooltip={t('muteParticipant', { name })}
|
||||
>
|
||||
{!isMuted ? <RiMicLine /> : <RiMicOffLine />}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,13 +19,11 @@ import {
|
||||
import Source = Track.Source
|
||||
import { RiMicFill, RiMicOffFill } from '@remixicon/react'
|
||||
import { Button } from '@/primitives'
|
||||
import { useState } from 'react'
|
||||
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
||||
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
|
||||
import { ParticipantMenuButton } from './menu/ParticipantMenuButton'
|
||||
import { PinBadge } from './PinBadge'
|
||||
import { UnauthenticatedBadge } from './UnauthenticatedBadge'
|
||||
import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog'
|
||||
import { openMuteDialog } from '@/stores/muteDialog'
|
||||
import { ParticipantName } from './ParticipantName'
|
||||
|
||||
type MicIndicatorProps = {
|
||||
@@ -34,7 +32,6 @@ type MicIndicatorProps = {
|
||||
|
||||
const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
const { muteParticipant } = useMuteParticipant()
|
||||
const { isMuted } = useTrackMutedIndicator({
|
||||
participant: participant,
|
||||
source: Source.Microphone,
|
||||
@@ -42,7 +39,6 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
||||
|
||||
const canMute = useCanMute(participant)
|
||||
const isSpeaking = useIsSpeaking(participant)
|
||||
const [isAlertOpen, setIsAlertOpen] = useState(false)
|
||||
const name = participant.name || participant.identity
|
||||
|
||||
const label = isLocal(participant)
|
||||
@@ -52,46 +48,34 @@ const MicIndicator = ({ participant }: MicIndicatorProps) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
square
|
||||
variant="greyscale"
|
||||
size="sm"
|
||||
tooltip={label}
|
||||
aria-label={label}
|
||||
isDisabled={isMuted || !canMute}
|
||||
onPress={async () =>
|
||||
!isMuted && isLocal(participant)
|
||||
? await (participant as LocalParticipant)?.setMicrophoneEnabled(
|
||||
false
|
||||
)
|
||||
: setIsAlertOpen(true)
|
||||
}
|
||||
data-attr="participants-mute"
|
||||
>
|
||||
{isMuted ? (
|
||||
<RiMicOffFill color={'gray'} aria-hidden={true} />
|
||||
) : (
|
||||
<RiMicFill
|
||||
className={css({
|
||||
color: isSpeaking ? 'primaryDark.300' : 'primaryDark.50',
|
||||
animation: isSpeaking
|
||||
? 'pulse_background 800ms infinite'
|
||||
: undefined,
|
||||
})}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
<MuteAlertDialog
|
||||
isOpen={isAlertOpen}
|
||||
onSubmit={() =>
|
||||
muteParticipant(participant).then(() => setIsAlertOpen(false))
|
||||
}
|
||||
onClose={() => setIsAlertOpen(false)}
|
||||
name={name}
|
||||
/>
|
||||
</>
|
||||
<Button
|
||||
square
|
||||
variant="greyscale"
|
||||
size="sm"
|
||||
tooltip={label}
|
||||
aria-label={label}
|
||||
isDisabled={isMuted || !canMute}
|
||||
onPress={async () =>
|
||||
!isMuted && isLocal(participant)
|
||||
? await (participant as LocalParticipant)?.setMicrophoneEnabled(false)
|
||||
: openMuteDialog(participant)
|
||||
}
|
||||
data-attr="participants-mute"
|
||||
>
|
||||
{isMuted ? (
|
||||
<RiMicOffFill color={'gray'} aria-hidden={true} />
|
||||
) : (
|
||||
<RiMicFill
|
||||
className={css({
|
||||
color: isSpeaking ? 'primaryDark.300' : 'primaryDark.50',
|
||||
animation: isSpeaking
|
||||
? 'pulse_background 800ms infinite'
|
||||
: undefined,
|
||||
})}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useStartRecording, useStopRecording } from '@/features/recording'
|
||||
import { recordingStore } from '@/stores/recording'
|
||||
import { captureEvent } from '@/features/analytics/telemetry'
|
||||
|
||||
export const useMutateRecording = () => {
|
||||
const { mutateAsync: startRecording, isPending: isPendingToStart } =
|
||||
useStartRecording({
|
||||
onError: () => {
|
||||
recordingStore.isErrorDialogOpen = 'start'
|
||||
captureEvent('error-starting-recording')
|
||||
},
|
||||
})
|
||||
const { mutateAsync: stopRecording, isPending: isPendingToStop } =
|
||||
useStopRecording({
|
||||
onError: () => {
|
||||
recordingStore.isErrorDialogOpen = 'stop'
|
||||
captureEvent('error-stopping-recording')
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
usePersistentUserChoices,
|
||||
} from '@livekit/components-react'
|
||||
import {
|
||||
ConnectionError,
|
||||
ConnectionErrorReason,
|
||||
DisconnectReason,
|
||||
MediaDeviceFailure,
|
||||
Room,
|
||||
@@ -25,7 +27,11 @@ import { VideoConference } from '../livekit/prefabs/VideoConference'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||
import { LocalUserChoices } from '@/stores/userChoices'
|
||||
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
||||
import {
|
||||
captureEvent,
|
||||
captureMediaEvent,
|
||||
reportError,
|
||||
} from '@/features/analytics/telemetry'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { isFireFox } from '@/utils/livekit'
|
||||
import { useIsMobile } from '@/utils/useIsMobile'
|
||||
@@ -227,6 +233,16 @@ export const Conference = ({
|
||||
onError={(e) => {
|
||||
const failure = MediaDeviceFailure.getFailure(e)
|
||||
if (failure && failure !== MediaDeviceFailure.Other) return
|
||||
|
||||
// connect() was aborted by a disconnect() before the join completed
|
||||
if (
|
||||
e instanceof ConnectionError &&
|
||||
e.reason === ConnectionErrorReason.Cancelled
|
||||
) {
|
||||
void captureEvent('connection-cancelled')
|
||||
return
|
||||
}
|
||||
|
||||
reportError('livekit_room_error', e, {
|
||||
path: 'connect_publish',
|
||||
})
|
||||
|
||||
@@ -28,6 +28,7 @@ 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'
|
||||
@@ -218,12 +219,14 @@ const switchTrackDevice =
|
||||
function getPreviewMessages({
|
||||
cameraFound,
|
||||
cameraDenied,
|
||||
cameraInUse,
|
||||
micDenied,
|
||||
videoEnabled,
|
||||
videoStarted,
|
||||
}: {
|
||||
cameraFound: boolean
|
||||
cameraDenied: boolean
|
||||
cameraInUse: boolean
|
||||
micDenied: boolean
|
||||
videoEnabled: boolean
|
||||
videoStarted: boolean
|
||||
@@ -235,6 +238,9 @@ 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 }
|
||||
}
|
||||
@@ -328,18 +334,20 @@ 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
|
||||
const isError = cameraMissing || cameraDenied || cameraInUse
|
||||
|
||||
return (
|
||||
<div className={styles.previewFrame}>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { deviceAvailabilityStore } from '@/stores/deviceAvailability'
|
||||
import { probeDeviceReleased } from '../livekit/utils/mediaPermissions'
|
||||
import type { PermissionKind } from '@/stores/permissions'
|
||||
|
||||
const RETRY_INTERVAL_MS = 30_000
|
||||
|
||||
/**
|
||||
* There is no browser event for "another app released the device", so
|
||||
* while a device is flagged in use, re-probe it periodically. Only clears
|
||||
* the flag (the toggle becomes usable again); it never re-enables the
|
||||
* device on the user's behalf.
|
||||
*/
|
||||
export function useWatchDeviceReleased() {
|
||||
const { cameraInUse, microphoneInUse } = useSnapshot(deviceAvailabilityStore)
|
||||
useWatchKind('camera', cameraInUse)
|
||||
useWatchKind('microphone', microphoneInUse)
|
||||
}
|
||||
|
||||
function useWatchKind(kind: PermissionKind, inUse: boolean) {
|
||||
useEffect(() => {
|
||||
if (!inUse) return
|
||||
const id = setInterval(
|
||||
() => void probeDeviceReleased(kind),
|
||||
RETRY_INTERVAL_MS
|
||||
)
|
||||
return () => clearInterval(id)
|
||||
}, [kind, inUse])
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useRef } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useMuteParticipant } from '@/features/rooms/api/muteParticipant'
|
||||
import { closeMuteDialog, muteDialogStore } from '@/stores/muteDialog'
|
||||
import { MuteAlertDialog } from './MuteAlertDialog'
|
||||
|
||||
export const MuteAlertDialogProvider = () => {
|
||||
const { participant } = useSnapshot(muteDialogStore)
|
||||
const { muteParticipant } = useMuteParticipant()
|
||||
|
||||
const lastNameRef = useRef('')
|
||||
if (participant) {
|
||||
lastNameRef.current = participant.name || participant.identity
|
||||
}
|
||||
|
||||
return (
|
||||
<MuteAlertDialog
|
||||
isOpen={!!participant}
|
||||
name={lastNameRef.current}
|
||||
onClose={closeMuteDialog}
|
||||
onSubmit={() => {
|
||||
const target = muteDialogStore.participant
|
||||
if (!target) return
|
||||
muteParticipant(target).then(closeMuteDialog)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
+1
-3
@@ -105,9 +105,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||
|
||||
_initVirtualBackgroundImage() {
|
||||
if (this.options.type !== 'virtual') {
|
||||
throw new Error(
|
||||
'Virtual background is only supported for virtual background'
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const needsUpdate =
|
||||
|
||||
+40
-21
@@ -20,8 +20,9 @@ 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 { requestDevicePermission } from '../../../utils/mediaPermissions'
|
||||
import { useDeviceIcons } from '../../../hooks/useDeviceIcons'
|
||||
import { useDeviceShortcut } from '../../../hooks/useDeviceShortcut'
|
||||
import type {
|
||||
@@ -97,38 +98,42 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
const deviceIcons = useDeviceIcons(kind)
|
||||
const cannotUseDevice = useCannotUseDevice(kind)
|
||||
const deviceMissing = useDeviceMissing(kind)
|
||||
const deviceInUse = useDeviceInUse(kind)
|
||||
const explainDeviceInUse = deviceInUse && context === 'room'
|
||||
const { status: silentMicStatus } = useSnapshot(silentMicStore)
|
||||
const silentMicWarning =
|
||||
kind === 'audioinput' &&
|
||||
silentMicStatus === 'silent' &&
|
||||
!cannotUseDevice &&
|
||||
!deviceMissing
|
||||
!deviceMissing &&
|
||||
!deviceInUse
|
||||
const deviceShortcut = useDeviceShortcut(kind)
|
||||
const announce = useScreenReaderAnnounce()
|
||||
|
||||
const isRequestingPermission = useRef(false)
|
||||
const [showDeviceNotFound, setShowDeviceNotFound] = useState(false)
|
||||
const [alertError, setAlertError] = useState<MediaDeviceFailure | null>(null)
|
||||
|
||||
const mediaPath = context === 'join' ? 'join_preview' : 'room'
|
||||
|
||||
const onPress = async () => {
|
||||
if (!enabled && deviceMissing) {
|
||||
setShowDeviceNotFound(true)
|
||||
setAlertError(MediaDeviceFailure.NotFound)
|
||||
return
|
||||
}
|
||||
if (!cannotUseDevice) {
|
||||
if (!cannotUseDevice && !deviceInUse) {
|
||||
toggle()
|
||||
return
|
||||
}
|
||||
if (isRequestingPermission.current) return
|
||||
isRequestingPermission.current = true
|
||||
try {
|
||||
const granted = await requestDevicePermission(
|
||||
kind,
|
||||
context === 'join' ? 'join_preview' : 'room'
|
||||
)
|
||||
if (granted) {
|
||||
const acquired = await requestDevicePermission(kind, mediaPath)
|
||||
if (acquired) {
|
||||
toggle()
|
||||
} else {
|
||||
} else if (cannotUseDevice) {
|
||||
openPermissionsDialog(kind)
|
||||
} else if (explainDeviceInUse) {
|
||||
setAlertError(MediaDeviceFailure.DeviceInUse)
|
||||
}
|
||||
} finally {
|
||||
isRequestingPermission.current = false
|
||||
@@ -179,13 +184,33 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
return <ActiveSpeakerWrapper />
|
||||
}
|
||||
|
||||
const getToggleTooltip = () => {
|
||||
if (deviceMissing) return t(`deviceNotFound.${kind}`)
|
||||
if (explainDeviceInUse) return t(`deviceInUse.${kind}`)
|
||||
if (cannotUseDevice) return t('tooltip', { keyPrefix: 'permissionsButton' })
|
||||
return toggleLabel
|
||||
}
|
||||
const toggleTooltip = getToggleTooltip()
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{(cannotUseDevice || deviceMissing) && (
|
||||
<PermissionNeededButton
|
||||
tooltip={deviceMissing ? t(`deviceNotFound.${kind}`) : undefined}
|
||||
onPress={
|
||||
deviceMissing ? () => setShowDeviceNotFound(true) : undefined
|
||||
deviceMissing
|
||||
? () => setAlertError(MediaDeviceFailure.NotFound)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{deviceInUse && (
|
||||
<PermissionNeededButton
|
||||
tooltip={explainDeviceInUse ? t(`deviceInUse.${kind}`) : undefined}
|
||||
onPress={
|
||||
explainDeviceInUse
|
||||
? () => setAlertError(MediaDeviceFailure.DeviceInUse)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -204,22 +229,16 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
shySelected
|
||||
onPress={onPress}
|
||||
aria-label={toggleLabel}
|
||||
tooltip={
|
||||
deviceMissing
|
||||
? t(`deviceNotFound.${kind}`)
|
||||
: cannotUseDevice
|
||||
? t('tooltip', { keyPrefix: 'permissionsButton' })
|
||||
: toggleLabel
|
||||
}
|
||||
tooltip={toggleTooltip}
|
||||
{...computedToggleButtonProps}
|
||||
{...overrideToggleButtonProps}
|
||||
>
|
||||
<Icon />
|
||||
</ToggleButton>
|
||||
<MediaDeviceErrorAlert
|
||||
error={showDeviceNotFound ? MediaDeviceFailure.NotFound : null}
|
||||
error={alertError}
|
||||
kind={kind}
|
||||
onClose={() => setShowDeviceNotFound(false)}
|
||||
onClose={() => setAlertError(null)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -37,8 +37,8 @@ import { reportError } from '@/features/analytics/telemetry'
|
||||
|
||||
enum BlurRadius {
|
||||
NONE = 0,
|
||||
LIGHT = 5,
|
||||
NORMAL = 10,
|
||||
LIGHT = 10,
|
||||
NORMAL = 20,
|
||||
}
|
||||
|
||||
const isSupported = BackgroundProcessorFactory.isSupported()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { deviceAvailabilityStore } from '@/stores/deviceAvailability'
|
||||
import { useCannotUseDevice } from './useCannotUseDevice'
|
||||
import { useDeviceMissing } from './useDeviceMissing'
|
||||
|
||||
export const useDeviceInUse = (kind: MediaDeviceKind): boolean => {
|
||||
const { cameraInUse, microphoneInUse } = useSnapshot(deviceAvailabilityStore)
|
||||
const cannotUseDevice = useCannotUseDevice(kind)
|
||||
const deviceMissing = useDeviceMissing(kind)
|
||||
|
||||
if (cannotUseDevice || deviceMissing) return false
|
||||
|
||||
switch (kind) {
|
||||
case 'videoinput':
|
||||
return cameraInUse
|
||||
case 'audioinput':
|
||||
return microphoneInUse
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,13 @@ import {
|
||||
} from 'livekit-client'
|
||||
import { BackgroundProcessorFactory } from '../components/blur'
|
||||
import {
|
||||
classifyPermissionError,
|
||||
isLikelySystemNotFound,
|
||||
isSystemPermissionError,
|
||||
noteGumSuccess,
|
||||
notePermissionDeniedFromGum,
|
||||
noteSystemPermissionDenied,
|
||||
type PermissionKind,
|
||||
} from '@/stores/permissions'
|
||||
import { getOS } from '@/utils/os'
|
||||
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
||||
import {
|
||||
noteDeviceReady,
|
||||
onMediaPermissionError,
|
||||
} from '../utils/mediaPermissions'
|
||||
import {
|
||||
saveAudioInputDeviceId,
|
||||
saveAudioInputEnabled,
|
||||
@@ -39,64 +36,6 @@ const VOICE_AUDIO_CONSTRAINTS = {
|
||||
sampleSize: 16,
|
||||
} as const
|
||||
|
||||
const PERMISSION_KIND: Record<'audioinput' | 'videoinput', PermissionKind> = {
|
||||
audioinput: 'microphone',
|
||||
videoinput: 'camera',
|
||||
}
|
||||
|
||||
type MediaPath = 'join_preview' | 'room'
|
||||
|
||||
const onMediaPermissionError = (
|
||||
e: Error,
|
||||
kind?: PermissionKind,
|
||||
path: MediaPath = 'join_preview'
|
||||
) => {
|
||||
if (
|
||||
MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.PermissionDenied
|
||||
) {
|
||||
void classifyPermissionError(e, kind).then((scope) => {
|
||||
if (scope === 'system') {
|
||||
noteSystemPermissionDenied(kind)
|
||||
} else {
|
||||
notePermissionDeniedFromGum(kind)
|
||||
}
|
||||
captureMediaEvent('permissions-denied', {
|
||||
path,
|
||||
kind,
|
||||
denied_scope: scope,
|
||||
os: getOS(),
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (MediaDeviceFailure.getFailure(e) === MediaDeviceFailure.NotFound) {
|
||||
// Firefox reports OS-level blocks as NotFoundError (macOS privacy
|
||||
// settings, missing Android app permissions).
|
||||
void isLikelySystemNotFound(e, kind).then((system) => {
|
||||
if (system) {
|
||||
noteSystemPermissionDenied(kind)
|
||||
captureMediaEvent('permissions-denied', {
|
||||
path,
|
||||
kind,
|
||||
denied_scope: 'system',
|
||||
os: getOS(),
|
||||
})
|
||||
return
|
||||
}
|
||||
captureMediaEvent('device-not-found', { path, kind })
|
||||
})
|
||||
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,
|
||||
{ path, kind }
|
||||
)
|
||||
}
|
||||
|
||||
// Module-level: effect dependencies, must be referentially stable.
|
||||
const disableAudio = () => saveAudioInputEnabled(false)
|
||||
const disableVideo = () => saveVideoInputEnabled(false)
|
||||
@@ -104,24 +43,6 @@ const disableVideo = () => saveVideoInputEnabled(false)
|
||||
const stopAll = (stream: MediaStream) =>
|
||||
stream.getTracks().forEach((track) => track.stop())
|
||||
|
||||
export const requestDevicePermission = async (
|
||||
kind: 'audioinput' | 'videoinput',
|
||||
path: MediaPath = 'join_preview'
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const track =
|
||||
kind === 'audioinput'
|
||||
? await createLocalAudioTrack()
|
||||
: await createLocalVideoTrack()
|
||||
track.stop()
|
||||
noteGumSuccess(PERMISSION_KIND[kind])
|
||||
return true
|
||||
} catch (error) {
|
||||
onMediaPermissionError(error as Error, PERMISSION_KIND[kind], path)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type WarmupState = {
|
||||
audioReady: boolean
|
||||
videoReady: boolean
|
||||
@@ -158,7 +79,7 @@ function useWarmupPermissions(): WarmupState {
|
||||
video: true,
|
||||
})
|
||||
)
|
||||
noteGumSuccess()
|
||||
noteDeviceReady()
|
||||
bothReady()
|
||||
} catch (error) {
|
||||
if (
|
||||
@@ -180,7 +101,7 @@ function useWarmupPermissions(): WarmupState {
|
||||
.getUserMedia({ audio: true })
|
||||
.then((stream) => {
|
||||
stopAll(stream)
|
||||
noteGumSuccess('microphone')
|
||||
noteDeviceReady('microphone')
|
||||
})
|
||||
.catch((e) => onMediaPermissionError(e as Error, 'microphone'))
|
||||
.finally(() =>
|
||||
@@ -190,7 +111,7 @@ function useWarmupPermissions(): WarmupState {
|
||||
.getUserMedia({ video: true })
|
||||
.then((stream) => {
|
||||
stopAll(stream)
|
||||
noteGumSuccess('camera')
|
||||
noteDeviceReady('camera')
|
||||
})
|
||||
.catch((e) => onMediaPermissionError(e as Error, 'camera'))
|
||||
.finally(() =>
|
||||
@@ -227,7 +148,7 @@ function useLocalTrack<T extends LocalAudioTrack | LocalVideoTrack>({
|
||||
let cancelled = false
|
||||
create()
|
||||
.then((newTrack) => {
|
||||
noteGumSuccess(permissionKind)
|
||||
noteDeviceReady(permissionKind)
|
||||
if (cancelled) {
|
||||
newTrack.stop()
|
||||
return
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
import { MediaDeviceFailure, RoomEvent } from 'livekit-client'
|
||||
import {
|
||||
type LocalTrackPublication,
|
||||
MediaDeviceFailure,
|
||||
RoomEvent,
|
||||
Track,
|
||||
} from 'livekit-client'
|
||||
import {
|
||||
PERMISSION_BY_DEVICE_KIND,
|
||||
type PermissionDeniedScope,
|
||||
@@ -10,7 +15,11 @@ import {
|
||||
notePermissionDeniedFromGum,
|
||||
noteSystemPermissionDenied,
|
||||
} from '@/stores/permissions'
|
||||
import { syncDeviceAvailability } from '@/stores/deviceAvailability'
|
||||
import {
|
||||
clearDeviceInUse,
|
||||
noteDeviceInUse,
|
||||
syncDeviceAvailability,
|
||||
} from '@/stores/deviceAvailability'
|
||||
import { captureMediaEvent } from '@/features/analytics/telemetry'
|
||||
import { getOS } from '@/utils/os'
|
||||
|
||||
@@ -21,6 +30,11 @@ type MediaDeviceAlert = {
|
||||
|
||||
const NO_ALERT: MediaDeviceAlert = { error: null, kind: null }
|
||||
|
||||
const PERMISSION_BY_SOURCE: Partial<Record<Track.Source, PermissionKind>> = {
|
||||
[Track.Source.Camera]: 'camera',
|
||||
[Track.Source.Microphone]: 'microphone',
|
||||
}
|
||||
|
||||
const capturePermissionsDenied = (
|
||||
scope: PermissionDeniedScope,
|
||||
kind?: PermissionKind
|
||||
@@ -63,6 +77,7 @@ export const useWatchMediaDeviceErrors = (): MediaDeviceAlert & {
|
||||
const permissionKind = PERMISSION_BY_DEVICE_KIND[kind]
|
||||
switch (failure) {
|
||||
case MediaDeviceFailure.DeviceInUse:
|
||||
noteDeviceInUse(permissionKind)
|
||||
setAlert({ error: failure, kind })
|
||||
break
|
||||
case MediaDeviceFailure.NotFound:
|
||||
@@ -92,9 +107,16 @@ export const useWatchMediaDeviceErrors = (): MediaDeviceAlert & {
|
||||
break
|
||||
}
|
||||
}
|
||||
const onTrackPublished = (publication: LocalTrackPublication) => {
|
||||
const permissionKind = PERMISSION_BY_SOURCE[publication.source]
|
||||
if (permissionKind) clearDeviceInUse(permissionKind)
|
||||
}
|
||||
room.on(RoomEvent.MediaDevicesError, onDeviceError)
|
||||
room.on(RoomEvent.LocalTrackPublished, onTrackPublished)
|
||||
return () => {
|
||||
room.off(RoomEvent.MediaDevicesError, onDeviceError)
|
||||
room.off(RoomEvent.LocalTrackPublished, onTrackPublished)
|
||||
clearDeviceInUse()
|
||||
}
|
||||
}, [room])
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SubtitlesToggle } from '../../components/controls/SubtitlesToggle'
|
||||
import { OptionsButton } from '../../components/controls/Options/OptionsButton'
|
||||
import { StartMediaButton } from '../../components/controls/StartMediaButton'
|
||||
import { MoreOptions } from './MoreOptions'
|
||||
import { useRef } from 'react'
|
||||
import { RefObject, useMemo, useState } from 'react'
|
||||
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
|
||||
import { useFullScreen } from '../../hooks/useFullScreen'
|
||||
import { VideoDeviceControl } from '../../components/controls/Device/VideoDeviceControl'
|
||||
@@ -21,7 +21,14 @@ export function DesktopControlBar({
|
||||
onDeviceError,
|
||||
}: Readonly<ControlBarAuxProps>) {
|
||||
const browserSupportsScreenSharing = supportsScreenSharing()
|
||||
const desktopControlBarEl = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [controlBarElement, setControlBarElement] =
|
||||
useState<HTMLDivElement | null>(null)
|
||||
|
||||
const desktopControlBarEl = useMemo<RefObject<HTMLDivElement>>(
|
||||
() => ({ current: controlBarElement }),
|
||||
[controlBarElement]
|
||||
)
|
||||
|
||||
const { toggleFullScreen, isFullscreenAvailable } = useFullScreen({})
|
||||
|
||||
@@ -45,7 +52,7 @@ export function DesktopControlBar({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={desktopControlBarEl}
|
||||
ref={setControlBarElement}
|
||||
className={css({
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isWeb } from '@livekit/components-core'
|
||||
import { Track } from 'livekit-client'
|
||||
import { MediaDeviceFailure, Track } from 'livekit-client'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
ConnectionStateToast,
|
||||
@@ -19,6 +19,7 @@ import { RoomMetadataSynchronizer } from '../components/RoomMetadataSynchronizer
|
||||
import { useNoiseReduction } from '../hooks/useNoiseReduction'
|
||||
import { VideoResolutionSubscription } from '../components/VideoResolutionSubscription'
|
||||
import { SettingsDialogProvider } from '@/features/settings/components/SettingsDialogProvider'
|
||||
import { MuteAlertDialogProvider } from '@/features/rooms/livekit/components/MuteAlertDialogProvider'
|
||||
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
|
||||
import { ReactionPortals } from '@/features/reactions/components/ReactionPortals'
|
||||
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
||||
@@ -99,6 +100,11 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (MediaDeviceFailure.getFailure(error) != MediaDeviceFailure.Other) {
|
||||
return
|
||||
}
|
||||
|
||||
reportError('device_switch_failure', error, {
|
||||
at: 'ControlBar.onDeviceError',
|
||||
source,
|
||||
@@ -144,6 +150,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||
<ConnectionStateToast />
|
||||
<RecordingProvider />
|
||||
<SettingsDialogProvider />
|
||||
<MuteAlertDialogProvider />
|
||||
<ReactionPortals />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
createLocalAudioTrack,
|
||||
createLocalVideoTrack,
|
||||
MediaDeviceFailure,
|
||||
} from 'livekit-client'
|
||||
import {
|
||||
classifyPermissionError,
|
||||
isLikelySystemNotFound,
|
||||
noteGumSuccess,
|
||||
notePermissionDeniedFromGum,
|
||||
noteSystemPermissionDenied,
|
||||
type PermissionKind,
|
||||
} from '@/stores/permissions'
|
||||
import { clearDeviceInUse, noteDeviceInUse } from '@/stores/deviceAvailability'
|
||||
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
||||
import { getOS } from '@/utils/os'
|
||||
|
||||
/**
|
||||
* Shared handling of getUserMedia() outcomes, used by both:
|
||||
* - the join preview (`useJoinTracks`: warmup + local track acquisition)
|
||||
* - the in-room device toggle (`ToggleDevice`, when permission is missing)
|
||||
*/
|
||||
export type MediaPath = 'join_preview' | 'room'
|
||||
|
||||
export const PERMISSION_KIND: Record<
|
||||
'audioinput' | 'videoinput',
|
||||
PermissionKind
|
||||
> = {
|
||||
audioinput: 'microphone',
|
||||
videoinput: 'camera',
|
||||
}
|
||||
|
||||
export const noteDeviceReady = (kind?: PermissionKind) => {
|
||||
noteGumSuccess(kind)
|
||||
clearDeviceInUse(kind)
|
||||
}
|
||||
|
||||
export const onMediaPermissionError = (
|
||||
e: Error,
|
||||
kind?: PermissionKind,
|
||||
path: MediaPath = 'join_preview'
|
||||
) => {
|
||||
const failure = MediaDeviceFailure.getFailure(e)
|
||||
|
||||
if (failure === MediaDeviceFailure.PermissionDenied) {
|
||||
void classifyPermissionError(e, kind).then((scope) => {
|
||||
if (scope === 'system') {
|
||||
noteSystemPermissionDenied(kind)
|
||||
} else {
|
||||
notePermissionDeniedFromGum(kind)
|
||||
}
|
||||
captureMediaEvent('permissions-denied', {
|
||||
path,
|
||||
kind,
|
||||
denied_scope: scope,
|
||||
os: getOS(),
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (failure === MediaDeviceFailure.NotFound) {
|
||||
// Firefox reports OS-level blocks as NotFoundError (macOS privacy
|
||||
// settings, missing Android app permissions).
|
||||
void isLikelySystemNotFound(e, kind).then((system) => {
|
||||
if (system) {
|
||||
noteSystemPermissionDenied(kind)
|
||||
captureMediaEvent('permissions-denied', {
|
||||
path,
|
||||
kind,
|
||||
denied_scope: 'system',
|
||||
os: getOS(),
|
||||
})
|
||||
return
|
||||
}
|
||||
captureMediaEvent('device-not-found', { path, kind })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (failure === MediaDeviceFailure.DeviceInUse) {
|
||||
noteDeviceInUse(kind)
|
||||
void captureMediaEvent('device-in-use', { path, kind, os: getOS() })
|
||||
return
|
||||
}
|
||||
|
||||
// "Other" is still reported as an error.
|
||||
reportError(
|
||||
path === 'room' ? 'room_media_failure' : 'join_preview_failure',
|
||||
e,
|
||||
{ path, kind }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Silent availability check for a device that was reported "in use":
|
||||
* acquires and releases it without any error reporting, so it can be
|
||||
* polled. Clears the in-use flag on success.
|
||||
*/
|
||||
export const probeDeviceReleased = async (
|
||||
kind: PermissionKind
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia(
|
||||
kind === 'camera' ? { video: true } : { audio: true }
|
||||
)
|
||||
stream.getTracks().forEach((track) => track.stop())
|
||||
noteDeviceReady(kind)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers the browser permission prompt for one device kind by acquiring
|
||||
* and immediately releasing a track. Resolves to whether access was granted.
|
||||
*/
|
||||
export const requestDevicePermission = async (
|
||||
kind: 'audioinput' | 'videoinput',
|
||||
path: MediaPath = 'join_preview'
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const track =
|
||||
kind === 'audioinput'
|
||||
? await createLocalAudioTrack()
|
||||
: await createLocalVideoTrack()
|
||||
track.stop()
|
||||
noteDeviceReady(PERMISSION_KIND[kind])
|
||||
return true
|
||||
} catch (error) {
|
||||
onMediaPermissionError(error as Error, PERMISSION_KIND[kind], path)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { useConfig } from '@/api/useConfig.ts'
|
||||
import { LogLevel, setLogLevel } from 'livekit-client'
|
||||
import { useWatchDeviceAvailability } from '@/features/rooms/hooks/useWatchDeviceAvailability'
|
||||
import { useWatchDeviceReleased } from '@/features/rooms/hooks/useWatchDeviceReleased'
|
||||
import { useRoomPageTitle } from '@/features/rooms/livekit/hooks/useRoomPageTitle'
|
||||
|
||||
const BaseRoom = ({ children }: { children: ReactNode }) => {
|
||||
@@ -49,6 +50,7 @@ const Room = () => {
|
||||
|
||||
useKeyboardShortcuts()
|
||||
useWatchDeviceAvailability()
|
||||
useWatchDeviceReleased()
|
||||
|
||||
const clearRouterState = () => {
|
||||
if (window?.history?.state) {
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
"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"
|
||||
@@ -67,6 +71,7 @@
|
||||
},
|
||||
"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,6 +16,10 @@
|
||||
"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"
|
||||
@@ -67,6 +71,7 @@
|
||||
},
|
||||
"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,6 +16,10 @@
|
||||
"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"
|
||||
@@ -67,6 +71,7 @@
|
||||
},
|
||||
"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,6 +16,10 @@
|
||||
"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"
|
||||
@@ -67,6 +71,7 @@
|
||||
},
|
||||
"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,14 +1,39 @@
|
||||
import { proxy } from 'valtio'
|
||||
import { captureMediaEvent, reportError } from '@/features/analytics/telemetry'
|
||||
import type { PermissionKind } from './permissions'
|
||||
|
||||
// Device presence (not permission): enumerateDevices() exposes kinds
|
||||
// before any grant. Optimistic defaults until the first sync.
|
||||
// Device availability (not permission):
|
||||
// - presence: enumerateDevices() exposes kinds before any grant.
|
||||
// Optimistic defaults until the first sync.
|
||||
// - in use: the device exists and is allowed, but getUserMedia() failed
|
||||
// because another application or tab is holding it.
|
||||
export const deviceAvailabilityStore = proxy({
|
||||
hasCamera: true,
|
||||
hasMicrophone: true,
|
||||
cameraInUse: false,
|
||||
microphoneInUse: false,
|
||||
synced: false,
|
||||
})
|
||||
|
||||
const IN_USE_KEY: Record<PermissionKind, 'cameraInUse' | 'microphoneInUse'> = {
|
||||
camera: 'cameraInUse',
|
||||
microphone: 'microphoneInUse',
|
||||
}
|
||||
|
||||
const ALL_KINDS: PermissionKind[] = ['camera', 'microphone']
|
||||
|
||||
const setDeviceInUse = (inUse: boolean, kind?: PermissionKind) => {
|
||||
for (const k of kind ? [kind] : ALL_KINDS) {
|
||||
deviceAvailabilityStore[IN_USE_KEY[k]] = inUse
|
||||
}
|
||||
}
|
||||
|
||||
export const noteDeviceInUse = (kind?: PermissionKind) =>
|
||||
setDeviceInUse(true, kind)
|
||||
|
||||
export const clearDeviceInUse = (kind?: PermissionKind) =>
|
||||
setDeviceInUse(false, kind)
|
||||
|
||||
export const syncDeviceAvailability = async (): Promise<void> => {
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { proxy, ref } from 'valtio'
|
||||
import type { Participant } from 'livekit-client'
|
||||
|
||||
type State = {
|
||||
participant: Participant | null
|
||||
}
|
||||
|
||||
export const muteDialogStore = proxy<State>({
|
||||
participant: null,
|
||||
})
|
||||
|
||||
export const openMuteDialog = (participant: Participant) => {
|
||||
muteDialogStore.participant = ref(participant)
|
||||
}
|
||||
|
||||
export const closeMuteDialog = () => {
|
||||
muteDialogStore.participant = null
|
||||
}
|
||||
@@ -5,16 +5,26 @@ import { visualizer } from 'rollup-plugin-visualizer'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||
|
||||
const mediapipeVersion: string = JSON.parse(
|
||||
readFileSync(
|
||||
new URL(
|
||||
'./node_modules/@mediapipe/tasks-vision/package.json',
|
||||
import.meta.url
|
||||
),
|
||||
'utf-8'
|
||||
)
|
||||
const readPackageJson = (path: string) =>
|
||||
JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf-8'))
|
||||
|
||||
const mediapipeVersion: string = readPackageJson(
|
||||
'./node_modules/@mediapipe/tasks-vision/package.json'
|
||||
).version
|
||||
|
||||
const livekitMediapipeVersion: string = readPackageJson(
|
||||
'./node_modules/@livekit/track-processors/package.json'
|
||||
).dependencies['@mediapipe/tasks-vision']
|
||||
|
||||
if (mediapipeVersion !== livekitMediapipeVersion) {
|
||||
throw new Error(
|
||||
`@mediapipe/tasks-vision@${mediapipeVersion} is installed, but ` +
|
||||
`@livekit/track-processors declares "${livekitMediapipeVersion}". ` +
|
||||
`The two must stay in sync: pin "@mediapipe/tasks-vision" to ` +
|
||||
`"${livekitMediapipeVersion}" in package.json.`
|
||||
)
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
|
||||
Generated
+18
-8
@@ -9,7 +9,7 @@
|
||||
"version": "1.27.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.6.0",
|
||||
"@html-to/text-cli": "0.6.1",
|
||||
"mjml": "5.4.0"
|
||||
}
|
||||
},
|
||||
@@ -52,14 +52,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@html-to/text-cli": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@html-to/text-cli/-/text-cli-0.6.0.tgz",
|
||||
"integrity": "sha512-JwlrCBccUM/QkUpc37P+qG+hpkXRbWOVcHd9vM+5D+O82Ak2p8anGRxisr33//aJzlkYNKNg2I8LDh3Bx2tBPg==",
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@html-to/text-cli/-/text-cli-0.6.1.tgz",
|
||||
"integrity": "sha512-fnbLS8bra4BkGinXWzUKfalqLPYmz8E2ABT+TgHUZ4inhAUYF2rBEEctOKqZ6FgaJF1iZ//KBBLKVz6nm3RbhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@selderee/plugin-htmlparser2": "~0.12.0",
|
||||
"aspargvs": "~0.7.0",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"deepmerge-ts": "^8.0.1",
|
||||
"htmlparser2": "^10.1.0",
|
||||
"selderee": "~0.12.0"
|
||||
},
|
||||
@@ -656,9 +656,19 @@
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/deepmerge-ts": {
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-8.0.1.tgz",
|
||||
"integrity": "sha512-szCXE7YLCvLKR9bFPJcvsezOShdalctSvrgN/LM/QGUEPZQajwjmsMObZ6/DuANT5lxzM/wtO8Feubwdkz8myA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "ko-fi",
|
||||
"url": "https://ko-fi.com/rebeccastevens"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/deepmerge-ts"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.6.0",
|
||||
"@html-to/text-cli": "0.6.1",
|
||||
"mjml": "5.4.0"
|
||||
},
|
||||
"private": true,
|
||||
|
||||
@@ -83,6 +83,7 @@ class Settings(BaseSettings):
|
||||
aws_s3_access_key_id: str
|
||||
aws_s3_secret_access_key: SecretStr
|
||||
aws_s3_secure_access: bool = True
|
||||
aws_s3_region_name: str | None = None
|
||||
aws_transcript_path: str = "transcripts"
|
||||
aws_summary_path: str = "summaries"
|
||||
|
||||
|
||||
@@ -282,6 +282,7 @@ class FileService:
|
||||
access_key=settings.aws_s3_access_key_id,
|
||||
secret_key=settings.aws_s3_secret_access_key.get_secret_value(),
|
||||
secure=settings.aws_s3_secure_access,
|
||||
region=settings.aws_s3_region_name,
|
||||
)
|
||||
|
||||
self._bucket_name = settings.aws_storage_bucket_name
|
||||
|
||||
Reference in New Issue
Block a user