diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 19d2922a..87550425 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -75,7 +75,7 @@ jobs: uses: docker/setup-buildx-action@dedd61cf5d839122591f5027c89bf3ad27691d18 - name: Build and push Docker image - uses: docker/build-push-action@4c1b68d83ad20cc1a09620ca477d5bbbb5fa14d0 + uses: docker/build-push-action@0f847266c302569530c95bfa228489494c43b002 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/package.json b/package.json index 90314904..e281939a 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "i18next-http-backend": "^2.0.0", "livekit-client": "^1.12.3", "lodash": "^4.17.21", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#6385c9c0dab8fe67bd3a8992a4777f243fdd1b68", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#c8f8fb587d29dce22d314bfc16bf25a76b04e8bb", "matrix-widget-api": "^1.3.1", "normalize.css": "^8.0.1", "pako": "^2.0.4", diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index bf12669c..5396d4b3 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -101,6 +101,17 @@ export function useLiveKit( // block audio from being enabled until the connection is finished. const [blockAudio, setBlockAudio] = useState(true); + // Store if audio/video are currently updating. If to prohibit unnecessary calls + // to setMicrophoneEnabled/setCameraEnabled + const audioMuteUpdating = useRef(false); + const videoMuteUpdating = useRef(false); + // Store the current button mute state that gets passed to this hook via props. + // We need to store it for awaited code that relies on the current value. + const buttonEnabled = useRef({ + audio: initialMuteStates.current.audio.enabled, + video: initialMuteStates.current.video.enabled, + }); + // We have to create the room manually here due to a bug inside // @livekit/components-react. JSON.stringify() is used in deps of a // useEffect() with an argument that references itself, if E2EE is enabled @@ -137,20 +148,50 @@ export function useLiveKit( // and setting tracks to be enabled during this time causes errors. if (room !== undefined && connectionState === ConnectionState.Connected) { const participant = room.localParticipant; - if (participant.isMicrophoneEnabled !== muteStates.audio.enabled) { - participant - .setMicrophoneEnabled(muteStates.audio.enabled) - .catch((e) => - logger.error("Failed to sync audio mute state with LiveKit", e) - ); - } - if (participant.isCameraEnabled !== muteStates.video.enabled) { - participant - .setCameraEnabled(muteStates.video.enabled) - .catch((e) => - logger.error("Failed to sync video mute state with LiveKit", e) - ); - } + // Always update the muteButtonState Ref so that we can read the current + // state in awaited blocks. + buttonEnabled.current = { + audio: muteStates.audio.enabled, + video: muteStates.video.enabled, + }; + const syncMuteStateAudio = async () => { + if ( + participant.isMicrophoneEnabled !== buttonEnabled.current.audio && + !audioMuteUpdating.current + ) { + audioMuteUpdating.current = true; + try { + await participant.setMicrophoneEnabled(buttonEnabled.current.audio); + } catch (e) { + logger.error("Failed to sync audio mute state with LiveKit", e); + } + audioMuteUpdating.current = false; + // Run the check again after the change is done. Because the user + // can update the state (presses mute button) while the device is enabling + // itself we need might need to update the mute state right away. + // This async recursion makes sure that setCamera/MicrophoneEnabled is + // called as little times as possible. + syncMuteStateAudio(); + } + }; + const syncMuteStateVideo = async () => { + if ( + participant.isCameraEnabled !== buttonEnabled.current.video && + !videoMuteUpdating.current + ) { + videoMuteUpdating.current = true; + try { + await participant.setCameraEnabled(buttonEnabled.current.video); + } catch (e) { + logger.error("Failed to sync audio mute state with LiveKit", e); + } + videoMuteUpdating.current = false; + // see above + syncMuteStateVideo(); + } + }; + syncMuteStateAudio(); + syncMuteStateVideo(); } }, [room, muteStates, connectionState]); diff --git a/src/matrix-utils.ts b/src/matrix-utils.ts index 58f8bab5..54105519 100644 --- a/src/matrix-utils.ts +++ b/src/matrix-utils.ts @@ -76,9 +76,18 @@ function waitForSync(client: MatrixClient) { function secureRandomString(entropyBytes: number): string { const key = new Uint8Array(entropyBytes); crypto.getRandomValues(key); + // encode to base64url as this value goes into URLs + // base64url is just base64 with thw two non-alphanum characters swapped out for + // ones that can be put in a URL without encoding. Browser JS has a native impl + // for base64 encoding but only a string (there isn't one that takes a UInt8Array + // yet) so just use the built-in one and convert, replace the chars and strip the + // padding from the end (otherwise we'd need to pull in another dependency). return btoa( key.reduce((acc, current) => acc + String.fromCharCode(current), "") - ).replace(/=*$/, ""); + ) + .replace("+", "-") + .replace("/", "_") + .replace(/=*$/, ""); } /** @@ -395,9 +404,16 @@ export function getRelativeRoomUrl( roomName?: string, password?: string ): string { + // The password shouldn't need URL encoding here (we generate URL-safe ones) but encode + // it in case it came from another client that generated a non url-safe one + const encodedPassword = password ? encodeURIComponent(password) : undefined; + if (password && encodedPassword !== password) { + logger.info("Encoded call password used non URL-safe chars: buggy client?"); + } + return `/room/#${ roomName ? "/" + roomAliasLocalpartFromRoomName(roomName) : "" - }?roomId=${roomId}${password ? "&" + PASSWORD_STRING + password : ""}`; + }?roomId=${roomId}${password ? "&" + PASSWORD_STRING + encodedPassword : ""}`; } export function getAvatarUrl( diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index d238176e..f834814e 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -110,7 +110,7 @@ export function GroupCallView({ // Count each member only once, regardless of how many devices they use const participantCount = useMemo( - () => new Set(memberships.map((m) => m.member.userId)).size, + () => new Set(memberships.map((m) => m.sender!)).size, [memberships] ); diff --git a/src/rtcSessionHelpers.ts b/src/rtcSessionHelpers.ts index 3cad5627..68baca6e 100644 --- a/src/rtcSessionHelpers.ts +++ b/src/rtcSessionHelpers.ts @@ -41,7 +41,7 @@ export function enterRTCSession(rtcSession: MatrixRTCSession) { // have started tracking by the time calls start getting created. //groupCallOTelMembership?.onJoinCall(); - // right now we asume everything is a room-scoped call + // right now we assume everything is a room-scoped call const livekitAlias = rtcSession.room.roomId; rtcSession.joinRoomSession([makeFocus(livekitAlias)]); diff --git a/yarn.lock b/yarn.lock index 57dfaf92..994c5f5b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3552,9 +3552,9 @@ integrity sha512-zI22/pJW2wUZOVyguFaUL1HABdmSVxpXrzIqkjsHmyUjNhPoWM1CKfvVuXfetHhIok4RY573cqS0mZ1SJEnoTg== "@types/node@^18.13.0": - version "18.18.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.3.tgz#e5188135fc2909b46530c798ef49be65083be3fd" - integrity sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA== + version "18.18.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.4.tgz#519fef47a13cf869be290c20fc6ae9b7fe887aa7" + integrity sha512-t3rNFBgJRugIhackit2mVcLfF6IRc0JE4oeizPQL8Zrm8n2WY/0wOdpOPhdtG0V9Q2TlW/axbF1MJ6z+Yj/kKQ== "@types/prop-types@*": version "15.7.8" @@ -3567,9 +3567,9 @@ integrity sha512-IKjZ8RjTSwD4/YG+2gtj7BPFRB/lNbWKTiSj3M7U/TD2B7HfYCxvp2Zz6xA2WIY7pAuL1QOUPw8gQRbUrrq4fQ== "@types/react-dom@^18.0.0": - version "18.2.8" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.8.tgz#338f1b0a646c9f10e0a97208c1d26b9f473dffd6" - integrity sha512-bAIvO5lN/U8sPGvs1Xm61rlRHHaq5rp5N3kp9C+NJ/Q41P8iqjkXSu0+/qu8POsjH9pNWb0OYabFez7taP7omw== + version "18.2.11" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.11.tgz#4332c315544698a0875dfdb6e320dda59e1b3d58" + integrity sha512-zq6Dy0EiCuF9pWFW6I6k6W2LdpUixLE4P6XjXU1QHLfak3GPACQfLwEuHzY5pOYa4hzj1d0GxX/P141aFjZsyg== dependencies: "@types/react" "*" @@ -3655,19 +3655,19 @@ integrity sha512-IDaobHimLQhjwsQ/NMwRVfa/yL7L/wriQPMhw1ZJall0KX6E1oxk29XMDeilW5qTIg5aoiqf5Udy8U/51aNoQQ== "@types/uuid@9": - version "9.0.4" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.4.tgz#e884a59338da907bda8d2ed03e01c5c49d036f1c" - integrity sha512-zAuJWQflfx6dYJM62vna+Sn5aeSWhh3OB+wfUEACNcqUSc0AGc5JKl+ycL1vrH7frGTXhJchYjE1Hak8L819dA== + version "9.0.5" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.5.tgz#25a71eb73eba95ac0e559ff3dd018fc08294acf6" + integrity sha512-xfHdwa1FMJ082prjSJpoEI57GZITiQz10r3vEJCHa2khEFQjKy91aWKz6+zybzssCvXUwE1LQWgWVwZ4nYUvHQ== "@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + version "21.0.1" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.1.tgz#07773d7160494d56aa882d7531aac7319ea67c3b" + integrity sha512-axdPBuLuEJt0c4yI5OZssC19K2Mq1uKdrfZBzuxLvaztgqUtFYZUNw7lETExPYJR9jdEoIg4mb7RQKRQzOkeGQ== "@types/yargs@^17.0.8": - version "17.0.24" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.24.tgz#b3ef8d50ad4aa6aecf6ddc97c580a00f5aa11902" - integrity sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw== + version "17.0.28" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.28.tgz#d106e4301fbacde3d1796ab27374dd16588ec851" + integrity sha512-N3e3fkS86hNhtk6BEnc0rj3zcehaxx8QWhCROJkqpl5Zaoi7nAic3jH8q94jVD3zu5LGk+PUB6KAiDmimYOEQw== dependencies: "@types/yargs-parser" "*" @@ -7286,9 +7286,9 @@ matrix-events-sdk@0.0.1: resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd" integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA== -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#6385c9c0dab8fe67bd3a8992a4777f243fdd1b68": - version "28.1.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6385c9c0dab8fe67bd3a8992a4777f243fdd1b68" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#c8f8fb587d29dce22d314bfc16bf25a76b04e8bb": + version "29.0.0" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f8fb587d29dce22d314bfc16bf25a76b04e8bb" dependencies: "@babel/runtime" "^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm" "^1.2.3-alpha.0" @@ -8078,9 +8078,9 @@ postcss@^8.4.27: source-map-js "^1.0.2" posthog-js@^1.29.0: - version "1.81.3" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.81.3.tgz#64f7fe50f31a13ad7290a3020328989045cee3c7" - integrity sha512-Aqqcj1n1KqZlxMaYYfd5OJC2BMIAP927+f7XqEbEplYJKigGTbQ6ygt2UeSJZe3xcDMxyDK4jxOWy68kD3YIlw== + version "1.83.0" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.83.0.tgz#e13d114922f863f4bfbf7c7cc4e11dc194139a91" + integrity sha512-3dp/yNbRCYsOgvJovFUMCLv9/KxnwmGBy5Ft27Q7/rbW++iJXVR64liX7i0NrXkudjoL9j1GW1LGh84rV7kv8Q== dependencies: fflate "^0.4.1"