/* Copyright 2022-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 { type ChangeEvent, type FC, type ReactNode, useCallback, useEffect, useMemo, useId, useState, } from "react"; import { useTranslation } from "react-i18next"; import { UNSTABLE_MSC4354_STICKY_EVENTS, type MatrixClient, } from "matrix-js-sdk"; import { logger } from "matrix-js-sdk/lib/logger"; import { EditInPlace, ErrorMessage, Root as Form, Heading, HelpMessage, InlineField, Label, RadioControl, Separator, } from "@vector-im/compound-web"; import { type Room as LivekitRoom } from "livekit-client"; import { FieldRow, InputField } from "../input/Input"; import { Config } from "../config/Config"; import { type Setting, useSetting, duplicateTiles as duplicateTilesSetting, debugTileLayout as debugTileLayoutSetting, showConnectionStats as showConnectionStatsSetting, muteAllAudio as muteAllAudioSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, matrixRTCMode as matrixRTCModeSetting, customLivekitUrl as customLivekitUrlSetting, advancedScreenShare as advancedScreenShareSetting, screenShareResolution as screenShareResolutionSetting, screenShareFramerate as screenShareFramerateSetting, screenShareBitrate as screenShareBitrateSetting, screenShareCodec as screenShareCodecSetting, advancedCamera as advancedCameraSetting, cameraResolution as cameraResolutionSetting, cameraFramerate as cameraFramerateSetting, cameraBitrate as cameraBitrateSetting, cameraCodec as cameraCodecSetting, echoCancellationSetting, noiseSuppressionSetting, autoGainControlSetting, type VideoCodec, enableExtendedLivekitLogs as enableExtendedLivekitLogsSetting, } from "./settings"; import { MatrixRTCMode } from "../config/ConfigOptions"; import styles from "./DeveloperSettingsTab.module.css"; import settingsStyles from "./SettingsModal.module.css"; import { Slider } from "../Slider"; import { useUrlParams } from "../UrlParams"; import { getSFUConfigWithOpenID } from "../livekit/openIDSFU"; import { useBehavior } from "../useBehavior"; import { type ViewModel } from "../state/ViewModel.ts"; /** * The state of MatrixRTC's media key rotation. */ export interface KeyRotationInfo { /** Whether the call is large enough that MatrixRTC has stopped rotating the media key. */ suppressed: boolean; participantCount: number; } /** * The Snapshot combines all fields the developer settings tab needs from the * surrounding call. Everything else in this tab is read from the settings store * or the environment directly. */ export interface DeveloperSettingsSnapshot { /** The media key rotation state, or `null` when we are not in a call. */ keyRotation: KeyRotationInfo | null; } /** * Shows whether the call is large enough that MatrixRTC has stopped rotating the media key. */ const KeyRotationStatus: FC<{ info: KeyRotationInfo }> = ({ info }) => (

Media key rotation:{" "} {info.suppressed ? `suppressed, participant limit reached (${info.participantCount} participants)` : `active (${info.participantCount} participants)`}

); interface Props { client: MatrixClient; roomId?: string; livekitRooms?: { room: LivekitRoom; url: string; isLocal?: boolean; livekitAlias?: string; }[]; env: ImportMetaEnv; vm: ViewModel; } export const DeveloperSettingsTab: FC = ({ client, livekitRooms, roomId, env, vm, }) => { const { t } = useTranslation(); const keyRotation = useBehavior(vm.keyRotation$); const [duplicateTiles, setDuplicateTiles] = useSetting(duplicateTilesSetting); const [debugTileLayout, setDebugTileLayout] = useSetting( debugTileLayoutSetting, ); const [stickyEventsSupported, setStickyEventsSupported] = useState(false); useEffect(() => { client .doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS) .then((result) => { setStickyEventsSupported(result); }) .catch((ex) => { logger.warn("Failed to check if sticky events are supported", ex); }); }, [client]); const [matrixRTCMode, setMatrixRTCMode] = useSetting(matrixRTCModeSetting); const matrixRTCModeRadioGroup = useId(); const onMatrixRTCModeChange = useCallback( (e: ChangeEvent) => { setMatrixRTCMode(e.target.value as MatrixRTCMode); }, [setMatrixRTCMode], ); const configMatrixRTCMode = Config.get().matrix_rtc_mode as | MatrixRTCMode | undefined; const matrixRTCModeForced = configMatrixRTCMode !== undefined; const effectiveMatrixRTCMode = configMatrixRTCMode ?? matrixRTCMode; const [showConnectionStats, setShowConnectionStats] = useSetting( showConnectionStatsSetting, ); const [alwaysShowIphoneEarpiece, setAlwaysShowIphoneEarpiece] = useSetting( alwaysShowIphoneEarpieceSetting, ); const [enableExtendedLivekitLogs, setEnableExtendedLivekitLogs] = useSetting( enableExtendedLivekitLogsSetting, ); const [customLivekitUrlUpdateError, setCustomLivekitUrlUpdateError] = useState(null); const [customLivekitUrl, setCustomLivekitUrl] = useSetting( customLivekitUrlSetting, ); const [customLivekitUrlTextBuffer, setCustomLivekitUrlTextBuffer] = useState(customLivekitUrl); useEffect(() => { setCustomLivekitUrlTextBuffer(customLivekitUrl); }, [customLivekitUrl]); const [muteAllAudio, setMuteAllAudio] = useSetting(muteAllAudioSetting); const urlParams = useUrlParams(); const localSfuUrl = useMemo((): URL | null => { const localRoom = livekitRooms?.find((r) => r.isLocal)?.room; if (localRoom?.engine.client.ws?.url) { // strip the URL params const url = new URL(localRoom.engine.client.ws.url); url.search = ""; return url; } return null; }, [livekitRooms]); const MediaQualitySettings: React.FC<{ id: string; header: string; toggleLabel: string; description: string; toggleSetting: Setting; resolutionSetting: Setting; framerateSetting: Setting; bitrateSetting: Setting; codecSetting: Setting; resolutionOptions: { value: string; label: string }[]; bitrateRange: { min: number; max: number; step: number }; }> = ({ id, header, toggleLabel, description, toggleSetting, resolutionSetting, framerateSetting, bitrateSetting, codecSetting, resolutionOptions, bitrateRange, }): ReactNode => { const [advancedEnabled, setAdvancedEnabled] = useSetting(toggleSetting); const [resolution, setResolution] = useSetting(resolutionSetting); const [framerate, setFramerate] = useSetting(framerateSetting); const [framerateRaw, setFramerateRaw] = useState(framerate); const [bitrate, setBitrate] = useSetting(bitrateSetting); const [bitrateRaw, setBitrateRaw] = useState(bitrate); const [codec, setCodec] = useSetting(codecSetting); return ( <>

{header}

setAdvancedEnabled(e.target.checked)} /> {advancedEnabled && ( <>
`${v} fps`} />
`${(v / 1_000_000).toFixed(1)} Mbps` } />
)} ); }; const AudioProcessingSettings: React.FC = (): ReactNode => { const [echoCancellation, setEchoCancellation] = useSetting( echoCancellationSetting, ); const [noiseSuppression, setNoiseSuppression] = useSetting( noiseSuppressionSetting, ); const [autoGainControl, setAutoGainControl] = useSetting( autoGainControlSetting, ); return ( <>

{t("settings.audio_processing_header", "Audio processing")}

{t( "settings.audio_processing_description", "Changes apply on next call join.", )}

setEchoCancellation(e.target.checked)} /> setNoiseSuppression(e.target.checked)} /> setAutoGainControl(e.target.checked)} /> ); }; return ( <>

{t("developer_mode.hostname", { hostname: window.location.hostname || "unknown", })}

{t("version", { productName: import.meta.env.VITE_PRODUCT_NAME || "Element Call", version: import.meta.env.VITE_APP_VERSION || "dev", })}

{t("developer_mode.crypto_version", { version: client.getCrypto()?.getVersion() || "unknown", })}

{t("developer_mode.matrix_id", { id: client.getUserId() || "unknown", })}

{t("developer_mode.device_id", { id: client.getDeviceId() || "unknown", })}

{keyRotation !== null && } ): void => { const value = event.target.valueAsNumber; if (value < 0) { return; } setDuplicateTiles(Number.isNaN(value) ? 0 : value); }, [setDuplicateTiles], )} /> ): void => setDebugTileLayout(event.target.checked) } /> ): void => { setShowConnectionStats(event.target.checked); }, [setShowConnectionStats], )} /> ): void => { setMuteAllAudio(event.target.checked); }, [setMuteAllAudio], )} /> {" "} ): void => { setAlwaysShowIphoneEarpiece(event.target.checked); }, [setAlwaysShowIphoneEarpiece], )} /> ): void => { setEnableExtendedLivekitLogs(event.target.checked); }, [setEnableExtendedLivekitLogs], )} /> e.preventDefault()} helpLabel={ customLivekitUrl === null ? t("developer_mode.custom_livekit_url.from_config") : t("developer_mode.custom_livekit_url.current_url") + customLivekitUrl } label={t("developer_mode.custom_livekit_url.label")} saveButtonLabel={t("developer_mode.custom_livekit_url.save")} savingLabel={t("developer_mode.custom_livekit_url.saving")} cancelButtonLabel={t("developer_mode.custom_livekit_url.reset")} onSave={useCallback( async (e: React.FormEvent): Promise => { if ( roomId === undefined || customLivekitUrlTextBuffer === "" || customLivekitUrlTextBuffer === null ) { setCustomLivekitUrl(null); return; } try { const userId = client.getUserId(); const deviceId = client.getDeviceId(); if (userId === null || deviceId === null) { throw new Error("Invalid user or device ID"); } await getSFUConfigWithOpenID( client, { userId, deviceId, memberId: "" }, customLivekitUrlTextBuffer, roomId, ); setCustomLivekitUrlUpdateError(null); setCustomLivekitUrl(customLivekitUrlTextBuffer); } catch { setCustomLivekitUrlUpdateError("invalid URL (did not update)"); } }, [customLivekitUrlTextBuffer, setCustomLivekitUrl, client, roomId], )} value={customLivekitUrlTextBuffer ?? ""} onChange={useCallback( (event: ChangeEvent): void => { setCustomLivekitUrlTextBuffer(event.target.value); }, [setCustomLivekitUrlTextBuffer], )} onCancel={useCallback( (e: React.FormEvent) => { setCustomLivekitUrl(null); }, [setCustomLivekitUrl], )} serverInvalid={customLivekitUrlUpdateError !== null} > {customLivekitUrlUpdateError !== null && ( {customLivekitUrlUpdateError} )} {t("developer_mode.matrixRTCMode.title")} {matrixRTCModeForced &&

Your deployment overrides the mode.

}
} > {t("developer_mode.matrixRTCMode.Legacy.description")} } > {t("developer_mode.matrixRTCMode.Comptibility.description")} } > {t("developer_mode.matrixRTCMode.Matrix_2_0.description")}
{livekitRooms?.map((livekitRoom) => (

{t("developer_mode.livekit_sfu", { url: livekitRoom.url || "unknown", })}

LivekitAlias: {livekitRoom.livekitAlias}

connectionState (wont hot reload): {livekitRoom.room.state}

{livekitRoom.isLocal &&

ws-url: {localSfuUrl?.href}

}

{t("developer_mode.livekit_server_info")}( {livekitRoom.isLocal ? "local" : "remote"})

            {livekitRoom.room.serverInfo
              ? JSON.stringify(livekitRoom.room.serverInfo, null, 2)
              : "undefined"}
            {livekitRoom.room.metadata}
          

Local Participant

            {livekitRoom.room.localParticipant.identity}
          

Remote Participants

    {Array.from(livekitRoom.room.remoteParticipants.keys()).map( (id) => (
  • {id}
  • ), )}
))}

{t("developer_mode.environment_variables")}

{JSON.stringify(env, null, 2)}

{t("developer_mode.url_params")}

{JSON.stringify(urlParams, null, 2)}
); };