From c798c79f416b7a00926b36034211e9ac0386c63f Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 20 Nov 2024 17:22:24 +0100 Subject: [PATCH 001/155] Add background Blur --- locales/en/app.json | 6 +++- package.json | 3 ++ src/livekit/useLiveKit.ts | 55 +++++++++++++++++++++++++++++++++- src/room/LobbyView.tsx | 35 ++++++++++++++++++---- src/settings/SettingsModal.tsx | 53 +++++++++++++++++++++++++++----- src/settings/settings.ts | 2 ++ yarn.lock | 22 ++++++++++++-- 7 files changed, 159 insertions(+), 17 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 07dd53e3..07edc5e5 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -150,6 +150,9 @@ "effect_volume_description": "Volume for sound effects such as: joining or leaving a call, and reactions.", "effect_volume_label": "Sound effect volume" }, + "background_blur_header": "Background", + "background_blur_label": "Enable background blurring", + "blur_not_supported_by_browser": "", "developer_settings_label": "Developer Settings", "developer_settings_label_description": "Expose developer settings in the settings window.", "developer_tab_title": "Developer", @@ -172,7 +175,8 @@ "preferences_tab_h4": "Preferences", "preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand", "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration", - "speaker_device_selection_label": "Speaker" + "speaker_device_selection_label": "Speaker", + "video_tab_activate_background_blur": "Turn on background blur on your webcam video" }, "star_rating_input_label_one": "{{count}} star", "star_rating_input_label_other": "{{count}} stars", diff --git a/package.json b/package.json index 71e3f9c9..f0705f97 100644 --- a/package.json +++ b/package.json @@ -122,5 +122,8 @@ }, "resolutions": { "strip-ansi": "6.0.1" + }, + "dependencies": { + "@livekit/track-processors": "^0.3.2" } } diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 4645fab7..208ed35d 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -9,7 +9,9 @@ import { ConnectionState, E2EEOptions, ExternalE2EEKeyProvider, + LocalTrackPublication, Room, + RoomEvent, RoomOptions, Track, } from "livekit-client"; @@ -17,6 +19,7 @@ import { useEffect, useMemo, useRef } from "react"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; +import { BackgroundBlur } from "@livekit/track-processors"; import { defaultLiveKitOptions } from "./options"; import { SFUConfig } from "./openIDSFU"; @@ -26,6 +29,7 @@ import { MediaDevices, useMediaDevices, } from "./MediaDevicesContext"; +import { backgroundBlur as backgroundBlurSettings } from "../settings/settings"; import { ECConnectionState, useECConnectionState, @@ -33,6 +37,7 @@ import { import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { E2eeType } from "../e2ee/e2eeType"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { useSetting } from "../settings/settings"; interface UseLivekitResult { livekitRoom?: Room; @@ -78,13 +83,16 @@ export function useLiveKit( const initialMuteStates = useRef(muteStates); const devices = useMediaDevices(); const initialDevices = useRef(devices); - + // eslint-disable-next-line new-cap + const blur = useMemo(() => BackgroundBlur(15), []); const roomOptions = useMemo( (): RoomOptions => ({ ...defaultLiveKitOptions, videoCaptureDefaults: { ...defaultLiveKitOptions.videoCaptureDefaults, deviceId: initialDevices.current.videoInput.selectedId, + // eslint-disable-next-line new-cap + processor: BackgroundBlur(15), }, audioCaptureDefaults: { ...defaultLiveKitOptions.audioCaptureDefaults, @@ -129,6 +137,51 @@ export function useLiveKit( sfuConfig, ); + const [showBackgroundBlur] = useSetting(backgroundBlurSettings); + const videoTrackPromise = useRef< + undefined | Promise + >(undefined); + + useEffect(() => { + if (!room || videoTrackPromise.current) return; + const update = async (): Promise => { + let publishCallback: undefined | ((track: LocalTrackPublication) => void); + videoTrackPromise.current = new Promise< + LocalTrackPublication | undefined + >((resolve) => { + const videoTrack = Array.from( + room.localParticipant.videoTrackPublications.values(), + ).find((v) => v.source === Track.Source.Camera); + if (videoTrack) { + resolve(videoTrack); + } + publishCallback = (videoTrack: LocalTrackPublication): void => { + if (videoTrack.source === Track.Source.Camera) { + resolve(videoTrack); + } + }; + room.on(RoomEvent.LocalTrackPublished, publishCallback); + }); + + const videoTrack = await videoTrackPromise.current; + + if (publishCallback) + room.off(RoomEvent.LocalTrackPublished, publishCallback); + + if (videoTrack !== undefined) { + if (showBackgroundBlur) { + logger.info("Blur: set blur"); + + void videoTrack.track?.setProcessor(blur); + } else { + void videoTrack.track?.stopProcessor(); + } + } + videoTrackPromise.current = undefined; + }; + void update(); + }, [blur, room, showBackgroundBlur]); + useEffect(() => { // Sync the requested mute states with LiveKit's mute states. We do it this // way around rather than using LiveKit as the source of truth, so that the diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index fd3df0c8..db630d09 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useMemo, useState } from "react"; +import { FC, useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Button } from "@vector-im/compound-web"; @@ -16,6 +16,7 @@ import { usePreviewTracks } from "@livekit/components-react"; import { LocalVideoTrack, Track } from "livekit-client"; import { useObservable } from "observable-hooks"; import { map } from "rxjs"; +import { BackgroundBlur } from "@livekit/track-processors"; import inCallStyles from "./InCallView.module.css"; import styles from "./LobbyView.module.css"; @@ -32,12 +33,14 @@ import { VideoButton, } from "../button/Button"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; +import { backgroundBlur as backgroundBlurSettings } from "../settings/settings"; import { useMediaQuery } from "../useMediaQuery"; import { E2eeType } from "../e2ee/e2eeType"; import { Link } from "../button/Link"; import { useMediaDevices } from "../livekit/MediaDevicesContext"; import { useInitial } from "../useInitial"; -import { useSwitchCamera } from "./useSwitchCamera"; +import { useSwitchCamera as useShowSwitchCamera } from "./useSwitchCamera"; +import { useSetting } from "../settings/settings"; interface Props { client: MatrixClient; @@ -108,6 +111,9 @@ export const LobbyView: FC = ({ muteStates.audio.enabled && { deviceId: devices.audioInput.selectedId }, ); + // eslint-disable-next-line new-cap + const blur = useMemo(() => BackgroundBlur(15), []); + const localTrackOptions = useMemo( () => ({ // The only reason we request audio here is to get the audio permission @@ -119,12 +125,15 @@ export const LobbyView: FC = ({ audio: Object.assign({}, initialAudioOptions), video: muteStates.video.enabled && { deviceId: devices.videoInput.selectedId, + // It should be possible to set a processor here: + // processor: blur, + // This causes a crash currently hence we do the effect below... }, }), [ initialAudioOptions, - devices.videoInput.selectedId, muteStates.video.enabled, + devices.videoInput.selectedId, ], ); @@ -146,7 +155,21 @@ export const LobbyView: FC = ({ [tracks], ); - const switchCamera = useSwitchCamera( + const [showBackgroundBlur] = useSetting(backgroundBlurSettings); + + useEffect(() => { + const updateBlur = async (showBlur: boolean): Promise => { + if (showBlur && !videoTrack?.getProcessor()) { + // eslint-disable-next-line new-cap + await videoTrack?.setProcessor(blur); + } else { + await videoTrack?.stopProcessor(); + } + }; + if (videoTrack) void updateBlur(showBackgroundBlur); + }, [videoTrack, showBackgroundBlur, blur]); + + const showSwitchCamera = useShowSwitchCamera( useObservable( (inputs) => inputs.pipe(map(([video]) => video)), [videoTrack], @@ -208,7 +231,9 @@ export const LobbyView: FC = ({ onClick={onVideoPress} disabled={muteStates.video.setEnabled === null} /> - {switchCamera && } + {showSwitchCamera && ( + + )} {!confineToRoom && } diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 9d7eb5a1..81e5f70e 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,10 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC, useCallback, useState } from "react"; +import { ChangeEvent, FC, ReactNode, useCallback, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { Root as Form, Text } from "@vector-im/compound-web"; +import { + Root as Form, + Separator, + Text, + Tooltip, +} from "@vector-im/compound-web"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -26,6 +31,7 @@ import { useSetting, developerSettingsTab as developerSettingsTabSetting, duplicateTiles as duplicateTilesSetting, + backgroundBlur as backgroundBlurSetting, useOptInAnalytics, soundEffectVolumeSetting, } from "./settings"; @@ -70,6 +76,33 @@ export const SettingsModal: FC = ({ ); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); + // Generate a `Checkbox` input to turn blur on or off. + const BlurCheckbox: React.FC = (): ReactNode => { + const [blur, setBlur] = useSetting(backgroundBlurSetting); + return ( + <> +

{t("settings.background_blur_header")}

+ + + setBlur(b.target.checked)} + disabled={isFirefox()} + /> + + + + ); + }; + const optInDescription = ( @@ -124,12 +157,16 @@ export const SettingsModal: FC = ({ key: "video", name: t("common.video"), content: ( -
- - + <> +
+ + + + + ), }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index b2da1674..6f52a525 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -88,6 +88,8 @@ export const videoInput = new Setting( undefined, ); +export const backgroundBlur = new Setting("background-blur", true); + export const showHandRaisedTimer = new Setting( "hand-raised-show-timer", false, diff --git a/yarn.lock b/yarn.lock index ab40c0c0..1a429fc8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1812,6 +1812,14 @@ dependencies: "@bufbuild/protobuf" "^1.10.0" +"@livekit/track-processors@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@livekit/track-processors/-/track-processors-0.3.2.tgz#eaff6a48b556c25e85f5dd2c4daf6dcf1bc3b143" + integrity sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA== + dependencies: + "@mediapipe/holistic" "0.5.1675471629" + "@mediapipe/tasks-vision" "0.10.9" + "@matrix-org/matrix-sdk-crypto-wasm@^9.0.0": version "9.1.0" resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-9.1.0.tgz#f889653eb4fafaad2a963654d586bd34de62acd5" @@ -1822,6 +1830,16 @@ resolved "https://registry.yarnpkg.com/@matrix-org/olm/-/olm-3.2.15.tgz#55f3c1b70a21bbee3f9195cecd6846b1083451ec" integrity sha512-S7lOrndAK9/8qOtaTq/WhttJC/o4GAzdfK0MUPpo8ApzsJEC0QjtwrkC3KBXdFP1cD1MXi/mlKR7aaoVMKgs6Q== +"@mediapipe/holistic@0.5.1675471629": + version "0.5.1675471629" + resolved "https://registry.yarnpkg.com/@mediapipe/holistic/-/holistic-0.5.1675471629.tgz#f1127d43161ff27e8889d5d39aaea164f9730980" + integrity sha512-qY+cxtDeSOvVtevrLgnodiwXYaAtPi7dHZtNv/bUCGEjFicAOYtMmrZSqMmbPkTB2+4jLnPF1vgshkAqQRSYAw== + +"@mediapipe/tasks-vision@0.10.9": + version "0.10.9" + resolved "https://registry.yarnpkg.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.9.tgz#fbd669f50ac2e888b2c64c9c9863927c111da02f" + integrity sha512-/gFguyJm1ng4Qr7VVH2vKO+zZcQd8wc3YafUfvBuYFX0Y5+CvrV+VNPEVkl5W/gUZF5KNKNZAiaHPULGPCIjyQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -6250,14 +6268,14 @@ matrix-js-sdk@matrix-org/matrix-js-sdk#develop: jwt-decode "^4.0.0" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.10.0" + matrix-widget-api "^1.8.2" oidc-client-ts "^3.0.1" p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" uuid "11" -matrix-widget-api@^1.10.0: +matrix-widget-api@^1.10.0, matrix-widget-api@^1.8.2: version "1.10.0" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55" integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw== From 140fe484d79bfdedd9f0068d65866a7561a0b4e4 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 20 Nov 2024 17:31:40 +0100 Subject: [PATCH 002/155] make compatible with unsopported browsers --- src/livekit/useLiveKit.ts | 18 ++++++++++++++---- src/room/LobbyView.tsx | 13 ++++++++++++- src/settings/SettingsModal.tsx | 17 +++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 208ed35d..f99fbabd 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -83,8 +83,16 @@ export function useLiveKit( const initialMuteStates = useRef(muteStates); const devices = useMediaDevices(); const initialDevices = useRef(devices); - // eslint-disable-next-line new-cap - const blur = useMemo(() => BackgroundBlur(15), []); + const blur = useMemo(() => { + let b = undefined; + try { + // eslint-disable-next-line new-cap + b = BackgroundBlur(15); + } catch (e) { + logger.error("disable background blur", e); + } + return b; + }, []); const roomOptions = useMemo( (): RoomOptions => ({ ...defaultLiveKitOptions, @@ -92,7 +100,7 @@ export function useLiveKit( ...defaultLiveKitOptions.videoCaptureDefaults, deviceId: initialDevices.current.videoInput.selectedId, // eslint-disable-next-line new-cap - processor: BackgroundBlur(15), + processor: blur, }, audioCaptureDefaults: { ...defaultLiveKitOptions.audioCaptureDefaults, @@ -103,7 +111,7 @@ export function useLiveKit( }, e2ee: e2eeOptions, }), - [e2eeOptions], + [blur, e2eeOptions], ); // Store if audio/video are currently updating. If to prohibit unnecessary calls @@ -143,6 +151,8 @@ export function useLiveKit( >(undefined); useEffect(() => { + // Fon't even try if we cannot blur on this platform + if (!blur) return; if (!room || videoTrackPromise.current) return; const update = async (): Promise => { let publishCallback: undefined | ((track: LocalTrackPublication) => void); diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index db630d09..6b5db196 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -112,7 +112,16 @@ export const LobbyView: FC = ({ ); // eslint-disable-next-line new-cap - const blur = useMemo(() => BackgroundBlur(15), []); + const blur = useMemo(() => { + let b = undefined; + try { + // eslint-disable-next-line new-cap + b = BackgroundBlur(15); + } catch (e) { + logger.error("disable background blur", e); + } + return b; + }, []); const localTrackOptions = useMemo( () => ({ @@ -158,6 +167,8 @@ export const LobbyView: FC = ({ const [showBackgroundBlur] = useSetting(backgroundBlurSettings); useEffect(() => { + // Fon't even try if we cannot blur on this platform + if (!blur) return; const updateBlur = async (showBlur: boolean): Promise => { if (showBlur && !videoTrack?.getProcessor()) { // eslint-disable-next-line new-cap diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 81e5f70e..2fd19c38 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -14,6 +14,8 @@ import { Text, Tooltip, } from "@vector-im/compound-web"; +import { BackgroundBlur } from "@livekit/track-processors"; +import { logger } from "matrix-js-sdk/src/logger"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -79,7 +81,18 @@ export const SettingsModal: FC = ({ // Generate a `Checkbox` input to turn blur on or off. const BlurCheckbox: React.FC = (): ReactNode => { const [blur, setBlur] = useSetting(backgroundBlurSetting); - return ( + let canBlur = true; + try { + // eslint-disable-next-line new-cap + BackgroundBlur(15); + } catch (e) { + logger.debug( + "Cannot blur, so we do not show the option in settings. error: ", + e, + ); + canBlur = false; + } + return canBlur ? ( <>

{t("settings.background_blur_header")}

@@ -100,7 +113,7 @@ export const SettingsModal: FC = ({ - ); + ) : null; }; const optInDescription = ( From 70ff435a8cedb650c5fc14d5d961cebc9ebd214a Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 21 Nov 2024 17:28:11 +0100 Subject: [PATCH 003/155] add types --- package.json | 2 ++ yarn.lock | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/package.json b/package.json index f0705f97..6e69dd28 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@types/content-type": "^1.1.5", + "@types/dom-mediacapture-transform": "^0.1.10", + "@types/dom-webcodecs": "^0.1.13", "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", diff --git a/yarn.lock b/yarn.lock index 1a429fc8..9b4f1335 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2999,6 +2999,18 @@ resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.8.tgz#319644d07ee6b4bfc734483008393b89b99f0219" integrity sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg== +"@types/dom-mediacapture-transform@^0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.10.tgz#93e2c04284c95dd2faa2992216417599ee9d27a6" + integrity sha512-zUxMN2iShu7p3Fz5sqfvLp93qW/3sLs+RwXWWOkMb969hsuoVqUUokqrENjXqTMNmEEcVXKoHuMMbIGcWyrVVA== + dependencies: + "@types/dom-webcodecs" "*" + +"@types/dom-webcodecs@*", "@types/dom-webcodecs@^0.1.13": + version "0.1.13" + resolved "https://registry.yarnpkg.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz#d8be5da4f01b20721307b08ad2cca903ccf4f47f" + integrity sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ== + "@types/estree@1.0.6", "@types/estree@^1.0.0": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" From d23beefb6091ee1bc96c3776313ea7196449002f Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 18:18:07 +0100 Subject: [PATCH 004/155] remove unused dependencies. --- package.json | 2 -- yarn.lock | 16 ++-------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 6e69dd28..f0705f97 100644 --- a/package.json +++ b/package.json @@ -50,8 +50,6 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@types/content-type": "^1.1.5", - "@types/dom-mediacapture-transform": "^0.1.10", - "@types/dom-webcodecs": "^0.1.13", "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", diff --git a/yarn.lock b/yarn.lock index 9b4f1335..4b5325c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2999,18 +2999,6 @@ resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.8.tgz#319644d07ee6b4bfc734483008393b89b99f0219" integrity sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg== -"@types/dom-mediacapture-transform@^0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.10.tgz#93e2c04284c95dd2faa2992216417599ee9d27a6" - integrity sha512-zUxMN2iShu7p3Fz5sqfvLp93qW/3sLs+RwXWWOkMb969hsuoVqUUokqrENjXqTMNmEEcVXKoHuMMbIGcWyrVVA== - dependencies: - "@types/dom-webcodecs" "*" - -"@types/dom-webcodecs@*", "@types/dom-webcodecs@^0.1.13": - version "0.1.13" - resolved "https://registry.yarnpkg.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz#d8be5da4f01b20721307b08ad2cca903ccf4f47f" - integrity sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ== - "@types/estree@1.0.6", "@types/estree@^1.0.0": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" @@ -6280,14 +6268,14 @@ matrix-js-sdk@matrix-org/matrix-js-sdk#develop: jwt-decode "^4.0.0" loglevel "^1.7.1" matrix-events-sdk "0.0.1" - matrix-widget-api "^1.8.2" + matrix-widget-api "^1.10.0" oidc-client-ts "^3.0.1" p-retry "4" sdp-transform "^2.14.1" unhomoglyph "^1.0.6" uuid "11" -matrix-widget-api@^1.10.0, matrix-widget-api@^1.8.2: +matrix-widget-api@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-1.10.0.tgz#d31ea073a5871a1fb1a511ef900b0c125a37bf55" integrity sha512-rkAJ29briYV7TJnfBVLVSKtpeBrBju15JZFSDP6wj8YdbCu1bdmlplJayQ+vYaw1x4fzI49Q+Nz3E85s46sRDw== From 95bc7db28ac38f59a3892e4a013fae4b351994a3 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 18:18:50 +0100 Subject: [PATCH 005/155] update blur checkbox (disabled state) --- locales/en/app.json | 2 +- src/settings/SettingsModal.tsx | 30 ++++++++++++++---------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 07edc5e5..3c2647bd 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -152,7 +152,7 @@ }, "background_blur_header": "Background", "background_blur_label": "Enable background blurring", - "blur_not_supported_by_browser": "", + "blur_not_supported_by_browser": "(Background blur is not supported by this browser.)", "developer_settings_label": "Developer Settings", "developer_settings_label_description": "Expose developer settings in the settings window.", "developer_tab_title": "Developer", diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 2fd19c38..41f66e8d 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -91,29 +91,27 @@ export const SettingsModal: FC = ({ e, ); canBlur = false; + setBlur(false); } - return canBlur ? ( + return ( <>

{t("settings.background_blur_header")}

+ - - setBlur(b.target.checked)} - disabled={isFirefox()} - /> - + type="checkbox" + checked={blur} + onChange={(b): void => setBlur(b.target.checked)} + disabled={!canBlur} + /> - ) : null; + ); }; const optInDescription = ( From 0be312a5e3f421277480f561fe7b6cc506c1015d Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 18:25:37 +0100 Subject: [PATCH 006/155] i18n --- locales/en/app.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 3c2647bd..2b610560 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -175,8 +175,7 @@ "preferences_tab_h4": "Preferences", "preferences_tab_show_hand_raised_timer_description": "Show a timer when a participant raises their hand", "preferences_tab_show_hand_raised_timer_label": "Show hand raise duration", - "speaker_device_selection_label": "Speaker", - "video_tab_activate_background_blur": "Turn on background blur on your webcam video" + "speaker_device_selection_label": "Speaker" }, "star_rating_input_label_one": "{{count}} star", "star_rating_input_label_other": "{{count}} stars", From aade8f94a651c34cead3105bc16f9bfd816a9163 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 18:34:33 +0100 Subject: [PATCH 007/155] eslint + prettier --- src/settings/SettingsModal.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 41f66e8d..84583b8f 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -8,12 +8,7 @@ Please see LICENSE in the repository root for full details. import { ChangeEvent, FC, ReactNode, useCallback, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; -import { - Root as Form, - Separator, - Text, - Tooltip, -} from "@vector-im/compound-web"; +import { Root as Form, Separator, Text } from "@vector-im/compound-web"; import { BackgroundBlur } from "@livekit/track-processors"; import { logger } from "matrix-js-sdk/src/logger"; From ae71c6e679463ec5c8eb5efdf50987e30fc5301d Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 20:05:35 +0100 Subject: [PATCH 008/155] add @types/dom-mediacapture-transform for the remaining type errors --- package.json | 3 ++- yarn.lock | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f0705f97..ede9bab3 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "strip-ansi": "6.0.1" }, "dependencies": { - "@livekit/track-processors": "^0.3.2" + "@livekit/track-processors": "^0.3.2", + "@types/dom-mediacapture-transform": "^0.1.10" } } diff --git a/yarn.lock b/yarn.lock index 4b5325c3..1a456433 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2999,6 +2999,18 @@ resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.8.tgz#319644d07ee6b4bfc734483008393b89b99f0219" integrity sha512-1tBhmVUeso3+ahfyaKluXe38p+94lovUZdoVfQ3OnJo9uJC42JT7CBoN3k9HYhAae+GwiBYmHu+N9FZhOG+2Pg== +"@types/dom-mediacapture-transform@^0.1.10": + version "0.1.10" + resolved "https://registry.yarnpkg.com/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.10.tgz#93e2c04284c95dd2faa2992216417599ee9d27a6" + integrity sha512-zUxMN2iShu7p3Fz5sqfvLp93qW/3sLs+RwXWWOkMb969hsuoVqUUokqrENjXqTMNmEEcVXKoHuMMbIGcWyrVVA== + dependencies: + "@types/dom-webcodecs" "*" + +"@types/dom-webcodecs@*": + version "0.1.13" + resolved "https://registry.yarnpkg.com/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz#d8be5da4f01b20721307b08ad2cca903ccf4f47f" + integrity sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ== + "@types/estree@1.0.6", "@types/estree@^1.0.0": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" From a6745ef34a6380cf9515dd8c46b1b9e3f08b08c6 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 20:15:37 +0100 Subject: [PATCH 009/155] properly add @livekit/track-processors --- knip.ts | 4 ++++ package.json | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/knip.ts b/knip.ts index becafc2e..5ffe03c4 100644 --- a/knip.ts +++ b/knip.ts @@ -24,6 +24,10 @@ export default { // then Knip will flag it as a false positive // https://github.com/webpro-nl/knip/issues/766 "@vector-im/compound-web", + // We need this so the eslint is happy with @livekit/track-processors. + // This might be a bug in the livekit repo but for now we fix it on the + // element call side. + "@types/dom-mediacapture-transform", "matrix-widget-api", ], ignoreExportsUsedInFile: true, diff --git a/package.json b/package.json index ede9bab3..36e03468 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.11.0", "@livekit/components-react": "^2.0.0", + "@livekit/track-processors": "^0.3.2", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^1.25.1", "@opentelemetry/exporter-trace-otlp-http": "^0.55.0", @@ -50,6 +51,7 @@ "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.1", "@types/content-type": "^1.1.5", + "@types/dom-mediacapture-transform": "^0.1.10", "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", @@ -122,9 +124,5 @@ }, "resolutions": { "strip-ansi": "6.0.1" - }, - "dependencies": { - "@livekit/track-processors": "^0.3.2", - "@types/dom-mediacapture-transform": "^0.1.10" } } From c2b651802745bb237f251b1a5cb61c94b6d469cc Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 20:39:45 +0100 Subject: [PATCH 010/155] dont update if processor already set --- src/livekit/useLiveKit.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index f99fbabd..6e563f1c 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -151,7 +151,7 @@ export function useLiveKit( >(undefined); useEffect(() => { - // Fon't even try if we cannot blur on this platform + // Don't even try if we cannot blur on this platform if (!blur) return; if (!room || videoTrackPromise.current) return; const update = async (): Promise => { @@ -178,7 +178,10 @@ export function useLiveKit( if (publishCallback) room.off(RoomEvent.LocalTrackPublished, publishCallback); - if (videoTrack !== undefined) { + if ( + videoTrack !== undefined && + videoTrack.track?.getProcessor() === undefined + ) { if (showBackgroundBlur) { logger.info("Blur: set blur"); From 7d8e9a79695a876d7549d36b47b6e1490584c891 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 27 Nov 2024 21:00:09 +0100 Subject: [PATCH 011/155] fixes and gpu based blurring --- src/livekit/useLiveKit.ts | 16 +++++++++------- src/room/LobbyView.tsx | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 6e563f1c..d33077d2 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -87,7 +87,7 @@ export function useLiveKit( let b = undefined; try { // eslint-disable-next-line new-cap - b = BackgroundBlur(15); + b = BackgroundBlur(15, { delegate: "GPU" }); } catch (e) { logger.error("disable background blur", e); } @@ -178,15 +178,17 @@ export function useLiveKit( if (publishCallback) room.off(RoomEvent.LocalTrackPublished, publishCallback); - if ( - videoTrack !== undefined && - videoTrack.track?.getProcessor() === undefined - ) { - if (showBackgroundBlur) { + if (videoTrack !== undefined) { + if ( + showBackgroundBlur && + videoTrack.track?.getProcessor()?.name !== "background-blur" + ) { logger.info("Blur: set blur"); void videoTrack.track?.setProcessor(blur); - } else { + } else if ( + videoTrack.track?.getProcessor()?.name === "background-blur" + ) { void videoTrack.track?.stopProcessor(); } } diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 6b5db196..486f872f 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -116,7 +116,7 @@ export const LobbyView: FC = ({ let b = undefined; try { // eslint-disable-next-line new-cap - b = BackgroundBlur(15); + b = BackgroundBlur(15, { delegate: "GPU" }); } catch (e) { logger.error("disable background blur", e); } From 9eae9190503e5171eb2c34db2aefacd7a4b5e284 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 29 Nov 2024 18:18:40 +0100 Subject: [PATCH 012/155] make default false --- src/settings/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 6f52a525..27d28011 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -88,7 +88,7 @@ export const videoInput = new Setting( undefined, ); -export const backgroundBlur = new Setting("background-blur", true); +export const backgroundBlur = new Setting("background-blur", false); export const showHandRaisedTimer = new Setting( "hand-raised-show-timer", From d179db1e8f9865617bedafef11cab44772162d32 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 2 Dec 2024 11:55:22 +0100 Subject: [PATCH 013/155] more detailed error message --- src/room/LobbyView.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 486f872f..4f9d3a85 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -118,7 +118,10 @@ export const LobbyView: FC = ({ // eslint-disable-next-line new-cap b = BackgroundBlur(15, { delegate: "GPU" }); } catch (e) { - logger.error("disable background blur", e); + logger.error( + "disable background blur because its not supported by the platform.", + e, + ); } return b; }, []); From 95c3ca88592c61e2d70a383d607cec4de0d9e47d Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 2 Dec 2024 17:35:43 +0100 Subject: [PATCH 014/155] review --- src/livekit/useLiveKit.ts | 6 ++---- src/room/LobbyView.tsx | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index d33077d2..85f3178b 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -19,7 +19,7 @@ import { useEffect, useMemo, useRef } from "react"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -import { BackgroundBlur } from "@livekit/track-processors"; +import { BackgroundBlur as backgroundBlur } from "@livekit/track-processors"; import { defaultLiveKitOptions } from "./options"; import { SFUConfig } from "./openIDSFU"; @@ -86,8 +86,7 @@ export function useLiveKit( const blur = useMemo(() => { let b = undefined; try { - // eslint-disable-next-line new-cap - b = BackgroundBlur(15, { delegate: "GPU" }); + b = backgroundBlur(15, { delegate: "GPU" }); } catch (e) { logger.error("disable background blur", e); } @@ -99,7 +98,6 @@ export function useLiveKit( videoCaptureDefaults: { ...defaultLiveKitOptions.videoCaptureDefaults, deviceId: initialDevices.current.videoInput.selectedId, - // eslint-disable-next-line new-cap processor: blur, }, audioCaptureDefaults: { diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 4f9d3a85..880fdc75 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -16,7 +16,7 @@ import { usePreviewTracks } from "@livekit/components-react"; import { LocalVideoTrack, Track } from "livekit-client"; import { useObservable } from "observable-hooks"; import { map } from "rxjs"; -import { BackgroundBlur } from "@livekit/track-processors"; +import { BackgroundBlur as backgroundBlur } from "@livekit/track-processors"; import inCallStyles from "./InCallView.module.css"; import styles from "./LobbyView.module.css"; @@ -115,8 +115,7 @@ export const LobbyView: FC = ({ const blur = useMemo(() => { let b = undefined; try { - // eslint-disable-next-line new-cap - b = BackgroundBlur(15, { delegate: "GPU" }); + b = backgroundBlur(15, { delegate: "GPU" }); } catch (e) { logger.error( "disable background blur because its not supported by the platform.", @@ -138,14 +137,14 @@ export const LobbyView: FC = ({ video: muteStates.video.enabled && { deviceId: devices.videoInput.selectedId, // It should be possible to set a processor here: - // processor: blur, - // This causes a crash currently hence we do the effect below... + processor: blur, }, }), [ initialAudioOptions, muteStates.video.enabled, devices.videoInput.selectedId, + blur, ], ); @@ -174,7 +173,6 @@ export const LobbyView: FC = ({ if (!blur) return; const updateBlur = async (showBlur: boolean): Promise => { if (showBlur && !videoTrack?.getProcessor()) { - // eslint-disable-next-line new-cap await videoTrack?.setProcessor(blur); } else { await videoTrack?.stopProcessor(); From c6d48446f40547fb2eeb77fc0de640cb7b783c36 Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 2 Dec 2024 17:45:32 +0100 Subject: [PATCH 015/155] review --- locales/en/app.json | 4 ++-- src/room/LobbyView.tsx | 1 - src/settings/SettingsModal.tsx | 5 ++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index 2b610560..6b72876a 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -151,8 +151,8 @@ "effect_volume_label": "Sound effect volume" }, "background_blur_header": "Background", - "background_blur_label": "Enable background blurring", - "blur_not_supported_by_browser": "(Background blur is not supported by this browser.)", + "background_blur_label": "Blur the background of the video", + "blur_not_supported_by_browser": "(Background blur is not supported by this browser)", "developer_settings_label": "Developer Settings", "developer_settings_label_description": "Expose developer settings in the settings window.", "developer_tab_title": "Developer", diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 880fdc75..118302c6 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -111,7 +111,6 @@ export const LobbyView: FC = ({ muteStates.audio.enabled && { deviceId: devices.audioInput.selectedId }, ); - // eslint-disable-next-line new-cap const blur = useMemo(() => { let b = undefined; try { diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 84583b8f..4e9da359 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -9,7 +9,7 @@ import { ChangeEvent, FC, ReactNode, useCallback, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Root as Form, Separator, Text } from "@vector-im/compound-web"; -import { BackgroundBlur } from "@livekit/track-processors"; +import { BackgroundBlur as backgroundBlur } from "@livekit/track-processors"; import { logger } from "matrix-js-sdk/src/logger"; import { Modal } from "../Modal"; @@ -78,8 +78,7 @@ export const SettingsModal: FC = ({ const [blur, setBlur] = useSetting(backgroundBlurSetting); let canBlur = true; try { - // eslint-disable-next-line new-cap - BackgroundBlur(15); + backgroundBlur(15); } catch (e) { logger.debug( "Cannot blur, so we do not show the option in settings. error: ", From 574c89529a0d3392ddb4c56f18638118ede7300f Mon Sep 17 00:00:00 2001 From: Timo Date: Mon, 2 Dec 2024 17:51:31 +0100 Subject: [PATCH 016/155] disable blur in local track again --- src/room/LobbyView.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 118302c6..3626f82e 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -136,14 +136,13 @@ export const LobbyView: FC = ({ video: muteStates.video.enabled && { deviceId: devices.videoInput.selectedId, // It should be possible to set a processor here: - processor: blur, + // processor: blur, }, }), [ initialAudioOptions, muteStates.video.enabled, devices.videoInput.selectedId, - blur, ], ); From b77c4afff2a539a5e4b80c7a4ee83e9587eed102 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 6 Dec 2024 18:12:51 +0100 Subject: [PATCH 017/155] refactor - still should not work without a fixing upstream LK: https://github.com/livekit/components-js/pull/1042 https://github.com/livekit/components-js/pull/1043 --- src/App.tsx | 54 ++++++++----- src/livekit/TrackProcessorContext.tsx | 111 ++++++++++++++++++++++++++ src/livekit/useLiveKit.ts | 90 +++++---------------- src/room/LobbyView.tsx | 60 +++++--------- src/settings/SettingsModal.tsx | 30 +++---- yarn.lock | 16 ++-- 6 files changed, 204 insertions(+), 157 deletions(-) create mode 100644 src/livekit/TrackProcessorContext.tsx diff --git a/src/App.tsx b/src/App.tsx index 8d841dba..5d96877b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import { Initializer } from "./initializer"; import { MediaDevicesProvider } from "./livekit/MediaDevicesContext"; import { widget } from "./widget"; import { useTheme } from "./useTheme"; +import { ProcessorProvider } from "./livekit/TrackProcessorContext"; const SentryRoute = Sentry.withSentryRouting(Route); @@ -82,27 +83,25 @@ export const App: FC = ({ history }) => { {loaded ? ( - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + ) : ( @@ -113,3 +112,16 @@ export const App: FC = ({ history }) => { ); }; + +const Providers: FC<{ + children: JSX.Element; +}> = ({ children }) => { + // We use this to stack all used providers to not make the App component to verbose + return ( + + + {children} + + + ); +}; diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx new file mode 100644 index 00000000..c2bc3826 --- /dev/null +++ b/src/livekit/TrackProcessorContext.tsx @@ -0,0 +1,111 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { + BackgroundBlur as backgroundBlur, + BackgroundOptions, + ProcessorWrapper, +} from "@livekit/track-processors"; +import { + createContext, + FC, + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { logger } from "matrix-js-sdk/src/logger"; +import { LocalVideoTrack } from "livekit-client"; + +import { + backgroundBlur as backgroundBlurSettings, + useSetting, +} from "../settings/settings"; + +type ProcessorState = { + supported: boolean | undefined; + processor: undefined | ProcessorWrapper; + /** + * Call this method to try to initialize a processor. + * This only needs to happen if supported is undefined. + * If the backgroundBlur setting is set to true this does not need to be called + * and the processorState.supported will update automatically to the correct value. + */ + checkSupported: () => void; +}; +const ProcessorContext = createContext(undefined); + +export const useTrackProcessor = (): ProcessorState | undefined => + useContext(ProcessorContext); + +export const useTrackProcessorSync = ( + videoTrack: LocalVideoTrack | null, +): void => { + const { processor } = useTrackProcessor() || {}; + useEffect(() => { + if (processor && !videoTrack?.getProcessor()) { + void videoTrack?.setProcessor(processor); + } + if (!processor && videoTrack?.getProcessor()) { + void videoTrack?.stopProcessor(); + } + }, [processor, videoTrack]); +}; + +interface Props { + children: JSX.Element; +} +export const ProcessorProvider: FC = ({ children }) => { + // The setting the user wants to have + const [blurActivated] = useSetting(backgroundBlurSettings); + + // If `ProcessorState.supported` is undefined the user can activate that we want + // to have it at least checked (this is useful to show the settings menu properly) + // We dont want to try initializing the blur if the user is not even looking at the setting + const [shouldCheckSupport, setShouldCheckSupport] = useState(blurActivated); + + // Cache the processor so we only need to initialize it once. + const blur = useRef | undefined>( + undefined, + ); + + const checkSupported = useCallback(() => { + setShouldCheckSupport(true); + }, []); + // This is the actual state exposed through the context + const [processorState, setProcessorState] = useState(() => ({ + supported: false, + processor: undefined, + checkSupported, + })); + + useEffect(() => { + if (!shouldCheckSupport) return; + try { + if (!blur.current) blur.current = backgroundBlur(15, { delegate: "GPU" }); + setProcessorState({ + checkSupported, + supported: true, + processor: blurActivated ? blur.current : undefined, + }); + } catch (e) { + setProcessorState({ + checkSupported, + supported: false, + processor: undefined, + }); + logger.error("disable background blur", e); + } + }, [blurActivated, checkSupported, shouldCheckSupport]); + + return ( + + {children} + + ); +}; diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 85f3178b..4836141e 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -9,9 +9,8 @@ import { ConnectionState, E2EEOptions, ExternalE2EEKeyProvider, - LocalTrackPublication, + LocalVideoTrack, Room, - RoomEvent, RoomOptions, Track, } from "livekit-client"; @@ -19,7 +18,6 @@ import { useEffect, useMemo, useRef } from "react"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -import { BackgroundBlur as backgroundBlur } from "@livekit/track-processors"; import { defaultLiveKitOptions } from "./options"; import { SFUConfig } from "./openIDSFU"; @@ -29,7 +27,6 @@ import { MediaDevices, useMediaDevices, } from "./MediaDevicesContext"; -import { backgroundBlur as backgroundBlurSettings } from "../settings/settings"; import { ECConnectionState, useECConnectionState, @@ -37,7 +34,11 @@ import { import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { E2eeType } from "../e2ee/e2eeType"; import { EncryptionSystem } from "../e2ee/sharedKeyManagement"; -import { useSetting } from "../settings/settings"; +import { + useTrackProcessor, + useTrackProcessorSync, +} from "./TrackProcessorContext"; +import { useInitial } from "../useInitial"; interface UseLivekitResult { livekitRoom?: Room; @@ -83,22 +84,16 @@ export function useLiveKit( const initialMuteStates = useRef(muteStates); const devices = useMediaDevices(); const initialDevices = useRef(devices); - const blur = useMemo(() => { - let b = undefined; - try { - b = backgroundBlur(15, { delegate: "GPU" }); - } catch (e) { - logger.error("disable background blur", e); - } - return b; - }, []); + + const { processor } = useTrackProcessor() || {}; + const initialProcessor = useInitial(() => processor); const roomOptions = useMemo( (): RoomOptions => ({ ...defaultLiveKitOptions, videoCaptureDefaults: { ...defaultLiveKitOptions.videoCaptureDefaults, deviceId: initialDevices.current.videoInput.selectedId, - processor: blur, + processor: initialProcessor, }, audioCaptureDefaults: { ...defaultLiveKitOptions.audioCaptureDefaults, @@ -109,7 +104,7 @@ export function useLiveKit( }, e2ee: e2eeOptions, }), - [blur, e2eeOptions], + [e2eeOptions, initialProcessor], ); // Store if audio/video are currently updating. If to prohibit unnecessary calls @@ -134,6 +129,15 @@ export function useLiveKit( return r; }, [roomOptions, e2eeSystem]); + const videoTrack = useMemo( + () => + Array.from(room.localParticipant.videoTrackPublications.values()).find( + (v) => v.source === Track.Source.Camera, + )?.track as LocalVideoTrack | null, + [room.localParticipant.videoTrackPublications], + ); + useTrackProcessorSync(videoTrack); + const connectionState = useECConnectionState( { deviceId: initialDevices.current.audioInput.selectedId, @@ -143,58 +147,6 @@ export function useLiveKit( sfuConfig, ); - const [showBackgroundBlur] = useSetting(backgroundBlurSettings); - const videoTrackPromise = useRef< - undefined | Promise - >(undefined); - - useEffect(() => { - // Don't even try if we cannot blur on this platform - if (!blur) return; - if (!room || videoTrackPromise.current) return; - const update = async (): Promise => { - let publishCallback: undefined | ((track: LocalTrackPublication) => void); - videoTrackPromise.current = new Promise< - LocalTrackPublication | undefined - >((resolve) => { - const videoTrack = Array.from( - room.localParticipant.videoTrackPublications.values(), - ).find((v) => v.source === Track.Source.Camera); - if (videoTrack) { - resolve(videoTrack); - } - publishCallback = (videoTrack: LocalTrackPublication): void => { - if (videoTrack.source === Track.Source.Camera) { - resolve(videoTrack); - } - }; - room.on(RoomEvent.LocalTrackPublished, publishCallback); - }); - - const videoTrack = await videoTrackPromise.current; - - if (publishCallback) - room.off(RoomEvent.LocalTrackPublished, publishCallback); - - if (videoTrack !== undefined) { - if ( - showBackgroundBlur && - videoTrack.track?.getProcessor()?.name !== "background-blur" - ) { - logger.info("Blur: set blur"); - - void videoTrack.track?.setProcessor(blur); - } else if ( - videoTrack.track?.getProcessor()?.name === "background-blur" - ) { - void videoTrack.track?.stopProcessor(); - } - } - videoTrackPromise.current = undefined; - }; - void update(); - }, [blur, room, showBackgroundBlur]); - useEffect(() => { // Sync the requested mute states with LiveKit's mute states. We do it this // way around rather than using LiveKit as the source of truth, so that the @@ -261,6 +213,7 @@ export function useLiveKit( audioMuteUpdating.current = true; trackPublication = await participant.setMicrophoneEnabled( buttonEnabled.current.audio, + room.options.audioCaptureDefaults, ); audioMuteUpdating.current = false; break; @@ -268,6 +221,7 @@ export function useLiveKit( videoMuteUpdating.current = true; trackPublication = await participant.setCameraEnabled( buttonEnabled.current.video, + room.options.videoCaptureDefaults, ); videoMuteUpdating.current = false; break; diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 3626f82e..545eff09 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -13,10 +13,13 @@ import classNames from "classnames"; import { useHistory } from "react-router-dom"; import { logger } from "matrix-js-sdk/src/logger"; import { usePreviewTracks } from "@livekit/components-react"; -import { LocalVideoTrack, Track } from "livekit-client"; +import { + CreateLocalTracksOptions, + LocalVideoTrack, + Track, +} from "livekit-client"; import { useObservable } from "observable-hooks"; import { map } from "rxjs"; -import { BackgroundBlur as backgroundBlur } from "@livekit/track-processors"; import inCallStyles from "./InCallView.module.css"; import styles from "./LobbyView.module.css"; @@ -33,14 +36,16 @@ import { VideoButton, } from "../button/Button"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; -import { backgroundBlur as backgroundBlurSettings } from "../settings/settings"; import { useMediaQuery } from "../useMediaQuery"; import { E2eeType } from "../e2ee/e2eeType"; import { Link } from "../button/Link"; import { useMediaDevices } from "../livekit/MediaDevicesContext"; import { useInitial } from "../useInitial"; import { useSwitchCamera as useShowSwitchCamera } from "./useSwitchCamera"; -import { useSetting } from "../settings/settings"; +import { + useTrackProcessor, + useTrackProcessorSync, +} from "../livekit/TrackProcessorContext"; interface Props { client: MatrixClient; @@ -111,20 +116,10 @@ export const LobbyView: FC = ({ muteStates.audio.enabled && { deviceId: devices.audioInput.selectedId }, ); - const blur = useMemo(() => { - let b = undefined; - try { - b = backgroundBlur(15, { delegate: "GPU" }); - } catch (e) { - logger.error( - "disable background blur because its not supported by the platform.", - e, - ); - } - return b; - }, []); + const { processor } = useTrackProcessor() || {}; - const localTrackOptions = useMemo( + const initialProcessor = useInitial(() => processor); + const localTrackOptions = useMemo( () => ({ // The only reason we request audio here is to get the audio permission // request over with at the same time. But changing the audio settings @@ -135,14 +130,14 @@ export const LobbyView: FC = ({ audio: Object.assign({}, initialAudioOptions), video: muteStates.video.enabled && { deviceId: devices.videoInput.selectedId, - // It should be possible to set a processor here: - // processor: blur, + processor: initialProcessor, }, }), [ initialAudioOptions, muteStates.video.enabled, devices.videoInput.selectedId, + initialProcessor, ], ); @@ -157,28 +152,11 @@ export const LobbyView: FC = ({ const tracks = usePreviewTracks(localTrackOptions, onError); - const videoTrack = useMemo( - () => - (tracks?.find((t) => t.kind === Track.Kind.Video) ?? - null) as LocalVideoTrack | null, - [tracks], - ); - - const [showBackgroundBlur] = useSetting(backgroundBlurSettings); - - useEffect(() => { - // Fon't even try if we cannot blur on this platform - if (!blur) return; - const updateBlur = async (showBlur: boolean): Promise => { - if (showBlur && !videoTrack?.getProcessor()) { - await videoTrack?.setProcessor(blur); - } else { - await videoTrack?.stopProcessor(); - } - }; - if (videoTrack) void updateBlur(showBackgroundBlur); - }, [videoTrack, showBackgroundBlur, blur]); - + const videoTrack = useMemo(() => { + const track = tracks?.find((t) => t.kind === Track.Kind.Video); + return track as LocalVideoTrack | null; + }, [tracks]); + useTrackProcessorSync(videoTrack); const showSwitchCamera = useShowSwitchCamera( useObservable( (inputs) => inputs.pipe(map(([video]) => video)), diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 4e9da359..11d73f4d 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,12 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC, ReactNode, useCallback, useState } from "react"; +import { ChangeEvent, FC, ReactNode, useCallback, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Root as Form, Separator, Text } from "@vector-im/compound-web"; -import { BackgroundBlur as backgroundBlur } from "@livekit/track-processors"; -import { logger } from "matrix-js-sdk/src/logger"; import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; @@ -36,6 +34,7 @@ import { isFirefox } from "../Platform"; import { PreferencesSettingsTab } from "./PreferencesSettingsTab"; import { Slider } from "../Slider"; import { DeviceSelection } from "./DeviceSelection"; +import { useTrackProcessor } from "../livekit/TrackProcessorContext"; type SettingsTab = | "audio" @@ -75,18 +74,11 @@ export const SettingsModal: FC = ({ // Generate a `Checkbox` input to turn blur on or off. const BlurCheckbox: React.FC = (): ReactNode => { - const [blur, setBlur] = useSetting(backgroundBlurSetting); - let canBlur = true; - try { - backgroundBlur(15); - } catch (e) { - logger.debug( - "Cannot blur, so we do not show the option in settings. error: ", - e, - ); - canBlur = false; - setBlur(false); - } + const { supported, checkSupported } = useTrackProcessor() || {}; + useEffect(() => checkSupported?.(), [checkSupported]); + + const [blurActive, setBlurActive] = useSetting(backgroundBlurSetting); + return ( <>

{t("settings.background_blur_header")}

@@ -96,12 +88,12 @@ export const SettingsModal: FC = ({ id="activateBackgroundBlur" label={t("settings.background_blur_label")} description={ - canBlur ? "" : t("settings.blur_not_supported_by_browser") + supported ? "" : t("settings.blur_not_supported_by_browser") } type="checkbox" - checked={blur} - onChange={(b): void => setBlur(b.target.checked)} - disabled={!canBlur} + checked={!!blurActive} + onChange={(b): void => setBlurActive(b.target.checked)} + disabled={!supported} /> diff --git a/yarn.lock b/yarn.lock index 1a456433..b4fc53aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1805,10 +1805,10 @@ resolved "https://registry.yarnpkg.com/@livekit/mutex/-/mutex-1.0.0.tgz#9493102d92ff75dfb0445eccc46c7c7ac189d385" integrity sha512-aiUhoThBNF9UyGTxEURFzJLhhPLIVTnQiEVMjRhPnfHNKLfo2JY9xovHKIus7B78UD5hsP6DlgpmAsjrz4U0Iw== -"@livekit/protocol@1.24.0": - version "1.24.0" - resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.24.0.tgz#b23acab25c11027bf26c1b42f9b782682f2da585" - integrity sha512-9dCsqnkMn7lvbI4NGh18zhLDsrXyUcpS++TEFgEk5Xv1WM3R2kT3EzqgL1P/mr3jaabM6rJ8wZA/KJLuQNpF5w== +"@livekit/protocol@1.29.3": + version "1.29.3" + resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.29.3.tgz#486ce215c0c591ad64036d9b13c7e28f5417cf03" + integrity sha512-5La/pm2LsSeCbm7xNe/TvHGYu7uVwDpLrlycpgo5nzofGq/TH67255vS8ni/1Y7vrFuAI8VYG/s42mcC1UF6tQ== dependencies: "@bufbuild/protobuf" "^1.10.0" @@ -6125,12 +6125,12 @@ lines-and-columns@^1.1.6: integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== livekit-client@^2.5.7: - version "2.7.0" - resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.7.0.tgz#d7a80aff4ad335dd093b0c90d0d715466539651a" - integrity sha512-4vjfSReFNAUD+2oLUz9qFRWztJaI/+AexpOmCgizNsPYpvvqgAvEGxapnhuAug9uP7JVYaKPXaTCq90MWZoDHg== + version "2.7.3" + resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.7.3.tgz#70a5f5016f3f50b1282f4b9090aa17a39f8bde09" + integrity sha512-oHEmUTFjIJARi5R87PsobZx8y2HCSUwla3Nu71EqDOAMnNY9aoGMLsJVao5Y+v1TSk71rgRm991fihgxtbg5xw== dependencies: "@livekit/mutex" "1.0.0" - "@livekit/protocol" "1.24.0" + "@livekit/protocol" "1.29.3" events "^3.3.0" loglevel "^1.8.0" sdp-transform "^2.14.1" From 59bc73cedfbd14ba22cf97a0e5db4cf4116af033 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 9 Dec 2024 22:04:09 +0000 Subject: [PATCH 018/155] Use local assets for blurring --- package.json | 1 + src/livekit/BlurBackgroundTransformer.ts | 62 ++++++++++++++++++ src/livekit/TrackProcessorContext.tsx | 14 ++-- src/mediapipe/imageSegmenter/README.md | 5 ++ .../imageSegmenter/selfie_segmenter.tflite | Bin 0 -> 249537 bytes yarn.lock | 5 ++ 6 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 src/livekit/BlurBackgroundTransformer.ts create mode 100644 src/mediapipe/imageSegmenter/README.md create mode 100644 src/mediapipe/imageSegmenter/selfie_segmenter.tflite diff --git a/package.json b/package.json index 36e03468..30f967e8 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@livekit/components-core": "^0.11.0", "@livekit/components-react": "^2.0.0", "@livekit/track-processors": "^0.3.2", + "@mediapipe/tasks-vision": "^0.10.18", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^1.25.1", "@opentelemetry/exporter-trace-otlp-http": "^0.55.0", diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts new file mode 100644 index 00000000..7273e487 --- /dev/null +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -0,0 +1,62 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only +Please see LICENSE in the repository root for full details. +*/ + +import { + BackgroundTransformer, + VideoTransformer, + VideoTransformerInitOptions, +} from "@livekit/track-processors"; +import { ImageSegmenter } from "@mediapipe/tasks-vision"; + +interface WasmFileset { + /** The path to the Wasm loader script. */ + wasmLoaderPath: string; + /** The path to the Wasm binary. */ + wasmBinaryPath: string; +} + +// n.b. this only includes the SIMD versions of the WASM files which have good support: +// https://caniuse.com/?search=simd +const wasmFileset: WasmFileset = { + wasmLoaderPath: new URL( + "../../node_modules/@mediapipe/tasks-vision/wasm/vision_wasm_internal.js", + import.meta.url, + ).href, + wasmBinaryPath: new URL( + "../../node_modules/@mediapipe/tasks-vision/wasm/vision_wasm_internal.wasm", + import.meta.url, + ).href, +}; + +const modelAssetPath = new URL( + "../mediapipe/imageSegmenter/selfie_segmenter.tflite", + import.meta.url, +).href; + +export class BlurBackgroundTransformer extends BackgroundTransformer { + public async init({ + outputCanvas, + inputElement: inputVideo, + }: VideoTransformerInitOptions): Promise { + // call super.super.init() + await VideoTransformer.prototype.init.call(this, { + outputCanvas, + inputElement: inputVideo, + }); + + this.imageSegmenter = await ImageSegmenter.createFromOptions(wasmFileset, { + baseOptions: { + modelAssetPath, + delegate: "GPU", + ...this.options.segmenterOptions, + }, + runningMode: "VIDEO", + outputCategoryMask: true, + outputConfidenceMasks: false, + }); + } +} diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index c2bc3826..71714b6e 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { - BackgroundBlur as backgroundBlur, - BackgroundOptions, - ProcessorWrapper, -} from "@livekit/track-processors"; +import { BackgroundOptions, ProcessorWrapper } from "@livekit/track-processors"; import { createContext, FC, @@ -26,6 +22,7 @@ import { backgroundBlur as backgroundBlurSettings, useSetting, } from "../settings/settings"; +import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer"; type ProcessorState = { supported: boolean | undefined; @@ -87,7 +84,12 @@ export const ProcessorProvider: FC = ({ children }) => { useEffect(() => { if (!shouldCheckSupport) return; try { - if (!blur.current) blur.current = backgroundBlur(15, { delegate: "GPU" }); + if (!blur.current) { + blur.current = new ProcessorWrapper( + new BlurBackgroundTransformer({ blurRadius: 15 }), + "background-blur", + ); + } setProcessorState({ checkSupported, supported: true, diff --git a/src/mediapipe/imageSegmenter/README.md b/src/mediapipe/imageSegmenter/README.md new file mode 100644 index 00000000..39bea2d8 --- /dev/null +++ b/src/mediapipe/imageSegmenter/README.md @@ -0,0 +1,5 @@ +# Google AI Edge MediaPipe Selfie Segmentation + +- See: https://ai.google.dev/edge/mediapipe/solutions/vision/image_segmenter +- Latest: https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite +- License: Apache 2.0 as per https://storage.googleapis.com/mediapipe-assets/Model%20Card%20MediaPipe%20Selfie%20Segmentation.pdf diff --git a/src/mediapipe/imageSegmenter/selfie_segmenter.tflite b/src/mediapipe/imageSegmenter/selfie_segmenter.tflite new file mode 100644 index 0000000000000000000000000000000000000000..a4ebd4777ede40ba04abc8e5c28a73407cb7a1a0 GIT binary patch literal 249537 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

SDFw!?va%i&|4Ps44A)N#z zes}P;^d4H}9;e90LS?Y9nmQy*20BD-whs*S(WAmysk_38-kf$+qQ-fGrSAA<9%S$8 z*BI0Be=)Nj^(|w&62Gb^tfPn`@_b-8x!T>56UQbHJ-cb z+aLslpf+!ufgBg&$+dVJJkvbMvW>WeFQx9I6u&=ihuX#7it2_968Zx#1MegY_J-)A z(k+hDD+6P&5>FQ>O}9As4#Wect%T{?fvxU`_fB@Im(vm#n#Y z{tL8W_PIK{ry45tW8jXu7;Io{B2?sFW#Eh@unVCQ$FO7aXy4?bZ14rx75;>znjD<9 z@FEcHPcs{t-dr=WCOQDQqW=}RU)WUwiaxqN7)tSn`H25dbAQiqjDq*DBj5?xQNL*W zYCA}LSGUlLxY$!wo5Pe-#faA5WA>tbiN#DAMdMLoVy0vxk^RA*YL?3-+O}{Dwp# z?*eqdFo(YC&G%8p#X^}c5iP~$B1NwK*dr{TjVbKIH^dI*F2>smE8vU#RdO`jh#ha) zAbN3vyWsO;<#aE8jb#^noqeRTP*byke*+W*MjBnr(m=VTmc_3-EtKI4-7TOt-hT_< zxw=xH(N{*buv7G>z{Eg_LH*xZCx5(!v5eQ+f7`6B;CK4g&;_W8>nl48jbZK+sT{pw zJU5SYw9UX;VR7)^QX`8VuyKA^Vb}UGOFY(1aFW@~5ak*W(zT_-L=TQKM35!8fd9z- zt=)&FiUSBaW@Bh=QP;$paul=ExEFth3(9PKdiSWe(Ueo#WTc#2vE6(9xs6CAJYHsz|kS;|DTffX{O;7vYYRjvDUv!%np zVq`P53@O4Zs0fsR_hA~ELA6zQ4029rmhW(nGED%sBXR=bqu6z$_$Kr|YNUXQYmrF-;=mH~Jzxm2*t2|~M)KX%GRNeDHWK9_72W;YEe=Up>4uw2?H|s&+E__;C!aRlt!VSf?SQk%B z0z*ebH-H$x88}>cgz>^N;u;C_a0}D{3<#sJ9CHlT0B&5kdbeE|!WW?pp#owa_@8hc z%SLg$Is2GAFCHWtpoJbiyf=<#QMTN+lW94)lki3{;v$X>K4|)x(fXxa}SZRaR-gQ zvaEy5&qNDX2iq=eY+khJFEEX1<}O!cVc$6f13) zc40TU`w(QGqvdSgYy5%lQf|;AoUnC6GKDU}`>Z%*9FV{~HSfb?*tG(VC&A~CSiCwj z26`*w@L=T}bH;Z*Q0%cG+bp{h2Noq`Hd8lxjBhZuo#ZUL6Bp6{#UC)l=8P;DYjHzv zZ|zo(=;h|Q1^3!MGA#8l#(#{pi|!<%;QKr*aK?Rr7U+$Mr;Bn5^63SD9lal0WdZJI{tfKGe=5kZ%5s_3a#6tNW&L=MAjcOlqpY)Z_71NY9!YSY)P7AA*p7C9S zQy4rzTOVop;^$`7@tb!C{-0dSU%{6l+o<)F%UsWvN0PWi&H&9Y#wDuHomH%}f-Azw zscY zU;w+$&L%D>ANXsQje%ucKiKO!Lg=)FNMt4l{DC9DDh{!i^1qw zW8Gv*^O?Da+;92a*(T6M?;T^4{D$IcUrm24x52v&D)kP*e}Q_*b3Gr-JE=DLHQ}wo z32buCeD1Sr4Ehg_GfU$W(Uq1uz)$owZ*BA!af1002U^bI6qN-rIIOxK_{AWz5^)jsY&aJkS#<#?XRE z&}8dkONo@jwv_tHchtF@>g*{mFIh`YQ&_o*M47ejuI&%o5o<(iF06qL*#2bGh_ULd zY>w(`8;;$P7A2Qk=X(DMQwrn=W8I}b;w9I`V1?s?Czcva5coR1QyPpcP+w6~nHA1q z)Y^ib%mC+R$}FxZIl$f~XTvYB4W`BgMsAES8yan28rV+XHP`hVP<6mAY9bSl?UA_r zKix;H1Kr!i!^kV;_dqp&p0>gLgBpwtQ@ZE3X8+E6gk50kU^3Lq)=^xjkM^F`xSKDD z=RC*^6*II}#22Pc(1y+Qo`I67jqWm{K6F>>*1wSZ>7Id$%?&ghDPEU&#wFI4;_mM5?(Q7itx%}9k(tT(Owv?vJ-E9&9N^$^;5T>O`>yv7XjalS z@_hHR_hBz# zm#T8m0dDX7zg~b;Ei#+LRYnW0D;PL7TYqiA63G;WQLm$*i zU~kcj{S?u_3Zj8COS;b!VzFHfh8*|)@3YD^E?2G%R0*D$G=q*o-wF?eWH6N~&wqoe zKm+rKf-aF}d#d}H%>`%ZJ*N5zZ&fRHnEFaq1Bb~)(gZ#QyU13PJ7-_w>OuF>_&@_} zBGMHYO~&(HDYM~dVTbv-bO27(o?Dk#2CXCiht`uGTGj-zR8UyObk3=SR)PUhq<>>7 z)e`|6T~x!h5(9#b2Q6AtlC{*9Jy{S1~GHqu+gWZNa|JZmGgXNdJ9 zb`(60BES-LjwM|D3vEFqNZ*AN@rN}T8lxk6u>wqTEC}IE>hIr+n+%69V*Df05 zXYoGj3;LKPPW(jQR441Z(sOfNN;CBtvRZzi5!S!*H}(Bl&40uVMjF$4aAVy`s9ZFT zKaP6%JLom`HWG@cBVN`th38;=V0vDjf0&mNXr;jO!=v}K@h??(qEDdPf*ikH@cGs; zL!=z`5L;%6&8}$87W8@t_KNMxRbeWNfAdE3cHj)EELs|VsV7#5;vRE{>80FD$8i3m+E@Ar?h_y7mD&avy{79*jOc_0Vx$@4=BP2` zM(|6ZnVKWk^tAA!Rx@?}z(;oew7_eOA4iK zxf9t9)HFC>n@w12u6*4#$@+@DjNCUqj|<~7n2BII5Tdj@ifv50us^Usdru zum)%y7^Lm|IODdsO76)`>t|nqw@?f4aO?xv+q2eBqIz^QvV~0NqNN!ctEvKYjcE=p z6u*n#I30YP>zFiFXsWj6RvGP@ucR_u!BSl}kNr(_mQDiUmbZn!{d2)q&~ah5bsy4= ziPsc^^09rzcf{EPAH_ufMCQ5B&FoV4d+xCp^BCxkJ=%C*u7g|^Pm&E-zgVhu{5=-` zg64q`-!CaH=;XEj3CDfQB&0p`0*Yj-@iwV~emeWq^N%pj-^{vS)@eJbSC~o8fKyE! z;mY)J;0E@DxQ&q-^R=;vTUU6J;4Ul!E-07--`AXz@4eOS`KI3NNzHxc;B(b;I^(EuqS?m)@K%_+z~c4%hEdRy5v+9pMsZSzwxcq3~7qk zfN2qX+R!SlKZ6RlGu8+u&ZW#NF6w zwR`+rbiTpnx$n246--zB)v*F~vOE#GuCXuDw0pdc^$|WtDbaYGtLlp4yjn0hE+4!pWa>w`bMi8Hs?d%8QN6-9ZEG}C zI;2~mS2#62-b8Wb`~=b-Xb1177kg1U0`5)P`Fhv~^_mg^Er$++L!kvwNY~7dsmmZr zN4+m4}AD~;=F2=flVaj)S3h@1$J9Hu*F0L^rxgkU(Ktt-*7d_ftq$=gl-p}!H>o5Kyb7naEx!0I05U+ zEv1`j&-Si(7FJ)_oKVFQPG07_fVY6wR5z%q5fR6;FQDe|UpYCtna0!fZ*@IbJ^wqn ziTj97hkGfJ(qE3ur-Q_g)h7^n2 zG!Ncz3gRkq?<4PFVTo1PMbrbRGB8lnnGyr%JuBcWAq~UXOktpTn($bx#QmqQNB82! zS_W!lma@GkrNv-h>T2WH(kJQzpC;T3);k@vv~b zC6m}nUl5*Hn@C@b@rG=()z%1m0d6J-$$D)b`elx>6dU%_3)qJVBDvU4*xI8c`IK2H zj7JA!TR09)4@7F@{ZgQz1X=&HxPW+cB{@l~33f662fPM5=u}v-yzkx4I8XthG zA@yV@L8qWD5n)IDdFqjYq{A`mn>3xED$M85Z>rg)K}KyN_}FokV_?6LkA4{ zGyZpxE0rFW;}FUY(Oh1a@C{n`$s4XUvMo{1K-TA^&HR8QZ9~YVyd+QI(-KE%y~=%) z&Y8Z5SMn~fpt(16QJ0F;0-DR;z^$g^)fs& z`z0t49f))61@IVz(HC~SfF3EI`A7-Vm>;L;kuiqPx?OEyn&f zl<>Wg+E|6y3fL-3I$BSgZ3VFcpOD@d?4f=#x3Nf?uWc2%yUAJKnu0@?rJe|}kA64S zQ}_YIfeHRiNEuWS?m=9EHnMF!`|&|i7@7~Nz9{}K*_z)ZwiJ8sj)WR8RpYOVAlArp z(H6=XY9|$wu7I$Lj(J^SOETKlOjrk=z z5Z&qHU^DrwA&f78y6{J6iUXyLU@In`Iin@R_W2~t#jU7CM5eKa5B(kUl}8@5s- zbECbR-46{;W)r*1++$-Y%+dA7Cbtu-z0uylx- z+k77H&zYvNUz&r>rC0IcQvb}O;CAy${{)`Vu80r7B;XTW&AJJi5z~m)Hs)ZyVWaU8 z+}54pdkb#m`ccX3J9v|C4Ki6bfEmM~U=Fc@@1Sn89@a%0gGd_sw~%Nm%j>M2yq_?O zh;Z}-eF2Qd2AFR#O@Tc92=9ACF26w%;Uus!xB+8@&hThMW3)Vf%)c3!jHV#d!N2Hh zh|koEZ)eIC-wUqnqfDLnY-Wf(Nh{i5UpOuMr)>S4%Q7$wI~~;kRRt`6_uRGEu4K-)dS- zjY3lJ15#gXgyv{kY|O|F8V@Re=^t1(gWI`9BH?Z7otfX&ve{fg7)aeDeqkfMXXyTk zN4;tI5UppTGB{r>P-|L1XbqQvo?+Vy3*0p|*Va&Jn7@km5`UQO!ne0{hFZw$`0-j| zB$|DKj}@;fUy01juT*^m$6EUb8>9Ux#6QA7u)FXF&I04bYV2XCllqf?PfP;qTRvM| zTpZIaj-)yo_Oh+mMCvxTN)@3Y$ToTk+Y_zCcQc3lH-wM0WQa?j239q%us&l0(qSOo zyW4t&OFY<1$}e!`HNycWPqykh6TRVa)|TRPsj~Kj8A;{&K4A0tEGV2^!xog5s@DaB=8tNV3qiqWDmy;;eFn9 zWCdDN>1A1jwnfw7`s#yx2WHY;V!*sC-(AgXI^KF(_z0w^hk#0ID{ac(BrZ3Hv+aP| z#0qekU*q3F1$bN5NCw$9mXShrWe-)$)Bq}$?ObPNb#{18bze73FAkGFuvhVNsFfW; zb&cF=X)Lc-W#EZ20Er5O zhMXW*43_h3z1rW^RhKwM;u$k6Wh^56x(6`e`Xar}Eu%Pcjyb0BJ8m-Pgin&6-yT0u+{T`?{*ik?fmjbY z9led7gMK#d9sAtB4!=xPigs#!5)ZYrU?SU>j79)B0!~Kj>$YkcDF!}=?n}57nTo}e zZIrv>1yjiCASW6hbA`w+I3RA}7O$&7uM#_XdlOgOT`eE2E3_Wcv*JduKGi_$qPWXt zN(0nY{+sA6^eH%uoty!27my=TspXl3sXpio@=I5RuuaIF+%tX)I!5ni`_U83^Ob4JfgBaQXA2AZsJ#3o zP<5~y??M5(V!~?A@yrwSRA>-j&^MF~iG8d);7(#Y|0HCY(hAwmoIrp2yBjEFG<#e5 zj^5!m%8TF$;A^c9sJ-IE?jxP7{WDgAqrk(=@tFOdaQtz!Rrd@Xt9~G-iVgfjnMJ5O zJBeKo*^etP@1nbT$6!bKhH`Iz70(gsho(R85$u+!<~;nBT4GF)3iN+BiDyM=nzb;o zr))>+5PgM;ct|@}JdN2|Fj~50G3cjLqmv+{0n}I717`U)aJ8Wg$Vjbm_r0v=VfsSO z-w-6JmiCszhB=;NOoZ`_L=xeEg_7OAj)W~nxWXfcSPI_f&YQA82=3zZO*jeUcy0@nx7RDc@va}gH zoI3(Z{M4KU=qxs|ItHI&8ldB)rchf7fq^tKj+t>5n`HmrFEd-2OhDzsH^T9mg`ajZ(D0Lm4-{wdOv{gVuymq zfOEQvzAw7?%)i-l`NK^0v)EeS zD*l4;F+k)-nBMYjq~n6GpgfQTljuEN9e;aaC)z;j=q_j2Nfd}RksEj~;E#SdyjR$V zCrBT({pv*b5_fg!1AIh0YD`210|`nK%S_(P?UlX*ZG5e)e(fFE6kZGTgpY~S5fnYC zuHdZNX?86+>Y2N`7` z{LRyke8bw}-h(4FPWKtScFZb%5EO$*&obe-&*AeY7s7gOZ<(XR3^?6 zFZSdi>R+IO>ef}~zI)n&gSnO5d?~CjQJkYs@y1Jg!7svd;G}=6=0IqZw8c8qQiuI% zrKprguD~hI-2vPHMRmP#t`u~HvtZZ59h3v%A+%M^@xcuSN=%eKR_%u)_oDw@!t(C zt&8#DsE(?n^^ICg@2pqp9b&F|91Qn5sluw4{n)M9ZAC)U^&hI zLXE&Dx8E!I78`x$lUhEz5j;CT${w#Z0uEySrZ>se#1vfwUTVGVZGae&b)cpBX10Un zk9m$U3@+3(8QFJT9PhpzzY6xNvuI4qr)8kmwY@Kkf{B&jf~1Y2*Br^T7N?<3u$x#Q z_G3?@7nNFhllYg|7jBHVgDKm)$ba5A6z4Vi7bu2!&3Oly9_Z&rOk@5E60$4C_sk9JjDfbt?xgRYV`l^70R@|!Yy<|Y9`h*t&sU?15(E& zKPP7Q;T%?jQv~cLDxMOai|yIN|{04 zNiB7f^!L$I&7#bsf2{IJ3o!w7IGN;F}glQ>&&nPwAFc9TQSB?KR1!r4LefxJ)SxU48AB zQwO>pq|Q&>>D-ty-FX;0<~(PU$NXPjvR=7zI-`YL5XO3 zyu%QHnqj@cIPf*rM0f~ZCN^pujtIQEU-Qu=s{AJSgZZ*Hwf;*@=N6c+lS6fH%~8H1 zTJn7@_S97gK4{#8MHk$6ErfUSMV<@3RcS70i4_-eRI`0D_`+3^+z!Yk%LUFh*~5Fq z?rayzEdO$+k=^k4f&%Saxq`XxJ8yl@M#aS0^Vm=3H_j_~1#2BoYxD)Q)z+Ttgr#9B z$pi|u?!ob3TXQ&4Bg>dR$&$?ejiphTD_!$b-U7q0QRudu@#yZN7wBOWW|*Q}VxPGt z?r3aDaH1x(qFGLM*_~)y+IBG{_Z7$CO36EOomZc1{Y_S@F84)1tmu+)n5`tOp%Sw@;emh!EQg&HAf zilF0bNbltN#9c9+`^dstkJW49h{+1C12@_a@)wfpOLj4+sf4e|_H0dTsedW)3O`N6 zLqFNKv9b1_NH??__X9erIh=;0GJn5Ta$}IJBdNyN7HiY z9X(jNNRKpUGcM;}UqGWkM5m^9QTW0OF^|AJ~vPRPND`da(P zuNwxS;ky`ay53xI*AwMk+P{7`IbX&`2@FWu?MOMNjV z9o16yQ_gR^rRiDfeCn%oi>+xK$cxN84zdk|Zsh9F*2rG|s)6LSJe`!MGRy?*ZQO0= ziI`jNw#at$6VIpAGT6{KS6V1jh!PukE|SlPAp8kBtJcIf7Y{bC;Hu_1lxw-OEdxxo z#Si>>w7@*0h z*V03_WWV@#6I;Q1`s#*Kbr4$K5-C4(tOI{ANWltU)8v2TdLrN}P}4{y&(NZ~@-?@b z_WgRnFn%a?IC%ytzpGGBx8qidMm|fe|(+u4el}2!L+lzH3iy) zi{LWoE;^rVht#n&rK25Iu{RwpzI9IqP7Cj$Fw@_Om$c1lCvmX))Y8J#oj#=-i}enq z+nl+$v_fFxNBT}m6R>B@h&a&sBl!Ys<-SQNiyXzTz%GLwfPTO^;xqU} zNr&~pV!Ixl2yRo}mFel4j*w}ZYQ#LDQDF00x%*qFE>} zGZFnRzP9X4ubKRY*oRb+>o|Y&wWP<`0b&tW4y{G>_l8qh;8Ai&{4uEsxYu0Muq>Wsvjv{A}5y;TM0bPRC z4?5AF>}}GFmUG4hp35HJ%wRcKO*W|e=}M3v2f*c)jm{!Vl!PaKM1uXiwR@WzGOWe13$-ytKXQW)P6bD zXFs@#`3F7X60NH6A~_*HkGrkz_w5j`Vi7jM`cGOqoaloj5#L6K&>t2VE5j=U8w(^b=TD zxK&-GuCi#%cD|iGkJ*)U&fOVkqOqWRleLhifhF(@TVM9A{Rex`-q$pq?2TISjk-c+ z8iZR6fpU#L+D3tsg{I=Fq6ZdPJ0H$bn(_k}#Tjm1pj{Pg&O}Pttw2{k6Wpo3bv}VMTfVmJz%oRZT9hAamR<9e z5l$Dnk|2^lyUI-YWH;}A=3`+s$;VGaYlfWq_8PmOs&%okzQSV7$>^eMMr-QK?4Brs zUNm&EHprbq*^>DDH#1vpw_AI_KKDiz!GqZf0<%cpHr3x6oI~ z#+rw%kJMW-@&8g3dD;H}&TuxtBE)}X%))EUNfYev!GFMitRpx@7{zz8!-fPN&5IJAvx79iOOpqjX!ii>+ARAgdd1GNO50o3)7 z2rAq}zVjelUi)SmtqyZJcA!;FKyn)31vf#Pn?B>W(S`72?jW@Q9Rrq&u!j6-aHg%FoMjtt`4G&o zZpMc}EA{tTQW?w`;TkR=xr2~sd!t;lDe`ywU}{!UGc=1WO|Fd9C>SY4V@;F8Jqh3= z@S!?VwIK~d6>L|uME4er{&|pXYe#T@%2<2`{-}5gTa{^xYCDhM2UH`%TTh{9U4?R= zKy~})yvxB8u60;GdS6;4O$mL@Y$0`!6br+-Y&tq!ey=>D1;OZiwMugXqf-?Vm!b}^ zr3W42g`CQ*$eqP&hiKgwNplMLrjNkZ{bZsD=`x9OXwWS)w$>{O*K8K zOodu|f@TXLpz)eawhNdbRv}wJ-&mddpVWTX-_BaeSCm0)g{D7jA8|+u1Q@hy-X=$E zaH?e>^~16SdWp6t`!eG#>up~RwGq(VJ!hf644ui85J$l0mQH-6qX+!2BgauavQu1y zzY1Cb^%BwgpLDT0jQnQY;arbR0aB@dOh?Q~s?&+)pYkmgyK0=EuEguC;ZQ#GDESrN zhCUB|4Ll}#Ku+?qvpX(eI>m@J=f3F?^||KuUgxhX9p{I7=3`gUMpS=g1Yd#LBM&hx z(xJ&w`7Nc};zY3_(c7F7n8glJX8V77bHqlSl3vy^>=x0m!EwOQKDbL1`jHRv(>`9u^5++^~&wcHhX z0yN5ffm-T)fd6s2g-%-9A%{rZ4xY2hYE0B)rXjN8s&$K+?kz~U0@L$MAkzY`G4 zmn>kd*LV5QW*r2fu)<4w$fAC_0d979`Cpp%iy z+$a4pTvum1FxhEIxnh;@CcMHmQI@K0G#6Ye;K- zxGho`iFgzch2&9iKGj@P&$GeWwl8+0I0$?I41+b`2>t}j@q9JuYz^rNspsMM)+Xpe z(=I428^n%C)yZ704Yqt!nrT{{^>}gp=d#hw+*4Fw!rt%0SfG&^%Wk;=QAM z$!O6P+9Pb`ejcnUIV~3Pvo@8M@UPssxJ*n!LfCeqo6ud@YJ4Vi5|2U#TVFAZeaden z7t?WEO{>k=gq==yA+y20a!cFY!amXkVTgV!3fqb`QffE8x4{@(%eG=Rsw1tv*Z^_D z(ipTlmlCxtsUf@SqZUUZ=s37Dx>{C!jmU7=fK)_X_H^HS?Vk35a}fgThP)&GgZCis zQy*9mL);9I`dC?L{v`6k0iLmub0eaPkJ}yVh3J1AaVmY!IHihq_HE*%% zM!LVnZ|cC8{Ld`PuE+Jt9(t*(cEM(jm5gjlKd`jfI! zp8_JTmv0Ffr5&MWu7B8xc0KsV^#@zaT?X4EcT+z4dIlo{VWOOto-nHj7w3pg<$df9 zaEF(-@AlRMI_Z*(qpY{kkx&(8PVQ`~7v9g_kgdgxVP3ar#2QZPEMdPp8lfR-v+p}Og?cBP zGMBTA0u<)H3kQ>IUwDD62xM|EnFZurrMp#IQ4|`)HK1GQTX@J-0o{l~fvJ3Rv9c70 zb@#`)GJ%y$SEUWMmRM&zX#&(t^LDD8^CJHS>s~NbSkBHg1>CC>JMsCZWwuP;U1++a zzIMK@1ZsN;qE_G-o<}aXJp{U|kM*KuBa^`WaEW{l+|T1PEDxY4fq&4p_DyY^TS`et|T7qN`dNcqC-*|!OFk6M&d624XxF0A$*4`m3opEqK+*> z`Tc)?TTauFMvz|0Z`X_CZKF!|Du;!EfupG%i$;^*kuY_HeA!>Kcyw^7>wO@Vfez3(KZHcSTd#f7wJ$cnHnX26TwhKS%I^>^J$6My(u-^ zRiUJoizc|>?-UO&0ZWvss$TlHbYgH8emqzy&@Fi|a8DkBji)v!i^a#G&&6XoDA-n+ zK=#EWOLh_4&qvj*QWdCyY zID12_i?s+jrQJ$~G%av;Z+0+4JSoM5`iO_5k;S`})p3BlMEs@7>L-=Jl5LcfEwz#g z#it>RnWSvc3ZI%YCXCVe{5|za#9KLNxWsd=yPkAxklIB#7P!KW+Own#3>-$T*eve1 zu$}0kyy9*PbZJv(-M|m5LvWdWk)etrE8`3=Z0)giO>e8M0)BP9ElTt;m;r|T#hb-B z%D|#9l;ygjkn3h(2l1~p8}014X-G@G?pTU`3M}9^OQ*048F8X;92wbUJ@o>s=YoObwKQ3xiH|vyx2>)ZBnmK_ef-zh%xD z=Z886Lwto|G4NDg5(*EMb1lVi@EPX}gv52Ej2=MkWO`s8dJWGe&mnJlQr&mR;>aZn z6g}2US>rLP<89sLNOHEax~NZ}OR=5aUL@`AV3=U|69^0SDXgjDav$X~^IiO4Yb)eo z&x9SSA+Ru~jco)ti)9>>i;g)z3m2@G&?5D?eV}r$Ob!rqLQx7gGWeful+dB{irS)V zolw^n?%Yepg?dX9ZO21-fuq8QoMgFcs5Lp$nMZz?>k1{pK5R+w68?pX&nhnMN46&> z2c4oDDkt_WI&JGz`a=2&>_TURDBEy+9JkR`uPCyVQy0)5?5m49k@@nHqVYk-e3{LU zrt$AZRsBx+qrH&)X}c4Qz#t-)XjYa3JJ3sZyJ;ua$b2nGs0qqw^ns(lqB^E(tnQtO z3y4VyV~Z%R8tBC9yyu8ySI5wQcz@@((9ysNPA!u|U%+$4kL8}Bmw}fy*3!v2O3Vqh zv;m?0LM1jHzOQ)-GvO4dRNu6OR|xc*?v%JRxFi3!+MDp0+mP3|a>Wm!_c6zaXQ&hP z6MpAQHQMr(SgLfjeZ&1&yyFxy!MibU;t+ zt)LU!3E;E;x6~rnSp0_`snqqp=SkojH8^QCw@+#ZOfpn}%JGZck1bYk1ltt;Wd6oI zW=Fc8P=$O$zLG|^{=iLvr!n1yWjRB|0nh?Nl;&MsomZE6o0mw{PmJJha&P6zQf;P! z-^8`hYPUSbSI~0)A~#TLJD%woFEpV_rS&wYHK>e03zClLjtDFGX>74h1g28A(6L;1 zZUGZTjg?<94(gd9Ug*U5IhU9yz4N~oMLvdZ2J6hTS!+~#u(Ne7cfS&VYlOCH zC*lOUMmge?)Gv;e#RH)IG8`Hs-7LE*94msjH$|=Gl2GSB8}G>C9-(bTtwV$O1?r7Z z)6(jmCfLf-0{Up6ekes;#YPvt13SZam66Z^_7U`#VH!J)F9Ko2lGEPT&zi3>klsXR zTf2x0;dpd2*q9$7BychQmey%lnX!u@Kf%PEqk3v?hYj>Su7kKme-fyvKH>$_C21!| zdZR1_mV7Z4a*I91hjAOZBBXB4D7k}#qQ49mH8*=XloeLt?>J(ljoXEKfKIM!3in5SuBcClymTFoE_g1C`YZaElGYYcB*H|ik#Iy)J>?v33 zBg-jpVt*_OU!`m>8ioC>`@%m`pK`azL&5JwgPmRZpG9YEZSDV&Es`VfhXj6Xv9n5E z8TXLbWA~}k$YOf|`M~x;Yad~QpU5CGUp^n$f?cuw6nAPBt9;Tvs^omxF{!-RnR!(*HPD~f$P7Sg`2UoRH^|)UP%qamse4&PtbOSDp~^-| z1Z|BRKIy65r8UadE$**YRo~OmdE)}*)QH^jd^x_0l!Q^@C$LPZm{=h{!Thf|Q!1nU z%2D(NoMfJ;UEQsCv+%0kZPbRASGPdTCD}4oNS4+b^O^aE!SYVxy|R<5Ctvq;GxgA- zqOXAyLeq3ie(pnrs%kRStSDA(Cy%k5VlR4H^OMBxY7|(@qEXzWBkBV+MV%tPWs?F& zq>JiIs--E4KP%2ttErt-3LL<969LgqeofrPJ`L>Nlxw}9mKTh|6L&3N5kH^5ZFGsd z*hp#6zTtVLhNn^8>2KUm|6-}Fzq!Q-CJO&hxthu|T*$OkcK@V$QyeoB{6Oue+ltqj zdJqP4U{p*{-xOXYycGVWVto_1y1901eYpwP3x3Va6ff+bpr2u$l-pGrL1%IWQYUa@ z!8=Lfw1#-PJUqzV#@iIw&qd@np?ZT;3>o}AeMX_#cP@JuyFO~Guz{xN+E$RYJyjkOqL+fT;6dn3Uga8d`M&U^MxI09P9IF2!1tJ= z%%ePvmT%ZbS*^pp_r-Vmp-O^~sNZlfB5Sw#8icUrz2^a3h~lfz&-qz-m4rH|-uwtU zdY}jIl9zLJEf&5OJx`2Mdt|PsYXPUk=eyJCo`y2Ef~kpoP1=@m+A7E)XuXg}=ji(7 zyI~i2P7H99j5Fmw{1B)ivO(L1B>Go~cc{UBZ6^aa=6V)s4qf;Wwt*&7!-*%R3|kAe zk`SHx^hkg_kEgKrlRuIF+GNuvdqc|x?iO)AImXr^ptJWBw65{M*uvVd&p#~6c&UG&kFi_`w7$}er zl#xm|>;owouSjdq+r}dEU{y9wBkw3^FdTcORxCPzA2N;z%_{+&hZ3g?Bc*ksEkuLY zsE*t{br<{F z-beRA{ZukiU2AI@xFlwgF4u7KIe9SjiVTIGp_$NklrB{dh4F_Re-|Zj<Z6Zg0wC~)#-u@r;u*e|aE_)vsfWwaC zMazR7iEhL{u96cI1F-~ee^ulRgqHzwZ*{9}B7GTqqjV^lTfER-$5zX_Bh;>RxlosU zEKYFNCy%Q?f`1EjQ*uHhgjMRA(1>EQ)~q$Lco*@T`@;To5`nt--Qu^lWdfdZv$$?q z6wuuAT)8H0HD4({(5kUBU2a`!C@~y~vxf%`h9GG|=n7P%-x4UnPZzH*8mc}eyTgl% z#*j~mzg;`AtHw^i9@_UgFY6j?_7|T=%^Y}?9c5p zidqN#jEo=Q?$}oc&yr!bt3t4-x&7qPW61zJ8Y@=wsD{a2{GqtjxnI?hhCmTM0Q-eE z!D|pvp&|B+a=_`uQ5NS8s}o_Fo~U$Ze^u!ZFKiZj{hh-YnMF-vaN-e{-&^Nm{4Iee10J zJ2|!Jt@7*aZOYHOo5VzKMftU2!IsN{|BN(R=_#HCt_M~igQU5(8d?hEo#P7dOW_@h z*~Y(GY5WRxNjS5b- zI^_^M#@nCh7%0#06x{~m;22>yRm-@`nv&32(n{daW5RwmoB70D#g=*t3HeAYT2>;J;n+CQjalw0orR<8L_)ufA zeaLJ(6Kp9z4t7wALMww`0}uRIT19AIvB}lL_rVzjZc^i+Jx8k9=oW~0S@uiSox7b5 zZLVEn_qmGU2g;celllhc$zJ5FGF$3GRfD@KJIQcYbLVAOg4_?E7i{HRN%RlAJW>a5 z>=|J1VC`9SFJ(vys#Fob+4L#<*gcL_Ef%&AoWpGhQ59T$^oLa9n50YyDNw(|v*fGB z&2$8L?f?5Nny$4W?~CRybW*X>ufnk&pTCY;POOkeuo3JsUk3_scLJ8dePRm(y{&VQ zGG>^eWzrzO6Fh3}2+Tn{s`lc#<_P~t^HS;@nvrB!*ORN`ZnVCRI{~h}SK_V#OD$9L z@1Z+>4!-psfZ3<1&@ zU11OC0hF9`NN-HM#=iIZkO67{9jGxh1NaDYHEEF9oaN*06-b47O^=HwxO#zQ%zA@` zdsJE@IGXq+e$^-vHH1mvZR`@pnuFHQ?z8wAb`3C`Z_8PLDO-jBn~CD!;v9eC3o zM79JvsCzA~qzB$zS&{ex&1c(NT~8%ynaav#?SdD%w#-A{aBd=X6WWF`(0uD3av2{* zjPyUz-xan0hIyc8uH}X!UiBTRfILaag`0|*)Gs>0`x$u-jr4Tmdl6HO+2RxYDcZ$Q z&cGPG;`gv!JUd9gADi#cWK#2^A)eRpke45e8xM} zI}WK!4o&d-7UWulf9bBc$2$UA;hO-wLEk|4RbkHzD{t=W4U3W475;xIn|y+6p{r+@ zFMr_r?3`tsKn>%^ntK8pz=!?~`VH7p<{6w!RV~oxSrP|Z^}AQ=X|Z0`+Tv-JU6u|& zFYg_GwwvC688~mAou5hPp~lz*aSkv9tj=xZzE~}EIeiwAz--E63~AJ;f=Puy){5ds z)X_MAt>}A|z5)5-y&p_b+G-wI}g%^xg=R9h7&k zPC4(+jF*K)o>tih^&1WA_kB-T8F>cF&`l_)YXWi)c)rj!Uu5>Y7{rra`AGa8k0&QQ zFGC2+8$>-(UK}{>nUb&rtnMxLlD*TjEX63Uluj8pX_x z)i{mjrS5hGv-0BOI`Yr5Y9)=`6CFF=vwbgzsGAge=Ogj#i(F{{YYO_ly58IFNMn4TI$ehShwwp;9@>^Z3hpTyv&qVAdg&ca5!VIOnMLXpzOuYTz1Ou6SEm#wO@W)n9kfWP zh&`#Iu(UBI1-tvk;bzNR%NoBiuwOiGF=GBhiDfM?iW~%~yI-r79A!_iZ9?BA*_EQ? z#_|KhIl5tRgq0IvvQiS7e&l#$a4mnsUMguz0r`>K?tiwb>@cNPavLChPhui3Rw5S}E@)Rrn7f(aEt^jTMm(l-wn633bKW6+LaEe1m0} zoSXttY04xqKNur|DW6p6wOyP+oFO0ChLu0Un_^C)q3AU2l&U&v^WTI@zABz6M7z>c zp?$&M(QcS2uo>?{+_6l;+%4WaaDRPku~3USqjVL1!ygL`%)jkx7+!t2N8mS=v0aJN zC@;7^&DvQ}JS<$5d9qM@A-sz}i_oEO_(XP(G#fp{4`5%@Ufd=$^sUxEHW?0R`Q>uw z#Em#smiPE!zX$+ zieI4V<{otp)2HNZWiEHTuU|r<_@4gaNp1*%E^I&B%ljLBgni&{tV9JX=YE2h1@CI_ z_>9KELKp6Ls)x2z61b>5d}$=S;^_m6!@kTobGhzt_|nI+}XV z56B;E<-WekXnh=h8rmnEfmZ|yRoeauVXLtxw~v>hO43C9W9pZrS`|fdGB_I_%4~AT zTmv}E_Y*X$H1i(z^-;aTl`S>IBDyO4T^|rT3SWt)Fy|K0Sb7}y1Wv%F^+6v+-+E3DJr=%fEsy5<5M$Wf!m&@%k3AE8~9n>+X1Bjq=a(orJDIk*Rk~y-K&J zUi4?dbp8(6!BIV&X*{nsMC+3jfu}(wm%*KWedXFXf<%2?JIYI@)9`xlViO`bL?KY=>B(g4fj?_m^ zq240<@e4vr8z+yI77?Z7t1M9^JAKmo6^VeEPt)_m?H}dk)N$T^{iVZ zBYs=bXmJqL&#_ebNVXR5TTbn3pZWIqZl$_1)3(OYqI8LR zvfZq9BQANCC`}zae7wfc;D8koZ*V&n62g1>|wG2h7v=uqEkxu2zf91_HWY2uvVRSTjFmTRDp z^v<-Eok|vXjcW7e37T1Aphp@%<0CCc5FUOjj7`3QvF_^pu7j$l-jz-DP`gogJvt#} z*COT@S4JKdYFlM~DBv!ufQw4)NiD&TWMf(e$AOyUGHpDw14)o;WB&y#w4@p3`-a7d zz1Fn`p2rV{yE7?P1A78|%rtRb1ACch?J&)1zJ=0xUnbce%%rlpNV=C`F=?QdSOWW= zxvA3UYPkn070Q~Jrf@gqH+`CKGZgFF>eUyUjpcyWvxklMHZS|8+pF^Hs^a4fIgl$N z3O`MG6C$@Pb7T~Ki0^Ow%PF`&Csub=fcL=Rx?tYl%r&$}{yF!?V1RiOoIsYl1L&E8 z2KZKuw`?hhlG}($z!KxElp<~yDB531U4`f9QzT!wB;N&h3$3tuSZ&wA!sFn6Aj3C^ z|7l80t*#&K_>1`tYr$f}IASE-MYozg%EFMYC=u!5E%zN`0kM_~4eY0)#l_SAE%`aFh8y9hJ62G8&=zeiWH5>n@|Dn*0R#O0U zWmj8do3EYkzA-|>699WyTo~|ZJ4rALlS`~qf*kzB{=%`xyo=CePFGsU=Tao>xk^;+ zL;R8|%SXY5rNfAISbyO?XXew?4NNoN9b`OZO{2;==ouG}GB{;>D8A-`{8nHj(uP}1 z?##;$6GhX+R#2>*;yNTwRXs;TNhjOL-i6snH3PTU+GU;K+gV#c@z#lI7W9gHdFo_r zg2?a%SjpJ$xO{G=L6to}qNi`@gD6A3?`G3P@;t!p#0nZ88WF?W@ z166VS>G;f!vuHDC$eH%b$=#6#>TIkQ{#c5^Cs7RoRp8;6pFhH*y7kOuV@o~`ZeCHC z^w0h;m`CakpSDbKl?b22k+KE)s$U_FB;&NV!<(f)rFv16nsja^u3@tHsg@xuD1+@TZMHPYjR^EBj!S(7{SxdvnVLnGcvx{C=dv9XM{{X{;BL-*hFZXH z{1x1FSFN(8?3I#i(wqAb8KLIn@Py;`srnd8y`luE85WWDie02%VEGDWi8FWKD7q$Y zByX|V$nVCXNHr#jZ-_3|EYY=P-RuLtu&k4>8QhHp;OW?L<*xS-H69pi_?zm;x$w(H z`$b&QimRn0AuRq9b9R5?jnG-DrsbKos`n&gGM}*3iVG3xKpf{X*5hOyW@%6HbXa<{z8}wo%>NM5ksI_5%gP1-b4_#CGaNo(8c%mG&f_Kq`-Nj1 z>x=W;UrpVkbA47rAKg&HDf&B8H!qES>wXU3lBd#>yz$B$Z(Gg>Oyt@Znv3r1KkJ|5 z_U4$Pxpd5qQcs@TO804ZI@B+I8a`K(xve_=4vJ-36wfuSrl-p9-D_e_=nY_25x2Wz z!YJyX_CNPsWHdKU^=Hm-2GKkDM~t;qzmP{)s9I2#7EPfikm&_9m~QoQ9?qekYF^5T zTt?AW@GRKgx6u38Gs^VYe8IiYdp!Dzl?wMuFERWAyXN0WNC!_Rq`0%$9dx6Fe7)0& zc`DF+wHaM%z9O5rY?Z-3D*rdEg{uI4*&JS{<^}6P zuZ7YsBl&N>e-jqkm-7u|O9XkVddK+}Ror^q26Qda9CYD)6ys z+f65$mu`Nn=D6ZLZ9H$e7VJV@36%^4y)6`*-nNXyXLEkJTj4_cTArd-se6}xx^nlzygmZ1-@`9*S_`{?W1Jo3v&0?Y zad9&HE?goz#k1%x=ND+FtCjHv_nV=@T+6zRc*~_3vuwZdmypL+3-rl1rtvmS4Beed z%KN1BgcgVd^OPMWl6G?M3{PFpY;v(K$xCniQxtY=&_y$&34PI_ zz+tJqX)~Q4?253=F|S?N4yU>1V5u4an8nUf`6}CVv+=LkMJS){qxMXk_5|-yccP)0 zbrybUYd*7&C&6#ruf)i_#?X-6hvms+v_Lo5$UIi=N*^T=KHy}a;jXPC7<8DEV0s1D*&J;o2pN9i>yDBWyNQV~u)qAv(vQk!}> zk3PRUwprMrsYD6B!ZE<|_dVCLQbnYOgv$Hj~RNL3J7Vf9}*Ep7K zpj$w^k2{BrMy~`{xU$Vd4JORP4c&AlA1tYgg1YeS#F=KCA8B;>dI_VU z)AW4EptFq_-zuf?D6#Bf%EF3eA(!J|+Wue@xNkTwb4VZ|igoue%P5X%pX;P^!?WR%`lJSl~dvaz*sfr26^PCIECtW-^ydo-- z&e+wmeuydWypPUWpNgeUOpTPcTI%dew;i{2ba1JY3;zFm`JcSmG4`EtpILA>@*ZOL z8A-!s@B~|1J5me4U)W!|O>zgk8kh%w#46wxm||!EbaM7KgB z$%{g3!8dt}p&sT~D1-4s3N!#(3ze$v?eAPo)s8eewiBG;^&@Io=I_Mov*#Ci%%P;dji^v23S`WmWvs~@)oZW4{F2O%ch!n zy{S`z!PvjBIyH~~)3qEZa5rO?0gsV(1>r6`XcpCk@68xZb+#ubHUw#I zuPXt5;{Av*?)Bz+Vng*_SBf1rX_~(gLVN|+i3{`h3N~}|4D-3#x+cX3ns?UDitDd0 zZxIvYaqZ9fs(TXGTXST~P=gqM!c)W5-tZ%#O>u^}Q# zqc^ch`n{e{@zwH|>+cs0NAo#rvs0cPm;fDxBk|$XU(k2@9Xc$2gE1euNx^JgZjZAI z#kdYraq{)t@m;>a@NbNBq>+6i6Zk@yt&6Lruq!FdGPmvGh9(|0)jrDhlBG<5~3IpA;3`lIQ$?&z z))NQ1B&m(h!qmhZ_(+N%7BJ!5L*Nqdu&!;wR-NA0*7U=jry2%wnYw(m*eIbUR$V1l z-UPoJxx&+2Kh1FFExU!)=Tr0><~DN9v_$--wZN^=+J>#}1?rWKI#bH~mjQf!FRJ^sw0u?GA1NlInJeV@z*Uv(0E^gV-0E zYVIqo<(FeF3M-h*-Bh>cmxO&_qK{#s;7wdCuJa#-TKP_jUv!u{*4tvLyKCh}6YF$eT?Dum`X{am^E%dA zuU~w<7S{Z;xifeCQ-5JzRt&eZYvH75rA4>qCwBM?swGy$Ecoq2i%NCh=N(+S0sfo! z)8BNR*iL+#nBG7)r&T-)2-G9ji|9>Ww`UTV?WxR7*}ay%X9##t?dWG*0cy}1^wXkC z1}EJc>t`H`MhpL;=e%D8LbC)L>HNZ-1p6m`H&5{Xoj=Od7>07V;_2cW4)D%*?WP9G zdXtmO5&Bb6MYE$1fe?I%IR!1Hs|%W9mdX&{xM}xm;e;n&G3hUIO@Kh&C4MS)25gAc zftKq2Ve9gp-?KmAQBh^;6_*B#P@SoetCqQnZW~m$Nib)H z>7rq}X?|f9U6%P}+_J<6MX}s0aIO^c_4m0s%Jtn?u1`1CH?^cA43}w3Q4<{jYdq(S zS;7GK7ISTP9{vU`Mpm(rzHRa(e*vFwUS(|#ZI@pICAx8HH>#HS(YINC-(U#0OZ=0L zz^hY{i4C}gFhwnM=a{lYE3=sQn9cr$a>Oo%x0weVO+)o%mzkwR5eik!@9NeYf#NLm&0uMILM2>(4 z8sq;1dazZ+mz$~!)4=nEcK`t{;bK6I`#k$npVlNo6XPm$&nO&Gc+u5{9jL1hPfVC; zyw1#`HLjO=OMqh2@5pRZ7WCXyN$w*2<~>WFuvVfHgVUXVX?s}1aGw7@*~FHqddVKc z3-Qa=2ksTpUCTV;X{?KWs9u5NM6YogP*P~w{UUy&t8PvgAjNY@J0`Jy;t98h-I>tA zz-+BTXHZLv7DLte_u4)jk2^KzkaF?)o*Ic^{d!YZp_2H_WE94`wES#1yLd`o5SR^a z$?eD7bk$YwaPx9XT=C#2k5&A}4L3akJLN;_j<1@o3~HjYGh?H|(J?#i6rFgcSkSMl zm#+UW;Q*A$_A}l~Z13tu>-414=RU9Lmw4CPhZ|zL!ajBXa%((?`IGEfXc@Ex3l!b+ zNkys5D9^jywAyH$NcEy6V_R-iTqJuqc4_e=ptq^FSejEG>{j@Ccc~%WSy?n(1tu_=g?T`Cp$!{&vko3nA}(Gi}RTrn$)N7M#|0drt(sy;Sa znk|g+Iv_U5D410pv55z#-5&N~Ne3*3EAO zWl{#Ns{68>0F4Jyn=RFTke6_OnJ?ZD z^F1`TsM^54%1|Y^C(*f`|0pdq0r@EZ9Ja{0{2d;M&;f@bzwssDGER@VM9)JGLkJS$ zc2gm6k#8h?9Bc-?LRt5%m_|ImX|2J8yiE9N-s0&8Cm=Vyx@Lp9QV0U8n$B?h30wqc;_71Wz>~mXVYZIqeg~$(-SA@WAtFM3jjJ$$ub{e%|7k}e zW@#|YYp3eF>YCHz3mOAr{P8`HcI?j^r0 zOX_P46>nnKxrx46P_i)0T&kZBK0$F}F5L)P>d_cf4+`^_yw`LMi^HOQ1KB&?RfS)4 z(fT90tJ^SUN@{f>fry0Ow(1L%4bbHu&gHF@{Hh7dKC z>RnuY*?I7;t`hSHG?>2biiWo*&Tuu-R?}W^uF0=y)*=Jq4q)SvC*l^NJv>*~YXVFe zI8D9CI}bXfqw;9$amXxg!X}G=bc0@K6rij4A5dke80|>SVOaNG=N|5@%42-a{-j#* zZssbLj;t}CF)_kZ&15wf_Xn6k?M@hi(%dR>9WaDjuiPyjiZ-`wjXDL2d;72xUFqhP z*n(mZiq8K6J=A?Es7bAg1vDj8glnQfA74m0<7c_b3*rj56%TEGSAX3-x$w32o@;f& zs-oZX6HSu~AF5~m3f}-OF?yP3KQqLI0N(flO^$N`Y<9hI--qu2R=1rWANz}%Xi68? zy#4m*QQ(CsPu9Q8K$thp^z!Z>pJ&>PJfeg>2ieIc%K{K}nw)~A|lb)MqW zasQcSZq6-Qn^UDA-q0^@PC@Q9=$cu$7B(vqR+&UU80`znd^_PhtSq4x#VzF-!CKS7uP-m8fy63fU#cJ7M|FQbk$ln* z&jk6kc$}NDXQ3H1Z`D|_6-W*5@w^+%rGmQDc ziTT$*4w+8YWXBQ1iua*&y;o(~#powvSK0Z*3)4z&DR3Vcn>z`4sei{+FB06h;6XwS zbKdS|(iUS??_F>baK`o3Rb4n)e3zXDo#dv%(}kv3J<=&=#H3)$h4(HS^ujwIds^6@ z*h6^>brRyMhkkY*%w5E7MI&X<-^p;_EW^9_H|7fScJpcTCN-zMkRqvb=+cmd(T_n|8qd?o zT%K{OE5=>u@fe@^I+`_<%}g_ORI2Y4_wP$#^HpP}iP+gj{KI5+vCV_j673W7F6Lp2 z(gGhb0B#_e4A-&S&;V+&@3DD;+!MvnuLe8xPCN_t!7lR@8>8eGJ~EqtNysUo2R0yZ zO0T+ieN+8;Vht$>48raBSa}e!3X3I9395;WJ8Nnn+jv$>d7j3bq3h=0*U#7<&2w}iSEhHB@w+SD?J3adpE2p$81^^yn>`LqfC`~O>bv}#>fK^2wVvv+ zXI8=h=L7Z_!?C@=SEeK8Xq58oFg5b@q)J<${4ppJo@vP98*Y6U+t4tx=%_2X@e6h( zpw5BW?NFj|jOR3EMKN>u{ zL_}krK!UdW>`eIs(@E)T@dvtuI!UT4g+~KcE-x@jCABs6z6v}hr+aM#iVhF*!E~ia z`~(=RKa1Q&4&p_~8mO4Qj5sY*Elz8; z^A_9P)700U6s(hd>LF`Epx)k#fysm-_^pBb{sof!E5}U^KkghQs3uj|=fO z#j#C!!YxA!9niRVGMRruy!6;z96;}+HUE?&lG24*WQ z!DG@w?mM;@h``Oh`=Ts0wZ7-d;HyY2ycg2XI0ATw*TRkk8p%QKUgBow5md+1)^^kk z{*q;{+z=Y=S?Tr2FYp}x0W;cLRs{R)0wV7;yuo;WtRDyLc zaf)Gqe-F}=TV#Pvb@(^bLuC}|(6`6d`x^U9@~^l$M5*~q?lzos>@-u}?&wR)YN$EV z)V&c#3cKRz+z4z}(>1=;{-3_gVuNZ*JIQ|H?i6IMeqf>SY`GL@g>8aejC%Gd?jheq zqnXaeT^6Kh2RS9K6S2;x5gUpdguPrM*V6r6e1V5if)U8Y%qdx=yC^TwmiRB`j)9{- zVIRen5RCTYEBpfxId)WU67D_yxHk z;7SKWvgx^~x=Euy<2{)6`m?^4>~Z*D;0;`0U4+fHboQSCqJ%}#)kN9A=RK94e;D;JK;X)*7#%4E?uOU zY8{K6R9#x}1k!w=dy(Ot{M2`z|C-PNosO2unNSm$LFZxJ(2H=kvXSYC*RUD92Q6(4 zLoCx!D0~&FZFzt_2Ww%I@IL%SES~cE7x7ht$z%a?TWLvO3SJ93)KBGtSm=A~gM%;W zHui%)1H)ME1RsL|TL4JJeg1)k6_(4ED9`VBw)L9n3uo~y6xl$IdQz`Iblw+o2#Uj~ zz!-v)Bd}Q}*6JWCLs?2c`4h3jvcVUNZ-owE>EuC3!Mow_T!rK!B1s$uB+Gk@Z>W3h zX@?g<4M&OJ$Wi=8$%-y!^MxZ!CHyuWPin<~$qlwXffaH+rWwe92S8)MxJ=ZNw2qedkg&wMTnD#_ejY$e0Tn;QoW?=Wsr_9aND*-I;f%*j#0!#g= zn2Fs)uTI!%UXL9CWyl;HjmD7s)V5N6D`zgW<@pfn3Ev=m1Mx%D(0e_xtDo zbA{XK-E2=k?yt!n6Z+A&lm}!4ei&KrUIP~7y47s+JMyg0ACny%MLtqfhAZ(-iV>!R z3=ri#O&&C@5Eq#kteNARxe>50uAk6Bx@*^xee6GM?a7{j&&~>!=`q_n6rb%}j85Hg zORVN?D(%UCzz?(>1gH7m#bMYBIt#jp-S^bcu9tXxDpI*HO+0LN1hTQA@+Ur#fUx7( ze0+rUday$7YD#2nDS_P2ZjtB~1ZBU2ld^mmXLf!2gNKpTO@Kt_%CBSv9eSEeG1Q_FTj`l^@+K@6yQ&5teS^g#vhhj z5N~ZUj%4$1Ld)o1xUuAk|6sU`{DRdGPr*F{)wK(yjkc%A08Gzs_g({U4V3N3u znWRzZarh&8AKEM~Ha^En71p!>n8FtnwZaaWXDM0Mr?%llJi0^9Pz_|ka7Sqp)>~;q ze-3saGJJ5nG z>q6xy@c{LQ=8@Hit(FMYGyw)`TN^oF1;#R`{0YwA5w|!)K&Y8euiUC+n&PvSDYpm? zS|OUaL-ILjfyb*%v^Z%UT1FlURrUShx=75kNCDB>i2T8&gLRcX=ojRxf}um~oaMzg{>`LcVkAe{{Q0H_1&o0`@=|8=*v*+l6@Mowc9Rj?a_+4RnRaU50G5nvF?FJV7nRcFBekM_@mG3oH^(#aVfa z>Tyjnmh#7`(m-cU3l0d~rfVXNr1`eie$shGcS?SSJPoea)Be+}2lpx(|L+_UNVfiI z>mF!VdP(4ef^WRq4;t%L{IhJ`_!xU);1_q+{!Z>EuOwB^lx-SzUHoFnr>21WRGM&B zNqv3-P)oVWb~AsJ%*1}Wz5E~1)~{F(3PZVn4S$dYwi4`?b%DGcP6hUf&55czXCa;O zJHj}1Y8)Au#;lFKn%G{YFSiXq&RGm=eku%B-eD7@&3ylWM0oN0tRIa;;sR-^89*ckQAh^@lUOg3{Hib)q$`zAf z^_^oHD4e$qc~jgYHa2#IrW4zPH)Kg0Oa9{?A+B_{gR-oVmJZNWQ;2AUeaYJkw}d~- zf8&nmXLv7hc_5ap6sS(T%4vrf@Nvj$r7tl~Y7IAl0N+sa8@VGkfT~ZM)E&(W;Wn*t zw)CClhQggOjXYHPs6LNp!0Emas6lM{kL($CS>XbbQQ%@LJV-LhkO-M#h)UXy49ovz z{1@3`_<=^^O}O{?HLxez5_pjV zd+A@e*WyHH8g*nA-_}&i)YDT!C*v0HQ*eQ4AKFzsYT0aSEjh{V(hms?RFa<)Ei53J zBW$u>)USZfD=@T$Tw+^J_Qgg>>vwm88B!%>`DxM)FZ4Akd@4^M{&iQij^ul(yu`7V zx7J&vh%F6n;`=y;V6!k>IqdtJyi8l!xBxr zO?nDW@OSVm3N+k#1&$>TiT_9+VU}$qmy%m_4lF+E8tYO@=mL zu={s(iPVKFOqdJq^JRyuiZ1d9D{jL6P`vpG02dhpXh~3ZM_^ja$JE zM4I{#dMPmts)YWePC+WO*C!@Ks(f||5|s>pH@Sv#gQ!G&1XF}A@L@PLKLMB~yi|Ee!4_+39;(Nfc!~l02>q26LMp@{)X39`mw5O>J$ZU5=&FeqG ze&Us}dGcv z^T8W|f&<<_q|IO+nB)nWTg}~H=Sc1Bqv0qdu4NzQSeBBs_E3w~X@SuhPq@zqx`zep z>4P7@E8&dRyF=TvZ2KT~T(IhaXW9C)y5;GahxZ*w&ufWy>e#Z+u{pMdrNNe0GExqA zG%XFfT2|#hC#^Zqy1eg!Vfw~O;Lrh;A9BNYHv4pzS(94cD1FaO_Ox=;ELM z`@3V25fOg`Z&Ez&Ukk+dqM-)xy&L55WHuYU7qKE6Hc>TO885h zXIU189p6;e(GOcMHXJTXxZ|pq(n_|Mu_Zdk%8DGq5dK|kfES5vsPw?|(zMhaiO$l#neNg) z@MQa7$F`6oV@ohfv1{s-*2~(JaVuC9oL{~%(6HnZ{(MIUH(RuV4EB`%>RU<9BPRx{ zb25np?RY-jtfWBgIL`FlL4DFva5?#jEZ)n5e_OW6RYND!);N?vV{r^7A@>}Z{ccdJ zti!_=MEk|OfPj2MR6E#TNo}lskYmC7oRG>jc zgxn&nuB9pD5Lx+L*$tJm6%S;E92Hek?w3bnpsjV;zvaiIyU3#86XikK0qGyxk<8X) zPJlx8V=a>3mqY|T$-nG5j3GFk_k?cay&TEmtl;1NvE}BHcBy?GFG?4e6qY{Cypg`1 zCxSi6Ir|DMJESMJpGk!!@6zvtD-$#PE0XyXZy+w?l(fp0FV{`br0VzA&RF2sSK%yu zUE&XgO6=n1tTFpHh0(M}=!PI{gZ7gp^D?Z-@ulbG6?kIl_>>BVF7tlUQgKJw1A7y>nsumWL6sVY2 zR{vmDII(2z{)1U;`Gu0p*{@Xsb8INqc2*J*LoyLJLj86Q`yQkYtB6q@Sp&+hlF{N& zUrAYS>-m)F*Z{{$W*T`>Y$=c78`JfbmnJ@QVd%VLtVD+chTlsks_awNrV~2aPZ{Ug z$0b)0dzVD1??tpFr#1=f3ic0tz|G3kKtKFg(f}NWGm+}>#;`>8@IMP_6-aHy@-e7& zl}-^_*%Y$cqw|h%}IRlZ^_AG5Ebe6aE?5lOHWT!d?nF;xaxBSZb@G zvti?1^Y|M?5Q*KJPMaK))fr1pFyB%v%=Zos)$#YnreY_-Gt^OQN8j)G!;%K@72j*y zUhZb7g)T}7QRl;X{#8(tuQwi(lth#!YngQa3P!PQuGk!I6pRQij_)CKavTXRrx&8B4yx zb`wUdnCR$hAs?~(#Tex$_&BMb&BnJ=-JY}T@u3KFr*yOQEzKUDt4kJ`k82`)JGoaf8e7<3w(k|lyPH2es`c$ znjC&$Ztfh7^Hl~@94vIleL3{L}jl$0qx722yY}L2KHTJ+G2C0+KwmWaj~*V zCv8DThzFVE6aRPLyAo=c=FVP{u{J%O9GcuOxh(6C%)^=gW_8P$pV2CFRwk8lDl;-O zKIKSye)vo#m03Ttf#Y&ndS+8`eENlqL*;EUUk5YNf0XLem*{PoyV6orG$+_PIirTY zQ;Ik9cGkO0Yi3*3s1aGdCM`Q_SF63{Waj5AQ^_K$m=@V`dioe&PD)CeCPOGk(VX+ z2aY!IEvdKeqwyP2gp9)~#HFgO)xmiacKI87SErPfY$Fw{QR#e3jK4-j`M&q|-)+aj zDJ9!7&O5%QMml^Y;pF>@KNw0*E34$4iGSga>N3r4)arYKU+`5oEeek`R>o5GTlnn( zjl>ar;i35UkRE$pi0jvqy}Xfd1n~u24p*gPi)*QSM+P6}^LncTm9W3?H9)*{1ItNh zfkZ)VH7l{_uJjaGY3X=QJPE!_Ek<6Xf-H znf)8d80jd*;fsj|N*@_S@@;bhE3q5!CWPYdi|0%q@OQoecmt+XeCaz2xS0~Ty=flW zBlK8)19dYm;u45czIYN8pK?LtOE#)-i3JLE3YHYy2)r>73N6Ceao>D=7Um{Gmi^G> zqMkaFWG(7OJqfi0mtt|v^5_w~2g*b~MR(@I#+!&zbO;$D?g0N&mdXoMhv|2rsbwX- z(6YwrL;=ra-yhJw;_qC(DOKqL_9Hf8w52hIeY=nOr z78d&X5Gfm!<2&=(~xyg<=Q_A8@j$3a6T67A(_MyvK7=uxeczAYBU(@%=3?h z9td`lcec+`#(_i?A5QTl9@lgmnG-J#t_s{zM;PJ?u&b7L#^+ zYkybpAzX;`N|=o`jPFVnc~Y$RZ-3T!nTuJ;A#^X=MqZO0Kbq)?o{pvmfITAzq?D5uUMa&YXhP1 z8dECYu7t%DODoJ<&`hioP@kZLs~((d;+GJkrK!>ej%E%^*?cQwg*?eLkMx6g4OJZT ziZ+Q^;3P=4*7TH+=}KiTnDiBEsy1v>h@p-)enpwCXxIq6E7>wIo|~D^TV&n>Mao_M z^J6wiGq}2N1_0qNNi>iS4hL_t7d^8$r(9FKj*(ct_!;~ud@mYkx(n@R8~dED3H((7 zz*F<inK#30@G5(o>HWL^QO~OTibsfB<n(@e zO|S@YDz70Y=$qb~8rb1$94h!|p6UiR?n7U@33`n?PLQ z8kc;vzS6g}!6g9o*bAk($gkfnOq^BrxK;NicQ?- z_i?Ss~%XC`Kn@k;Q!I~-f>N2 zU%)pi_TEtu0|ZE!Bs1xuhR!PXvi6FKYhA_eDk_#$S6zDnr3F$Z$xIpnLI-g~AMCgKc$ z&o_;Mcj0P0>nscPzuO<;xt2V#vA7fOHRlm!7cHc25=z3-wU*x-KdIlPH)hN<2?f0* zRyvFSi)pyEvvfF)?s>V1_>B$sjuZHsqIgW|A8dWVNd3IPbSz#P;oF!aeKS?(m^gOd`IDlSZ#oPZ8I!t65d}duj+- zNwo^#V=o0y>3-PM+$ceqdT#y-Tw%VhtHF%;mi;XZoy0k0EEZ4A(jG7kC-O0tX@*vc z%`t8wDrvo>86HBX^QO}h$u&G9V2%E2kih)F@DHER-SPFo6AdHCL)`8f4YknJ-#E;2 zMmJYjVQsA$5Eu(rW_ii-rRQ3Sd{3>9$!wiOJ+%^&W5fh)H_<BOWnvou0AGUbl?S* zh_i{Q??`n?cUptlY+;Z9Go?b z;kv;+=3|2mUg@=(4ILu<<8M|F*W!A6K$HpY7Z#S#74em$}v~KDVZ5|?=V(~Sp_j`*1q=}Mg zq6&Swu%omzw^reU^@eNa-B8FxYjd~Y$(D5mLdi2*A0eDYr^4kYWiuTMOBB{&AtL<` zGH=@%$GO56vRC$%;xgmvkR_trBKSs_eo5@(-M6lw&+($LLHX_ATN*7VNqvi4LC+$1 z=Ib0O{NBIF5T~C))fac+w+`7TPPP6{e{X0f6We__7mYm1Bso#=3;x*l3Qsd{X5Xi5 z(h&J=aS*lD|4ph zIL|{cE%$=<8FfW)oEt{%5=}F9EPSt-tIrZ{)i3AYV>b*~Prep$OmlPYka zX^wS`I6!hkF+lRYe24r$`Gb%~hEDeLMYW1u@?jQ6@!68^xVUUpk)m*ub&%zmbgRiI zpGh27>BMV{(V8@SzNIVC9?q5;Wq4wJLYfU*jQu$$n7`7KtPQvUhFpe@Jt}vyz6fK$ z@2vXJ7g@u!8d!(5S~-7}pcnaTMh-T?vRN?0oaoca+%#yWaHFx0If)`!^>*=@;eze_ z?OZOK!e}~x_Mu|4FmAYXwfZh+jNlXr_f>^&Tsw<SoW2mR37|$`DGjW4^8hx!sVMCnB8NnVZ@$&soR3bdb8^+DX z^p-J&^|)z73qsA+7#m4TwYw<>)7$vBX($%Vde0xvm?Z0Bjv%#`)4y%h`n7IS*9lIO*30ug% zPadX@lJ~Kvl0<$kmoQ!+&T@Y-$K#WD^9%~X39SNaAE=gds0?xcfJ5|B*#a7q78+aN zO#WMAamp{b>=W9-`q8X)+7(7GOH1)4-h3)u zFu+jADGcb#e`3luw9+-A?*(>IPv9ToXow|-W%&w2ivMHdM$Hu6Q1-4|lYu1H;l*r| z&(F*StV#I1+~4@uf*4p4Met`cdl?The`hT3aLwC~-^AR)bS7Y^LBRPVYnJaM;d$L^ z>LB?VziS=OF;WL|gDmIsZH6b}C7PvhG(HgDC3Da@>`ZGDI6r2%{26sK@UgBN-B7<- zC1q{q@1VDt7xMbkM@fHrm|+L?AJ?RwC`iffwx_@3-ex^r#P7?G=T?wR{%8R!Yo~67 zQ3=0VEXZq1E!JqUG*ugwhjEYKAKqJodDlwq{al+G=h@W#^}lQ9VSW9EGs4(^<;%$C z_zS!tMbe?H1TH0NWgj9KPE=xdI1}{C{4x!b{eLI!2Nkk!3u)6;K2M)mZlaWRar{OF9z{e}|}cE$bb-zs|Bo>U}QdPF@dDXp_jZRz3RO_$Rn^=Vd> zz{6Odh&Bgwok}ghZqxO2>v09Et7@WQ6TXWz2%9UQ>1FzOs(*SDY84;j738G{_E(j& z7Si3Qa^sKU_xd@+JL8BwSB>xW?5rF4*_pziapd>(PTwfbcjO~hj4IxEj(-WRSoBOg z#*iYI1m}WWp}VsZGZtcdsV(Lu-M=^P)sG4y;kTea3CFOb1^*eE@ik;meFZ+(sLO3< zS_gN^DdxAt8KxZm4ze?=F%}p!R3yOJId@r`;B1vS8+N5f=#-jUJm0KUcuTASImG9H zEsD6!`i+nXa?Qy?3EhZy0-Hn2nOjZytk$9zaBuA`rgXSx<#j5Ry@)rI)lj=$yx&kB zRAV@7-XrLx7mJV5y^Yc20>V!nh{~6LQy*&BsN12-&wtLk&UXY+ykx^& zlLC%fOzF9K^Q_x3DOJI4>+eG@63{}=9J?Cxw+ZTT7wT^rGbIV;+v?NYc}xa@n>vc$ zv!7B8Eo1OGbX!gzBUAGq@lxLazYJFjl9$3T=4+vC*e`5DLa$m8x`prFFKP~q`pfA>wcp3+Dh>y--&qw@+9v*oh%OF)Do43zWL)V z<!se-AN0S7Z=2ua!>}j$PxWg! zeKkx*2HB0j${;ZIHYyE!t*z5H3xWkZtP9l>h_;qIUF(9ghCTdt>Nz}7_U!#}T0cXd z?r<&?ahJWxv`G^$SZz2$d*XkQH&yfCs{S1~MVV2SkrEsBJo_-VNc0Pj?si?{gI(V0 z375})Eow{+;~r73-h5l#K|9Nst=r6*BrG&L@Ky!hS$ln})f4oSs2tuCYYZ=g=)$hi z^7SWuX?&%=r}3TcQeHiJs-=CvYu*ncnffntK38WQW%kLfzq13COs-~*z~Gueg5NZ2 z@-pE4iP?~WCkLjBmeB;yBzmH~$l@AyW^T~F#S(dEiFJ4z>wChK)0wx9hI3lgwSj-* zH&tV?zf?cdlMTaoVHQ~M&{X2XU%qEL&cQE(9=(*fQL{Zpb9Z=mQb zewA5^zrk&+6^3q_OY{<5td~yLP5h5_vN%@vGbf3}u^f#rZ!484Qjl|rFub>BzNoSO zgxMpYQfRdtCL{K2m1NTi`Z3ZQWUM8M%hcxtjuO2Rcc4myal#&w)!g4C#lq$LQt@Rv ziDk3&Jobh&93RY((N%Z^9?B623Bk`YD)6Q35Vu;ygddSi7v~V0c&*?kE-K=IE=~NG zuz}>Qwr5Z`&SW_2D@Pc@NrAgOFw9D7q4YKXuK*V7wfKSGDA5j{#P=`5oPF|&`c@>t#l-cB(4b(`oE4MzrnYdy8C+`V0-jGdIkiQ!eb>r}*!Z2cu zd7L^-xSrX}lx%FKo@m(~_zGJ~J%e9H{cIr28^tU6@6CHGvP45_508mpNU#SS1rZ6W7wi1dHe{ zX11t65<+(rOt3uV@r~p79C6!jH-$3BZZ5l{mDN&Tfps$<&+jBThc6=g(NUrbQ?3ak z_p{+z;rX5M$N43qVTMO^SK%XCXjI_E0(O2o?h{+Ac_`hUI%-oGm-J{&t+kz2O`*N4 zO4DxkWkV}Ilh>HFFca?Y5YU<5#xh&GgfwaMxvK;|0duwUx$E?A1QLSl8wvL>ek-tR z(=l&hEjO8LU^tI+@b|oXSRg){){B2K|4d)d!=12k2G?M!F^1*pxNq|paweKe*E70yt~yu;@4Ate~-( ztpmiEioU^^$4j+#l+4EUIB76iyvbNo8adirMlY9KvrMG3XoI+#-;^BBjfH#R^tT2H zy2Cx?PvX~vC5C;(J;5NsLf`CsG2FL1jBYH=Gh8Mv`DNHv2+z|0nlj;PH1CP|)@?Xu z`cd?jiZDL55qPD86kyF@>&T7 z32#~kTfK}ojqS;fWE*U*IFqix52{w|Z{WXA7)dQN)Dy`B!?0zvr~V23oU=;Z3-7D( zV%D-7Sts#3>Nj!2Y%7e_;&J+N@~NSL6{ELUz9Tmo<0LDwDf=4GgR&TgBN&%Skriy4 zcnTHAF&hJSkMkXZvy2Zc5>=w$5!Rpi^Zpoom_D1jX}(1A#6x_|*%BgxMZi_At_hOa zJB=p-f1&=MGiW2+J;IZ#4oI@}!x{;$8Sd!X8m8b~%&(|QlMwe{5`l-a-m}|Loz(we zjq-hjhxm&8D8q4M3Q-vJD;-R)Afh=9-V!a?9 zF)wObb7<;H7S6tA>|peREAXxoZxVMhCXZS5i`5naif zo0UM$)uxJ*wJ9VnY-cE@Jq5Rgcg4-=1Nia4HM2GEH@|M~< zi$>#_`TrW0Yuco*!w>Rrapp4Y+!oqrybd~B;0WwTzg7b9roSmp6%B!VpR8jq;4iim zyl9%tyqwvIo+LF02Xn*d z_X43{VPL#&D7GW$m}xRA2=0W(mYnnbL;I($oHf%x!aKz4-50RnEV<4b=|;QOU_7cR zc@-ZnzC=rc_}Uv8UcBC_LTmy1IsXzb&DX?#k~PY-LcN%_n5uNIM6<+i_ydXTj#tS) z)O}PHMxN=2<^bkPrNJ*Oj%zg{+Wg*9NKK&QsV-z&T!pX4*27V4qPQ2o0<-IIUNd|R zA%jmN-@&iQ*^Z(NK7jzXZ$2Rd-1=TNj zjHm8@DOiKyMv*v{J>PKC(1uPD`(u1;p<$L}t(HwJ5nGAzyjeMW%%Ru`@|fr(NgB@* zp~AcPd7mFmi6>BRn!>$j_9dG3)#bh2HK;MlVb$p3D{z9xIctKW!huh&0iefGuA8Uvb}Be z_Nad175O6yS$Kx{UE(KP8rMwUTbW>QO0&)NzGFpwFKotM8+*Ghwl)ntJbH(u9 z;aS;>l9&DNsCKls%quSpYZiUk(wu5g*}Akri4R^$G?ouh&MNBZXkw5QEH`9`1eOYQ z77@pl+K^mL@3*%NvFzhIcKHTKiGf;CO3G>Y(_o;8L+sc*e2PemIm? zcZfJBhz~neFwN3AqN~FR2h%n zzx3GWXs;L*+SwjcwA{cd?Lon9QY*b8LZdmxQIxf;o}@tbHgu7FZRotv%7{2?6Jtt+ zS8-1&K>cedGh_?g@94NFEd0E5iurfNjL7;SKii%;g3AV%?=7Q5rjjc8fT|Or3rk*E z`&;?ulH%_6rp6TUF2SA`0_7$P+v99?otS$MM;Amx_@>0nk zrk@o@!(LC z%25T6sU86;TVGBjJJwG{o;Ix26T&xP`@(yKKG{`muS|r;M>eIcHH}KC7GT3hqt!vzU1mk^#X3vYzKH zP~0f;p;^R8>IK|8xVtD`?^F6jIm~=m|InOm9#h=av6)w(Y#8yoT%#VE`NFy~bccFB znPNU$)GF*MUZvapK-M zqJC6eQSIPtDHH~j%HHxKOU|pemOtVxE^TF9zx#W6%W#QGB8VthXH8&Kix|3t_9e_h z_Ez~Gg)H-a{&`K4;QZoV#tZTx(p7LSWpl$z_8hEt@H@$FULRFEO?9zn(L7OJNV#Gu zaliY&#Z@A0SOg_?G%qQX50+=`8yG%36Eie|o2>2Id)`#o@fS&3QJnuK!z0yN+LxJT zp>v0X{+F9!>cqNV5Nm42%N6gEZroe{!KeLpPW1Zd__!_y>p4!w?Tic$T^rvpvM~B? z#NV+S`kEvDj;=WnADt0Z9(hz5998d7R9JjeF`VCZKDJ4I7iIs*dlAN{LZ#L^;}|D8 zK)F5g$M2TM_EV-RqoXziFIR?_T~)G_;;5^UR6?h+C9(OTbj?f0Uy(PWYh!1}8tM6w zoMZhX+ZOO5Xv-vJ?V)x`;h}wXv3WH0@aRRhB#z}~3Ficz$gK;D+3e`VjgCXN)EtdmosS$>v_=ozH*KnpRtvYs2m53YM`tV)xYw zQHG?Ic^Q@j*TNjlX-}K=$EYp(zv)rbGwVjY2i%LXrSP%nJHwyGH@wa=5AhM%AZ{ye zLt?7+yu1z1OpU@`FaxxK;$Jihza$oyQ--|^ytU^Er;2e88)G{VxSzip&U^YL_keY) zWdSeBZvd-X-V|-BWv2D~&S-I!wO-*nyn*Fs;&)DwZJ=dvu*p7AzsS?^q3P-)<*SBpfn&@hHho9Q2_23H< zyR$L|=QlGwz#=u%RAW^Ww26C#`ReSqoSlYkR1cP??oL*2Krwqdwb9>??2$Ql|J_Z` zG_9HK^(=U&F;a4uqJ-->^^F(EN_@J?k(-ZmsmsC%OdHWz+KhRDwM4c)WSFgm&k)wM z(8Lef}5xw9=JN-rLHmRK;QQKiqCX3gKkMBMG0`+T23h1pj$uEd1tTl;M_`AsJPW<`*N0#T(lE zL%Uetk*dOiykFp64(%0vq{a3s3}fzP9oLT}pK6-wFYkq``%x2wzvpGELv|n6p2Pmb zj#z^5F^n6U!Q$@e9W}E8Z}V31dul|S$HuE1N;n!H?$;9A>Ni?*%5a_#YHqP=_WqIa zT{r*0NbO~wlD$K4jW?M;+R&WJGib%p`aijUFyHHw8GrL)%*Tj_dpvo40*$^4 zsxjfo-4DNT;swSSmgM?p9Spc3jMui}8urfgn$Na$AH!V4I;*?H`#!6onSfvGS7L7j zJG|%Xbp9`yz42z6;hZcu-{CRwj9#nW&DQyb1vJY_qmSz93%c+OJ&t6Vb*cLP`lf8X zG0Rpg3=F;^e;{5=jSgsE5MNkHzmxQo8^s4j8e)v_GG`-WqOL~$p0icx$+@QQn9EXC zWemXLb7>ygZK1V_%I1wRKC^JN9-;{5QR;DCeYR5E)G`zAMZ}X=g0Awnld1c2GFRxt zq&WNEoaMPqL>+d92C2M4111xwM>#!NNl z^?@^p$bcX7>-)FXSL9#I{*|>xJaDTo$C9~o-$b#_e}Sr)QYyYH&<0)4Z7k}@OoBVc zJTl5z8pAyUL&IdfF-hHpJ7E(mp*vYt*wMe)dD;IXNa*WGGDIF&Bu8~i2 z|K^lX`Q$d%GS)Bpe~C91cHW(gLc?abl2^TeTXeYCVTo3(H%UWz7O`!(z1Z5=v9-`w z{!IQ<`mA6b++%vH^e;(E`U<_+zTLdq5UjP)g9>Kxs_DM?drPcnJ=~2g9=|}q?>J>U zZLy+FWKZh7;Cq=mds|qVeJnc;YhoKH^wrO1{3Oi5exh@^@Y@{AEMW*;LXV-}TAK(u z@QVva^V5QPSeWKGaZ2{DjVp}DZiSK-DQ2>4_K%@zR7?FA!>1KNy7JtKk~zNn*#G*S zmjq(j{G)j{v?BtFMZf4qo7S;!>wn7{T6j|1jh%+S@JUpaGW>;OgT_#}Wpv&lUXLtp z{vax!-r(~}y_`R8FRtl;HO2LXMDcvFmryEG(NdoVmX2~=;UIdGqeGFSWRX0pNac4n z_ZRJMV;p-*_O`CH_R$VDSqoR~Te)kX-_Y!Yy-N$b^SpJ(cxQ-O;~@Lu{1LqO!ag|H zqBY6Q>rHW@{{%y^(T07we=zR}6In?DoVuzB-+zX^(tkeDma58}$9}^qs%2kw3|$B=_JB*w5L74O7m%v{ ztWI=)JcY;*EeKxMeHiC@=#zpC3P$j5hhJ%4d1df;sXQc(o*$z9@8fYA1KwYsQZ9^c zRoe4V$0||C<#Iv9{OaJt#;R_IZXQexyI*=Id?J`A@Vxx-3F(YOUaAZWQ*Zycs|9@CsqG(jhhXLfS_DR*pxV zKF}(vEXFhTw&Qu&jwojN&yJTN_oD0yPkU6{j|JBvGD=p&&bB?3D#E*m^(@~X85zG& z-p$fFVq(qj)!nN1*()R8M6IfPUHLT96vC|hDL*4x9Ti+5IP_=qq8Lu8_pvik&4U}& zJP$sRD+?2qs4DJ-Zz$C#Gzm?XUzCjKHSIe~n9<>gTM{E6qRUiu6U)e zYJ;pT<-yekr6F`n&8Fz=%2n#Kj+?-&uPOR3 zz5zBSd|6~;$9(zh(hbqy#cr25s`NF_iMHV>u?NEk*yQ4;6?dY`bDKssH~&+S6&!o0 z66;q)1NK z^a4T6sfu<7{fp>`yS9}MopP}9NPHtpK|!17t~LEaFO^y1uD|>=Z+4c%6?uldwQ0iU zmIo<%I`%k}g{O!C^k~g$`$gNe;#RLd>Uz}k@YpZ5(`gc!b-sLU5iXO80|GdfwZzrn zW8z2F(*c-eh52{5{|io1`m~^Ga(r+bG_^8!VKmwMMa`MS3HP z7r8_{STT{e(L7MplkUUavFWg!5b<;WG9B8tR>ZVL734`KD{Sf!*42`Ca0P~Bp|8Zg z?}5MF8%?H34v0M*HrosF9IWf>kF|iGNO(xCYYj*6`chT!9qCM23N5XZp*rd0Q;p*l zw+g+BdK67Hl=+MmjIcHh-ec_&wmG;v+>>i#KrH`=C|uoC@{7Hfs-b=e+;P&oz^Cvz zgJG8#TUs)Lb&C6i6*y0R34ewEE9}FsDH<1WU7jSI?f9egsbY>??XW50#na`@|9?VG99@@pZbL|5A=15rG=;1d6e;CIXAtL^bSfQ- zPd6!pSK{ULfUs+_cKA!tY3v69X}n{bDqLsv6wOksEtpU0G)rXrB*#lklwMj@G}X9^ zHPw+`oE{QTc-p#2KS%#J{?PVX@=zDTsxCN1Jh08g9CSbHG(76fr}?!#Ov5@8bUlBj zG$3MoZd=Q%kcLrej>OnQ$`Ct56GCS=b)jy#dB<2U0^c#;?meyx3(WOD(Yab3!lwdr zxTo#=>{r#9`FFU>0y#l5vQLrs0ykm<`CW_}o|QeAL71C4BDCXjB3Z*bw-1QPyxrB4 zwVrF>m1#}PTK}e+wlaaWyLwta7fWVU1T|0{C;tmf;ttUFF&yUlvyWoS9OoE=waJc9%WzjA-b zBmKmjDzcyMD)l;GIa?#_9(p*hMa~}o2h6~(f9#X}>d&n3`#$4chD;O5_%Yyy_$Tq| zoIP(p)zwS+Qpt(6*zOQK<>c7I!MmehN874@iCi6iJ90$0(0-s!7wzvKsefjlQcRUz z$jYJFyxW2QC@A|Y_5tzAk^=!rrZ>VsNrY^u&69pev#9SWwUJ@ZB-MBmX&fci4m6wm z4RmkXpWi_mToy5r3B$SYz#Z@Ext=q`%x10RCm65NTgZo;uGXU-rlSx*z}HTY?GQY?1jd+1#lg7qJw#FP+~b|JOX8Zi)E> zwIb+n(PZ6LB8T;i2r=~4Q^s1`2<8m+1MO4=4-28p?u^%49k z?=Zw9pML*o-kd3(U9hBVc;pIY1f3D`P@EdM2|o84 z6v>GEJM?Ynp-2ypMjlN(JmG&2_}{~0Yy&v9IPxT_)ptZSrksF#%Z=r62P%)T4*OM& z=+n6O-CpM+%{6BZzmMpyyc%CwvHFnDq4NpzN@J_i4+)PyIB~4+%HD4hTOF8u;P@dy zV$Ys0dbU3N?$G!W!C_|ggW|tqy2eh3J$3x2V-Jrsk8hqBl$b3ZB09iNmz3kH;C{_A z@l(+{MI-qHUbcLx=&eO0=`5+jJLPN^H1yjqBBW8=p|)#u1(|HSYN@4`*~VF~F~h7z za~U^R3z*lTa>RT?^V$$QlP8~pBnon zrc2DBntL^UY6cW1I9?lD#8t+gh?UozuijRzDBo9lv-o@EcBM#p(^@8bDs5HNRnb#% z-+W5eM!d@LPSiHoRC-e}BW!Zv99c7YAIr~NhOA9-lu#Z#mYgf~l^m#?Rn@h+FhUd) z6uG~$OXY}i!paIu3Of*UBSDta`xTz5#rt`tYxdcf}@+7@34{iRS;6hLKk_^W%ODA)8a z-39MpFSCc`g;~NoUXc#7jJ3x}yo%xOwbBc)o-g4I=?l~iLSpS~PscV}T3Ig@C#rfV z_$49orD3m&T3}=4ic+TZTBu*e76l#=P!cP981h?TGH;T!N69+LtKez&KElB=^`XZH zn;+VBWbC2(hn9cmk*G}6ml|q*JelPz(THRJ z9UTz9E5bYMqO5JG0lwQ9C2xywH7%EwcJeNMfWI$t7#xynbG~C?@y^0IrH$cp^GEt6 zy!yfv#h8+)h~F$dD_@8A#g`Nh2yGyiRQM@JP(7>Dqpr#BI%Y+9irYB$1$#;VD~Swe z;pvWxqHPIl<9RW|D_@qas=9RGKn?HU#_vWaa1uPrHRTiV%W(85%2%8$8eG~qbXMuJg3@3=TS*Z!Jj3yQnL?RdJ~YQ3A+(muH-`GN zgADt^Iuxx7d>|igySViI`@XQ&rcRtaZjLZz(zGcl6UWS%IBn9DiE}1So<4C#%D9v% zGiQvMK5goh6rNBbT>$Xl%HqTroESqWc=HG$A>n^C$x)C$-D^aC)U32?J3#0M+^A{W z!u@a@zye?pfCG=(f-ay2(tsF%0o1|)`5_<|NCw2fqt>7csDWf424DbJVe&hGT|g4R z0N%j4!smfqKoYA@A^aC)UwmI|#Yy-vup@0`~z8Q=wuo;*JumDfsR8#N) zqyYVZHo(Ou&<3Oc{eU(=ZDa5Y07fVDsppai^ui}hhH18G1%00Wu;hw4ELKq@d2@B*H~@RbAUzyd%H zfNPIN=ik4FlE8k&!vnha09B8db=w!PZS%i<4m<%1q=$g1>cIe@6tGA7Xa`-RL)8yH zkS&Dh@eFtibOFb+UHXEue$YehsL%g^EpFt<7RjFji{QL)m~QB=If?;2hVpt0O?&?y z>7c$)|BnEa?{yb{&bUxJ()|}W^2uL)XFe!q2ecy{SCBrEA>T|8gaA<&KGn{d4{Aqt zPk`}mfgp72-C3;shfj=%URHq`FaMX}Wa0%+(7Tz2v8 zj19FT{RhB1H*#c)N;yM_>9ncchDQzYWBI@lfDT7vIj@Q9JT|2YBU1j%<()${?hr^411c=};}p#G8lHK6p9{by?^ea*+(A|2;AU4_ScU|hc(hufFG z9tWh0a=ip}hR*x}G*_K<5(nYx+12|wD2E23e?aUpu zBi|sLH|mSy8=%?tRvJvvOfr1|78E!xR$T~7(dc+juRT! zQW)1Sb8U9x>s*m8%C!oZ2%R+q&|Lkp&Pax0sRr`g$dMhA*8m$Kr@CA}+ech{{b?V8 z`a|(n0F{VOHlO8}x#45XNDt{dfO3EX^>D_B+EL%A9c{~ic93gb44>^IJY8ylbe;OB z2FUmKb&cy>{5!`R$&k1N(7KT$A(EE@M_lC2_>g}jF9t?KzF&@y_U6~)gLF}Bc3=V+ zHwOa2)|tCAHY7v73xPN{a%6|(MF1cAakr0XvGHU7s4oPM|wyf z2W-Frm;Rmm2-J@Hw*tdq48F`g@7CA3BVClc31|Yweh9GrGI!LDd|QAhH*#c)4$v;>L1zbfkvR`Zd`9}`WQdbagGxj*XuB@UycL+@2_)3x+qr-poGqb z0BEj$IS!~D#i9d_xRE1UB-a9_x}4x)YXj+@YN1o|Z9<6q+s?_uEahdfc=DE53n z4Ltdj>t}QF)aH-zBR!K7efPI8%($S=}GdF}@4LxVRE3_4%tiQ19=USPNzIkH9aJwVjm z|K<7Fd}3_<80Uw+KyMSU3(&gwbIvE!j{NNe!t3gTnothjz`A>1&ljYNa@htXfpI&a z5!im23z8w<+krqga%6|(JAgPBxpOWf|467)GC1AJ(V1y;EDcjkxMkrh=6C+Z8u zxe!b23<#kgck3`4+EGldz|#*kLj5CqRC}$9f7Iq|N81I!mQVgaTZd^b`8n;7 zKFV({+Crlb(Cq%#`Jr}XGaul%kt16qp9l1Gk)t*Q@{hJDz-5p-Ywui#C!o!#>(oPW zq4m%P8rVR#i(hBmQ9II~15~<^BU>b&4QLSIHv>sp`$mj?Fl+x&JQF*am)aIa3e={NInx7@aTVWezp$rJs;yleW5rf z0MmdX7k|!mh}x0AsX!md+gWetT;KEfQ@xR@Qy1lu48%jjD4-MAewhnuNBTbiQa5sB zi{w*)Aue*~Iz;}Fd@^v)O>EL9U&n@YQEW*-cQ6(MCKun%`GDGy?@7REH*#c)eV>`(qbTZh|R@^jiDeU#sKz(8nx2ivYszs?WI zkWEh@*o_?7A$c!gxQiUMA&`HxO#mK%+?ktm9bSeur>;{E#f8?xSZHVpm|Xli>yBhd zKMpwUMvm-|JRX=-S6_JezjZa_i<+SRP`^LrL6>3NRlmP$vr}{bP?xGi;>W5UFx+s@0AQBoz z0MA`~J8OvAk$wcw34DK-6&L*pM!YO#yTT<5#fV z;^NyG8`|7m|Wzj4T1cltq6#NIKEt0 z(z@83`bZDOg#&${kpP;3jx%p(Tu6p&2!P{8j_i7lq#okPKJFmTVsuXB7+JMtR{Gy@-B#*J)|JOJ3|rr(WU^o#mK{rUpq!LTJT-i==* zgMS`=zzR2VWQXMbK(>qASr1=mM{)+R?#=(^-Dl&q1lo}fvT@oWeH34JKn}v*Kz;a* z=*zrOJF@Ko__>iITO{`ZqFm(8evp48_Xf^_{LAq=0BuO_1nHr;x&YD8I1Je1;@3G| zs2$mK1B%?pku8#U1^Ph#U-lbbR}-XzK>eY9I{^}C=m4|>oiFQwWJtd=Aao-~c1Vt1 zhq}J|-*|lXI`<;~W6r2AR43F&M_`4EKj-TlYDfOie!SD$PkA_B=f=PPR6nHZ)J3_p z1%jX<0=Vqr+gU@@j`Z6D@7&0dEt0nbe4uYJ z>ksE=I{QTW@b@83WQXEx3A_e+fpE1;KTaFej_g_kd2Zy$7Rg%y8=)SkfB0*NUJKCc zm?wG$QZE=a*Yib{(CP|2{SX(52l+$!c7iedGH>)8$(sWo{eCJVqlV z3_v-y_)w*f=c+pGQ5~JNM81$-6JR7bZwzd4>DyTgB!hn+&NDN)ks~`KNBXB-*;*1N)kUpBXJ>AHW9g;T!esGbaHU#pI zwx};uQ?xeFSUGFzoLdZN|2V$QK8z1KuOG$WT1({PqaQd%1E+(4%PxI7V@B<$Ph|UX zu6`UJB>yr$is}9DLh>G@u{g2^@k0?+w6Ez#BLp3O2wjKnc8ogW^NLHee*c0&c>A zGzBaHLV*`>09*}h21WvHfHSZY*##s4EZ`;_%u>J-AOY|MPQd|dI*0LCm;u48XyOr;&847umMN}+5pD{ z;1@^&7(gu_`~Yb{BH#sF<-vEiKoWohZ!oAakP3tXO@K38hzA%6uz;r=&;ii->OvCGe&z*Z`@35@-S(>H_@$O5l+f z#0sPWIPj=5tN|be2nEnN=9W&78z2Ynbc7lL=)7_ac-jGK2rK}^06L$X1Qq~d;7vQ| zA4mn10D6ITv;{e!1eyT$Ht-x!0&iM_97qF{z?)WJ4{QKJffp?y9$*O&1E3e=LoGlb zNCY|n)y<(l0G%8DrWyDLQh`taofBT(6zqW*0KGUln!s~F4m@oP^#c|FN}vf)-3a^x zN&ubvUEL7;0qDH%7oPAO*Z`n&zCD3s4PZ=wet;KntUf#kMgmOWYCV`gz)%1kgPnpf zHUNVFF!zv~-oJ-XKD^Ik!ZvB*td!*WQ|3$@Ghyb8dBUVIa7&Jfvu8}4HYRD}k8{V( zNSU%|BB*_^1$Q4_PoUcmpBtvt`9gkRxIOATh;c#GlyP%DcYthR2t4dAHZX;pHX@0e zZc)6*2H7HD3VFP6Z!eU6!+va?t+%AWx^`%i_(FTPbA-20WHqKIQK+WTEEu_XED1n{HD4YF?uG;%#-CBSCy4-mzfQ&&9@NT;<>D({)^j zg8gO}dnBt{3lcY7ei#ol7ph%sT*gBDtvFF`FY0V?c#P)omo*yWHsY_>FN!h7=C)3v zZ}pFC7P#2BxM-`YnZ8>c-39$$2^cbOML z={I6V@uzv=5;N@k-N!uErB5_= zL*3iMKGD6s>v*Sv{Vo@Km$~e2ck>}vRHtKZZQOJSx@e9d8*~ijq=n;C&MtL=D~f;n zI85nc<1#kxc2OVt918t8#|Gtk#qvQ6REMu{&J)b{Qkszx|(UJQnQV ze8c`*$3V!&HTKYwkM+W#9nI~pV|Nn>=%LypKhC(VAXqyUp4)->2 zw-@*0g=`+V*tm?B+gC#89C7B1YL1t>og;+&8+|gsCdtLdrB8QD;eYeewa;7^dzae4 z>w^0{QJ?2sY+TllyTis0V=)$NT9kdt&1F8pe%<|iLiKQsGts?0yq5gm*>9+`|K@yh zyQ)6TC+G1B%H_OEEG{(@eQTUhE-lJGT=9{l&ZWa)VITiT-8o8PvE3PU$FQ7GLOHhf+|_CVBvyA z7NbT*#W*S|D(XauqN1XrqN4Hp`kvE$-o2;ET!R&P6AM)2gp?R-nEGWJS1)sXHNgj`FI}CZ;q;(eFcFX>2#Tk9nJl z^OIAwa2%DZxL`~jH5?ODW1gnWQ6)rUDxiPC=OZurF_rSye>K|K>TkzjwRh)>ah)Pj z*Xk+Z4#EawH*%}5H*HnBX~TKLH|sOnw&%(3d&XVUQD+NOa}}VB8;;epZGE_=swer2 z!Vl=<)?J_cfJwGvm2=Xc5BG4{u8u$WwZ=!rUgXl%@n>83ZRZ8+FZNFw zKL4-x$1;cE7=1?jFZvuWE!LcYuQztu_-U*E3e3|LPU~KAP9D^83Ub)YXZ(uX?faGU zX-ow4pBDZw`rI>FZu2%gaLgGTN6(if`u*Q%Y`40s<~&w_KWX@8jhx*c=O*T`a9Z_u z&nmh8od2Ae->ltO+j)ia-}E`Shv=U>131Iy8b6tNM%wau3C^tH7~hV0=5cMPo31na zCxy=#3y^f39d#={_rZ#ATYc;$0q`_Ndd`2}bZy5yXWw6(zc3uyB74_M$(7E;92HKh z&R#)kbk?{o@Y#x)-}oNq1b>eOaMbtYhi^4DTfVoo=reMZANYUax8?`O6Xeqm$IaX} z*4pw>3C{A1&F5?g&PVRRw;NkIpPrX;Pc`v@MviF<&&N|U4=?U{JpO#x2xCrTUnk;x8_!4Ia85jKQs21P zR4c?eqdw@L%(s6XkA>HrTy&I^8l9||6i(Q173f1{W6`I6`rfag8onc3Otk+JFN zLv#D~<5AS!g_*_a>5+z80z;vGH-(Ewg4Op#9-Et<29CT>aa^XQ{>=u>{);MJO_ACK9%KP&nL zd|t2n=<3$}y0Hz9TKo1MoL}6tF#m<=Exs=XzZ-qQHDer`{Q^eAvHDF3hyJ$yV{oaz zEq)nz+W0vLTIRo4xJ4fj*S8$;pJm_ipVm-3$oW$L-7xwwE@JGDwA!-~^J)B8dxm9v zwT17T>xAgH2Ys~6o|vB+dzJ;iTkvT-Xsyq~o;~M8Ki!)%kK+dpADx?C*uQ^%&%Q_T zKzfVo_rC8%y8_0o<7i}cakN#q$r_K*iMqxm`V{))OThE}Q3zV<6SQ|FX7(vfIQFVdNUtS zg2(s>klK&K?SAY6N1;ES5N>a78kxYMTE{N>l**5Sr$6|0jVu?MeM}C8yzw7I+h);6 z>jb>;`ZMRpWTmk@FT96=Me7XW&C+N6Hj3cjv6zGhRMd$;6E z?&^9-xLos|91P<;+fE6dv7F@Ko;b1kT{3f6|Iy((hsE#uVf0^t<7k=xV%^!;|AgSD z1)uIuy@$!ep8pPm@5IBsidWs#-=_`U+Cze8&)|=uJ)hx#jy>@@ud!#B;6E++)E>R; z;9<|69pKX*^6<5)zB~s{xOzSzya0~I(Znj_Xi2zf4&H>Z)F)l^DU9EVA4i{j3VpO} zjpMakjb>GIarbvW1sU~6W$d1 zXq`d4xH(uaY;j!IUvsdIcWU{LTuV?~(Lc$L^65oXesgKj;09o>~_FPh-8r zXT{H>jvqoFt@`^f=D8lOWMHoMZIB({Xe{;YC!C;l4%$sAA%_J|y^{Up)&S(3F>`P( zsUaDD?i?!yZ{u~?;5A-Ld^YuqX!i%vN6Xg6UbmG1JSq4O5Dk19ujzGLNsvgudkMb! z%;-0Y65h4vhRnAE$I(*#4zl`>5Cpy=!{{9mWa*axe)h~cg{o41_y|tc} zHzm9PuI{JtTkI!|3*wIoH`!046F8Q}5%B{n(Z=_qkCyqw*8Ks1t%83a(T-2dDYTj| z5F}FIeH?sUvz|ELlc(psnUlIM@gFafernYt`e%-I4{qL^e z**>hl6UD>yA33UDfqp;uw4*)FH@SZA3XieMSlB&%0DlFro3}!1yvBBc;|pdU=F{vN z+SDGFcTMcu1`b*$;N^Bm&Tp>W3mo70>*$yJ(MRhH;>E2S&4O!u+2<%6N7tL{r*?XI zs=)E71h3Bm81Op2wQpJUd>4+RW$SIPO9~<`N4wt1KJZE2#q<06Rzx*lL6Atm`wh{L zea%nKZSzc5;>R(!^@Z9tj$^59#Ge*jyXd23w%M9E0B}k0_Yv*bme$09Adv#^7mR*A z`3VZ9`VqhO`DhRKL~=?q>^@xg+vo?*ZWA~)mY0sC|&Kn6{_pq(mEq>ujw2yv6%huc0>;(WP z1kd@=viy|R>;*w00q@7ar#vT9a~Q|ecWN8)JAM~!tBP*NHe2%& ze^~HU@#5H)*1Y7XZ6*4P;OkoTJLR6pWR3ZDBk$xlo-HOJ0z(ad4;Q}uvH=dk#l zzmGns5?<~D`FpEqM~OL0!B-08KB)gLLG>d?=L_f`0-rwMI`DaNte^5{Gy%s>3N~5K z>H$G&3-NdSA=*-16gjq}_3#eCSH+uS%dP6+67y{XpSHB;rZIPo4hv81(miWyLXA_- zbK1<~<|YKB`or>uu10?hV+^!Tz>Axkq{!epm5^EC-Ge?_XAm!LZZgG&&ab(t1c&2j zyxZD|`$zFs-=hg$a?^;H%Q*hPAESMDqmP#6Cj7lr{KkRWwN>zM5qxqks{g)meC z`UQeSO1!TFU)QQ9H(@h*YtN}j^b^o^EPl=w?4=r17 zTlbd$TonB4i3UDh@3ihO2@(l-UvANln^Emodx)R>YxLvm&_~Pkvvqs`;E>>7OSGe3 zTE_>1L<+nwfUo}U$uBthruq?oLhw$0aqqQ|bHU7E^NX!*txh}s7JU%FMaz7U*0zTQ zkDtG&@aY=es!3I=+Xnd2m@$bFD`W<%=ynqLZVIF81<%`Y|<18|fAa>U@tll-y;-P*o@ zKBa*4|0CMR?*yV%>&<5z%9GNkTC)P5PY9m2pjCK2m#*=0+laOl1c-3mhrsPwYkb!- z{i3D%AOKGxZ&r9*Yh%*{{+roE{3X$meYDgjE#E8Mh#B~2^a0luEw!mG->Dn%YSR?a z;8G{XOmnVp?&$}7P9b(ku#6=)1`)ie{p6h%{a%eeS|{MejX_Da)Q`N&!efnz)*gc< z>FWGzK%nS2xsA>J+yESf*mXCWcr|Oa8HZfko&=u0-#KL+UCE6ufS7sn{XPGL@BlReK8zqYN5K85&Y(UIR7MoZU9OZ>{`#P|It+QeLk zmhSnw#HSs^>snFw>%MN|IF#u|{JhM?doO6IZd$Tea}a-A@c3ybwp6#eBBgG`TiuR; zYyGC*P3AzlCSCL?#63u$qn9)f6d@IkK}!pul4vAzx!tNGsi|tZP)U(w*8pk zd9G8uIlt6r%hY!2@)Wpyr(I1h&aY31IV;#CzL0@)JBT0tZ?uDbwA2nQb1OTDDFjbF z(9$)mGcN5Q-ui48xcrR!o4F+W9vnW?wY0pagx70-Q$KaVqwx4yGY9{lJ_o}`^&DOF zDa1|uFZuyL5ysXDc;Wda<=eV}yanO0wn6Izym+}sA3Ldinm;(kJjS43*ylLaJwLoF zwnU%8tS`$Z7r*X>XcxyqOV?LR_&QFzf~S6pH*s2LLXJm$>^hBut9CFxq;_@n zDh!rBkNeHH;ezON8*pgVZ^$!;L{9TulllxGv+8fbGB2r)RiT_1GMVNBU6lTf!nTO`)58S Date: Mon, 9 Dec 2024 22:09:26 +0000 Subject: [PATCH 019/155] Lint --- src/settings/SettingsModal.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 11d73f4d..b7d4c846 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,7 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { ChangeEvent, FC, ReactNode, useCallback, useEffect, useState } from "react"; +import { + ChangeEvent, + FC, + ReactNode, + useCallback, + useEffect, + useState, +} from "react"; import { Trans, useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Root as Form, Separator, Text } from "@vector-im/compound-web"; From aa5cf2f5918b8328e3a3431f9b259e3936d583a7 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 9 Dec 2024 22:11:30 +0000 Subject: [PATCH 020/155] Wording change --- locales/en/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/app.json b/locales/en/app.json index 6b72876a..7b57e125 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -152,7 +152,7 @@ }, "background_blur_header": "Background", "background_blur_label": "Blur the background of the video", - "blur_not_supported_by_browser": "(Background blur is not supported by this browser)", + "blur_not_supported_by_browser": "(Background blur is not supported by this device)", "developer_settings_label": "Developer Settings", "developer_settings_label_description": "Expose developer settings in the settings window.", "developer_tab_title": "Developer", From b6cc9c534ba749e327790a11f0cbc91599162141 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 9 Dec 2024 22:12:49 +0000 Subject: [PATCH 021/155] Lint --- src/room/LobbyView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index 545eff09..0321672b 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { FC, useCallback, useEffect, useMemo, useState } from "react"; +import { FC, useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { Button } from "@vector-im/compound-web"; From c860be73f6a2cc0bbdd6455b2704710b8d18ecfc Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 11 Dec 2024 10:30:45 +0000 Subject: [PATCH 022/155] . --- src/settings/SettingsModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 58703ad4..46839b0d 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -27,7 +27,7 @@ import { import { widget } from "../widget"; import { useSetting, - developerSettingsTab as developerSettingsTabSetting, + developerSettingsTab, backgroundBlur as backgroundBlurSetting, soundEffectVolumeSetting, } from "./settings"; @@ -102,7 +102,7 @@ export const SettingsModal: FC = ({ const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting); const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume); - const [showDeveloperSettingsTab] = useSetting(developerSettingsTabSetting); + const [showDeveloperSettingsTab] = useSetting(developerSettingsTab); const audioTab: Tab = { key: "audio", From f18ce5adf1563e7af4c0e153665c54fc61f955d3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 11 Dec 2024 10:34:24 +0000 Subject: [PATCH 023/155] Format --- src/settings/SettingsModal.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 46839b0d..e3bd5017 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { - type FC, - type ReactNode, - useEffect, - useState, -} from "react"; +import { type FC, type ReactNode, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { type MatrixClient } from "matrix-js-sdk/src/matrix"; import { Root as Form, Separator } from "@vector-im/compound-web"; From 3f48810034a4a13ba737635fff92e3ba5d2d7f99 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 13 Dec 2024 09:57:55 +0100 Subject: [PATCH 024/155] bump components-js (not yet working) --- src/livekit/TrackProcessorContext.tsx | 18 +++++++++++------- yarn.lock | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index 71714b6e..e131dcb1 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -5,10 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ -import { BackgroundOptions, ProcessorWrapper } from "@livekit/track-processors"; +import { + type BackgroundOptions, + ProcessorWrapper, +} from "@livekit/track-processors"; import { createContext, - FC, + type FC, useCallback, useContext, useEffect, @@ -16,7 +19,7 @@ import { useState, } from "react"; import { logger } from "matrix-js-sdk/src/logger"; -import { LocalVideoTrack } from "livekit-client"; +import { type LocalVideoTrack } from "livekit-client"; import { backgroundBlur as backgroundBlurSettings, @@ -45,11 +48,12 @@ export const useTrackProcessorSync = ( ): void => { const { processor } = useTrackProcessor() || {}; useEffect(() => { - if (processor && !videoTrack?.getProcessor()) { - void videoTrack?.setProcessor(processor); + if (!videoTrack) return; + if (processor && !videoTrack.getProcessor()) { + void videoTrack.setProcessor(processor); } - if (!processor && videoTrack?.getProcessor()) { - void videoTrack?.stopProcessor(); + if (!processor && videoTrack.getProcessor()) { + void videoTrack.stopProcessor(); } }, [processor, videoTrack]); }; diff --git a/yarn.lock b/yarn.lock index 830228c1..880d8e52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1941,9 +1941,9 @@ rxjs "7.8.1" "@livekit/components-react@^2.0.0": - version "2.6.9" - resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.6.9.tgz#2ff4691dc2cae6ed4c4b2e586a255d00e494bf9c" - integrity sha512-j43i/Dm8dlI2jxv5wv0s+69QPVqVEjg0y2tyznfs/7RDcaIZsIIzNijPu1kLditerzvzQdRsOgFQ3UWONcTkGA== + version "2.6.10" + resolved "https://registry.yarnpkg.com/@livekit/components-react/-/components-react-2.6.10.tgz#949d6e65e8507e2d8a4c75bf190adf56f6358175" + integrity sha512-aR8rqCIEvT3QYHuVEm67THRmNd9x25FTmU3Phi928FhzQJXDBO0N1/5d6qEE/wuDOgXMOoBgA98qsdYDIi2f+g== dependencies: "@livekit/components-core" "0.11.10" clsx "2.1.1" From 7983adb98e3b40ce10016c7f025a6297ea08bf4f Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 18 Dec 2024 09:42:52 +0000 Subject: [PATCH 025/155] Lint --- src/livekit/BlurBackgroundTransformer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts index 7273e487..51b6a536 100644 --- a/src/livekit/BlurBackgroundTransformer.ts +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details. import { BackgroundTransformer, VideoTransformer, - VideoTransformerInitOptions, + type VideoTransformerInitOptions, } from "@livekit/track-processors"; import { ImageSegmenter } from "@mediapipe/tasks-vision"; From 7c985b0d9c823b9e99e4fdd49c1da3fd85e6313f Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 19 Dec 2024 17:26:14 +0100 Subject: [PATCH 026/155] Revert "Bump matrix-js-sdk for bundle improvements (#2911)" This reverts commit 6b8dddfaaad68fd0bd1f4f5c0c2fb7b351775ffe. --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 64d418fc..0460f4bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1969,10 +1969,10 @@ "@mediapipe/holistic" "0.5.1675471629" "@mediapipe/tasks-vision" "0.10.9" -"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a" - integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q== +"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" + integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -6603,10 +6603,10 @@ matrix-events-sdk@0.0.1: matrix-js-sdk@matrix-org/matrix-js-sdk#develop: version "34.13.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e4182eb75227c283a18704727021e99ced72868d" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1de32ea2773df4c6f8a956678bbd19b6d022475" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^12.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^11.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" From e0f4bf8eab514a96177d4b970c8ece05d238a87b Mon Sep 17 00:00:00 2001 From: Timo Date: Thu, 19 Dec 2024 17:39:34 +0100 Subject: [PATCH 027/155] working version --- src/livekit/TrackProcessorContext.tsx | 17 +++++++++++------ src/livekit/useLiveKit.ts | 8 +++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index e131dcb1..1fc0005a 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -6,8 +6,9 @@ Please see LICENSE in the repository root for full details. */ import { + BackgroundBlur as backgroundBlur, + type ProcessorWrapper, type BackgroundOptions, - ProcessorWrapper, } from "@livekit/track-processors"; import { createContext, @@ -25,7 +26,6 @@ import { backgroundBlur as backgroundBlurSettings, useSetting, } from "../settings/settings"; -import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer"; type ProcessorState = { supported: boolean | undefined; @@ -89,10 +89,15 @@ export const ProcessorProvider: FC = ({ children }) => { if (!shouldCheckSupport) return; try { if (!blur.current) { - blur.current = new ProcessorWrapper( - new BlurBackgroundTransformer({ blurRadius: 15 }), - "background-blur", - ); + // TODO: move to our own local version of the transformer. + // Currently this is broken: error when trying to pipe IndexSizeError: Failed to construct 'ImageData': The source width is zero or not a number. + // blur.current = new ProcessorWrapper( + // new BlurBackgroundTransformer({}), + // "background-blur", + // ); + + // eslint-disable-next-line new-cap + blur.current = backgroundBlur(); } setProcessorState({ checkSupported, diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index 72a972a7..aca6ed92 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -134,8 +134,14 @@ export function useLiveKit( Array.from(room.localParticipant.videoTrackPublications.values()).find( (v) => v.source === Track.Source.Camera, )?.track as LocalVideoTrack | null, - [room.localParticipant.videoTrackPublications], + [ + room.localParticipant.videoTrackPublications, + // We need to update on map changes + // eslint-disable-next-line react-hooks/exhaustive-deps + room.localParticipant.videoTrackPublications.keys(), + ], ); + useTrackProcessorSync(videoTrack); const connectionState = useECConnectionState( From bb0e1f38dc08ffea8c79c9a6678c5e8b09354bb3 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 6 Jan 2025 15:48:43 +0000 Subject: [PATCH 028/155] Exclude tasks-vision from knip --- knip.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/knip.ts b/knip.ts index 5ffe03c4..a0250354 100644 --- a/knip.ts +++ b/knip.ts @@ -29,6 +29,8 @@ export default { // element call side. "@types/dom-mediacapture-transform", "matrix-widget-api", + // we bundle files from this and lazy load them: + "@mediapipe/tasks-vision", ], ignoreExportsUsedInFile: true, } satisfies KnipConfig; From 96ea920e9892a0de713a51e7775c949e5ccb0a8c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Mon, 6 Jan 2025 15:53:31 +0000 Subject: [PATCH 029/155] Try reenabling local implementation of BlurBackgroundTransformer --- src/livekit/TrackProcessorContext.tsx | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index 1fc0005a..8edec4f5 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -6,8 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { - BackgroundBlur as backgroundBlur, - type ProcessorWrapper, + ProcessorWrapper, type BackgroundOptions, } from "@livekit/track-processors"; import { @@ -26,6 +25,7 @@ import { backgroundBlur as backgroundBlurSettings, useSetting, } from "../settings/settings"; +import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer"; type ProcessorState = { supported: boolean | undefined; @@ -89,15 +89,10 @@ export const ProcessorProvider: FC = ({ children }) => { if (!shouldCheckSupport) return; try { if (!blur.current) { - // TODO: move to our own local version of the transformer. - // Currently this is broken: error when trying to pipe IndexSizeError: Failed to construct 'ImageData': The source width is zero or not a number. - // blur.current = new ProcessorWrapper( - // new BlurBackgroundTransformer({}), - // "background-blur", - // ); - - // eslint-disable-next-line new-cap - blur.current = backgroundBlur(); + blur.current = new ProcessorWrapper( + new BlurBackgroundTransformer({}), + "background-blur", + ); } setProcessorState({ checkSupported, From 4c4d01211c55a9ac6d33b0a1e7b28b2cfa358a1c Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 8 Jan 2025 14:20:12 +0000 Subject: [PATCH 030/155] Fix @matrix-org/matrix-sdk-crypto-wasm version --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index e7bcb72e..6bafd620 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1847,10 +1847,10 @@ "@mediapipe/holistic" "0.5.1675471629" "@mediapipe/tasks-vision" "0.10.9" -"@matrix-org/matrix-sdk-crypto-wasm@^11.0.0": - version "11.0.0" - resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-11.0.0.tgz#c49a1a0d1e367d3c00a2144a4ab23caee0b1eec2" - integrity sha512-a7NUH8Kjc8hwzNCPpkOGXoceFqWJiWvA8OskXeDrKyODJuDz4yKrZ/nvgaVRfQe45Ab5UC1ZXYqaME+ChlJuqg== +"@matrix-org/matrix-sdk-crypto-wasm@^12.0.0": + version "12.0.0" + resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-wasm/-/matrix-sdk-crypto-wasm-12.0.0.tgz#e3a5150ccbb21d5e98ee3882e7057b9f17fb962a" + integrity sha512-nkkXAxUIk9UTso4TbU6Bgqsv/rJShXQXRx0ti/W+AWXHJ2HoH4sL5LsXkc7a8yYGn8tyXqxGPsYA1UeHqLwm0Q== "@matrix-org/olm@3.2.15": version "3.2.15" @@ -6443,10 +6443,10 @@ matrix-events-sdk@0.0.1: matrix-js-sdk@matrix-org/matrix-js-sdk#develop: version "34.13.0" - resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/d1de32ea2773df4c6f8a956678bbd19b6d022475" + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/e4182eb75227c283a18704727021e99ced72868d" dependencies: "@babel/runtime" "^7.12.5" - "@matrix-org/matrix-sdk-crypto-wasm" "^11.0.0" + "@matrix-org/matrix-sdk-crypto-wasm" "^12.0.0" "@matrix-org/olm" "3.2.15" another-json "^0.2.0" bs58 "^6.0.0" From 00d724a3a96df21faf643d3b8a6d5d762eccbfa0 Mon Sep 17 00:00:00 2001 From: Hugh Nimmo-Smith Date: Wed, 8 Jan 2025 14:27:19 +0000 Subject: [PATCH 031/155] Bump @livekit/track-processors to ignore empty frames --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index bbe84e90..8a66a034 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.11.0", "@livekit/components-react": "^2.0.0", - "@livekit/track-processors": "^0.3.2", + "@livekit/track-processors": "^0.3.3", "@mediapipe/tasks-vision": "^0.10.18", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^1.25.1", @@ -124,4 +124,4 @@ "resolutions": { "strip-ansi": "6.0.1" } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6bafd620..7ebda63d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1839,10 +1839,10 @@ dependencies: "@bufbuild/protobuf" "^1.10.0" -"@livekit/track-processors@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@livekit/track-processors/-/track-processors-0.3.2.tgz#eaff6a48b556c25e85f5dd2c4daf6dcf1bc3b143" - integrity sha512-4JUCzb7yIKoVsTo8J6FTzLZJHcI6DihfX/pGRDg0SOGaxprcDPrt8jaDBBTsnGBSXHeMxl2ugN+xQjdCWzLKEA== +"@livekit/track-processors@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@livekit/track-processors/-/track-processors-0.3.3.tgz#c44ec54229bf21ccb404b96c43702bc263c4f05d" + integrity sha512-C5mBWe34ie0pfhCf9zAD65D26ZytzW/xGrj8Zc1BGh5kLISFDVQlefO78LZWw9K5saTamk60O4fzSCKqWMPMLA== dependencies: "@mediapipe/holistic" "0.5.1675471629" "@mediapipe/tasks-vision" "0.10.9" From 6884b19a7336cf9aa7b8064fbb886bfee539b541 Mon Sep 17 00:00:00 2001 From: Timo Date: Sat, 5 Apr 2025 00:01:56 +0200 Subject: [PATCH 032/155] package and lock file --- package.json | 2 +- yarn.lock | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 03facac7..2041dbff 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,9 @@ "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.12.0", "@livekit/components-react": "^2.0.0", + "@livekit/protocol": "^1.33.0", "@livekit/track-processors": "^0.3.3", "@mediapipe/tasks-vision": "^0.10.18", - "@livekit/protocol": "^1.33.0", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^1.25.1", "@opentelemetry/exporter-trace-otlp-http": "^0.57.0", diff --git a/yarn.lock b/yarn.lock index c2507f4b..2216ae77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2545,6 +2545,18 @@ __metadata: languageName: node linkType: hard +"@livekit/track-processors@npm:^0.3.3": + version: 0.3.3 + resolution: "@livekit/track-processors@npm:0.3.3" + dependencies: + "@mediapipe/holistic": "npm:0.5.1675471629" + "@mediapipe/tasks-vision": "npm:0.10.9" + peerDependencies: + livekit-client: ^1.12.0 || ^2.1.0 + checksum: 10c0/a33478a340f89ae489ac2e1dcfbc1c69744943301c5f3bd22f604b63b188ef0feebe065d4cf084fcb53d9766d10c7655f8045e19549772471d52d3a976c36778 + languageName: node + linkType: hard + "@matrix-org/matrix-sdk-crypto-wasm@npm:^14.0.1": version: 14.0.1 resolution: "@matrix-org/matrix-sdk-crypto-wasm@npm:14.0.1" @@ -2559,6 +2571,27 @@ __metadata: languageName: node linkType: hard +"@mediapipe/holistic@npm:0.5.1675471629": + version: 0.5.1675471629 + resolution: "@mediapipe/holistic@npm:0.5.1675471629" + checksum: 10c0/241cacfbff10b6418a815dfb24c36d77c626f5e10c60045a3d8cdb9f89c6e346e0d356dcfe1e8bbc4d00040983b4bc426e802a45af3f7142554afda3239ff8fc + languageName: node + linkType: hard + +"@mediapipe/tasks-vision@npm:0.10.9": + version: 0.10.9 + resolution: "@mediapipe/tasks-vision@npm:0.10.9" + checksum: 10c0/38a3112bf5f073e6b15f3a1556c4d628371f1068cbe16aabeae288f43be62f8c17f4ae66590bda11be6a9aeb098736c85d102751277db5bb4b881516a23e43a5 + languageName: node + linkType: hard + +"@mediapipe/tasks-vision@npm:^0.10.18": + version: 0.10.21 + resolution: "@mediapipe/tasks-vision@npm:0.10.21" + checksum: 10c0/11b2bdf98b8cb6e044f2a954e7c8393169e62c86ff49b3d0b61c3b327d18e1ccd47a187999b023bad48380c9da41bfa66eb165301c80da07746390482cb18a19 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4612,6 +4645,22 @@ __metadata: languageName: node linkType: hard +"@types/dom-mediacapture-transform@npm:^0.1.10": + version: 0.1.11 + resolution: "@types/dom-mediacapture-transform@npm:0.1.11" + dependencies: + "@types/dom-webcodecs": "npm:*" + checksum: 10c0/19c76d54cf31aa2a925011fc5f973dff9a10bdecfdf2285e5e568e61850a0fa2b8c9f1807a1462cbefd57ec26d32eeaa9c359117aca9d9fe7f0d6f2fff33f51e + languageName: node + linkType: hard + +"@types/dom-webcodecs@npm:*": + version: 0.1.14 + resolution: "@types/dom-webcodecs@npm:0.1.14" + checksum: 10c0/20f2e9a52986f861eb5725365561c57d4d8c7494322e55f5df1b0639e97d399c3c427e05d2bc7772c9c975197dc12db3e047e3e0ad7b49ae13181514e3d54f56 + languageName: node + linkType: hard + "@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -6862,6 +6911,8 @@ __metadata: "@livekit/components-core": "npm:^0.12.0" "@livekit/components-react": "npm:^2.0.0" "@livekit/protocol": "npm:^1.33.0" + "@livekit/track-processors": "npm:^0.3.3" + "@mediapipe/tasks-vision": "npm:^0.10.18" "@opentelemetry/api": "npm:^1.4.0" "@opentelemetry/core": "npm:^1.25.1" "@opentelemetry/exporter-trace-otlp-http": "npm:^0.57.0" @@ -6882,6 +6933,7 @@ __metadata: "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.5.1" "@types/content-type": "npm:^1.1.5" + "@types/dom-mediacapture-transform": "npm:^0.1.10" "@types/grecaptcha": "npm:^3.0.9" "@types/jsdom": "npm:^21.1.7" "@types/lodash-es": "npm:^4.17.12" From 4694354f695f73dbf44c536b6caa671477ab256b Mon Sep 17 00:00:00 2001 From: fkwp Date: Tue, 15 Apr 2025 17:53:27 +0200 Subject: [PATCH 033/155] add informatino about required synapse listeners --- docs/self-hosting.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 7a515930..5fdbb143 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -63,6 +63,11 @@ rc_delayed_event_mgmt: burst_count: 20 ``` +As a prerequisites for the +[Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service) +make sure that your Synapse server has either a `federation` or `openid` +[listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners). + ### MatrixRTC Backend In order to **guarantee smooth operation** of Element Call MatrixRTC backend is From 2cf2e090f6eaab55ff978571f86060894d30ba39 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 17 Apr 2025 16:31:52 +0200 Subject: [PATCH 034/155] Update docs/self-hosting.md Co-authored-by: Robin --- docs/self-hosting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 5fdbb143..0e3e8a82 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -63,8 +63,8 @@ rc_delayed_event_mgmt: burst_count: 20 ``` -As a prerequisites for the -[Matrix Livekit JWT auth service](https://github.com/element-hq/lk-jwt-service) +As a prerequisite for the +[Matrix LiveKit JWT auth service](https://github.com/element-hq/lk-jwt-service) make sure that your Synapse server has either a `federation` or `openid` [listener configured](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#listeners). From e751248fee3a549b684448ed304b75e2610d933e Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 22 Apr 2025 16:31:44 +0200 Subject: [PATCH 035/155] Revert livekit-client to 2.10.0 Closes https://github.com/element-hq/element-call/issues/3219. --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7c2edc31..fdeda543 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "i18next-parser": "^9.1.0", "jsdom": "^26.0.0", "knip": "^5.27.2", - "livekit-client": "2.11.2", + "livekit-client": "2.10.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#64e27f5d3cdab6aafeb7c22f1264416ffa72b83f", diff --git a/yarn.lock b/yarn.lock index 95137dbd..cbcea660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6910,7 +6910,7 @@ __metadata: i18next-parser: "npm:^9.1.0" jsdom: "npm:^26.0.0" knip: "npm:^5.27.2" - livekit-client: "npm:2.11.2" + livekit-client: "npm:2.10.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" matrix-js-sdk: "github:matrix-org/matrix-js-sdk#64e27f5d3cdab6aafeb7c22f1264416ffa72b83f" @@ -9280,9 +9280,9 @@ __metadata: languageName: node linkType: hard -"livekit-client@npm:2.11.2": - version: 2.11.2 - resolution: "livekit-client@npm:2.11.2" +"livekit-client@npm:2.10.0": + version: 2.10.0 + resolution: "livekit-client@npm:2.10.0" dependencies: "@livekit/mutex": "npm:1.1.1" "@livekit/protocol": "npm:1.36.1" @@ -9293,7 +9293,7 @@ __metadata: tslib: "npm:2.8.1" typed-emitter: "npm:^2.1.0" webrtc-adapter: "npm:^9.0.1" - checksum: 10c0/03a184f3a4f81beefb47a78adb37d3ba3b95a5d1f01e2ebbb83fb3769a120f6b8726ee8ccd96eb634164eeec508850cf05d40e2330fe7c1eda0e3d7f481561f7 + checksum: 10c0/bc5d1d1e08576da3356f567836090a58dfad9b2d9ca2280584acf31676949a88d9526940ab10a9b6cf79545a49f82678cb0665f65851628e6148a9d559cacc90 languageName: node linkType: hard From b7b3fde2023eef04e8bf859f3790696a8bb6ee4b Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 22 Apr 2025 18:00:41 +0200 Subject: [PATCH 036/155] Upgrade LiveKit track processors --- package.json | 2 +- yarn.lock | 28 ++++++++++------------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 413ae6cd..f44264b3 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@livekit/components-core": "^0.12.0", "@livekit/components-react": "^2.0.0", "@livekit/protocol": "^1.33.0", - "@livekit/track-processors": "^0.3.3", + "@livekit/track-processors": "^0.5.5", "@mediapipe/tasks-vision": "^0.10.18", "@opentelemetry/api": "^1.4.0", "@opentelemetry/core": "^1.25.1", diff --git a/yarn.lock b/yarn.lock index 165e6f57..b23ad6ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2536,15 +2536,14 @@ __metadata: languageName: node linkType: hard -"@livekit/track-processors@npm:^0.3.3": - version: 0.3.3 - resolution: "@livekit/track-processors@npm:0.3.3" +"@livekit/track-processors@npm:^0.5.5": + version: 0.5.5 + resolution: "@livekit/track-processors@npm:0.5.5" dependencies: - "@mediapipe/holistic": "npm:0.5.1675471629" - "@mediapipe/tasks-vision": "npm:0.10.9" + "@mediapipe/tasks-vision": "npm:0.10.14" peerDependencies: livekit-client: ^1.12.0 || ^2.1.0 - checksum: 10c0/a33478a340f89ae489ac2e1dcfbc1c69744943301c5f3bd22f604b63b188ef0feebe065d4cf084fcb53d9766d10c7655f8045e19549772471d52d3a976c36778 + checksum: 10c0/b86737c4ce6b93c714dfc814c997d07f1d40b0869b5e269b28e00034a3c06da811d9117c4914d8a2706ac760be31f2e9d0705108c07b29f4bbb4626dcd41ed64 languageName: node linkType: hard @@ -2562,17 +2561,10 @@ __metadata: languageName: node linkType: hard -"@mediapipe/holistic@npm:0.5.1675471629": - version: 0.5.1675471629 - resolution: "@mediapipe/holistic@npm:0.5.1675471629" - checksum: 10c0/241cacfbff10b6418a815dfb24c36d77c626f5e10c60045a3d8cdb9f89c6e346e0d356dcfe1e8bbc4d00040983b4bc426e802a45af3f7142554afda3239ff8fc - languageName: node - linkType: hard - -"@mediapipe/tasks-vision@npm:0.10.9": - version: 0.10.9 - resolution: "@mediapipe/tasks-vision@npm:0.10.9" - checksum: 10c0/38a3112bf5f073e6b15f3a1556c4d628371f1068cbe16aabeae288f43be62f8c17f4ae66590bda11be6a9aeb098736c85d102751277db5bb4b881516a23e43a5 +"@mediapipe/tasks-vision@npm:0.10.14": + version: 0.10.14 + resolution: "@mediapipe/tasks-vision@npm:0.10.14" + checksum: 10c0/dea1d094153fcb91e76d1a266b6a79baf9cbdd106a786adc0236ded87099b5242c89e0e5b73a3f5d57d7e23ff657dd37f40b9261e12a330762041cd9549fadcc languageName: node linkType: hard @@ -6902,7 +6894,7 @@ __metadata: "@livekit/components-core": "npm:^0.12.0" "@livekit/components-react": "npm:^2.0.0" "@livekit/protocol": "npm:^1.33.0" - "@livekit/track-processors": "npm:^0.3.3" + "@livekit/track-processors": "npm:^0.5.5" "@mediapipe/tasks-vision": "npm:^0.10.18" "@opentelemetry/api": "npm:^1.4.0" "@opentelemetry/core": "npm:^1.25.1" From 7142122a742202392f8ddfc53998acd265452698 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 22 Apr 2025 18:05:31 +0200 Subject: [PATCH 037/155] Dedupe Mediapipe dependencies --- package.json | 1 + yarn.lock | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/package.json b/package.json index f44264b3..f091280e 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ }, "resolutions": { "@livekit/components-core/rxjs": "^7.8.1", + "@livekit/track-processors/@mediapipe/tasks-vision": "^0.10.18", "matrix-widget-api": "1.11.0" }, "packageManager": "yarn@4.7.0" diff --git a/yarn.lock b/yarn.lock index b23ad6ba..703c9ebf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2561,13 +2561,6 @@ __metadata: languageName: node linkType: hard -"@mediapipe/tasks-vision@npm:0.10.14": - version: 0.10.14 - resolution: "@mediapipe/tasks-vision@npm:0.10.14" - checksum: 10c0/dea1d094153fcb91e76d1a266b6a79baf9cbdd106a786adc0236ded87099b5242c89e0e5b73a3f5d57d7e23ff657dd37f40b9261e12a330762041cd9549fadcc - languageName: node - linkType: hard - "@mediapipe/tasks-vision@npm:^0.10.18": version: 0.10.21 resolution: "@mediapipe/tasks-vision@npm:0.10.21" From 63ad4a3ad316eb4e871461f890709398d2f698ba Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 22 Apr 2025 18:32:01 +0200 Subject: [PATCH 038/155] Get blurring working again --- src/livekit/BlurBackgroundTransformer.ts | 5 +++++ src/livekit/TrackProcessorContext.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts index 51b6a536..d57524fc 100644 --- a/src/livekit/BlurBackgroundTransformer.ts +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -54,9 +54,14 @@ export class BlurBackgroundTransformer extends BackgroundTransformer { delegate: "GPU", ...this.options.segmenterOptions, }, + canvas: this.canvas, runningMode: "VIDEO", outputCategoryMask: true, outputConfidenceMasks: false, }); + + if (this.options.blurRadius) { + this.gl?.setBlurRadius(this.options.blurRadius); + } } } diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index 8edec4f5..ba94e257 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -90,7 +90,7 @@ export const ProcessorProvider: FC = ({ children }) => { try { if (!blur.current) { blur.current = new ProcessorWrapper( - new BlurBackgroundTransformer({}), + new BlurBackgroundTransformer({ blurRadius: 15 }), "background-blur", ); } From 3fc4cc908e8d7e1c605d5996ab1a6c0724f0ae8b Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 22 Apr 2025 19:19:40 +0200 Subject: [PATCH 039/155] Cleanup + use a better check for blurring support --- src/livekit/TrackProcessorContext.tsx | 89 +++++++++------------------ src/livekit/useLiveKit.ts | 2 +- src/room/LobbyView.tsx | 2 +- src/settings/SettingsModal.tsx | 7 +-- 4 files changed, 33 insertions(+), 67 deletions(-) diff --git a/src/livekit/TrackProcessorContext.tsx b/src/livekit/TrackProcessorContext.tsx index ba94e257..b310e7b3 100644 --- a/src/livekit/TrackProcessorContext.tsx +++ b/src/livekit/TrackProcessorContext.tsx @@ -7,18 +7,10 @@ Please see LICENSE in the repository root for full details. import { ProcessorWrapper, + supportsBackgroundProcessors, type BackgroundOptions, } from "@livekit/track-processors"; -import { - createContext, - type FC, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "react"; -import { logger } from "matrix-js-sdk/src/logger"; +import { createContext, type FC, useContext, useEffect, useMemo } from "react"; import { type LocalVideoTrack } from "livekit-client"; import { @@ -30,23 +22,23 @@ import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer"; type ProcessorState = { supported: boolean | undefined; processor: undefined | ProcessorWrapper; - /** - * Call this method to try to initialize a processor. - * This only needs to happen if supported is undefined. - * If the backgroundBlur setting is set to true this does not need to be called - * and the processorState.supported will update automatically to the correct value. - */ - checkSupported: () => void; }; + const ProcessorContext = createContext(undefined); -export const useTrackProcessor = (): ProcessorState | undefined => - useContext(ProcessorContext); +export function useTrackProcessor(): ProcessorState { + const state = useContext(ProcessorContext); + if (state === undefined) + throw new Error( + "useTrackProcessor must be used within a ProcessorProvider", + ); + return state; +} export const useTrackProcessorSync = ( videoTrack: LocalVideoTrack | null, ): void => { - const { processor } = useTrackProcessor() || {}; + const { processor } = useTrackProcessor(); useEffect(() => { if (!videoTrack) return; if (processor && !videoTrack.getProcessor()) { @@ -61,53 +53,28 @@ export const useTrackProcessorSync = ( interface Props { children: JSX.Element; } + export const ProcessorProvider: FC = ({ children }) => { // The setting the user wants to have const [blurActivated] = useSetting(backgroundBlurSettings); - - // If `ProcessorState.supported` is undefined the user can activate that we want - // to have it at least checked (this is useful to show the settings menu properly) - // We dont want to try initializing the blur if the user is not even looking at the setting - const [shouldCheckSupport, setShouldCheckSupport] = useState(blurActivated); - - // Cache the processor so we only need to initialize it once. - const blur = useRef | undefined>( - undefined, + const supported = useMemo(() => supportsBackgroundProcessors(), []); + const blur = useMemo( + () => + new ProcessorWrapper( + new BlurBackgroundTransformer({ blurRadius: 15 }), + "background-blur", + ), + [], ); - const checkSupported = useCallback(() => { - setShouldCheckSupport(true); - }, []); // This is the actual state exposed through the context - const [processorState, setProcessorState] = useState(() => ({ - supported: false, - processor: undefined, - checkSupported, - })); - - useEffect(() => { - if (!shouldCheckSupport) return; - try { - if (!blur.current) { - blur.current = new ProcessorWrapper( - new BlurBackgroundTransformer({ blurRadius: 15 }), - "background-blur", - ); - } - setProcessorState({ - checkSupported, - supported: true, - processor: blurActivated ? blur.current : undefined, - }); - } catch (e) { - setProcessorState({ - checkSupported, - supported: false, - processor: undefined, - }); - logger.error("disable background blur", e); - } - }, [blurActivated, checkSupported, shouldCheckSupport]); + const processorState = useMemo( + () => ({ + supported, + processor: supported && blurActivated ? blur : undefined, + }), + [supported, blurActivated, blur], + ); return ( diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index c9bfd78e..f214a203 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -88,7 +88,7 @@ export function useLiveKit( const devices = useMediaDevices(); const initialDevices = useRef(devices); - const { processor } = useTrackProcessor() || {}; + const { processor } = useTrackProcessor(); const initialProcessor = useInitial(() => processor); const roomOptions = useMemo( (): RoomOptions => ({ diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index f3739175..e6bf5362 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -120,7 +120,7 @@ export const LobbyView: FC = ({ muteStates.audio.enabled && { deviceId: devices.audioInput.selectedId }, ); - const { processor } = useTrackProcessor() || {}; + const { processor } = useTrackProcessor(); const initialProcessor = useInitial(() => processor); const localTrackOptions = useMemo( diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 44d06116..b24674dc 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { type FC, type ReactNode, useEffect, useState } from "react"; +import { type FC, type ReactNode, useState } from "react"; import { useTranslation } from "react-i18next"; import { type MatrixClient } from "matrix-js-sdk"; -import { Root as Form ,Separator} from "@vector-im/compound-web"; +import { Root as Form, Separator } from "@vector-im/compound-web"; import { type Room as LivekitRoom } from "livekit-client"; import { Modal } from "../Modal"; @@ -69,8 +69,7 @@ export const SettingsModal: FC = ({ // Generate a `Checkbox` input to turn blur on or off. const BlurCheckbox: React.FC = (): ReactNode => { - const { supported, checkSupported } = useTrackProcessor() || {}; - useEffect(() => checkSupported?.(), [checkSupported]); + const { supported } = useTrackProcessor(); const [blurActive, setBlurActive] = useSetting(backgroundBlurSetting); From 78a41c09ded0ee4735d6e72cb89603f9cccf28d8 Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:24:09 +0200 Subject: [PATCH 040/155] Fix MatrixRTC js-sdk logs in rageshakes (#3226) --- package.json | 2 +- src/settings/rageshake.ts | 5 ----- yarn.lock | 14 +++++++------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index fdeda543..07289035 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "livekit-client": "2.10.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#64e27f5d3cdab6aafeb7c22f1264416ffa72b83f", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#19b1b901f575755d29d1fe03ca48cbf7c1cae05c", "matrix-widget-api": "1.11.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 21ed5956..6c1a0f61 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -473,11 +473,6 @@ export async function init(): Promise { // configure loglevel based loggers: setLogExtension(logger, global.mx_rage_logger.log); - // these are the child/prefixed loggers we want to capture from js-sdk - // there doesn't seem to be an easy way to capture all children - ["MatrixRTCSession", "MatrixRTCSessionManager"].forEach((loggerName) => { - setLogExtension(logger.getChild(loggerName), global.mx_rage_logger.log); - }); // intercept console logging so that we can get matrix_sdk logs: // this is nasty, but no logging hooks are provided diff --git a/yarn.lock b/yarn.lock index cbcea660..4a41df01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6913,7 +6913,7 @@ __metadata: livekit-client: "npm:2.10.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#64e27f5d3cdab6aafeb7c22f1264416ffa72b83f" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#19b1b901f575755d29d1fe03ca48cbf7c1cae05c" matrix-widget-api: "npm:1.11.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -9350,7 +9350,7 @@ __metadata: languageName: node linkType: hard -"loglevel@npm:^1.7.1, loglevel@npm:^1.9.1, loglevel@npm:^1.9.2": +"loglevel@npm:^1.9.1, loglevel@npm:^1.9.2": version: 1.9.2 resolution: "loglevel@npm:1.9.2" checksum: 10c0/1e317fa4648fe0b4a4cffef6de037340592cee8547b07d4ce97a487abe9153e704b98451100c799b032c72bb89c9366d71c9fb8192ada8703269263ae77acdc7 @@ -9504,9 +9504,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#64e27f5d3cdab6aafeb7c22f1264416ffa72b83f": - version: 37.3.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=64e27f5d3cdab6aafeb7c22f1264416ffa72b83f" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#19b1b901f575755d29d1fe03ca48cbf7c1cae05c": + version: 37.4.0 + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=19b1b901f575755d29d1fe03ca48cbf7c1cae05c" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^14.0.1" @@ -9515,7 +9515,7 @@ __metadata: bs58: "npm:^6.0.0" content-type: "npm:^1.0.4" jwt-decode: "npm:^4.0.0" - loglevel: "npm:^1.7.1" + loglevel: "npm:^1.9.2" matrix-events-sdk: "npm:0.0.1" matrix-widget-api: "npm:^1.10.0" oidc-client-ts: "npm:^3.0.1" @@ -9523,7 +9523,7 @@ __metadata: sdp-transform: "npm:^2.14.1" unhomoglyph: "npm:^1.0.6" uuid: "npm:11" - checksum: 10c0/716f89b4fffc67ae42fad55f47b96792a0c49bd6d741410758dfbe1d0e4673097627b4c8f8a77063702cb41a5e0f7f0c5ff96c109ed332d3b3bc973f4d31f555 + checksum: 10c0/68a30a113059ba052b2e66502abcd9805f9a18a1bfd1d209203d728b36508af257a57e6248fb237c7018c81bfbe1ec78fa17aea8968c8af0729ea935398dcf8b languageName: node linkType: hard From a1d6eef12a9ea3877cf942a0adb81fc6cf98037e Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Wed, 23 Apr 2025 09:22:54 +0200 Subject: [PATCH 041/155] Set `ratchetWindowSize` to > 0 enable rachet compatibility. (#3224) --- src/e2ee/matrixKeyProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e2ee/matrixKeyProvider.ts b/src/e2ee/matrixKeyProvider.ts index c5f6c879..9b190ed8 100644 --- a/src/e2ee/matrixKeyProvider.ts +++ b/src/e2ee/matrixKeyProvider.ts @@ -16,7 +16,7 @@ export class MatrixKeyProvider extends BaseKeyProvider { private rtcSession?: MatrixRTCSession; public constructor() { - super({ ratchetWindowSize: 0, keyringSize: 256 }); + super({ ratchetWindowSize: 10, keyringSize: 256 }); } public setRTCSession(rtcSession: MatrixRTCSession): void { From a6696c2304cac57110c6a12039612b4c09feb429 Mon Sep 17 00:00:00 2001 From: Robin Date: Wed, 23 Apr 2025 11:59:13 +0200 Subject: [PATCH 042/155] Return to latest livekit-client version (2.11.3) This latest release of livekit-client fixes the regression observed in https://github.com/element-hq/element-call/issues/3219. --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 07289035..3730004b 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "i18next-parser": "^9.1.0", "jsdom": "^26.0.0", "knip": "^5.27.2", - "livekit-client": "2.10.0", + "livekit-client": "^2.11.3", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#19b1b901f575755d29d1fe03ca48cbf7c1cae05c", diff --git a/yarn.lock b/yarn.lock index 4a41df01..70de0fc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6910,7 +6910,7 @@ __metadata: i18next-parser: "npm:^9.1.0" jsdom: "npm:^26.0.0" knip: "npm:^5.27.2" - livekit-client: "npm:2.10.0" + livekit-client: "npm:^2.11.3" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" matrix-js-sdk: "github:matrix-org/matrix-js-sdk#19b1b901f575755d29d1fe03ca48cbf7c1cae05c" @@ -9280,9 +9280,9 @@ __metadata: languageName: node linkType: hard -"livekit-client@npm:2.10.0": - version: 2.10.0 - resolution: "livekit-client@npm:2.10.0" +"livekit-client@npm:^2.11.3": + version: 2.11.3 + resolution: "livekit-client@npm:2.11.3" dependencies: "@livekit/mutex": "npm:1.1.1" "@livekit/protocol": "npm:1.36.1" @@ -9293,7 +9293,7 @@ __metadata: tslib: "npm:2.8.1" typed-emitter: "npm:^2.1.0" webrtc-adapter: "npm:^9.0.1" - checksum: 10c0/bc5d1d1e08576da3356f567836090a58dfad9b2d9ca2280584acf31676949a88d9526940ab10a9b6cf79545a49f82678cb0665f65851628e6148a9d559cacc90 + checksum: 10c0/d56444f31c107b46ccd5532038ac77bd21038042910619008267c17894f1d3f054262ae2354d89df6fe0ba325aba01909b0612ad4c290906487c40d91641f6e4 languageName: node linkType: hard From 09177b90d3a9abdf1eec9e76c9e1f10fc9fa1ab1 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 24 Apr 2025 23:40:32 +0200 Subject: [PATCH 043/155] added matrix-rtc server to route jwt and sfu traffic. renamed file to dev_nginx.conf --- ...ls_localhost_nginx.conf => dev_nginx.conf} | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) rename backend/{tls_localhost_nginx.conf => dev_nginx.conf} (53%) diff --git a/backend/tls_localhost_nginx.conf b/backend/dev_nginx.conf similarity index 53% rename from backend/tls_localhost_nginx.conf rename to backend/dev_nginx.conf index 2a593210..e4d665d6 100644 --- a/backend/tls_localhost_nginx.conf +++ b/backend/dev_nginx.conf @@ -16,9 +16,9 @@ server { # currently rely for local development environment on deprecated config.json # setting for livekit_service_url location /.well-known/matrix/client { - return 200 '{"m.homeserver": {"base_url": "http://synapse.localhost:8008"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "http://localhost:8080"}]}'; - default_type application/json; add_header Access-Control-Allow-Origin *; + return 200 '{"m.homeserver": {"base_url": "http://synapse.localhost:8008"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "http://matrix-rtc.localhost:8008/livekit/jwt"}]}'; + default_type application/json; } # Reverse proxy for Matrix Synapse Homeserver @@ -38,3 +38,48 @@ server { error_page 500 502 503 504 /50x.html; } + +server { + listen 80; + listen [::]:80; + listen 443 ssl; + listen 8448 ssl; + listen [::]:443 ssl; + listen [::]:8448 ssl; + server_name matrix-rtc.localhost; + ssl_certificate /root/ssl/cert.pem; + ssl_certificate_key /root/ssl/key.pem; + + + location ^~ /livekit/jwt/ { + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # JWT Service running at port 8080 + proxy_pass http://auth-server:8080/; + } + + location ^~ /livekit/sfu/ { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_send_timeout 120; + proxy_read_timeout 120; + proxy_buffering off; + + proxy_set_header Accept-Encoding gzip; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # LiveKit SFU websocket connection running at port 7880 + proxy_pass http://livekit-sfu:7880/; + } + + error_page 500 502 503 504 /50x.html; + +} From 1ae876de26260fad6f14b2f8b92b8ee52fbb711b Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 24 Apr 2025 23:40:58 +0200 Subject: [PATCH 044/155] remove legacy livekit key --- config/config.devenv.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/config.devenv.json b/config/config.devenv.json index cebca705..b0e38ee4 100644 --- a/config/config.devenv.json +++ b/config/config.devenv.json @@ -5,9 +5,6 @@ "server_name": "synapse.localhost" } }, - "livekit": { - "livekit_service_url": "http://localhost:8009" - }, "features": { "feature_use_device_session_member_events": true }, From 8ad1d60975303de635dfd53b397ce6abc85ff83f Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 24 Apr 2025 23:45:53 +0200 Subject: [PATCH 045/155] remove NOTE wrt. legacy livekit key from README.md --- README.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.md b/README.md index 8ac32d80..4d843d55 100644 --- a/README.md +++ b/README.md @@ -192,11 +192,6 @@ To use it, create a local config by, e.g., The `config.devenv.json` config should work with the backend development environment as outlined in the next section out of box. -> [!NOTE] -> Be aware, that this `config.devenv.json` is exposing a deprecated fallback -> LiveKit config key. If the homeserver advertises SFU backend via -> `.well-known/matrix/client` this has precedence. - You're now ready to launch the development server: ```sh From b61bed019778ccea72acfd27eb08be695f4920f6 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 24 Apr 2025 23:46:45 +0200 Subject: [PATCH 046/155] cleanup used ports, reflect endpoint routing of JWT service and SFU websocket connection --- dev-backend-docker-compose.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 8d70bfc2..38853054 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -7,7 +7,7 @@ services: hostname: auth-server environment: - LK_JWT_PORT=8080 - - LIVEKIT_URL=ws://localhost:7880 + - LIVEKIT_URL=ws://matrix-rtc.localhost/livekit/sfu - LIVEKIT_KEY=devkey - LIVEKIT_SECRET=secret # If the configured homeserver runs on localhost, it'll probably be using @@ -18,12 +18,13 @@ services: condition: on-failure ports: # HOST_PORT:CONTAINER_PORT - - 8009:8080 + - 8080:8080 networks: - ecbackend livekit: image: livekit/livekit-server:latest + hostname: livekit-sfu command: --dev --config /etc/livekit.yaml restart: unless-stopped # The SFU seems to work far more reliably when we let it share the host @@ -85,11 +86,13 @@ services: hostname: synapse.localhost image: nginx:latest volumes: - - ./backend/tls_localhost_nginx.conf:/etc/nginx/conf.d/default.conf:Z + - ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z - ./backend/tls_localhost_key.pem:/root/ssl/key.pem:Z - ./backend/tls_localhost_cert.pem:/root/ssl/cert.pem:Z ports: # HOST_PORT:CONTAINER_PORT + - "80:80" + - "443:443" - "8008:80" - "4443:443" depends_on: From 9416c41d6215a70caaf4cc76fb854fa5dee68bb4 Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 25 Apr 2025 00:09:39 +0200 Subject: [PATCH 047/155] added note about certificate exceptions --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 4d843d55..db7cc43f 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,13 @@ yarn backend # podman-compose -f dev-backend-docker-compose.yml up ``` +> [!NOTE] +> To ensure your local development frontend functions properly, you’ll need to add +> certificate exceptions in your browser for both `https://localhost:3000` and +> `https://synapse.localhost/.well-known/matrix/client`. The easiest way to do this +> is to simply copy and paste each URL into your browser’s address bar and follow +> the prompts to add the exception. + ### Playwright tests Our Playwright tests run automatically as part of our CI along with our other From de4875ac7ccdf82bd62d7a9a1148865d275a9d98 Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 25 Apr 2025 00:31:13 +0200 Subject: [PATCH 048/155] replace jsonc with nginx highlighting --- docs/self-hosting.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 0e3e8a82..4946c3a1 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -93,7 +93,7 @@ the example above, this results in: Using Nginx, you can achieve this by: -```jsonc +```nginx configuration file` server { ... location ^~ /livekit/jwt/ { @@ -203,7 +203,7 @@ Because Element Call uses client-side routing, your server must be able to route any requests to non-existing paths back to `/index.html`. For example, in Nginx you can achieve this with the `try_files` directive: -```jsonc +```nginx configuration file server { ... location / { From 984383b60d2fbe7ad40487975bd7dfd9871903fd Mon Sep 17 00:00:00 2001 From: fkwp Date: Fri, 25 Apr 2025 00:32:19 +0200 Subject: [PATCH 049/155] typo --- docs/self-hosting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/self-hosting.md b/docs/self-hosting.md index 4946c3a1..85ace615 100644 --- a/docs/self-hosting.md +++ b/docs/self-hosting.md @@ -93,7 +93,7 @@ the example above, this results in: Using Nginx, you can achieve this by: -```nginx configuration file` +```nginx configuration file server { ... location ^~ /livekit/jwt/ { From c91cc79162ddde77275a189f0d9d9c9a76d76e41 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 15:56:26 +0200 Subject: [PATCH 050/155] Remove unneeded Knip override for MediaPipe --- knip.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/knip.ts b/knip.ts index 4732aea3..f1189ca8 100644 --- a/knip.ts +++ b/knip.ts @@ -32,8 +32,6 @@ export default { // element call side. "@types/dom-mediacapture-transform", "matrix-widget-api", - // we bundle files from this and lazy load them: - "@mediapipe/tasks-vision", ], ignoreExportsUsedInFile: true, } satisfies KnipConfig; From e88e0b19019089ab63c82b17e017d9d6c99ef343 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 15:58:50 +0200 Subject: [PATCH 051/155] Import the segmenter model in a more statically-analyzable way --- src/livekit/BlurBackgroundTransformer.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts index d57524fc..0ff363c2 100644 --- a/src/livekit/BlurBackgroundTransformer.ts +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -1,5 +1,5 @@ /* -Copyright 2024 New Vector Ltd. +Copyright 2024-2025 New Vector Ltd. SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. @@ -12,6 +12,8 @@ import { } from "@livekit/track-processors"; import { ImageSegmenter } from "@mediapipe/tasks-vision"; +import modelAssetPath from "../mediapipe/imageSegmenter/selfie_segmenter.tflite?url"; + interface WasmFileset { /** The path to the Wasm loader script. */ wasmLoaderPath: string; @@ -32,11 +34,6 @@ const wasmFileset: WasmFileset = { ).href, }; -const modelAssetPath = new URL( - "../mediapipe/imageSegmenter/selfie_segmenter.tflite", - import.meta.url, -).href; - export class BlurBackgroundTransformer extends BackgroundTransformer { public async init({ outputCanvas, From 02ce1f701aed930c1e3dbd7e9c65d499c5124cb0 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 15:59:32 +0200 Subject: [PATCH 052/155] Document the background transformer a bit better --- src/livekit/BlurBackgroundTransformer.ts | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/livekit/BlurBackgroundTransformer.ts b/src/livekit/BlurBackgroundTransformer.ts index 0ff363c2..72256fe1 100644 --- a/src/livekit/BlurBackgroundTransformer.ts +++ b/src/livekit/BlurBackgroundTransformer.ts @@ -21,8 +21,16 @@ interface WasmFileset { wasmBinaryPath: string; } -// n.b. this only includes the SIMD versions of the WASM files which have good support: -// https://caniuse.com/?search=simd +// The MediaPipe package, by default, ships some alternative versions of the +// WASM files which avoid SIMD for compatibility with older browsers. But SIMD +// in WASM is actually fine by our support policy, so we include just the SIMD +// versions. +// It's really not ideal that we have to reference these internal files from +// MediaPipe and depend on node_modules having this specific structure. It's +// easy to see this breaking if our dependencies changed and MediaPipe were +// no longer hoisted, or if we switched to another dependency loader such as +// Yarn PnP. +// https://github.com/google-ai-edge/mediapipe/issues/5961 const wasmFileset: WasmFileset = { wasmLoaderPath: new URL( "../../node_modules/@mediapipe/tasks-vision/wasm/vision_wasm_internal.js", @@ -34,12 +42,20 @@ const wasmFileset: WasmFileset = { ).href, }; +/** + * Track processor that applies effects such as blurring to a user's background. + * + * This is just like LiveKit's prebuilt BackgroundTransformer except that it + * loads the segmentation models from our own bundle rather than as an external + * resource fetched from the public internet. + */ export class BlurBackgroundTransformer extends BackgroundTransformer { public async init({ outputCanvas, inputElement: inputVideo, }: VideoTransformerInitOptions): Promise { - // call super.super.init() + // Call super.super.init() since we're totally replacing the init method of + // BackgroundTransformer here, rather than extending it await VideoTransformer.prototype.init.call(this, { outputCanvas, inputElement: inputVideo, From cd9035b5fca34ed6cfaea038164f4e52d6c9cfba Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 16:00:03 +0200 Subject: [PATCH 053/155] Make background blurring actually react to track restarts --- src/livekit/useLiveKit.ts | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLiveKit.ts index f214a203..99eda021 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLiveKit.ts @@ -9,7 +9,7 @@ import { ConnectionState, type E2EEManagerOptions, ExternalE2EEKeyProvider, - type LocalVideoTrack, + LocalVideoTrack, Room, type RoomOptions, Track, @@ -18,6 +18,8 @@ import { useEffect, useMemo, useRef } from "react"; import E2EEWorker from "livekit-client/e2ee-worker?worker"; import { logger } from "matrix-js-sdk/lib/logger"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; +import { useObservable, useObservableEagerState } from "observable-hooks"; +import { map } from "rxjs"; import { defaultLiveKitOptions } from "./options"; import { type SFUConfig } from "./openIDSFU"; @@ -39,6 +41,7 @@ import { useTrackProcessorSync, } from "./TrackProcessorContext"; import { useInitial } from "../useInitial"; +import { observeTrackReference$ } from "../state/MediaViewModel"; interface UseLivekitResult { livekitRoom?: Room; @@ -132,21 +135,25 @@ export function useLiveKit( return r; }, [roomOptions, e2eeSystem]); - const videoTrack = useMemo( - () => - Array.from(room.localParticipant.videoTrackPublications.values()).find( - (v) => v.source === Track.Source.Camera, - )?.track as LocalVideoTrack | null, - [ - room.localParticipant.videoTrackPublications, - // We need to update on map changes - // eslint-disable-next-line react-hooks/exhaustive-deps - room.localParticipant.videoTrackPublications.keys(), - ], + // Sync the requested track processors with LiveKit + useTrackProcessorSync( + useObservableEagerState( + useObservable( + (room$) => + observeTrackReference$( + room$.pipe(map(([room]) => room.localParticipant)), + Track.Source.Camera, + ).pipe( + map((trackRef) => { + const track = trackRef?.publication?.track; + return track instanceof LocalVideoTrack ? track : null; + }), + ), + [room], + ), + ), ); - useTrackProcessorSync(videoTrack); - const connectionState = useECConnectionState( { deviceId: initialDevices.current.audioInput.selectedId, From 7d0117903ebff92748988b43d0108571bac9e9d0 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 16:01:34 +0200 Subject: [PATCH 054/155] Don't lie about the type of the video track --- src/room/LobbyView.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/room/LobbyView.tsx b/src/room/LobbyView.tsx index e6bf5362..0cabc645 100644 --- a/src/room/LobbyView.tsx +++ b/src/room/LobbyView.tsx @@ -156,10 +156,12 @@ export const LobbyView: FC = ({ const tracks = usePreviewTracks(localTrackOptions, onError); - const videoTrack = useMemo(() => { - const track = tracks?.find((t) => t.kind === Track.Kind.Video); - return track as LocalVideoTrack | null; - }, [tracks]); + const videoTrack = useMemo( + () => + (tracks?.find((t) => t.kind === Track.Kind.Video) ?? + null) as LocalVideoTrack | null, + [tracks], + ); useTrackProcessorSync(videoTrack); const showSwitchCamera = useShowSwitchCamera( useObservable( From 8f1761442b2e554b39c33ba0ab2604a24e83c5d1 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 16:37:53 +0200 Subject: [PATCH 055/155] Fix tests --- src/room/GroupCallView.test.tsx | 34 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 76765270..75961c72 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -38,6 +38,7 @@ import { GroupCallView } from "./GroupCallView"; import { type WidgetHelpers } from "../widget"; import { LazyEventEmitter } from "../LazyEventEmitter"; import { MatrixRTCFocusMissingError } from "../utils/errors"; +import { ProcessorProvider } from "../livekit/TrackProcessorContext"; vi.mock("../soundUtils"); vi.mock("../useAudioContext"); @@ -46,6 +47,13 @@ vi.mock("react-use-measure", () => ({ default: (): [() => void, object] => [(): void => {}, {}], })); +vi.hoisted( + () => + (global.ImageData = class MockImageData { + public data: number[] = []; + } as unknown as typeof ImageData), +); + const enterRTCSession = vi.hoisted(() => vi.fn(async () => Promise.resolve())); const leaveRTCSession = vi.hoisted(() => vi.fn( @@ -137,18 +145,20 @@ function createGroupCallView( const { getByText } = render( - + + + , ); From 0b82dcf701d4df6d91b6523c7dd7b71be1646510 Mon Sep 17 00:00:00 2001 From: Robin Date: Mon, 28 Apr 2025 17:44:22 +0200 Subject: [PATCH 056/155] Upgrade TypeScript for ImageBufferSource support --- package.json | 6 +-- yarn.lock | 134 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 100 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 0650d263..2b570028 100644 --- a/package.json +++ b/package.json @@ -73,8 +73,8 @@ "@types/react-dom": "^18.3.0", "@types/sdp-transform": "^2.4.5", "@types/uuid": "10", - "@typescript-eslint/eslint-plugin": "^8.0.0", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/eslint-plugin": "^8.31.0", + "@typescript-eslint/parser": "^8.31.0", "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^3.0.0", "@vector-im/compound-web": "^7.2.0", @@ -121,7 +121,7 @@ "react-use-measure": "^2.1.1", "rxjs": "^7.8.1", "sass": "^1.42.1", - "typescript": "^5.1.6", + "typescript": "^5.8.3", "typescript-eslint-language-service": "^5.0.5", "unique-names-generator": "^4.6.0", "vaul": "^1.0.0", diff --git a/yarn.lock b/yarn.lock index ab0d017f..d7a49a1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4831,15 +4831,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:^8.0.0": - version: 8.25.0 - resolution: "@typescript-eslint/eslint-plugin@npm:8.25.0" +"@typescript-eslint/eslint-plugin@npm:^8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.31.0" dependencies: "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.25.0" - "@typescript-eslint/type-utils": "npm:8.25.0" - "@typescript-eslint/utils": "npm:8.25.0" - "@typescript-eslint/visitor-keys": "npm:8.25.0" + "@typescript-eslint/scope-manager": "npm:8.31.0" + "@typescript-eslint/type-utils": "npm:8.31.0" + "@typescript-eslint/utils": "npm:8.31.0" + "@typescript-eslint/visitor-keys": "npm:8.31.0" graphemer: "npm:^1.4.0" ignore: "npm:^5.3.1" natural-compare: "npm:^1.4.0" @@ -4847,8 +4847,8 @@ __metadata: peerDependencies: "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10c0/11d63850f5f03b29cd31166f8da111788dc74e46877c2e16a5c488d6c4aa4b6c68c0857b9a396ad920aa7f0f3e7166f4faecbb194c19cd2bb9d3f687c5d2b292 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/7d78e0cdcc967742752d49d2d38986ee38d0b7ca64af247e5fe0816cea9ae5f1bfa5c126154acc0846af515c4fb1c52c96926ee25c73b4c3f7e6fd73cb6d2b0e languageName: node linkType: hard @@ -4863,19 +4863,19 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^8.0.0": - version: 8.25.0 - resolution: "@typescript-eslint/parser@npm:8.25.0" +"@typescript-eslint/parser@npm:^8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/parser@npm:8.31.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.25.0" - "@typescript-eslint/types": "npm:8.25.0" - "@typescript-eslint/typescript-estree": "npm:8.25.0" - "@typescript-eslint/visitor-keys": "npm:8.25.0" + "@typescript-eslint/scope-manager": "npm:8.31.0" + "@typescript-eslint/types": "npm:8.31.0" + "@typescript-eslint/typescript-estree": "npm:8.31.0" + "@typescript-eslint/visitor-keys": "npm:8.31.0" debug: "npm:^4.3.4" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10c0/9a54539ba297791f23093ff42a885cc57d36b26205d7a390e114d1f01cc584ce91ac6ead01819daa46b48f873cac6c829fcf399a436610bdbfa98e5cd78148a2 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/9bd903b3ea4e24bfeb444d7a5c2ed82e591ef5cffc0874c609de854c05d34935cd85543e66678ecdb8e0e3eae2cda2df5c1ba66eb72010632cb9f8779031d56d languageName: node linkType: hard @@ -4899,18 +4899,28 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.25.0": - version: 8.25.0 - resolution: "@typescript-eslint/type-utils@npm:8.25.0" +"@typescript-eslint/scope-manager@npm:8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/scope-manager@npm:8.31.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.25.0" - "@typescript-eslint/utils": "npm:8.25.0" + "@typescript-eslint/types": "npm:8.31.0" + "@typescript-eslint/visitor-keys": "npm:8.31.0" + checksum: 10c0/eae758a24cc578fa351b8bf0c30c50de384292c0b05a58762f9b632d65a009bd5d902d806eccb6b678cc0b09686289fb4f1fd67da7f12d59ad43ff033b35cc4f + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/type-utils@npm:8.31.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:8.31.0" + "@typescript-eslint/utils": "npm:8.31.0" debug: "npm:^4.3.4" ts-api-utils: "npm:^2.0.1" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10c0/b7477a2d239cfd337f7d28641666763cf680a43a8d377a09dc42415f715670d35fbb4e772e103dfe8cd620c377e66bce740106bb3983ee65a739c28fab7325d1 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/f6938413a583430468b259f6823bb2ab1b5cd77cd6d4e21e1803df70e329046b9579aed5bdc9bdcf4046c8091615a911ac3990859db78d00210bb867915ba37f languageName: node linkType: hard @@ -4928,6 +4938,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/types@npm:8.31.0" + checksum: 10c0/04130a30aac477d36d6a155399b27773457aeb9b485ef8fb56fee05725b6e36768c9fac7e4d1f073fd16988de0eb7dffc743c3f834ae907cf918cabb075e5cd8 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" @@ -4964,6 +4981,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.31.0" + dependencies: + "@typescript-eslint/types": "npm:8.31.0" + "@typescript-eslint/visitor-keys": "npm:8.31.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^2.0.1" + peerDependencies: + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/0ec074b2b9c49f80fafea716aa0cc4b05085e65730a3ef7c7d2d39db1657a40b38abe83f22bbe15ac4f6fdf576692f47d2d057347242e6cef5be81d070f55064 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -4982,7 +5017,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.25.0, @typescript-eslint/utils@npm:^8.13.0": +"@typescript-eslint/utils@npm:8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/utils@npm:8.31.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.31.0" + "@typescript-eslint/types": "npm:8.31.0" + "@typescript-eslint/typescript-estree": "npm:8.31.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.9.0" + checksum: 10c0/1fd4f62e16a44a5be2de501f70ba4b2d64479e014370bde7bbc6de6897cf1699766a8b7be4deb9b0328e74c2b4171839336ede4e3c60fec6ac8378b623a75275 + languageName: node + linkType: hard + +"@typescript-eslint/utils@npm:^8.13.0": version: 8.25.0 resolution: "@typescript-eslint/utils@npm:8.25.0" dependencies: @@ -5017,6 +5067,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.31.0": + version: 8.31.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.31.0" + dependencies: + "@typescript-eslint/types": "npm:8.31.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10c0/e41e2a9e287d11232cda6126377d1df4de69c6e9dc2a14058819cff15280ec654a3877886a6806728196f299766cfbb0b299eb021c2ce168eb15dff5eb07b51b + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -6920,8 +6980,8 @@ __metadata: "@types/react-dom": "npm:^18.3.0" "@types/sdp-transform": "npm:^2.4.5" "@types/uuid": "npm:10" - "@typescript-eslint/eslint-plugin": "npm:^8.0.0" - "@typescript-eslint/parser": "npm:^8.0.0" + "@typescript-eslint/eslint-plugin": "npm:^8.31.0" + "@typescript-eslint/parser": "npm:^8.31.0" "@use-gesture/react": "npm:^10.2.11" "@vector-im/compound-design-tokens": "npm:^3.0.0" "@vector-im/compound-web": "npm:^7.2.0" @@ -6968,7 +7028,7 @@ __metadata: react-use-measure: "npm:^2.1.1" rxjs: "npm:^7.8.1" sass: "npm:^1.42.1" - typescript: "npm:^5.1.6" + typescript: "npm:^5.8.3" typescript-eslint-language-service: "npm:^5.0.5" unique-names-generator: "npm:^4.6.0" vaul: "npm:^1.0.0" @@ -12709,13 +12769,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.1.6": - version: 5.7.3 - resolution: "typescript@npm:5.7.3" +"typescript@npm:^5.8.3": + version: 5.8.3 + resolution: "typescript@npm:5.8.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/b7580d716cf1824736cc6e628ab4cd8b51877408ba2be0869d2866da35ef8366dd6ae9eb9d0851470a39be17cbd61df1126f9e211d8799d764ea7431d5435afa + checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48 languageName: node linkType: hard @@ -12729,13 +12789,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.1.6#optional!builtin": - version: 5.7.3 - resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5786d5" +"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": + version: 5.8.3 + resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 10c0/6fd7e0ed3bf23a81246878c613423730c40e8bdbfec4c6e4d7bf1b847cbb39076e56ad5f50aa9d7ebd89877999abaee216002d3f2818885e41c907caaa192cc4 + checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb languageName: node linkType: hard From 9df0f581178885d50dfbcce40168348e9b6c1dc7 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 29 Apr 2025 12:04:08 +0200 Subject: [PATCH 057/155] Inline a patched version of WebCodecs types --- knip.ts | 4 - package.json | 1 - src/@types/dom-mediacapture-transform.d.ts | 137 ++++ src/@types/dom-webcodecs.d.ts | 736 +++++++++++++++++++++ yarn.lock | 17 - 5 files changed, 873 insertions(+), 22 deletions(-) create mode 100644 src/@types/dom-mediacapture-transform.d.ts create mode 100644 src/@types/dom-webcodecs.d.ts diff --git a/knip.ts b/knip.ts index f1189ca8..05bd029d 100644 --- a/knip.ts +++ b/knip.ts @@ -27,10 +27,6 @@ export default { // then Knip will flag it as a false positive // https://github.com/webpro-nl/knip/issues/766 "@vector-im/compound-web", - // We need this so the eslint is happy with @livekit/track-processors. - // This might be a bug in the livekit repo but for now we fix it on the - // element call side. - "@types/dom-mediacapture-transform", "matrix-widget-api", ], ignoreExportsUsedInFile: true, diff --git a/package.json b/package.json index 2b570028..36c9c9cf 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.1", "@types/content-type": "^1.1.5", - "@types/dom-mediacapture-transform": "^0.1.10", "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", diff --git a/src/@types/dom-mediacapture-transform.d.ts b/src/@types/dom-mediacapture-transform.d.ts new file mode 100644 index 00000000..ac55dc90 --- /dev/null +++ b/src/@types/dom-mediacapture-transform.d.ts @@ -0,0 +1,137 @@ +/* eslint-disable */ +// The contents of this file below the line are copied from +// @types/dom-mediacapture-transform, which is inlined here into Element Call so +// that we can apply the patch to @types/dom-webcodecs found in +// ./dom-webcodecs.d.ts, which it depends on. +// (https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72625) +// Once that PR is merged and released, we can remove this file and return to +// depending on @types/dom-mediacapture-transform. +// ----------------------------------------------------------------------------- + +// In general, these types are only available behind a command line flag or an origin trial in +// Chrome 90+. + +// This API depends on WebCodecs. + +// Versioning: +// Until the above-mentioned spec is finalized, the major version number is 0. Although not +// necessary for version 0, consider incrementing the minor version number for breaking changes. + +// The following modify existing DOM types to allow defining type-safe APIs on audio and video tracks. + +/** Specialize MediaStreamTrack so that we can refer specifically to an audio track. */ +interface MediaStreamAudioTrack extends MediaStreamTrack { + readonly kind: "audio"; + clone(): MediaStreamAudioTrack; +} + +/** Specialize MediaStreamTrack so that we can refer specifically to a video track. */ +interface MediaStreamVideoTrack extends MediaStreamTrack { + readonly kind: "video"; + clone(): MediaStreamVideoTrack; +} + +/** Assert that getAudioTracks and getVideoTracks return the tracks with the appropriate kind. */ +interface MediaStream { + getAudioTracks(): MediaStreamAudioTrack[]; + getVideoTracks(): MediaStreamVideoTrack[]; +} + +// The following were originally generated from the spec using +// https://github.com/microsoft/TypeScript-DOM-lib-generator, then heavily modified. + +/** + * A track sink that is capable of exposing the unencoded frames from the track to a + * ReadableStream, and exposes a control channel for signals going in the oppposite direction. + */ +interface MediaStreamTrackProcessor { + /** + * Allows reading the frames flowing through the MediaStreamTrack provided to the constructor. + */ + readonly readable: ReadableStream; + /** Allows sending control signals to the MediaStreamTrack provided to the constructor. */ + readonly writableControl: WritableStream; +} + +declare var MediaStreamTrackProcessor: { + prototype: MediaStreamTrackProcessor; + + /** Constructor overrides based on the type of track. */ + new ( + init: MediaStreamTrackProcessorInit & { track: MediaStreamAudioTrack }, + ): MediaStreamTrackProcessor; + new ( + init: MediaStreamTrackProcessorInit & { track: MediaStreamVideoTrack }, + ): MediaStreamTrackProcessor; +}; + +interface MediaStreamTrackProcessorInit { + track: MediaStreamTrack; + /** + * If media frames are not read from MediaStreamTrackProcessor.readable quickly enough, the + * MediaStreamTrackProcessor will internally buffer up to maxBufferSize of the frames produced + * by the track. If the internal buffer is full, each time the track produces a new frame, the + * oldest frame in the buffer will be dropped and the new frame will be added to the buffer. + */ + maxBufferSize?: number | undefined; +} + +/** + * Takes video frames as input, and emits control signals that result from subsequent processing. + */ +interface MediaStreamTrackGenerator + extends MediaStreamTrack { + /** + * Allows writing media frames to the MediaStreamTrackGenerator, which is itself a + * MediaStreamTrack. When a frame is written to writable, the frame’s close() method is + * automatically invoked, so that its internal resources are no longer accessible from + * JavaScript. + */ + readonly writable: WritableStream; + /** + * Allows reading control signals sent from any sinks connected to the + * MediaStreamTrackGenerator. + */ + readonly readableControl: ReadableStream; +} + +type MediaStreamAudioTrackGenerator = MediaStreamTrackGenerator & + MediaStreamAudioTrack; +type MediaStreamVideoTrackGenerator = MediaStreamTrackGenerator & + MediaStreamVideoTrack; + +declare var MediaStreamTrackGenerator: { + prototype: MediaStreamTrackGenerator; + + /** Constructor overrides based on the type of track. */ + new ( + init: MediaStreamTrackGeneratorInit & { + kind: "audio"; + signalTarget?: MediaStreamAudioTrack | undefined; + }, + ): MediaStreamAudioTrackGenerator; + new ( + init: MediaStreamTrackGeneratorInit & { + kind: "video"; + signalTarget?: MediaStreamVideoTrack | undefined; + }, + ): MediaStreamVideoTrackGenerator; +}; + +interface MediaStreamTrackGeneratorInit { + kind: MediaStreamTrackGeneratorKind; + /** + * (Optional) track to which the MediaStreamTrackGenerator will automatically forward control + * signals. If signalTarget is provided and signalTarget.kind and kind do not match, the + * MediaStreamTrackGenerator’s constructor will raise an exception. + */ + signalTarget?: MediaStreamTrack | undefined; +} + +type MediaStreamTrackGeneratorKind = "audio" | "video"; + +type MediaStreamTrackSignalType = "request-frame"; + +interface MediaStreamTrackSignal { + signalType: MediaStreamTrackSignalType; +} diff --git a/src/@types/dom-webcodecs.d.ts b/src/@types/dom-webcodecs.d.ts new file mode 100644 index 00000000..f06ceedd --- /dev/null +++ b/src/@types/dom-webcodecs.d.ts @@ -0,0 +1,736 @@ +/* eslint-disable */ +// The contents of this file below the line are copied from +// @types/dom-webcodecs, which is inlined here into Element Call so that we can +// apply the patch https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72625 +// which is needed for TypeScript 5.8 compatibility. Once that PR is merged and +// released, we can remove this file and return to depending on +// @types/dom-webcodecs. +// ----------------------------------------------------------------------------- + +// Versioning: +// Until the WebCodecs spec is finalized, the major version number is 0. I have chosen to use minor +// version 1 to denote the API as defined by the IDL files from the Chromium repo at +// https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs. +// Please use a version number above 0.1 if using the spec at https://w3c.github.io/webcodecs/ as +// the source. + +// The declarations in webcodecs.generated.d.ts have been generated using the code in +// https://github.com/yume-chan/webcodecs-lib-generator. See +// https://github.com/yume-chan/webcodecs-lib-generator/blob/main/README.md for more detail. + +// The following declarations are copied from +// https://github.com/microsoft/TypeScript-DOM-lib-generator/blob/a75338e1ea8a958bf08a5745141d2ab8f14ba2ca/baselines/dom.generated.d.ts +// and modified to expand the types to include VideoFrame. + +/** Shim for OffscreenCanvas, which was removed in TS 4.4 */ +interface OffscreenCanvas extends EventTarget {} + +/** + * Replaces CanvasImageSource; only applies if WebCodecs is available. + */ +type CanvasImageSourceWebCodecs = + | HTMLOrSVGImageElement + | HTMLVideoElement + | HTMLCanvasElement + | ImageBitmap + | OffscreenCanvas + | VideoFrame; + +interface CanvasRenderingContext2D { + drawImage(image: CanvasImageSourceWebCodecs, dx: number, dy: number): void; + drawImage( + image: CanvasImageSourceWebCodecs, + dx: number, + dy: number, + dw: number, + dh: number, + ): void; + drawImage( + image: CanvasImageSourceWebCodecs, + sx: number, + sy: number, + sw: number, + sh: number, + dx: number, + dy: number, + dw: number, + dh: number, + ): void; + createPattern( + image: CanvasImageSourceWebCodecs, + repetition: string | null, + ): CanvasPattern | null; +} + +interface OffscreenCanvasRenderingContext2D { + drawImage(image: CanvasImageSourceWebCodecs, dx: number, dy: number): void; + drawImage( + image: CanvasImageSourceWebCodecs, + dx: number, + dy: number, + dw: number, + dh: number, + ): void; + drawImage( + image: CanvasImageSourceWebCodecs, + sx: number, + sy: number, + sw: number, + sh: number, + dx: number, + dy: number, + dw: number, + dh: number, + ): void; + createPattern( + image: CanvasImageSourceWebCodecs, + repetition: string | null, + ): CanvasPattern | null; +} + +/** + * Replaces ImageBitmapSource; only applies if WebCodecs is available. + */ +type ImageBitmapSourceWebCodecs = CanvasImageSourceWebCodecs | Blob | ImageData; + +declare function createImageBitmap( + image: ImageBitmapSourceWebCodecs, + options?: ImageBitmapOptions, +): Promise; +declare function createImageBitmap( + image: ImageBitmapSourceWebCodecs, + sx: number, + sy: number, + sw: number, + sh: number, + options?: ImageBitmapOptions, +): Promise; + +/** + * Replaces TexImageSource; only applies if WebCodecs is available. + */ +type TexImageSourceWebCodecs = + | ImageBitmap + | ImageData + | HTMLImageElement + | HTMLCanvasElement + | HTMLVideoElement + | OffscreenCanvas + | VideoFrame; + +interface WebGLRenderingContextOverloads { + texImage2D( + target: GLenum, + level: GLint, + internalformat: GLint, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; + texSubImage2D( + target: GLenum, + level: GLint, + xoffset: GLint, + yoffset: GLint, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; +} + +interface WebGL2RenderingContextBase { + texImage3D( + target: GLenum, + level: GLint, + internalformat: GLint, + width: GLsizei, + height: GLsizei, + depth: GLsizei, + border: GLint, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; + texSubImage3D( + target: GLenum, + level: GLint, + xoffset: GLint, + yoffset: GLint, + zoffset: GLint, + width: GLsizei, + height: GLsizei, + depth: GLsizei, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; +} + +interface WebGL2RenderingContextOverloads { + texImage2D( + target: GLenum, + level: GLint, + internalformat: GLint, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; + texImage2D( + target: GLenum, + level: GLint, + internalformat: GLint, + width: GLsizei, + height: GLsizei, + border: GLint, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; + texSubImage2D( + target: GLenum, + level: GLint, + xoffset: GLint, + yoffset: GLint, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; + texSubImage2D( + target: GLenum, + level: GLint, + xoffset: GLint, + yoffset: GLint, + width: GLsizei, + height: GLsizei, + format: GLenum, + type: GLenum, + source: TexImageSourceWebCodecs, + ): void; +} + +///////////////////////////// +/// webcodecs APIs +///////////////////////////// + +interface AudioDataCopyToOptions { + format?: AudioSampleFormat | undefined; + frameCount?: number | undefined; + frameOffset?: number | undefined; + planeIndex: number; +} + +interface AudioDataInit { + data: AllowSharedBufferSource; + format: AudioSampleFormat; + numberOfChannels: number; + numberOfFrames: number; + sampleRate: number; + timestamp: number; +} + +interface AudioDecoderConfig { + codec: string; + description?: AllowSharedBufferSource | undefined; + numberOfChannels: number; + sampleRate: number; +} + +interface AudioDecoderInit { + error: WebCodecsErrorCallback; + output: AudioDataOutputCallback; +} + +interface AudioDecoderSupport { + config?: AudioDecoderConfig; + supported?: boolean; +} + +interface AudioEncoderConfig { + bitrate?: number | undefined; + codec: string; + numberOfChannels: number; + sampleRate: number; +} + +interface AudioEncoderInit { + error: WebCodecsErrorCallback; + output: EncodedAudioChunkOutputCallback; +} + +interface AudioEncoderSupport { + config?: AudioEncoderConfig; + supported?: boolean; +} + +interface AvcEncoderConfig { + format?: AvcBitstreamFormat | undefined; +} + +interface EncodedAudioChunkInit { + data: AllowSharedBufferSource; + duration?: number | undefined; + timestamp: number; + type: EncodedAudioChunkType; +} + +interface EncodedAudioChunkMetadata { + decoderConfig?: AudioDecoderConfig | undefined; +} + +interface EncodedVideoChunkInit { + data: AllowSharedBufferSource; + duration?: number | undefined; + timestamp: number; + type: EncodedVideoChunkType; +} + +interface EncodedVideoChunkMetadata { + decoderConfig?: VideoDecoderConfig | undefined; + temporalLayerId?: number | undefined; +} + +interface ImageDecodeOptions { + completeFramesOnly?: boolean | undefined; + frameIndex?: number | undefined; +} + +interface ImageDecodeResult { + complete: boolean; + image: VideoFrame; +} + +interface ImageDecoderInit { + colorSpaceConversion?: ColorSpaceConversion | undefined; + data: ImageBufferSource; + desiredHeight?: number | undefined; + desiredWidth?: number | undefined; + preferAnimation?: boolean | undefined; + premultiplyAlpha?: PremultiplyAlpha | undefined; + type: string; +} + +interface PlaneLayout { + offset: number; + stride: number; +} + +interface VideoColorSpaceInit { + fullRange?: boolean | null | undefined; + matrix?: VideoMatrixCoefficients | null | undefined; + primaries?: VideoColorPrimaries | null | undefined; + transfer?: VideoTransferCharacteristics | null | undefined; +} + +interface VideoDecoderConfig { + codec: string; + codedHeight?: number | undefined; + codedWidth?: number | undefined; + colorSpace?: VideoColorSpaceInit | undefined; + description?: AllowSharedBufferSource | undefined; + displayAspectHeight?: number | undefined; + displayAspectWidth?: number | undefined; + hardwareAcceleration?: HardwarePreference | undefined; + optimizeForLatency?: boolean | undefined; +} + +interface VideoDecoderInit { + error: WebCodecsErrorCallback; + output: VideoFrameOutputCallback; +} + +interface VideoDecoderSupport { + config?: VideoDecoderConfig; + supported?: boolean; +} + +interface VideoEncoderConfig { + alpha?: AlphaOption | undefined; + avc?: AvcEncoderConfig | undefined; + bitrate?: number | undefined; + bitrateMode?: VideoEncoderBitrateMode | undefined; + codec: string; + displayHeight?: number | undefined; + displayWidth?: number | undefined; + framerate?: number | undefined; + hardwareAcceleration?: HardwarePreference | undefined; + height: number; + latencyMode?: LatencyMode | undefined; + scalabilityMode?: string | undefined; + width: number; +} + +interface VideoEncoderEncodeOptions { + keyFrame?: boolean; +} + +interface VideoEncoderInit { + error: WebCodecsErrorCallback; + output: EncodedVideoChunkOutputCallback; +} + +interface VideoEncoderSupport { + config?: VideoEncoderConfig; + supported?: boolean; +} + +interface VideoFrameBufferInit { + codedHeight: number; + codedWidth: number; + colorSpace?: VideoColorSpaceInit | undefined; + displayHeight?: number | undefined; + displayWidth?: number | undefined; + duration?: number | undefined; + format: VideoPixelFormat; + layout?: PlaneLayout[] | undefined; + timestamp: number; + visibleRect?: DOMRectInit | undefined; +} + +interface VideoFrameCopyToOptions { + layout?: PlaneLayout[] | undefined; + rect?: DOMRectInit | undefined; +} + +interface VideoFrameInit { + alpha?: AlphaOption | undefined; + displayHeight?: number | undefined; + displayWidth?: number | undefined; + duration?: number | undefined; + timestamp?: number | undefined; + visibleRect?: DOMRectInit | undefined; +} + +interface AudioData { + readonly duration: number; + readonly format: AudioSampleFormat | null; + readonly numberOfChannels: number; + readonly numberOfFrames: number; + readonly sampleRate: number; + readonly timestamp: number; + allocationSize(options: AudioDataCopyToOptions): number; + clone(): AudioData; + close(): void; + copyTo( + destination: AllowSharedBufferSource, + options: AudioDataCopyToOptions, + ): void; +} + +declare var AudioData: { + prototype: AudioData; + new (init: AudioDataInit): AudioData; +}; + +interface AudioDecoderEventMap { + dequeue: Event; +} + +/** Available only in secure contexts. */ +interface AudioDecoder { + readonly decodeQueueSize: number; + readonly state: CodecState; + ondequeue: ((this: AudioDecoder, ev: Event) => any) | null; + close(): void; + configure(config: AudioDecoderConfig): void; + decode(chunk: EncodedAudioChunk): void; + flush(): Promise; + reset(): void; + addEventListener( + type: K, + listener: (this: AudioDecoder, ev: AudioDecoderEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: AudioDecoder, ev: AudioDecoderEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} + +declare var AudioDecoder: { + prototype: AudioDecoder; + new (init: AudioDecoderInit): AudioDecoder; + isConfigSupported(config: AudioDecoderConfig): Promise; +}; + +interface AudioEncoderEventMap { + dequeue: Event; +} + +/** Available only in secure contexts. */ +interface AudioEncoder { + readonly encodeQueueSize: number; + readonly state: CodecState; + ondequeue: ((this: AudioEncoder, ev: Event) => any) | null; + close(): void; + configure(config: AudioEncoderConfig): void; + encode(data: AudioData): void; + flush(): Promise; + reset(): void; + addEventListener( + type: K, + listener: (this: AudioEncoder, ev: AudioEncoderEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: AudioEncoder, ev: AudioEncoderEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} + +declare var AudioEncoder: { + prototype: AudioEncoder; + new (init: AudioEncoderInit): AudioEncoder; + isConfigSupported(config: AudioEncoderConfig): Promise; +}; + +interface EncodedAudioChunk { + readonly byteLength: number; + readonly duration: number | null; + readonly timestamp: number; + readonly type: EncodedAudioChunkType; + copyTo(destination: AllowSharedBufferSource): void; +} + +declare var EncodedAudioChunk: { + prototype: EncodedAudioChunk; + new (init: EncodedAudioChunkInit): EncodedAudioChunk; +}; + +interface EncodedVideoChunk { + readonly byteLength: number; + readonly duration: number | null; + readonly timestamp: number; + readonly type: EncodedVideoChunkType; + copyTo(destination: AllowSharedBufferSource): void; +} + +declare var EncodedVideoChunk: { + prototype: EncodedVideoChunk; + new (init: EncodedVideoChunkInit): EncodedVideoChunk; +}; + +/** Available only in secure contexts. */ +interface ImageDecoder { + readonly complete: boolean; + readonly completed: Promise; + readonly tracks: ImageTrackList; + readonly type: string; + close(): void; + decode(options?: ImageDecodeOptions): Promise; + reset(): void; +} + +// declare var ImageDecoder: { +// prototype: ImageDecoder; +// new(init: ImageDecoderInit): ImageDecoder; +// isTypeSupported(type: string): Promise; +// }; + +// interface ImageTrack { +// readonly animated: boolean; +// readonly frameCount: number; +// readonly repetitionCount: number; +// selected: boolean; +// } + +// declare var ImageTrack: { +// prototype: ImageTrack; +// new(): ImageTrack; +// }; + +// interface ImageTrackList { +// readonly length: number; +// readonly ready: Promise; +// readonly selectedIndex: number; +// readonly selectedTrack: ImageTrack | null; +// [index: number]: ImageTrack; +// } + +// declare var ImageTrackList: { +// prototype: ImageTrackList; +// new(): ImageTrackList; +// }; + +interface VideoColorSpace { + readonly fullRange: boolean | null; + readonly matrix: VideoMatrixCoefficients | null; + readonly primaries: VideoColorPrimaries | null; + readonly transfer: VideoTransferCharacteristics | null; + toJSON(): VideoColorSpaceInit; +} + +declare var VideoColorSpace: { + prototype: VideoColorSpace; + new (init?: VideoColorSpaceInit): VideoColorSpace; +}; + +interface VideoDecoderEventMap { + dequeue: Event; +} + +/** Available only in secure contexts. */ +interface VideoDecoder { + readonly decodeQueueSize: number; + readonly state: CodecState; + ondequeue: ((this: VideoDecoder, ev: Event) => any) | null; + close(): void; + configure(config: VideoDecoderConfig): void; + decode(chunk: EncodedVideoChunk): void; + flush(): Promise; + reset(): void; + addEventListener( + type: K, + listener: (this: VideoDecoder, ev: VideoDecoderEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: VideoDecoder, ev: VideoDecoderEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} + +declare var VideoDecoder: { + prototype: VideoDecoder; + new (init: VideoDecoderInit): VideoDecoder; + isConfigSupported(config: VideoDecoderConfig): Promise; +}; + +interface VideoEncoderEventMap { + dequeue: Event; +} + +/** Available only in secure contexts. */ +interface VideoEncoder { + readonly encodeQueueSize: number; + readonly state: CodecState; + close(): void; + ondequeue: ((this: VideoEncoder, ev: Event) => any) | null; + configure(config: VideoEncoderConfig): void; + encode(frame: VideoFrame, options?: VideoEncoderEncodeOptions): void; + flush(): Promise; + reset(): void; + addEventListener( + type: K, + listener: (this: VideoEncoder, ev: VideoEncoderEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: VideoEncoder, ev: VideoEncoderEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; +} + +declare var VideoEncoder: { + prototype: VideoEncoder; + new (init: VideoEncoderInit): VideoEncoder; + isConfigSupported(config: VideoEncoderConfig): Promise; +}; + +interface VideoFrame { + readonly codedHeight: number; + readonly codedRect: DOMRectReadOnly | null; + readonly codedWidth: number; + readonly colorSpace: VideoColorSpace; + readonly displayHeight: number; + readonly displayWidth: number; + readonly duration: number | null; + readonly format: VideoPixelFormat | null; + readonly timestamp: number; + readonly visibleRect: DOMRectReadOnly | null; + allocationSize(options?: VideoFrameCopyToOptions): number; + clone(): VideoFrame; + close(): void; + copyTo( + destination: AllowSharedBufferSource, + options?: VideoFrameCopyToOptions, + ): Promise; +} + +declare var VideoFrame: { + prototype: VideoFrame; + new (source: CanvasImageSource, init?: VideoFrameInit): VideoFrame; + new (data: AllowSharedBufferSource, init: VideoFrameBufferInit): VideoFrame; +}; + +interface AudioDataOutputCallback { + (output: AudioData): void; +} + +interface EncodedAudioChunkOutputCallback { + (output: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata): void; +} + +interface EncodedVideoChunkOutputCallback { + (chunk: EncodedVideoChunk, metadata: EncodedVideoChunkMetadata): void; +} + +interface VideoFrameOutputCallback { + (output: VideoFrame): void; +} + +interface WebCodecsErrorCallback { + (error: DOMException): void; +} + +// type AllowSharedBufferSource = ArrayBuffer | ArrayBufferView; +// type BitrateMode = "constant" | "variable"; +// type ImageBufferSource = ArrayBuffer | ArrayBufferView | ReadableStream; +// type AlphaOption = "discard" | "keep"; +// type AudioSampleFormat = "f32" | "f32-planar" | "s16" | "s16-planar" | "s32" | "s32-planar" | "u8" | "u8-planar"; +// type AvcBitstreamFormat = "annexb" | "avc"; +// type CodecState = "closed" | "configured" | "unconfigured"; +// type EncodedAudioChunkType = "delta" | "key"; +// type EncodedVideoChunkType = "delta" | "key"; +type HardwarePreference = + | "no-preference" + | "prefer-hardware" + | "prefer-software"; +// type LatencyMode = "quality" | "realtime"; +// type VideoColorPrimaries = "bt470bg" | "bt709" | "smpte170m"; +// type VideoMatrixCoefficients = "bt470bg" | "bt709" | "rgb" | "smpte170m"; +// type VideoPixelFormat = "BGRA" | "BGRX" | "I420" | "I420A" | "I422" | "I444" | "NV12" | "RGBA" | "RGBX"; +// type VideoTransferCharacteristics = "bt709" | "iec61966-2-1" | "smpte170m"; diff --git a/yarn.lock b/yarn.lock index d7a49a1f..44a0d217 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4621,22 +4621,6 @@ __metadata: languageName: node linkType: hard -"@types/dom-mediacapture-transform@npm:^0.1.10": - version: 0.1.11 - resolution: "@types/dom-mediacapture-transform@npm:0.1.11" - dependencies: - "@types/dom-webcodecs": "npm:*" - checksum: 10c0/19c76d54cf31aa2a925011fc5f973dff9a10bdecfdf2285e5e568e61850a0fa2b8c9f1807a1462cbefd57ec26d32eeaa9c359117aca9d9fe7f0d6f2fff33f51e - languageName: node - linkType: hard - -"@types/dom-webcodecs@npm:*": - version: 0.1.14 - resolution: "@types/dom-webcodecs@npm:0.1.14" - checksum: 10c0/20f2e9a52986f861eb5725365561c57d4d8c7494322e55f5df1b0639e97d399c3c427e05d2bc7772c9c975197dc12db3e047e3e0ad7b49ae13181514e3d54f56 - languageName: node - linkType: hard - "@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -6969,7 +6953,6 @@ __metadata: "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.5.1" "@types/content-type": "npm:^1.1.5" - "@types/dom-mediacapture-transform": "npm:^0.1.10" "@types/grecaptcha": "npm:^3.0.9" "@types/jsdom": "npm:^21.1.7" "@types/lodash-es": "npm:^4.17.12" From fa0af6403e24a938ce123e55b49b4c0e211c6107 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 29 Apr 2025 12:21:56 +0200 Subject: [PATCH 058/155] Fix Playwright tests in Firefox As explained by the comment. --- playwright/fixtures/widget-user.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index 0a422d20..d1412bd8 100644 --- a/playwright/fixtures/widget-user.ts +++ b/playwright/fixtures/widget-user.ts @@ -154,8 +154,13 @@ export const widgetTest = test.extend({ ewPage1.getByRole("heading", { name: "Invite to Welcome Room" }), ).toBeVisible(); - await ewPage1.getByRole("textbox").fill(whistlerMxId); - await ewPage1.getByRole("textbox").click(); + // To get the invite textbox we need to specifically select within the + // dialog, since there is another textbox in the background (the message + // composer). In theory the composer shouldn't be visible to Playwright at + // all because the invite dialog has trapped focus, but the focus trap + // doesn't quite work right on Firefox. + await ewPage1.getByRole("dialog").getByRole("textbox").fill(whistlerMxId); + await ewPage1.getByRole("dialog").getByRole("textbox").click(); await ewPage1.getByRole("button", { name: "Invite" }).click(); // Accept the invite From dfb9eb4b1f828bc7ceac091f5b69eedfe7aa23fe Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 29 Apr 2025 12:41:23 +0200 Subject: [PATCH 059/155] Upgrade Playwright to fix Element's unsupported browser warning --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 3730004b..78960c8b 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@opentelemetry/sdk-trace-base": "^1.25.1", "@opentelemetry/sdk-trace-web": "^1.9.1", "@opentelemetry/semantic-conventions": "^1.25.1", - "@playwright/test": "^1.51.0", + "@playwright/test": "^1.52.0", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-visually-hidden": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 70de0fc6..be477d19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3045,14 +3045,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.51.0": - version: 1.51.0 - resolution: "@playwright/test@npm:1.51.0" +"@playwright/test@npm:^1.52.0": + version: 1.52.0 + resolution: "@playwright/test@npm:1.52.0" dependencies: - playwright: "npm:1.51.0" + playwright: "npm:1.52.0" bin: playwright: cli.js - checksum: 10c0/ae83dd2c3a32133de58f44a9dbcd73a8059155ebd8acc736ba8bd0a7ca99b194afe2e8f5a500861d18b1c8f06b4e4ea8de4a2402297c59053d4becc404b47e0a + checksum: 10c0/1c428b421593eb4f79b7c99783a389c3ab3526c9051ec772749f4fca61414dfa9f2344eba846faac5f238084aa96c836364a91d81d3034ac54924f239a93e247 languageName: node linkType: hard @@ -6860,7 +6860,7 @@ __metadata: "@opentelemetry/sdk-trace-base": "npm:^1.25.1" "@opentelemetry/sdk-trace-web": "npm:^1.9.1" "@opentelemetry/semantic-conventions": "npm:^1.25.1" - "@playwright/test": "npm:^1.51.0" + "@playwright/test": "npm:^1.52.0" "@radix-ui/react-dialog": "npm:^1.0.4" "@radix-ui/react-slider": "npm:^1.1.2" "@radix-ui/react-visually-hidden": "npm:^1.0.3" @@ -10295,27 +10295,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.51.0": - version: 1.51.0 - resolution: "playwright-core@npm:1.51.0" +"playwright-core@npm:1.52.0": + version: 1.52.0 + resolution: "playwright-core@npm:1.52.0" bin: playwright-core: cli.js - checksum: 10c0/8f5de23088c5e97c00327f356b17e0223181e921baf99f4e38d9a3b18d0693db288f8b5389e96d0cb4a1b55f03870f140dd7346128a0c02ce36d11eb92153841 + checksum: 10c0/640945507e6ca2144e9f596b2a6ecac042c2fd3683ff99e6271e9a7b38f3602d415f282609d569456f66680aab8b3c5bb1b257d8fb63a7fc0ed648261110421f languageName: node linkType: hard -"playwright@npm:1.51.0": - version: 1.51.0 - resolution: "playwright@npm:1.51.0" +"playwright@npm:1.52.0": + version: 1.52.0 + resolution: "playwright@npm:1.52.0" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.51.0" + playwright-core: "npm:1.52.0" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/e8509ea500e03e8051fd243f2347ac3196ff8dde4c20ae3aba4cf723e2b647a0158d209fba062995dab90590229a483d723562cf1ea8b2fc11698617027416fd + checksum: 10c0/2c6edf1e15e59bbaf77f3fa0fe0ac975793c17cff835d9c8b8bc6395a3b6f1c01898b3058ab37891b2e4d424bcc8f1b4844fe70d943e0143d239d7451408c579 languageName: node linkType: hard From 9f5eae0ac104fd0d90c27e50617d79eb7c8c73fa Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 29 Apr 2025 13:02:02 +0200 Subject: [PATCH 060/155] Clean up a string --- locales/en/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/app.json b/locales/en/app.json index ab3f8dd0..d185d2f8 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -166,7 +166,7 @@ }, "background_blur_header": "Background", "background_blur_label": "Blur the background of the video", - "blur_not_supported_by_browser": "(Background blur is not supported by this device)", + "blur_not_supported_by_browser": "(Background blur is not supported by this device.)", "developer_tab_title": "Developer", "devices": { "camera": "Camera", From eeb3a1a9914f4eb16a20f7de3d5baed5c07c3e07 Mon Sep 17 00:00:00 2001 From: Robin Date: Tue, 29 Apr 2025 22:19:00 +0200 Subject: [PATCH 061/155] Add license notices to inlined type declarations --- src/@types/dom-mediacapture-transform.d.ts | 9 +++++++++ src/@types/dom-webcodecs.d.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/@types/dom-mediacapture-transform.d.ts b/src/@types/dom-mediacapture-transform.d.ts index ac55dc90..d4c1f8f8 100644 --- a/src/@types/dom-mediacapture-transform.d.ts +++ b/src/@types/dom-mediacapture-transform.d.ts @@ -8,6 +8,15 @@ // depending on @types/dom-mediacapture-transform. // ----------------------------------------------------------------------------- +// This project is licensed under the MIT license. +// Copyrights are respective of each contributor listed at the beginning of each definition file. + +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + // In general, these types are only available behind a command line flag or an origin trial in // Chrome 90+. diff --git a/src/@types/dom-webcodecs.d.ts b/src/@types/dom-webcodecs.d.ts index f06ceedd..55e4c7d4 100644 --- a/src/@types/dom-webcodecs.d.ts +++ b/src/@types/dom-webcodecs.d.ts @@ -7,6 +7,15 @@ // @types/dom-webcodecs. // ----------------------------------------------------------------------------- +// This project is licensed under the MIT license. +// Copyrights are respective of each contributor listed at the beginning of each definition file. + +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + // Versioning: // Until the WebCodecs spec is finalized, the major version number is 0. I have chosen to use minor // version 1 to denote the API as defined by the IDL files from the Chromium repo at From aee30d84022771dc1aed5ef3d21e558d03ec809a Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Fri, 2 May 2025 11:49:49 +0200 Subject: [PATCH 062/155] Fix release notes (#3244) --- .github/{realease.yml => release.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{realease.yml => release.yml} (100%) diff --git a/.github/realease.yml b/.github/release.yml similarity index 100% rename from .github/realease.yml rename to .github/release.yml From 06e1a4eb1e8120edef266467fc233580fef78b00 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 12:57:23 +0200 Subject: [PATCH 063/155] Adapt new naming schema *.m.localhost --- backend/dev_homeserver.yaml | 4 ++-- backend/dev_nginx.conf | 6 +++--- dev-backend-docker-compose.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/dev_homeserver.yaml b/backend/dev_homeserver.yaml index 5697c32e..eab4e698 100644 --- a/backend/dev_homeserver.yaml +++ b/backend/dev_homeserver.yaml @@ -1,5 +1,5 @@ -server_name: "synapse.localhost" -public_baseurl: http://synapse.localhost:8008/ +server_name: "synapse.m.localhost" +public_baseurl: https://synapse.m.localhost/ pid_file: /data/homeserver.pid diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index e4d665d6..bc60f9c1 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -5,7 +5,7 @@ server { listen 8448 ssl; listen [::]:443 ssl; listen [::]:8448 ssl; - server_name synapse.localhost; + server_name synapse.m.localhost; ssl_certificate /root/ssl/cert.pem; ssl_certificate_key /root/ssl/key.pem; @@ -17,7 +17,7 @@ server { # setting for livekit_service_url location /.well-known/matrix/client { add_header Access-Control-Allow-Origin *; - return 200 '{"m.homeserver": {"base_url": "http://synapse.localhost:8008"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "http://matrix-rtc.localhost:8008/livekit/jwt"}]}'; + return 200 '{"m.homeserver": {"base_url": "https://synapse.m.localhost"}, "org.matrix.msc4143.rtc_foci": [{"type": "livekit", "livekit_service_url": "https://matrix-rtc.m.localhost/livekit/jwt"}]}'; default_type application/json; } @@ -46,7 +46,7 @@ server { listen 8448 ssl; listen [::]:443 ssl; listen [::]:8448 ssl; - server_name matrix-rtc.localhost; + server_name matrix-rtc.m.localhost; ssl_certificate /root/ssl/cert.pem; ssl_certificate_key /root/ssl/key.pem; diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 38853054..ad25bf75 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -7,7 +7,7 @@ services: hostname: auth-server environment: - LK_JWT_PORT=8080 - - LIVEKIT_URL=ws://matrix-rtc.localhost/livekit/sfu + - LIVEKIT_URL=wss://matrix-rtc.m.localhost/livekit/sfu - LIVEKIT_KEY=devkey - LIVEKIT_SECRET=secret # If the configured homeserver runs on localhost, it'll probably be using @@ -83,7 +83,7 @@ services: nginx: # openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls_localhost_key.pem -out tls_localhost_cert.pem -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost" - hostname: synapse.localhost + hostname: synapse.m.localhost image: nginx:latest volumes: - ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z From 08f034251c2abb8216a6f41aa8e90a8a57c49737 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 13:02:58 +0200 Subject: [PATCH 064/155] Add call.m.localhost pointing to yarn dev --host --- backend/dev_nginx.conf | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index bc60f9c1..1b201a89 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -1,3 +1,4 @@ +# Synapse reverse proxy including .well-known/matrix/client server { listen 80; listen [::]:80; @@ -39,12 +40,15 @@ server { } +# MatrixRTC reverse proxy +# - MatrixRTC Authorization Service +# - LiveKit SFU websocket signaling connection server { listen 80; listen [::]:80; listen 443 ssl; - listen 8448 ssl; listen [::]:443 ssl; + listen 8448 ssl; listen [::]:8448 ssl; server_name matrix-rtc.m.localhost; ssl_certificate /root/ssl/cert.pem; @@ -83,3 +87,37 @@ server { error_page 500 502 503 504 /50x.html; } + + +# Convenience reverse proxy for the call.m.localhost domain to yarn dev --host +server { + listen 80; + listen [::]:80; + server_name call.m.localhost; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name call.m.localhost; + ssl_certificate /root/ssl/cert.pem; + ssl_certificate_key /root/ssl/key.pem; + + + location ^~ / { + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass https://host.docker.internal:3000; + proxy_ssl_verify off; + + } + + error_page 500 502 503 504 /50x.html; + +} \ No newline at end of file From 14ff6dce9364ed362540d47cfe3e45f88b3829ff Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 13:05:07 +0200 Subject: [PATCH 065/155] localhost TLS mini CA including wildcard certs for *.m.localhost --- backend/dev_tls_local-ca.crt | 19 +++++++++++++++++ backend/dev_tls_local-ca.key | 28 +++++++++++++++++++++++++ backend/dev_tls_m.localhost.crt | 20 ++++++++++++++++++ backend/dev_tls_m.localhost.key | 28 +++++++++++++++++++++++++ backend/dev_tls_setup | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 132 insertions(+) create mode 100644 backend/dev_tls_local-ca.crt create mode 100644 backend/dev_tls_local-ca.key create mode 100644 backend/dev_tls_m.localhost.crt create mode 100644 backend/dev_tls_m.localhost.key create mode 100644 backend/dev_tls_setup diff --git a/backend/dev_tls_local-ca.crt b/backend/dev_tls_local-ca.crt new file mode 100644 index 00000000..9c8ee3d7 --- /dev/null +++ b/backend/dev_tls_local-ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDGjCCAgKgAwIBAgIUGdiFHhH4KL2pqBjMQHQ+PVIkSV8wDQYJKoZIhvcNAQEL +BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy +MDJaFw0zNTA1MDMxMDMyMDJaMB4xHDAaBgNVBAMME0VsZW1lbnQgQ2FsbCBEZXYg +Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDA2y0hjmNn1vRsVSdy +8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH9jdT +tG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeFw9fw +eRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZNWnie +Ui7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAveL9K +FGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BLPiQU +KGKrAgMBAAGjUDBOMB0GA1UdDgQWBBQJqBjMu61c1p24txw/y+kv3D+V6DAfBgNV +HSMEGDAWgBQJqBjMu61c1p24txw/y+kv3D+V6DAMBgNVHRMEBTADAQH/MA0GCSqG +SIb3DQEBCwUAA4IBAQB8m2YfFGLugNt5vAAOvNxVqDA8c72yCVYr3CBCpmTIEY5Z +d3qVGhG9//ux6+J8ntkSwd9nV5GJyYXHukCG1VavnAWolWdNF/WAllf0jhLuz7kD +/cJnuI1By4tBsBmSz851i6HJ4t5k99Be+6GQVzi0e7zzfxTHZE4xP2J6Ox8QbPsP +n0m76nIp/WbWaJqzvIIjJhmUUPPv+4wN+eOArgjiGLzptM2qTtGZtd0c9nS5gvep ++mEbSUN9zkhAroZf80wf+hEvy+fJ94VbZ9QjTzTg7odZLrsXGIe8DaG63EYRQ25b +W5iYBAreln5fGSt7qHsGfqwZibTEk/Lx3dydO1Kg +-----END CERTIFICATE----- diff --git a/backend/dev_tls_local-ca.key b/backend/dev_tls_local-ca.key new file mode 100644 index 00000000..c6de05c4 --- /dev/null +++ b/backend/dev_tls_local-ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDA2y0hjmNn1vRs +VSdy8IOfo8N1q9UgkhQWpGKXzPh+D5d1fnuJEmHIVwtDEtS/PwQ43LTmegChPtKH +9jdTtG0IihW9Ja5YNG+9xAwaoA/sB3CGCBYsz+2/XjVUpXoBJXIPoFBWsn+K0oeF +w9fweRO1z9abM4cl+LjKzMNM8CCyu9uI1MaGjYez2YIWvG854VucLxX7HSlMJxZN +WnieUi7fMakuJhB2+aiIQjdKxy4E5RHNhzYG/LXhvP+wBYBDPNRsP3rtzEaE9HAv +eL9KFGqd3R4cBia6r1WIXmpAzyu5RGP5Eou0TZlGkal96/bF0I7q/pKlL23Jt1BL +PiQUKGKrAgMBAAECggEAAPX2kxi5AQ7ul82SzT1KgpSXyDHLdYaUyAoYnaX9RO+B +8ylmpyeqygs4+KQS4EMJm9jpo85Oy37bIKdG3kljU6wQcKlL5Y+ZUOo1nzpV6fid +hGVs6ts8VXw8KshKQ9AyccZ8L/pirUfgOffgTwfjY7/90zceAL/s98GuZWc62nkX +55joQv/OikqYfAGP/U6Bp2Zyf23DwJB09Z3B6NnZj/ZyAbDrDEHuA15LhCOcCczp +IU/mFEywBPHT9Tg4w4Beq78PeAETvku2UalYRLhP3RLlXr2oEbwUtINRVt2QjZ85 +Esps4uCqL/mgQluIebtudD9HL/YMlNPXue1mDXFxJQKBgQDgZZY4yJBcf488T1V6 +HNm06b/LvVGj253pKgw14hpY1xQu3Ymgzv1GEqzhSYdzxhpmj0tMUNHxAp+YdGQu +SZ0wcPKhw0aYVkIjDRYDC3Wn5GJhyIEYHGYMo/n4l49UzHRBPOTDzp49DkHTKBgh +XgIIazYT3CkjTIMRrkUv+qfIPQKBgQDcBGu/mqbjxs4sN3zqPS4aB21o6t6W0sXs +ZP9w6RlTPQi5U2oRbftjZtYc0bbEgkMUImB1HwYPQT5pJ+MyC414xDvSc2exBr5d +To6yyPIy78Tf5PHM12fpKV92nSvoz/pSjYcGxxDtKfPqu+t8mOJfjCV1lLLA+xuB +DDaE4p8dBwKBgQCdAne6A5v/HMH8UQZeCxHJpESvKiiVnnU/UEx651nID7XvlNNX +0X0mKqsMd4ZvW43ddSYan/JF0LAa3FW8jYWO/3jF9vzOWoysOdvNBZetgf/Uq5ao +aDZ/YbzmVCXWD7jIbPMkjs3pqrAkL0mzDzQc7+dGviWKrV6IYIfIqnn7gQKBgDCz +vdIk/qpO+JZrFfiX4Fucp0hhLTJ/p5ZDaRPqVVPKn+K+Jy2ChfIj8mNgvK9VEloj +nexvGJ1J2PHYBX+vdPp1nbRhHWPfVUY8PHQw7QP/dToGaMvqJrNDGEGeWvjnCMc7 +UtdaO1H0Rm0AegkTopB56lTTvJnhO95eALd7nrMDAoGAEPdzJtWoKafp49svhSj0 +hiXQv2SPBwVUN4LZ4SOWiXUcmYYm80aNpYKLkBxYjrfqFWhE7NUHLGp8YorQWKY2 +acD9AReHk/xku0ABy6jeYmSCmCxASxst5liKD+l12sk0gB0rk5MBxB4Uu1MIbQZ2 +aCASX3AVD2/XyC2MKkzc8Eg= +-----END PRIVATE KEY----- diff --git a/backend/dev_tls_m.localhost.crt b/backend/dev_tls_m.localhost.crt new file mode 100644 index 00000000..be3eb0a3 --- /dev/null +++ b/backend/dev_tls_m.localhost.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDXDCCAkSgAwIBAgIUXizLjwkdqepX0bh0K3abeJxj68AwDQYJKoZIhvcNAQEL +BQAwHjEcMBoGA1UEAwwTRWxlbWVudCBDYWxsIERldiBDQTAeFw0yNTA1MDUxMDMy +MDJaFw0zNTA1MDMxMDMyMDJaMBgxFjAUBgNVBAMMDSoubS5sb2NhbGhvc3QwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbr79gttr7X8j+ISfdCV53PD8f +R6JsLf6nmkCbRqCaIq85Y82tnYbUB3B6F9RcosrxF+UHFMa/i1UiLSNL0GHisclB +5LII2RycsLJYShkO9pVioVDf3gh+hyVRySBQ2FgtLHB+ZgcZOCG8f75g9CdeVDmv +Kw4J29QV8bxFSafvTLOdqtupylfTSqYVTAE8HnIOsdnZ+mE6SjeS2wV3DYqdSXoa +xWmGranZUmrCgeZdukAZTWgAlHgQvuWVtgyAxPmhcr2KA50QHB/IJ2SDIaUiI++R +4nXkVChbePnNaxqw0kc0QD3Jpd3B1QhHlOhKi9R6Mo5Iyf0nsHnZaQ0bAzPDAgMB +AAGjgZcwgZQwHwYDVR0jBBgwFoAUCagYzLutXNaduLccP8vpL9w/legwCQYDVR0T +BAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwJQYDVR0RBB4w +HIILbS5sb2NhbGhvc3SCDSoubS5sb2NhbGhvc3QwHQYDVR0OBBYEFJgJZkgE6cem +HbSQ7P47rVhmeWjHMA0GCSqGSIb3DQEBCwUAA4IBAQBDocJIUHVxNvbvigPyZvZa +uAmj5eqhf8fDNtQM2tl8AuzOJm0TlggUuKDQNM6zRBXVHQRhCmtaZ3CMkmkTNNhH +aMfG7o/JVvQsxIuORMvAnPlivla2DgiEWr/NEaWISlINMov44DysOyupbHRXcbKd +WWB1cA+D5ZNb8ivOPT1edNSGavAiyEaCPA/qqGFZwq54EtJKIuteqV1UGn1nYD/W +a0niB157moRtlnzwNfwDDeW1Y4HBbuVkX2sipCO+HC6sn7Vni90LzK9zBolaWXTw +RxauTzS9IvtU1G/Gv5/VRzhzIb+ds2jEsdLLnBlTyA+Jh2Cqs002t7QJki6Qto5p +-----END CERTIFICATE----- diff --git a/backend/dev_tls_m.localhost.key b/backend/dev_tls_m.localhost.key new file mode 100644 index 00000000..d83c1dea --- /dev/null +++ b/backend/dev_tls_m.localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbr79gttr7X8j+ +ISfdCV53PD8fR6JsLf6nmkCbRqCaIq85Y82tnYbUB3B6F9RcosrxF+UHFMa/i1Ui +LSNL0GHisclB5LII2RycsLJYShkO9pVioVDf3gh+hyVRySBQ2FgtLHB+ZgcZOCG8 +f75g9CdeVDmvKw4J29QV8bxFSafvTLOdqtupylfTSqYVTAE8HnIOsdnZ+mE6SjeS +2wV3DYqdSXoaxWmGranZUmrCgeZdukAZTWgAlHgQvuWVtgyAxPmhcr2KA50QHB/I +J2SDIaUiI++R4nXkVChbePnNaxqw0kc0QD3Jpd3B1QhHlOhKi9R6Mo5Iyf0nsHnZ +aQ0bAzPDAgMBAAECggEARLRazvnzCnLbVrbYCjX7v7/RFWM9/OKRWnJ6p2uULWE4 +FaoDFuaJHSHJU8AXYegfiiTi1+ylxtrcr4/e3zKvN+UAbXlYzgnOFCHwGoFcrJtK +EnQhJiIsenX2lLCe9755rznIzScGY+0/ChoPsGaexwSBTlnAQL6HykVbMfKOz03H +ywEx4g3AK1rgTnqNLFHkl+1ainoW6ffeM6thMD/bObGz+PoGSMqbTA80TGMswgMN +Ipnt0AwSgKweLmYG00t667c9htxY6DPRUoJ55dqsAFS8VMa4hhcslyhktPXTGEXh +x2r8UAFavEo2IdRnR8vfNfOv6twsWSHTVRGc7qmKDQKBgQDX0HnMAnBb8KB1zj/O +1prhAlhc6Jtwf3s5Hm/2MW0Jg/u7bZx81s206rvcTJtUJ2ROH+K7Rx3iASWzcsuW +XljCWA9G156SuOBE6mIS1EMI1EKgjbJBru1cOco6AIwI0SuJKcEX/1RtzoBbIIbZ +qhn99RszqAKDjw1iqbpyZCX5PQKBgQC4rRLsMTVvFTqWPEAA7SeJr3LZF+eoap/U +1+MA+J49D5ykQMFHjL1VSdfWgKIm3i4xDbDLAX1BYELxeKVLIp6CL808zEldGQy5 +g+O4dJlmz1PUGorb28qKGJnfwXK7F5tJuX+NgQM2zJnueyTv+fsskBp79CWNQvzr +ueG41o6w/wKBgG7sA+3LQxy+LHrgKwOQYcJMhkYad+n2W8sbzcfn13cQkw3eZJP1 +g3z9ONkdtqgmJvPQh6RiBQXoOQxmcCU1EMGyqQdsQ2B+DSbeoNG0r0+WaThEG96O +ngjM2xe8uDy/5XR2NXy0Cxz1ChvMOAMf3oQcuoJuU/xyRhrzyZSJzMqxAoGAH8hx +nEKvzolZxudhoIcwKcsPOfuaO+r1zPzGrbEcEqgwLjiSywyWvSnzQpBq18OfMYQI +rDd6Zhj6DHLWB8NSgldVvCPwcFxSS08+js1KZV5DMBrNUR9XkULAoLi7VSWv7RVG +tYTBl9nImDmLVt2v87BtTm3rVI911d/s0BHlBuMCgYEAs0AFMsTE+22Y44JMcTAE +OeHEsEDXI5cTlcNmwFKWY+UCZnb2FXflO2XNeqyi6ReYMUyBI2wHdUGvh2B1c2Ac +3z/SShBLS7bMGgyvYE/By1xnemiy+6vG2NIYHKExZfOphx8rDTfm5Qlj6LxstY9+ +Tx2VzAs01UIZGDhJ94u5imo= +-----END PRIVATE KEY----- diff --git a/backend/dev_tls_setup b/backend/dev_tls_setup new file mode 100644 index 00000000..4276e148 --- /dev/null +++ b/backend/dev_tls_setup @@ -0,0 +1,37 @@ +#!/bin/bash + +# Step 1: Create a Root CA key and cert +openssl genrsa -out dev_tls_local-ca.key 2048 +openssl req -x509 -new -nodes \ + -days 3650 \ + -subj "/CN=Element Call Dev CA" \ + -key dev_tls_local-ca.key \ + -out dev_tls_local-ca.crt \ + -sha256 -addext "basicConstraints=CA:TRUE" + +# Step 2: Create a private key and CSR for *.m.localhost +openssl req -new -nodes -newkey rsa:2048 \ + -keyout dev_tls_m.localhost.key \ + -out dev_tls_m.localhost.csr \ + -subj "/CN=*.m.localhost" + +# Step 3: Sign the CSR with your CA +openssl x509 \ + -req -in dev_tls_m.localhost.csr \ + -CA dev_tls_local-ca.crt -CAkey dev_tls_local-ca.key \ + -CAcreateserial \ + -out dev_tls_m.localhost.crt \ + -days 3650 \ + -sha256 \ + -extfile <( cat < Date: Mon, 5 May 2025 13:06:02 +0200 Subject: [PATCH 066/155] add new certs to nginx section --- dev-backend-docker-compose.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index ad25bf75..c2786583 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -82,19 +82,22 @@ services: - ecbackend nginx: - # openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls_localhost_key.pem -out tls_localhost_cert.pem -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost" + # see backend/dev_tls_setup for how to generate the tls certs hostname: synapse.m.localhost image: nginx:latest volumes: - ./backend/dev_nginx.conf:/etc/nginx/conf.d/default.conf:Z - - ./backend/tls_localhost_key.pem:/root/ssl/key.pem:Z - - ./backend/tls_localhost_cert.pem:/root/ssl/cert.pem:Z + - ./backend/dev_tls_m.localhost.key:/root/ssl/key.pem:Z + - ./backend/dev_tls_m.localhost.crt:/root/ssl/cert.pem:Z ports: # HOST_PORT:CONTAINER_PORT - "80:80" - "443:443" - "8008:80" - "4443:443" + - "8448:8448" + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: - synapse networks: From fb63e64eb4476c14398c2f7a27975391845f49dc Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 13:06:58 +0200 Subject: [PATCH 067/155] removed old localhost tls certificates --- backend/tls_localhost_cert.pem | 22 ---------------------- backend/tls_localhost_key.pem | 28 ---------------------------- 2 files changed, 50 deletions(-) delete mode 100644 backend/tls_localhost_cert.pem delete mode 100644 backend/tls_localhost_key.pem diff --git a/backend/tls_localhost_cert.pem b/backend/tls_localhost_cert.pem deleted file mode 100644 index 267ce0d5..00000000 --- a/backend/tls_localhost_cert.pem +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIUCmJjl3HAeLmrPwRg+/OzikW6peQwDQYJKoZIhvcNAQEL -BQAwazELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9u -ZG9uMQ4wDAYDVQQKDAVBbHJvczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDESMBAG -A1UEAwwJbG9jYWxob3N0MB4XDTI0MTEwNDIxNDcwMFoXDTM0MTEwMjIxNDcwMFow -azELMAkGA1UEBhMCR0IxDzANBgNVBAgMBkxvbmRvbjEPMA0GA1UEBwwGTG9uZG9u -MQ4wDAYDVQQKDAVBbHJvczEWMBQGA1UECwwNSVQgRGVwYXJ0bWVudDESMBAGA1UE -AwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs368 -ExLSudP8luNoY5UfaPqBSVJUPYBi+JGyd36tyN75p5OI7xSfHTttQxuD4KrExBFP -C8mAhE1eoZPBVBOZJ4FYWBJfMaQnCjeqU+laP36td65kSJYbUYlKYH1WpxEpCdgx -wWOKkP/kPX5YXbYqODx9aBJXgoT3yAJW7AniIoL+eLFnS9Xo86TPqCDBTJU9ocwK -gPIDLhDv60724rhZT1kbGp7ECqRovndoDTQjuws2D3yNMfQ+4rrQGPXHGmP5PcaR -0R7uueB+6APyC7MJbuhbxxg/+DFHrRi3lJsgwxuh2hi/+vWw8zgKlgYIwHFA9X0l -cX0UlQdENMH3bgcGIwIDAQABo1MwUTAdBgNVHQ4EFgQUUFGxw7zoiHXGwRqtagjZ -RPYc85cwHwYDVR0jBBgwFoAUUFGxw7zoiHXGwRqtagjZRPYc85cwDwYDVR0TAQH/ -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEALokb1z2lu3qW141b2wm14ilZQKCZ -reNNuUR95Uom96FXPH4QVEH+mYTXXJ5UrfNhQYKQFpdE+5S4HL/UqEOxtWvbAHpK -nsLQ62J8m+0+uwiJGqeQpWr03KJgXDAVE9X3XwMlp/+buxSLhc+GIHWuXW56itV2 -jiZJYjhO5SnhhgTWNoVZk93qXuuWEN0yacw7c3Fr1IvFYYYWufbXTk70dbZihPDK -VD141o8tpp6FerSKHNYDqkVFDyTz3DVOhQQJ59zfMre7bFr+PpTTl4vIuGzXEY+E -HPjUSlOzwkCoh5fu7Fs3qG55rJt8akhTEoKpiBTaLucgAjVWNHeci1+Yxg== ------END CERTIFICATE----- diff --git a/backend/tls_localhost_key.pem b/backend/tls_localhost_key.pem deleted file mode 100644 index 32801b3c..00000000 --- a/backend/tls_localhost_key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzfrwTEtK50/yW -42hjlR9o+oFJUlQ9gGL4kbJ3fq3I3vmnk4jvFJ8dO21DG4PgqsTEEU8LyYCETV6h -k8FUE5kngVhYEl8xpCcKN6pT6Vo/fq13rmRIlhtRiUpgfVanESkJ2DHBY4qQ/+Q9 -flhdtio4PH1oEleChPfIAlbsCeIigv54sWdL1ejzpM+oIMFMlT2hzAqA8gMuEO/r -TvbiuFlPWRsansQKpGi+d2gNNCO7CzYPfI0x9D7iutAY9ccaY/k9xpHRHu654H7o -A/ILswlu6FvHGD/4MUetGLeUmyDDG6HaGL/69bDzOAqWBgjAcUD1fSVxfRSVB0Q0 -wfduBwYjAgMBAAECggEACTqdSExxzJ+LX5ARFaWyOBSWly2GKqSyR14+aInOklhx -9QgkmfOxJrCf3TvJ8RWhXloW0Aqr8qGDxG0Ixgjn7rG7gskXCey1xn8MNppLS0kj -ztaG+NB3AR89ABm8XdoHsSY45geh3/Ni9I0i1VardGQafUJhgNLTZqjwIodzkBtJ -S/bi4uFk1lGNfuvWQvWqzGXUvd1l1YupV6iA4GfhXlUvrSBZwftLBD6xEvQaSqsA -pHvBxTfMXG4RMAkNPDIElkuQ8++CGi1gIRkJfmrv4OgbbitteMnxqqqGYV0zSNCg -R/5FG6umIV7lDLBHZCSCk7wmfmq2UUvzhHThHy4yMQKBgQDu4TwFJCIcVIj7Wj4r -DUBFvz6Lgbltqb+YAMUBtpiDcAQxDJWmedh6dK04ts5CFAFRlRjjuz2uFn7qlVBm -uye9R7tL+tOv5viqDXU78a4snFywoXub6yzpbxrW8B4W1pdIUvQmhwCcDwvO1V24 -7Vj2vxcM5I9dsk1aCQSi3VY5yQKBgQDAW/VoTRwhU6OUc6sji5Z5dnkMjkP6NZK9 -CSrTWLAMGaLPY+g6fFS7JMNSvfWm/okypD6rcN7p0cxMK3mfFKmMiyPRde0wdrci -sGFjGxM/2d2D7KTMC9iMYwA0K17UIna+UiYPfhR/muIg/dCyjlkKDFs9Z4jk//r1 -91bmznt2iwKBgFdiYXhn/Wprqih4nKFXGZnqGdEixVhObl4GegrkZuo+AeqHdf8O -N5ikMfG7PbyCYPEdH5u/FRMn+4mI0X6jHChroyJqQSHp1jEu9yHUiSicknOyvusM -nsNN932FHRyxp2m3nsSxQhHUlzc0ajKJ8K9iu+XlfmSCIzW6cs25Nh+xAoGBAJro -M0wIdPPdsCj3sUVRvx8XqknTM6kGhaIYBNXoYPWNm5BaC4U15OJEq8sxUOdnqcMP -g6x6m/k+S8C3bh0O/a9Bydl/l0BlCfw0gGjYP/s2ju4Tn272xy/e9iYNGzPIgUmp -TB9D0GwmpZ4d6HgyrD+sTbm4bATGpCp6QhBjDggbAoGBAJVMMtZ4pF8D6mLMRZGR -pQjNPy+MH13XYmDRc/BSF8KJ4yKk3tohr9LSXzxR0SEB43NoL1bHkucZrNjGyL8x -jktnwkoIs96kO2mPrl1TqWkXs5RjGkkSTbAJovIcvkRU31SWap/WzN2kHpmRVcQc -KEFKXT5fUYZCLLWxhgZFlGPp ------END PRIVATE KEY----- From 9c65b402251a8307215c8f437579a2aaa62758f9 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 13:07:37 +0200 Subject: [PATCH 068/155] adapt new hostname schema and move to https URLs --- config/config.devenv.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/config.devenv.json b/config/config.devenv.json index b0e38ee4..59608d13 100644 --- a/config/config.devenv.json +++ b/config/config.devenv.json @@ -1,8 +1,8 @@ { "default_server_config": { "m.homeserver": { - "base_url": "http://synapse.localhost:8008", - "server_name": "synapse.localhost" + "base_url": "https://synapse.m.localhost", + "server_name": "synapse.m.localhost" } }, "features": { From 25afbf430322aeaec83fa2bb4ff2feaedf6217ad Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 14:20:51 +0200 Subject: [PATCH 069/155] base playwright backend on dev-backend --- backend/ew.test.config.json | 4 +- backend/playwright_homeserver.yaml | 4 +- ...wright-backend-docker-compose.override.yml | 4 + playwright-backend-docker-compose.yml | 99 +------------------ 4 files changed, 10 insertions(+), 101 deletions(-) create mode 100644 playwright-backend-docker-compose.override.yml diff --git a/backend/ew.test.config.json b/backend/ew.test.config.json index fac478dd..3644714b 100644 --- a/backend/ew.test.config.json +++ b/backend/ew.test.config.json @@ -1,8 +1,8 @@ { "default_server_config": { "m.homeserver": { - "base_url": "http://synapse.localhost:8008", - "server_name": "synapse.localhost" + "base_url": "http://synapse.m.localhost:8008", + "server_name": "synapse.m.localhost" } }, "disable_custom_urls": false, diff --git a/backend/playwright_homeserver.yaml b/backend/playwright_homeserver.yaml index d4d0a041..ca45cf3f 100644 --- a/backend/playwright_homeserver.yaml +++ b/backend/playwright_homeserver.yaml @@ -1,5 +1,5 @@ -server_name: "synapse.localhost" -public_baseurl: http://synapse.localhost:8008/ +server_name: "synapse.m.localhost" +public_baseurl: https://synapse.m.localhost/ pid_file: /data/homeserver.pid diff --git a/playwright-backend-docker-compose.override.yml b/playwright-backend-docker-compose.override.yml new file mode 100644 index 00000000..34f01682 --- /dev/null +++ b/playwright-backend-docker-compose.override.yml @@ -0,0 +1,4 @@ +services: + synapse: + volumes: + - ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z \ No newline at end of file diff --git a/playwright-backend-docker-compose.yml b/playwright-backend-docker-compose.yml index e5cf12b5..4ac42faf 100644 --- a/playwright-backend-docker-compose.yml +++ b/playwright-backend-docker-compose.yml @@ -1,97 +1,2 @@ -networks: - ecbackend: - -services: - auth-service: - image: ghcr.io/element-hq/lk-jwt-service:latest-ci - hostname: auth-server - environment: - - LK_JWT_PORT=8080 - - LIVEKIT_URL=ws://localhost:7880 - - LIVEKIT_KEY=devkey - - LIVEKIT_SECRET=secret - # If the configured homeserver runs on localhost, it'll probably be using - # a self-signed certificate - - LIVEKIT_INSECURE_SKIP_VERIFY_TLS=YES_I_KNOW_WHAT_I_AM_DOING - deploy: - restart_policy: - condition: on-failure - ports: - # HOST_PORT:CONTAINER_PORT - - 8009:8080 - networks: - - ecbackend - - livekit: - image: livekit/livekit-server:latest - command: --dev --config /etc/livekit.yaml - restart: unless-stopped - # The SFU seems to work far more reliably when we let it share the host - # network rather than opening specific ports (but why?? we're not missing - # any…) - ports: - # HOST_PORT:CONTAINER_PORT - - 7880:7880/tcp - - 7881:7881/tcp - - 7882:7882/tcp - - 50100-50200:50100-50200/udp - volumes: - - ./backend/dev_livekit.yaml:/etc/livekit.yaml:Z - networks: - - ecbackend - - redis: - image: redis:6-alpine - command: redis-server /etc/redis.conf - ports: - # HOST_PORT:CONTAINER_PORT - - 6379:6379 - volumes: - - ./backend/redis.conf:/etc/redis.conf:Z - networks: - - ecbackend - - element-web: - image: ghcr.io/element-hq/element-web:develop - volumes: - - ./backend/ew.test.config.json:/app/config.json - environment: - ELEMENT_WEB_PORT: 81 - ports: - - "8081:81" - networks: - - ecbackend - - synapse: - hostname: homeserver - image: docker.io/matrixdotorg/synapse:latest - environment: - - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml - # Needed for rootless podman-compose such that the uid/gid mapping does - # fit local user uid. If the container runs as root (uid 0) it is fine as - # it actually maps to your non-root user on the host (e.g. 1000). - # Otherwise uid mapping will not match your non-root user. - - UID=0 - - GID=0 - volumes: - - ./backend/synapse_tmp:/data:Z - - ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z - networks: - - ecbackend - - nginx: - # openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls_localhost_key.pem -out tls_localhost_cert.pem -subj "/C=GB/ST=London/L=London/O=Alros/OU=IT Department/CN=localhost" - hostname: synapse.localhost - image: nginx:latest - volumes: - - ./backend/tls_localhost_nginx.conf:/etc/nginx/conf.d/default.conf:Z - - ./backend/tls_localhost_key.pem:/root/ssl/key.pem:Z - - ./backend/tls_localhost_cert.pem:/root/ssl/cert.pem:Z - ports: - # HOST_PORT:CONTAINER_PORT - - "8008:80" - - "4443:443" - depends_on: - - synapse - networks: - - ecbackend +include: + - dev-backend-docker-compose.yml \ No newline at end of file From 76fb16dbfd391b5c94f9a476f3ae731a72a7a3d9 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 14:44:21 +0200 Subject: [PATCH 070/155] update README.md to reflect TLS settings --- README.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index db7cc43f..f7530e4c 100644 --- a/README.md +++ b/README.md @@ -207,12 +207,19 @@ See also: A docker compose file `dev-backend-docker-compose.yml` is provided to start the whole stack of components which is required for a local development environment: -- Minimum Synapse Setup (servername: `synapse.localhost`) -- LiveKit JWT Service (Note requires Federation API and hence a TLS reverse proxy) -- Minimum TLS reverse proxy (servername: `synapse.localhost`) Note certificates - are valid for at least 10 years from now +- Minimum Synapse Setup (servername: `synapse.m.localhost`) +- LiveKit Authorization Service (Note requires Federation API and hence a TLS reverse proxy) - Minimum LiveKit SFU Setup using dev defaults for config - Redis db for completeness +- Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS) + - Hostnames: `m.localhost`, `*.m.localhost` + - Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted + certificates +- Minimum TLS reverse proxy for + - Synapse homeserver: `synapse.m.localhost` + - MatrixRTC backend: `matrix-rtc.m.localhost` + - Local Element Call development `call.m.localhost` + - Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT These use a test 'secret' published in this repository, so this must be used only for local development and **_never be exposed to the public Internet._** @@ -226,11 +233,14 @@ yarn backend ``` > [!NOTE] -> To ensure your local development frontend functions properly, you’ll need to add -> certificate exceptions in your browser for both `https://localhost:3000` and -> `https://synapse.localhost/.well-known/matrix/client`. The easiest way to do this -> is to simply copy and paste each URL into your browser’s address bar and follow -> the prompts to add the exception. +> To ensure your local development frontend functions properly, you’ll need to +> add certificate exceptions in your browser for `https://localhost:3000`, +> `https://matrix-rtc.m.localhost/livekit/jwt/healthz` and +> `https://synapse.m.localhost/.well-known/matrix/client`. This can be either +> done by adding the minimum localhost CA +> ([./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt)) to your web +> browsers trusted certificates or by simply copying and pasting each URL into +> your browser’s address bar and follow the prompts to add the exception. ### Playwright tests From 005402d8d148110e1aee3c548e8a358c0b454a16 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 14:48:45 +0200 Subject: [PATCH 071/155] prettier --- README.md | 4 ++-- playwright-backend-docker-compose.override.yml | 2 +- playwright-backend-docker-compose.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f7530e4c..ac9a61db 100644 --- a/README.md +++ b/README.md @@ -212,8 +212,8 @@ whole stack of components which is required for a local development environment: - Minimum LiveKit SFU Setup using dev defaults for config - Redis db for completeness - Minimum `localhost` Certificate Authority (CA) for Transport Layer Security (TLS) - - Hostnames: `m.localhost`, `*.m.localhost` - - Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted + - Hostnames: `m.localhost`, `*.m.localhost` + - Add [./backend/dev_tls_local-ca.crt](./backend/dev_tls_local-ca.crt) to your web browsers trusted certificates - Minimum TLS reverse proxy for - Synapse homeserver: `synapse.m.localhost` diff --git a/playwright-backend-docker-compose.override.yml b/playwright-backend-docker-compose.override.yml index 34f01682..dadbccc2 100644 --- a/playwright-backend-docker-compose.override.yml +++ b/playwright-backend-docker-compose.override.yml @@ -1,4 +1,4 @@ services: synapse: volumes: - - ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z \ No newline at end of file + - ./backend/playwright_homeserver.yaml:/data/cfg/homeserver.yaml:Z diff --git a/playwright-backend-docker-compose.yml b/playwright-backend-docker-compose.yml index 4ac42faf..bb6686d0 100644 --- a/playwright-backend-docker-compose.yml +++ b/playwright-backend-docker-compose.yml @@ -1,2 +1,2 @@ include: - - dev-backend-docker-compose.yml \ No newline at end of file + - dev-backend-docker-compose.yml From 34a223f04b54b950080dc26001db00d5d1e25987 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 14:54:14 +0200 Subject: [PATCH 072/155] add yarn dev --host hint --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac9a61db..9fb653f5 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ whole stack of components which is required for a local development environment: - Minimum TLS reverse proxy for - Synapse homeserver: `synapse.m.localhost` - MatrixRTC backend: `matrix-rtc.m.localhost` - - Local Element Call development `call.m.localhost` + - Local Element Call development `call.m.localhost` via `yarn dev --host ` - Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT These use a test 'secret' published in this repository, so this must be used From 9dcaa60982ab864e07e60c7e298afa35a94e012e Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 15:26:47 +0200 Subject: [PATCH 073/155] added app.m.localhost for element web --- backend/dev_nginx.conf | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index 1b201a89..59a25ba0 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -88,7 +88,6 @@ server { } - # Convenience reverse proxy for the call.m.localhost domain to yarn dev --host server { listen 80; @@ -120,4 +119,37 @@ server { error_page 500 502 503 504 /50x.html; +} + +# Convenience reverse proxy app.m.localhost for element web +server { + listen 80; + listen [::]:80; + server_name app.m.localhost; + + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name app.m.localhost; + ssl_certificate /root/ssl/cert.pem; + ssl_certificate_key /root/ssl/key.pem; + + + location ^~ / { + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://element-web:81; + proxy_ssl_verify off; + + } + + error_page 500 502 503 504 /50x.html; + } \ No newline at end of file From 679ff8c2afe027da9c7617b6de7119aee1d03857 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 15:47:03 +0200 Subject: [PATCH 074/155] adapt to TLS setup --- README.md | 1 + backend/ew.test.config.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9fb653f5..510b7c76 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ whole stack of components which is required for a local development environment: - Synapse homeserver: `synapse.m.localhost` - MatrixRTC backend: `matrix-rtc.m.localhost` - Local Element Call development `call.m.localhost` via `yarn dev --host ` + - Element Web `app.m.localhost` - Note certificates will expire on Thu, 03 May 2035 10:32:02 GMT These use a test 'secret' published in this repository, so this must be used diff --git a/backend/ew.test.config.json b/backend/ew.test.config.json index 3644714b..52be51b8 100644 --- a/backend/ew.test.config.json +++ b/backend/ew.test.config.json @@ -1,7 +1,7 @@ { "default_server_config": { "m.homeserver": { - "base_url": "http://synapse.m.localhost:8008", + "base_url": "https://synapse.m.localhost", "server_name": "synapse.m.localhost" } }, From 128851263b2191090926eb3e9bad0daf77b9a81f Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 16:01:58 +0200 Subject: [PATCH 075/155] use ssl cert from mini localhost CA --- vite.config.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/vite.config.js b/vite.config.js index 590f3c16..4985e75a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -11,8 +11,8 @@ import { createHtmlPlugin } from "vite-plugin-html"; import { codecovVitePlugin } from "@codecov/vite-plugin"; import { sentryVitePlugin } from "@sentry/vite-plugin"; import react from "@vitejs/plugin-react"; -import basicSsl from "@vitejs/plugin-basic-ssl"; import { realpathSync } from "fs"; +import * as fs from "node:fs"; // https://vitejs.dev/config/ export default defineConfig(({ mode, packageType }) => { @@ -24,7 +24,6 @@ export default defineConfig(({ mode, packageType }) => { process.env.VITE_PACKAGE = packageType ?? "full"; const plugins = [ react(), - basicSsl(), svgrPlugin({ svgrOptions: { // This enables ref forwarding on SVGR components, which is needed, for @@ -83,7 +82,11 @@ export default defineConfig(({ mode, packageType }) => { return { server: { port: 3000, - fs: { allow }, + fs: { allow }, + https: { + key: fs.readFileSync('./backend/dev_tls_m.localhost.key'), + cert: fs.readFileSync('./backend/dev_tls_m.localhost.crt'), + }, }, build: { sourcemap: true, From f2b68e6deeaad13fbe0be60ed82646db46ffc1db Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 16:02:19 +0200 Subject: [PATCH 076/155] add localhost domain to certificate --- backend/dev_tls_m.localhost.crt | 37 +++++++++++------------ backend/dev_tls_m.localhost.key | 52 ++++++++++++++++----------------- backend/dev_tls_setup | 5 ++-- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/backend/dev_tls_m.localhost.crt b/backend/dev_tls_m.localhost.crt index be3eb0a3..5d6251a9 100644 --- a/backend/dev_tls_m.localhost.crt +++ b/backend/dev_tls_m.localhost.crt @@ -1,20 +1,21 @@ -----BEGIN CERTIFICATE----- -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 +v2z2/aMI/7sAD2Q= -----END CERTIFICATE----- diff --git a/backend/dev_tls_m.localhost.key b/backend/dev_tls_m.localhost.key index d83c1dea..73d89ce4 100644 --- a/backend/dev_tls_m.localhost.key +++ b/backend/dev_tls_m.localhost.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCbr79gttr7X8j+ -ISfdCV53PD8fR6JsLf6nmkCbRqCaIq85Y82tnYbUB3B6F9RcosrxF+UHFMa/i1Ui -LSNL0GHisclB5LII2RycsLJYShkO9pVioVDf3gh+hyVRySBQ2FgtLHB+ZgcZOCG8 -f75g9CdeVDmvKw4J29QV8bxFSafvTLOdqtupylfTSqYVTAE8HnIOsdnZ+mE6SjeS -2wV3DYqdSXoaxWmGranZUmrCgeZdukAZTWgAlHgQvuWVtgyAxPmhcr2KA50QHB/I -J2SDIaUiI++R4nXkVChbePnNaxqw0kc0QD3Jpd3B1QhHlOhKi9R6Mo5Iyf0nsHnZ -aQ0bAzPDAgMBAAECggEARLRazvnzCnLbVrbYCjX7v7/RFWM9/OKRWnJ6p2uULWE4 -FaoDFuaJHSHJU8AXYegfiiTi1+ylxtrcr4/e3zKvN+UAbXlYzgnOFCHwGoFcrJtK -EnQhJiIsenX2lLCe9755rznIzScGY+0/ChoPsGaexwSBTlnAQL6HykVbMfKOz03H -ywEx4g3AK1rgTnqNLFHkl+1ainoW6ffeM6thMD/bObGz+PoGSMqbTA80TGMswgMN -Ipnt0AwSgKweLmYG00t667c9htxY6DPRUoJ55dqsAFS8VMa4hhcslyhktPXTGEXh -x2r8UAFavEo2IdRnR8vfNfOv6twsWSHTVRGc7qmKDQKBgQDX0HnMAnBb8KB1zj/O -1prhAlhc6Jtwf3s5Hm/2MW0Jg/u7bZx81s206rvcTJtUJ2ROH+K7Rx3iASWzcsuW -XljCWA9G156SuOBE6mIS1EMI1EKgjbJBru1cOco6AIwI0SuJKcEX/1RtzoBbIIbZ -qhn99RszqAKDjw1iqbpyZCX5PQKBgQC4rRLsMTVvFTqWPEAA7SeJr3LZF+eoap/U -1+MA+J49D5ykQMFHjL1VSdfWgKIm3i4xDbDLAX1BYELxeKVLIp6CL808zEldGQy5 -g+O4dJlmz1PUGorb28qKGJnfwXK7F5tJuX+NgQM2zJnueyTv+fsskBp79CWNQvzr -ueG41o6w/wKBgG7sA+3LQxy+LHrgKwOQYcJMhkYad+n2W8sbzcfn13cQkw3eZJP1 -g3z9ONkdtqgmJvPQh6RiBQXoOQxmcCU1EMGyqQdsQ2B+DSbeoNG0r0+WaThEG96O -ngjM2xe8uDy/5XR2NXy0Cxz1ChvMOAMf3oQcuoJuU/xyRhrzyZSJzMqxAoGAH8hx -nEKvzolZxudhoIcwKcsPOfuaO+r1zPzGrbEcEqgwLjiSywyWvSnzQpBq18OfMYQI -rDd6Zhj6DHLWB8NSgldVvCPwcFxSS08+js1KZV5DMBrNUR9XkULAoLi7VSWv7RVG -tYTBl9nImDmLVt2v87BtTm3rVI911d/s0BHlBuMCgYEAs0AFMsTE+22Y44JMcTAE -OeHEsEDXI5cTlcNmwFKWY+UCZnb2FXflO2XNeqyi6ReYMUyBI2wHdUGvh2B1c2Ac -3z/SShBLS7bMGgyvYE/By1xnemiy+6vG2NIYHKExZfOphx8rDTfm5Qlj6LxstY9+ -Tx2VzAs01UIZGDhJ94u5imo= +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrzGSScSgaQuZd +ELGFYiLiYRwrLKyUdNr0rsPcOo0bvbeZ3zQMeUMRNlA69zGFdarumiDRXUoAmZI3 +9WmH95aX3d+AU7EFnWev7xpWSVhSYj8T0d4rke8HjGk3LpaffJ93tbJuagBIH1ou +uN6AOdzWs8hpRYIomWleEeeuVnnfaMwaXOdc+ihJJ6wzm2hwQSfdpjZPWBDd/DFf +t1ZXxIZOCjDsrEIiI7uU8iZPLB3QEM/tgxSSAOxrcKvQvxZokk+FD7aMJFP71Ifi +eLCEzMTP1VXatP7UTAKAqB2NyDJ8m3IHbOINiqcdFvFR3R1D9bXOYE4oRynNvYZr +QUGnL2RtAgMBAAECggEAJaFQii8U/KOYt9vXNoMnZvSkaeSQLLhn2V6Kciu1CtWE +aMTWLsFE6nk+G5xXkYcTmM3T0GghtH3u5CjyI6EcsEkeEorCZJt0wbmayDmqiekR +LfMzOdHuTHX5+edPgMGYYG1BFyRKyYFsjH1b5zRFZhXdGQnrl5760GsVlz9D1KZQ +iHcT+q1S2tmZeoUukQnADENKXUMCyTGM5FCddgNtsWnGDsTDayh7hUdvDkB+mW4G +lSp+BZuc3PCwpbD6qkXvfugWs6CUAAtXoV3ceWgxQ+TEnNlwxaG1AyugfgNUBolk +8xgeZt4r5QId03jsHDf7hpBAofcaCd5EMIIQYFvWoQKBgQDlbAvAzEFPTZZn2nRV +Xagw4xjqVc1LLEKLCWq0N5rEkwn0h90Dz5N7/3NuonP/sIDsDHCbyiOYBI1Ck6Xi +0WuB+OyKDh+xeF2mekN9G9ywPahdK5lT/TVsxXFyZlwtVv1x/6KBO4yv5URizxqU +gyAPDDxfD/KcNjkOBaodWEwQGQKBgQC/s2gPDBtQkjLwkHXchBomLww5eLlVrac1 +WK4UX6uSdOgrjJ375OOgMTxe8NVZdOuAKytGXRWDwgH3nVWvuZhe7dGlX3JMuSer +e9VwDpBESrvqcR4ruL6wm8wej6BXyjH0wD3FHb0S5HfuBDxTn+4bDwrbRzOUMNgy +lSppuflxdQKBgQDiZcIfazFT8evn5nMAvuC4BZNTxIJHmZC9JfjPiUPIkpWzYtOe +7BvNtKOT3Op9uw8uYYRKqKqBXJSNy6ha8XCXHS9HeXKbLn20SFkLQBCDNwVLlDfF +40zyXtF6JDr4XyzSb4NM5pgKCER5AYloXxGm59s3sEQpFXUuOjbKqJS/GQKBgAoI +c7vF4HAZFr1sch62cz/oWnVvkhOf4Q5zs7ixQSOLJtOQqnwSgK9TpFs7s47ZBbJR +kBRAru2Ua9Hv1Bo8VnMxczV6h1roneDlvEf/GyHX33nnrbKQGrrXjJlU3wl5NaAf +p5v3cHvapUQ5yIZ/6lBUOzc6xMJOxCHxmKSr7Rg5AoGAbEE4lt6Xh2dnBPJ81eNI +IDrw/3ITY53qAY4Bx88CByIFuu8CEUdUZprh98jSl6ic1tMinZfUhRMwABLrUD51 +DGst8iGLPD9u83iMcUHI/L+p7AbxrKLvWXZrF5UZm440c9mSWqfhPaTBosPtNDsG +LfETwH1flKXMTXd2xA9RTE4= -----END PRIVATE KEY----- diff --git a/backend/dev_tls_setup b/backend/dev_tls_setup index 4276e148..8a778dc8 100644 --- a/backend/dev_tls_setup +++ b/backend/dev_tls_setup @@ -31,7 +31,8 @@ extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] -DNS.1 = m.localhost -DNS.2 = *.m.localhost +DNS.1 = localhost +DNS.2 = m.localhost +DNS.3 = *.m.localhost EOF ) From 48b4fd63fe1e24689cec49971d146cad0a71fbae Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 16:09:34 +0200 Subject: [PATCH 077/155] prettier --- vite.config.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/vite.config.js b/vite.config.js index 4985e75a..5b800f7a 100644 --- a/vite.config.js +++ b/vite.config.js @@ -82,11 +82,11 @@ export default defineConfig(({ mode, packageType }) => { return { server: { port: 3000, - fs: { allow }, - https: { - key: fs.readFileSync('./backend/dev_tls_m.localhost.key'), - cert: fs.readFileSync('./backend/dev_tls_m.localhost.crt'), - }, + fs: { allow }, + https: { + key: fs.readFileSync("./backend/dev_tls_m.localhost.key"), + cert: fs.readFileSync("./backend/dev_tls_m.localhost.crt"), + }, }, build: { sourcemap: true, From e12dfa9a9fb7523b234c4be8c367b1ee44e425ed Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 16:17:16 +0200 Subject: [PATCH 078/155] prettier --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index ab541644..2f693995 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "@use-gesture/react": "^10.2.11", "@vector-im/compound-design-tokens": "^3.0.0", "@vector-im/compound-web": "^7.2.0", - "@vitejs/plugin-basic-ssl": "^1.0.1", "@vitejs/plugin-react": "^4.0.1", "@vitest/coverage-v8": "^3.0.0", "babel-plugin-transform-vite-meta-env": "^1.0.3", From b7e5b81dbf2182a552c8875422d6d5e2159ab365 Mon Sep 17 00:00:00 2001 From: fkwp Date: Mon, 5 May 2025 16:25:18 +0200 Subject: [PATCH 079/155] update yarn.lock to remove plugin-basic-ssl --- yarn.lock | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 394e81a7..ef372359 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5127,15 +5127,6 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-basic-ssl@npm:^1.0.1": - version: 1.2.0 - resolution: "@vitejs/plugin-basic-ssl@npm:1.2.0" - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 - checksum: 10c0/0d360fcca01f91ade6e451edbea09a107ff9e95cd3c3766c7a069d1a168709df92d96c0bd1eccc66e2739a153e07c75a45321ec487450c0da942606200d8441d - languageName: node - linkType: hard - "@vitejs/plugin-react@npm:^4.0.1": version: 4.3.4 resolution: "@vitejs/plugin-react@npm:4.3.4" @@ -6968,7 +6959,6 @@ __metadata: "@use-gesture/react": "npm:^10.2.11" "@vector-im/compound-design-tokens": "npm:^3.0.0" "@vector-im/compound-web": "npm:^7.2.0" - "@vitejs/plugin-basic-ssl": "npm:^1.0.1" "@vitejs/plugin-react": "npm:^4.0.1" "@vitest/coverage-v8": "npm:^3.0.0" babel-plugin-transform-vite-meta-env: "npm:^1.0.3" From 380880137e50ff966aebacb0a6c9dbe4d32db839 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 5 May 2025 17:12:00 +0200 Subject: [PATCH 080/155] Keep the HTML lang attribute in sync with the i18next language (#3247) --- src/initializer.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/initializer.tsx b/src/initializer.tsx index 1087dc94..d0797e9d 100644 --- a/src/initializer.tsx +++ b/src/initializer.tsx @@ -136,6 +136,11 @@ export class Initializer { lookup: () => getUrlParams().lang ?? undefined, }); + // Synchronise the HTML lang attribute with the i18next language + i18n.on("languageChanged", (lng) => { + document.documentElement.lang = lng; + }); + await i18n .use(Backend) .use(languageDetector) From 9f727b904b4d3db8222a86e336e57cfd0ed8d56c Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 9 May 2025 10:48:09 -0400 Subject: [PATCH 081/155] Remove patched version of WebCodecs types The patch has now been upstreamed to @types/dom-webcodecs. --- knip.ts | 4 + package.json | 1 + src/@types/dom-mediacapture-transform.d.ts | 146 ---- src/@types/dom-webcodecs.d.ts | 745 --------------------- yarn.lock | 17 + 5 files changed, 22 insertions(+), 891 deletions(-) delete mode 100644 src/@types/dom-mediacapture-transform.d.ts delete mode 100644 src/@types/dom-webcodecs.d.ts diff --git a/knip.ts b/knip.ts index 05bd029d..2381356c 100644 --- a/knip.ts +++ b/knip.ts @@ -27,6 +27,10 @@ export default { // then Knip will flag it as a false positive // https://github.com/webpro-nl/knip/issues/766 "@vector-im/compound-web", + // We need this so that TypeScript is happy with @livekit/track-processors. + // This might be a bug in the LiveKit repo but for now we fix it on the + // Element Call side. + "@types/dom-mediacapture-transform", "matrix-widget-api", ], ignoreExportsUsedInFile: true, diff --git a/package.json b/package.json index 2f693995..74567edb 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.1", "@types/content-type": "^1.1.5", + "@types/dom-mediacapture-transform": "^0.1.11", "@types/grecaptcha": "^3.0.9", "@types/jsdom": "^21.1.7", "@types/lodash-es": "^4.17.12", diff --git a/src/@types/dom-mediacapture-transform.d.ts b/src/@types/dom-mediacapture-transform.d.ts deleted file mode 100644 index d4c1f8f8..00000000 --- a/src/@types/dom-mediacapture-transform.d.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* eslint-disable */ -// The contents of this file below the line are copied from -// @types/dom-mediacapture-transform, which is inlined here into Element Call so -// that we can apply the patch to @types/dom-webcodecs found in -// ./dom-webcodecs.d.ts, which it depends on. -// (https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72625) -// Once that PR is merged and released, we can remove this file and return to -// depending on @types/dom-mediacapture-transform. -// ----------------------------------------------------------------------------- - -// This project is licensed under the MIT license. -// Copyrights are respective of each contributor listed at the beginning of each definition file. - -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -// In general, these types are only available behind a command line flag or an origin trial in -// Chrome 90+. - -// This API depends on WebCodecs. - -// Versioning: -// Until the above-mentioned spec is finalized, the major version number is 0. Although not -// necessary for version 0, consider incrementing the minor version number for breaking changes. - -// The following modify existing DOM types to allow defining type-safe APIs on audio and video tracks. - -/** Specialize MediaStreamTrack so that we can refer specifically to an audio track. */ -interface MediaStreamAudioTrack extends MediaStreamTrack { - readonly kind: "audio"; - clone(): MediaStreamAudioTrack; -} - -/** Specialize MediaStreamTrack so that we can refer specifically to a video track. */ -interface MediaStreamVideoTrack extends MediaStreamTrack { - readonly kind: "video"; - clone(): MediaStreamVideoTrack; -} - -/** Assert that getAudioTracks and getVideoTracks return the tracks with the appropriate kind. */ -interface MediaStream { - getAudioTracks(): MediaStreamAudioTrack[]; - getVideoTracks(): MediaStreamVideoTrack[]; -} - -// The following were originally generated from the spec using -// https://github.com/microsoft/TypeScript-DOM-lib-generator, then heavily modified. - -/** - * A track sink that is capable of exposing the unencoded frames from the track to a - * ReadableStream, and exposes a control channel for signals going in the oppposite direction. - */ -interface MediaStreamTrackProcessor { - /** - * Allows reading the frames flowing through the MediaStreamTrack provided to the constructor. - */ - readonly readable: ReadableStream; - /** Allows sending control signals to the MediaStreamTrack provided to the constructor. */ - readonly writableControl: WritableStream; -} - -declare var MediaStreamTrackProcessor: { - prototype: MediaStreamTrackProcessor; - - /** Constructor overrides based on the type of track. */ - new ( - init: MediaStreamTrackProcessorInit & { track: MediaStreamAudioTrack }, - ): MediaStreamTrackProcessor; - new ( - init: MediaStreamTrackProcessorInit & { track: MediaStreamVideoTrack }, - ): MediaStreamTrackProcessor; -}; - -interface MediaStreamTrackProcessorInit { - track: MediaStreamTrack; - /** - * If media frames are not read from MediaStreamTrackProcessor.readable quickly enough, the - * MediaStreamTrackProcessor will internally buffer up to maxBufferSize of the frames produced - * by the track. If the internal buffer is full, each time the track produces a new frame, the - * oldest frame in the buffer will be dropped and the new frame will be added to the buffer. - */ - maxBufferSize?: number | undefined; -} - -/** - * Takes video frames as input, and emits control signals that result from subsequent processing. - */ -interface MediaStreamTrackGenerator - extends MediaStreamTrack { - /** - * Allows writing media frames to the MediaStreamTrackGenerator, which is itself a - * MediaStreamTrack. When a frame is written to writable, the frame’s close() method is - * automatically invoked, so that its internal resources are no longer accessible from - * JavaScript. - */ - readonly writable: WritableStream; - /** - * Allows reading control signals sent from any sinks connected to the - * MediaStreamTrackGenerator. - */ - readonly readableControl: ReadableStream; -} - -type MediaStreamAudioTrackGenerator = MediaStreamTrackGenerator & - MediaStreamAudioTrack; -type MediaStreamVideoTrackGenerator = MediaStreamTrackGenerator & - MediaStreamVideoTrack; - -declare var MediaStreamTrackGenerator: { - prototype: MediaStreamTrackGenerator; - - /** Constructor overrides based on the type of track. */ - new ( - init: MediaStreamTrackGeneratorInit & { - kind: "audio"; - signalTarget?: MediaStreamAudioTrack | undefined; - }, - ): MediaStreamAudioTrackGenerator; - new ( - init: MediaStreamTrackGeneratorInit & { - kind: "video"; - signalTarget?: MediaStreamVideoTrack | undefined; - }, - ): MediaStreamVideoTrackGenerator; -}; - -interface MediaStreamTrackGeneratorInit { - kind: MediaStreamTrackGeneratorKind; - /** - * (Optional) track to which the MediaStreamTrackGenerator will automatically forward control - * signals. If signalTarget is provided and signalTarget.kind and kind do not match, the - * MediaStreamTrackGenerator’s constructor will raise an exception. - */ - signalTarget?: MediaStreamTrack | undefined; -} - -type MediaStreamTrackGeneratorKind = "audio" | "video"; - -type MediaStreamTrackSignalType = "request-frame"; - -interface MediaStreamTrackSignal { - signalType: MediaStreamTrackSignalType; -} diff --git a/src/@types/dom-webcodecs.d.ts b/src/@types/dom-webcodecs.d.ts deleted file mode 100644 index 55e4c7d4..00000000 --- a/src/@types/dom-webcodecs.d.ts +++ /dev/null @@ -1,745 +0,0 @@ -/* eslint-disable */ -// The contents of this file below the line are copied from -// @types/dom-webcodecs, which is inlined here into Element Call so that we can -// apply the patch https://github.com/DefinitelyTyped/DefinitelyTyped/pull/72625 -// which is needed for TypeScript 5.8 compatibility. Once that PR is merged and -// released, we can remove this file and return to depending on -// @types/dom-webcodecs. -// ----------------------------------------------------------------------------- - -// This project is licensed under the MIT license. -// Copyrights are respective of each contributor listed at the beginning of each definition file. - -// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -// Versioning: -// Until the WebCodecs spec is finalized, the major version number is 0. I have chosen to use minor -// version 1 to denote the API as defined by the IDL files from the Chromium repo at -// https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/webcodecs. -// Please use a version number above 0.1 if using the spec at https://w3c.github.io/webcodecs/ as -// the source. - -// The declarations in webcodecs.generated.d.ts have been generated using the code in -// https://github.com/yume-chan/webcodecs-lib-generator. See -// https://github.com/yume-chan/webcodecs-lib-generator/blob/main/README.md for more detail. - -// The following declarations are copied from -// https://github.com/microsoft/TypeScript-DOM-lib-generator/blob/a75338e1ea8a958bf08a5745141d2ab8f14ba2ca/baselines/dom.generated.d.ts -// and modified to expand the types to include VideoFrame. - -/** Shim for OffscreenCanvas, which was removed in TS 4.4 */ -interface OffscreenCanvas extends EventTarget {} - -/** - * Replaces CanvasImageSource; only applies if WebCodecs is available. - */ -type CanvasImageSourceWebCodecs = - | HTMLOrSVGImageElement - | HTMLVideoElement - | HTMLCanvasElement - | ImageBitmap - | OffscreenCanvas - | VideoFrame; - -interface CanvasRenderingContext2D { - drawImage(image: CanvasImageSourceWebCodecs, dx: number, dy: number): void; - drawImage( - image: CanvasImageSourceWebCodecs, - dx: number, - dy: number, - dw: number, - dh: number, - ): void; - drawImage( - image: CanvasImageSourceWebCodecs, - sx: number, - sy: number, - sw: number, - sh: number, - dx: number, - dy: number, - dw: number, - dh: number, - ): void; - createPattern( - image: CanvasImageSourceWebCodecs, - repetition: string | null, - ): CanvasPattern | null; -} - -interface OffscreenCanvasRenderingContext2D { - drawImage(image: CanvasImageSourceWebCodecs, dx: number, dy: number): void; - drawImage( - image: CanvasImageSourceWebCodecs, - dx: number, - dy: number, - dw: number, - dh: number, - ): void; - drawImage( - image: CanvasImageSourceWebCodecs, - sx: number, - sy: number, - sw: number, - sh: number, - dx: number, - dy: number, - dw: number, - dh: number, - ): void; - createPattern( - image: CanvasImageSourceWebCodecs, - repetition: string | null, - ): CanvasPattern | null; -} - -/** - * Replaces ImageBitmapSource; only applies if WebCodecs is available. - */ -type ImageBitmapSourceWebCodecs = CanvasImageSourceWebCodecs | Blob | ImageData; - -declare function createImageBitmap( - image: ImageBitmapSourceWebCodecs, - options?: ImageBitmapOptions, -): Promise; -declare function createImageBitmap( - image: ImageBitmapSourceWebCodecs, - sx: number, - sy: number, - sw: number, - sh: number, - options?: ImageBitmapOptions, -): Promise; - -/** - * Replaces TexImageSource; only applies if WebCodecs is available. - */ -type TexImageSourceWebCodecs = - | ImageBitmap - | ImageData - | HTMLImageElement - | HTMLCanvasElement - | HTMLVideoElement - | OffscreenCanvas - | VideoFrame; - -interface WebGLRenderingContextOverloads { - texImage2D( - target: GLenum, - level: GLint, - internalformat: GLint, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; - texSubImage2D( - target: GLenum, - level: GLint, - xoffset: GLint, - yoffset: GLint, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; -} - -interface WebGL2RenderingContextBase { - texImage3D( - target: GLenum, - level: GLint, - internalformat: GLint, - width: GLsizei, - height: GLsizei, - depth: GLsizei, - border: GLint, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; - texSubImage3D( - target: GLenum, - level: GLint, - xoffset: GLint, - yoffset: GLint, - zoffset: GLint, - width: GLsizei, - height: GLsizei, - depth: GLsizei, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; -} - -interface WebGL2RenderingContextOverloads { - texImage2D( - target: GLenum, - level: GLint, - internalformat: GLint, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; - texImage2D( - target: GLenum, - level: GLint, - internalformat: GLint, - width: GLsizei, - height: GLsizei, - border: GLint, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; - texSubImage2D( - target: GLenum, - level: GLint, - xoffset: GLint, - yoffset: GLint, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; - texSubImage2D( - target: GLenum, - level: GLint, - xoffset: GLint, - yoffset: GLint, - width: GLsizei, - height: GLsizei, - format: GLenum, - type: GLenum, - source: TexImageSourceWebCodecs, - ): void; -} - -///////////////////////////// -/// webcodecs APIs -///////////////////////////// - -interface AudioDataCopyToOptions { - format?: AudioSampleFormat | undefined; - frameCount?: number | undefined; - frameOffset?: number | undefined; - planeIndex: number; -} - -interface AudioDataInit { - data: AllowSharedBufferSource; - format: AudioSampleFormat; - numberOfChannels: number; - numberOfFrames: number; - sampleRate: number; - timestamp: number; -} - -interface AudioDecoderConfig { - codec: string; - description?: AllowSharedBufferSource | undefined; - numberOfChannels: number; - sampleRate: number; -} - -interface AudioDecoderInit { - error: WebCodecsErrorCallback; - output: AudioDataOutputCallback; -} - -interface AudioDecoderSupport { - config?: AudioDecoderConfig; - supported?: boolean; -} - -interface AudioEncoderConfig { - bitrate?: number | undefined; - codec: string; - numberOfChannels: number; - sampleRate: number; -} - -interface AudioEncoderInit { - error: WebCodecsErrorCallback; - output: EncodedAudioChunkOutputCallback; -} - -interface AudioEncoderSupport { - config?: AudioEncoderConfig; - supported?: boolean; -} - -interface AvcEncoderConfig { - format?: AvcBitstreamFormat | undefined; -} - -interface EncodedAudioChunkInit { - data: AllowSharedBufferSource; - duration?: number | undefined; - timestamp: number; - type: EncodedAudioChunkType; -} - -interface EncodedAudioChunkMetadata { - decoderConfig?: AudioDecoderConfig | undefined; -} - -interface EncodedVideoChunkInit { - data: AllowSharedBufferSource; - duration?: number | undefined; - timestamp: number; - type: EncodedVideoChunkType; -} - -interface EncodedVideoChunkMetadata { - decoderConfig?: VideoDecoderConfig | undefined; - temporalLayerId?: number | undefined; -} - -interface ImageDecodeOptions { - completeFramesOnly?: boolean | undefined; - frameIndex?: number | undefined; -} - -interface ImageDecodeResult { - complete: boolean; - image: VideoFrame; -} - -interface ImageDecoderInit { - colorSpaceConversion?: ColorSpaceConversion | undefined; - data: ImageBufferSource; - desiredHeight?: number | undefined; - desiredWidth?: number | undefined; - preferAnimation?: boolean | undefined; - premultiplyAlpha?: PremultiplyAlpha | undefined; - type: string; -} - -interface PlaneLayout { - offset: number; - stride: number; -} - -interface VideoColorSpaceInit { - fullRange?: boolean | null | undefined; - matrix?: VideoMatrixCoefficients | null | undefined; - primaries?: VideoColorPrimaries | null | undefined; - transfer?: VideoTransferCharacteristics | null | undefined; -} - -interface VideoDecoderConfig { - codec: string; - codedHeight?: number | undefined; - codedWidth?: number | undefined; - colorSpace?: VideoColorSpaceInit | undefined; - description?: AllowSharedBufferSource | undefined; - displayAspectHeight?: number | undefined; - displayAspectWidth?: number | undefined; - hardwareAcceleration?: HardwarePreference | undefined; - optimizeForLatency?: boolean | undefined; -} - -interface VideoDecoderInit { - error: WebCodecsErrorCallback; - output: VideoFrameOutputCallback; -} - -interface VideoDecoderSupport { - config?: VideoDecoderConfig; - supported?: boolean; -} - -interface VideoEncoderConfig { - alpha?: AlphaOption | undefined; - avc?: AvcEncoderConfig | undefined; - bitrate?: number | undefined; - bitrateMode?: VideoEncoderBitrateMode | undefined; - codec: string; - displayHeight?: number | undefined; - displayWidth?: number | undefined; - framerate?: number | undefined; - hardwareAcceleration?: HardwarePreference | undefined; - height: number; - latencyMode?: LatencyMode | undefined; - scalabilityMode?: string | undefined; - width: number; -} - -interface VideoEncoderEncodeOptions { - keyFrame?: boolean; -} - -interface VideoEncoderInit { - error: WebCodecsErrorCallback; - output: EncodedVideoChunkOutputCallback; -} - -interface VideoEncoderSupport { - config?: VideoEncoderConfig; - supported?: boolean; -} - -interface VideoFrameBufferInit { - codedHeight: number; - codedWidth: number; - colorSpace?: VideoColorSpaceInit | undefined; - displayHeight?: number | undefined; - displayWidth?: number | undefined; - duration?: number | undefined; - format: VideoPixelFormat; - layout?: PlaneLayout[] | undefined; - timestamp: number; - visibleRect?: DOMRectInit | undefined; -} - -interface VideoFrameCopyToOptions { - layout?: PlaneLayout[] | undefined; - rect?: DOMRectInit | undefined; -} - -interface VideoFrameInit { - alpha?: AlphaOption | undefined; - displayHeight?: number | undefined; - displayWidth?: number | undefined; - duration?: number | undefined; - timestamp?: number | undefined; - visibleRect?: DOMRectInit | undefined; -} - -interface AudioData { - readonly duration: number; - readonly format: AudioSampleFormat | null; - readonly numberOfChannels: number; - readonly numberOfFrames: number; - readonly sampleRate: number; - readonly timestamp: number; - allocationSize(options: AudioDataCopyToOptions): number; - clone(): AudioData; - close(): void; - copyTo( - destination: AllowSharedBufferSource, - options: AudioDataCopyToOptions, - ): void; -} - -declare var AudioData: { - prototype: AudioData; - new (init: AudioDataInit): AudioData; -}; - -interface AudioDecoderEventMap { - dequeue: Event; -} - -/** Available only in secure contexts. */ -interface AudioDecoder { - readonly decodeQueueSize: number; - readonly state: CodecState; - ondequeue: ((this: AudioDecoder, ev: Event) => any) | null; - close(): void; - configure(config: AudioDecoderConfig): void; - decode(chunk: EncodedAudioChunk): void; - flush(): Promise; - reset(): void; - addEventListener( - type: K, - listener: (this: AudioDecoder, ev: AudioDecoderEventMap[K]) => any, - options?: boolean | AddEventListenerOptions, - ): void; - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, - ): void; - removeEventListener( - type: K, - listener: (this: AudioDecoder, ev: AudioDecoderEventMap[K]) => any, - options?: boolean | EventListenerOptions, - ): void; - removeEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions, - ): void; -} - -declare var AudioDecoder: { - prototype: AudioDecoder; - new (init: AudioDecoderInit): AudioDecoder; - isConfigSupported(config: AudioDecoderConfig): Promise; -}; - -interface AudioEncoderEventMap { - dequeue: Event; -} - -/** Available only in secure contexts. */ -interface AudioEncoder { - readonly encodeQueueSize: number; - readonly state: CodecState; - ondequeue: ((this: AudioEncoder, ev: Event) => any) | null; - close(): void; - configure(config: AudioEncoderConfig): void; - encode(data: AudioData): void; - flush(): Promise; - reset(): void; - addEventListener( - type: K, - listener: (this: AudioEncoder, ev: AudioEncoderEventMap[K]) => any, - options?: boolean | AddEventListenerOptions, - ): void; - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, - ): void; - removeEventListener( - type: K, - listener: (this: AudioEncoder, ev: AudioEncoderEventMap[K]) => any, - options?: boolean | EventListenerOptions, - ): void; - removeEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions, - ): void; -} - -declare var AudioEncoder: { - prototype: AudioEncoder; - new (init: AudioEncoderInit): AudioEncoder; - isConfigSupported(config: AudioEncoderConfig): Promise; -}; - -interface EncodedAudioChunk { - readonly byteLength: number; - readonly duration: number | null; - readonly timestamp: number; - readonly type: EncodedAudioChunkType; - copyTo(destination: AllowSharedBufferSource): void; -} - -declare var EncodedAudioChunk: { - prototype: EncodedAudioChunk; - new (init: EncodedAudioChunkInit): EncodedAudioChunk; -}; - -interface EncodedVideoChunk { - readonly byteLength: number; - readonly duration: number | null; - readonly timestamp: number; - readonly type: EncodedVideoChunkType; - copyTo(destination: AllowSharedBufferSource): void; -} - -declare var EncodedVideoChunk: { - prototype: EncodedVideoChunk; - new (init: EncodedVideoChunkInit): EncodedVideoChunk; -}; - -/** Available only in secure contexts. */ -interface ImageDecoder { - readonly complete: boolean; - readonly completed: Promise; - readonly tracks: ImageTrackList; - readonly type: string; - close(): void; - decode(options?: ImageDecodeOptions): Promise; - reset(): void; -} - -// declare var ImageDecoder: { -// prototype: ImageDecoder; -// new(init: ImageDecoderInit): ImageDecoder; -// isTypeSupported(type: string): Promise; -// }; - -// interface ImageTrack { -// readonly animated: boolean; -// readonly frameCount: number; -// readonly repetitionCount: number; -// selected: boolean; -// } - -// declare var ImageTrack: { -// prototype: ImageTrack; -// new(): ImageTrack; -// }; - -// interface ImageTrackList { -// readonly length: number; -// readonly ready: Promise; -// readonly selectedIndex: number; -// readonly selectedTrack: ImageTrack | null; -// [index: number]: ImageTrack; -// } - -// declare var ImageTrackList: { -// prototype: ImageTrackList; -// new(): ImageTrackList; -// }; - -interface VideoColorSpace { - readonly fullRange: boolean | null; - readonly matrix: VideoMatrixCoefficients | null; - readonly primaries: VideoColorPrimaries | null; - readonly transfer: VideoTransferCharacteristics | null; - toJSON(): VideoColorSpaceInit; -} - -declare var VideoColorSpace: { - prototype: VideoColorSpace; - new (init?: VideoColorSpaceInit): VideoColorSpace; -}; - -interface VideoDecoderEventMap { - dequeue: Event; -} - -/** Available only in secure contexts. */ -interface VideoDecoder { - readonly decodeQueueSize: number; - readonly state: CodecState; - ondequeue: ((this: VideoDecoder, ev: Event) => any) | null; - close(): void; - configure(config: VideoDecoderConfig): void; - decode(chunk: EncodedVideoChunk): void; - flush(): Promise; - reset(): void; - addEventListener( - type: K, - listener: (this: VideoDecoder, ev: VideoDecoderEventMap[K]) => any, - options?: boolean | AddEventListenerOptions, - ): void; - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, - ): void; - removeEventListener( - type: K, - listener: (this: VideoDecoder, ev: VideoDecoderEventMap[K]) => any, - options?: boolean | EventListenerOptions, - ): void; - removeEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions, - ): void; -} - -declare var VideoDecoder: { - prototype: VideoDecoder; - new (init: VideoDecoderInit): VideoDecoder; - isConfigSupported(config: VideoDecoderConfig): Promise; -}; - -interface VideoEncoderEventMap { - dequeue: Event; -} - -/** Available only in secure contexts. */ -interface VideoEncoder { - readonly encodeQueueSize: number; - readonly state: CodecState; - close(): void; - ondequeue: ((this: VideoEncoder, ev: Event) => any) | null; - configure(config: VideoEncoderConfig): void; - encode(frame: VideoFrame, options?: VideoEncoderEncodeOptions): void; - flush(): Promise; - reset(): void; - addEventListener( - type: K, - listener: (this: VideoEncoder, ev: VideoEncoderEventMap[K]) => any, - options?: boolean | AddEventListenerOptions, - ): void; - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | AddEventListenerOptions, - ): void; - removeEventListener( - type: K, - listener: (this: VideoEncoder, ev: VideoEncoderEventMap[K]) => any, - options?: boolean | EventListenerOptions, - ): void; - removeEventListener( - type: string, - listener: EventListenerOrEventListenerObject, - options?: boolean | EventListenerOptions, - ): void; -} - -declare var VideoEncoder: { - prototype: VideoEncoder; - new (init: VideoEncoderInit): VideoEncoder; - isConfigSupported(config: VideoEncoderConfig): Promise; -}; - -interface VideoFrame { - readonly codedHeight: number; - readonly codedRect: DOMRectReadOnly | null; - readonly codedWidth: number; - readonly colorSpace: VideoColorSpace; - readonly displayHeight: number; - readonly displayWidth: number; - readonly duration: number | null; - readonly format: VideoPixelFormat | null; - readonly timestamp: number; - readonly visibleRect: DOMRectReadOnly | null; - allocationSize(options?: VideoFrameCopyToOptions): number; - clone(): VideoFrame; - close(): void; - copyTo( - destination: AllowSharedBufferSource, - options?: VideoFrameCopyToOptions, - ): Promise; -} - -declare var VideoFrame: { - prototype: VideoFrame; - new (source: CanvasImageSource, init?: VideoFrameInit): VideoFrame; - new (data: AllowSharedBufferSource, init: VideoFrameBufferInit): VideoFrame; -}; - -interface AudioDataOutputCallback { - (output: AudioData): void; -} - -interface EncodedAudioChunkOutputCallback { - (output: EncodedAudioChunk, metadata: EncodedAudioChunkMetadata): void; -} - -interface EncodedVideoChunkOutputCallback { - (chunk: EncodedVideoChunk, metadata: EncodedVideoChunkMetadata): void; -} - -interface VideoFrameOutputCallback { - (output: VideoFrame): void; -} - -interface WebCodecsErrorCallback { - (error: DOMException): void; -} - -// type AllowSharedBufferSource = ArrayBuffer | ArrayBufferView; -// type BitrateMode = "constant" | "variable"; -// type ImageBufferSource = ArrayBuffer | ArrayBufferView | ReadableStream; -// type AlphaOption = "discard" | "keep"; -// type AudioSampleFormat = "f32" | "f32-planar" | "s16" | "s16-planar" | "s32" | "s32-planar" | "u8" | "u8-planar"; -// type AvcBitstreamFormat = "annexb" | "avc"; -// type CodecState = "closed" | "configured" | "unconfigured"; -// type EncodedAudioChunkType = "delta" | "key"; -// type EncodedVideoChunkType = "delta" | "key"; -type HardwarePreference = - | "no-preference" - | "prefer-hardware" - | "prefer-software"; -// type LatencyMode = "quality" | "realtime"; -// type VideoColorPrimaries = "bt470bg" | "bt709" | "smpte170m"; -// type VideoMatrixCoefficients = "bt470bg" | "bt709" | "rgb" | "smpte170m"; -// type VideoPixelFormat = "BGRA" | "BGRX" | "I420" | "I420A" | "I422" | "I444" | "NV12" | "RGBA" | "RGBX"; -// type VideoTransferCharacteristics = "bt709" | "iec61966-2-1" | "smpte170m"; diff --git a/yarn.lock b/yarn.lock index ef372359..33b73a4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4621,6 +4621,22 @@ __metadata: languageName: node linkType: hard +"@types/dom-mediacapture-transform@npm:^0.1.11": + version: 0.1.11 + resolution: "@types/dom-mediacapture-transform@npm:0.1.11" + dependencies: + "@types/dom-webcodecs": "npm:*" + checksum: 10c0/19c76d54cf31aa2a925011fc5f973dff9a10bdecfdf2285e5e568e61850a0fa2b8c9f1807a1462cbefd57ec26d32eeaa9c359117aca9d9fe7f0d6f2fff33f51e + languageName: node + linkType: hard + +"@types/dom-webcodecs@npm:*": + version: 0.1.15 + resolution: "@types/dom-webcodecs@npm:0.1.15" + checksum: 10c0/1407f0352156c99c9b4378fb4c0c799b061520d031903a7f359ad09a6f706cc1fd56bafb272bb1a3decffcb32e54a51d2f07442eb72622464a950cff7f9e8862 + languageName: node + linkType: hard + "@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" @@ -6944,6 +6960,7 @@ __metadata: "@testing-library/react": "npm:^16.0.0" "@testing-library/user-event": "npm:^14.5.1" "@types/content-type": "npm:^1.1.5" + "@types/dom-mediacapture-transform": "npm:^0.1.11" "@types/grecaptcha": "npm:^3.0.9" "@types/jsdom": "npm:^21.1.7" "@types/lodash-es": "npm:^4.17.12" From b681e53c352baa275b3ca7ec04b305da32704235 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 01:54:04 +0000 Subject: [PATCH 082/155] Update dependency livekit-client to v2.11.4 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 33b73a4e..11023415 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9368,8 +9368,8 @@ __metadata: linkType: hard "livekit-client@npm:^2.11.3": - version: 2.11.3 - resolution: "livekit-client@npm:2.11.3" + version: 2.11.4 + resolution: "livekit-client@npm:2.11.4" dependencies: "@livekit/mutex": "npm:1.1.1" "@livekit/protocol": "npm:1.36.1" @@ -9380,7 +9380,7 @@ __metadata: tslib: "npm:2.8.1" typed-emitter: "npm:^2.1.0" webrtc-adapter: "npm:^9.0.1" - checksum: 10c0/d56444f31c107b46ccd5532038ac77bd21038042910619008267c17894f1d3f054262ae2354d89df6fe0ba325aba01909b0612ad4c290906487c40d91641f6e4 + checksum: 10c0/e674c75ff2c9cd2115c11e40e009a6ca85b41ab20740a5df3b7fafb67ebe683d5214be49532a5aedd81cb85768df393caf4571c83b17584cd168118bce24d315 languageName: node linkType: hard From 9f8dd000e0d1bcadc6245ebd3446e364e00b2e1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 May 2025 14:20:03 +0200 Subject: [PATCH 083/155] Update embedded package dependencies (#3242) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- embedded/android/gradle/libs.versions.toml | 4 ++-- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 2 +- embedded/android/gradlew | 9 ++++----- embedded/android/gradlew.bat | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/embedded/android/gradle/libs.versions.toml b/embedded/android/gradle/libs.versions.toml index 8d38daab..2d9bfa40 100644 --- a/embedded/android/gradle/libs.versions.toml +++ b/embedded/android/gradle/libs.versions.toml @@ -2,11 +2,11 @@ # https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format [versions] -android_gradle_plugin = "8.8.0" +android_gradle_plugin = "8.10.0" [libraries] android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref = "android_gradle_plugin" } [plugins] android_library = { id = "com.android.library", version.ref = "android_gradle_plugin" } -maven_publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } \ No newline at end of file +maven_publish = { id = "com.vanniktech.maven.publish", version = "0.31.0" } \ No newline at end of file diff --git a/embedded/android/gradle/wrapper/gradle-wrapper.jar b/embedded/android/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch delta 34753 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%0AF=STgWRWL+V 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 W(m*lz*HoBlEiM*bU{am~Dx&}eeNGnu diff --git a/embedded/android/gradle/wrapper/gradle-wrapper.properties b/embedded/android/gradle/wrapper/gradle-wrapper.properties index 79eb9d00..6514f919 100644 --- a/embedded/android/gradle/wrapper/gradle-wrapper.properties +++ b/embedded/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/embedded/android/gradlew b/embedded/android/gradlew index f5feea6d..23d15a93 100755 --- a/embedded/android/gradlew +++ b/embedded/android/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/embedded/android/gradlew.bat b/embedded/android/gradlew.bat index 9d21a218..db3a6ac2 100644 --- a/embedded/android/gradlew.bat +++ b/embedded/android/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell From de8a2cb53309776eb23de6590e8a32ad5a7a3496 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 07:25:55 +0000 Subject: [PATCH 084/155] Update dependency livekit-client to v2.12.0 --- yarn.lock | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 11023415..0e8a2fb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2527,7 +2527,16 @@ __metadata: languageName: node linkType: hard -"@livekit/protocol@npm:1.36.1, @livekit/protocol@npm:^1.33.0": +"@livekit/protocol@npm:1.38.0": + version: 1.38.0 + resolution: "@livekit/protocol@npm:1.38.0" + dependencies: + "@bufbuild/protobuf": "npm:^1.10.0" + checksum: 10c0/ca64d4f984853054ff60574730b08a761afcd3bdc084e5218663e54b0e7f395aa2022d9d15d982fa094bbc0179cb19ef6a96ec74b1aa3265d118a85d1a4fde33 + languageName: node + linkType: hard + +"@livekit/protocol@npm:^1.33.0": version: 1.36.1 resolution: "@livekit/protocol@npm:1.36.1" dependencies: @@ -9368,11 +9377,11 @@ __metadata: linkType: hard "livekit-client@npm:^2.11.3": - version: 2.11.4 - resolution: "livekit-client@npm:2.11.4" + version: 2.12.0 + resolution: "livekit-client@npm:2.12.0" dependencies: "@livekit/mutex": "npm:1.1.1" - "@livekit/protocol": "npm:1.36.1" + "@livekit/protocol": "npm:1.38.0" events: "npm:^3.3.0" loglevel: "npm:^1.9.2" sdp-transform: "npm:^2.15.0" @@ -9380,7 +9389,7 @@ __metadata: tslib: "npm:2.8.1" typed-emitter: "npm:^2.1.0" webrtc-adapter: "npm:^9.0.1" - checksum: 10c0/e674c75ff2c9cd2115c11e40e009a6ca85b41ab20740a5df3b7fafb67ebe683d5214be49532a5aedd81cb85768df393caf4571c83b17584cd168118bce24d315 + checksum: 10c0/8a4657aa6c0f0bc5d1fe77c2cd9603a3b07d4acefa634f1c5151190eed69711e7e599dd09c07915939a418dc8770d87e3529ecf1b029f2a9af7f2172d83acb1c languageName: node linkType: hard From 44ed04475d33f369750a37d74659c214c5aa8bff Mon Sep 17 00:00:00 2001 From: fkwp Date: Tue, 13 May 2025 09:38:43 +0200 Subject: [PATCH 085/155] bump livekit protocol version --- package.json | 2 +- yarn.lock | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 74567edb..6ff4198d 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@formatjs/intl-segmenter": "^11.7.3", "@livekit/components-core": "^0.12.0", "@livekit/components-react": "^2.0.0", - "@livekit/protocol": "^1.33.0", + "@livekit/protocol": "^1.38.0", "@livekit/track-processors": "^0.5.5", "@mediapipe/tasks-vision": "^0.10.18", "@opentelemetry/api": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 0e8a2fb0..a42427b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2527,7 +2527,7 @@ __metadata: languageName: node linkType: hard -"@livekit/protocol@npm:1.38.0": +"@livekit/protocol@npm:1.38.0, @livekit/protocol@npm:^1.38.0": version: 1.38.0 resolution: "@livekit/protocol@npm:1.38.0" dependencies: @@ -2536,15 +2536,6 @@ __metadata: languageName: node linkType: hard -"@livekit/protocol@npm:^1.33.0": - version: 1.36.1 - resolution: "@livekit/protocol@npm:1.36.1" - dependencies: - "@bufbuild/protobuf": "npm:^1.10.0" - checksum: 10c0/bb2e56785c542446bef3e2f2fd20b33d01db43b786be87ccb834feee8a664fd32c8231e249b4e1915d7a8eda13af0d59eea479fa710327079a1a370daf05c42e - languageName: node - linkType: hard - "@livekit/track-processors@npm:^0.5.5": version: 0.5.5 resolution: "@livekit/track-processors@npm:0.5.5" @@ -6946,7 +6937,7 @@ __metadata: "@formatjs/intl-segmenter": "npm:^11.7.3" "@livekit/components-core": "npm:^0.12.0" "@livekit/components-react": "npm:^2.0.0" - "@livekit/protocol": "npm:^1.33.0" + "@livekit/protocol": "npm:^1.38.0" "@livekit/track-processors": "npm:^0.5.5" "@mediapipe/tasks-vision": "npm:^0.10.18" "@opentelemetry/api": "npm:^1.4.0" From 04bc6c77a3063a7038575f9c2257b0244b5413be Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 13 May 2025 21:11:12 +0200 Subject: [PATCH 086/155] developer option to mute all audio (#3239) --- locales/en/app.json | 1 + src/room/CallEventAudioRenderer.tsx | 3 +++ src/room/GroupCallView.tsx | 7 +++++-- src/room/InCallView.tsx | 11 +++++++---- src/room/ReactionAudioRenderer.tsx | 3 +++ src/settings/DeveloperSettingsTab.tsx | 22 ++++++++++++++++++++-- src/settings/settings.ts | 6 ++++-- src/useAudioContext.tsx | 3 ++- 8 files changed, 45 insertions(+), 11 deletions(-) diff --git a/locales/en/app.json b/locales/en/app.json index d185d2f8..963a3f55 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -70,6 +70,7 @@ "livekit_server_info": "LiveKit Server Info", "livekit_sfu": "LiveKit SFU: {{url}}", "matrix_id": "Matrix ID: {{id}}", + "mute_all_audio": "Mute all audio (participants, reactions, join sounds)", "show_connection_stats": "Show connection statistics", "show_non_member_tiles": "Show tiles for non-member media", "url_params": "URL parameters", diff --git a/src/room/CallEventAudioRenderer.tsx b/src/room/CallEventAudioRenderer.tsx index 6eeef4c4..a0d685ff 100644 --- a/src/room/CallEventAudioRenderer.tsx +++ b/src/room/CallEventAudioRenderer.tsx @@ -47,12 +47,15 @@ export const callEventAudioSounds = prefetchSounds({ export function CallEventAudioRenderer({ vm, + muted, }: { vm: CallViewModel; + muted?: boolean; }): ReactNode { const audioEngineCtx = useAudioContext({ sounds: callEventAudioSounds, latencyHint: "interactive", + muted, }); const audioEngineRef = useLatest(audioEngineCtx); diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index 0d727485..960b4a05 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -62,8 +62,9 @@ import { } from "../utils/errors.ts"; import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx"; import { - useExperimentalToDeviceTransportSetting, - useNewMembershipManagerSetting as useNewMembershipManagerSetting, + useNewMembershipManager as useNewMembershipManagerSetting, + useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, + muteAllAudio as muteAllAudioSetting, useSetting, } from "../settings/settings"; import { useTypedEventEmitter } from "../useEvents"; @@ -104,11 +105,13 @@ export const GroupCallView: FC = ({ null, ); + const [muteAllAudio] = useSetting(muteAllAudioSetting); const memberships = useMatrixRTCSessionMemberships(rtcSession); const leaveSoundContext = useLatest( useAudioContext({ sounds: callEventAudioSounds, latencyHint: "interactive", + muted: muteAllAudio, }), ); // This should use `useEffectEvent` (only available in experimental versions) diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index b434a1da..33f42f1b 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -96,7 +96,8 @@ import { ReactionsOverlay } from "./ReactionsOverlay"; import { CallEventAudioRenderer } from "./CallEventAudioRenderer"; import { debugTileLayout as debugTileLayoutSetting, - useExperimentalToDeviceTransportSetting, + useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, + muteAllAudio as muteAllAudioSetting, useSetting, } from "../settings/settings"; import { ReactionsReader } from "../reactions/ReactionsReader"; @@ -220,6 +221,8 @@ export const InCallView: FC = ({ room: livekitRoom, }); + const [muteAllAudio] = useSetting(muteAllAudioSetting); + const [toDeviceEncryptionSetting] = useSetting( useExperimentalToDeviceTransportSetting, ); @@ -693,10 +696,10 @@ export const InCallView: FC = ({ ) } - + {renderContent()} - - + + {footer} {layout.type !== "pip" && ( diff --git a/src/room/ReactionAudioRenderer.tsx b/src/room/ReactionAudioRenderer.tsx index c65f6094..2b95acb9 100644 --- a/src/room/ReactionAudioRenderer.tsx +++ b/src/room/ReactionAudioRenderer.tsx @@ -24,8 +24,10 @@ const soundMap = Object.fromEntries([ export function ReactionsAudioRenderer({ vm, + muted, }: { vm: CallViewModel; + muted?: boolean; }): ReactNode { const [shouldPlay] = useSetting(playReactionsSound); const [soundCache, setSoundCache] = useState = ({ client, livekitRoom }) => { useExperimentalToDeviceTransport, setUseExperimentalToDeviceTransport, ] = useSetting(useExperimentalToDeviceTransportSetting); + + const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting); + const urlParams = useUrlParams(); const sfuUrl = useMemo((): URL | null => { @@ -175,6 +179,20 @@ export const DeveloperSettingsTab: FC = ({ client, livekitRoom }) => { )} /> + + ): void => { + setMuteAllAudio(event.target.checked); + }, + [setMuteAllAudio], + )} + /> + {livekitRoom ? ( <>

diff --git a/src/settings/settings.ts b/src/settings/settings.ts index a3b52c7a..8820aea2 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -115,14 +115,16 @@ export const soundEffectVolumeSetting = new Setting( 0.5, ); -export const useNewMembershipManagerSetting = new Setting( +export const useNewMembershipManager = new Setting( "new-membership-manager", true, ); -export const useExperimentalToDeviceTransportSetting = new Setting( +export const useExperimentalToDeviceTransport = new Setting( "experimental-to-device-transport", true, ); +export const muteAllAudio = new Setting("mute-all-audio", false); + export const alwaysShowSelf = new Setting("always-show-self", true); diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index d96b9fdc..d6bc314b 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -47,6 +47,7 @@ interface Props { */ sounds: PrefetchedSounds | null; latencyHint: AudioContextLatencyCategory; + muted?: boolean; } interface UseAudioContext { @@ -112,7 +113,7 @@ export function useAudioContext( }, [audioContext, devices]); // Don't return a function until we're ready. - if (!audioContext || !audioBuffers) { + if (!audioContext || !audioBuffers || props.muted) { return null; } return { From e6710f72e395dbf7f15b37468f6482449313723b Mon Sep 17 00:00:00 2001 From: Timo <16718859+toger5@users.noreply.github.com> Date: Tue, 13 May 2025 22:05:55 +0200 Subject: [PATCH 087/155] Fix to-device encryption info label (#3208) * Fix to-device encryption info label The label was shown also without checking that we use PerParticipantE2EE. Which is a prerequisite for toDevice transport. As a result the label was shown when not desired. * rename: useLiveKit -> useLivekit * make the settings naming consistent --- src/livekit/{useLiveKit.ts => useLivekit.ts} | 2 +- src/room/InCallView.test.tsx | 249 ++++++++++++++++++ src/room/InCallView.tsx | 38 ++- src/room/ReactionAudioRenderer.test.tsx | 14 +- .../__snapshots__/InCallView.test.tsx.snap | 181 +++++++++++++ src/settings/SettingsModal.tsx | 2 +- src/settings/settings.ts | 2 +- src/useAudioContext.test.tsx | 2 +- src/useAudioContext.tsx | 4 +- src/utils/test.ts | 8 +- 10 files changed, 474 insertions(+), 28 deletions(-) rename src/livekit/{useLiveKit.ts => useLivekit.ts} (99%) create mode 100644 src/room/InCallView.test.tsx create mode 100644 src/room/__snapshots__/InCallView.test.tsx.snap diff --git a/src/livekit/useLiveKit.ts b/src/livekit/useLivekit.ts similarity index 99% rename from src/livekit/useLiveKit.ts rename to src/livekit/useLivekit.ts index 99eda021..10de9577 100644 --- a/src/livekit/useLiveKit.ts +++ b/src/livekit/useLivekit.ts @@ -48,7 +48,7 @@ interface UseLivekitResult { connState: ECConnectionState; } -export function useLiveKit( +export function useLivekit( rtcSession: MatrixRTCSession, muteStates: MuteStates, sfuConfig: SFUConfig | undefined, diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx new file mode 100644 index 00000000..66e070d6 --- /dev/null +++ b/src/room/InCallView.test.tsx @@ -0,0 +1,249 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { + beforeEach, + describe, + expect, + it, + type MockedFunction, + vi, +} from "vitest"; +import { act, render, type RenderResult } from "@testing-library/react"; +import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk"; +import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; +import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container"; +import { ConnectionState, type LocalParticipant } from "livekit-client"; +import { of } from "rxjs"; +import { BrowserRouter } from "react-router-dom"; +import { TooltipProvider } from "@vector-im/compound-web"; +import { + RoomAudioRenderer, + RoomContext, + useLocalParticipant, +} from "@livekit/components-react"; +import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; + +import { type MuteStates } from "./MuteStates"; +import { InCallView } from "./InCallView"; +import { + mockLivekitRoom, + mockLocalParticipant, + mockMatrixRoom, + mockMatrixRoomMember, + mockRemoteParticipant, + mockRtcMembership, + type MockRTCSession, +} from "../utils/test"; +import { E2eeType } from "../e2ee/e2eeType"; +import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; +import { alice, local } from "../utils/test-fixtures"; +import { useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting } from "../settings/settings"; +import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; +import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; + +// vi.hoisted(() => { +// localStorage = {} as unknown as Storage; +// }); +vi.hoisted( + () => + (global.ImageData = class MockImageData { + public data: number[] = []; + } as unknown as typeof ImageData), +); + +vi.mock("../soundUtils"); +vi.mock("../useAudioContext"); +vi.mock("../tile/GridTile"); +vi.mock("../tile/SpotlightTile"); +vi.mock("@livekit/components-react"); +vi.mock("../e2ee/sharedKeyManagement"); +vi.mock("react-use-measure", () => ({ + default: (): [() => void, object] => [(): void => {}, {}], +})); + +const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC"); +const localParticipant = mockLocalParticipant({ + identity: "@local:example.org:AAAAAA", +}); +const remoteParticipant = mockRemoteParticipant({ + identity: "@alice:example.org:AAAAAA", +}); +const carol = mockMatrixRoomMember(localRtcMember); +const roomMembers = new Map([carol].map((p) => [p.userId, p])); + +const roomId = "!foo:bar"; +let useRoomEncryptionSystemMock: MockedFunction; +beforeEach(() => { + vi.clearAllMocks(); + // RoomAudioRenderer is tested separately. + ( + RoomAudioRenderer as MockedFunction + ).mockImplementation((_props) => { + return

; + }); + ( + useLocalParticipant as MockedFunction + ).mockImplementation( + () => + ({ + isScreenShareEnabled: false, + localParticipant: localRtcMember as unknown as LocalParticipant, + }) as unknown as ReturnType, + ); + + useRoomEncryptionSystemMock = + useRoomEncryptionSystem as typeof useRoomEncryptionSystemMock; + useRoomEncryptionSystemMock.mockReturnValue({ kind: E2eeType.NONE }); +}); + +function createInCallView(): RenderResult & { + rtcSession: MockRTCSession; +} { + const client = { + getUser: () => null, + getUserId: () => localRtcMember.sender, + getDeviceId: () => localRtcMember.deviceId, + getRoom: (rId) => (rId === roomId ? room : null), + } as Partial as MatrixClient; + const room = mockMatrixRoom({ + relations: { + getChildEventsForEvent: () => + vi.mocked({ + getRelations: () => [], + }), + } as unknown as RelationsContainer, + client, + roomId, + getMember: (userId) => roomMembers.get(userId) ?? null, + getMxcAvatarUrl: () => null, + hasEncryptionStateEvent: vi.fn().mockReturnValue(true), + getCanonicalAlias: () => null, + currentState: { + getJoinRule: () => JoinRule.Invite, + } as Partial as RoomState, + }); + + const muteState = { + audio: { enabled: false }, + video: { enabled: false }, + } as MuteStates; + const livekitRoom = mockLivekitRoom( + { + localParticipant, + }, + { + remoteParticipants$: of([remoteParticipant]), + }, + ); + const { vm, rtcSession } = getBasicCallViewModelEnvironment([local, alice]); + + rtcSession.joined = true; + const renderResult = render( + + + + + + + + + , + ); + return { + ...renderResult, + rtcSession, + }; +} + +describe("InCallView", () => { + describe("rendering", () => { + it("renders", () => { + const { container } = createInCallView(); + expect(container).toMatchSnapshot(); + }); + }); + describe("toDevice label", () => { + it("is shown if setting activated and room encrypted", () => { + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + useExperimentalToDeviceTransportSetting.setValue(true); + const { getByText } = createInCallView(); + expect(getByText("using to Device key transport")).toBeInTheDocument(); + }); + + it("is not shown in unenecrypted room", () => { + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.NONE, + }); + useExperimentalToDeviceTransportSetting.setValue(true); + const { queryByText } = createInCallView(); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + + it("is hidden once fallback was triggered", async () => { + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + useExperimentalToDeviceTransportSetting.setValue(true); + const { rtcSession, queryByText } = createInCallView(); + expect(queryByText("using to Device key transport")).toBeInTheDocument(); + expect(rtcSession).toBeDefined(); + await act(() => + rtcSession.emit(RoomAndToDeviceEvents.EnabledTransportsChanged, { + toDevice: true, + room: true, + }), + ); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + it("is not shown if setting is disabled", () => { + useExperimentalToDeviceTransportSetting.setValue(false); + + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + const { queryByText } = createInCallView(); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 33f42f1b..7cc3eab2 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -56,7 +56,7 @@ import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; import { useRageshakeRequestModal } from "../settings/submit-rageshake"; import { RageshakeRequestModal } from "./RageshakeRequestModal"; -import { useLiveKit } from "../livekit/useLiveKit"; +import { useLivekit } from "../livekit/useLivekit.ts"; import { useWakeLock } from "../useWakeLock"; import { useMergedRefs } from "../useMergedRefs"; import { type MuteStates } from "./MuteStates"; @@ -73,7 +73,10 @@ import { import { Grid, type TileProps } from "../grid/Grid"; import { useInitial } from "../useInitial"; import { SpotlightTile } from "../tile/SpotlightTile"; -import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; +import { + useRoomEncryptionSystem, + type EncryptionSystem, +} from "../e2ee/sharedKeyManagement"; import { E2eeType } from "../e2ee/e2eeType"; import { makeGridLayout } from "../grid/GridLayout"; import { @@ -115,7 +118,7 @@ export interface ActiveCallProps export const ActiveCall: FC = (props) => { const sfuConfig = useOpenIDSFU(props.client, props.rtcSession); - const { livekitRoom, connState } = useLiveKit( + const { livekitRoom, connState } = useLivekit( props.rtcSession, props.muteStates, sfuConfig, @@ -223,19 +226,28 @@ export const InCallView: FC = ({ const [muteAllAudio] = useSetting(muteAllAudioSetting); - const [toDeviceEncryptionSetting] = useSetting( - useExperimentalToDeviceTransportSetting, - ); - const [showToDeviceEncryption, setShowToDeviceEncryption] = useState( - () => toDeviceEncryptionSetting, - ); - useEffect(() => { - setShowToDeviceEncryption(toDeviceEncryptionSetting); - }, [toDeviceEncryptionSetting]); + // This seems like it might be enough logic to use move it into the call view model? + const [didFallbackToRoomKey, setDidFallbackToRoomKey] = useState(false); useTypedEventEmitter( rtcSession, RoomAndToDeviceEvents.EnabledTransportsChanged, - (enabled) => setShowToDeviceEncryption(enabled.to_device), + (enabled) => setDidFallbackToRoomKey(enabled.room), + ); + const [useExperimentalToDeviceTransport] = useSetting( + useExperimentalToDeviceTransportSetting, + ); + const encryptionSystem = useRoomEncryptionSystem(rtcSession.room.roomId); + + const showToDeviceEncryption = useMemo( + () => + useExperimentalToDeviceTransport && + encryptionSystem.kind === E2eeType.PER_PARTICIPANT && + !didFallbackToRoomKey, + [ + encryptionSystem.kind, + didFallbackToRoomKey, + useExperimentalToDeviceTransport, + ], ); const toggleMicrophone = useCallback( diff --git a/src/room/ReactionAudioRenderer.test.tsx b/src/room/ReactionAudioRenderer.test.tsx index fa7df166..c61cbd82 100644 --- a/src/room/ReactionAudioRenderer.test.tsx +++ b/src/room/ReactionAudioRenderer.test.tsx @@ -21,8 +21,8 @@ import { act, type ReactNode } from "react"; import { ReactionsAudioRenderer } from "./ReactionAudioRenderer"; import { - playReactionsSound, - soundEffectVolumeSetting, + playReactionsSound as playReactionsSoundSetting, + soundEffectVolume as soundEffectVolumeSetting, } from "../settings/settings"; import { useAudioContext } from "../useAudioContext"; import { GenericReaction, ReactionSet } from "../reactions"; @@ -50,7 +50,7 @@ vitest.mock("../soundUtils"); afterEach(() => { vitest.resetAllMocks(); - playReactionsSound.setValue(playReactionsSound.defaultValue); + playReactionsSoundSetting.setValue(playReactionsSoundSetting.defaultValue); soundEffectVolumeSetting.setValue(soundEffectVolumeSetting.defaultValue); }); @@ -74,7 +74,7 @@ beforeEach(() => { test("preloads all audio elements", () => { const { vm } = getBasicCallViewModelEnvironment([local, alice]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); expect(prefetchSounds).toHaveBeenCalledOnce(); }); @@ -84,7 +84,7 @@ test("will play an audio sound when there is a reaction", () => { local, alice, ]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); // Find the first reaction with a sound effect @@ -110,7 +110,7 @@ test("will play the generic audio sound when there is soundless reaction", () => local, alice, ]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); // Find the first reaction with a sound effect @@ -136,7 +136,7 @@ test("will play multiple audio sounds when there are multiple different reaction local, alice, ]); - playReactionsSound.setValue(true); + playReactionsSoundSetting.setValue(true); render(); // Find the first reaction with a sound effect diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap new file mode 100644 index 00000000..427973b6 --- /dev/null +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -0,0 +1,181 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`InCallView > rendering > renders 1`] = ` +
+
+
+
+ mocked: RoomAudioRenderer +
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+`; diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b24674dc..b0a4b79e 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -23,7 +23,7 @@ import { import { widget } from "../widget"; import { useSetting, - soundEffectVolumeSetting, + soundEffectVolume as soundEffectVolumeSetting, backgroundBlur as backgroundBlurSetting, developerMode, } from "./settings"; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 8820aea2..f63148ef 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -110,7 +110,7 @@ export const playReactionsSound = new Setting( true, ); -export const soundEffectVolumeSetting = new Setting( +export const soundEffectVolume = new Setting( "sound-effect-volume", 0.5, ); diff --git a/src/useAudioContext.test.tsx b/src/useAudioContext.test.tsx index 29949bf8..92d3a947 100644 --- a/src/useAudioContext.test.tsx +++ b/src/useAudioContext.test.tsx @@ -12,7 +12,7 @@ import userEvent from "@testing-library/user-event"; import { deviceStub, MediaDevicesContext } from "./livekit/MediaDevicesContext"; import { useAudioContext } from "./useAudioContext"; -import { soundEffectVolumeSetting } from "./settings/settings"; +import { soundEffectVolume as soundEffectVolumeSetting } from "./settings/settings"; const staticSounds = Promise.resolve({ aSound: new ArrayBuffer(0), diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index d6bc314b..da94f387 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -9,7 +9,7 @@ import { logger } from "matrix-js-sdk/lib/logger"; import { useState, useEffect } from "react"; import { - soundEffectVolumeSetting as effectSoundVolumeSetting, + soundEffectVolume as soundEffectVolumeSetting, useSetting, } from "./settings/settings"; import { useMediaDevices } from "./livekit/MediaDevicesContext"; @@ -63,7 +63,7 @@ interface UseAudioContext { export function useAudioContext( props: Props, ): UseAudioContext | null { - const [effectSoundVolume] = useSetting(effectSoundVolumeSetting); + const [effectSoundVolume] = useSetting(soundEffectVolumeSetting); const devices = useMediaDevices(); const [audioContext, setAudioContext] = useState(); const [audioBuffers, setAudioBuffers] = useState>(); diff --git a/src/utils/test.ts b/src/utils/test.ts index 039b6983..6e1b5457 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -29,6 +29,10 @@ import { type Room as LivekitRoom, } from "livekit-client"; import { randomUUID } from "crypto"; +import { + type RoomAndToDeviceEvents, + type RoomAndToDeviceEventsHandlerMap, +} from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; import { LocalUserMediaViewModel, @@ -269,8 +273,8 @@ export function mockConfig(config: Partial = {}): void { } export class MockRTCSession extends TypedEventEmitter< - MatrixRTCSessionEvent, - MatrixRTCSessionEventHandlerMap + MatrixRTCSessionEvent | RoomAndToDeviceEvents, + MatrixRTCSessionEventHandlerMap & RoomAndToDeviceEventsHandlerMap > { public readonly statistics = { counters: {}, From 1ff2fb3fffad81f8c72cf8cb4fa0f4c09064b71d Mon Sep 17 00:00:00 2001 From: Valere Fedronic Date: Tue, 13 May 2025 22:22:56 +0200 Subject: [PATCH 088/155] only show to-device transport label in dev mode (#3221) * Only show to device encryption label if developer mode on * Add tests for developer mode to device label --------- Co-authored-by: Timo --- src/room/InCallView.test.tsx | 21 +++++++++++++++++++-- src/room/InCallView.tsx | 7 ++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index 66e070d6..4d02160c 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -42,7 +42,10 @@ import { import { E2eeType } from "../e2ee/e2eeType"; import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel"; import { alice, local } from "../utils/test-fixtures"; -import { useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting } from "../settings/settings"; +import { + developerMode as developerModeSetting, + useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, +} from "../settings/settings"; import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; @@ -201,6 +204,7 @@ describe("InCallView", () => { kind: E2eeType.PER_PARTICIPANT, }); useExperimentalToDeviceTransportSetting.setValue(true); + developerModeSetting.setValue(true); const { getByText } = createInCallView(); expect(getByText("using to Device key transport")).toBeInTheDocument(); }); @@ -210,6 +214,7 @@ describe("InCallView", () => { kind: E2eeType.NONE, }); useExperimentalToDeviceTransportSetting.setValue(true); + developerModeSetting.setValue(true); const { queryByText } = createInCallView(); expect( queryByText("using to Device key transport"), @@ -221,6 +226,7 @@ describe("InCallView", () => { kind: E2eeType.PER_PARTICIPANT, }); useExperimentalToDeviceTransportSetting.setValue(true); + developerModeSetting.setValue(true); const { rtcSession, queryByText } = createInCallView(); expect(queryByText("using to Device key transport")).toBeInTheDocument(); expect(rtcSession).toBeDefined(); @@ -236,7 +242,18 @@ describe("InCallView", () => { }); it("is not shown if setting is disabled", () => { useExperimentalToDeviceTransportSetting.setValue(false); - + developerModeSetting.setValue(true); + useRoomEncryptionSystemMock.mockReturnValue({ + kind: E2eeType.PER_PARTICIPANT, + }); + const { queryByText } = createInCallView(); + expect( + queryByText("using to Device key transport"), + ).not.toBeInTheDocument(); + }); + it("is not shown if developer mode is disabled", () => { + useExperimentalToDeviceTransportSetting.setValue(true); + developerModeSetting.setValue(false); useRoomEncryptionSystemMock.mockReturnValue({ kind: E2eeType.PER_PARTICIPANT, }); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 7cc3eab2..768ddfdd 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -101,6 +101,7 @@ import { debugTileLayout as debugTileLayoutSetting, useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, muteAllAudio as muteAllAudioSetting, + developerMode as developerModeSetting, useSetting, } from "../settings/settings"; import { ReactionsReader } from "../reactions/ReactionsReader"; @@ -233,6 +234,8 @@ export const InCallView: FC = ({ RoomAndToDeviceEvents.EnabledTransportsChanged, (enabled) => setDidFallbackToRoomKey(enabled.room), ); + + const [developerMode] = useSetting(developerModeSetting); const [useExperimentalToDeviceTransport] = useSetting( useExperimentalToDeviceTransportSetting, ); @@ -240,13 +243,15 @@ export const InCallView: FC = ({ const showToDeviceEncryption = useMemo( () => + developerMode && useExperimentalToDeviceTransport && encryptionSystem.kind === E2eeType.PER_PARTICIPANT && !didFallbackToRoomKey, [ + developerMode, + useExperimentalToDeviceTransport, encryptionSystem.kind, didFallbackToRoomKey, - useExperimentalToDeviceTransport, ], ); From 56328108ca9534e34b7c12b5b8e38282c5d5f563 Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 14 May 2025 10:41:03 +0200 Subject: [PATCH 089/155] Add custom audio renderer to only render joined participants & add ios earpice workaround fix left right to match chromium + safari (firefox is swapped) earpice as setting Simpler code and documentation The doc explains, what this class actually does and why it is so complicated. Signed-off-by: Timo K use only one audioContext, remove (non working) standby fallback --- locales/en/app.json | 2 + src/livekit/MatrixAudioRenderer.tsx | 201 ++++++++++++++++++++++++++ src/livekit/MediaDevicesContext.tsx | 74 +++++++++- src/room/InCallView.tsx | 12 +- src/settings/DeveloperSettingsTab.tsx | 18 +++ src/settings/DeviceSelection.tsx | 30 +++- src/settings/SettingsModal.tsx | 8 +- src/settings/settings.ts | 8 + src/useAudioContext.tsx | 28 +++- 9 files changed, 351 insertions(+), 30 deletions(-) create mode 100644 src/livekit/MatrixAudioRenderer.tsx diff --git a/locales/en/app.json b/locales/en/app.json index 963a3f55..0b4c0599 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -61,6 +61,7 @@ "video": "Video" }, "developer_mode": { + "always_show_iphone_earpiece": "Show iPhone earpiece option on all platforms", "crypto_version": "Crypto version: {{version}}", "debug_tile_layout_label": "Debug tile layout", "device_id": "Device ID: {{id}}", @@ -174,6 +175,7 @@ "camera_numbered": "Camera {{n}}", "default": "Default", "default_named": "Default <2>({{name}})", + "earpiece": "Earpiece", "microphone": "Microphone", "microphone_numbered": "Microphone {{n}}", "speaker": "Speaker", diff --git a/src/livekit/MatrixAudioRenderer.tsx b/src/livekit/MatrixAudioRenderer.tsx new file mode 100644 index 00000000..4b8218f3 --- /dev/null +++ b/src/livekit/MatrixAudioRenderer.tsx @@ -0,0 +1,201 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { getTrackReferenceId } from "@livekit/components-core"; +import { type RemoteAudioTrack, Track } from "livekit-client"; +import { useEffect, useMemo, useRef, type ReactNode } from "react"; +import { + useTracks, + AudioTrack, + type AudioTrackProps, +} from "@livekit/components-react"; +import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc"; +import { logger } from "matrix-js-sdk/lib/logger"; + +import { useEarpieceAudioConfig } from "./MediaDevicesContext"; +import { useReactiveState } from "../useReactiveState"; + +export interface MatrixAudioRendererProps { + /** + * The list of participants to render audio for. + * This list needs to be composed based on the matrixRTC members so that we do not play audio from users + * that are not expected to be in the rtc session. + */ + members: CallMembership[]; + /** + * If set to `true`, mutes all audio tracks rendered by the component. + * @remarks + * If set to `true`, the server will stop sending audio track data to the client. + */ + muted?: boolean; +} + +/** + * The `MatrixAudioRenderer` component is a drop-in solution for adding audio to your LiveKit app. + * It takes care of handling remote participants’ audio tracks and makes sure that microphones and screen share are audible. + * + * It also takes care of the earpiece audio configuration for iOS devices. + * This is done by using the WebAudio API to create a stereo pan effect that mimics the earpiece audio. + * @example + * ```tsx + * + * + * + * ``` + * @public + */ +export function MatrixAudioRenderer({ + members, + muted, +}: MatrixAudioRendererProps): ReactNode { + const validIdentities = useMemo( + () => + new Set(members?.map((member) => `${member.sender}:${member.deviceId}`)), + [members], + ); + + const loggedInvalidIdentities = useRef(new Set()); + /** + * Log an invalid livekit track identity. + * A invalid identity is one that does not match any of the matrix rtc members. + * + * @param identity The identity of the track that is invalid + * @param validIdentities The list of valid identities + */ + const logInvalid = (identity: string, validIdentities: Set): void => { + if (loggedInvalidIdentities.current.has(identity)) return; + logger.warn( + `Audio track ${identity} has no matching matrix call member`, + `current members: ${Array.from(validIdentities.values())}`, + `track will not get rendered`, + ); + loggedInvalidIdentities.current.add(identity); + }; + + const tracks = useTracks( + [ + Track.Source.Microphone, + Track.Source.ScreenShareAudio, + Track.Source.Unknown, + ], + { + updateOnlyOn: [], + onlySubscribed: true, + }, + ).filter((ref) => { + const isValid = validIdentities?.has(ref.participant.identity); + if (!isValid) logInvalid(ref.participant.identity, validIdentities); + return ( + !ref.participant.isLocal && + ref.publication.kind === Track.Kind.Audio && + isValid + ); + }); + + // This component is also (in addition to the "only play audio for connected members" logic above) + // to mimic earpice audio on iphones. + // The safari audio devices enumeration does not expose an earpice audio device. + // We alternatively use the audioContext pan node to only use one of the stereo channels. + + // This component does get additionally complicated because of a safari bug. + // (see: https://bugs.webkit.org/show_bug.cgi?id=251532 + // and the related issues: https://bugs.webkit.org/show_bug.cgi?id=237878 + // and https://bugs.webkit.org/show_bug.cgi?id=231105) + // + // AudioContext gets stopped if the webview gets moved into the background. + // Once the phone is in standby audio playback will stop. + // So we can only use the pan trick only works is the phone is not in standby. + // If earpice mode is not used we do not use audioContext to allow standby playback. + // shouldUseAudioContext is set to false if stereoPan === 0 to allow standby bluetooth playback. + + const { pan: stereoPan, volume: volumeFactor } = useEarpieceAudioConfig(); + const shouldUseAudioContext = stereoPan !== 0; + + // initialize the potentially used audio context. + const audioContext = useMemo(() => new AudioContext(), []); + const audioNodes = useMemo( + () => ({ + gain: audioContext.createGain(), + pan: audioContext.createStereoPanner(), + }), + [audioContext], + ); + + // Simple effects to update the gain and pan node based on the props + useEffect(() => { + audioNodes.pan.pan.value = stereoPan; + }, [audioNodes.pan.pan, stereoPan]); + useEffect(() => { + // *4 to balance the transition from audio context to normal audio playback. + // probably needed due to gain behaving differently than el.volume + audioNodes.gain.gain.value = volumeFactor; + }, [audioNodes.gain.gain, volumeFactor]); + + return ( + // We add all audio elements into one
for the browser developer tool experience/tidyness. +
+ {tracks.map((trackRef) => ( + + ))} +
+ ); +} + +interface StereoPanAudioTrackProps { + muted?: boolean; + audioContext?: AudioContext; + audioNodes: { + gain: GainNode; + pan: StereoPannerNode; + }; +} + +/** + * This wraps `livekit.AudioTrack` to allow adding audio nodes to a track. + * It main purpose is to remount the AudioTrack component when switching from + * audiooContext to normal audio playback. + * As of now the AudioTrack component does not support adding audio nodes while being mounted. + * @param param0 + * @returns + */ +function AudioTrackWithAudioNodes({ + trackRef, + muted, + audioContext, + audioNodes, + ...props +}: StereoPanAudioTrackProps & + AudioTrackProps & + React.RefAttributes): ReactNode { + // This is used to unmount/remount the AudioTrack component. + // Mounting needs to happen after the audioContext is set. + // (adding the audio context when already mounted did not work outside strict mode) + const [trackReady, setTrackReady] = useReactiveState( + () => false, + [audioContext || audioNodes], + ); + + useEffect(() => { + if (!trackRef || trackReady) return; + const track = trackRef.publication.track as RemoteAudioTrack; + track.setAudioContext(audioContext); + track.setWebAudioPlugins( + audioContext ? [audioNodes.gain, audioNodes.pan] : [], + ); + setTrackReady(true); + }, [audioContext, audioNodes, setTrackReady, trackReady, trackRef]); + + return ( + trackReady && + ); +} diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index c2fc63e5..3e30c04d 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -27,11 +27,15 @@ import { audioOutput as audioOutputSetting, videoInput as videoInputSetting, type Setting, + alwaysShowIphoneEarpieceSetting, } from "../settings/settings"; +export const EARPIECE_CONFIG_ID = "earpiece-id"; + export type DeviceLabel = | { type: "name"; name: string } | { type: "number"; number: number } + | { type: "earpiece" } | { type: "default"; name: string | null }; export interface MediaDevice { @@ -40,6 +44,11 @@ export interface MediaDevice { */ available: Map; selectedId: string | undefined; + /** + * An additional device configuration that makes us use only one channel of the + * output device and a reduced volume. + */ + useAsEarpiece: boolean | undefined; /** * The group ID of the selected device. */ @@ -65,6 +74,7 @@ function useMediaDevice( ): MediaDevice { // Make sure we don't needlessly reset to a device observer without names, // once permissions are already given + const [alwaysShowIphoneEarpice] = useSetting(alwaysShowIphoneEarpieceSetting); const hasRequestedPermissions = useRef(false); const requestPermissions = usingNames || hasRequestedPermissions.current; hasRequestedPermissions.current ||= usingNames; @@ -102,27 +112,39 @@ function useMediaDevice( // Create a virtual default audio output for browsers that don't have one. // Its device ID must be the empty string because that's what setSinkId // recognizes. + // We also create this if we do not have any available devices, so that + // we can use the default or the earpiece. + const showEarpiece = + navigator.userAgent.match("iPhone") || alwaysShowIphoneEarpice; if ( kind === "audiooutput" && - available.size && !available.has("") && - !available.has("default") + !available.has("default") && + (available.size || showEarpiece) ) available = new Map([ ["", { type: "default", name: availableRaw[0]?.label || null }], ...available, ]); + if (kind === "audiooutput" && showEarpiece) + // On IPhones we have to create a virtual earpiece device, because + // the earpiece is not available as a device ID. + available = new Map([ + ...available, + [EARPIECE_CONFIG_ID, { type: "earpiece" }], + ]); // Note: creating virtual default input devices would be another problem // entirely, because requesting a media stream from deviceId "" won't // automatically track the default device. return available; }), ), - [kind, deviceObserver$], + [alwaysShowIphoneEarpice, deviceObserver$, kind], ), ); - const [preferredId, select] = useSetting(setting); + const [preferredId, setPreferredId] = useSetting(setting); + const [asEarpice, setAsEarpiece] = useState(false); const selectedId = useMemo(() => { if (available.size) { // If the preferred device is available, use it. Or if every available @@ -138,6 +160,7 @@ function useMediaDevice( } return undefined; }, [available, preferredId]); + const selectedGroupId = useObservableEagerState( useMemo( () => @@ -151,14 +174,27 @@ function useMediaDevice( ), ); + const select = useCallback( + (id: string) => { + if (id === EARPIECE_CONFIG_ID) { + setAsEarpiece(true); + } else { + setAsEarpiece(false); + setPreferredId(id); + } + }, + [setPreferredId], + ); + return useMemo( () => ({ available, selectedId, + useAsEarpiece: asEarpice, selectedGroupId, select, }), - [available, selectedId, selectedGroupId, select], + [available, selectedId, asEarpice, selectedGroupId, select], ); } @@ -167,6 +203,7 @@ export const deviceStub: MediaDevice = { selectedId: undefined, selectedGroupId: undefined, select: () => {}, + useAsEarpiece: false, }; export const devicesStub: MediaDevices = { audioInput: deviceStub, @@ -255,3 +292,30 @@ export const useMediaDeviceNames = ( return context.stopUsingDeviceNames; } }, [context, enabled]); + +/** + * A convenience hook to get the audio node configuration for the earpiece. + * It will check the `useAsEarpiece` of the `audioOutput` device and return + * the appropriate pan and volume values. + * + * @returns pan and volume values for the earpiece audio node configuration. + */ +export const useEarpieceAudioConfig = (): { + pan: number; + volume: number; +} => { + const { audioOutput } = useMediaDevices(); + // We use only the right speaker (pan = 1) for the earpiece. + // This mimics the behavior of the native earpiece speaker (only the top speaker on an iPhone) + const pan = useMemo( + () => (audioOutput.useAsEarpiece ? 1 : 0), + [audioOutput.useAsEarpiece], + ); + // We also do lower the volume by a factor of 10 to optimize for the usecase where + // a user is holding the phone to their ear. + const volume = useMemo( + () => (audioOutput.useAsEarpiece ? 0.1 : 1), + [audioOutput.useAsEarpiece], + ); + return { pan, volume }; +}; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 768ddfdd..9fa15547 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -5,11 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { - RoomAudioRenderer, - RoomContext, - useLocalParticipant, -} from "@livekit/components-react"; +import { RoomContext, useLocalParticipant } from "@livekit/components-react"; import { Text } from "@vector-im/compound-web"; import { ConnectionState, type Room } from "livekit-client"; import { type MatrixClient } from "matrix-js-sdk"; @@ -107,6 +103,7 @@ import { import { ReactionsReader } from "../reactions/ReactionsReader"; import { ConnectionLostError } from "../utils/errors.ts"; import { useTypedEventEmitter } from "../useEvents.ts"; +import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer.tsx"; const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {}); @@ -713,7 +710,10 @@ export const InCallView: FC = ({ ) } - + {renderContent()} diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index fdeaa704..36df5c39 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -18,6 +18,7 @@ import { useNewMembershipManager as useNewMembershipManagerSetting, useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting, muteAllAudio as muteAllAudioSetting, + alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, } from "./settings"; import type { MatrixClient } from "matrix-js-sdk"; import type { Room as LivekitRoom } from "livekit-client"; @@ -46,6 +47,9 @@ export const DeveloperSettingsTab: FC = ({ client, livekitRoom }) => { useNewMembershipManagerSetting, ); + const [alwaysShowIphoneEarpiece, setAlwaysShowIphoneEarpiece] = useSetting( + alwaysShowIphoneEarpieceSetting, + ); const [ useExperimentalToDeviceTransport, setUseExperimentalToDeviceTransport, @@ -192,6 +196,20 @@ export const DeveloperSettingsTab: FC = ({ client, livekitRoom }) => { [setMuteAllAudio], )} /> + {" "} + + ): void => { + setAlwaysShowIphoneEarpiece(event.target.checked); + }, + [setAlwaysShowIphoneEarpiece], + )} + />{" "} {livekitRoom ? ( <> diff --git a/src/settings/DeviceSelection.tsx b/src/settings/DeviceSelection.tsx index 0bdabbe7..12e89f56 100644 --- a/src/settings/DeviceSelection.tsx +++ b/src/settings/DeviceSelection.tsx @@ -22,17 +22,20 @@ import { } from "@vector-im/compound-web"; import { Trans, useTranslation } from "react-i18next"; -import { type MediaDevice } from "../livekit/MediaDevicesContext"; +import { + EARPIECE_CONFIG_ID, + type MediaDevice, +} from "../livekit/MediaDevicesContext"; import styles from "./DeviceSelection.module.css"; interface Props { - devices: MediaDevice; + device: MediaDevice; title: string; numberedLabel: (number: number) => string; } export const DeviceSelection: FC = ({ - devices, + device, title, numberedLabel, }) => { @@ -40,12 +43,13 @@ export const DeviceSelection: FC = ({ const groupId = useId(); const onChange = useCallback( (e: ChangeEvent) => { - devices.select(e.target.value); + device.select(e.target.value); }, - [devices], + [device], ); - if (devices.available.size == 0) return null; + // There is no need to show the menu if there is no choice that can be made. + if (device.available.size == 1) return null; return (
@@ -60,7 +64,7 @@ export const DeviceSelection: FC = ({
- {[...devices.available].map(([id, label]) => { + {[...device.available].map(([id, label]) => { let labelText: ReactNode; switch (label.type) { case "name": @@ -85,6 +89,16 @@ export const DeviceSelection: FC = ({ ); break; + case "earpiece": + labelText = t("settings.devices.earpiece"); + break; + } + + let isSelected = false; + if (device.useAsEarpiece) { + isSelected = id === EARPIECE_CONFIG_ID; + } else { + isSelected = id === device.selectedId; } return ( @@ -93,7 +107,7 @@ export const DeviceSelection: FC = ({ name={groupId} control={ diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index b0a4b79e..1c97a87d 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -98,7 +98,6 @@ export const SettingsModal: FC = ({ useMediaDeviceNames(devices, open); const [soundVolume, setSoundVolume] = useSetting(soundEffectVolumeSetting); const [soundVolumeRaw, setSoundVolumeRaw] = useState(soundVolume); - const [showDeveloperSettingsTab] = useSetting(developerMode); const { available: isRageshakeAvailable } = useSubmitRageshake(); @@ -110,17 +109,18 @@ export const SettingsModal: FC = ({ <>
t("settings.devices.microphone_numbered", { n }) } /> t("settings.devices.speaker_numbered", { n })} /> +

{t("settings.audio_tab.effect_volume_description")}

@@ -146,7 +146,7 @@ export const SettingsModal: FC = ({ <> t("settings.devices.camera_numbered", { n })} /> diff --git a/src/settings/settings.ts b/src/settings/settings.ts index f63148ef..0c7b9191 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -44,6 +44,9 @@ export class Setting { this._value$.next(value); localStorage.setItem(this.key, JSON.stringify(value)); }; + public readonly getValue = (): T => { + return this._value$.getValue(); + }; } /** @@ -128,3 +131,8 @@ export const useExperimentalToDeviceTransport = new Setting( export const muteAllAudio = new Setting("mute-all-audio", false); export const alwaysShowSelf = new Setting("always-show-self", true); + +export const alwaysShowIphoneEarpiece = new Setting( + "always-show-iphone-earpice", + false, +); diff --git a/src/useAudioContext.tsx b/src/useAudioContext.tsx index da94f387..5a689fdf 100644 --- a/src/useAudioContext.tsx +++ b/src/useAudioContext.tsx @@ -12,7 +12,10 @@ import { soundEffectVolume as soundEffectVolumeSetting, useSetting, } from "./settings/settings"; -import { useMediaDevices } from "./livekit/MediaDevicesContext"; +import { + useEarpieceAudioConfig, + useMediaDevices, +} from "./livekit/MediaDevicesContext"; import { type PrefetchedSounds } from "./soundUtils"; /** @@ -28,12 +31,15 @@ async function playSound( ctx: AudioContext, buffer: AudioBuffer, volume: number, + stereoPan: number, ): Promise { const gain = ctx.createGain(); gain.gain.setValueAtTime(volume, 0); + const pan = ctx.createStereoPanner(); + pan.pan.setValueAtTime(stereoPan, 0); const src = ctx.createBufferSource(); src.buffer = buffer; - src.connect(gain).connect(ctx.destination); + src.connect(gain).connect(pan).connect(ctx.destination); const p = new Promise((r) => src.addEventListener("ended", () => r())); src.start(); return p; @@ -63,8 +69,9 @@ interface UseAudioContext { export function useAudioContext( props: Props, ): UseAudioContext | null { - const [effectSoundVolume] = useSetting(soundEffectVolumeSetting); - const devices = useMediaDevices(); + const [soundEffectVolume] = useSetting(soundEffectVolumeSetting); + const { audioOutput } = useMediaDevices(); + const [audioContext, setAudioContext] = useState(); const [audioBuffers, setAudioBuffers] = useState>(); @@ -106,23 +113,30 @@ export function useAudioContext( if (audioContext && "setSinkId" in audioContext) { // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/setSinkId // @ts-expect-error - setSinkId doesn't exist yet in types, maybe because it's not supported everywhere. - audioContext.setSinkId(devices.audioOutput.selectedId).catch((ex) => { + audioContext.setSinkId(audioOutput.selectedId).catch((ex) => { logger.warn("Unable to change sink for audio context", ex); }); } - }, [audioContext, devices]); + }, [audioContext, audioOutput.selectedId]); + const { pan: earpiecePan, volume: earpieceVolume } = useEarpieceAudioConfig(); // Don't return a function until we're ready. if (!audioContext || !audioBuffers || props.muted) { return null; } + return { playSound: async (name): Promise => { if (!audioBuffers[name]) { logger.debug(`Tried to play a sound that wasn't buffered (${name})`); return; } - return playSound(audioContext, audioBuffers[name], effectSoundVolume); + return playSound( + audioContext, + audioBuffers[name], + soundEffectVolume * earpieceVolume, + earpiecePan, + ); }, }; } From 6b8c620bbb3ae309e7f645606d531e09716beffe Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 14 May 2025 10:41:08 +0200 Subject: [PATCH 090/155] Add tests --- playwright/access.spec.ts | 2 +- src/UserMenu.tsx | 2 +- src/livekit/MatrixAudioRenderer.test.tsx | 104 ++++++++++++++ src/livekit/MediaDevicesContext.tsx | 2 +- src/room/InCallView.test.tsx | 18 +-- src/room/MuteStates.test.tsx | 1 + .../__snapshots__/InCallView.test.tsx.snap | 2 +- src/useAudioContext.test.tsx | 136 +++++++++++------- src/utils/test.ts | 23 +++ 9 files changed, 228 insertions(+), 62 deletions(-) create mode 100644 src/livekit/MatrixAudioRenderer.test.tsx diff --git a/playwright/access.spec.ts b/playwright/access.spec.ts index 14a70873..da7ec364 100644 --- a/playwright/access.spec.ts +++ b/playwright/access.spec.ts @@ -49,7 +49,7 @@ test("Sign up a new account, then login, then logout", async ({ browser }) => { // logout await returningUserPage.getByTestId("usermenu_open").click(); - await returningUserPage.locator('[data-test-id="usermenu_logout"]').click(); + await returningUserPage.locator('[data-testid="usermenu_logout"]').click(); await expect( returningUserPage.getByRole("link", { name: "Log In" }), diff --git a/src/UserMenu.tsx b/src/UserMenu.tsx index 52cc4a5a..e431c328 100644 --- a/src/UserMenu.tsx +++ b/src/UserMenu.tsx @@ -119,7 +119,7 @@ export const UserMenu: FC = ({ key={key} Icon={Icon} label={label} - data-test-id={dataTestid} + data-testid={dataTestid} onSelect={() => onAction(key)} /> ))} diff --git a/src/livekit/MatrixAudioRenderer.test.tsx b/src/livekit/MatrixAudioRenderer.test.tsx new file mode 100644 index 00000000..9d91e6fa --- /dev/null +++ b/src/livekit/MatrixAudioRenderer.test.tsx @@ -0,0 +1,104 @@ +/* +Copyright 2023, 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { render } from "@testing-library/react"; +import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc"; +import { + getTrackReferenceId, + type TrackReference, +} from "@livekit/components-core"; +import { type RemoteAudioTrack } from "livekit-client"; +import { type ReactNode } from "react"; +import { useTracks } from "@livekit/components-react"; + +import { testAudioContext } from "../useAudioContext.test"; +import * as MediaDevicesContext from "./MediaDevicesContext"; +import { MatrixAudioRenderer } from "./MatrixAudioRenderer"; +import { mockTrack } from "../utils/test"; + +export const TestAudioContextConstructor = vi.fn(() => testAudioContext); + +beforeEach(() => { + vi.stubGlobal("AudioContext", TestAudioContextConstructor); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.clearAllMocks(); +}); + +vi.mock("@livekit/components-react", async (importOriginal) => { + return { + ...(await importOriginal()), // this will only affect "foo" outside of the original module + AudioTrack: (props: { trackRef: TrackReference }): ReactNode => { + return ( + + ); + }, + useTracks: vi.fn(), + }; +}); + +const tracks = [mockTrack("test:123")]; +vi.mocked(useTracks).mockReturnValue(tracks); + +it("should render for member", () => { + const { container, queryAllByTestId } = render( + , + ); + expect(container).toBeTruthy(); + expect(queryAllByTestId("audio")).toHaveLength(1); +}); +it("should not render without member", () => { + const { container, queryAllByTestId } = render( + , + ); + expect(container).toBeTruthy(); + expect(queryAllByTestId("audio")).toHaveLength(0); +}); + +it("should not setup audioContext gain and pan if there is no need to.", () => { + render( + , + ); + const audioTrack = tracks[0].publication.track! as RemoteAudioTrack; + + expect(audioTrack.setAudioContext).toHaveBeenCalledTimes(1); + expect(audioTrack.setAudioContext).toHaveBeenCalledWith(undefined); + expect(audioTrack.setWebAudioPlugins).toHaveBeenCalledTimes(1); + expect(audioTrack.setWebAudioPlugins).toHaveBeenCalledWith([]); + + expect(testAudioContext.gain.gain.value).toEqual(1); + expect(testAudioContext.pan.pan.value).toEqual(0); +}); +it("should setup audioContext gain and pan", () => { + vi.spyOn(MediaDevicesContext, "useEarpieceAudioConfig").mockReturnValue({ + pan: 1, + volume: 0.1, + }); + render( + , + ); + + const audioTrack = tracks[0].publication.track! as RemoteAudioTrack; + expect(audioTrack.setAudioContext).toHaveBeenCalled(); + expect(audioTrack.setWebAudioPlugins).toHaveBeenCalled(); + + expect(testAudioContext.gain.gain.value).toEqual(0.1); + expect(testAudioContext.pan.pan.value).toEqual(1); +}); diff --git a/src/livekit/MediaDevicesContext.tsx b/src/livekit/MediaDevicesContext.tsx index 3e30c04d..7d82032a 100644 --- a/src/livekit/MediaDevicesContext.tsx +++ b/src/livekit/MediaDevicesContext.tsx @@ -26,8 +26,8 @@ import { audioInput as audioInputSetting, audioOutput as audioOutputSetting, videoInput as videoInputSetting, + alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, type Setting, - alwaysShowIphoneEarpieceSetting, } from "../settings/settings"; export const EARPIECE_CONFIG_ID = "earpiece-id"; diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index 4d02160c..f7bed6fe 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -21,11 +21,7 @@ import { ConnectionState, type LocalParticipant } from "livekit-client"; import { of } from "rxjs"; import { BrowserRouter } from "react-router-dom"; import { TooltipProvider } from "@vector-im/compound-web"; -import { - RoomAudioRenderer, - RoomContext, - useLocalParticipant, -} from "@livekit/components-react"; +import { RoomContext, useLocalParticipant } from "@livekit/components-react"; import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; import { type MuteStates } from "./MuteStates"; @@ -48,6 +44,8 @@ import { } from "../settings/settings"; import { ReactionsSenderProvider } from "../reactions/useReactionsSender"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; +// import { testAudioContext } from "../useAudioContext.test"; +import { MatrixAudioRenderer } from "../livekit/MatrixAudioRenderer"; // vi.hoisted(() => { // localStorage = {} as unknown as Storage; @@ -65,6 +63,7 @@ vi.mock("../tile/GridTile"); vi.mock("../tile/SpotlightTile"); vi.mock("@livekit/components-react"); vi.mock("../e2ee/sharedKeyManagement"); +vi.mock("../livekit/MatrixAudioRenderer"); vi.mock("react-use-measure", () => ({ default: (): [() => void, object] => [(): void => {}, {}], })); @@ -81,13 +80,15 @@ const roomMembers = new Map([carol].map((p) => [p.userId, p])); const roomId = "!foo:bar"; let useRoomEncryptionSystemMock: MockedFunction; + beforeEach(() => { vi.clearAllMocks(); - // RoomAudioRenderer is tested separately. + + // MatrixAudioRenderer is tested separately. ( - RoomAudioRenderer as MockedFunction + MatrixAudioRenderer as MockedFunction ).mockImplementation((_props) => { - return
mocked: RoomAudioRenderer
; + return
mocked: MatrixAudioRenderer
; }); ( useLocalParticipant as MockedFunction @@ -98,7 +99,6 @@ beforeEach(() => { localParticipant: localRtcMember as unknown as LocalParticipant, }) as unknown as ReturnType, ); - useRoomEncryptionSystemMock = useRoomEncryptionSystem as typeof useRoomEncryptionSystemMock; useRoomEncryptionSystemMock.mockReturnValue({ kind: E2eeType.NONE }); diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 4a375c8f..eb066603 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -79,6 +79,7 @@ function mockDevices(available: Map): MediaDevice { selectedId: "", selectedGroupId: "", select: (): void => {}, + useAsEarpiece: false, }; } diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index 427973b6..98edb8a4 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -9,7 +9,7 @@ exports[`InCallView > rendering > renders 1`] = ` class="header filler" />
- mocked: RoomAudioRenderer + mocked: MatrixAudioRenderer
{ ); }; -class MockAudioContext { - public static testContext: MockAudioContext; - - public constructor() { - MockAudioContext.testContext = this; - } - - public gain = vitest.mocked( - { - connect: () => {}, - gain: { - setValueAtTime: vitest.fn(), - }, +const gainNode = vi.mocked( + { + connect: (node: AudioNode) => node, + gain: { + setValueAtTime: vi.fn(), + value: 1, }, - true, - ); - - public setSinkId = vitest.fn().mockResolvedValue(undefined); - public decodeAudioData = vitest.fn().mockReturnValue(1); - public createBufferSource = vitest.fn().mockReturnValue( - vitest.mocked({ + }, + true, +); +const panNode = vi.mocked( + { + connect: (node: AudioNode) => node, + pan: { + setValueAtTime: vi.fn(), + value: 0, + }, + }, + true, +); +/** + * A shared audio context test instance. + * It can also be used to mock the `AudioContext` constructor in tests: + * `vi.stubGlobal("AudioContext", () => testAudioContext);` + */ +export const testAudioContext = { + gain: gainNode, + pan: panNode, + setSinkId: vi.fn().mockResolvedValue(undefined), + decodeAudioData: vi.fn().mockReturnValue(1), + createBufferSource: vi.fn().mockReturnValue( + vi.mocked({ connect: (v: unknown) => v, start: () => {}, addEventListener: (_name: string, cb: () => void) => cb(), }), - ); - public createGain = vitest.fn().mockReturnValue(this.gain); - public close = vitest.fn().mockResolvedValue(undefined); -} + ), + createGain: vi.fn().mockReturnValue(gainNode), + createStereoPanner: vi.fn().mockReturnValue(panNode), + close: vi.fn().mockResolvedValue(undefined), +}; +export const TestAudioContextConstructor = vi.fn(() => testAudioContext); + +let user: UserEvent; +beforeEach(() => { + vi.stubGlobal("AudioContext", TestAudioContextConstructor); + user = userEvent.setup(); +}); afterEach(() => { - vitest.unstubAllGlobals(); + vi.unstubAllGlobals(); + vi.clearAllMocks(); }); test("can play a single sound", async () => { - const user = userEvent.setup(); - vitest.stubGlobal("AudioContext", MockAudioContext); const { findByText } = render(); await user.click(await findByText("Valid sound")); - expect( - MockAudioContext.testContext.createBufferSource, - ).toHaveBeenCalledOnce(); + expect(testAudioContext.createBufferSource).toHaveBeenCalledOnce(); }); + test("will ignore sounds that are not registered", async () => { - const user = userEvent.setup(); - vitest.stubGlobal("AudioContext", MockAudioContext); const { findByText } = render(); await user.click(await findByText("Invalid sound")); - expect( - MockAudioContext.testContext.createBufferSource, - ).not.toHaveBeenCalled(); + expect(testAudioContext.createBufferSource).not.toHaveBeenCalled(); }); test("will use the correct device", () => { - vitest.stubGlobal("AudioContext", MockAudioContext); render( { selectedGroupId: "", available: new Map(), select: () => {}, + useAsEarpiece: false, }, videoInput: deviceStub, startUsingDeviceNames: () => {}, @@ -112,21 +125,46 @@ test("will use the correct device", () => { , ); - expect( - MockAudioContext.testContext.createBufferSource, - ).not.toHaveBeenCalled(); - expect(MockAudioContext.testContext.setSinkId).toHaveBeenCalledWith( - "chosen-device", - ); + expect(testAudioContext.createBufferSource).not.toHaveBeenCalled(); + expect(testAudioContext.setSinkId).toHaveBeenCalledWith("chosen-device"); }); test("will use the correct volume level", async () => { - const user = userEvent.setup(); - vitest.stubGlobal("AudioContext", MockAudioContext); soundEffectVolumeSetting.setValue(0.33); const { findByText } = render(); await user.click(await findByText("Valid sound")); - expect( - MockAudioContext.testContext.gain.gain.setValueAtTime, - ).toHaveBeenCalledWith(0.33, 0); + expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith( + 0.33, + 0, + ); + expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(0, 0); +}); + +test("will use the pan if earpice is selected", async () => { + const { findByText } = render( + {}, + useAsEarpiece: true, + }, + videoInput: deviceStub, + startUsingDeviceNames: () => {}, + stopUsingDeviceNames: () => {}, + }} + > + + , + ); + await user.click(await findByText("Valid sound")); + expect(testAudioContext.pan.pan.setValueAtTime).toHaveBeenCalledWith(1, 0); + + expect(testAudioContext.gain.gain.setValueAtTime).toHaveBeenCalledWith( + soundEffectVolumeSetting.getValue() * 0.1, + 0, + ); }); diff --git a/src/utils/test.ts b/src/utils/test.ts index 6e1b5457..51ed1ed2 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -27,12 +27,14 @@ import { type RemoteParticipant, type RemoteTrackPublication, type Room as LivekitRoom, + Track, } from "livekit-client"; import { randomUUID } from "crypto"; import { type RoomAndToDeviceEvents, type RoomAndToDeviceEventsHandlerMap, } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; +import { type TrackReference } from "@livekit/components-core"; import { LocalUserMediaViewModel, @@ -309,3 +311,24 @@ export class MockRTCSession extends TypedEventEmitter< return this; } } + +export const mockTrack = (identity: string): TrackReference => + ({ + participant: { + identity, + }, + publication: { + kind: Track.Kind.Audio, + source: "mic", + trackSid: "123", + track: { + attach: vi.fn(), + detach: vi.fn(), + setAudioContext: vi.fn(), + setWebAudioPlugins: vi.fn(), + setVolume: vi.fn(), + }, + }, + track: {}, + source: {}, + }) as unknown as TrackReference; From 4569d0135307bc7a79dd7b21b9de934bc6759a53 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 12:09:53 +0200 Subject: [PATCH 091/155] Update dependency vite to v6.2.7 [SECURITY] (#3240) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 212 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 122 insertions(+), 90 deletions(-) diff --git a/yarn.lock b/yarn.lock index a42427b4..bf7244c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4007,142 +4007,142 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.37.0" +"@rollup/rollup-android-arm-eabi@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.40.2" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-android-arm64@npm:4.37.0" +"@rollup/rollup-android-arm64@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-android-arm64@npm:4.40.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.37.0" +"@rollup/rollup-darwin-arm64@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-darwin-arm64@npm:4.40.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.37.0" +"@rollup/rollup-darwin-x64@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-darwin-x64@npm:4.40.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.37.0" +"@rollup/rollup-freebsd-arm64@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.40.2" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.37.0" +"@rollup/rollup-freebsd-x64@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-freebsd-x64@npm:4.40.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.2" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.37.0" +"@rollup/rollup-linux-arm-musleabihf@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.40.2" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.37.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.40.2" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.37.0" +"@rollup/rollup-linux-arm64-musl@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.40.2" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.2" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.2" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.37.0" +"@rollup/rollup-linux-riscv64-gnu@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.40.2" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.37.0" +"@rollup/rollup-linux-riscv64-musl@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.40.2" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.37.0" +"@rollup/rollup-linux-s390x-gnu@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.40.2" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.37.0" +"@rollup/rollup-linux-x64-gnu@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.40.2" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.37.0" +"@rollup/rollup-linux-x64-musl@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.40.2" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.37.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.40.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.37.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.40.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.37.0" +"@rollup/rollup-win32-x64-msvc@npm:4.40.2": + version: 4.40.2 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.40.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -4637,7 +4637,14 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.6, @types/estree@npm:^1.0.0": +"@types/estree@npm:1.0.7": + version: 1.0.7 + resolution: "@types/estree@npm:1.0.7" + checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c + languageName: node + linkType: hard + +"@types/estree@npm:^1.0.0": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" checksum: 10c0/cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a @@ -7849,6 +7856,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.4": + version: 6.4.4 + resolution: "fdir@npm:6.4.4" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/6ccc33be16945ee7bc841e1b4178c0b4cf18d3804894cb482aa514651c962a162f96da7ffc6ebfaf0df311689fb70091b04dd6caffe28d56b9ebdc0e7ccadfdd + languageName: node + linkType: hard + "fflate@npm:^0.4.8": version: 0.4.8 resolution: "fflate@npm:0.4.8" @@ -11567,31 +11586,31 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.30.1": - version: 4.37.0 - resolution: "rollup@npm:4.37.0" +"rollup@npm:^4.34.9": + version: 4.40.2 + resolution: "rollup@npm:4.40.2" dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.37.0" - "@rollup/rollup-android-arm64": "npm:4.37.0" - "@rollup/rollup-darwin-arm64": "npm:4.37.0" - "@rollup/rollup-darwin-x64": "npm:4.37.0" - "@rollup/rollup-freebsd-arm64": "npm:4.37.0" - "@rollup/rollup-freebsd-x64": "npm:4.37.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.37.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.37.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.37.0" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.37.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-riscv64-musl": "npm:4.37.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.37.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-x64-musl": "npm:4.37.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.37.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.37.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.37.0" - "@types/estree": "npm:1.0.6" + "@rollup/rollup-android-arm-eabi": "npm:4.40.2" + "@rollup/rollup-android-arm64": "npm:4.40.2" + "@rollup/rollup-darwin-arm64": "npm:4.40.2" + "@rollup/rollup-darwin-x64": "npm:4.40.2" + "@rollup/rollup-freebsd-arm64": "npm:4.40.2" + "@rollup/rollup-freebsd-x64": "npm:4.40.2" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.40.2" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.40.2" + "@rollup/rollup-linux-arm64-gnu": "npm:4.40.2" + "@rollup/rollup-linux-arm64-musl": "npm:4.40.2" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.40.2" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.40.2" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.40.2" + "@rollup/rollup-linux-riscv64-musl": "npm:4.40.2" + "@rollup/rollup-linux-s390x-gnu": "npm:4.40.2" + "@rollup/rollup-linux-x64-gnu": "npm:4.40.2" + "@rollup/rollup-linux-x64-musl": "npm:4.40.2" + "@rollup/rollup-win32-arm64-msvc": "npm:4.40.2" + "@rollup/rollup-win32-ia32-msvc": "npm:4.40.2" + "@rollup/rollup-win32-x64-msvc": "npm:4.40.2" + "@types/estree": "npm:1.0.7" fsevents: "npm:~2.3.2" dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -11638,7 +11657,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/2e00382e08938636edfe0a7547ea2eaa027205dc0b6ff85d8b82be0fbe55a4ef88a1995fee2a5059e33dbccf12d1376c236825353afb89c96298cc95c5160a46 + checksum: 10c0/cbe9b766891da74fbf7c3b50420bb75102e5c59afc0ea45751f7e43a581d2cd93367763f521f820b72e341cf1f6b9951fbdcd3be67a1b0aa774b754525a8b9c7 languageName: node linkType: hard @@ -12471,6 +12490,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.13": + version: 0.2.13 + resolution: "tinyglobby@npm:0.2.13" + dependencies: + fdir: "npm:^6.4.4" + picomatch: "npm:^4.0.2" + checksum: 10c0/ef07dfaa7b26936601d3f6d999f7928a4d1c6234c5eb36896bb88681947c0d459b7ebe797022400e555fe4b894db06e922b95d0ce60cb05fd827a0a66326b18c + languageName: node + linkType: hard + "tinypool@npm:^1.0.2": version: 1.0.2 resolution: "tinypool@npm:1.0.2" @@ -13179,13 +13208,16 @@ __metadata: linkType: hard "vite@npm:^5.0.0 || ^6.0.0, vite@npm:^6.0.0": - version: 6.2.6 - resolution: "vite@npm:6.2.6" + version: 6.3.5 + resolution: "vite@npm:6.3.5" dependencies: esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.4" fsevents: "npm:~2.3.3" + picomatch: "npm:^4.0.2" postcss: "npm:^8.5.3" - rollup: "npm:^4.30.1" + rollup: "npm:^4.34.9" + tinyglobby: "npm:^0.2.13" peerDependencies: "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: ">=1.21.0" @@ -13226,7 +13258,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/68a2ed3e61bdd654c59b817b4f3203065241c66d1739faa707499130f3007bc3a666c7a8320a4198e275e62b5e4d34d9b78a6533f69e321d366e76f5093b2071 + checksum: 10c0/df70201659085133abffc6b88dcdb8a57ef35f742a01311fc56a4cfcda6a404202860729cc65a2c401a724f6e25f9ab40ce4339ed4946f550541531ced6fe41c languageName: node linkType: hard From 18a59dd7dbb851eaa5ac44dfd12e577092031b3e Mon Sep 17 00:00:00 2001 From: Timo Date: Wed, 14 May 2025 17:23:42 +0200 Subject: [PATCH 092/155] use optional audio context and effect to initiate it + review --- src/livekit/MatrixAudioRenderer.test.tsx | 2 +- src/livekit/MatrixAudioRenderer.tsx | 51 ++++++++++++++---------- src/room/InCallView.test.tsx | 1 - src/settings/DeviceSelection.tsx | 2 +- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/livekit/MatrixAudioRenderer.test.tsx b/src/livekit/MatrixAudioRenderer.test.tsx index 9d91e6fa..637e02ed 100644 --- a/src/livekit/MatrixAudioRenderer.test.tsx +++ b/src/livekit/MatrixAudioRenderer.test.tsx @@ -34,7 +34,7 @@ afterEach(() => { vi.mock("@livekit/components-react", async (importOriginal) => { return { - ...(await importOriginal()), // this will only affect "foo" outside of the original module + ...(await importOriginal()), AudioTrack: (props: { trackRef: TrackReference }): ReactNode => { return (