From 78ea43cad7976b320b3e44c51dcf6f25da011bd2 Mon Sep 17 00:00:00 2001 From: Alexis Faizeau Date: Fri, 1 Sep 2023 18:17:05 +0200 Subject: [PATCH] Deployment to master (#3468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix bad types on wam file type (#3393) * Modify Sentry release names (#3397) Co-authored-by: Alexis Faizeau * Bump @grpc/grpc-js from 1.7.3 to 1.8.8 in /messages Bumps [@grpc/grpc-js](https://github.com/grpc/grpc-node) from 1.7.3 to 1.8.8. - [Release notes](https://github.com/grpc/grpc-node/releases) - [Commits](https://github.com/grpc/grpc-node/compare/@grpc/grpc-js@1.7.3...@grpc/grpc-js@1.8.8) --- updated-dependencies: - dependency-name: "@grpc/grpc-js" dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Fix sentry release on dsn (#3402) Co-authored-by: Alexis Faizeau * Bump protobufjs in /libs/room-api-clients/room-api-client-js Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.2.2 to 7.2.4. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.2.2...protobufjs-v7.2.4) --- updated-dependencies: - dependency-name: protobufjs dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Bump tough-cookie from 4.1.2 to 4.1.3 Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/salesforce/tough-cookie/releases) - [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md) - [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3) --- updated-dependencies: - dependency-name: tough-cookie dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump protobufjs from 6.11.3 to 7.2.4 in /messages Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 6.11.3 to 7.2.4. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/v6.11.3...protobufjs-v7.2.4) --- updated-dependencies: - dependency-name: protobufjs dependency-type: direct:development ... Signed-off-by: dependabot[bot] * Bump protobufjs from 7.1.2 to 7.2.4 Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 7.1.2 to 7.2.4. - [Release notes](https://github.com/protobufjs/protobuf.js/releases) - [Changelog](https://github.com/protobufjs/protobuf.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/protobufjs/protobuf.js/compare/protobufjs-v7.1.2...protobufjs-v7.2.4) --- updated-dependencies: - dependency-name: protobufjs dependency-type: direct:production ... Signed-off-by: dependabot[bot] * Bump semver from 6.3.0 to 6.3.1 in /desktop/electron Bumps [semver](https://github.com/npm/node-semver) from 6.3.0 to 6.3.1. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v6.3.1/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v6.3.0...v6.3.1) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump semver from 5.7.1 to 5.7.2 Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump word-wrap from 1.2.3 to 1.2.4 in /desktop/electron Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump word-wrap from 1.2.3 to 1.2.4 in /tests Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump word-wrap from 1.2.3 to 1.2.4 in /messages Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump word-wrap from 1.2.3 to 1.2.4 Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] * Bump word-wrap in /libs/room-api-clients/room-api-client-js Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] * Fix positions on item menu (#3437) Co-authored-by: Alexis Faizeau * Removing doc blocks Now that we don't copy any more the Message files in different places, the comments added to the message files need to be removed. * Fix audio and video prefered (#3444) * Fix audio and video prefered * Set video and audio inputs on camera scene * Fix camera setting * Check if media devices are availables * feature: limit bandwidth of video and screenshare on p2p connections (2) (#3391) * feature: limit bandwidth of video and screenshare on p2p connections * fix: parse of max bandwidth for video and screenshare * fix: linting import/order * chore: revert import of webrtc-adapter, which isn't used * Limiting bandwidth in CD environment. * Add bandwisth settings on settings menu * Implement new colors from tailwind * Change bandwidth rates --------- Co-authored-by: Felipe Cecagno Co-authored-by: Nolway * Update README.md * Use real cursors on area editor (#3464) Co-authored-by: Alexis Faizeau * Fix no media input (#3462) * Implement screenSharing * Better handle of screenShare * Finishing focusScreen * Fixing UT back * Fix UT play * Pretty * Fix loader blocked * Fixing race condition that caused a pointless error to be displayed. * Adding a huge hack to detect screen shared shares in Jitsi correctly despite lib-jitsi-meet * Adding an intermediate notion of a JitsiTrackStreamWrapper that contains either audio/video or screensharing for a given JitsiTrackWrapper. * Refactoring an debugging JitsiTrackWrapper Now, handling correctly the switch to P2P streams. * Highlighting new screensharing in Jitsi. * Adding a subject inside space users to allow tracking if the screensharing has been enabled by a user or not * Do not reverse screen sharing * Displaying "my" camera in Jitsi at the bottom * Adding proper unsubscribers Adding constraints * Fix main video height * Fixing selector in test --------- Signed-off-by: dependabot[bot] Co-authored-by: Alexis Faizeau Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Négrier Co-authored-by: Felipe Cecagno Co-authored-by: grégoire parant Co-authored-by: César Cardinale --- .env.template | 6 + back/src/Model/Space.ts | 4 +- back/tests/Space.test.ts | 6 +- chat/src/Components/ChatMessageForm.svelte | 8 +- chat/src/Components/ChatMessagesList.svelte | 2 +- chat/src/Components/ChatUser.svelte | 8 +- contrib/docker/.env.prod.template | 6 + contrib/docker/README.md | 2 +- contrib/docker/docker-compose.prod.yaml | 4 + deeployer.libsonnet | 4 + desktop/electron/yarn.lock | 20 +- docker-compose.yaml | 4 + libs/messages/package.json | 2 +- .../messages/src/JsonMessages/AdminApiData.ts | 5 - .../src/JsonMessages/CompanionTextures.ts | 5 - .../messages/src/JsonMessages/ErrorApiData.ts | 5 - .../src/JsonMessages/MapDetailsData.ts | 5 - .../src/JsonMessages/PlayerTextures.ts | 5 - .../messages/src/JsonMessages/RegisterData.ts | 5 - .../messages/src/JsonMessages/RoomRedirect.ts | 5 - .../room-api-client-js/package-lock.json | 24 +- libs/tailwind/tailwind.config.js | 152 +++++-- messages/package-lock.json | 213 +++++---- messages/package.json | 4 +- messages/protos/messages.proto | 11 +- package-lock.json | 430 +++++++++--------- play/package.json | 2 +- .../src/common/FrontConfigurationInterface.ts | 4 + play/src/front/Api/Iframe/player.ts | 2 +- .../Components/ActionBar/ActionBar.svelte | 65 ++- .../ActionBar/MegaphoneConfirm.svelte | 16 +- .../Components/ActionsMenu/ActionsMenu.svelte | 67 +-- .../EmbedScreens/CamerasContainer.svelte | 6 +- .../EmbedScreens/Layouts/MozaicLayout.svelte | 2 +- .../Layouts/PresentationLayout.svelte | 5 +- .../EnableCamera/EnableCameraScene.svelte | 19 +- play/src/front/Components/MainLayout.svelte | 8 +- .../ConfigureMyRoom/Megaphone.svelte | 2 +- .../OpenWebsitePropertyEditor.svelte | 2 +- .../Components/Menu/SettingsSubMenu.svelte | 187 ++++---- .../front/Components/Video/ChatLayout.svelte | 2 +- .../Components/Video/JitsiAudioElement.svelte | 30 ++ .../Components/Video/JitsiMediaBox.svelte | 96 ++-- .../Components/Video/JitsiVideoElement.svelte | 39 ++ .../front/Components/Video/MediaBox.svelte | 4 +- play/src/front/Components/Video/utils.ts | 41 ++ .../Components/images/screenshare-off-alt.png | Bin 0 -> 2121 bytes play/src/front/Connection/LocalUserStore.ts | 89 +++- play/src/front/Connection/RoomConnection.ts | 11 + play/src/front/Enum/EnvironmentVariable.ts | 5 + .../Components/MapEditor/AreaPreview.ts | 18 +- .../MapEditor/SizeAlteringSquare.ts | 4 +- play/src/front/Phaser/Game/GameManager.ts | 25 +- play/src/front/Phaser/Game/GameScene.ts | 5 + .../Game/MapEditor/Tools/AreaEditorTool.ts | 50 +- play/src/front/Space/Space.ts | 26 +- play/src/front/Stores/MediaStore.ts | 315 +++++++------ play/src/front/Stores/MegaphoneStore.ts | 34 +- play/src/front/Stores/ScreenSharingStore.ts | 15 + .../front/Stores/StreamableCollectionStore.ts | 32 +- play/src/front/Stores/VideoFocusStore.ts | 4 +- play/src/front/Streaming/BroadcastService.ts | 39 +- .../Streaming/Jitsi/JitsiConferenceWrapper.ts | 205 ++++++--- .../Streaming/Jitsi/JitsiLocalTracksStore.ts | 8 +- .../Jitsi/JitsiTrackStreamWrapper.ts | 51 +++ .../Streaming/Jitsi/JitsiTrackWrapper.ts | 160 +++++-- play/src/front/WebRtc/ScreenSharingPeer.ts | 8 +- play/src/front/WebRtc/VideoPeer.ts | 9 +- .../front/style/wa-theme/action-menu-bar.scss | 6 +- play/src/front/style/wa-theme/video-ui.scss | 9 +- play/src/i18n/ca-ES/menu.ts | 30 +- play/src/i18n/de-DE/menu.ts | 30 +- play/src/i18n/en-US/megaphone.ts | 1 + play/src/i18n/en-US/menu.ts | 29 +- play/src/i18n/en-US/warning.ts | 2 +- play/src/i18n/es-ES/menu.ts | 30 +- play/src/i18n/fr-FR/menu.ts | 30 +- play/src/i18n/hsb-DE/menu.ts | 30 +- play/src/i18n/pt-BR/menu.ts | 28 +- play/src/i18n/zh-CN/menu.ts | 28 +- .../pusher/controllers/IoSocketController.ts | 10 +- play/src/pusher/enums/EnvironmentVariable.ts | 4 + .../enums/EnvironmentVariableValidator.ts | 6 + play/src/pusher/models/Space.ts | 13 +- .../models/Websocket/ExSocketInterface.ts | 1 + play/src/pusher/services/SocketManager.ts | 12 + .../tests/front/Components/Video/UtilsTest.ts | 129 ++++++ play/tests/pusher/Space.test.ts | 8 +- tests/package-lock.json | 12 +- tests/tests/api_players.spec.ts | 2 +- tests/tests/map_editor.spec.ts | 2 +- tests/tests/translate.spec.ts | 1 - tests/tests/utils/roles.ts | 7 +- 93 files changed, 2005 insertions(+), 1112 deletions(-) create mode 100644 play/src/front/Components/Video/JitsiAudioElement.svelte create mode 100644 play/src/front/Components/Video/JitsiVideoElement.svelte create mode 100644 play/src/front/Components/images/screenshare-off-alt.png create mode 100644 play/src/front/Streaming/Jitsi/JitsiTrackStreamWrapper.ts create mode 100644 play/tests/front/Components/Video/UtilsTest.ts diff --git a/.env.template b/.env.template index ae474ab3cc..b8ddd7076d 100644 --- a/.env.template +++ b/.env.template @@ -47,6 +47,12 @@ ACME_EMAIL= MAX_PER_GROUP=4 MAX_USERNAME_LENGTH=10 +# Configure low and recommended bandwidth used by video and screen share in the peer-to-peer connection (in kbit/s) +PEER_VIDEO_LOW_BANDWIDTH=150 +PEER_VIDEO_RECOMMENDED_BANDWIDTH=150 +PEER_SCREEN_SHARE_LOW_BANDWIDTH=250 +PEER_SCREEN_SHARE_RECOMMENDED_BANDWIDTH=1000 + OPID_CLIENT_ID= OPID_CLIENT_SECRET= OPID_CLIENT_ISSUER= diff --git a/back/src/Model/Space.ts b/back/src/Model/Space.ts index da362dd0b3..5f85f156fe 100644 --- a/back/src/Model/Space.ts +++ b/back/src/Model/Space.ts @@ -67,8 +67,8 @@ export class Space implements CustomJsonReplacerInterface { if (spaceUser.visitCardUrl) { user.visitCardUrl = spaceUser.visitCardUrl; } - if (spaceUser.screenSharing !== undefined) { - user.screenSharing = spaceUser.screenSharing; + if (spaceUser.screenSharingState !== undefined) { + user.screenSharingState = spaceUser.screenSharingState; } if (spaceUser.microphoneState !== undefined) { user.microphoneState = spaceUser.microphoneState; diff --git a/back/tests/Space.test.ts b/back/tests/Space.test.ts index bda1a4d83a..10230ac3fa 100644 --- a/back/tests/Space.test.ts +++ b/back/tests/Space.test.ts @@ -56,7 +56,7 @@ describe("Space", () => { cameraState: false, microphoneState: false, megaphoneState: false, - screenSharing: false, + screenSharingState: false, }); // Add user to space from watcher1 space.addUser(watcher1, spaceUser); @@ -94,7 +94,7 @@ describe("Space", () => { cameraState: true, microphoneState: true, megaphoneState: true, - screenSharing: true, + screenSharingState: true, visitCardUrl: "test2", }); @@ -117,7 +117,7 @@ describe("Space", () => { expect(user?.cameraState).toBe(true); expect(user?.microphoneState).toBe(true); expect(user?.megaphoneState).toBe(true); - expect(user?.screenSharing).toBe(true); + expect(user?.screenSharingState).toBe(true); expect(user?.visitCardUrl).toBe("test2"); } diff --git a/chat/src/Components/ChatMessageForm.svelte b/chat/src/Components/ChatMessageForm.svelte index 516ea6535d..69498726c6 100644 --- a/chat/src/Components/ChatMessageForm.svelte +++ b/chat/src/Components/ChatMessageForm.svelte @@ -334,7 +334,7 @@
@@ -360,7 +360,7 @@ {/if} {#if fileUploaded.errorCode === 423 && $me && $me.isAdmin}
+
+
analyticsClient.screenSharing()} @@ -496,6 +504,7 @@
{#if !$inExternalServiceStore && !$silentStore && $proximityMeetingStore} {#if $myCameraStore} +
analyticsClient.camera()} @@ -525,7 +534,7 @@ {/if} - {#if $requestedCameraState && $cameraListStore.length > 1} + {#if $requestedCameraState && $cameraListStore && $cameraListStore.length > 1} - {#if $requestedMicrophoneState && $microphoneListStore.length > 1} + {#if $requestedMicrophoneState && $microphoneListStore && $microphoneListStore.length > 1} +
+ {/if} + +
analyticsClient.openedChat()} on:click={toggleChat} @@ -688,6 +729,7 @@ {/if}
+
@@ -696,6 +738,7 @@
{#if $megaphoneCanBeUsedStore && !$silentStore && ($myMicrophoneStore || $myCameraStore)} +
{#if $streamingMegaphoneStore} @@ -708,7 +751,7 @@ {/if}
+
analyticsClient.openedMenu()} @@ -741,6 +785,7 @@
{#if $mapEditorActivated} +
{/if} {#if $userHasAccessToBackOfficeStore} +
analyticsClient.openBackOffice()} @@ -771,6 +817,7 @@ {#if $addActionButtonActionBarEvent.length > 0}
{#each $addActionButtonActionBarEvent as button} +
{#each $addClassicButtonActionBarEvent as button} +

- {#if !$requestedCameraState && !$requestedMicrophoneState} + {#if !$requestedCameraState && !$requestedMicrophoneState && !$requestedScreenSharingState} {$LL.warning.megaphoneNeeds()} {:else} {$LL.megaphone.modal.goingToStream()} {$requestedCameraState ? $LL.megaphone.modal.yourCamera() : ""} - {$requestedCameraState && $requestedMicrophoneState ? $LL.megaphone.modal.and() : ""} + {$requestedCameraState && $requestedMicrophoneState && !$requestedScreenSharingState + ? $LL.megaphone.modal.and() + : ""} + {$requestedCameraState && $requestedMicrophoneState && $requestedScreenSharingState ? "," : ""} {$requestedMicrophoneState ? $LL.megaphone.modal.yourMicrophone() : ""} + {($requestedCameraState || $requestedMicrophoneState) && $requestedScreenSharingState + ? $LL.megaphone.modal.and() + : ""} + {$requestedScreenSharingState ? $LL.megaphone.modal.yourScreen() : ""} {$LL.megaphone.modal.toAll()}. {/if}

@@ -26,7 +34,9 @@ - {#if actionsMenuData.menuName} -

{actionsMenuData.menuName}

- {/if} - {#if sortedActions} -
- {#each sortedActions ?? [] as action} - - {/each} -
- {/if} +
+
+ + {#if actionsMenuData.menuName} +

{actionsMenuData.menuName}

+ {/if} + {#if sortedActions} +
+ {#each sortedActions ?? [] as action} + + {/each} +
+ {/if} +
{/if} diff --git a/play/src/front/Components/Video/ChatLayout.svelte b/play/src/front/Components/Video/ChatLayout.svelte index 10572c24e4..e13ed4775b 100644 --- a/play/src/front/Components/Video/ChatLayout.svelte +++ b/play/src/front/Components/Video/ChatLayout.svelte @@ -29,7 +29,7 @@
- {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} + {#each [...$streamableCollectionStore] as [uniqueId, peer] (uniqueId)} {/each}
diff --git a/play/src/front/Components/Video/JitsiAudioElement.svelte b/play/src/front/Components/Video/JitsiAudioElement.svelte new file mode 100644 index 0000000000..fe28764e83 --- /dev/null +++ b/play/src/front/Components/Video/JitsiAudioElement.svelte @@ -0,0 +1,30 @@ + + +
-{:else if streamable instanceof JitsiTrackWrapper} +{:else if streamable instanceof JitsiTrackStreamWrapper}
>> 0) * 1000 : bandwidth; + const nextBWPos = sdp.indexOf(`b=${modifier}:`, targetMediaPos + 1); + + let mediaSlice = sdp.slice(targetMediaPos); + const bwFieldAlreadyExists = nextBWPos !== -1 && (nextBWPos < nextMediaPos || nextMediaPos === -1); + if (bwFieldAlreadyExists) { + // delete it + mediaSlice = mediaSlice.replace(new RegExp(`b=${modifier}:.*[\r?\n]`), ""); + } + // insert b= after c= line. + mediaSlice = mediaSlice.replace(/c=IN (.*)(\r?\n)/, `c=IN $1$2b=${modifier}:${newBandwidth}$2`); + + // update the sdp + sdp = sdp.slice(0, targetMediaPos) + mediaSlice; + } + } + + return sdp; +} diff --git a/play/src/front/Components/images/screenshare-off-alt.png b/play/src/front/Components/images/screenshare-off-alt.png new file mode 100644 index 0000000000000000000000000000000000000000..2fe7aa287ddf090f23b8fe1e41d5e2dab34da4a0 GIT binary patch literal 2121 zcmbVN2~ZPP7+y{hv}m;zZIMm1ULo1tY!YNiL=0A-1Qk-njt9$T2`fo9-7F+HqE$+)TCjp>>(RFysqNUyOg8)W?f1U_|Nbj0 zXT(h#=sUs}f}nxXQIYZBjtZBLH~5|H{@4j_{jE`RY!Ku(M7Z3by!-$N^6X^viF~3i zmY`U(j5M&xw9H|)0yG4v!yQ(VN~3u=nKm*OjkxXTaWTvoG~(Gx9jdcx=~O0a5l1I1 ziqlhz(x^~_ID8VUb`XHTO!Fk{FqcN2&pa8!jlfE#Wt#sA(FOH9Ao7f)&dKRWHOu1 zYsA3Y9vNn)46QC)EBeaWx~c!%>Xib1M*U|O`mC{k$Bq1rgIcMH3PUgN>F1Z zv>Z+HET?B#Q(v-X^tlYn6*3$irz0uGB8V9O!Vx->Ca?kb;@>s#7SGq|t;maH)^ z!AH({o>I}((GlEeQoU7C*V*>d)SLCk?`P{LWVw~EkC|?2?8=J>sZ_4*@T%Ybouc+& ziO=FOm)@}s*ydJLXAgK(*g05TS#s>YHVQAFwBmT|gS#a^J7*-bb?Pv}XTD#SxBpQI zIwHHWDr%rlwlgo7hjPQ32OKspZ$H(qN*gXgy~b2(cXt%Tw=x$h%i7PeKJN!;=f@NU z#S{%Wgb%%DpIqRbf8dAfOE0LuGEIB$QBh-k>&QX-;+7>ed)~TV{cvZd3fVCKZvTSA z_qGPW#&wdK73lpVlI?3h4RD-)O?{X7$kDWIYw@hg1MAaL`n-3}^vPj+J9$%?&A{&Bi-5pRBs;l*B&VoLu*1*gl&h$FC3{kN11#n@g!a$UZc|x!gI?Bd+L)bpFzB z9f7eek1q{VR}V)wa9--9X1@UqX969mjvO5R{Fm6h?nWfB)6M_4t=X0x<$=BkNPmx0 zkpEGUUgW zxp_}+E6zy@ zc<8o!eoNXGHhFlQ80NX;WZ52NlIJ%6YYkx{g}Vb~)KSkCoY}5M#-6|BsF=4QCaoqc zLvn2RAfND)PjkDeMAxpzv^ G^4|eE#SIVu literal 0 HcmV?d00001 diff --git a/play/src/front/Connection/LocalUserStore.ts b/play/src/front/Connection/LocalUserStore.ts index 950d2ee596..969d8daca9 100644 --- a/play/src/front/Connection/LocalUserStore.ts +++ b/play/src/front/Connection/LocalUserStore.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { PEER_SCREEN_SHARE_RECOMMENDED_BANDWIDTH, PEER_VIDEO_RECOMMENDED_BANDWIDTH } from "../Enum/EnvironmentVariable"; import { arrayEmoji, Emoji } from "../Stores/Utils/emojiSchema"; import type { LocalUser } from "./LocalUser"; import { areCharacterTexturesValid, isUserNameValid } from "./LocalUser"; @@ -10,7 +11,6 @@ const requestedCameraStateKey = "requestedCameraStateKey"; const requestedMicrophoneStateKey = "requestedMicrophoneStateKey"; const characterTexturesKey = "characterTextures"; const companionKey = "companion"; -const videoQualityKey = "videoQuality"; const audioPlayerVolumeKey = "audioVolume"; const audioPlayerMuteKey = "audioMute"; const helpCameraSettingsShown = "helpCameraSettingsShown"; @@ -22,7 +22,8 @@ const lastRoomUrl = "lastRoomUrl"; const authToken = "authToken"; const notification = "notificationPermission"; const chatSounds = "chatSounds"; -const cameraSetup = "cameraSetup"; +const preferredVideoInputDevice = "preferredVideoInputDevice"; +const preferredAudioInputDevice = "preferredAudioInputDevice"; const cacheAPIIndex = "workavdenture-cache"; const userProperties = "user-properties"; const cameraPrivacySettings = "cameraPrivacySettings"; @@ -128,14 +129,6 @@ class LocalUserStore { return localStorage.getItem(companionKey) ? true : false; } - setVideoQualityValue(value: number): void { - localStorage.setItem(videoQualityKey, "" + value); - } - - getVideoQualityValue(): number { - return parseInt(localStorage.getItem(videoQualityKey) || "20"); - } - setAudioPlayerVolume(value: number): void { localStorage.setItem(audioPlayerVolumeKey, "" + value); } @@ -269,13 +262,43 @@ class LocalUserStore { return localStorage.getItem(chatSounds) !== "false"; } - setCameraSetup(cameraId: string) { - localStorage.setItem(cameraSetup, cameraId); + setPreferredVideoInputDevice(deviceId?: string) { + console.log("setPreferredVideoInputDevice", deviceId); + if (deviceId === undefined) { + localStorage.removeItem(preferredVideoInputDevice); + return; + } + + localStorage.setItem(preferredVideoInputDevice, deviceId); } - getCameraSetup(): { video: unknown; audio: unknown } | undefined { - const cameraSetupValues = localStorage.getItem(cameraSetup); - return cameraSetupValues != undefined ? JSON.parse(cameraSetupValues) : undefined; + setPreferredAudioInputDevice(deviceId?: string) { + if (deviceId === undefined) { + localStorage.removeItem(preferredAudioInputDevice); + return; + } + + localStorage.setItem(preferredAudioInputDevice, deviceId); + } + + getPreferredVideoInputDevice(): string | undefined { + const deviceId = localStorage.getItem(preferredVideoInputDevice); + + if (deviceId === null) { + return undefined; + } + + return deviceId; + } + + getPreferredAudioInputDevice(): string | undefined { + const deviceId = localStorage.getItem(preferredAudioInputDevice); + + if (deviceId === null) { + return undefined; + } + + return deviceId; } setCameraPrivacySettings(option: boolean) { @@ -421,6 +444,42 @@ class LocalUserStore { getSpeakerDeviceId() { return localStorage.getItem(speakerDeviceId); } + + setVideoBandwidth(value: number | "unlimited") { + localStorage.setItem("videoBandwidth", value.toString()); + } + + getVideoBandwidth(): number | "unlimited" { + const value = localStorage.getItem("videoBandwidth"); + + if (!value) { + return PEER_VIDEO_RECOMMENDED_BANDWIDTH; + } + + if (value === "unlimited") { + return value; + } + + return parseInt(value); + } + + setScreenShareBandwidth(value: number | "unlimited") { + localStorage.setItem("screenShareBandwidth", value.toString()); + } + + getScreenShareBandwidth(): number | "unlimited" { + const value = localStorage.getItem("screenShareBandwidth"); + + if (!value) { + return PEER_SCREEN_SHARE_RECOMMENDED_BANDWIDTH; + } + + if (value === "unlimited") { + return value; + } + + return parseInt(value); + } } export const localUserStore = new LocalUserStore(); diff --git a/play/src/front/Connection/RoomConnection.ts b/play/src/front/Connection/RoomConnection.ts index f52f581dbc..b9428d0bdd 100644 --- a/play/src/front/Connection/RoomConnection.ts +++ b/play/src/front/Connection/RoomConnection.ts @@ -1442,6 +1442,17 @@ export class RoomConnection implements RoomConnection { }); } + public emitScreenSharingState(state: boolean) { + this.send({ + message: { + $case: "screenSharingStateMessage", + screenSharingStateMessage: { + value: state, + }, + }, + }); + } + public emitMegaphoneState(state: boolean) { const currentMegaphoneName = get(currentMegaphoneNameStore); this.send({ diff --git a/play/src/front/Enum/EnvironmentVariable.ts b/play/src/front/Enum/EnvironmentVariable.ts index 65ab4d759a..604e2887de 100644 --- a/play/src/front/Enum/EnvironmentVariable.ts +++ b/play/src/front/Enum/EnvironmentVariable.ts @@ -39,6 +39,11 @@ export const OPID_WOKA_NAME_POLICY = env.OPID_WOKA_NAME_POLICY; export const ENABLE_REPORT_ISSUES_MENU = env.ENABLE_REPORT_ISSUES_MENU; export const REPORT_ISSUES_URL = env.REPORT_ISSUES_URL; +export const PEER_VIDEO_LOW_BANDWIDTH = env.PEER_VIDEO_LOW_BANDWIDTH; +export const PEER_VIDEO_RECOMMENDED_BANDWIDTH = env.PEER_VIDEO_RECOMMENDED_BANDWIDTH; +export const PEER_SCREEN_SHARE_LOW_BANDWIDTH = env.PEER_SCREEN_SHARE_LOW_BANDWIDTH; +export const PEER_SCREEN_SHARE_RECOMMENDED_BANDWIDTH = env.PEER_SCREEN_SHARE_RECOMMENDED_BANDWIDTH; + export const POSITION_DELAY = 200; // Wait 200ms between sending position events export const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player diff --git a/play/src/front/Phaser/Components/MapEditor/AreaPreview.ts b/play/src/front/Phaser/Components/MapEditor/AreaPreview.ts index c58d69f191..6a91477044 100644 --- a/play/src/front/Phaser/Components/MapEditor/AreaPreview.ts +++ b/play/src/front/Phaser/Components/MapEditor/AreaPreview.ts @@ -56,21 +56,21 @@ export class AreaPreview extends Phaser.GameObjects.Rectangle { this.squareSelected = false; this.squares = [ - new SizeAlteringSquare(this.scene, this.getTopLeft()), - new SizeAlteringSquare(this.scene, this.getTopCenter()), - new SizeAlteringSquare(this.scene, this.getTopRight()), - new SizeAlteringSquare(this.scene, this.getLeftCenter()), - new SizeAlteringSquare(this.scene, this.getRightCenter()), - new SizeAlteringSquare(this.scene, this.getBottomLeft()), - new SizeAlteringSquare(this.scene, this.getBottomCenter()), - new SizeAlteringSquare(this.scene, this.getBottomRight()), + new SizeAlteringSquare(this.scene, this.getTopLeft(), "nw-resize"), + new SizeAlteringSquare(this.scene, this.getTopCenter(), "n-resize"), + new SizeAlteringSquare(this.scene, this.getTopRight(), "ne-resize"), + new SizeAlteringSquare(this.scene, this.getLeftCenter(), "w-resize"), + new SizeAlteringSquare(this.scene, this.getRightCenter(), "e-resize"), + new SizeAlteringSquare(this.scene, this.getBottomLeft(), "sw-resize"), + new SizeAlteringSquare(this.scene, this.getBottomCenter(), "s-resize"), + new SizeAlteringSquare(this.scene, this.getBottomRight(), "se-resize"), ]; this.squares.forEach((square) => square.setDepth(this.depth + 1)); const bounds = this.getBounds(); this.setSize(bounds.width, bounds.height); - this.setInteractive({ cursor: "pointer" }); + this.setInteractive({ cursor: "grab" }); this.scene.input.setDraggable(this); this.showSizeAlteringSquares(false); diff --git a/play/src/front/Phaser/Components/MapEditor/SizeAlteringSquare.ts b/play/src/front/Phaser/Components/MapEditor/SizeAlteringSquare.ts index 5e693adf6c..30a4c3c099 100644 --- a/play/src/front/Phaser/Components/MapEditor/SizeAlteringSquare.ts +++ b/play/src/front/Phaser/Components/MapEditor/SizeAlteringSquare.ts @@ -20,13 +20,13 @@ export enum SizeAlteringSquareEvent { export class SizeAlteringSquare extends Phaser.GameObjects.Rectangle { private selected: boolean; - constructor(scene: Phaser.Scene, pos: { x: number; y: number }) { + constructor(scene: Phaser.Scene, pos: { x: number; y: number }, private cursor: string) { super(scene, pos.x, pos.y, 7, 7, 0xffffff); this.selected = false; this.setStrokeStyle(1, 0x000000); - this.setInteractive({ cursor: "pointer" }); + this.setInteractive({ cursor }); this.scene.input.setDraggable(this); this.bindEventHandlers(); diff --git a/play/src/front/Phaser/Game/GameManager.ts b/play/src/front/Phaser/Game/GameManager.ts index a80d43d50b..389bd01534 100644 --- a/play/src/front/Phaser/Game/GameManager.ts +++ b/play/src/front/Phaser/Game/GameManager.ts @@ -3,7 +3,12 @@ import { connectionManager } from "../../Connection/ConnectionManager"; import { localUserStore } from "../../Connection/LocalUserStore"; import type { Room } from "../../Connection/Room"; import { helpCameraSettingsVisibleStore } from "../../Stores/HelpSettingsStore"; -import { requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore"; +import { + requestedCameraDeviceIdStore, + requestedCameraState, + requestedMicrophoneDeviceIdStore, + requestedMicrophoneState, +} from "../../Stores/MediaStore"; import { menuIconVisiblilityStore } from "../../Stores/MenuStore"; import { EnableCameraSceneName } from "../Login/EnableCameraScene"; import { LoginSceneName } from "../Login/LoginScene"; @@ -21,7 +26,6 @@ export class GameManager { private characterTextureIds: string[] | null; private companionTextureId: string | null; private startRoom!: Room; - private cameraSetup?: { video: unknown; audio: unknown }; private currentGameSceneName: string | null = null; // Note: this scenePlugin is the scenePlugin of the EntryScene. We should always provide a key in methods called on this scenePlugin. private scenePlugin!: Phaser.Scenes.ScenePlugin; @@ -31,7 +35,6 @@ export class GameManager { this.playerName = localUserStore.getName(); this.characterTextureIds = localUserStore.getCharacterTextures(); this.companionTextureId = localUserStore.getCompanionTextureId(); - this.cameraSetup = localUserStore.getCameraSetup(); } public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise { @@ -46,6 +49,12 @@ export class GameManager { this.startRoom = result; this.loadMap(this.startRoom); + const preferredAudioInputDeviceId = localUserStore.getPreferredAudioInputDevice(); + const preferredVideoInputDeviceId = localUserStore.getPreferredVideoInputDevice(); + + console.info("Preferred audio input device: " + preferredAudioInputDeviceId); + console.info("Preferred video input device: " + preferredVideoInputDeviceId); + //If player name was not set show login scene with player name //If Room si not public and Auth was not set, show login scene to authenticate user (OpenID - SSO - Anonymous) if (!this.playerName || (this.startRoom.authenticationMandatory && !localUserStore.getAuthToken())) { @@ -53,9 +62,17 @@ export class GameManager { } else if (!this.characterTextureIds) { console.info("Any Woka texture has been found, you will be redirect to the Woka selection scene"); return SelectCharacterSceneName; - } else if (this.cameraSetup == undefined) { + } else if (preferredVideoInputDeviceId === undefined || preferredAudioInputDeviceId === undefined) { return EnableCameraSceneName; } else { + if (preferredVideoInputDeviceId !== "") { + requestedCameraDeviceIdStore.set(preferredVideoInputDeviceId); + } + + if (preferredAudioInputDeviceId !== "") { + requestedMicrophoneDeviceIdStore.set(preferredAudioInputDeviceId); + } + this.activeMenuSceneAndHelpCameraSettings(); //TODO fix to return href with # saved in localstorage return this.startRoom.key; diff --git a/play/src/front/Phaser/Game/GameScene.ts b/play/src/front/Phaser/Game/GameScene.ts index 6cc6293150..719d028094 100644 --- a/play/src/front/Phaser/Game/GameScene.ts +++ b/play/src/front/Phaser/Game/GameScene.ts @@ -132,6 +132,7 @@ import { megaphoneCanBeUsedStore, megaphoneEnabledStore } from "../../Stores/Meg import { CompanionTextureError } from "../../Exception/CompanionTextureError"; import { SelectCompanionScene, SelectCompanionSceneName } from "../Login/SelectCompanionScene"; import { scriptUtils } from "../../Api/ScriptUtils"; +import { requestedScreenSharingState } from "../../Stores/ScreenSharingStore"; import { GameMapFrontWrapper } from "./GameMap/GameMapFrontWrapper"; import { gameManager } from "./GameManager"; import { EmoteManager } from "./EmoteManager"; @@ -1306,6 +1307,10 @@ export class GameScene extends DirtyScene { this.connection?.emitMicrophoneState(state); }); + requestedScreenSharingState.subscribe((state) => { + this.connection?.emitScreenSharingState(state); + }); + megaphoneEnabledStore.subscribe((state) => { this.connection?.emitMegaphoneState(state); }); diff --git a/play/src/front/Phaser/Game/MapEditor/Tools/AreaEditorTool.ts b/play/src/front/Phaser/Game/MapEditor/Tools/AreaEditorTool.ts index d78e7265d5..97110def79 100644 --- a/play/src/front/Phaser/Game/MapEditor/Tools/AreaEditorTool.ts +++ b/play/src/front/Phaser/Game/MapEditor/Tools/AreaEditorTool.ts @@ -89,19 +89,19 @@ export class AreaEditorTool extends MapEditorTool { this.drawinNewAreaStartPos = undefined; mapEditorSelectedAreaPreviewStore.set(undefined); this.setAreaPreviewsVisibility(false); - this.scene.input.setDefaultCursor("auto"); + this.scene.input.setDefaultCursor("crosshair"); this.unbindEventHandlers(); this.scene.markDirty(); } public activate(): void { this.active = true; - this.scene.input.topOnly = false; + this.scene.input.setTopOnly(false); this.updateAreaPreviews(); this.setAreaPreviewsVisibility(true); this.bindEventHandlers(); if (get(mapEditorAreaModeStore) === "ADD") { - this.scene.input.setDefaultCursor("copy"); + this.scene.input.setDefaultCursor("crosshair"); } this.scene.markDirty(); } @@ -109,7 +109,7 @@ export class AreaEditorTool extends MapEditorTool { public destroy(): void { this.selectedAreaPreviewStoreSubscriber(); this.unbindEventHandlers(); - this.scene.input.setDefaultCursor("auto"); + this.scene.input.setDefaultCursor("crosshair"); } public async handleIncomingCommandMessage(editMapCommandMessage: EditMapCommandMessage): Promise { @@ -179,6 +179,9 @@ export class AreaEditorTool extends MapEditorTool { .executeCommand( new DeleteAreaFrontCommand(this.scene.getGameMap(), areaPreview.getId(), undefined, this, true) ) + .then(() => { + this.scene.input.setDefaultCursor("crosshair"); + }) .catch((e) => console.error(e)); break; } @@ -204,7 +207,9 @@ export class AreaEditorTool extends MapEditorTool { this.scene.input.on(Phaser.Input.Events.POINTER_UP, this.pointerUpEventHandler); this.scene.input.on(Phaser.Input.Events.POINTER_DOWN, this.pointerDownEventHandler); + this.scene.input.on(Phaser.Input.Events.POINTER_OVER, this.pointerHoverEventHandler); this.scene.input.on(Phaser.Input.Events.POINTER_MOVE, this.pointerMoveEventHandler); + this.scene.input.on(Phaser.Input.Events.POINTER_OUT, this.pointerOutEventHandler); this.shiftKey?.on(Phaser.Input.Keyboard.Events.DOWN, () => { if (this.drawingNewArea && this.drawinNewAreaStartPos) { @@ -218,19 +223,48 @@ export class AreaEditorTool extends MapEditorTool { } }); this.ctrlKey?.on(Phaser.Input.Keyboard.Events.DOWN, () => { - this.scene.input.setDefaultCursor("copy"); + this.scene.input.setDefaultCursor("crosshair"); }); this.ctrlKey?.on(Phaser.Input.Keyboard.Events.UP, () => { - this.scene.input.setDefaultCursor("auto"); + this.scene.input.setDefaultCursor("grab"); }); } private unbindEventHandlers(): void { this.scene.input.off(Phaser.Input.Events.POINTER_UP, this.pointerUpEventHandler); this.scene.input.off(Phaser.Input.Events.POINTER_DOWN, this.pointerDownEventHandler); + this.scene.input.off(Phaser.Input.Events.POINTER_OVER, this.pointerHoverEventHandler); this.scene.input.off(Phaser.Input.Events.POINTER_MOVE, this.pointerMoveEventHandler); + this.scene.input.off(Phaser.Input.Events.POINTER_OUT, this.pointerOutEventHandler); } + private pointerHoverEventHandler = ( + pointer: Phaser.Input.Pointer, + gameObjects: Phaser.GameObjects.GameObject[] + ) => { + if (!this.active) { + return; + } + const areaEditorToolObjects = this.getAreaEditorToolObjectsFromGameObjects(gameObjects); + if (areaEditorToolObjects.length === 1) { + if (this.isAreaPreview(areaEditorToolObjects[0])) { + this.scene.input.setDefaultCursor("grab"); + } + } + }; + + private pointerOutEventHandler = (pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]) => { + if (!this.active) { + return; + } + const areaEditorToolObjects = this.getAreaEditorToolObjectsFromGameObjects(gameObjects); + if (areaEditorToolObjects.length === 1) { + if (this.isAreaPreview(areaEditorToolObjects[0])) { + this.scene.input.setDefaultCursor("crosshair"); + } + } + }; + private handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void { const areaEditorToolObjects = this.getAreaEditorToolObjectsFromGameObjects(gameObjects); if (pointer.rightButtonDown()) { @@ -259,6 +293,7 @@ export class AreaEditorTool extends MapEditorTool { if (areaEditorToolObjects.length === 1) { if (this.isAreaPreview(areaEditorToolObjects[0])) { this.changeAreaMode("EDIT", areaEditorToolObjects[0]); + this.scene.input.setDefaultCursor("grabbing"); this.wasAreaMoved = true; } } @@ -307,6 +342,7 @@ export class AreaEditorTool extends MapEditorTool { if (this.wasAreaMoved) { this.draggingdArea = false; this.wasAreaMoved = false; + this.scene.input.setDefaultCursor("grab"); } else { const nextAreaIndex = (sortedAreaPreviews.indexOf(currentlySelectedArea) + 1) % sortedAreaPreviews.length; @@ -391,7 +427,7 @@ export class AreaEditorTool extends MapEditorTool { private changeAreaMode(mode: MapEditorAreaToolMode, areaPreview?: AreaPreview): void { mapEditorAreaModeStore.set(mode); - this.scene.input.setDefaultCursor(mode === "ADD" ? "copy" : "auto"); + this.scene.input.setDefaultCursor("crosshair"); if (document.activeElement instanceof HTMLElement) { document.activeElement.blur(); } diff --git a/play/src/front/Space/Space.ts b/play/src/front/Space/Space.ts index 0274ae0f0c..969fa77603 100644 --- a/play/src/front/Space/Space.ts +++ b/play/src/front/Space/Space.ts @@ -1,7 +1,7 @@ -import { SpaceFilterMessage, SpaceUser } from "@workadventure/messages"; +import { PartialSpaceUser, SpaceFilterMessage, SpaceUser } from "@workadventure/messages"; import { MapStore } from "@workadventure/store-utils"; import debug from "debug"; -import { Subscription } from "rxjs"; +import { Subject, Subscription } from "rxjs"; import { Readable } from "svelte/store"; import { RoomConnection } from "../Connection/RoomConnection"; import { CharacterLayerManager } from "../Phaser/Entity/CharacterLayerManager"; @@ -9,6 +9,10 @@ import { CharacterLayerManager } from "../Phaser/Entity/CharacterLayerManager"; export interface SpaceUserExtended extends SpaceUser { wokaPromise: Promise | undefined; getWokaBase64(): Promise; + updateSubject: Subject<{ + newUser: SpaceUserExtended; + changes: PartialSpaceUser; + }>; } const spaceLogger = debug("space"); @@ -75,8 +79,8 @@ export class Space { if (partialUser.megaphoneState !== undefined) { user.megaphoneState = partialUser.megaphoneState; } - if (partialUser.screenSharing !== undefined) { - user.screenSharing = partialUser.screenSharing; + if (partialUser.screenSharingState !== undefined) { + user.screenSharingState = partialUser.screenSharingState; } if (partialUser.jitsiParticipantId !== undefined) { user.jitsiParticipantId = partialUser.jitsiParticipantId; @@ -84,6 +88,10 @@ export class Space { if (partialUser.uuid !== undefined) { user.uuid = partialUser.uuid; } + user.updateSubject.next({ + newUser: user, + changes: partialUser, + }); this._users.set(partialUser.id, user); } } @@ -93,7 +101,11 @@ export class Space { this.connection.removeSpaceUserMessageStream.subscribe((message) => { spaceLogger(`Space => ${this.name} => removeSpaceUserMessageStream`, message); if (message.spaceName === name && message.filterName === spaceFilter.filterName) { - this._users.delete(message.userId); + const user = this._users.get(message.userId); + if (user !== undefined) { + user.updateSubject.complete(); + this._users.delete(message.userId); + } } }) ); @@ -122,6 +134,10 @@ export class Space { } return this.wokaPromise; }, + updateSubject: new Subject<{ + newUser: SpaceUserExtended; + changes: PartialSpaceUser; + }>(), }; } } diff --git a/play/src/front/Stores/MediaStore.ts b/play/src/front/Stores/MediaStore.ts index 81471e75d8..a49ec18ff8 100644 --- a/play/src/front/Stores/MediaStore.ts +++ b/play/src/front/Stores/MediaStore.ts @@ -4,7 +4,7 @@ import deepEqual from "fast-deep-equal"; import { AvailabilityStatus } from "@workadventure/messages"; import { localUserStore } from "../Connection/LocalUserStore"; import { HtmlUtils } from "../WebRtc/HtmlUtils"; -import { getNavigatorType, isIOS, NavigatorType } from "../WebRtc/DeviceUtils"; +import { isIOS } from "../WebRtc/DeviceUtils"; import { ObtainedMediaStreamConstraints } from "../WebRtc/P2PMessages/ConstraintMessage"; import { isMediaBreakpointUp } from "../Utils/BreakpointsUtils"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; @@ -142,6 +142,40 @@ const userMoved5SecondsAgoStore = readable(false, function start(set) { }; }); +/** + * A store awaiting the loading of devices information. + */ +const devicesNotLoaded = writable(true); + +const deviceChanged10SecondsAgoStore = readable(false, function start(set) { + let timeout: NodeJS.Timeout | null = null; + + const unsubscribeCamera = videoConstraintStore.subscribe((constraints) => { + if (timeout) { + clearTimeout(timeout); + } + set(true); + timeout = setTimeout(() => { + set(false); + }, 10000); + }); + + const unsubscribeAudio = audioConstraintStore.subscribe((constraints) => { + if (timeout) { + clearTimeout(timeout); + } + set(true); + timeout = setTimeout(() => { + set(false); + }, 10000); + }); + + return function stop() { + unsubscribeCamera(); + unsubscribeAudio(); + }; +}); + /** * A store containing whether the mouse is getting close the bottom right corner. */ @@ -183,40 +217,15 @@ export const cameraNoEnergySavingStore = writable(false); export const streamingMegaphoneStore = writable(false); -/** - * A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation. - */ -export const cameraEnergySavingStore = derived( - [ - userMoved5SecondsAgoStore, - peerStore, - enabledWebCam10secondsAgoStore, - mouseInCameraTriggerArea, - cameraNoEnergySavingStore, - streamingMegaphoneStore, - ], - ([ - $userMoved5SecondsAgoStore, - $peerStore, - $enabledWebCam10secondsAgoStore, - $mouseInBottomRight, - $cameraNoEnergySavingStore, - $streamingMegaphoneStore, - ]) => { - return ( - !$mouseInBottomRight && - !$userMoved5SecondsAgoStore && - $peerStore.size === 0 && - !$enabledWebCam10secondsAgoStore && - !$cameraNoEnergySavingStore && - !$streamingMegaphoneStore - ); - } +export const requestedCameraDeviceIdStore: Writable = writable( + localUserStore.getPreferredVideoInputDevice() ? localUserStore.getPreferredVideoInputDevice() : undefined ); -export const requestedCameraDeviceIdStore: Writable = writable(); export const frameRateStore: Writable = writable(); -export const requestedMicrophoneDeviceIdStore: Writable = writable(); +export const requestedMicrophoneDeviceIdStore: Writable = writable( + localUserStore.getPreferredAudioInputDevice() ? localUserStore.getPreferredAudioInputDevice() : undefined +); + export const usedCameraDeviceIdStore: Writable = writable(); export const usedMicrophoneDeviceIdStore: Writable = writable(); @@ -231,13 +240,14 @@ export const videoConstraintStore = derived( const constraints = { width: { min: 640, ideal: 1280, max: 1920 }, height: { min: 400, ideal: 720, max: 1080 }, - frameRate: { ideal: localUserStore.getVideoQualityValue() }, + frameRate: { ideal: undefined }, facingMode: "user", resizeMode: "crop-and-scale", aspectRatio: 1.777777778, } as MediaTrackConstraints; if ($cameraDeviceIdStore !== undefined) { + console.log("Using camera device ID", $cameraDeviceIdStore); constraints.deviceId = { exact: $cameraDeviceIdStore, }; @@ -264,12 +274,53 @@ export const audioConstraintStore = derived(requestedMicrophoneDeviceIdStore, ($ if (typeof constraints === "boolean") { constraints = {}; } - if ($microphoneDeviceIdStore !== undefined && navigator.mediaDevices.getSupportedConstraints().deviceId === true) { + if ( + $microphoneDeviceIdStore !== undefined && + navigator.mediaDevices && + navigator.mediaDevices.getSupportedConstraints().deviceId === true + ) { constraints.deviceId = { exact: $microphoneDeviceIdStore }; } return constraints; }); +/** + * A store that contains "true" if the webcam should be stopped for energy efficiency reason - i.e. we are not moving and not in a conversation. + */ +export const cameraEnergySavingStore = derived( + [ + deviceChanged10SecondsAgoStore, + userMoved5SecondsAgoStore, + peerStore, + enabledWebCam10secondsAgoStore, + mouseInCameraTriggerArea, + cameraNoEnergySavingStore, + streamingMegaphoneStore, + devicesNotLoaded, + ], + ([ + $deviceChanged10SecondsAgoStore, + $userMoved5SecondsAgoStore, + $peerStore, + $enabledWebCam10secondsAgoStore, + $mouseInBottomRight, + $cameraNoEnergySavingStore, + $streamingMegaphoneStore, + $devicesNotLoaded, + ]) => { + return ( + !$mouseInBottomRight && + !$userMoved5SecondsAgoStore && + !$deviceChanged10SecondsAgoStore && + $peerStore.size === 0 && + !$enabledWebCam10secondsAgoStore && + !$cameraNoEnergySavingStore && + !$devicesNotLoaded && + !$streamingMegaphoneStore + ); + } +); + export const inJitsiStore = writable(false); export const inBbbStore = writable(false); export const isSpeakerStore = writable(false); @@ -417,14 +468,7 @@ export const mediaStreamConstraintsStore = derived( }); } - if ($enableCameraSceneVisibilityStore) { - localUserStore.setCameraSetup( - JSON.stringify({ - video: currentVideoConstraint, - audio: currentAudioConstraint, - }) - ); - } + console.info("Media constraints changed", currentVideoConstraint, currentAudioConstraint); }, { video: false, @@ -446,46 +490,6 @@ interface StreamErrorValue { let currentStream: MediaStream | null = null; let oldConstraints = { video: false, audio: false }; -//only firefox correctly implements the 'enabled' track property, on chrome we have to stop the track then reinstantiate the stream -const implementCorrectTrackBehavior = getNavigatorType() === NavigatorType.firefox; - -/** - * Stops the camera from filming - */ -async function applyCameraConstraints( - currentStream: MediaStream | null, - constraints: MediaTrackConstraints | boolean -): Promise { - if (!currentStream) { - return []; - } - return Promise.all(currentStream.getVideoTracks().map((track) => toggleConstraints(track, constraints))); -} - -/** - * Stops the microphone from listening - */ -async function applyMicrophoneConstraints( - currentStream: MediaStream | null, - constraints: MediaTrackConstraints | boolean -): Promise { - if (!currentStream) { - return []; - } - return Promise.all(currentStream.getAudioTracks().map((track) => toggleConstraints(track, constraints))); -} - -async function toggleConstraints(track: MediaStreamTrack, constraints: MediaTrackConstraints | boolean): Promise { - if (implementCorrectTrackBehavior) { - track.enabled = constraints !== false; - } else if (constraints === false) { - track.stop(); - } - - if (typeof constraints !== "boolean") { - return track.applyConstraints(constraints); - } -} // This promise is important to queue the calls to "getUserMedia" // Otherwise, this can happen: @@ -565,7 +569,6 @@ export const localStreamStore = derived, LocalS if (navigator.mediaDevices === undefined) { if (window.location.protocol === "http:") { - //throw new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'); set({ type: "error", error: new Error("Unable to access your camera or microphone. You need to use a HTTPS connection."), @@ -594,67 +597,39 @@ export const localStreamStore = derived, LocalS }); } - (async () => { - let applyNewConstraintSuccess = true; - try { - await applyMicrophoneConstraints(currentStream, constraints.audio || false); - } catch (err) { - console.error("applyMicrophoneConstraints => error :", err); - applyNewConstraintSuccess = false; - } - try { - await applyCameraConstraints(currentStream, constraints.video || false); - } catch (err) { - console.error("applyCameraConstraints => error :", err); - applyNewConstraintSuccess = false; - } - - if (implementCorrectTrackBehavior && applyNewConstraintSuccess) { - //on good navigators like firefox, we can instantiate the stream once and simply disable or enable the tracks as needed - if (currentStream === null) { - initStream(constraints).catch((e) => { - set({ - type: "error", - error: e instanceof Error ? e : new Error("An unknown error happened"), - }); - }); + //on bad navigators like chrome, we have to stop the tracks when we mute and reinstantiate the stream when we need to unmute + if (constraints.audio === false && constraints.video === false) { + currentGetUserMediaPromise = currentGetUserMediaPromise.then(() => { + if (currentStream) { + //we need stop all tracks to make sure the old stream will be garbage collected + currentStream.getTracks().forEach((t) => t.stop()); } - } else { - //on bad navigators like chrome, we have to stop the tracks when we mute and reinstantiate the stream when we need to unmute - if (constraints.audio === false && constraints.video === false) { - currentGetUserMediaPromise = currentGetUserMediaPromise.then(() => { - if (currentStream) { - //we need stop all tracks to make sure the old stream will be garbage collected - currentStream.getTracks().forEach((t) => t.stop()); - } - currentStream = null; - set({ - type: "success", - stream: null, - }); - return undefined; - }); - } //we reemit the stream if it was muted just to be sure - else if ( - constraints.audio /* && !oldConstraints.audio*/ || - (!oldConstraints.video && constraints.video) || - !deepEqual(oldConstraints.audio, constraints.audio) || - !deepEqual(oldConstraints.video, constraints.video) - ) { - initStream(constraints).catch((e) => { - set({ - type: "error", - error: e instanceof Error ? e : new Error("An unknown error happened"), - }); - }); - } - oldConstraints = { - video: !!constraints.video, - audio: !!constraints.audio, - }; - } - })().catch((e) => console.error(e)); + currentStream = null; + set({ + type: "success", + stream: null, + }); + return undefined; + }); + } //we reemit the stream if it was muted just to be sure + else if ( + constraints.audio /* && !oldConstraints.audio*/ || + (!oldConstraints.video && constraints.video) || + !deepEqual(oldConstraints.audio, constraints.audio) || + !deepEqual(oldConstraints.video, constraints.video) + ) { + initStream(constraints).catch((e) => { + set({ + type: "error", + error: e instanceof Error ? e : new Error("An unknown error happened"), + }); + }); + } + oldConstraints = { + video: !!constraints.video, + audio: !!constraints.audio, + }; } ); @@ -724,7 +699,7 @@ export const localVolumeStore = readable(undefined, (set) /** * Device list */ -export const deviceListStore = readable([], function start(set) { +export const deviceListStore = readable(undefined, function start(set) { let deviceListCanBeQueried = false; const queryDeviceList = () => { @@ -733,9 +708,11 @@ export const deviceListStore = readable([], function start(se .enumerateDevices() .then((mediaDeviceInfos) => { set(mediaDeviceInfos); + devicesNotLoaded.set(false); }) .catch((e) => { console.error(e); + devicesNotLoaded.set(false); throw e; }); }; @@ -762,14 +739,26 @@ export const deviceListStore = readable([], function start(se }); export const cameraListStore = derived(deviceListStore, ($deviceListStore) => { + if ($deviceListStore === undefined) { + return undefined; + } + return $deviceListStore.filter((device) => device.kind === "videoinput"); }); export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => { + if ($deviceListStore === undefined) { + return undefined; + } + return $deviceListStore.filter((device) => device.kind === "audioinput"); }); export const speakerListStore = derived(deviceListStore, ($deviceListStore) => { + if ($deviceListStore === undefined) { + return undefined; + } + const audiooutput = $deviceListStore.filter((device) => device.kind === "audiooutput"); // if the previous speaker used isn`t defined in the list, apply default speaker const value = audiooutput.find((device) => device.deviceId === get(speakerSelectedStore)); @@ -792,6 +781,10 @@ function isConstrainDOMStringParameters(param: ConstrainDOMString): param is Con // TODO: detect the new webcam and automatically switch on it. cameraListStore.subscribe((devices) => { + // Store not initialized yet + if (devices === undefined) { + return; + } // If the selected camera is unplugged, let's remove the constraint on deviceId const constraints = get(videoConstraintStore); const deviceId = constraints.deviceId; @@ -802,12 +795,18 @@ cameraListStore.subscribe((devices) => { // If we cannot find the device ID, let's remove it. if (isConstrainDOMStringParameters(deviceId)) { if (!devices.find((device) => device.deviceId === deviceId.exact)) { + console.log("Camera unplugged, removing constraint on deviceId"); requestedCameraDeviceIdStore.set(undefined); } } }); microphoneListStore.subscribe((devices) => { + // Store not initialized yet + if (devices === undefined) { + return; + } + // If the selected camera is unplugged, let's remove the constraint on deviceId const constraints = get(audioConstraintStore); if (typeof constraints === "boolean") { @@ -836,16 +835,32 @@ localStreamStore.subscribe((streamResult) => { // When the stream is initialized, the new sound constraint is recreated and the first speaker is set. // If the user did not select the new speaker, the first new speaker cannot be selected automatically. -speakerSelectedStore.subscribe((value) => { +speakerSelectedStore.subscribe((speaker) => { const oldValue = localUserStore.getSpeakerDeviceId(); - const currentValue = value; - const oldDevice = oldValue - ? get(speakerListStore).find((mediaDeviceInfo) => mediaDeviceInfo.deviceId == oldValue) - : null; + const currentValue = speaker; + const speakerList = get(speakerListStore); + const oldDevice = + oldValue && speakerList ? speakerList.find((mediaDeviceInfo) => mediaDeviceInfo.deviceId == oldValue) : null; if ( oldDevice != undefined && + speakerList != undefined && currentValue !== oldDevice.deviceId && - get(speakerListStore).find((value) => value.deviceId == oldValue) - ) + speakerList.find((value) => value.deviceId == oldValue) + ) { speakerSelectedStore.set(oldDevice.deviceId); + } }); + +function createVideoBandwidthStore() { + const { subscribe, set } = writable(localUserStore.getVideoBandwidth()); + + return { + subscribe, + setBandwidth: (bandwidth: number | "unlimited") => { + set(bandwidth); + localUserStore.setVideoBandwidth(bandwidth); + }, + }; +} + +export const videoBandwidthStore = createVideoBandwidthStore(); diff --git a/play/src/front/Stores/MegaphoneStore.ts b/play/src/front/Stores/MegaphoneStore.ts index 3018756bd9..25efde1beb 100644 --- a/play/src/front/Stores/MegaphoneStore.ts +++ b/play/src/front/Stores/MegaphoneStore.ts @@ -1,5 +1,6 @@ import { derived, Readable, writable } from "svelte/store"; -import { requestedCameraState, requestedMicrophoneState } from "./MediaStore"; +import { isSpeakerStore, requestedCameraState, requestedMicrophoneState } from "./MediaStore"; +import { requestedScreenSharingState } from "./ScreenSharingStore"; export const currentMegaphoneNameStore = writable(); export const megaphoneCanBeUsedStore = writable(false); @@ -7,10 +8,33 @@ export const megaphoneCanBeUsedStore = writable(false); export const requestedMegaphoneStore = writable(false); export const megaphoneEnabledStore: Readable = derived( - [requestedMegaphoneStore, requestedCameraState, requestedMicrophoneState], - ([$requestedMegaphoneStore, $requestedCameraState, $requestedMicrophoneState], set) => { - set($requestedMegaphoneStore && ($requestedCameraState || $requestedMicrophoneState)); - if ($requestedMegaphoneStore && !$requestedCameraState && !$requestedMicrophoneState) { + [ + isSpeakerStore, + requestedMegaphoneStore, + requestedCameraState, + requestedMicrophoneState, + requestedScreenSharingState, + ], + ( + [ + $isSpeakerStore, + $requestedMegaphoneStore, + $requestedCameraState, + $requestedMicrophoneState, + $requestedScreenSharingState, + ], + set + ) => { + set( + ($isSpeakerStore || $requestedMegaphoneStore) && + ($requestedCameraState || $requestedMicrophoneState || $requestedScreenSharingState) + ); + if ( + ($isSpeakerStore || $requestedMegaphoneStore) && + !$requestedCameraState && + !$requestedMicrophoneState && + !$requestedScreenSharingState + ) { requestedMegaphoneStore.set(false); } } diff --git a/play/src/front/Stores/ScreenSharingStore.ts b/play/src/front/Stores/ScreenSharingStore.ts index 2e63dd2d78..17e86cc643 100644 --- a/play/src/front/Stores/ScreenSharingStore.ts +++ b/play/src/front/Stores/ScreenSharingStore.ts @@ -1,6 +1,7 @@ import type { Readable } from "svelte/store"; import { derived, readable, writable } from "svelte/store"; import type { DesktopCapturerSource } from "../Interfaces/DesktopAppInterfaces"; +import { localUserStore } from "../Connection/LocalUserStore"; import { peerStore } from "./PeerStore"; import type { LocalStreamStoreValue } from "./MediaStore"; import { inExternalServiceStore, myCameraStore, myMicrophoneStore } from "./MyMediaStore"; @@ -40,6 +41,20 @@ function stopScreenSharing(): void { let previousComputedVideoConstraint: boolean | MediaTrackConstraints = false; let previousComputedAudioConstraint: boolean | MediaTrackConstraints = false; +function createScreenShareBandwidthStore() { + const { subscribe, set } = writable(localUserStore.getScreenShareBandwidth()); + + return { + subscribe, + setBandwidth: (bandwidth: number | "unlimited") => { + set(bandwidth); + localUserStore.setScreenShareBandwidth(bandwidth); + }, + }; +} + +export const screenShareBandwidthStore = createScreenShareBandwidthStore(); + /** * A store containing the media constraints we want to apply. */ diff --git a/play/src/front/Stores/StreamableCollectionStore.ts b/play/src/front/Stores/StreamableCollectionStore.ts index d6558d40b4..460a5af169 100644 --- a/play/src/front/Stores/StreamableCollectionStore.ts +++ b/play/src/front/Stores/StreamableCollectionStore.ts @@ -4,13 +4,14 @@ import { createNestedStore } from "@workadventure/store-utils"; import type { RemotePeer } from "../WebRtc/SimplePeer"; import { GameScene } from "../Phaser/Game/GameScene"; import { JitsiTrackWrapper } from "../Streaming/Jitsi/JitsiTrackWrapper"; +import { JitsiTrackStreamWrapper } from "../Streaming/Jitsi/JitsiTrackStreamWrapper"; import type { ScreenSharingLocalMedia } from "./ScreenSharingStore"; import { screenSharingLocalMedia } from "./ScreenSharingStore"; import { peerStore, screenSharingStreamStore } from "./PeerStore"; import { highlightedEmbedScreen } from "./HighlightedEmbedScreenStore"; import { gameSceneStore } from "./GameSceneStore"; -export type Streamable = RemotePeer | ScreenSharingLocalMedia | JitsiTrackWrapper; +export type Streamable = RemotePeer | ScreenSharingLocalMedia | JitsiTrackStreamWrapper; const jitsiTracksStore = createNestedStore>( gameSceneStore, @@ -33,7 +34,20 @@ function createStreamableCollectionStore(): Readable> { $screenSharingStreamStore.forEach(addPeer); $peerStore.forEach(addPeer); - $jitsiTracksStore.forEach((jitsiTrackStore) => addPeer(jitsiTrackStore)); + console.warn("streamableCollectionStore triggerred"); + $jitsiTracksStore.forEach((jitsiTrackWrapper) => { + const cameraTrackWrapper = jitsiTrackWrapper.cameraTrackWrapper; + if (!cameraTrackWrapper.isEmpty() && !jitsiTrackWrapper.isLocal) { + addPeer(cameraTrackWrapper); + } + const screenSharingTrackWrapper = jitsiTrackWrapper.screenSharingTrackWrapper; + if ( + !screenSharingTrackWrapper.isEmpty() && + screenSharingTrackWrapper.jitsiTrackWrapper.spaceUser?.screenSharingState !== false + ) { + addPeer(screenSharingTrackWrapper); + } + }); if ($screenSharingLocalMedia?.stream) { addPeer($screenSharingLocalMedia); @@ -45,10 +59,22 @@ function createStreamableCollectionStore(): Readable> { highlightedEmbedScreen.removeHighlight(); } - //set(peers); return peers; } ); } export const streamableCollectionStore = createStreamableCollectionStore(); + +export const myJitsiCameraStore = derived([jitsiTracksStore], ([$jitsiTracksStore]) => { + for (const jitsiTrackWrapper of $jitsiTracksStore.values()) { + if (jitsiTrackWrapper.isLocal) { + const cameraTrackWrapper = jitsiTrackWrapper.cameraTrackWrapper; + if (cameraTrackWrapper.isEmpty()) { + return null; + } + return cameraTrackWrapper; + } + } + return null; +}); diff --git a/play/src/front/Stores/VideoFocusStore.ts b/play/src/front/Stores/VideoFocusStore.ts index 7e0580ab54..243704bb80 100644 --- a/play/src/front/Stores/VideoFocusStore.ts +++ b/play/src/front/Stores/VideoFocusStore.ts @@ -1,5 +1,5 @@ import { get, writable } from "svelte/store"; -import { JitsiTrackWrapper } from "../Streaming/Jitsi/JitsiTrackWrapper"; +import { JitsiTrackStreamWrapper } from "../Streaming/Jitsi/JitsiTrackStreamWrapper"; import type { Streamable } from "./StreamableCollectionStore"; import { peerStore } from "./PeerStore"; @@ -32,7 +32,7 @@ export const videoFocusStore = createVideoFocusStore(); peerStore.subscribe((peers) => { const focusedMedia: Streamable | null = get(videoFocusStore); - if (focusedMedia instanceof JitsiTrackWrapper) { + if (focusedMedia instanceof JitsiTrackStreamWrapper) { return; } if (focusedMedia && focusedMedia.userId !== undefined && !peers.get(focusedMedia.userId)) { diff --git a/play/src/front/Streaming/BroadcastService.ts b/play/src/front/Streaming/BroadcastService.ts index 434a70abb5..dac63e25fa 100644 --- a/play/src/front/Streaming/BroadcastService.ts +++ b/play/src/front/Streaming/BroadcastService.ts @@ -42,30 +42,26 @@ class BroadcastSpace extends Space { this.jitsiConference = undefined; }) .catch((e) => { + // TODO : Handle the error and retry to leave the conference console.error(e); }) .finally(() => { + jitsiLoadingStore.set(false); broadcastService.checkIfCanDisconnect(); }); } } else { - if (this.jitsiConference === undefined) { - limit(async () => { - if (this.jitsiConference === undefined) { - jitsiLoadingStore.set(true); - return await broadcastService.joinJitsiConference(spaceName, this); - } - throw new Error("Jitsi conference already exists"); - }) - .then((jitsiConference) => { - this.jitsiConference = jitsiConference; - broadcastService.emitJitsiParticipantIdSpace(spaceName, jitsiConference.participantId); - jitsiLoadingStore.set(false); - }) - .catch((e) => { - console.error("Error while joining the conference", e); - }); - } + limit(async () => { + if (this.jitsiConference === undefined) { + jitsiLoadingStore.set(true); + this.jitsiConference = await broadcastService.joinJitsiConference(spaceName, this); + broadcastService.emitJitsiParticipantIdSpace(spaceName, this.jitsiConference.participantId); + jitsiLoadingStore.set(false); + } + }).catch((e) => { + // TODO : Handle the error and retry to join the conference + console.error("Error while joining the conference", e); + }); } }) ); @@ -80,6 +76,7 @@ class BroadcastSpace extends Space { }) .finally(() => { this.broadcastService.checkIfCanDisconnect(); + jitsiLoadingStore.set(false); }); this.unsubscribes.forEach((unsubscribe) => unsubscribe()); super.destroy(); @@ -110,7 +107,9 @@ export class BroadcastService { this.broadcastSpaces.forEach((broadcastSpace) => { if (broadcastSpace.jitsiConference) { broadcastSpace.jitsiConference.broadcast(["video", "audio"]); - void broadcastSpace.jitsiConference.firstLocalTrackInit(); + broadcastSpace.jitsiConference.firstLocalTrackInit().catch((e) => { + console.error(e); + }); } }); } else { @@ -140,6 +139,7 @@ export class BroadcastService { this.broadcastSpaces = this.broadcastSpaces.filter((space) => space.name !== spaceName); broadcastServiceLogger("BroadcastService => leaveSpace", spaceName); } + jitsiLoadingStore.set(false); } private async connect() { @@ -166,7 +166,7 @@ export class BroadcastService { } } - debug("Joining Jitsi conference, jitsiConnecton is defined" + roomName); + debug("Joining Jitsi conference, jitsiConnection is defined " + roomName); const jitsiConference = await JitsiConferenceWrapper.join(this.jitsiConnection, roomName); jitsiConferencesStore.set(roomName, jitsiConference); @@ -240,6 +240,7 @@ export class BroadcastService { }) .catch((e) => { console.error(e); + jitsiLoadingStore.set(false); }); } } diff --git a/play/src/front/Streaming/Jitsi/JitsiConferenceWrapper.ts b/play/src/front/Streaming/Jitsi/JitsiConferenceWrapper.ts index 08c8f843c8..1bc8d706cf 100644 --- a/play/src/front/Streaming/Jitsi/JitsiConferenceWrapper.ts +++ b/play/src/front/Streaming/Jitsi/JitsiConferenceWrapper.ts @@ -1,5 +1,5 @@ // eslint-disable @typescript-eslint/ban-ts-comment -import { get, readable, Readable, Unsubscriber, Writable, writable } from "svelte/store"; +import { get, Readable, Unsubscriber, Writable, writable } from "svelte/store"; // eslint-disable-next-line import/no-unresolved import JitsiTrack from "lib-jitsi-meet/types/hand-crafted/modules/RTC/JitsiTrack"; // eslint-disable-next-line import/no-unresolved @@ -7,7 +7,6 @@ import JitsiConnection from "lib-jitsi-meet/types/hand-crafted/JitsiConnection"; // eslint-disable-next-line import/no-unresolved import JitsiConference from "lib-jitsi-meet/types/hand-crafted/JitsiConference"; import Debug from "debug"; -import { Result } from "@workadventure/map-editor"; // eslint-disable-next-line import/no-unresolved import JitsiLocalTrack from "lib-jitsi-meet/types/hand-crafted/modules/RTC/JitsiLocalTrack"; // eslint-disable-next-line import/no-unresolved @@ -19,9 +18,11 @@ import { requestedMicrophoneState, usedCameraDeviceIdStore, usedMicrophoneDeviceIdStore, + videoConstraintStore, } from "../../Stores/MediaStore"; import { megaphoneEnabledStore } from "../../Stores/MegaphoneStore"; import { gameManager } from "../../Phaser/Game/GameManager"; +import { requestedScreenSharingState } from "../../Stores/ScreenSharingStore"; import { JitsiTrackWrapper } from "./JitsiTrackWrapper"; import { JitsiLocalTracks } from "./JitsiLocalTracks"; @@ -36,12 +37,11 @@ export class JitsiConferenceWrapper { private readonly _broadcastDevicesStore: Writable; - private localTracksStore: Readable | undefined>; - private requestedCameraStateUnsubscriber: Unsubscriber | undefined; private requestedMicrophoneStateUnsubscriber: Unsubscriber | undefined; private cameraDeviceIdStoreUnsubscriber: Unsubscriber | undefined; private microphoneDeviceIdStoreUnsubscriber: Unsubscriber | undefined; + private requestedScreenSharingStateUnsubscriber: Unsubscriber | undefined; private cameraDeviceId: string | undefined = undefined; private microphoneDeviceId: string | undefined = undefined; @@ -59,7 +59,6 @@ export class JitsiConferenceWrapper { constructor(private jitsiConference: JitsiConference) { this._streamStore = writable>(new Map()); this._broadcastDevicesStore = writable([]); - this.localTracksStore = readable | undefined>(undefined); } public static join(connection: JitsiConnection, jitsiRoomName: string): Promise { @@ -73,7 +72,9 @@ export class JitsiConferenceWrapper { //const localTracks: any[] = []; room.on(JitsiMeetJS.events.conference.CONFERENCE_JOINED, () => { debug("CONFERENCE_JOINED"); - void jitsiConferenceWrapper.firstLocalTrackInit(); + jitsiConferenceWrapper.firstLocalTrackInit().catch((e) => { + console.error(e); + }); resolve(jitsiConferenceWrapper); }); room.on(JitsiMeetJS.events.conference.CONFERENCE_FAILED, (e) => { @@ -91,31 +92,43 @@ export class JitsiConferenceWrapper { }); jitsiConferenceWrapper.requestedCameraStateUnsubscriber = requestedCameraState.subscribe( - (requestedCameraState) => { + (requestedCameraState_) => { if (jitsiConferenceWrapper.firstLocalTrackInitialization) { - (async (): Promise => - jitsiConferenceWrapper.handleLocalTrackState("video", requestedCameraState))() - .then((newTracks) => { - debug("this is a success"); - }) - .catch((e) => { - console.error("jitsiLocalTracks", e); - }); + if ( + (jitsiConferenceWrapper.tracks.video && !requestedCameraState_) || + (!jitsiConferenceWrapper.tracks.video && requestedCameraState_) + ) { + (async (): Promise => + jitsiConferenceWrapper.handleLocalTrackState("video", requestedCameraState_))() + .then((newTracks) => { + debug("requestedCameraState => subscribe => localTrack added"); + }) + .catch((e) => { + requestedCameraState.disableWebcam(); + console.error("jitsiLocalTracks", e); + }); + } } } ); jitsiConferenceWrapper.requestedMicrophoneStateUnsubscriber = requestedMicrophoneState.subscribe( - (requestedMicrophoneState) => { + (requestedMicrophoneState_) => { if (jitsiConferenceWrapper.firstLocalTrackInitialization) { - (async (): Promise => - jitsiConferenceWrapper.handleLocalTrackState("audio", requestedMicrophoneState))() - .then((newTracks) => { - debug("this is a success"); - }) - .catch((e) => { - console.error("jitsiLocalTracks", e); - }); + if ( + (jitsiConferenceWrapper.tracks.audio && !requestedMicrophoneState_) || + (!jitsiConferenceWrapper.tracks.audio && requestedMicrophoneState_) + ) { + (async (): Promise => + jitsiConferenceWrapper.handleLocalTrackState("audio", requestedMicrophoneState_))() + .then((newTracks) => { + debug("requestedMicrophoneState => subscribe => localTrack added"); + }) + .catch((e) => { + requestedMicrophoneState.disableMicrophone(); + console.error("jitsiLocalTracks", e); + }); + } } } ); @@ -128,7 +141,7 @@ export class JitsiConferenceWrapper { ) { (async () => jitsiConferenceWrapper.handleLocalTrackState("video", true))() .then((newTracks) => { - debug("this is a success"); + debug("requestedCameraDeviceIdStore => subscribe => localTrack added"); }) .catch((e) => { console.error("jitsiLocalTracks", e); @@ -145,7 +158,7 @@ export class JitsiConferenceWrapper { ) { (async () => jitsiConferenceWrapper.handleLocalTrackState("audio", true))() .then((newTracks) => { - debug("this is a success"); + debug("requestedMicrophoneDeviceIdStore => subscribe => localTrack added"); }) .catch((e) => { console.error("jitsiLocalTracks", e); @@ -154,6 +167,27 @@ export class JitsiConferenceWrapper { } ); + jitsiConferenceWrapper.requestedScreenSharingStateUnsubscriber = requestedScreenSharingState.subscribe( + (requestedScreenSharingState_) => { + if (jitsiConferenceWrapper.firstLocalTrackInitialization) { + if ( + (jitsiConferenceWrapper.tracks.screenSharing && !requestedScreenSharingState_) || + (!jitsiConferenceWrapper.tracks.screenSharing && requestedScreenSharingState_) + ) { + (async (): Promise => + jitsiConferenceWrapper.handleLocalTrackState("desktop", requestedScreenSharingState_))() + .then((newTracks) => { + debug("requestedScreenSharingState => subscribe => localTrack changed"); + }) + .catch((e) => { + requestedScreenSharingState.disableScreenSharing(); + console.error("jitsiLocalTracks", e); + }); + } + } + } + ); + /** * Handles remote tracks * @param track JitsiTrackWrapper object @@ -179,28 +213,17 @@ export class JitsiConferenceWrapper { track.addEventListener(JitsiMeetJS.events.track.TRACK_VIDEOTYPE_CHANGED, (event) => { debug("track video type changed"); }); - track.addEventListener(JitsiMeetJS.events.track.LOCAL_TRACK_STOPPED, () => { - debug("local track stopped"); + track.addEventListener(JitsiMeetJS.events.track.LOCAL_TRACK_STOPPED, (track: JitsiTrack) => { + // TODO : Remove track that is stopped and update all other users + debug("local track stopped", track); + + if (track.isVideoTrack() && track.getVideoType() === "desktop") { + requestedScreenSharingState.disableScreenSharing(); + } }); track.addEventListener(JitsiMeetJS.events.track.TRACK_AUDIO_OUTPUT_CHANGED, (deviceId) => debug(`track audio output device was changed to ${deviceId}`) ); - - /*const id = participant + track.getType() + idx; - - if (track.getType() === 'video') { - $('body').prepend( - `