Compare commits

..

12 Commits

Author SHA1 Message Date
Hugh Nimmo-Smith
564349c96f Update src/room/InCallView.tsx 2025-02-26 10:18:56 +00:00
Robin
8bf81defd4 Leave session when error occurs and show error screens in widget mode
This is a refactor of how we handle and present errors that happen during a call. Rather than assuming that any errors will be manually passed to onLeave, this wraps the call in a React error boundary that takes care of leaving the session and presenting connection errors or crash screens as needed. Importantly, this now allows the 'connection lost' screen to be visible even in widget mode, since we aren't entangling errors with the 'call ended' component (which is deliberately hidden from widgets).
2025-02-24 18:26:41 +07:00
Robin
cd05df3e33 Merge pull request #3020 from element-hq/robin/clarify-use-latest
Clarify what useLatest is for
2025-02-24 18:22:46 +07:00
Robin
9b59544db2 Clarify what useLatest is for
I was touching this file and noticed a chance to clean up this little TODO comment by swapping in our useLatest hook and giving it better documentation.
2025-02-24 18:18:15 +07:00
Robin
91671d9ff1 Merge pull request #3019 from element-hq/robin/actually-hide-header
Actually hide the header on error pages
2025-02-24 17:47:22 +07:00
Robin
e4d7baf7bb Actually hide the header on error pages
Currently, if you set hideHeader to true, you get a big empty <header> on error pages which serves no purpose except to push the screen's content off center.
2025-02-24 17:37:11 +07:00
Robin
832a5aaf57 Merge pull request #3011 from element-hq/robin/close-action
Send a 'close' action when the widget is ready to close
2025-02-24 17:23:33 +07:00
fkwp
5e8395262c Merge pull request #3017 from element-hq/renovate/livekit-client
Update dependency livekit-client to v2.9.3
2025-02-24 08:28:40 +01:00
renovate[bot]
ad461a7643 Update dependency livekit-client to v2.9.3 2025-02-24 02:54:51 +00:00
Robin
028b3104e4 Merge pull request #3009 from element-hq/renovate/livekit-client
Update dependency livekit-client to v2.9.1
2025-02-20 13:24:17 +07:00
Timo
4015efa6c6 fix lk usage in test 2025-02-20 06:33:01 +03:00
renovate[bot]
f111c64264 Update dependency livekit-client to v2.9.1 2025-02-18 15:52:41 +00:00
9 changed files with 178 additions and 140 deletions

View File

@@ -31,6 +31,7 @@
"@formatjs/intl-segmenter": "^11.7.3",
"@livekit/components-core": "^0.12.0",
"@livekit/components-react": "^2.0.0",
"@livekit/protocol": "^1.33.0",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/core": "^1.25.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.57.0",

View File

