mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Compare commits
150 Commits
toger5/sho
...
hughns/mem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b31c7ec1e | ||
|
|
9b68539719 | ||
|
|
2c5b1de6b4 | ||
|
|
76a494464e | ||
|
|
486430d1f0 | ||
|
|
e6ddf40b1b | ||
|
|
9f521a79f7 | ||
|
|
83784a717a | ||
|
|
0729deee79 | ||
|
|
77c3114cf8 | ||
|
|
82a56c8204 | ||
|
|
b39896d8c6 | ||
|
|
79b3fdb645 | ||
|
|
0f877cd021 | ||
|
|
db2acc75b2 | ||
|
|
a5dbfbf2c1 | ||
|
|
34c7d02de2 | ||
|
|
ca45067158 | ||
|
|
5a6eb7c573 | ||
|
|
5ad2a27a92 | ||
|
|
68daaa45f9 | ||
|
|
c40ea35937 | ||
|
|
d27f433175 | ||
|
|
8a6101cd14 | ||
|
|
4db7c2bc68 | ||
|
|
18740fc686 | ||
|
|
0c39398493 | ||
|
|
949145f04b | ||
|
|
8578dcadf2 | ||
|
|
959db44eca | ||
|
|
a031c0e128 | ||
|
|
591833505f | ||
|
|
f7ad5074d8 | ||
|
|
e0aef74bf5 | ||
|
|
b2378bf899 | ||
|
|
255f6b1814 | ||
|
|
4c491b5363 | ||
|
|
61c808d4cf | ||
|
|
13ef3183e2 | ||
|
|
afd4fdcea2 | ||
|
|
982181ccd4 | ||
|
|
30629ebba2 | ||
|
|
7f6a32d21a | ||
|
|
320ade0a50 | ||
|
|
8c6fee3150 | ||
|
|
5c6acaf915 | ||
|
|
c46549b2b6 | ||
|
|
97a58f6db7 | ||
|
|
b6288579c9 | ||
|
|
44bf987cdc | ||
|
|
a7d55824bb | ||
|
|
8fa038c61f | ||
|
|
869d9b43cb | ||
|
|
974a2fe49b | ||
|
|
022497c8e5 | ||
|
|
207a5f047c | ||
|
|
d3c63f9314 | ||
|
|
f9ef037cea | ||
|
|
8666ffec81 | ||
|
|
4b85879891 | ||
|
|
f376291f50 | ||
|
|
f0f56bf101 | ||
|
|
ba647780e8 | ||
|
|
812ae2ce89 | ||
|
|
09ca3b4dc0 | ||
|
|
86afde8612 | ||
|
|
469f0b5983 | ||
|
|
69d1beaf28 | ||
|
|
7f22f442b1 | ||
|
|
ab0f8fa2e3 | ||
|
|
f5abbb1e5e | ||
|
|
a80c96d187 | ||
|
|
fb0c8fb92b | ||
|
|
438a6c2a42 | ||
|
|
0c8cd0842a | ||
|
|
b39d35d5d1 | ||
|
|
6f00a961c9 | ||
|
|
0ed7853958 | ||
|
|
4acf279f32 | ||
|
|
a7c065f300 | ||
|
|
a0d248065d | ||
|
|
c18008e039 | ||
|
|
0bab898c25 | ||
|
|
53d1f717c9 | ||
|
|
0a69664186 | ||
|
|
d1307e61b9 | ||
|
|
a267eca78d | ||
|
|
51a8d2b718 | ||
|
|
2d76a3780d | ||
|
|
e70818b2da | ||
|
|
03144783ac | ||
|
|
b9ab2fdf1b | ||
|
|
183a1845bb | ||
|
|
e423fc0ace | ||
|
|
40cbe5408e | ||
|
|
2c3e95f401 | ||
|
|
1fdff9bbd4 | ||
|
|
bd7209cd40 | ||
|
|
70fdc68b13 | ||
|
|
f9bed2c2a9 | ||
|
|
33437d9743 | ||
|
|
1f4139ae0a | ||
|
|
b8ad8baf9d | ||
|
|
694048dd7f | ||
|
|
a5088d4ae9 | ||
|
|
d2bdaf7049 | ||
|
|
0aeb68b445 | ||
|
|
f40740edd3 | ||
|
|
cd47b63d29 | ||
|
|
07ce272e9f | ||
|
|
a3c4e3e2a5 | ||
|
|
6b6ad16306 | ||
|
|
0f38445fdd | ||
|
|
b9b53ec251 | ||
|
|
b1a5c8c120 | ||
|
|
b44680149d | ||
|
|
cd0cec32b5 | ||
|
|
2faa9c9d50 | ||
|
|
caab45ee7f | ||
|
|
d3687298e0 | ||
|
|
42612476b8 | ||
|
|
7021ea6a5c | ||
|
|
ff09631546 | ||
|
|
28a3dfef23 | ||
|
|
2b5561a88c | ||
|
|
9dc740c2de | ||
|
|
0b93374e86 | ||
|
|
d36af0cae6 | ||
|
|
7fae106da2 | ||
|
|
a84b692f20 | ||
|
|
86a5c24750 | ||
|
|
ed6f02ac56 | ||
|
|
f701886aa9 | ||
|
|
d298e3438c | ||
|
|
c405b61c66 | ||
|
|
6be67aa145 | ||
|
|
603c658949 | ||
|
|
ebc33e003d | ||
|
|
023ab9fc47 | ||
|
|
417faf795d | ||
|
|
c8fe393fcf | ||
|
|
e6a9555a91 | ||
|
|
fd65baed58 | ||
|
|
d079bee5e0 | ||
|
|
1918478069 | ||
|
|
c6d8d5e137 | ||
|
|
7a4583dcb0 | ||
|
|
929175d826 | ||
|
|
49ce642c2d | ||
|
|
43f7f9b76c |
12
.github/workflows/docker.yaml
vendored
12
.github/workflows/docker.yaml
vendored
@@ -23,10 +23,10 @@ jobs:
|
||||
packages: write
|
||||
steps:
|
||||
- name: Check it out
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ inputs.artifact_run_id }}
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Log in to container registry
|
||||
uses: docker/login-action@7840e6ddd4a9223910798f6a315544257fccd96e
|
||||
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -42,16 +42,16 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@2ee3d3070bb41b40bf7305d15233321e12c1dc5c
|
||||
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: ${{ inputs.docker_tags}}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@be3701b2116d2f723573ca9e8cdb4ca85d3cdaf0
|
||||
uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@6003d3266a923de06063d23a19bd1c719af997c0
|
||||
uses: docker/build-push-action@1a162644f9a7e87d8f4b053101d1d9a712edc18c # v6.3.0
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
2
.github/workflows/e2e.yml
vendored
2
.github/workflows/e2e.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out test private repo
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
with:
|
||||
repository: element-hq/static-call-participant
|
||||
ref: refs/heads/main
|
||||
|
||||
7
.github/workflows/element-call.yaml
vendored
7
.github/workflows/element-call.yaml
vendored
@@ -21,11 +21,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: ".nvmrc"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
- name: Build
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
VITE_APP_VERSION: ${{ inputs.vite_app_version }}
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4
|
||||
with:
|
||||
name: build-output
|
||||
path: dist
|
||||
|
||||
5
.github/workflows/lint.yaml
vendored
5
.github/workflows/lint.yaml
vendored
@@ -7,11 +7,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: ".nvmrc"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
- name: Prettier
|
||||
|
||||
8
.github/workflows/netlify.yaml
vendored
8
.github/workflows/netlify.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
environment: Netlify
|
||||
steps:
|
||||
- name: 📝 Create Deployment
|
||||
uses: bobheadxi/deployments@v1
|
||||
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1
|
||||
id: deployment
|
||||
with:
|
||||
step: start
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
Exercise caution. Use test accounts.
|
||||
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4
|
||||
with:
|
||||
github-token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
run-id: ${{ inputs.artifact_run_id }}
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
|
||||
- name: ☁️ Deploy to Netlify
|
||||
id: netlify
|
||||
uses: nwtgck/actions-netlify@v3.0
|
||||
uses: nwtgck/actions-netlify@4cbaf4c08f1a7bfa537d6113472ef4424e4eb654 # v3.0
|
||||
with:
|
||||
publish-dir: webapp
|
||||
deploy-message: "Deploy from GitHub Actions"
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
timeout-minutes: 1
|
||||
|
||||
- name: 🚦 Update deployment status
|
||||
uses: bobheadxi/deployments@v1
|
||||
uses: bobheadxi/deployments@648679e8e4915b27893bd7dbc35cb504dc915bc8 # v1
|
||||
if: always()
|
||||
with:
|
||||
step: finish
|
||||
|
||||
2
.github/workflows/pr-deploy.yaml
vendored
2
.github/workflows/pr-deploy.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
pr_data_json: ${{ steps.prdetails.outputs.data }}
|
||||
steps:
|
||||
- id: prdetails
|
||||
uses: matrix-org/pr-details-action@v1.3
|
||||
uses: matrix-org/pr-details-action@15bde5285d7850ba276cc3bd8a03733e3f24622a # v1.3
|
||||
continue-on-error: true
|
||||
with:
|
||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||
|
||||
4
.github/workflows/publish.yaml
vendored
4
.github/workflows/publish.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
id: current-time
|
||||
run: echo "unix_time=$(date +'%s')" >> $GITHUB_OUTPUT
|
||||
- name: 📥 Download artifact
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id || github.run_id }}
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
run: |
|
||||
tar --numeric-owner --transform "s/dist/element-call-${TARBALL_VERSION}/" -cvzf element-call-${TARBALL_VERSION}.tar.gz dist
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@552bf3722c16e81001aea7db72d8cedf64eb5f68
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
|
||||
7
.github/workflows/test.yaml
vendored
7
.github/workflows/test.yaml
vendored
@@ -9,16 +9,17 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: ".nvmrc"
|
||||
- name: Install dependencies
|
||||
run: "yarn install"
|
||||
- name: Vitest
|
||||
run: "yarn run test"
|
||||
- name: Upload to codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
|
||||
with:
|
||||
flags: unittests
|
||||
|
||||
9
.github/workflows/translations-download.yaml
vendored
9
.github/workflows/translations-download.yaml
vendored
@@ -13,11 +13,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
|
||||
with:
|
||||
cache: "yarn"
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Install Deps
|
||||
run: "yarn install --frozen-lockfile"
|
||||
@@ -26,7 +27,7 @@ jobs:
|
||||
run: "rm -R public/locales"
|
||||
|
||||
- name: Download translation files
|
||||
uses: localazy/download@v1.1.0
|
||||
uses: localazy/download@0a79880fb66150601e3b43606fab69c88123c087 # v1.1.0
|
||||
with:
|
||||
groups: "-p includeSourceLang:true"
|
||||
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
uses: peter-evans/create-pull-request@v6.0.5
|
||||
uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0
|
||||
with:
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
branch: actions/localazy-download
|
||||
|
||||
4
.github/workflows/translations-upload.yaml
vendored
4
.github/workflows/translations-upload.yaml
vendored
@@ -14,9 +14,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
|
||||
- name: Upload
|
||||
uses: localazy/upload@v1
|
||||
uses: localazy/upload@27e6b5c0fddf4551596b42226b1c24124335d24a # v1
|
||||
with:
|
||||
write_key: ${{ secrets.LOCALAZY_WRITE_KEY }}
|
||||
|
||||
26
README.md
26
README.md
@@ -64,6 +64,28 @@ experimental_features:
|
||||
MSC3266 allows to request a room summary of rooms you are not joined.
|
||||
The summary contains the room join rules. We need that to decide if the user gets prompted with the option to knock ("ask to join"), a cannot join error or the join view.
|
||||
|
||||
Element Call requires a Livekit SFU behind a Livekit jwt service to work. The url to the Livekit jwt service can either be configured in the config of Element Call (fallback/legacy configuration) or be configured by your homeserver via the `.well-known`.
|
||||
This is the recommended method.
|
||||
|
||||
The configuration is a list of Foci configs:
|
||||
|
||||
```json
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://someurl.com"
|
||||
},
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://livekit2.com"
|
||||
},
|
||||
{
|
||||
"type": "another_foci",
|
||||
"props_for_another_foci": "val"
|
||||
},
|
||||
]
|
||||
```
|
||||
|
||||
## Translation
|
||||
|
||||
If you'd like to help translate Element Call, head over to [Localazy](https://localazy.com/p/element-call). You're also encouraged to join the [Element Translators](https://matrix.to/#/#translators:element.io) space to discuss and coordinate translation efforts.
|
||||
@@ -103,7 +125,9 @@ service for development. These use a test 'secret' published in this
|
||||
repository, so this must be used only for local development and
|
||||
**_never be exposed to the public Internet._**
|
||||
|
||||
To use it, add SFU parameter in your local config `./public/config.json`:
|
||||
To use it, add a SFU parameter in your local config `./public/config.json`:
|
||||
(Be aware, that this is only the fallback Livekit SFU. If the homeserver
|
||||
advertises one in the client well-known, this will not be used.)
|
||||
|
||||
```json
|
||||
"livekit": {
|
||||
|
||||
30
package.json
30
package.json
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "element-call",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -22,12 +23,13 @@
|
||||
"@livekit/components-core": "^0.10.0",
|
||||
"@livekit/components-react": "^2.0.0",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz",
|
||||
"@nkzw/use-relative-time": "^1.1.0",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@opentelemetry/context-zone": "^1.9.1",
|
||||
"@opentelemetry/exporter-jaeger": "^1.9.1",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-document-load": "^0.36.0",
|
||||
"@opentelemetry/instrumentation-user-interaction": "^0.36.0",
|
||||
"@opentelemetry/exporter-trace-otlp-http": "^0.52.0",
|
||||
"@opentelemetry/instrumentation-document-load": "^0.39.0",
|
||||
"@opentelemetry/instrumentation-user-interaction": "^0.39.0",
|
||||
"@opentelemetry/sdk-trace-web": "^1.9.1",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-slider": "^1.1.2",
|
||||
@@ -46,7 +48,7 @@
|
||||
"@react-stately/select": "^3.1.3",
|
||||
"@react-stately/tooltip": "^3.0.5",
|
||||
"@react-stately/tree": "^3.2.0",
|
||||
"@sentry/react": "^7.0.0",
|
||||
"@sentry/react": "^8.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@use-gesture/react": "^10.2.11",
|
||||
@@ -58,11 +60,11 @@
|
||||
"classnames": "^2.3.1",
|
||||
"events": "^3.3.0",
|
||||
"i18next": "^23.0.0",
|
||||
"i18next-browser-languagedetector": "^7.0.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"i18next-http-backend": "^2.0.0",
|
||||
"livekit-client": "^2.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#e874468ba3e84819cf4b342d2e66af67ab4cf804",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#c795c7eb656f2f6136ac4c6b17015c0f000df9a6",
|
||||
"matrix-widget-api": "^1.3.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"pako": "^2.0.4",
|
||||
@@ -76,9 +78,9 @@
|
||||
"react-use-measure": "^2.1.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sdp-transform": "^2.14.1",
|
||||
"tinyqueue": "^2.0.3",
|
||||
"tinyqueue": "^3.0.0",
|
||||
"unique-names-generator": "^4.6.0",
|
||||
"uuid": "9",
|
||||
"uuid": "10",
|
||||
"vaul": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -89,17 +91,19 @@
|
||||
"@react-spring/rafz": "^9.7.3",
|
||||
"@react-types/dialog": "^3.5.5",
|
||||
"@sentry/vite-plugin": "^2.0.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/dom": "^10.1.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@testing-library/user-event": "^14.5.1",
|
||||
"@types/content-type": "^1.1.5",
|
||||
"@types/dom-screen-wake-lock": "^1.0.1",
|
||||
"@types/dompurify": "^3.0.2",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/request": "^2.48.8",
|
||||
"@types/sdp-transform": "^2.4.5",
|
||||
"@types/uuid": "9",
|
||||
"@types/uuid": "10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"babel-loader": "^9.0.0",
|
||||
@@ -113,8 +117,8 @@
|
||||
"eslint-plugin-matrix-org": "^1.2.1",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"i18next-parser": "^8.0.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"i18next-parser": "^9.0.0",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^3.0.0",
|
||||
"sass": "^1.42.1",
|
||||
@@ -123,6 +127,6 @@
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-html-template": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.0.0",
|
||||
"vitest": "^1.2.2"
|
||||
"vitest": "^2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,42 @@
|
||||
"extends": ["config:base"],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Disable renoavte for packages we want to monitor ourselves",
|
||||
"matchPackagePatterns": ["matrix-js-sdk"],
|
||||
"extends": ["group:allNonMajor", "schedule:weekly"]
|
||||
},
|
||||
{
|
||||
"groupName": "GitHub Actions",
|
||||
"matchDepTypes": ["action"],
|
||||
"pinDigests": true,
|
||||
"extends": ["schedule:monthly"]
|
||||
},
|
||||
{
|
||||
"description": "Disable Renovate for packages we want to monitor ourselves",
|
||||
"groupName": "manually updated packages",
|
||||
"matchDepNames": ["matrix-js-sdk"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"groupName": "matrix-widget-api",
|
||||
"matchDepNames": ["matrix-widget-api"]
|
||||
},
|
||||
{
|
||||
"groupName": "Compound",
|
||||
"matchPackagePrefixes": ["@vector-im/compound-"],
|
||||
"schedule": "before 5am on Tuesday and Friday"
|
||||
},
|
||||
{
|
||||
"groupName": "LiveKit client",
|
||||
"matchDepNames": ["livekit-client"]
|
||||
},
|
||||
{
|
||||
"groupName": "LiveKit components",
|
||||
"matchPackagePrefixes": ["@livekit/components-"]
|
||||
},
|
||||
{
|
||||
"groupName": "Vaul",
|
||||
"matchDepNames": ["vaul"],
|
||||
"extends": ["schedule:monthly"],
|
||||
"prHeader": "Please review modals on mobile for visual regressions."
|
||||
}
|
||||
],
|
||||
"semanticCommits": "disabled",
|
||||
|
||||
6
src/@types/global.d.ts
vendored
6
src/@types/global.d.ts
vendored
@@ -28,12 +28,6 @@ declare global {
|
||||
OLM_OPTIONS: Record<string, string>;
|
||||
}
|
||||
|
||||
// TypeScript doesn't know about the experimental setSinkId method, so we
|
||||
// declare it ourselves
|
||||
interface MediaElement extends HTMLVideoElement {
|
||||
setSinkId: (id: string) => void;
|
||||
}
|
||||
|
||||
interface HTMLElement {
|
||||
// Safari only supports this prefixed, so tell the type system about it
|
||||
webkitRequestFullscreen: () => void;
|
||||
|
||||
@@ -25,7 +25,11 @@ import {
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import {
|
||||
ClientEvent,
|
||||
ICreateClientOpts,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/client";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
@@ -317,7 +321,7 @@ export const ClientProvider: FC<Props> = ({ children }) => {
|
||||
initClientState.client.on(ClientEvent.Sync, onSync);
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
if (initClientState.client) {
|
||||
initClientState.client.removeListener(ClientEvent.Sync, onSync);
|
||||
}
|
||||
@@ -360,13 +364,13 @@ async function loadClient(): Promise<InitResult | null> {
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
const { user_id, device_id, access_token, passwordlessUser } = session;
|
||||
const initClientParams = {
|
||||
const initClientParams: ICreateClientOpts = {
|
||||
baseUrl: Config.defaultHomeserverUrl()!,
|
||||
accessToken: access_token,
|
||||
userId: user_id,
|
||||
deviceId: device_id,
|
||||
fallbackICEServerAllowed: fallbackICEServerAllowed,
|
||||
livekitServiceURL: Config.get().livekit!.livekit_service_url,
|
||||
livekitServiceURL: Config.get().livekit?.livekit_service_url,
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
@@ -109,6 +109,7 @@ limitations under the License.
|
||||
|
||||
.participantsLine {
|
||||
grid-area: participants;
|
||||
color: var(--cpd-color-text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--cpd-space-1-5x);
|
||||
|
||||
@@ -76,7 +76,7 @@ export const Toast: FC<Props> = ({
|
||||
useEffect(() => {
|
||||
if (open && autoDismiss !== undefined) {
|
||||
const timeout = setTimeout(onDismiss, autoDismiss);
|
||||
return () => clearTimeout(timeout);
|
||||
return (): void => clearTimeout(timeout);
|
||||
}
|
||||
}, [open, autoDismiss, onDismiss]);
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ limitations under the License.
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
createClient,
|
||||
LoginResponse,
|
||||
MatrixClient,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { initClient } from "../matrix-utils";
|
||||
import { Session } from "../ClientContext";
|
||||
@@ -37,7 +41,7 @@ export function useInteractiveLogin(): (
|
||||
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient,
|
||||
doRequest: () =>
|
||||
doRequest: (): Promise<LoginResponse> =>
|
||||
authClient.login("m.login.password", {
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
|
||||
@@ -16,7 +16,11 @@ limitations under the License.
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
|
||||
import { createClient, MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||
import {
|
||||
createClient,
|
||||
MatrixClient,
|
||||
RegisterResponse,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { initClient } from "../matrix-utils";
|
||||
import { Session } from "../ClientContext";
|
||||
@@ -69,7 +73,7 @@ export const useInteractiveRegistration = (): {
|
||||
): Promise<[MatrixClient, Session]> => {
|
||||
const interactiveAuth = new InteractiveAuth({
|
||||
matrixClient: authClient.current!,
|
||||
doRequest: (auth) =>
|
||||
doRequest: (auth): Promise<RegisterResponse> =>
|
||||
authClient.current!.registerRequest({
|
||||
username,
|
||||
password,
|
||||
|
||||
@@ -55,16 +55,31 @@ export interface ConfigOptions {
|
||||
|
||||
// Describes the LiveKit configuration to be used.
|
||||
livekit?: {
|
||||
// The link to the service that returns a livekit url and token to use it
|
||||
// The link to the service that returns a livekit url and token to use it.
|
||||
// This is a fallback link in case the homeserver in use does not advertise
|
||||
// a livekit service url in the client well-known.
|
||||
// The well known needs to be formatted like so:
|
||||
// {"type":"livekit", "livekit_service_url":"https://livekit.example.com"}
|
||||
// and stored under the key: "livekit_focus"
|
||||
livekit_service_url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Allow to join a group calls without audio and video.
|
||||
* TEMPORARY: Is a feature that's not proved and experimental
|
||||
* TEMPORARY experimental features.
|
||||
*/
|
||||
features?: {
|
||||
feature_group_calls_without_video_and_audio: boolean;
|
||||
/**
|
||||
* Allow to join group calls without audio and video.
|
||||
*/
|
||||
feature_group_calls_without_video_and_audio?: boolean;
|
||||
/**
|
||||
* Send device-specific call session membership state events instead of
|
||||
* legacy user-specific call membership state events.
|
||||
* This setting has no effect when the user joins an active call with
|
||||
* legacy state events. For compatibility, Element Call will always join
|
||||
* active legacy calls with legacy state events.
|
||||
*/
|
||||
feature_use_device_session_member_events?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -166,7 +166,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
|
||||
updateRooms,
|
||||
);
|
||||
client.on(RoomEvent.MyMembership, updateRooms);
|
||||
return () => {
|
||||
return (): void => {
|
||||
client.matrixRTC.off(
|
||||
MatrixRTCSessionManagerEvents.SessionStarted,
|
||||
updateRooms,
|
||||
|
||||
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { BrowserTracing } from "@sentry/browser";
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
@@ -160,10 +159,7 @@ export class Initializer {
|
||||
dsn: Config.get().sentry?.DSN,
|
||||
environment: Config.get().sentry?.environment,
|
||||
integrations: [
|
||||
new BrowserTracing({
|
||||
routingInstrumentation:
|
||||
Sentry.reactRouterV5Instrumentation(history),
|
||||
}),
|
||||
Sentry.reactRouterV5BrowserTracingIntegration({ history }),
|
||||
],
|
||||
tracesSampleRate: 1.0,
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
Copyright 2023 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Focus } from "matrix-js-sdk/src/matrixrtc/focus";
|
||||
|
||||
export interface LivekitFocus extends Focus {
|
||||
type: "livekit";
|
||||
livekit_service_url: string;
|
||||
livekit_alias: string;
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ function useObservableState<T>(
|
||||
// observable state doesn't run in SSR
|
||||
if (typeof window === "undefined" || !observable) return;
|
||||
const subscription = observable.subscribe(setState);
|
||||
return () => subscription.unsubscribe();
|
||||
return (): void => subscription.unsubscribe();
|
||||
}, [observable]);
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ import { IOpenIDToken, MatrixClient } from "matrix-js-sdk";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LivekitFocus } from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
|
||||
|
||||
import { LivekitFocus } from "./LivekitFocus";
|
||||
import { useActiveFocus } from "../room/useActiveFocus";
|
||||
import { useActiveLivekitFocus } from "../room/useActiveFocus";
|
||||
|
||||
export interface SFUConfig {
|
||||
url: string;
|
||||
@@ -46,7 +46,7 @@ export function useOpenIDSFU(
|
||||
): SFUConfig | undefined {
|
||||
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);
|
||||
|
||||
const activeFocus = useActiveFocus(rtcSession);
|
||||
const activeFocus = useActiveLivekitFocus(rtcSession);
|
||||
|
||||
useEffect(() => {
|
||||
(async (): Promise<void> => {
|
||||
|
||||
@@ -192,7 +192,7 @@ export function useECConnectionState(
|
||||
livekitRoom.on(RoomEvent.ConnectionStateChanged, onConnStateChanged);
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
if (oldRoom)
|
||||
oldRoom.off(RoomEvent.ConnectionStateChanged, onConnStateChanged);
|
||||
};
|
||||
|
||||
@@ -55,7 +55,13 @@ const SYNC_STORE_NAME = "element-call-sync";
|
||||
// (It's a good opportunity to make the database names consistent.)
|
||||
const CRYPTO_STORE_NAME = "element-call-crypto";
|
||||
|
||||
function waitForSync(client: MatrixClient): Promise<void> {
|
||||
async function waitForSync(client: MatrixClient): Promise<void> {
|
||||
// If there is a saved sync, the client will fire an additional sync event
|
||||
// for restoring it before it runs the first network sync.
|
||||
// However, the sync we want to wait for is the network sync,
|
||||
// as the saved sync may be missing some state.
|
||||
// Thus, don't resolve on the first sync when we know it's for the saved sync.
|
||||
let waitForSavedSync = !!(await client.store.getSavedSyncToken());
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const onSync = (
|
||||
state: SyncState,
|
||||
@@ -63,8 +69,12 @@ function waitForSync(client: MatrixClient): Promise<void> {
|
||||
data?: ISyncStateData,
|
||||
): void => {
|
||||
if (state === "PREPARED") {
|
||||
client.removeListener(ClientEvent.Sync, onSync);
|
||||
resolve();
|
||||
if (waitForSavedSync) {
|
||||
waitForSavedSync = false;
|
||||
} else {
|
||||
client.removeListener(ClientEvent.Sync, onSync);
|
||||
resolve();
|
||||
}
|
||||
} else if (state === "ERROR") {
|
||||
client.removeListener(ClientEvent.Sync, onSync);
|
||||
reject(data?.error);
|
||||
@@ -190,8 +200,14 @@ export async function initClient(
|
||||
|
||||
await client.initCrypto();
|
||||
client.setGlobalErrorOnUnknownDevices(false);
|
||||
await client.startClient();
|
||||
await waitForSync(client);
|
||||
// Once startClient is called, syncs are run asynchronously.
|
||||
// Also, sync completion is communicated only via events.
|
||||
// So, apply the event listener *before* starting the client.
|
||||
// Otherwise, a sync may complete before the listener gets applied,
|
||||
// and we will miss it.
|
||||
const syncPromise = waitForSync(client);
|
||||
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
|
||||
await syncPromise;
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
SummaryStatsReport,
|
||||
CallFeedReport,
|
||||
} from "matrix-js-sdk/src/webrtc/stats/statsReport";
|
||||
import { setSpan } from "@opentelemetry/api/build/esm/trace/context-utils";
|
||||
|
||||
import { ElementCallOpenTelemetry } from "./otel";
|
||||
import { ObjectFlattener } from "./ObjectFlattener";
|
||||
@@ -446,7 +445,7 @@ export class OTelGroupCallMembership {
|
||||
const type = OTelStatsReportType.SummaryReport;
|
||||
const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport);
|
||||
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
|
||||
const ctx = setSpan(
|
||||
const ctx = opentelemetry.trace.setSpan(
|
||||
opentelemetry.context.active(),
|
||||
this.callMembershipSpan,
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function useProfile(client: MatrixClient | undefined): UseProfile {
|
||||
user?.on(UserEvent.AvatarUrl, onChangeUser);
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
if (user) {
|
||||
user.removeListener(UserEvent.DisplayName, onChangeUser);
|
||||
user.removeListener(UserEvent.AvatarUrl, onChangeUser);
|
||||
|
||||
@@ -94,7 +94,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
|
||||
useEffect(() => {
|
||||
window.rtcSession = rtcSession;
|
||||
return () => {
|
||||
return (): void => {
|
||||
delete window.rtcSession;
|
||||
};
|
||||
}, [rtcSession]);
|
||||
@@ -196,11 +196,11 @@ export const GroupCallView: FC<Props> = ({
|
||||
ev: CustomEvent<IWidgetApiRequest>,
|
||||
): Promise<void> => {
|
||||
defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
|
||||
enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||
await widget!.api.transport.reply(ev.detail, {});
|
||||
};
|
||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||
return () => {
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||
};
|
||||
} else if (widget && !preload && skipLobby) {
|
||||
@@ -255,7 +255,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
await leaveRTCSession(rtcSession);
|
||||
};
|
||||
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||
return () => {
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||
};
|
||||
}
|
||||
@@ -318,7 +318,7 @@ export const GroupCallView: FC<Props> = ({
|
||||
client={client}
|
||||
matrixInfo={matrixInfo}
|
||||
muteStates={muteStates}
|
||||
onEnter={(): void => enterRTCSession(rtcSession, perParticipantE2EE)}
|
||||
onEnter={() => void enterRTCSession(rtcSession, perParticipantE2EE)}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={hideHeader}
|
||||
participantCount={participantCount}
|
||||
|
||||
@@ -85,13 +85,13 @@ limitations under the License.
|
||||
|
||||
@media (min-height: 400px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-10x);
|
||||
padding-block: var(--cpd-space-8x);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 800px) {
|
||||
.footer {
|
||||
padding-block: var(--cpd-space-15x);
|
||||
padding-block: var(--cpd-space-10x);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
return (): void => {
|
||||
livekitRoom?.disconnect();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -220,7 +220,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
onSpotlightLayout,
|
||||
);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
widget!.lazyActions.off(
|
||||
ElementWidgetActions.TileLayout,
|
||||
onTileLayout,
|
||||
@@ -238,7 +238,7 @@ export const InCallView: FC<InCallViewProps> = subscribe(
|
||||
const noControls = reducedControls && bounds.height <= 400;
|
||||
|
||||
const vm = useCallViewModel(
|
||||
rtcSession.room,
|
||||
rtcSession,
|
||||
livekitRoom,
|
||||
matrixInfo.e2eeSystem.kind !== E2eeType.NONE,
|
||||
connState,
|
||||
|
||||
@@ -110,7 +110,7 @@ export const VideoPreview: FC<Props> = ({
|
||||
if (videoEl.current) {
|
||||
videoTrack?.attach(videoEl.current);
|
||||
}
|
||||
return () => {
|
||||
return (): void => {
|
||||
videoTrack?.detach();
|
||||
};
|
||||
}, [videoTrack]);
|
||||
|
||||
@@ -21,33 +21,28 @@ import {
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { deepCompare } from "matrix-js-sdk/src/utils";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { LivekitFocus } from "../livekit/LivekitFocus";
|
||||
|
||||
function getActiveFocus(
|
||||
rtcSession: MatrixRTCSession,
|
||||
): LivekitFocus | undefined {
|
||||
const oldestMembership = rtcSession.getOldestMembership();
|
||||
const focus = oldestMembership?.getActiveFoci()[0] as LivekitFocus;
|
||||
|
||||
return focus;
|
||||
}
|
||||
import {
|
||||
LivekitFocus,
|
||||
isLivekitFocus,
|
||||
} from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
|
||||
|
||||
/**
|
||||
* Gets the currently active (livekit) focus for a MatrixRTC session
|
||||
* This logic is specific to livekit foci where the whole call must use one
|
||||
* and the same focus.
|
||||
*/
|
||||
export function useActiveFocus(
|
||||
export function useActiveLivekitFocus(
|
||||
rtcSession: MatrixRTCSession,
|
||||
): LivekitFocus | undefined {
|
||||
const [activeFocus, setActiveFocus] = useState(() =>
|
||||
getActiveFocus(rtcSession),
|
||||
);
|
||||
const [activeFocus, setActiveFocus] = useState(() => {
|
||||
const f = rtcSession.getActiveFocus();
|
||||
// Only handle foci with type="livekit" for now.
|
||||
return !!f && isLivekitFocus(f) ? f : undefined;
|
||||
});
|
||||
|
||||
const onMembershipsChanged = useCallback(() => {
|
||||
const newActiveFocus = getActiveFocus(rtcSession);
|
||||
|
||||
const newActiveFocus = rtcSession.getActiveFocus();
|
||||
if (!!newActiveFocus && !isLivekitFocus(newActiveFocus)) return;
|
||||
if (!deepCompare(activeFocus, newActiveFocus)) {
|
||||
const oldestMembership = rtcSession.getOldestMembership();
|
||||
logger.warn(
|
||||
@@ -64,7 +59,7 @@ export function useActiveFocus(
|
||||
onMembershipsChanged,
|
||||
);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
rtcSession.off(
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
onMembershipsChanged,
|
||||
|
||||
@@ -217,8 +217,6 @@ export const useLoadGroupCall = (
|
||||
"Room not found. The widget-api did not pass over the relevant room events/information.",
|
||||
);
|
||||
|
||||
// If the room does not exist we first search for it with viaServers
|
||||
const roomSummary = await client.getRoomSummary(roomId, viaServers);
|
||||
if (membership === KnownMembership.Ban) {
|
||||
throw bannedError();
|
||||
} else if (membership === KnownMembership.Invite) {
|
||||
@@ -226,15 +224,39 @@ export const useLoadGroupCall = (
|
||||
viaServers,
|
||||
});
|
||||
} else {
|
||||
if (roomSummary.join_rule === JoinRule.Public) {
|
||||
room = await client.joinRoom(roomSummary.room_id, {
|
||||
// If the room does not exist we first search for it with viaServers
|
||||
let roomSummary: RoomSummary | undefined = undefined;
|
||||
try {
|
||||
roomSummary = await client.getRoomSummary(roomId, viaServers);
|
||||
} catch (error) {
|
||||
// If the room summary endpoint is not supported we let it be undefined and treat this case like
|
||||
// `JoinRule.Public`.
|
||||
// This is how the logic was done before: "we expect any room id passed to EC
|
||||
// to be for a public call" Which is definitely not ideal but worth a try if fetching
|
||||
// the summary crashes.
|
||||
logger.warn(
|
||||
`Could not load room summary to decide whether we want to join or knock.
|
||||
EC will fallback to join as if this would be a public room.
|
||||
Reach out to your homeserver admin to ask them about supporting the \`/summary\` endpoint (im.nheko.summary):`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
if (
|
||||
roomSummary === undefined ||
|
||||
roomSummary.join_rule === JoinRule.Public
|
||||
) {
|
||||
room = await client.joinRoom(roomId, {
|
||||
viaServers,
|
||||
});
|
||||
} else if (roomSummary.join_rule === JoinRule.Knock) {
|
||||
// bind room summary in this scope so we have it stored in a binding of type `RoomSummary`
|
||||
// instead of `RoomSummary | undefined`. Because we use it in a promise the linter does not accept
|
||||
// the type check from the if condition above.
|
||||
const _roomSummary = roomSummary;
|
||||
let knock: () => void = () => {};
|
||||
const userPressedAskToJoinPromise: Promise<void> = new Promise(
|
||||
(resolve) => {
|
||||
if (roomSummary.membership !== KnownMembership.Knock) {
|
||||
if (_roomSummary.membership !== KnownMembership.Knock) {
|
||||
knock = resolve;
|
||||
} else {
|
||||
// resolve immediately if the user already knocked
|
||||
@@ -242,12 +264,13 @@ export const useLoadGroupCall = (
|
||||
}
|
||||
},
|
||||
);
|
||||
setState({ kind: "canKnock", roomSummary, knock });
|
||||
setState({ kind: "canKnock", roomSummary: _roomSummary, knock });
|
||||
await userPressedAskToJoinPromise;
|
||||
room = await getRoomByKnocking(
|
||||
roomSummary.room_id,
|
||||
viaServers,
|
||||
() => setState({ kind: "waitForInvite", roomSummary }),
|
||||
() =>
|
||||
setState({ kind: "waitForInvite", roomSummary: _roomSummary }),
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
|
||||
@@ -50,7 +50,7 @@ export function usePageUnload(callback: () => void): void {
|
||||
// @ts-ignore
|
||||
window.addEventListener("beforeunload", onBeforeUnload);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
window.removeEventListener("pagehide", onBeforeUnload);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
@@ -15,29 +15,90 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import {
|
||||
LivekitFocus,
|
||||
LivekitFocusActive,
|
||||
isLivekitFocus,
|
||||
isLivekitFocusConfig,
|
||||
} from "matrix-js-sdk/src/matrixrtc/LivekitFocus";
|
||||
|
||||
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||
import { LivekitFocus } from "./livekit/LivekitFocus";
|
||||
import { Config } from "./config/Config";
|
||||
import { ElementWidgetActions, WidgetHelpers, widget } from "./widget";
|
||||
|
||||
function makeFocus(livekitAlias: string): LivekitFocus {
|
||||
const urlFromConf = Config.get().livekit!.livekit_service_url;
|
||||
if (!urlFromConf) {
|
||||
throw new Error("No livekit_service_url is configured!");
|
||||
}
|
||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
||||
|
||||
export function makeActiveFocus(): LivekitFocusActive {
|
||||
return {
|
||||
type: "livekit",
|
||||
livekit_service_url: urlFromConf,
|
||||
livekit_alias: livekitAlias,
|
||||
focus_selection: "oldest_membership",
|
||||
};
|
||||
}
|
||||
|
||||
export function enterRTCSession(
|
||||
async function makePreferredLivekitFoci(
|
||||
rtcSession: MatrixRTCSession,
|
||||
livekitAlias: string,
|
||||
): Promise<LivekitFocus[]> {
|
||||
logger.log("Start building foci_preferred list: ", rtcSession.room.roomId);
|
||||
|
||||
const preferredFoci: LivekitFocus[] = [];
|
||||
|
||||
// Make the Focus from the running rtc session the highest priority one
|
||||
// This minimizes how often we need to switch foci during a call.
|
||||
const focusInUse = rtcSession.getFocusInUse();
|
||||
if (focusInUse && isLivekitFocus(focusInUse)) {
|
||||
logger.log("Adding livekit focus from oldest member: ", focusInUse);
|
||||
preferredFoci.push(focusInUse);
|
||||
}
|
||||
|
||||
// Prioritize the client well known over the configured sfu.
|
||||
const wellKnownFoci =
|
||||
rtcSession.room.client.getClientWellKnown()?.[FOCI_WK_KEY];
|
||||
if (Array.isArray(wellKnownFoci)) {
|
||||
preferredFoci.push(
|
||||
...wellKnownFoci
|
||||
.filter((f) => !!f)
|
||||
.filter(isLivekitFocusConfig)
|
||||
.map((wellKnownFocus) => {
|
||||
logger.log("Adding livekit focus from well known: ", wellKnownFocus);
|
||||
return { ...wellKnownFocus, livekit_alias: livekitAlias };
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const urlFromConf = Config.get().livekit?.livekit_service_url;
|
||||
if (urlFromConf) {
|
||||
const focusFormConf: LivekitFocus = {
|
||||
type: "livekit",
|
||||
livekit_service_url: urlFromConf,
|
||||
livekit_alias: livekitAlias,
|
||||
};
|
||||
logger.log("Adding livekit focus from config: ", focusFormConf);
|
||||
preferredFoci.push(focusFormConf);
|
||||
}
|
||||
|
||||
if (preferredFoci.length === 0)
|
||||
throw new Error(
|
||||
`No livekit_service_url is configured so we could not create a focus.
|
||||
Currently we skip computing a focus based on other users in the room.`,
|
||||
);
|
||||
|
||||
return preferredFoci;
|
||||
|
||||
// TODO: we want to do something like this:
|
||||
//
|
||||
// const focusOtherMembers = await focusFromOtherMembers(
|
||||
// rtcSession,
|
||||
// livekitAlias,
|
||||
// );
|
||||
// if (focusOtherMembers) preferredFoci.push(focusOtherMembers);
|
||||
}
|
||||
|
||||
export async function enterRTCSession(
|
||||
rtcSession: MatrixRTCSession,
|
||||
encryptMedia: boolean,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||
|
||||
@@ -47,8 +108,18 @@ export function enterRTCSession(
|
||||
|
||||
// right now we assume everything is a room-scoped call
|
||||
const livekitAlias = rtcSession.room.roomId;
|
||||
|
||||
rtcSession.joinRoomSession([makeFocus(livekitAlias)], encryptMedia);
|
||||
const useDeviceSessionMemberEvents =
|
||||
Config.get().features?.feature_use_device_session_member_events;
|
||||
rtcSession.joinRoomSession(
|
||||
await makePreferredLivekitFoci(rtcSession, livekitAlias),
|
||||
makeActiveFocus(),
|
||||
{
|
||||
manageMediaKeys: encryptMedia,
|
||||
...(useDeviceSessionMemberEvents !== undefined && {
|
||||
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
||||
}),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const widgetPostHangupProcedure = async (
|
||||
|
||||
@@ -47,7 +47,7 @@ export const ProfileSettingsTab: FC<Props> = ({ client }) => {
|
||||
useEffect(() => {
|
||||
const form = formRef.current!;
|
||||
// Auto-save when the user dismisses this component
|
||||
return () => {
|
||||
return (): void => {
|
||||
if (formChanged.current) {
|
||||
const data = new FormData(form);
|
||||
const displayNameDataEntry = data.get("displayName");
|
||||
|
||||
@@ -334,7 +334,7 @@ export function useRageshakeRequestModal(
|
||||
|
||||
client.on(ClientEvent.Event, onEvent);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
client.removeListener(ClientEvent.Event, onEvent);
|
||||
};
|
||||
}, [setOpen, roomId, client]);
|
||||
|
||||
@@ -52,6 +52,8 @@ import {
|
||||
} from "rxjs";
|
||||
import { StateObservable, state } from "@react-rxjs/core";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
import { CallMembership } from "matrix-js-sdk/src/matrixrtc/CallMembership";
|
||||
|
||||
import { ViewModel } from "./ViewModel";
|
||||
import { useObservable } from "./useObservable";
|
||||
@@ -64,6 +66,7 @@ import {
|
||||
MediaViewModel,
|
||||
UserMediaViewModel,
|
||||
ScreenShareViewModel,
|
||||
MembershipOnlyViewModel,
|
||||
} from "./MediaViewModel";
|
||||
import { finalizeValue } from "../observable-utils";
|
||||
import { ObservableScope } from "./ObservableScope";
|
||||
@@ -204,25 +207,45 @@ class ScreenShare {
|
||||
}
|
||||
}
|
||||
|
||||
type MediaItem = UserMedia | ScreenShare;
|
||||
class MembershipOnly {
|
||||
public readonly vm: MembershipOnlyViewModel;
|
||||
|
||||
function findMatrixMember(
|
||||
room: MatrixRoom,
|
||||
id: string,
|
||||
): RoomMember | undefined {
|
||||
public constructor(member: RoomMember) {
|
||||
this.vm = new MembershipOnlyViewModel(member);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.vm.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
type MediaItem = UserMedia | ScreenShare | MembershipOnly;
|
||||
|
||||
function matrixUserIdFromParticipantId(id: string): string | undefined {
|
||||
if (!id) return undefined;
|
||||
|
||||
const parts = id.split(":");
|
||||
// must be at least 3 parts because we know the first part is a userId which must necessarily contain a colon
|
||||
if (parts.length < 3) {
|
||||
logger.warn(
|
||||
"Livekit participants ID doesn't look like a userId:deviceId combination",
|
||||
`Livekit participants ID doesn't look like a userId:deviceId combination: ${id}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
parts.pop();
|
||||
const userId = parts.join(":");
|
||||
return userId;
|
||||
}
|
||||
|
||||
function findMatrixMember(
|
||||
room: MatrixRoom,
|
||||
id: string,
|
||||
): RoomMember | undefined {
|
||||
const userId = matrixUserIdFromParticipantId(id);
|
||||
|
||||
if (!userId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return room.getMember(userId) ?? undefined;
|
||||
}
|
||||
@@ -304,6 +327,24 @@ export class CallViewModel extends ViewModel {
|
||||
},
|
||||
);
|
||||
|
||||
private readonly membershipsWithoutParticipant = combineLatest([
|
||||
of(this.rtcSession.memberships),
|
||||
this.remoteParticipants,
|
||||
of(this.livekitRoom.localParticipant),
|
||||
]).pipe(
|
||||
scan((prev, [memberships, remoteParticipants, localParticipant]) => {
|
||||
const participantIds = new Set(
|
||||
remoteParticipants.map((p) =>
|
||||
matrixUserIdFromParticipantId(p.identity),
|
||||
),
|
||||
);
|
||||
participantIds.add(
|
||||
matrixUserIdFromParticipantId(localParticipant.identity),
|
||||
);
|
||||
return memberships.filter((m) => !participantIds.has(m.sender ?? ""));
|
||||
}, [] as CallMembership[]),
|
||||
);
|
||||
|
||||
private readonly mediaItems: StateObservable<MediaItem[]> = state(
|
||||
combineLatest([
|
||||
this.remoteParticipants,
|
||||
@@ -495,90 +536,136 @@ export class CallViewModel extends ViewModel {
|
||||
combineLatest([
|
||||
this.remoteParticipants,
|
||||
observeParticipantMedia(this.livekitRoom.localParticipant),
|
||||
this.membershipsWithoutParticipant,
|
||||
]).pipe(
|
||||
scan((ts, [remoteParticipants, { participant: localParticipant }]) => {
|
||||
const ps = [localParticipant, ...remoteParticipants];
|
||||
const tilesById = new Map(ts.map((t) => [t.id, t]));
|
||||
const now = Date.now();
|
||||
let allGhosts = true;
|
||||
scan(
|
||||
(
|
||||
ts,
|
||||
[
|
||||
remoteParticipants,
|
||||
{ participant: localParticipant },
|
||||
membershipsWithoutParticipant,
|
||||
],
|
||||
) => {
|
||||
const ps = [
|
||||
localParticipant,
|
||||
...remoteParticipants,
|
||||
...membershipsWithoutParticipant,
|
||||
];
|
||||
const tilesById = new Map(ts.map((t) => [t.id, t]));
|
||||
const now = Date.now();
|
||||
let allGhosts = true;
|
||||
|
||||
const newTiles = ps.flatMap((p) => {
|
||||
const userMediaId = p.identity;
|
||||
const member = findMatrixMember(this.matrixRoom, userMediaId);
|
||||
allGhosts &&= member === undefined;
|
||||
const spokeRecently =
|
||||
p.lastSpokeAt !== undefined && now - +p.lastSpokeAt <= 10000;
|
||||
const newTiles = ps.flatMap((p) => {
|
||||
if (p instanceof CallMembership) {
|
||||
const userId = p.sender ?? "";
|
||||
const member = this.matrixRoom.getMember(userId);
|
||||
if (!member) {
|
||||
logger.warn(
|
||||
`Ruh, roh! No matrix member found for call membership '${userId}': ignoring`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
const membershipOnlyVm =
|
||||
tilesById.get(userId)?.data ??
|
||||
new MembershipOnlyViewModel(member);
|
||||
tilesById.delete(userId);
|
||||
|
||||
// We always start with a local participant with the empty string as
|
||||
// their ID before we're connected, this is fine and we'll be in
|
||||
// "all ghosts" mode.
|
||||
if (userMediaId !== "" && member === undefined) {
|
||||
logger.warn(
|
||||
`Ruh, roh! No matrix member found for SFU participant '${userMediaId}': creating g-g-g-ghost!`,
|
||||
);
|
||||
}
|
||||
const membershipOnlyTile: TileDescriptor<MediaViewModel> = {
|
||||
id: userId,
|
||||
focused: false,
|
||||
isPresenter: false,
|
||||
isSpeaker: false,
|
||||
hasVideo: false,
|
||||
local: false,
|
||||
largeBaseSize: false,
|
||||
data: membershipOnlyVm,
|
||||
};
|
||||
return [membershipOnlyTile];
|
||||
}
|
||||
|
||||
const userMediaVm =
|
||||
tilesById.get(userMediaId)?.data ??
|
||||
new UserMediaViewModel(userMediaId, member, p, this.encrypted);
|
||||
tilesById.delete(userMediaId);
|
||||
const userMediaId = p.identity;
|
||||
const member = findMatrixMember(this.matrixRoom, userMediaId);
|
||||
allGhosts &&= member === undefined;
|
||||
const spokeRecently =
|
||||
p.lastSpokeAt !== undefined && now - +p.lastSpokeAt <= 10000;
|
||||
|
||||
const userMediaTile: TileDescriptor<MediaViewModel> = {
|
||||
id: userMediaId,
|
||||
focused: false,
|
||||
isPresenter: p.isScreenShareEnabled,
|
||||
isSpeaker: (p.isSpeaking || spokeRecently) && !p.isLocal,
|
||||
hasVideo: p.isCameraEnabled,
|
||||
local: p.isLocal,
|
||||
largeBaseSize: false,
|
||||
data: userMediaVm,
|
||||
};
|
||||
|
||||
if (p.isScreenShareEnabled) {
|
||||
const screenShareId = `${userMediaId}:screen-share`;
|
||||
const screenShareVm =
|
||||
tilesById.get(screenShareId)?.data ??
|
||||
new ScreenShareViewModel(
|
||||
screenShareId,
|
||||
member,
|
||||
p,
|
||||
this.encrypted,
|
||||
// We always start with a local participant with the empty string as
|
||||
// their ID before we're connected, this is fine and we'll be in
|
||||
// "all ghosts" mode.
|
||||
if (userMediaId !== "" && member === undefined) {
|
||||
logger.warn(
|
||||
`Ruh, roh! No matrix member found for SFU participant '${userMediaId}': creating g-g-g-ghost!`,
|
||||
);
|
||||
tilesById.delete(screenShareId);
|
||||
}
|
||||
|
||||
const screenShareTile: TileDescriptor<MediaViewModel> = {
|
||||
id: screenShareId,
|
||||
focused: true,
|
||||
isPresenter: false,
|
||||
isSpeaker: false,
|
||||
hasVideo: true,
|
||||
const userMediaVm =
|
||||
tilesById.get(userMediaId)?.data ??
|
||||
new UserMediaViewModel(userMediaId, member, p, this.encrypted);
|
||||
tilesById.delete(userMediaId);
|
||||
|
||||
const userMediaTile: TileDescriptor<MediaViewModel> = {
|
||||
id: userMediaId,
|
||||
focused: false,
|
||||
isPresenter: p.isScreenShareEnabled,
|
||||
isSpeaker: (p.isSpeaking || spokeRecently) && !p.isLocal,
|
||||
hasVideo: p.isCameraEnabled,
|
||||
local: p.isLocal,
|
||||
largeBaseSize: true,
|
||||
placeNear: userMediaId,
|
||||
data: screenShareVm,
|
||||
largeBaseSize: false,
|
||||
data: userMediaVm,
|
||||
};
|
||||
return [userMediaTile, screenShareTile];
|
||||
} else {
|
||||
return [userMediaTile];
|
||||
}
|
||||
});
|
||||
|
||||
// Any tiles left in the map are unused and should be destroyed
|
||||
for (const t of tilesById.values()) t.data.destroy();
|
||||
if (p.isScreenShareEnabled) {
|
||||
const screenShareId = `${userMediaId}:screen-share`;
|
||||
const screenShareVm =
|
||||
tilesById.get(screenShareId)?.data ??
|
||||
new ScreenShareViewModel(
|
||||
screenShareId,
|
||||
member,
|
||||
p,
|
||||
this.encrypted,
|
||||
);
|
||||
tilesById.delete(screenShareId);
|
||||
|
||||
// If every item is a ghost, that probably means we're still connecting
|
||||
// and shouldn't bother showing anything yet
|
||||
return allGhosts ? [] : newTiles;
|
||||
}, [] as TileDescriptor<MediaViewModel>[]),
|
||||
const screenShareTile: TileDescriptor<MediaViewModel> = {
|
||||
id: screenShareId,
|
||||
focused: true,
|
||||
isPresenter: false,
|
||||
isSpeaker: false,
|
||||
hasVideo: true,
|
||||
local: p.isLocal,
|
||||
largeBaseSize: true,
|
||||
placeNear: userMediaId,
|
||||
data: screenShareVm,
|
||||
};
|
||||
return [userMediaTile, screenShareTile];
|
||||
} else {
|
||||
return [userMediaTile];
|
||||
}
|
||||
});
|
||||
|
||||
// Any tiles left in the map are unused and should be destroyed
|
||||
for (const t of tilesById.values()) t.data.destroy();
|
||||
|
||||
// If every item is a ghost, that probably means we're still connecting
|
||||
// and shouldn't bother showing anything yet
|
||||
return allGhosts ? [] : newTiles;
|
||||
},
|
||||
[] as TileDescriptor<MediaViewModel>[],
|
||||
),
|
||||
finalizeValue((ts) => {
|
||||
for (const t of ts) t.data.destroy();
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
private get matrixRoom(): MatrixRoom {
|
||||
return this.rtcSession.room;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
// A call is permanently tied to a single Matrix room and LiveKit room
|
||||
private readonly matrixRoom: MatrixRoom,
|
||||
private readonly rtcSession: MatrixRTCSession,
|
||||
private readonly livekitRoom: LivekitRoom,
|
||||
private readonly encrypted: boolean,
|
||||
private readonly connectionState: Observable<ECConnectionState>,
|
||||
@@ -588,25 +675,25 @@ export class CallViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
export function useCallViewModel(
|
||||
matrixRoom: MatrixRoom,
|
||||
rtcSession: MatrixRTCSession,
|
||||
livekitRoom: LivekitRoom,
|
||||
encrypted: boolean,
|
||||
connectionState: ECConnectionState,
|
||||
): CallViewModel {
|
||||
const prevMatrixRoom = usePrevious(matrixRoom);
|
||||
const prevRTCSession = usePrevious(rtcSession);
|
||||
const prevLivekitRoom = usePrevious(livekitRoom);
|
||||
const prevEncrypted = usePrevious(encrypted);
|
||||
const connectionStateObservable = useObservable(connectionState);
|
||||
|
||||
const vm = useRef<CallViewModel>();
|
||||
if (
|
||||
matrixRoom !== prevMatrixRoom ||
|
||||
rtcSession !== prevRTCSession ||
|
||||
livekitRoom !== prevLivekitRoom ||
|
||||
encrypted !== prevEncrypted
|
||||
) {
|
||||
vm.current?.destroy();
|
||||
vm.current = new CallViewModel(
|
||||
matrixRoom,
|
||||
rtcSession,
|
||||
livekitRoom,
|
||||
encrypted,
|
||||
connectionStateObservable,
|
||||
|
||||
@@ -110,7 +110,10 @@ abstract class BaseMediaViewModel extends ViewModel {
|
||||
/**
|
||||
* Some participant's media.
|
||||
*/
|
||||
export type MediaViewModel = UserMediaViewModel | ScreenShareViewModel;
|
||||
export type MediaViewModel =
|
||||
| UserMediaViewModel
|
||||
| ScreenShareViewModel
|
||||
| MembershipOnlyViewModel;
|
||||
|
||||
/**
|
||||
* Some participant's user media.
|
||||
@@ -239,3 +242,15 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for a call membership that does not have a LiveKit participant associated with it.
|
||||
*/
|
||||
export class MembershipOnlyViewModel extends ViewModel {
|
||||
public id: string;
|
||||
public local = false;
|
||||
public constructor(public readonly member: RoomMember) {
|
||||
super();
|
||||
this.id = member.userId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useEventTarget<T extends Event>(
|
||||
useEffect(() => {
|
||||
if (target) {
|
||||
target.addEventListener(eventType, listener as EventListener, options);
|
||||
return () =>
|
||||
return (): void =>
|
||||
target.removeEventListener(
|
||||
eventType,
|
||||
listener as EventListener,
|
||||
@@ -55,7 +55,7 @@ export function useTypedEventEmitter<
|
||||
): void {
|
||||
useEffect(() => {
|
||||
emitter.on(eventType, listener);
|
||||
return () => {
|
||||
return (): void => {
|
||||
emitter.off(eventType, listener);
|
||||
};
|
||||
}, [emitter, eventType, listener]);
|
||||
@@ -72,7 +72,7 @@ export function useEventEmitterThree<
|
||||
): void {
|
||||
useEffect(() => {
|
||||
emitter.on(eventType, listener);
|
||||
return () => {
|
||||
return (): void => {
|
||||
emitter.off(eventType, listener);
|
||||
};
|
||||
}, [emitter, eventType, listener]);
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useLocalStorage = (
|
||||
|
||||
useEffect(() => {
|
||||
localStorageBus.on(key, setValue);
|
||||
return () => {
|
||||
return (): void => {
|
||||
localStorageBus.off(key, setValue);
|
||||
};
|
||||
}, [key, setValue]);
|
||||
|
||||
@@ -34,7 +34,7 @@ export function useLocationNavigation(enabled = false): void {
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (unblock) {
|
||||
|
||||
@@ -26,19 +26,22 @@ export function useMatrixRTCSessionJoinState(
|
||||
): boolean {
|
||||
const [isJoined, setJoined] = useState(rtcSession.isJoined());
|
||||
|
||||
const onJoinStateChanged = useCallback(() => {
|
||||
logger.info(
|
||||
`Session in room ${rtcSession.room.roomId} changed to ${
|
||||
rtcSession.isJoined() ? "joined" : "left"
|
||||
}`,
|
||||
);
|
||||
setJoined(rtcSession.isJoined());
|
||||
}, [rtcSession]);
|
||||
const onJoinStateChanged = useCallback(
|
||||
(isJoined: boolean) => {
|
||||
logger.info(
|
||||
`Session in room ${rtcSession.room.roomId} changed to ${
|
||||
isJoined ? "joined" : "left"
|
||||
}`,
|
||||
);
|
||||
setJoined(isJoined);
|
||||
},
|
||||
[rtcSession],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
rtcSession.off(
|
||||
MatrixRTCSessionEvent.JoinStateChanged,
|
||||
onJoinStateChanged,
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useMatrixRTCSessionMemberships(
|
||||
onMembershipsChanged,
|
||||
);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
rtcSession.off(
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
onMembershipsChanged,
|
||||
|
||||
@@ -27,8 +27,8 @@ export function useWakeLock(): void {
|
||||
let lock: WakeLockSentinel | null = null;
|
||||
|
||||
// The lock is automatically released whenever the window goes invisible,
|
||||
// so we need to reacquire it on visiblity changes
|
||||
const onVisiblityChange = async (): Promise<void> => {
|
||||
// so we need to reacquire it on visibility changes
|
||||
const onVisibilityChange = async (): Promise<void> => {
|
||||
if (document.visibilityState === "visible") {
|
||||
try {
|
||||
lock = await navigator.wakeLock.request("screen");
|
||||
@@ -44,16 +44,16 @@ export function useWakeLock(): void {
|
||||
}
|
||||
};
|
||||
|
||||
onVisiblityChange();
|
||||
document.addEventListener("visiblitychange", onVisiblityChange);
|
||||
onVisibilityChange();
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
mounted = false;
|
||||
if (lock !== null)
|
||||
lock
|
||||
.release()
|
||||
.catch((e) => logger.warn("Can't release wake lock", e));
|
||||
document.removeEventListener("visiblitychange", onVisiblityChange);
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
VideoGridProps as Props,
|
||||
TileSpring,
|
||||
ChildrenProperties,
|
||||
TileSpringUpdate,
|
||||
} from "./VideoGrid";
|
||||
import { useReactiveState } from "../useReactiveState";
|
||||
import { useMergedRefs } from "../useMergedRefs";
|
||||
@@ -110,7 +111,7 @@ export function NewVideoGrid<T>({
|
||||
});
|
||||
|
||||
observer.observe(slotsRoot, { attributes: true });
|
||||
return () => observer.disconnect();
|
||||
return (): void => observer.disconnect();
|
||||
}
|
||||
}, [slotsRoot, setRenderedGeneration]);
|
||||
|
||||
@@ -174,8 +175,8 @@ export function NewVideoGrid<T>({
|
||||
const [tileTransitions, springRef] = useTransition(
|
||||
tiles,
|
||||
() => ({
|
||||
key: ({ item }: Tile<T>) => item.id,
|
||||
from: ({ x, y, width, height }: Tile<T>) => ({
|
||||
key: ({ item }: Tile<T>): string => item.id,
|
||||
from: ({ x, y, width, height }: Tile<T>): TileSpringUpdate => ({
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
shadow: 0,
|
||||
@@ -188,7 +189,13 @@ export function NewVideoGrid<T>({
|
||||
immediate: disableAnimations,
|
||||
}),
|
||||
enter: { opacity: 1, scale: 1, immediate: disableAnimations },
|
||||
update: ({ item, x, y, width, height }: Tile<T>) =>
|
||||
update: ({
|
||||
item,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}: Tile<T>): TileSpringUpdate | null =>
|
||||
item.id === dragState.current?.tileId
|
||||
? null
|
||||
: {
|
||||
@@ -230,7 +237,7 @@ export function NewVideoGrid<T>({
|
||||
disableAnimations || ((key): boolean => key === "zIndex"),
|
||||
// Allow the tile's position to settle before pushing its
|
||||
// z-index back down
|
||||
delay: (key) => (key === "zIndex" ? 500 : 0),
|
||||
delay: (key): number => (key === "zIndex" ? 500 : 0),
|
||||
}
|
||||
: {
|
||||
scale: 1.1,
|
||||
|
||||
@@ -77,6 +77,13 @@ export interface TileSpring {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface TileSpringUpdate extends Partial<TileSpring> {
|
||||
from?: Partial<TileSpring>;
|
||||
reset?: boolean;
|
||||
immediate?: boolean | ((key: string) => boolean);
|
||||
delay?: (key: string) => number;
|
||||
}
|
||||
|
||||
type LayoutDirection = "vertical" | "horizontal";
|
||||
|
||||
export function useVideoGridLayout(hasScreenshareFeeds: boolean): {
|
||||
@@ -120,7 +127,7 @@ function useIsMounted(): MutableRefObject<boolean> {
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
return (): void => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
@@ -1026,7 +1033,7 @@ export function VideoGrid<T>({
|
||||
const oneOnOneLayout =
|
||||
tiles.length === 2 && !tiles.some((t) => t.focused);
|
||||
|
||||
return (tileIndex: number) => {
|
||||
return (tileIndex: number): TileSpringUpdate => {
|
||||
const tile = tiles[tileIndex];
|
||||
const tilePosition = tilePositions[tile.order];
|
||||
const draggingTile = draggingTileRef.current;
|
||||
@@ -1045,7 +1052,7 @@ export function VideoGrid<T>({
|
||||
zIndex: 2,
|
||||
shadow: 15,
|
||||
shadowSpread: 0,
|
||||
immediate: (key: string) =>
|
||||
immediate: (key: string): boolean =>
|
||||
disableAnimations ||
|
||||
key === "zIndex" ||
|
||||
key === "x" ||
|
||||
@@ -1112,14 +1119,14 @@ export function VideoGrid<T>({
|
||||
shadowSpread: oneOnOneLayout && tile.item.local ? 1 : 0,
|
||||
from,
|
||||
reset,
|
||||
immediate: (key: string) =>
|
||||
immediate: (key: string): boolean =>
|
||||
disableAnimations ||
|
||||
key === "zIndex" ||
|
||||
key === "shadow" ||
|
||||
key === "shadowSpread",
|
||||
// If we just stopped dragging a tile, give it time for the
|
||||
// animation to settle before pushing its z-index back down
|
||||
delay: (key: string) => (key === "zIndex" ? 500 : 0),
|
||||
delay: (key: string): number => (key === "zIndex" ? 500 : 0),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
Menu,
|
||||
} from "@vector-im/compound-web";
|
||||
import { useStateObservable } from "@react-rxjs/core";
|
||||
import useRelativeTime from "@nkzw/use-relative-time";
|
||||
|
||||
import { Avatar } from "../Avatar";
|
||||
import styles from "./VideoTile.module.css";
|
||||
@@ -61,6 +62,7 @@ import {
|
||||
ScreenShareViewModel,
|
||||
MediaViewModel,
|
||||
UserMediaViewModel,
|
||||
MembershipOnlyViewModel,
|
||||
} from "../state/MediaViewModel";
|
||||
import { subscribe } from "../state/subscribe";
|
||||
import { useMergedRefs } from "../useMergedRefs";
|
||||
@@ -72,7 +74,7 @@ interface TileProps {
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
video: TrackReferenceOrPlaceholder;
|
||||
video?: TrackReferenceOrPlaceholder;
|
||||
member: RoomMember | undefined;
|
||||
videoEnabled: boolean;
|
||||
maximised: boolean;
|
||||
@@ -110,6 +112,29 @@ const Tile = forwardRef<HTMLDivElement, TileProps>(
|
||||
const { t } = useTranslation();
|
||||
const mergedRef = useMergedRefs(tileRef, ref);
|
||||
|
||||
const [lastActiveTime, setLastActiveTime] = useState(
|
||||
member?.getLastTypingTime() ?? 0,
|
||||
);
|
||||
const [online, setOnline] = useState(member?.typing ?? false);
|
||||
|
||||
useEffect(() => {
|
||||
const onTyping = (): void => {
|
||||
setOnline(member?.typing ?? false);
|
||||
setLastActiveTime(member?.getLastTypingTime() ?? 0);
|
||||
};
|
||||
|
||||
member?.on(RoomMemberEvent.Typing, onTyping);
|
||||
|
||||
return (): void => {
|
||||
member?.off(RoomMemberEvent.Typing, onTyping);
|
||||
};
|
||||
}, [member]);
|
||||
|
||||
const joinedCallTime = member?.events.member?.getTs() ?? 0;
|
||||
|
||||
const lastActiveAgo = useRelativeTime(lastActiveTime ?? 0);
|
||||
const joinedCallAgo = useRelativeTime(joinedCallTime ?? 0);
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
className={classNames(styles.videoTile, className, {
|
||||
@@ -129,7 +154,7 @@ const Tile = forwardRef<HTMLDivElement, TileProps>(
|
||||
src={member?.getMxcAvatarUrl()}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
{video.publication !== undefined && (
|
||||
{video?.publication !== undefined && (
|
||||
<VideoTrack
|
||||
trackRef={video}
|
||||
// There's no reason for this to be focusable
|
||||
@@ -137,12 +162,38 @@ const Tile = forwardRef<HTMLDivElement, TileProps>(
|
||||
disablePictureInPicture
|
||||
/>
|
||||
)}
|
||||
{!video && member && joinedCallTime > 0 && (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<span>
|
||||
{nameTag} ({member.userId})
|
||||
</span>{" "}
|
||||
joined the call{" "}
|
||||
<span title={new Date(joinedCallTime).toLocaleString()}>
|
||||
{joinedCallAgo}
|
||||
</span>{" "}
|
||||
but is currently unreachable.
|
||||
{lastActiveTime > 0 && (
|
||||
<span>
|
||||
They were last reachable{" "}
|
||||
<span title={new Date(lastActiveTime).toLocaleString()}>
|
||||
{lastActiveAgo}
|
||||
</span>
|
||||
.
|
||||
</span>
|
||||
)}{" "}
|
||||
Are they having connection problems?
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.fg}>
|
||||
<div className={styles.nameTag}>
|
||||
{nameTagLeadingIcon}
|
||||
<Text as="span" size="sm" weight="medium" className={styles.name}>
|
||||
{nameTag}
|
||||
<span
|
||||
title={`${member?.userId}${lastActiveTime > 0 ? ` last reachable ${online ? "now" : lastActiveAgo}` : ""}`}
|
||||
>
|
||||
{nameTag}
|
||||
</span>{" "}
|
||||
</Text>
|
||||
{unencryptedWarning && (
|
||||
<Tooltip
|
||||
@@ -402,6 +453,75 @@ const ScreenShareTile = subscribe<ScreenShareTileProps, HTMLDivElement>(
|
||||
|
||||
ScreenShareTile.displayName = "ScreenShareTile";
|
||||
|
||||
interface MembershipOnlyTileProps {
|
||||
vm: MembershipOnlyViewModel;
|
||||
className?: string;
|
||||
style?: ComponentProps<typeof animated.div>["style"];
|
||||
targetWidth: number;
|
||||
targetHeight: number;
|
||||
nameTag: string;
|
||||
displayName: string;
|
||||
maximised: boolean;
|
||||
fullscreen: boolean;
|
||||
onToggleFullscreen: (itemId: string) => void;
|
||||
}
|
||||
|
||||
const MembershipOnlyTile = subscribe<MembershipOnlyTileProps, HTMLDivElement>(
|
||||
(
|
||||
{
|
||||
vm,
|
||||
className,
|
||||
style,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
nameTag,
|
||||
displayName,
|
||||
maximised,
|
||||
fullscreen,
|
||||
onToggleFullscreen,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const onClickFullScreen = useCallback(
|
||||
() => onToggleFullscreen(vm.id),
|
||||
[onToggleFullscreen, vm],
|
||||
);
|
||||
|
||||
const FullScreenIcon = fullscreen ? CollapseIcon : ExpandIcon;
|
||||
|
||||
return (
|
||||
<Tile
|
||||
ref={ref}
|
||||
className={classNames(className, styles.membershipOnly)}
|
||||
style={style}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
member={vm.member}
|
||||
videoEnabled={true}
|
||||
maximised={maximised}
|
||||
unencryptedWarning={false}
|
||||
nameTag={nameTag}
|
||||
displayName={displayName}
|
||||
primaryButton={
|
||||
<button
|
||||
aria-label={
|
||||
fullscreen
|
||||
? t("video_tile.full_screen")
|
||||
: t("video_tile.exit_full_screen")
|
||||
}
|
||||
onClick={onClickFullScreen}
|
||||
>
|
||||
<FullScreenIcon aria-hidden width={20} height={20} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
MembershipOnlyTile.displayName = "MembershipOnlyTile";
|
||||
|
||||
interface Props {
|
||||
vm: MediaViewModel;
|
||||
maximised: boolean;
|
||||
@@ -471,7 +591,7 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
showSpeakingIndicator={showSpeakingIndicator}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
} else if (vm instanceof ScreenShareViewModel) {
|
||||
return (
|
||||
<ScreenShareTile
|
||||
ref={ref}
|
||||
@@ -487,6 +607,22 @@ export const VideoTile = forwardRef<HTMLDivElement, Props>(
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
/>
|
||||
);
|
||||
} else if (vm instanceof MembershipOnlyViewModel) {
|
||||
return (
|
||||
<MembershipOnlyTile
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={style}
|
||||
vm={vm}
|
||||
targetWidth={targetWidth}
|
||||
targetHeight={targetHeight}
|
||||
nameTag={nameTag}
|
||||
displayName={displayName}
|
||||
maximised={maximised}
|
||||
fullscreen={fullscreen}
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -77,8 +77,6 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
logger.info("Widget API is available");
|
||||
const api = new WidgetApi(widgetId, parentOrigin);
|
||||
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
||||
api.requestCapabilityToSendEvent(EventType.CallEncryptionKeysPrefix);
|
||||
api.requestCapabilityToReceiveEvent(EventType.CallEncryptionKeysPrefix);
|
||||
|
||||
// Set up the lazy action emitter, but only for select actions that we
|
||||
// intend for the app to handle
|
||||
@@ -116,14 +114,23 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
if (!baseUrl) throw new Error("Base URL must be supplied");
|
||||
|
||||
// These are all the event types the app uses
|
||||
const sendRecvEvent = ["org.matrix.rageshake_request"];
|
||||
const sendState = [
|
||||
{ eventType: EventType.GroupCallMemberPrefix, stateKey: userId },
|
||||
const sendRecvEvent = [
|
||||
"org.matrix.rageshake_request",
|
||||
EventType.CallEncryptionKeysPrefix,
|
||||
];
|
||||
|
||||
const sendState = [
|
||||
userId, // legacy call membership events
|
||||
`_${userId}_${deviceId}`, // session membership events
|
||||
`${userId}_${deviceId}`, // MSC3779 session membership events
|
||||
].map((stateKey) => ({
|
||||
eventType: EventType.GroupCallMemberPrefix,
|
||||
stateKey,
|
||||
}));
|
||||
const receiveState = [
|
||||
{ eventType: EventType.RoomCreate },
|
||||
{ eventType: EventType.RoomMember },
|
||||
{ eventType: EventType.RoomEncryption },
|
||||
{ eventType: EventType.GroupCallPrefix },
|
||||
{ eventType: EventType.GroupCallMemberPrefix },
|
||||
];
|
||||
const sendRecvToDevice = [
|
||||
@@ -168,7 +175,7 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
// wait for the config file to be ready (we load very early on so it might not
|
||||
// be otherwise)
|
||||
await Config.init();
|
||||
await client.startClient();
|
||||
await client.startClient({ clientWellKnownPollPeriod: 60 * 10 });
|
||||
resolve(client);
|
||||
})();
|
||||
});
|
||||
|
||||
93
test/rtcSessionHelper-test.ts
Normal file
93
test/rtcSessionHelper-test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||
|
||||
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
||||
import { Config } from "../src/config/Config";
|
||||
|
||||
test("It joins the correct Session", async () => {
|
||||
const focusFromOlderMembership = {
|
||||
type: "livekit",
|
||||
livekit_service_url: "http://my-oldest-member-service-url.com",
|
||||
livekit_alias: "my-oldest-member-service-alias",
|
||||
};
|
||||
|
||||
const focusConfigFromWellKnown = {
|
||||
type: "livekit",
|
||||
livekit_service_url: "http://my-well-known-service-url.com",
|
||||
};
|
||||
const focusConfigFromWellKnown2 = {
|
||||
type: "livekit",
|
||||
livekit_service_url: "http://my-well-known-service-url2.com",
|
||||
};
|
||||
const clientWellKnown = {
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
focusConfigFromWellKnown,
|
||||
focusConfigFromWellKnown2,
|
||||
],
|
||||
};
|
||||
|
||||
vi.spyOn(Config, "get").mockReturnValue({
|
||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||
eula: "",
|
||||
});
|
||||
const mockedSession = vi.mocked({
|
||||
room: {
|
||||
roomId: "roomId",
|
||||
client: {
|
||||
getClientWellKnown: vi.fn().mockReturnValue(clientWellKnown),
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership),
|
||||
getOldestMembership: vi.fn().mockReturnValue({
|
||||
getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]),
|
||||
}),
|
||||
joinRoomSession: vi.fn(),
|
||||
}) as unknown as MatrixRTCSession;
|
||||
await enterRTCSession(mockedSession, false);
|
||||
|
||||
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
||||
[
|
||||
{
|
||||
livekit_alias: "my-oldest-member-service-alias",
|
||||
livekit_service_url: "http://my-oldest-member-service-url.com",
|
||||
type: "livekit",
|
||||
},
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-well-known-service-url.com",
|
||||
type: "livekit",
|
||||
},
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-well-known-service-url2.com",
|
||||
type: "livekit",
|
||||
},
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-default-service-url.com",
|
||||
type: "livekit",
|
||||
},
|
||||
],
|
||||
{
|
||||
focus_selection: "oldest_membership",
|
||||
type: "livekit",
|
||||
},
|
||||
{ manageMediaKeys: false },
|
||||
);
|
||||
});
|
||||
@@ -22,6 +22,9 @@
|
||||
"livekit-client/dist/src/room/Room": [
|
||||
"./node_modules/livekit-client/dist/src/room/Room.d.ts"
|
||||
],
|
||||
"livekit-client/dist/src/room/track/TrackPublication": [
|
||||
"./node_modules/livekit-client/dist/src/room/track/TrackPublication.d.ts"
|
||||
],
|
||||
"livekit-client/dist/src/room/participant/Participant": [
|
||||
"./node_modules/livekit-client/dist/src/room/participant/Participant.d.ts"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user