Compare commits

...

38 Commits

Author SHA1 Message Date
Дамян Минков
54161a828c Tests PR testing do not merge
Added a new line for formatting in the README.
2025-11-30 11:55:06 -07:00
Saúl Ibarra Corretgé
9013881f76 chore(ci) clean Android build to save space
Add git clean command to CI workflow for Android.
2025-11-27 12:45:54 +01:00
dependabot[bot]
b6e7e0a19e chore(ci): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 12:21:16 +01:00
dependabot[bot]
ae42e42534 chore(ci): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 11:17:58 +01:00
Saúl Ibarra Corretgé
21e2504cf9 chore(ci) add Dependabot configuration for GitHub Actions 2025-11-27 09:55:51 +01:00
dependabot[bot]
7a9ba79783 chore(deps): bump node-forge from 1.3.1 to 1.3.2
Bumps [node-forge](https://github.com/digitalbazaar/forge) from 1.3.1 to 1.3.2.
- [Changelog](https://github.com/digitalbazaar/forge/blob/main/CHANGELOG.md)
- [Commits](https://github.com/digitalbazaar/forge/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: node-forge
  dependency-version: 1.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 08:51:30 +01:00
Damien Fetis
1f5a3b5b0f fix(recording): allow samesite iframe embeds to work with local recording
* fix(recording): allow samesite iframe embeds to work with local recording

Skip capture handle validation when inside an iframe to ensure local
recording works. This only applies if the iframe is served from the
same domain.

* fix(recording): add missing line breaks for better readability in LocalRecordingManager
2025-11-26 07:35:23 -07:00
bgrozev
fe2aff4f3c chore(deps) lib-jitsi-meet@latest (#16706)
https://github.com/jitsi/lib-jitsi-meet/compare/v2114.0.0+0e62818c...v2115.0.0+cc2f34c2
2025-11-25 11:57:53 -06:00
bgrozev
d847f6f96b feat: Accept transcription messages from non-participant entities. (#16631) 2025-11-25 11:06:16 -06:00
damencho
45ce467dcd feat(polls): Fixes support for breakout rooms.
Fixes #16693.
2025-11-25 09:30:10 -06:00
bgrozev
2b81fa6bd3 config(webpack): Listen on localhost by default. (#16703) 2025-11-25 08:42:19 -06:00
damencho
6f6100ceb2 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2113.0.0+ffcffaa7...v2114.0.0+0e62818c
2025-11-21 05:56:46 -06:00
Calin-Teodor
62cd1c29d7 align react native docker version with package json version 2025-11-21 13:45:53 +02:00
Дамян Минков
64869e8970 fix(deb): Adds Include on upgrade prosody. (#16687)
* fix(deb): Adds Include on upgrade prosody.

* squash: Restart if config has changed.
2025-11-20 16:13:29 -06:00
Saúl Ibarra Corretgé
29464e6886 Revert "chore(deps-dev): bump @react-native-community/cli from 15.0.1 to 17.0.1"
This reverts commit 421b21edeb.
2025-11-20 13:45:05 +01:00
Saúl Ibarra Corretgé
5ed92f2bc5 fix(deps) use Olm from npm
The Matrix GitLab repo was behind CF and thus affected by today's
outage.

Since they released the last Olm version to npm, let's consume that one.
2025-11-20 13:01:52 +01:00
srijan
048d12de24 feat(logging): replace console.* with centralized logger infrastructure (#16655)
* feat(logging): replace console.* with centralized logger infrastructure

* fix(logging): remove logger from size-constrained bundles
2025-11-19 18:31:35 -06:00
emrah
40c240c7ca fix(lang): add the missing translation (German) 2025-11-18 17:00:51 -06:00
Boris Grozev
289c1907e7 test: Skip iframe tests when the API is disabled. 2025-11-18 17:00:21 -06:00
damencho
35adea48ae fix(muc_rate_limit): Check connection when processing rate limited events.
If it happens that a connection was closed during waiting in the rate limited queue, we want to ignore those occupant events.
2025-11-18 14:24:26 -06:00
bgrozev
d72114d5bc test: Expect pin to have digits only, configure length. (#16670) 2025-11-18 12:00:06 -06:00
bgrozev
2f6b6ca837 fix: Fix transcription test expectation. (#16664) 2025-11-18 07:45:07 -06:00
bgrozev
615bbdc39b test: Order attachments by participant. (#16663) 2025-11-17 16:12:58 -06:00
bgrozev
ef97778158 test: Assert jaas visitors enabled. (#16662) 2025-11-17 16:12:45 -06:00
bgrozev
2885f39355 More test expectations (#16661)
* test: Add iframe API expectation.

* test: Add expectations for recording and live streaming.

* test: Remove iframe references from jaas/.

* test: Add a transcription expectation.
2025-11-17 16:12:34 -06:00
damencho
ae256b23b8 fix(cleanup_backend): Avoids cleanup when breakout rooms are active. 2025-11-17 12:56:00 -06:00
Дамян Минков
412aa83268 feat(jwt): Supports JWKS endpoint. (#16649)
* feat(jwt): Supports JWKS endpoint.

* squash: Allow setting just cache_keys_url.
2025-11-17 09:48:28 -06:00
damencho
f4c61e4760 fix(prosody): Order room-destroyed event.
Make sure we execute before prosody cleans it up from the list of room. If we try to look it up after that we will not find it. If we also add at 0 we cannot guarantee the order of hook execution.
2025-11-14 14:01:12 -06:00
Jaya Allamsetty
f313fb81d0 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2109.0.0+cb9d000c...v2113.0.0+ffcffaa7
2025-11-13 21:25:10 -05:00
Srijan
975af80e27 fix(chat): remove debug console.log statements from resize handlers 2025-11-13 16:52:24 -06:00
damencho
0a30a51bab feat(localstorage): Filter items. 2025-11-13 11:02:34 -06:00
Дамян Минков
54e28e223c fix(tests): Split participants presence. (#16642)
* fix(tests): Split participants presence.

* squash: Drop unused listener.
2025-11-12 11:42:43 -06:00
Edgars Voroboks
a4def96763 fix(lang): Update Latvian language translation 2025-11-12 11:42:29 -06:00
Hristo Terezov
dad4fb9e06 Revert "fix(large-video): Prevents unnecessary updates when container is hidden"
This reverts commit 6deb0a6385.
2025-11-11 12:48:16 -06:00
Vishal Malyan
3772b9a5ae feat(toolbar): implement toolbar background color via configOverwrite for web and mobile
* Change toolbar background color from IFrame API #16468 fixed

* fix(toolbar #16468): implement toolbar background color via configOverwrite for web and mobile

* keep toolbarConfig defaults commented in config.js

* add trailing comma to commented toolbarConfig.backgroundColor

* fix: resolve linting errors

Fixes #16468
2025-11-11 07:02:28 -06:00
bgrozev
89b9c75242 test: Default sort by test order in allure report. (#16636) 2025-11-10 14:07:23 -06:00
Дамян Минков
b24b60b735 fix(tests): Wait for transcriptions to be off via an event. (#16635) 2025-11-10 12:55:35 -06:00
Дамян Минков
486a1f6511 fix(tests): Avoids being blocked by notification when clicking toolbar buttons
* fix(tests): Avoids clicking UI buttons to avoid being blocked by notification.

In AV moderation tests sometimes clicking mute/unmute buttons is blocked by askedToUnmute notification.

* squash: fix waiting for button.

* squash: adds some docs.
2025-11-10 11:04:36 -06:00
58 changed files with 870 additions and 1550 deletions

12
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
version: 2
updates:
# Monitor GitHub Actions versions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "chore(ci)"

View File

@@ -7,7 +7,7 @@ jobs:
name: Luacheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install luarocks
run: sudo apt-get --install-recommends -y install luarocks

View File

@@ -7,8 +7,8 @@ jobs:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -42,8 +42,8 @@ jobs:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -59,8 +59,8 @@ jobs:
name: Build mobile bundle (Android)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -74,8 +74,8 @@ jobs:
name: Build mobile bundle (iOS)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -103,10 +103,10 @@ jobs:
android-sdk-build:
name: Build mobile SDK (Android)
runs-on: ubuntu-latest
container: reactnativecommunity/react-native-android:v18.0
container: reactnativecommunity/react-native-android:v15.0
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -119,12 +119,15 @@ jobs:
cd android
./gradlew :sdk:clean
./gradlew :sdk:assembleRelease
- run: |
git config --global --add safe.directory /__w/jitsi-meet/jitsi-meet
git clean -dfx
ios-sdk-build:
name: Build mobile SDK (iOS)
runs-on: macos-15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -173,8 +176,8 @@ jobs:
name: Test Debian packages build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
cache: 'npm'

View File

@@ -1,5 +1,8 @@
# <p align="center">Jitsi Meet</p>
Jitsi Meet is a set of Open Source projects which empower users to use and deploy
video conferencing platforms with state-of-the-art video quality and features.

View File

@@ -907,6 +907,8 @@ var config = {
// alwaysVisible: false,
// // Indicates whether the toolbar should still autohide when chat is open
// autoHideWhileChatIsOpen: false,
// // Default background color for the main toolbar. Accepts any valid CSS color.
// // backgroundColor: '#ffffff',
// },
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed

View File

@@ -124,10 +124,17 @@ case "$1" in
ln -s $PROSODY_HOST_CONFIG /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
fi
PROSODY_CREATE_JICOFO_USER="true"
fi
if ! grep -q "VirtualHost \"$JVB_HOSTNAME\"" $PROSODY_CONFIG_OLD; then
# on some distributions main prosody config doesn't include configs
# from conf.d folder enable it as this where we put our config by default
# also when upgrading to new prosody version from prosody repo we need to add it again
if ! grep -q "Include \"conf\.d\/\*\.cfg.lua\"" $PROSODY_CONFIG_OLD; then
echo -e "\nInclude \"conf.d/*.cfg.lua\"" >> $PROSODY_CONFIG_OLD
# trigger a restart
PROSODY_CONFIG_PRESENT="false"
fi
fi

View File

@@ -1484,6 +1484,7 @@
"connectionInfo": "Verbindungsinformationen",
"demote": "Zu Gästen verschieben",
"domute": "Stummschalten",
"domuteDesktopOfOthers": "Bildschirm freigeben für alle beenden",
"domuteOthers": "Alle anderen stummschalten",
"domuteVideo": "Kamera ausschalten",
"domuteVideoOfOthers": "Alle anderen Kameras auschalten",

View File

@@ -114,6 +114,9 @@
"error": "Kļūda: Jūsu ziņa netika nosūtīta. Cēlonis: {{error}}",
"everyone": "Visi",
"fieldPlaceHolder": "Rakstiet ziņu šeit",
"fileAccessibleTitle": "{{user}} augšuplādēja failu",
"fileAccessibleTitleMe": "es augšuplādēju failu",
"fileDeleted": "Fails tika dzēsts",
"guestsChatIndicator": "(viesis)",
"lobbyChatMessageTo": "Vestibila tērzēšanas ziņa adresātam {{recipient}}",
"message": "Ziņa",
@@ -123,12 +126,20 @@
"messagebox": "Rakstiet ziņu",
"newMessages": "Jaunas ziņas",
"nickname": {
"popover": "Izvēlieties vārdu",
"title": "Ierakstiet vārdu, lai izmantotu tērzēšanā",
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā un slēptos subtitros",
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanā un aptaujās",
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās un slēptos subtitros",
"titleWithPollsAndCCAndFileSharing": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās, slēptos subtitros un failos"
"featureChat": "tērzētava",
"featureClosedCaptions": "slēgtie subtitri",
"featureFileSharing": "failu kopīgošana",
"featurePolls": "aptaujas",
"popover": "Izvēlieties segvārdu",
"title": "Ierakstiet segvārdu, lai izmantotu tērzēšanu",
"titleWith1Features": "Ievadiet segvārdu, lai izmantotu {{feature1}}",
"titleWith2Features": "Ievadiet segvārdu, lai izmantotu {{feature1}} un {{feature2}}",
"titleWith3Features": "Ievadiet segvārdu, lai izmantotu {{feature1}}, {{feature2}} un {{feature3}}",
"titleWith4Features": "Ievadiet segvārdu, lai izmantotu {{feature1}}, {{feature2}}, {{feature3}} un {{feature4}}",
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanu un slēgtos subtitrus",
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanu un aptaujas",
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanu, aptaujas un slēgtos subtitrus",
"titleWithPollsAndCCAndFileSharing": "Ievadiet segvārdu, lai izmantotu tērzēšanu, aptaujas, slēgtos subtitrus un failus"
},
"noMessagesMessage": "Sapulcē pagaidām nav nevienas ziņas. Uzsāciet saraksti!",
"privateNotice": "Privāta ziņa adresātam {{recipient}}",
@@ -137,12 +148,12 @@
"systemDisplayName": "Sistēma",
"tabs": {
"chat": "Tērzēšana",
"closedCaptions": "Slēptie subtitri",
"closedCaptions": "Slēgtie subtitri",
"fileSharing": "Faili",
"polls": "Aptaujas"
},
"title": "Tērzēšana",
"titleWithCC": "Tērzēšana un Slēptie subtitri",
"titleWithCC": "Tērzēšana un Slēgtie subtitri",
"titleWithFeatures": "Tērzēšana un",
"titleWithFileSharing": "Faili",
"titleWithPolls": "Tērzēšana un Aptaujas",
@@ -156,8 +167,8 @@
"installExtensionText": "Uzstādīt spraudni Google kalendāra un Office 365 integrācijai"
},
"closedCaptionsTab": {
"emptyState": "Slēpto subtitru saturs būs pieejams, tiklīdz moderators uzsāks to.",
"startClosedCaptionsButton": "Uzsākt slēptos subtitrus"
"emptyState": "Slēgto subtitru saturs būs pieejams, tiklīdz moderators uzsāks to.",
"startClosedCaptionsButton": "Uzsākt slēgtos subtitrus"
},
"connectingOverlay": {
"joiningRoom": "Notiek pieslēgšanās jūsu sapulcei…"
@@ -280,7 +291,6 @@
"Submit": "Iesniegt",
"Understand": "Saprotu",
"UnderstandAndUnmute": "Es saprotu, lūdzu, ieslēdziet skaņu.",
"WaitForHostMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, autorizējieties, lai kļūtu par moderatoru. Pretējā gadījumā, lūdzu, uzgaidiet.",
"WaitForHostNoAuthMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, uzgaidiet.",
"WaitingForHostButton": "Gaidīt rīkotāju",
"WaitingForHostTitle": "Gaida rīkotāju…",
@@ -417,7 +427,7 @@
"muteParticipantsVideoDialog": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Jūs nevarēsiet to ieslēgt atpakaļ, taču dalībnieks pats to varēs izdarīt jebkurā laikā.",
"muteParticipantsVideoDialogModerationOn": "Vai tiešām vēlaties izslēgt šī dalībnieka kameru? Ne Jūs, ne dalībnieks nevarēsiet to ieslēgt atpakaļ.",
"muteParticipantsVideoTitle": "Vai izslēgt šī dalībnieka video?",
"noDropboxToken": "Nav derīga Dropbox tokena",
"noDropboxToken": "Nav derīgas Dropbox pilnvaras",
"password": "Parole",
"passwordLabel": "Dalībnieks ir aizslēdzis sapulci. Lūdzu, ievadiet $t(lockRoomPassword), lai pievienotos.",
"passwordNotSupported": "Sapulces slēgšana ar $t(lockRoomPassword) netiek atbalstīta.",
@@ -501,7 +511,7 @@
"stopStreamingWarning": "Tiešām vēlaties beigt tiešraidi?",
"streamKey": "Tiešraides atslēga",
"thankYou": "Paldies, ka izmantojāt {{appName}}!",
"token": "tokens",
"token": "pilnvara",
"tokenAuthFailed": "Atvainojiet, jums nav atļauts pievienoties šim zvanam.",
"tokenAuthFailedReason": {
"audInvalid": "Nederīga `aud` vērtība. Tai vajadzētu būt `jitsi`.",
@@ -517,12 +527,13 @@
"nbfFuture": "`nbf` vērtība ir nākotnē.",
"nbfInvalid": "Nederīga `nbf` vērtība.",
"payloadNotFound": "Trūkst satura.",
"tokenExpired": "Token ir beidzies."
"tokenExpired": "Pilnvara ir beigusies."
},
"tokenAuthFailedTitle": "Autentifikācijas kļūda",
"tokenAuthFailedWithReasons": "Atvainojiet, jums nav atļauts pievienoties šim zvanam. Iespējamie iemesli: {{reason}}",
"tokenAuthUnsupported": "Token URL netiek atbalstīts.",
"tokenAuthUnsupported": "Pilnvaras URL nav atbalstīts.",
"transcribing": "Notiek atšifrējuma izveide",
"unauthenticatedAccessDisabled": "Šim zvanam nepieciešama autentifikācija. Lūdzu, piesakieties, lai turpinātu.",
"unlockRoom": "Noņemt $t(lockRoomPassword)",
"user": "Lietotājs",
"userIdentifier": "Lietotājvārds",
@@ -570,10 +581,12 @@
"downloadStarted": "Sākta faila lejuplāde",
"dragAndDrop": "Velciet un palaidiet failus šeit, vai jebkurā ekrāna vietā",
"fileAlreadyUploaded": "Fails jau ir augšuplādēts šajā sanāksmē.",
"fileRemovedByOther": "Jūsu fails '{{ fileName }}' tika noņemts",
"fileTooLargeDescription": "Lūdzu, pārliecinieties, vai faila lielums nepārsniedz {{ maxFileSize }}.",
"fileTooLargeTitle": "Izvēlētais fails ir pārāk liels",
"fileUploadProgress": "Faila augšuplādes gaita",
"fileUploadedSuccessfully": "Fails veiksmīgi augšuplādēts",
"newFileNotification": "{{ participantName }} kopīgoja '{{ fileName }}'",
"removeFile": "Noņemt",
"removeFileSuccess": "Fails veiksmīgi noņemts",
"uploadFailedDescription": "Lūdzu, mēģiniet vēlreiz.",
@@ -748,7 +761,8 @@
"notificationTitle": "Vestibils",
"passwordJoinButton": "Pievienoties",
"title": "Vestibils",
"toggleLabel": "Iespējot vestibilu"
"toggleLabel": "Iespējot vestibilu",
"waitForModerator": "Konference vēl nav sākusies, jo vēl nav ieradušies moderatori. Ja vēlaties kļūt par moderatoru, lūdzu, piesakieties. Pretējā gadījumā, lūdzu, uzgaidiet."
},
"localRecording": {
"clientState": {
@@ -775,7 +789,7 @@
"participant": "Dalībnieks",
"participantStats": "Dalībnieku statistika",
"selectTabTitle": "🎥 Lūdzu, atveriet šo cilni ierakstīšanai",
"sessionToken": "Sessijas tokens",
"sessionToken": "Sesijas Pilnvara",
"start": "Sākt ierakstu",
"stop": "Beigt ierakstu",
"stopping": "Ierakstīšanas pārtraukšana",
@@ -865,6 +879,7 @@
"oldElectronClientDescription1": "Izskatās, ka jūs izmantojat vecu Jitsi Meet klienta versiju, kurai ir zināmas drošības ievainojamības. Lūdzu, atjauniniet uz ",
"oldElectronClientDescription2": "jaunākā versija",
"oldElectronClientDescription3": "tagad!",
"openChat": "Atvērt tērzētavu",
"participantWantsToJoin": "Vēlas pievienoties sapulcei",
"participantsWantToJoin": "Vēlas pievienoties sapulcei",
"passwordRemovedRemotely": "Kāds dalībnieks noņēma $t(lockRoomPasswordUppercase).",
@@ -963,6 +978,9 @@
"by": "Pēc {{ name }} iniciatīvas",
"closeButton": "Slēgt aptauju",
"create": {
"accessibilityLabel": {
"send": "Nosūtīt aptauju"
},
"addOption": "Pievienot opciju",
"answerPlaceholder": "Opcija {{index}}",
"cancel": "Atcelt",
@@ -971,8 +989,7 @@
"pollQuestion": "Aptaujas Jautājums",
"questionPlaceholder": "Uzdod jautājumu",
"removeOption": "Noņemt opciju",
"save": "Saglabāt",
"send": "Nosūtīt"
"save": "Saglabāt"
},
"errors": {
"notUniqueOption": "Iespējām jābūt unikālām"
@@ -1091,7 +1108,7 @@
}
},
"recording": {
"authDropboxText": "Augšupielādēt uz Dropbox",
"authDropboxText": "Augšuplādēt uz Dropbox",
"availableSpace": "Pieejama vieta: {{spaceLeft}} MB (apmēram {{duration}} ieraksta minūtes)",
"beta": "BETA",
"busy": "Cenšamies nodrošināt ierakstam vairāk resursu. Lūdzu, pēc dažām minūtēm pamēģiniet vēlreiz.",
@@ -1145,7 +1162,7 @@
"title": "Ieraksts",
"unavailable": "Hmm! {{serviceName}} pašlaik nav pieejams. Mēs strādājam pie problēmas risināšanas. Lūdzu, pamēģiniet vēlreiz vēlāk.",
"unavailableTitle": "Ieraksts nav iespējams",
"uploadToCloud": "Augšupielādēt mākonī"
"uploadToCloud": "Augšuplādēt mākonī"
},
"screenshareDisplayName": "{{name}} ekrāns",
"sectionList": {
@@ -1300,7 +1317,7 @@
"closeChat": "Aizvērt tērzēšanu",
"closeMoreActions": "Aizvērt vairāk darbību izvēlni",
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
"closedCaptions": "Slēptie subtitri",
"closedCaptions": "Slēgtie subtitri",
"collapse": "Sakļaut",
"document": "Kopīgotais dokuments (iesl./izsl.)",
"documentClose": "Aizvērt kopīgoto dokumentu",
@@ -1378,7 +1395,21 @@
"videomuteGUMPending": "Kameras pievienošana",
"videounmute": "Ieslēgt kameru"
},
"addPeople": "Pievienot cilvēkus savai sesijai/zvanam",
"addPeople": "Pievienot cilvēkus savam zvanam",
"advancedAudioSettings": {
"aec": {
"label": "Akustiskās atbalss slāpēšana"
},
"agc": {
"label": "Automātiska pastiprinājuma kontrole"
},
"ns": {
"label": "Trokšņu slāpēšana"
},
"stereo": {
"label": "Stereo"
}
},
"audioOnlyOff": "Atspējot kanāla/trafika taupības režīmu",
"audioOnlyOn": "Iespējot kanāla/trafika taupības režīmu",
"audioRoute": "Izvēlēties audioierīci",
@@ -1391,7 +1422,7 @@
"closeChat": "Aizvērt tērzētavu",
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
"closeReactionsMenu": "Aizvērt reakciju izvēlni",
"closedCaptions": "Slēptie subtitri",
"closedCaptions": "Slēgtie subtitri",
"disableNoiseSuppression": "Atspējot trokšņu slāpēšanu",
"disableReactionSounds": "Šai sapulcei varat atspējot reakcijas skaņas",
"documentClose": "Aizvērt kopīgoto dokumentu",
@@ -1406,6 +1437,7 @@
"exitFullScreen": "Pilnekrāna režīms",
"exitTileView": "Tuvplāna režīms",
"feedback": "Atstāts atsauksmi",
"fileSharing": "Failu kopīgošana",
"giphy": "GIPHY izvēlne (rādīt/nerādīt)",
"hangup": "Iziet no sapulces",
"help": "Palīdzība",
@@ -1441,17 +1473,19 @@
"openReactionsMenu": "Atvērt reakciju izvēlni",
"participants": "Dalībnieki",
"pip": "Iesl. attēls attēlā (PIP) režīmu",
"polls": "Aptaujas",
"privateMessage": "Nosūtīt privātu ziņu",
"profile": "Rediģēt profilu",
"raiseHand": "Pacelt roku",
"raiseYourHand": "Pacelt roku",
"reactionBoo": "Nosūtīt būū reakciju",
"reactionClap": "Nosūtīt aplausu reakciju",
"reactionHeart": "Nosūtīt sirds reakciju",
"reactionLaugh": "Nosūtīt smieklu reakciju",
"reactionLike": "Nosūtīt īkšķi augšup reakciju",
"reactionSilence": "Nosūtīt klusuma reakciju",
"reactionSurprised": "Nosūtīt pārsteigts reakciju",
"reactionBoo": "Sūtīt būū reakciju",
"reactionClap": "Sūtīt aplausu reakciju",
"reactionHeart": "Sūtīt sirds reakciju",
"reactionLaugh": "Sūtīt smieklu reakciju",
"reactionLike": "Sūtīt īkšķis augšup reakciju",
"reactionLove": "Sūtīt mīlestības reakciju",
"reactionSilence": "Sūtīt klusuma reakciju",
"reactionSurprised": "Sūtīt pārsteiguma reakciju",
"reactions": "Reakcijas",
"security": "Drošības iespējas",
"selectBackground": "Izvēlēties fonu",
@@ -1484,7 +1518,7 @@
"failed": "Atšifrējuma izveide neizdevās",
"labelTooltip": "Šajā sapulcē notiek atšifrējuma izveide.",
"labelTooltipExtra": "Turklāt vēlāk būs pieejams atšifrējums.",
"openClosedCaptions": "Atvērt slēptos subtitrus",
"openClosedCaptions": "Atvērt slēgtos subtitrus",
"original": "Oriģināls",
"sourceLanguageDesc": "Pašlaik sapulces valoda ir iestatīta uz <b>{{sourceLanguage}}</b>. <br/> Varat to mainīt no ",
"sourceLanguageHere": "šeit",
@@ -1582,7 +1616,7 @@
"removeBackground": "Noņemt fonu",
"slightBlur": "Viegli izplūdis",
"title": "Virtuālie foni",
"uploadedImage": "Augšupielādēts attēls {{index}}",
"uploadedImage": "Augšuplādēts attēls {{index}}",
"webAssemblyWarning": "WebAssembly netiek atbalstīts",
"webAssemblyWarningDescription": "WebAssemb ir atspējots vai šī pārlūkprogramma to neatbalsta"
},
@@ -1601,6 +1635,8 @@
"noMainParticipantsTitle": "Šī sapulce vēl nav sākusies.",
"noVisitorLobby": "Jūs nevarat pievienoties, kamēr sapulcei ir iespējots vestibils.",
"notAllowedPromotion": "Dalībniekam vispirms ir jāatļauj jūsu pieprasījums.",
"requestToJoin": "Roka Pacelta",
"requestToJoinDescription": "Jūsu pieprasījums tika nosūtīts moderatoriem. Uzgaidiet!",
"title": "Jūs esat sapulces apmeklētājs"
},
"waitingMessage": "Jūs pievienosities sapulcei, tiklīdz tā sāksies!"

View File

@@ -340,6 +340,7 @@ function initCommands() {
APP.store.dispatch(setAssumedBandwidthBps(value));
},
'set-blurred-background': blurType => {
const tracks = APP.store.getState()['features/base/tracks'];
const videoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;

View File

@@ -158,11 +158,10 @@ const VideoLayout = {
return;
}
const state = APP.store.getState();
const currentContainer = largeVideo.getCurrentContainer();
const currentContainerType = largeVideo.getCurrentContainerType();
const isOnLarge = this.isCurrentlyOnLarge(id);
const state = APP.store.getState();
const participant = getParticipantById(state, id);
const videoTrack = getVideoTrackByParticipant(state, participant);
const videoStream = videoTrack?.jitsiTrack;

1367
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,7 @@
"@jitsi/js-utils": "2.6.7",
"@jitsi/logger": "2.1.1",
"@jitsi/rnnoise-wasm": "0.2.1",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@matrix-org/olm": "3.2.15",
"@microsoft/microsoft-graph-client": "3.0.1",
"@mui/material": "5.12.1",
"@react-native-async-storage/async-storage": "1.23.1",
@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2109.0.0+cb9d000c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2115.0.0+cc2f34c2/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -136,7 +136,7 @@
"@babel/preset-env": "7.25.9",
"@babel/preset-react": "7.25.9",
"@jitsi/eslint-config": "6.0.4",
"@react-native-community/cli": "17.0.1",
"@react-native-community/cli": "15.0.1",
"@react-native-community/cli-platform-android": "15.0.1",
"@react-native-community/cli-platform-ios": "15.0.1",
"@react-native/babel-preset": "0.77.2",

View File

@@ -618,6 +618,10 @@ export interface IConfig {
toolbarConfig?: {
alwaysVisible?: boolean;
autoHideWhileChatIsOpen?: boolean;
/**
* Background color for the main toolbar. Accepts any valid CSS color.
*/
backgroundColor?: string;
initialTimeout?: number;
timeout?: number;
};

View File

@@ -3,13 +3,14 @@ import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
// eslint-disable-next-line lines-around-comment
// @ts-ignore
import { safeJsonParse } from '@jitsi/js-utils/json';
import { pick } from 'lodash-es';
import { browser } from '../lib-jitsi-meet';
import { isEmbedded } from '../util/embedUtils';
import { parseURLParams } from '../util/parseURLParams';
import logger from './logger';
import WHITELIST from './whitelist';
/**
* Handles changes of the fake local storage.
@@ -61,7 +62,7 @@ function setupJitsiLocalStorage() {
if (shouldUseHostPageLocalStorage(urlParams)) {
try {
const localStorageContent = safeJsonParse(urlParams['appData.localStorageContent']);
let localStorageContent = safeJsonParse(urlParams['appData.localStorageContent']);
// We need to disable the local storage before setting the data in case the browser local storage doesn't
// throw exception (in some cases when this happens the local storage may be cleared for every session.
@@ -71,6 +72,10 @@ function setupJitsiLocalStorage() {
jitsiLocalStorage.setLocalStorageDisabled(true);
if (typeof localStorageContent === 'object') {
if (!isEmbedded()) {
localStorageContent = pick(localStorageContent, WHITELIST);
}
Object.keys(localStorageContent).forEach(key => {
jitsiLocalStorage.setItem(key, localStorageContent[key]);
});

View File

@@ -0,0 +1,11 @@
/**
* Keys of localStorage that are used by jibri.
*/
export default [
'callStatsUserName',
'displayname',
'email',
'xmpp_username_override',
'xmpp_password_override',
'xmpp_conference_password_override'
];

View File

@@ -1,5 +1,7 @@
import { IReduxState, IStore } from '../../app/types';
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
import { handleToggleVideoMuted } from '../../toolbox/actions.any';
import { muteLocal } from '../../video-menu/actions.any';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
import {
@@ -78,3 +80,43 @@ export function isRemoteVideoReceived({ getState }: IStore, id: string): boolean
return Boolean(videoTrack && !videoTrack.muted && isTrackStreamingStatusActive(videoTrack));
}
/**
* Mutes the local audio. Same as clicking the audio mute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function audioMute({ dispatch }: IStore) {
return dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
}
/**
* Unmutes the local audio. Same as clicking the audio unmute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function audioUnmute({ dispatch }: IStore) {
return dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
}
/**
* Mutes the local video. Same as clicking the video mute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function videoMute({ dispatch }: IStore) {
return dispatch(handleToggleVideoMuted(true, true, true));
}
/**
* Unmutes the local video. Same as clicking the video unmute button.
*
* @param {IStore} store - The redux store.
* @returns {Promise} Resolves when the action is complete.
*/
export function videoUnmute({ dispatch }: IStore) {
return dispatch(handleToggleVideoMuted(false, true, true));
}

View File

@@ -8,11 +8,15 @@ import { getJitsiMeetGlobalNS } from '../util/helpers';
import { setConnectionState } from './actions';
import {
audioMute,
audioUnmute,
getLocalCameraEncoding,
getRemoteVideoType,
isLargeVideoReceived,
isRemoteVideoReceived,
isTestModeEnabled
isTestModeEnabled,
videoMute,
videoUnmute
} from './functions';
import logger from './logger';
@@ -85,10 +89,14 @@ function _bindTortureHelpers(store: IStore) {
// All torture helper methods go in here
getJitsiMeetGlobalNS().testing = {
audioMute: audioMute.bind(null, store),
audioUnmute: audioUnmute.bind(null, store),
getRemoteVideoType: getRemoteVideoType.bind(null, store),
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
getLocalCameraEncoding: getLocalCameraEncoding.bind(null, store),
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store),
videoMute: videoMute.bind(null, store),
videoUnmute: videoUnmute.bind(null, store),
};
}

View File

@@ -6,6 +6,7 @@ import { setScreenshareMuted } from '../media/actions';
import { addLocalTrack, replaceLocalTrack } from './actions.any';
import { getLocalDesktopTrack, getTrackState } from './functions.native';
import logger from './logger';
export * from './actions.any';
@@ -63,6 +64,6 @@ async function _startScreenSharing(dispatch: IStore['dispatch'], state: IReduxSt
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
} catch (error: any) {
console.log('ERROR creating screen-sharing stream ', error);
logger.error('Error creating screen-sharing stream', error);
}
}

View File

@@ -298,8 +298,6 @@ const Chat = ({
// Disable text selection during resize
document.body.style.userSelect = 'none';
console.log('Chat resize: Mouse down', { clientX: e.clientX, initialWidth: _width });
}, [ _width, dispatch ]);
/**
@@ -315,8 +313,6 @@ const Chat = ({
// Restore cursor and text selection
document.body.style.cursor = '';
document.body.style.userSelect = '';
console.log('Chat resize: Mouse up');
}
}, [ isMouseDown, dispatch ]);
@@ -327,7 +323,6 @@ const Chat = ({
* @returns {void}
*/
const onChatResize = useCallback(throttle((e: MouseEvent) => {
// console.log('Chat resize: Mouse move', { clientX: e.clientX, isMouseDown, mousePosition, _width });
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
// For chat panel resizing on the left edge:
// - Dragging left (decreasing X coordinate) should make the panel wider

View File

@@ -12,6 +12,7 @@ import Button from '../../../base/ui/components/web/Button';
import { BUTTON_TYPES } from '../../../base/ui/constants.any';
import { copyText } from '../../../base/util/copyText.web';
import { handleLobbyChatInitialized, openChat } from '../../actions.web';
import logger from '../../logger';
export interface IProps {
className?: string;
@@ -125,11 +126,11 @@ const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, en
setShowCopiedMessage(false);
}, 2000);
} else {
console.error('Failed to copy text');
logger.error('Failed to copy text');
}
})
.catch(error => {
console.error('Error copying text:', error);
.catch((error: Error) => {
logger.error('Error copying text', error);
});
handleClose();
}, [ message ]);

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../base/logging/functions';
export default getLogger('app:chat');

View File

@@ -11,6 +11,7 @@ import {
getVirtualScreenshareParticipantByOwnerId
} from '../base/participants/functions';
import { toState } from '../base/redux/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { getAutoPinSetting } from '../video-layout/functions';
import {
@@ -18,7 +19,6 @@ import {
SET_LARGE_VIDEO_DIMENSIONS,
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
} from './actionTypes';
import { shouldHideLargeVideo } from './functions';
/**
* Action to select the participant to be displayed in LargeVideo based on the
@@ -34,8 +34,12 @@ export function selectParticipantInLargeVideo(participant?: string) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
// Skip large video updates when the large video container is hidden.
if (shouldHideLargeVideo(state)) {
if (isStageFilmstripAvailable(state, 2)) {
return;
}
// Keep Etherpad open.
if (state['features/etherpad'].editing) {
return;
}

View File

@@ -1,7 +1,5 @@
import { IReduxState } from '../app/types';
import { getParticipantById } from '../base/participants/functions';
import { isStageFilmstripAvailable } from '../filmstrip/functions';
import { shouldDisplayTileView } from '../video-layout/functions.any';
/**
* Selector for the participant currently displaying on the large video.
@@ -14,17 +12,3 @@ export function getLargeVideoParticipant(state: IReduxState) {
return getParticipantById(state, participantId ?? '');
}
/**
* Determines whether the large video container should be hidden.
* Large video is hidden in tile view, stage filmstrip mode (with multiple participants),
* or when editing etherpad.
*
* @param {IReduxState} state - The Redux state.
* @returns {boolean} True if large video should be hidden, false otherwise.
*/
export function shouldHideLargeVideo(state: IReduxState): boolean {
return shouldDisplayTileView(state)
|| isStageFilmstripAvailable(state, 2)
|| Boolean(state['features/etherpad']?.editing);
}

View File

@@ -1,26 +0,0 @@
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
import { selectParticipantInLargeVideo } from './actions.any';
import { shouldHideLargeVideo } from './functions';
/**
* Updates the large video when transitioning from a hidden state to visible state.
* This ensures the large video is properly updated when exiting tile view, stage filmstrip,
* whiteboard, or etherpad editing modes.
*/
StateListenerRegistry.register(
/* selector */ state => shouldHideLargeVideo(state),
/* listener */ (isHidden, { dispatch }) => {
// When transitioning from hidden to visible state, select participant (because currently it is undefined).
// Otherwise set it to undefined because we don't show the large video.
if (!isHidden) {
dispatch(selectParticipantInLargeVideo());
} else {
dispatch({
type: SELECT_LARGE_VIDEO_PARTICIPANT,
participantId: undefined
});
}
}
);

View File

@@ -1 +0,0 @@
import './subscriber.any';

View File

@@ -4,7 +4,6 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
import { getLargeVideoParticipant } from './functions';
import './subscriber.any';
/**
* Updates the on stage participant video.

View File

@@ -11,6 +11,7 @@ import {
RESET_UNREAD_POLLS_COUNT,
SAVE_POLL
} from './actionTypes';
import logger from './logger';
import { IIncomingAnswerData, IPollData } from './types';
const INITIAL_STATE = {
@@ -87,7 +88,7 @@ ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action
// if the poll doesn't exist
if (!(pollId in state.polls)) {
console.warn('requested poll does not exist: pollId ', pollId);
logger.warn('Requested poll does not exist', { pollId });
return state;
}

View File

@@ -213,13 +213,16 @@ const LocalRecordingManager: ILocalRecordingManager = {
});
const gdmVideoTrack = gdmStream.getVideoTracks()[0];
const isBrowser = gdmVideoTrack.getSettings().displaySurface === 'browser';
const matchesHandle = (supportsCaptureHandle // @ts-ignore
&& gdmVideoTrack.getCaptureHandle()?.handle === `JitsiMeet-${tabId}`);
if (!isBrowser || !matchesHandle) {
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
throw new Error('WrongSurfaceSelected');
if (supportsCaptureHandle) {
const isBrowser = gdmVideoTrack.getSettings().displaySurface === 'browser';
const matchesHandle = (supportsCaptureHandle // @ts-ignore
&& gdmVideoTrack.getCaptureHandle()?.handle === `JitsiMeet-${tabId}`);
if (!isBrowser || !matchesHandle) {
gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
throw new Error('WrongSurfaceSelected');
}
}
this.initializeAudioMixer();

View File

@@ -1,7 +1,7 @@
import { AnyAction } from 'redux';
import { IStore } from '../app/types';
import { ENDPOINT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import { ENDPOINT_MESSAGE_RECEIVED, NON_PARTICIPANT_MESSAGE_RECEIVED } from '../base/conference/actionTypes';
import { MEET_FEATURES } from '../base/jwt/constants';
import { isJwtFeatureEnabled } from '../base/jwt/functions';
import JitsiMeetJS from '../base/lib-jitsi-meet';
@@ -76,6 +76,7 @@ const STABLE_TRANSCRIPTION_FACTOR = 0.85;
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case ENDPOINT_MESSAGE_RECEIVED:
case NON_PARTICIPANT_MESSAGE_RECEIVED:
return _endpointMessageReceived(store, next, action);
case TOGGLE_REQUESTING_SUBTITLES: {

View File

@@ -60,6 +60,7 @@ function Toolbox(props: IProps) {
const { clientWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const { customToolbarButtons } = useSelector((state: IReduxState) => state['features/base/config']);
const toolbarBackgroundColor = useSelector((state: IReduxState) => state['features/base/config'].toolbarConfig?.backgroundColor);
const {
mainToolbarButtonsThresholds,
toolbarButtons
@@ -79,6 +80,11 @@ function Toolbox(props: IProps) {
const { buttonStylesBorderless, hangupButtonStyles } = _styles;
const style = { ...styles.toolbox };
// Allow overriding the toolbox background color from config (configOverwrite/overwriteConfig).
if (toolbarBackgroundColor) {
style.backgroundColor = toolbarBackgroundColor as any;
}
// We have only hangup and raisehand button in _iAmVisitor mode
if (_iAmVisitor) {
style.justifyContent = 'center';

View File

@@ -37,6 +37,11 @@ import Separator from './Separator';
*/
interface IProps {
/**
* Optional toolbar background color passed as a prop.
*/
toolbarBackgroundColor?: string;
/**
* Explicitly passed array with the buttons which this Toolbox should display.
*/
@@ -65,7 +70,8 @@ const useStyles = makeStyles()(() => {
* @returns {ReactElement}
*/
export default function Toolbox({
toolbarButtons
toolbarButtons,
toolbarBackgroundColor: toolbarBackgroundColorProp
}: IProps) {
const { classes, cx } = useStyles();
const { t } = useTranslation();
@@ -92,7 +98,10 @@ export default function Toolbox({
const localParticipant = useSelector(getLocalParticipant);
const transcribing = useSelector(isTranscribing);
const _isCCTabEnabled = useSelector(isCCTabEnabled);
// Read toolbar background color from config (if provided) or from props.
const toolbarBackgroundColorFromConfig = useSelector((state: IReduxState) =>
state['features/base/config'].toolbarConfig?.backgroundColor);
const toolbarBackgroundColor = toolbarBackgroundColorProp || toolbarBackgroundColorFromConfig;
// Do not convert to selector, it returns new array and will cause re-rendering of toolbox on every action.
const jwtDisabledButtons = getJwtDisabledButtons(transcribing, _isCCTabEnabled, localParticipant?.features);
@@ -242,7 +251,8 @@ export default function Toolbox({
return (
<div
className = { cx(rootClassNames, shiftUp && 'shift-up') }
id = 'new-toolbox'>
id = 'new-toolbox'
style = { toolbarBackgroundColor ? { backgroundColor: toolbarBackgroundColor } : undefined }>
<div className = { containerClassName }>
<div
className = 'toolbox-content-wrapper'

View File

@@ -153,7 +153,7 @@ end
module:hook('message/full', on_message); -- private messages
module:hook('message/bare', on_message); -- room messages
module:hook('muc-room-destroyed', room_destroyed, -1);
module:hook('muc-room-destroyed', room_destroyed, 1); -- prosody handles it at 0
module:hook("muc-occupant-left", function(event)
local occupant, room = event.occupant, event.room;
local session = event.origin;

View File

@@ -15,7 +15,6 @@
-- muc_room_locking = false
-- muc_room_default_public_jids = true
--
module:depends('room_destroy');
-- we use async to detect Prosody 0.10 and earlier
@@ -650,7 +649,7 @@ function process_main_muc_loaded(main_muc, host_module)
module:log("info", "Hook to muc events on %s", main_muc_component_config);
host_module:hook('muc-occupant-joined', on_occupant_joined);
host_module:hook('muc-occupant-left', on_occupant_left);
host_module:hook('muc-room-destroyed', on_main_room_destroyed);
host_module:hook('muc-room-destroyed', on_main_room_destroyed, 1); -- prosody handles it at 0
end
-- process or waits to process the main muc component

View File

@@ -26,7 +26,8 @@ end, -100); -- make sure we are last in the chain
module:hook('muc-occupant-left', function (event)
local occupant, room = event.occupant, event.room;
if is_admin(occupant.bare_jid) or is_jibri(occupant.jid) or is_transcriber(occupant.jid) then
if is_admin(occupant.bare_jid) or is_jibri(occupant.jid) or is_transcriber(occupant.jid)
or room._data.breakout_rooms_active then
return;
end
@@ -54,5 +55,4 @@ module:hook('muc-room-destroyed', function (event)
room.empty_destroy_timer:stop();
room.empty_destroy_timer = nil;
end
end);
end, 1); -- prosody handles it at 0

View File

@@ -512,7 +512,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
if room._data.lobbyroom then
destroy_lobby_room(room, nil);
end
end);
end, 1); -- prosody handles it at 0
host_module:hook('muc-disco#info', function (event)
local room = event.room;
if (room._data.lobbyroom and room:get_members_only()) then

View File

@@ -118,9 +118,12 @@ module:hook("muc-occupant-pre-join", function (event)
join_rate_per_conference,
room.join_rate_presence_queue,
function(ev)
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
ev.stanza.delayed_join_skip = true;
room:handle_normal_presence(ev.origin, ev.stanza);
-- if the connection was closed while waiting in the queue, ignore
if ev.origin.conn then
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
ev.stanza.delayed_join_skip = true;
room:handle_normal_presence(ev.origin, ev.stanza);
end
end,
function() -- empty callback
room.join_rate_queue_timer = false;
@@ -164,7 +167,7 @@ module:hook('muc-room-destroyed',function(event)
if event.room.leave_rate_presence_queue then
event.room.leave_rate_presence_queue.empty = true;
end
end);
end, 1); -- prosody handles it at 0
module:hook('muc-occupant-pre-leave', function (event)
local occupant, room, stanza = event.occupant, event.room, event.stanza;

View File

@@ -12,6 +12,7 @@ local muc = module:depends("muc");
local NS_NICK = 'http://jabber.org/protocol/nick';
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
@@ -138,6 +139,7 @@ end
end
local room;
local occupant;
if session.type == 's2sin' then
if not json_message.attr.roomJid then
module:log('warn', 'No room jid found in %s', stanza);
@@ -145,7 +147,32 @@ end
end
room = get_room_from_jid(room_jid_match_rewrite(json_message.attr.roomJid));
else
room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
local main_room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
local occupant_jid = stanza.attr.from;
occupant = main_room:get_occupant_by_real_jid(occupant_jid);
if main_room._data.breakout_rooms_active then
-- let's find is this participant in the main room or in some breakout room
if not occupant then
-- not in main room, let's check breakout rooms
for breakout_room_jid, subject in pairs(main_room._data.breakout_rooms or {}) do
local breakout_room = get_room_from_jid(breakout_room_jid);
occupant = breakout_room:get_occupant_by_real_jid(occupant_jid);
if occupant then
room = breakout_room;
break;
end
end
end
else
room = main_room;
end
if not occupant then
module:log('error', 'Occupant sending poll msg %s was not found in room %s', occupant_jid, room.jid)
return;
end
end
if not room then
@@ -177,11 +204,6 @@ end
local occupant_details;
if session.type ~= 's2sin' then
local occupant_jid = stanza.attr.from;
occupant = room:get_occupant_by_real_jid(occupant_jid);
if not occupant then
module:log("error", "Occupant sending msg %s was not found in room %s", occupant_jid, room.jid)
return;
end
occupant_details = get_occupant_details(occupant)
if not occupant_details then
module:log("error", "Cannot retrieve poll creator or voter id and name for %s from %s",

View File

@@ -331,7 +331,7 @@ function process_main_muc_loaded(main_muc, host_module)
host_module:hook("muc-room-created", room_created, -1);
host_module:hook("muc-occupant-joined", occupant_joined, -1);
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
host_module:hook("muc-room-destroyed", room_destroyed, -1);
host_module:hook("muc-room-destroyed", room_destroyed, 1); -- prosody handles it at 0
end
function process_breakout_muc_loaded(breakout_muc, host_module)
@@ -340,7 +340,7 @@ function process_breakout_muc_loaded(breakout_muc, host_module)
host_module:hook("muc-room-created", breakout_room_created, -1);
host_module:hook("muc-occupant-joined", occupant_joined, -1);
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
host_module:hook("muc-room-destroyed", room_destroyed, -1);
host_module:hook("muc-room-destroyed", room_destroyed, 1); -- prosody handles it at 0
end
-- process or waits to process the conference muc component

View File

@@ -329,7 +329,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
visitors_nodes[room.jid] = nil;
end
end);
end, 1); -- prosody handles it at 0
-- detects new participants joining main room and sending them to the visitor nodes
host_module:hook('muc-occupant-joined', function (event)

View File

@@ -582,7 +582,7 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
host_module:hook('muc-room-destroyed', function (event)
visitors_promotion_map[event.room.jid] = nil;
visitors_promotion_requests[event.room.jid] = nil;
end);
end, 1); -- prosody handles it at 0
host_module:hook('muc-occupant-joined', function (event)
local room, occupant = event.room, event.occupant;
@@ -754,7 +754,7 @@ function handle_occupant_leaving_breakout(event)
local main_room, occupant, stanza = event.main_room, event.occupant, event.stanza;
local presence_status = stanza:get_child_text('status');
if presence_status ~= 'switch_room' or not visitors_promotion_map[main_room.jid] then
if presence_status ~= 'switch_room' or not main_room or not visitors_promotion_map[main_room.jid] then
return;
end

View File

@@ -0,0 +1,134 @@
local basexx = require "basexx";
local M = {}
-- Helper function to encode bytes to base64
function base64_encode(bytes)
return basexx.to_base64(bytes)
end
-- Pure Lua ASN.1 DER encoder (no external dependencies)
local ASN1 = {}
-- Encode ASN.1 length field
function ASN1.encode_length(len)
if len < 128 then
return string.char(len)
elseif len < 256 then
return string.char(0x81, len)
elseif len < 65536 then
return string.char(0x82, math.floor(len / 256), len % 256)
else
local b1 = math.floor(len / 65536)
local b2 = math.floor((len % 65536) / 256)
local b3 = len % 256
return string.char(0x83, b1, b2, b3)
end
end
-- Encode ASN.1 INTEGER
function ASN1.encode_integer(bytes)
-- ASN.1 INTEGER tag is 0x02
-- If the high bit is set, prepend 0x00 to indicate positive number
if bytes:byte(1) >= 0x80 then
bytes = string.char(0x00) .. bytes
end
return string.char(0x02) .. ASN1.encode_length(#bytes) .. bytes
end
-- Encode ASN.1 SEQUENCE
function ASN1.encode_sequence(content)
-- ASN.1 SEQUENCE tag is 0x30
return string.char(0x30) .. ASN1.encode_length(#content) .. content
end
-- Encode ASN.1 BIT STRING
function ASN1.encode_bit_string(content)
-- ASN.1 BIT STRING tag is 0x03
-- First byte indicates number of unused bits (0x00 for byte-aligned)
return string.char(0x03) .. ASN1.encode_length(#content + 1) .. string.char(0x00) .. content
end
-- Encode ASN.1 OBJECT IDENTIFIER
function ASN1.encode_oid(oid_bytes)
-- ASN.1 OID tag is 0x06
return string.char(0x06) .. ASN1.encode_length(#oid_bytes) .. oid_bytes
end
-- Encode ASN.1 NULL
function ASN1.encode_null()
-- ASN.1 NULL tag is 0x05, length 0
return string.char(0x05, 0x00)
end
-- Convert DER to PEM format
function ASN1.der_to_pem(der, label)
label = label or "PUBLIC KEY"
local base64 = base64_encode(der)
-- Break into 64-character lines
local lines = {}
for i = 1, #base64, 64 do
table.insert(lines, base64:sub(i, i + 63))
end
return "-----BEGIN " .. label .. "-----\n" ..
table.concat(lines, "\n") .. "\n" ..
"-----END " .. label .. "-----\n"
end
-- Helper function to decode base64url
function base64url_decode(str)
-- Convert base64url to base64
str = str:gsub('-', '+'):gsub('_', '/')
-- Add padding if needed
local padding = #str % 4
if padding > 0 then
str = str .. string.rep('=', 4 - padding)
end
return basexx.from_base64(str)
end
-- Helper function to convert JWK to PEM format
function M.jwk_to_pem(jwk)
-- Decode the modulus (n) and exponent (e) from base64url
local n_bytes = base64url_decode(jwk.n)
local e_bytes = base64url_decode(jwk.e)
-- Build RSA public key structure
-- RSAPublicKey ::= SEQUENCE {
-- modulus INTEGER, -- n
-- publicExponent INTEGER -- e
-- }
local modulus_asn1 = ASN1.encode_integer(n_bytes)
local exponent_asn1 = ASN1.encode_integer(e_bytes)
local rsa_pubkey = ASN1.encode_sequence(modulus_asn1 .. exponent_asn1)
-- Build SubjectPublicKeyInfo structure
-- SubjectPublicKeyInfo ::= SEQUENCE {
-- algorithm AlgorithmIdentifier,
-- subjectPublicKey BIT STRING
-- }
-- RSA OID: 1.2.840.113549.1.1.1 (rsaEncryption)
-- Encoded as: 06 09 2A 86 48 86 F7 0D 01 01 01
local rsa_oid = string.char(0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01)
local rsa_oid_encoded = ASN1.encode_oid(rsa_oid)
-- AlgorithmIdentifier ::= SEQUENCE {
-- algorithm OBJECT IDENTIFIER,
-- parameters NULL
-- }
local algorithm_id = ASN1.encode_sequence(rsa_oid_encoded .. ASN1.encode_null())
-- Wrap the RSA public key in a BIT STRING
local subject_public_key = ASN1.encode_bit_string(rsa_pubkey)
-- Final SubjectPublicKeyInfo
local spki = ASN1.encode_sequence(algorithm_id .. subject_public_key)
-- Convert to PEM format
return ASN1.der_to_pem(spki, "PUBLIC KEY")
end
return M

View File

@@ -5,6 +5,7 @@ local basexx = require "basexx";
local have_async, async = pcall(require, "util.async");
local hex = require "util.hex";
local jwt = module:require "luajwtjitsi";
local jwk_to_pem = module:require "token/jwk".jwk_to_pem;
local jid = require "util.jid";
local json_safe = require "cjson.safe";
local path = require "util.paths";
@@ -111,14 +112,14 @@ function Util.new(module)
return nil;
end
if self.appSecret == nil and self.asapKeyServer == nil then
module:log("error", "'app_secret' or 'asap_key_server' must be specified");
if self.appSecret == nil and self.asapKeyServer == nil and self.cacheKeysUrl == nil then
module:log("error", "'app_secret', 'asap_key_server or 'cacheKeysUrl' must be specified");
return nil;
end
-- Set defaults for signature algorithm
if self.signatureAlgorithm == nil then
if self.asapKeyServer ~= nil then
if self.asapKeyServer ~= nil or self.cacheKeysUrl then
self.signatureAlgorithm = "RS256"
elseif self.appSecret ~= nil then
self.signatureAlgorithm = "HS256"
@@ -133,7 +134,7 @@ function Util.new(module)
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
if self.asapKeyServer and not have_async then
if (self.asapKeyServer or self.cacheKeysUrl) and not have_async then
module:log("error", "requires a version of Prosody with util.async");
return nil;
end
@@ -148,7 +149,18 @@ function Util.new(module)
local keys_to_delete = table_shallow_copy(self.cachedKeys);
-- Let's convert any certificate to public key
for k, v in pairs(cjson_safe.decode(content)) do
if starts_with(v, '-----BEGIN CERTIFICATE-----') then
-- JWKS format
if k == "keys" and type(v) == "table" then
for _, key in ipairs(v) do
if key.kid then
self.cachedKeys[key.kid] = jwk_to_pem(key);
-- do not clean this key if it already exists
keys_to_delete[key.kid] = nil;
end
end
-- direct PEM mapping (Firebase)
elseif starts_with(v, '-----BEGIN CERTIFICATE-----') then
self.cachedKeys[k] = ssl.loadcertificate(v):pubkey();
-- do not clean this key if it already exists
keys_to_delete[k] = nil;
@@ -263,7 +275,7 @@ function Util:process_and_verify_token(session)
-- We're using an public key stored in the session
-- module:log("debug","Public key was found on the session");
key = session.public_key;
elseif self.asapKeyServer and session.auth_token ~= nil then
elseif (self.asapKeyServer or self.cacheKeysUrl) and session.auth_token ~= nil then
-- We're fetching an public key from an ASAP server
local dotFirst = session.auth_token:find("%.");
if not dotFirst then return false, "not-allowed", "Invalid token" end

View File

@@ -12,12 +12,21 @@ const defaultExpectations = {
* null -> if the config is enabled, assert the UI elements are displayed and the feature works.
*/
enabled: null,
minPinLength: 8
},
iframe: {
// Whether the iframe integration is enabled (the inverse of `disableIframeAPI` from config.js)
enabled: true
},
jaas: {
liveStreamingEnabled: true,
recordingEnabled: true,
transcriptionEnabled: true,
/**
* Whether the jaas account is configured with the account-level setting to allow unauthenticated users to join.
*/
unauthenticatedJoins: false
unauthenticatedJoins: false,
visitors: true
},
moderation: {
// Everyone is a moderator.

View File

@@ -124,20 +124,6 @@ export default class Filmstrip extends BasePageObject {
return await elem.isExisting() ? await elem.getAttribute('src') : null;
}
/**
* Returns true if the endpoint is dominant speaker and false otherwise.
* Uses the dominant-speaker class on the video thumbnail in order to check.
*
* @param {string} endpointId - The endpoint id of the participant we want to check.
* @returns {boolean} - True if the endpoint is dominant speaker and false otherwise.
*/
async isDominantSpeaker(endpointId: string) {
const elem = this.participant.driver.$(
`//span[@id='participant_${endpointId}' and contains(@class,'dominant-speaker')]`);
return await elem.isExisting();
}
/**
* Grants moderator rights to a participant.
* @param participant

View File

@@ -58,7 +58,12 @@ export default class Toolbar extends BasePageObject {
async clickAudioMuteButton(): Promise<void> {
await this.participant.log('Clicking on: Audio Mute Button');
return this.audioMuteBtn.click();
await this.audioMuteBtn.waitForExist({
timeout: 2000, timeoutMsg: 'Audio mute button not found'
});
// not directly clicking the button to avoid issues of UI notifications preventing it
return this.participant.execute(() => JitsiMeetJS.app.testing.audioMute());
}
/**
@@ -69,7 +74,12 @@ export default class Toolbar extends BasePageObject {
async clickAudioUnmuteButton(): Promise<void> {
await this.participant.log('Clicking on: Audio Unmute Button');
return this.audioUnMuteBtn.click();
await this.audioUnMuteBtn.waitForExist({
timeout: 2000, timeoutMsg: 'Audio unmute button not found'
});
// not directly clicking the button to avoid issues of UI notifications preventing it
return this.participant.execute(() => JitsiMeetJS.app.testing.audioUnmute());
}
/**
@@ -94,7 +104,12 @@ export default class Toolbar extends BasePageObject {
async clickVideoMuteButton(): Promise<void> {
await this.participant.log('Clicking on: Video Mute Button');
return this.videoMuteBtn.click();
await this.videoMuteBtn.waitForExist({
timeout: 2000, timeoutMsg: 'Video mute button not found'
});
// not directly clicking the button to avoid issues of UI notifications preventing it
return this.participant.execute(() => JitsiMeetJS.app.testing.videoMute());
}
/**
@@ -105,7 +120,12 @@ export default class Toolbar extends BasePageObject {
async clickVideoUnmuteButton(): Promise<void> {
await this.participant.log('Clicking on: Video Unmute Button');
return this.videoUnMuteBtn.click();
await this.videoUnMuteBtn.waitForExist({
timeout: 2000, timeoutMsg: 'Video unmute button not found'
});
// not directly clicking the button to avoid issues of UI notifications preventing it
return this.participant.execute(() => JitsiMeetJS.app.testing.videoUnmute());
}
/**

View File

@@ -5,6 +5,8 @@ import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { joinMuc } from '../../helpers/joinMuc';
import { checkIframeApi } from './util';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
@@ -14,13 +16,10 @@ describe('Chat', () => {
it('setup', async () => {
p1 = await joinMuc({ name: 'p1', iFrameApi: true, token: testsConfig.jwt.preconfiguredToken });
p2 = await joinMuc({ name: 'p2', iFrameApi: true });
if (await p1.execute(() => config.disableIframeAPI)) {
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
if (!await checkIframeApi(p1)) {
return;
}
p2 = await joinMuc({ name: 'p2', iFrameApi: true });
await p1.switchToMainFrame();
await p2.switchToMainFrame();

View File

@@ -0,0 +1,136 @@
import { isEqual } from 'lodash-es';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
import { checkIframeApi } from './util';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Kick participants', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p1, p2 } = ctx;
if (!await checkIframeApi(p1)) {
return;
}
await Promise.all([
p1.switchToMainFrame(),
p2.switchToMainFrame()
]);
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
expect(await p2.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
});
it('kick participant', async () => {
await ctx.p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await ctx.p2.getIframeAPI().addEventListener('videoConferenceLeft');
await ctx.p2.switchToMainFrame();
await ctx.p2.getIframeAPI().executeCommand('hangup');
await ctx.p2.driver.waitUntil(() =>
ctx.p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 4000,
timeoutMsg: 'videoConferenceLeft not received'
});
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p1, p2, roomName } = ctx;
const p1EpId = await p1.getEndpointId();
const p2EpId = await p2.getEndpointId();
const p1DisplayName = await p1.getLocalDisplayName();
const p2DisplayName = await p2.getLocalDisplayName();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
await p1.getIframeAPI().addEventListener('participantKickedOut');
await p2.getIframeAPI().addEventListener('participantKickedOut');
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
const eventP1 = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on p1 side'
});
const eventP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on p2 side'
});
expect(eventP1).toBeDefined();
expect(eventP2).toBeDefined();
expect(isEqual(eventP1, {
kicked: {
id: p2EpId,
local: false,
name: p2DisplayName
},
kicker: {
id: p1EpId,
local: true,
name: p1DisplayName
}
})).toBe(true);
expect(isEqual(eventP2, {
kicked: {
id: 'local',
local: true,
name: p2DisplayName
},
kicker: {
id: p1EpId,
name: p1DisplayName
}
})).toBe(true);
const eventConferenceLeftP2 = await p2.driver.waitUntil(() =>
p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 4000,
timeoutMsg: 'videoConferenceLeft not received'
});
expect(eventConferenceLeftP2).toBeDefined();
expect(eventConferenceLeftP2.roomName).toBe(roomName);
});
it('join after kick', async () => {
const { p1 } = ctx;
await p1.getIframeAPI().addEventListener('participantJoined');
// join again
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p2 } = ctx;
await p1.switchToMainFrame();
const event = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantJoined'), {
timeout: 2000,
timeoutMsg: 'participantJoined not received'
});
const p2DisplayName = await p2.getLocalDisplayName();
expect(event).toBeDefined();
expect(event.id).toBe(await p2.getEndpointId());
expect(event.displayName).toBe(p2DisplayName);
expect(event.formattedDisplayName).toBe(p2DisplayName);
});
});

View File

@@ -1,9 +1,9 @@
import { isEqual } from 'lodash-es';
import { P1, P2 } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
import { checkIframeApi } from './util';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
@@ -14,9 +14,7 @@ describe('Participants presence', () => {
const { p1, p2 } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
if (!await checkIframeApi(p1)) {
return;
}
@@ -71,111 +69,6 @@ describe('Participants presence', () => {
expect((await p1.getIframeAPI().getEventResult('participantsPaneToggled'))?.open).toBe(false);
});
it('kick participant', async () => {
await ctx.p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await ctx.p2.getIframeAPI().addEventListener('videoConferenceLeft');
await ctx.p2.switchToMainFrame();
await ctx.p2.getIframeAPI().executeCommand('hangup');
await ctx.p2.driver.waitUntil(() =>
ctx.p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 4000,
timeoutMsg: 'videoConferenceLeft not received'
});
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p1, p2, roomName } = ctx;
const p1EpId = await p1.getEndpointId();
const p2EpId = await p2.getEndpointId();
const p1DisplayName = await p1.getLocalDisplayName();
const p2DisplayName = await p2.getLocalDisplayName();
await p1.switchToMainFrame();
await p2.switchToMainFrame();
await p1.getIframeAPI().addEventListener('participantKickedOut');
await p2.getIframeAPI().addEventListener('participantKickedOut');
await p2.getIframeAPI().clearEventResults('videoConferenceLeft');
await p2.getIframeAPI().addEventListener('videoConferenceLeft');
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
const eventP1 = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on p1 side'
});
const eventP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on p2 side'
});
expect(eventP1).toBeDefined();
expect(eventP2).toBeDefined();
expect(isEqual(eventP1, {
kicked: {
id: p2EpId,
local: false,
name: p2DisplayName
},
kicker: {
id: p1EpId,
local: true,
name: p1DisplayName
}
})).toBe(true);
expect(isEqual(eventP2, {
kicked: {
id: 'local',
local: true,
name: p2DisplayName
},
kicker: {
id: p1EpId,
name: p1DisplayName
}
})).toBe(true);
const eventConferenceLeftP2 = await p2.driver.waitUntil(() =>
p2.getIframeAPI().getEventResult('videoConferenceLeft'), {
timeout: 4000,
timeoutMsg: 'videoConferenceLeft not received'
});
expect(eventConferenceLeftP2).toBeDefined();
expect(eventConferenceLeftP2.roomName).toBe(roomName);
});
it('join after kick', async () => {
const { p1 } = ctx;
await p1.getIframeAPI().addEventListener('participantJoined');
await p1.getIframeAPI().addEventListener('participantMenuButtonClick');
// join again
await ensureTwoParticipants({}, { name: 'p1', iFrameApi: true });
const { p2 } = ctx;
await p1.switchToMainFrame();
const event = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantJoined'), {
timeout: 2000,
timeoutMsg: 'participantJoined not received'
});
const p2DisplayName = await p2.getLocalDisplayName();
expect(event).toBeDefined();
expect(event.id).toBe(await p2.getEndpointId());
expect(event.displayName).toBe(p2DisplayName);
expect(event.formattedDisplayName).toBe(p2DisplayName);
});
it('overwrite names', async () => {
const { p1, p2 } = ctx;

View File

@@ -0,0 +1,15 @@
import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { expectations } from '../../helpers/expectations';
export async function checkIframeApi(p: Participant) {
const iframeEnabled = !await p.execute(() => config.disableIframeAPI);
expect(iframeEnabled).toBe(expectations.iframe.enabled);
if (!iframeEnabled) {
ctx.skipSuiteTests = 'The iFrame API is disabled';
}
return iframeEnabled;
}

View File

@@ -2,6 +2,7 @@ import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import WebhookProxy from '../../helpers/WebhookProxy';
import { expectations } from '../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
@@ -12,16 +13,13 @@ setTestProperties(__filename, {
/**
* Tests the recording and live-streaming functionality of JaaS (including relevant webhooks) exercising the iFrame API
* commands and functions.
* TODO: read flags from config.
* TODO: also assert "this meeting is being recorder" notificaitons are show/played?
* TODO: also assert "this meeting is being recorded" notifications are show/played?
*/
describe('Recording and live-streaming', () => {
const tenant = testsConfig.jaas.tenant;
const customerId = tenant?.replace('vpaas-magic-cookie-', '');
// TODO: read from config
let recordingDisabled: boolean;
// TODO: read from config
let liveStreamingDisabled: boolean;
let recordingEnabled: boolean;
let liveStreamingEnabled: boolean;
let p: Participant;
let webhooksProxy: WebhookProxy;
@@ -29,18 +27,17 @@ describe('Recording and live-streaming', () => {
webhooksProxy = ctx.webhooksProxy;
p = await joinJaasMuc({ iFrameApi: true, token: t({ moderator: true }) }, { roomName: ctx.roomName });
// TODO: what should we do in this case? Add a config for this?
if (await p.execute(() => config.disableIframeAPI)) {
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
recordingEnabled = Boolean(await p.execute(() => config.recordingService?.enabled));
expect(recordingEnabled).toBe(expectations.jaas.recordingEnabled);
return;
liveStreamingEnabled = Boolean(await p.execute(() => config.liveStreaming?.enabled));
expect(liveStreamingEnabled).toBe(expectations.jaas.liveStreamingEnabled);
if (liveStreamingEnabled && !process.env.YTUBE_TEST_STREAM_KEY) {
liveStreamingEnabled = false;
console.log('Skipping live-streaming tests because YTUBE_TEST_STREAM_KEY is not set.');
}
// TODO: only read if config says so
recordingDisabled = Boolean(!await p.execute(() => config.recordingService?.enabled));
liveStreamingDisabled = Boolean(!await p.execute(() => config.liveStreaming?.enabled))
|| !process.env.YTUBE_TEST_STREAM_KEY;
await p.switchToMainFrame();
});
@@ -138,7 +135,7 @@ describe('Recording and live-streaming', () => {
}
it('start/stop recording using the iFrame command', async () => {
if (recordingDisabled) {
if (!recordingEnabled) {
return;
}
@@ -150,7 +147,7 @@ describe('Recording and live-streaming', () => {
});
it('start/stop recording using the iFrame function', async () => {
if (recordingDisabled) {
if (!recordingEnabled) {
return;
}
@@ -162,7 +159,7 @@ describe('Recording and live-streaming', () => {
});
it('start/stop live-streaming using the iFrame command', async () => {
if (liveStreamingDisabled) {
if (!liveStreamingEnabled) {
return;
}

View File

@@ -3,6 +3,7 @@ import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import type WebhookProxy from '../../helpers/WebhookProxy';
import { expectations } from '../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
@@ -25,12 +26,9 @@ describe('Transcription', () => {
token: t({ room, moderator: true }),
iFrameApi: true });
if (await p1.execute(() => config.disableIframeAPI || !config.transcription?.enabled)) {
// skip the test if iframeAPI or transcriptions are disabled
ctx.skipSuiteTests = 'The environment has the iFrame API or transcriptions disabled.';
const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
return;
}
expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
p2 = await joinJaasMuc({
name: 'p2',
@@ -57,10 +55,16 @@ describe('Transcription', () => {
await checkReceivingChunks(p1, p2, webhooksProxy);
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
await p1.getIframeAPI().executeCommand('toggleSubtitles');
// give it some time to process
await p1.driver.pause(5000);
await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 15000,
timeoutMsg: 'transcribingStatusChanged event not received by p1'
});
});
it('set subtitles on and off', async () => {
@@ -72,18 +76,23 @@ describe('Transcription', () => {
await checkReceivingChunks(p1, p2, webhooksProxy);
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p1.getIframeAPI().executeCommand('setSubtitles', false);
// give it some time to process
await p1.driver.pause(5000);
await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcribingStatusChanged'), {
timeout: 15000,
timeoutMsg: 'transcribingStatusChanged event not received by p1'
});
});
it('start/stop transcriptions via recording', async () => {
// we need to clear results or the last one will be used, from the previous time subtitles were on
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });

View File

@@ -2,6 +2,7 @@ import { expect } from '@wdio/globals';
import { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { expectations } from '../../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
setTestProperties(__filename, {
@@ -24,12 +25,13 @@ describe('Visitors live', () => {
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
});
// TODO: Remove this in favor of configurable test expectations
await moderator.driver.waitUntil(() => moderator.execute(() => APP.conference._room.isVisitorsSupported()), {
timeout: 2000
}).catch(e => {
ctx.skipSuiteTests = `Because isVisitorsSupported() returned an error: ${e}.`;
});
if (expectations.jaas.visitors) {
await moderator.driver.waitUntil(() => moderator.execute(() => APP.conference._room.isVisitorsSupported()), {
timeout: 2000
}).catch(e => {
throw new Error(`isVisitorsSupported() returned an error: ${e}.`);
});
}
visitor = await joinJaasMuc({
name: 'p2',

View File

@@ -66,10 +66,10 @@ async function testActiveSpeaker(
const otherParticipant1Driver = otherParticipant1.driver;
await otherParticipant1Driver.waitUntil(
async () => await otherParticipant1.getFilmstrip().isDominantSpeaker(speakerEndpoint),
async () => await otherParticipant1.getLargeVideo().getResource() === speakerEndpoint,
{
timeout: 30_000, // 30 seconds
timeoutMsg: `${activeSpeaker.name} is not selected as active speaker.`
timeoutMsg: 'Active speaker not displayed on large video.'
});
// just a debug print to go in logs

View File

@@ -49,7 +49,6 @@ describe('Codec selection', () => {
it('asymmetric codecs with AV1', async () => {
await ensureThreeParticipants({
configOverwrite: {
disableTileView: true,
videoQuality: {
codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ]
}
@@ -99,7 +98,6 @@ describe('Codec selection', () => {
await ensureThreeParticipants({
configOverwrite: {
disableTileView: true,
videoQuality: {
codecPreferenceOrder: [ 'VP8' ]
}

View File

@@ -152,7 +152,7 @@ describe('Desktop sharing', () => {
await checkForScreensharingTile(p1, p3);
await checkForScreensharingTile(p2, p3);
// Add another particpant to verify multiple screenshares are visible without gaps in filmstrip.
// Add another participant to verify multiple screenshares are visible without gaps in filmstrip.
await ensureFourParticipants({
configOverwrite: {
filmstrip: {

View File

@@ -92,7 +92,10 @@ describe('Dial-in', () => {
throw new Error('no pin');
}
expect(dialInPin.length >= 8).toBe(true);
if (!dialInPin.match(/^[0-9]+$/)) {
throw new Error(`The dial-in PIN contains non-digit characters: ${dialInPin}`);
}
expect(dialInPin.length).toBeGreaterThanOrEqual(expectations.dialIn.minPinLength);
});
it('skip the rest if a dial-in URL is not configured', async () => {

View File

@@ -1,6 +1,7 @@
import AllureReporter from '@wdio/allure-reporter';
import { multiremotebrowser } from '@wdio/globals';
import { Buffer } from 'buffer';
import fs from 'fs';
import { glob } from 'glob';
import path from 'node:path';
import process from 'node:process';
@@ -381,24 +382,25 @@ export const config: WebdriverIO.MultiremoteConfig = {
}));
const allProcessing: Promise<any>[] = [];
const attachments: { content: string | Buffer; filename: string; type: string; }[] = [];
multiremotebrowser.instances.forEach((instance: string) => {
const bInstance = multiremotebrowser.getInstance(instance);
allProcessing.push(bInstance.takeScreenshot().then(shot => {
AllureReporter.addAttachment(
`Screenshot-${instance}`,
Buffer.from(shot, 'base64'),
'image/png');
attachments.push({
filename: `${instance}-screenshot`,
content: Buffer.from(shot, 'base64'),
type: 'image/png' });
}));
// @ts-ignore
allProcessing.push(bInstance.execute(() => typeof APP !== 'undefined' && APP.connection?.getLogs())
.then(logs =>
logs && AllureReporter.addAttachment(
`debug-logs-${instance}`,
JSON.stringify(logs, null, ' '),
'text/plain'))
logs && attachments.push({
filename: `${instance}-debug-logs`,
content: JSON.stringify(logs, null, ' '),
type: 'text/plain' }))
.catch(e => console.error('Failed grabbing debug logs', e)));
allProcessing.push(
@@ -407,15 +409,29 @@ export const config: WebdriverIO.MultiremoteConfig = {
saveLogs(bInstance, res);
}
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
attachments.push({
filename: `${instance}-console-logs`,
content: getLogs(bInstance) || '',
type: 'text/plain' });
}));
allProcessing.push(bInstance.getPageSource().then(source => {
AllureReporter.addAttachment(`html-source-${instance}`, pretty(source), 'text/plain');
attachments.push({
filename: `${instance}-html-source`,
content: pretty(source),
type: 'text/plain' });
}));
});
await Promise.allSettled(allProcessing);
attachments.sort(
(a, b) => {
return a.filename < b.filename ? -1 : 1;
}).forEach(
a => {
AllureReporter.addAttachment(a.filename, a.content, a.type);
}
);
}
},
@@ -460,6 +476,15 @@ export const config: WebdriverIO.MultiremoteConfig = {
}
console.log('Allure report successfully generated');
// An ugly hack to sort by test order by default in the allure report.
const content = fs.readFileSync(`${TEST_RESULTS_DIR}/allure-report/index.html`, 'utf8');
const modifiedContent = content.replace('<body>',
'<body><script>localStorage.setItem("ALLURE_REPORT_SETTINGS_SUITES", \'{"treeSorting":{"sorter":"sorter.order","ascending":true}}\')</script>'
);
fs.writeFileSync(`${TEST_RESULTS_DIR}/allure-report/index.html`, modifiedContent);
resolve();
});
});

View File

@@ -250,7 +250,7 @@ function getDevServerConfig() {
warnings: false
}
},
host: '::',
host: 'localhost',
hot: true,
proxy: [
{