@@ -30,10 +30,14 @@ export const FullScreenView: FC<FullScreenViewProps> = ({
const { hideHeader } = useUrlParams();
return (
<div className={classNames(styles.page, className)}>
<Header>
<LeftNav>{!hideHeader && <HeaderLogo />}</LeftNav>
<RightNav />
</Header>
{!hideHeader && (
<Header>
<LeftNav>
<HeaderLogo />
</LeftNav>
<RightNav />
</Header>
)}
<div className={styles.container}>
<div className={styles.content}>{children}</div>
</div>

View File

@@ -5,17 +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 FormEventHandler,
type ReactNode,
useCallback,
useState,
} from "react";
import { type FC, type FormEventHandler, useCallback, useState } from "react";
import { type MatrixClient } from "matrix-js-sdk/src/client";
import { Trans, useTranslation } from "react-i18next";
import { Button, Heading, Text } from "@vector-im/compound-web";
import { OfflineIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { useNavigate } from "react-router-dom";
import { logger } from "matrix-js-sdk/src/logger";
@@ -28,15 +21,12 @@ import { FieldRow, InputField } from "../input/Input";
import { StarRatingInput } from "../input/StarRatingInput";
import { Link } from "../button/Link";
import { LinkButton } from "../button";
import { ErrorView } from "../ErrorView";
interface Props {
client: MatrixClient;
isPasswordlessUser: boolean;
confineToRoom: boolean;
endedCallId: string;
leaveError?: Error;
reconnect: () => void;
}
export const CallEndedView: FC<Props> = ({
@@ -44,8 +34,6 @@ export const CallEndedView: FC<Props> = ({
isPasswordlessUser,
confineToRoom,
endedCallId,
leaveError,
reconnect,
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
@@ -143,61 +131,32 @@ export const CallEndedView: FC<Props> = ({
</div>
);
const renderBody = (): ReactNode => {
if (leaveError) {
return (
<>
<main className={styles.main}>
<ErrorView
Icon={OfflineIcon}
title={t("error.connection_lost")}
rageshake
>
<p>{t("error.connection_lost_description")}</p>
<Button onClick={reconnect}>
{t("call_ended_view.reconnect_button")}
</Button>
</ErrorView>
</main>
</>
);
} else {
return (
<>
<main className={styles.main}>
<Heading size="xl" weight="semibold" className={styles.headline}>
{surveySubmitted
? t("call_ended_view.headline", {
displayName,
})
: t("call_ended_view.headline", {
displayName,
}) +
"\n" +
t("call_ended_view.survey_prompt")}
</Heading>
{(!surveySubmitted || confineToRoom) &&
PosthogAnalytics.instance.isEnabled()
? qualitySurveyDialog
: createAccountDialog}
</main>
{!confineToRoom && (
<Text className={styles.footer}>
<Link to="/"> {t("call_ended_view.not_now_button")} </Link>
</Text>
)}
</>
);
}
};
return (
<>
<Header>
<LeftNav>{!confineToRoom && <HeaderLogo />}</LeftNav>
<RightNav />
</Header>
<div className={styles.container}>{renderBody()}</div>
<div className={styles.container}>
<main className={styles.main}>
<Heading size="xl" weight="semibold" className={styles.headline}>
{surveySubmitted
? t("call_ended_view.headline", { displayName })
: t("call_ended_view.headline", { displayName }) +
"\n" +
t("call_ended_view.survey_prompt")}
</Heading>
{(!surveySubmitted || confineToRoom) &&
PosthogAnalytics.instance.isEnabled()
? qualitySurveyDialog
: createAccountDialog}
</main>
{!confineToRoom && (
<Text className={styles.footer}>
<Link to="/"> {t("call_ended_view.not_now_button")} </Link>
</Text>
)}
</div>
</>
);
};

View File

@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
*/
import { beforeEach, expect, type MockedFunction, test, vitest } from "vitest";
import { render, waitFor } from "@testing-library/react";
import { render, waitFor, screen } from "@testing-library/react";
import { type MatrixClient } from "matrix-js-sdk/src/client";
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import { of } from "rxjs";
@@ -14,6 +14,7 @@ import { JoinRule, type RoomState } from "matrix-js-sdk/src/matrix";
import { BrowserRouter } from "react-router-dom";
import userEvent from "@testing-library/user-event";
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
import { useState } from "react";
import { type MuteStates } from "./MuteStates";
import { prefetchSounds } from "../soundUtils";
@@ -184,3 +185,28 @@ test("will play a leave sound synchronously in widget mode", async () => {
);
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
});
test("GroupCallView leaves the session when an error occurs", async () => {
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(() => {
const [error, setError] = useState<Error | null>(null);
if (error !== null) throw error;
return (
<div>
<button onClick={() => setError(new Error())}>Panic!</button>
</div>
);
});
const user = userEvent.setup();
const { rtcSession } = createGroupCallView(null);
await user.click(screen.getByRole("button", { name: "Panic!" }));
screen.getByText("error.generic");
expect(leaveRTCSession).toHaveBeenCalledWith(
rtcSession,
"error",
expect.any(Promise),
);
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
// Ensure that the playSound promise resolves within this test to avoid
// impacting the results of other tests
await waitFor(() => expect(leaveRTCSession).toHaveResolved());
});

View File

@@ -7,10 +7,11 @@ Please see LICENSE in the repository root for full details.
import {
type FC,
type ReactElement,
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { type MatrixClient } from "matrix-js-sdk/src/client";
@@ -21,9 +22,14 @@ import {
import { logger } from "matrix-js-sdk/src/logger";
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
import { JoinRule } from "matrix-js-sdk/src/matrix";
import { WebBrowserIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import {
OfflineIcon,
WebBrowserIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { ErrorBoundary } from "@sentry/react";
import { Button } from "@vector-im/compound-web";
import type { IWidgetApiRequest } from "matrix-widget-api";
import {
@@ -31,19 +37,16 @@ import {
type JoinCallData,
type WidgetHelpers,
} from "../widget";
import { FullScreenView } from "../FullScreenView";
import { ErrorPage, FullScreenView } from "../FullScreenView";
import { LobbyView } from "./LobbyView";
import { type MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
import { useProfile } from "../profile/useProfile";
import { findDeviceByName } from "../utils/media";
import { ActiveCall } from "./InCallView";
import { ActiveCall, ConnectionLostError } from "./InCallView";
import { MUTE_PARTICIPANT_COUNT, type MuteStates } from "./MuteStates";
import {
useMediaDevices,
type MediaDevices,
} from "../livekit/MediaDevicesContext";
import { useMediaDevices } from "../livekit/MediaDevicesContext";
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers";
import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement";
@@ -65,6 +68,11 @@ declare global {
}
}
interface GroupCallErrorPageProps {
error: Error | unknown;
resetError: () => void;
}
interface Props {
client: MatrixClient;
isPasswordlessUser: boolean;
@@ -154,12 +162,8 @@ export const GroupCallView: FC<Props> = ({
);
const deviceContext = useMediaDevices();
const latestDevices = useRef<MediaDevices | undefined>(undefined);
latestDevices.current = deviceContext;
// TODO: why do we use a ref here instead of using muteStates directly?
const latestMuteStates = useRef<MuteStates | undefined>(undefined);
latestMuteStates.current = muteStates;
const latestDevices = useLatest(deviceContext);
const latestMuteStates = useLatest(muteStates);
useEffect(() => {
const defaultDeviceSetup = async ({
@@ -232,19 +236,25 @@ export const GroupCallView: FC<Props> = ({
void enterRTCSession(rtcSession, perParticipantE2EE);
}
}
}, [widget, rtcSession, preload, skipLobby, perParticipantE2EE]);
}, [
widget,
rtcSession,
preload,
skipLobby,
perParticipantE2EE,
latestDevices,
latestMuteStates,
]);
const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
const navigate = useNavigate();
const onLeave = useCallback(
(leaveError?: Error): void => {
(cause: "user" | "error" = "user"): void => {
const audioPromise = leaveSoundContext.current?.playSound("left");
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
// therefore we want the event to be sent instantly without getting queued/batched.
const sendInstantly = !!widget;
setLeaveError(leaveError);
setLeft(true);
// we need to wait until the callEnded event is tracked on posthog.
// Otherwise the iFrame gets killed before the callEnded event got tracked.
@@ -260,7 +270,7 @@ export const GroupCallView: FC<Props> = ({
leaveRTCSession(
rtcSession,
leaveError === undefined ? "user" : "error",
cause,
// Wait for the sound in widget mode (it's not long)
Promise.all([audioPromise, posthogRequest]),
)
@@ -309,14 +319,6 @@ export const GroupCallView: FC<Props> = ({
}
}, [widget, isJoined, rtcSession]);
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
logger.error("Error re-entering RTC session on reconnect", e);
});
}, [rtcSession, perParticipantE2EE]);
const joinRule = useJoinRule(rtcSession.room);
const [shareModalOpen, setInviteModalOpen] = useState(false);
@@ -333,6 +335,43 @@ export const GroupCallView: FC<Props> = ({
const { t } = useTranslation();
const errorPage = useMemo(() => {
function GroupCallErrorPage({
error,
resetError,
}: GroupCallErrorPageProps): ReactElement {
useEffect(() => {
if (rtcSession.isJoined()) onLeave("error");
}, [error]);
const onReconnect = useCallback(() => {
setLeft(false);
resetError();
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
logger.error("Error re-entering RTC session on reconnect", e);
});
}, [resetError]);
return error instanceof ConnectionLostError ? (
<FullScreenView>
<ErrorView
Icon={OfflineIcon}
title={t("error.connection_lost")}
rageshake
>
<p>{t("error.connection_lost_description")}</p>
<Button onClick={onReconnect}>
{t("call_ended_view.reconnect_button")}
</Button>
</ErrorView>
</FullScreenView>
) : (
<ErrorPage error={error} />
);
}
return GroupCallErrorPage;
}, [onLeave, rtcSession, perParticipantE2EE, t]);
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
// If we have a encryption system but the browser does not support it.
return (
@@ -367,8 +406,9 @@ export const GroupCallView: FC<Props> = ({
</>
);
let body: ReactNode;
if (isJoined) {
return (
body = (
<>
{shareModal}
<ActiveCall
@@ -396,36 +436,32 @@ export const GroupCallView: FC<Props> = ({
// submitting anything.
if (
isPasswordlessUser ||
(PosthogAnalytics.instance.isEnabled() && widget === null) ||
leaveError
(PosthogAnalytics.instance.isEnabled() && widget === null)
) {
return (
<>
<CallEndedView
endedCallId={rtcSession.room.roomId}
client={client}
isPasswordlessUser={isPasswordlessUser}
confineToRoom={confineToRoom}
leaveError={leaveError}
reconnect={onReconnect}
/>
;
</>
body = (
<CallEndedView
endedCallId={rtcSession.room.roomId}
client={client}
isPasswordlessUser={isPasswordlessUser}
confineToRoom={confineToRoom}
/>
);
} else {
// If the user is a regular user, we'll have sent them back to the homepage,
// so just sit here & do nothing: otherwise we would (briefly) mount the
// LobbyView again which would open capture devices again.
return null;
body = null;
}
} else if (left && widget !== null) {
// Left in widget mode:
if (!returnToLobby) {
return null;
body = null;
}
} else if (preload || skipLobby) {
return null;
body = null;
} else {
body = lobbyView;
}
return lobbyView;
return <ErrorBoundary fallback={errorPage}>{body}</ErrorBoundary>;
};

View File

@@ -102,6 +102,8 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
const maxTapDurationMs = 400;
export class ConnectionLostError extends Error {}
export interface ActiveCallProps
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
e2eeSystem: EncryptionSystem;
@@ -173,7 +175,8 @@ export interface InCallViewProps {
livekitRoom: Room;
muteStates: MuteStates;
participantCount: number;
onLeave: (error?: Error) => void;
/** Function to call when the user explicitly ends the call */
onLeave: () => void;
hideHeader: boolean;
otelGroupCallMembership?: OTelGroupCallMembership;
connState: ECConnectionState;
@@ -198,13 +201,10 @@ export const InCallView: FC<InCallViewProps> = ({
useWakeLock();
useEffect(() => {
if (connState === ConnectionState.Disconnected) {
// annoyingly we don't get the disconnection reason this way,
// only by listening for the emitted event
onLeave(new Error("Disconnected from call server"));
}
}, [connState, onLeave]);
// annoyingly we don't get the disconnection reason this way,
// only by listening for the emitted event
if (connState === ConnectionState.Disconnected)
throw new ConnectionLostError();
const containerRef1 = useRef<HTMLDivElement | null>(null);
const [containerRef2, bounds] = useMeasure();

View File

@@ -13,7 +13,8 @@ import {
type TrackReference,
type TrackReferencePlaceholder,
} from "@livekit/components-core";
import { Track, TrackPublication } from "livekit-client";
import { LocalTrackPublication, Track } from "livekit-client";
import { TrackInfo } from "@livekit/protocol";
import { type ComponentProps } from "react";
import { MediaView } from "./MediaView";
@@ -28,7 +29,10 @@ describe("MediaView", () => {
};
const trackReference: TrackReference = {
...trackReferencePlaceholder,
publication: new TrackPublication(Track.Kind.Video, "id", "name"),
publication: new LocalTrackPublication(
Track.Kind.Video,
new TrackInfo({ sid: "id", name: "name" }),
),
};
const baseProps: ComponentProps<typeof MediaView> = {

View File

@@ -8,12 +8,13 @@ Please see LICENSE in the repository root for full details.
import { type RefObject, useRef } from "react";
export interface LatestRef<T> extends RefObject<T> {
current: T;
current: T; // Always defined, unlike RefObject["current"]
}
/**
* React hook that returns a ref containing the value given on the latest
* render.
* render. Useful for accessing the latest value of something in an effect or
* callback when you don't want reactivity.
*/
export function useLatest<T>(value: T): LatestRef<T> {
const ref = useRef<T>(value);

View File

@@ -1756,10 +1756,10 @@
resolved "https://registry.yarnpkg.com/@livekit/mutex/-/mutex-1.1.1.tgz#72492b611d55be8130ba2271b7a436d94b1bc6d4"
integrity sha512-EsshAucklmpuUAfkABPxJNhzj9v2sG7JuzFDL4ML1oJQSV14sqrpTYnsaOudMAw9yOaW53NU3QQTlUQoRs4czw==
"@livekit/protocol@1.30.0":
version "1.30.0"
resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.30.0.tgz#c79e98aca2a822f9bd217d016c6efd0f472f206f"
integrity sha512-SDI9ShVKj8N3oOSinr8inaxD3FXgmgoJlqN35uU/Yx1sdoDeQbzAuBFox7bYjM+VhnZ1V22ivIDjAsKr00H+XQ==
"@livekit/protocol@1.33.0", "@livekit/protocol@^1.33.0":
version "1.33.0"
resolved "https://registry.yarnpkg.com/@livekit/protocol/-/protocol-1.33.0.tgz#b8dfcf82ca501e747533d519e51b5c80176bf03e"
integrity sha512-361mBlFgI3nvn8oSQIL38gDUBGbOSwsEOqPgX0c1Jwz75/sD/TTvPeAM4zAz6OrV5Q4vI4Ruswecnyv5SG4oig==
dependencies:
"@bufbuild/protobuf" "^1.10.0"
@@ -6165,19 +6165,19 @@ lines-and-columns@^1.1.6:
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
livekit-client@^2.5.7:
version "2.8.1"
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.8.1.tgz#2492587cde93ffb7bafcc3ce91f67e108fc63c4a"
integrity sha512-HPv9iHNrnBANI9ucK7CKZspx0sBZK3hjR2EbwaV08+J3RM9+tNGL2ob2n76nxJLEZG7LzdWlLZdbr4fQBP6Hkg==
version "2.9.3"
resolved "https://registry.yarnpkg.com/livekit-client/-/livekit-client-2.9.3.tgz#1f0e494561ba8790d214047d413e37db3aebdf16"
integrity sha512-mDLgenjmGI3ga1qXsuonw8cTpQMUTm6T8tmjB/O+uzjsjI9UP0ebzjgrcOBn+IamjbulIdWWvtrt5A/6ftuH5A==
dependencies:
"@livekit/mutex" "1.1.1"
"@livekit/protocol" "1.30.0"
"@livekit/protocol" "1.33.0"
events "^3.3.0"
loglevel "^1.8.0"
sdp-transform "^2.14.1"
loglevel "^1.9.2"
sdp-transform "^2.15.0"
ts-debounce "^4.0.0"
tslib "2.8.1"
typed-emitter "^2.1.0"
webrtc-adapter "^9.0.0"
webrtc-adapter "^9.0.1"
locate-path@^5.0.0:
version "5.0.0"
@@ -6218,7 +6218,7 @@ loglevel@1.9.1:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.1.tgz#d63976ac9bcd03c7c873116d41c2a85bafff1be7"
integrity sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==
loglevel@^1.7.1, loglevel@^1.8.0, loglevel@^1.9.1:
loglevel@^1.7.1, loglevel@^1.9.1, loglevel@^1.9.2:
version "1.9.2"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08"
integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==
@@ -7626,13 +7626,20 @@ rxjs-report-usage@^1.0.4:
glob "~7.2.0"
prompts "~2.4.2"
rxjs@7.8.1, rxjs@^7.5.2, rxjs@^7.8.1:
rxjs@7.8.1, rxjs@^7.8.1:
version "7.8.1"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543"
integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==
dependencies:
tslib "^2.1.0"
rxjs@^7.5.2:
version "7.8.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
dependencies:
tslib "^2.1.0"
safe-array-concat@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3"
@@ -7701,7 +7708,7 @@ scheduler@^0.23.2:
dependencies:
loose-envify "^1.1.0"
sdp-transform@^2.14.1:
sdp-transform@^2.14.1, sdp-transform@^2.15.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/sdp-transform/-/sdp-transform-2.15.0.tgz#79d37a2481916f36a0534e07b32ceaa87f71df42"
integrity sha512-KrOH82c/W+GYQ0LHqtr3caRpM3ITglq3ljGUIb8LTki7ByacJZ9z+piSGiwZDsRyhQbYBOBJgr2k6X4BZXi3Kw==
@@ -8745,7 +8752,7 @@ webpack-virtual-modules@^0.6.2:
resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8"
integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==
webrtc-adapter@^9.0.0:
webrtc-adapter@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-9.0.1.tgz#d4efa22ca9604cb2c8cdb9e492815ba37acfa0b2"
integrity sha512-1AQO+d4ElfVSXyzNVTOewgGT/tAomwwztX/6e3totvyyzXPvXIIuUUjAmyZGbKBKbZOXauuJooZm3g6IuFuiNQ==