Merge branch 'livekit' into renovate/all-minor-patch

This commit is contained in:
Robin
2026-01-05 19:29:24 +01:00
39 changed files with 398 additions and 2120 deletions

View File

@@ -8,7 +8,7 @@ Please see LICENSE in the repository root for full details.
`;
module.exports = {
plugins: ["matrix-org", "rxjs"],
plugins: ["matrix-org", "rxjs", "jsdoc"],
extends: [
"plugin:matrix-org/react",
"plugin:matrix-org/a11y",
@@ -26,6 +26,13 @@ module.exports = {
node: true,
},
rules: {
"jsdoc/no-types": "error",
"jsdoc/empty-tags": "error",
"jsdoc/check-property-names": "error",
"jsdoc/check-values": "error",
"jsdoc/check-param-names": "warn",
// "jsdoc/require-param": "warn",
"jsdoc/require-param-description": "warn",
"matrix-org/require-copyright-header": ["error", COPYRIGHT_HEADER],
"jsx-a11y/media-has-caption": "off",
"react/display-name": "error",
@@ -75,6 +82,23 @@ module.exports = {
"no-console": ["error"],
},
},
{
files: [
"**/*.test.ts",
"**/*.test.tsx",
"**/test.ts",
"**/test.tsx",
"**/test-**",
],
rules: {
"jsdoc/no-types": "off",
"jsdoc/empty-tags": "off",
"jsdoc/check-property-names": "off",
"jsdoc/check-values": "off",
"jsdoc/check-param-names": "off",
"jsdoc/require-param-description": "off",
},
},
],
settings: {
react: {

View File

@@ -61,6 +61,7 @@ jobs:
docker_tags: |
type=sha,format=short,event=branch
type=raw,value=${{ github.event.release.tag_name }}
type=raw,value=latest
# Like before, using ${{ env.VERSION }} above doesn't work
add_docker_release_note:
needs: publish_docker

View File

@@ -1,5 +1,12 @@
# OpenTelemetry Collector for development
## Edit:
Open telemetry has been removed in: https://github.com/element-hq/element-call/pull/3586
Check this PR to get back the implementation or to use it as reference to add it back.
---
This directory contains a docker compose file that starts a jaeger all-in-one instance
with an in-memory database, along with a standalone OpenTelemetry collector that forwards
traces into the jaeger. Jaeger has a built-in OpenTelemetry collector, but it can't be

View File

@@ -49,13 +49,6 @@
"@livekit/protocol": "^1.42.2",
"@livekit/track-processors": "^0.6.0 || ^0.7.1",
"@mediapipe/tasks-vision": "^0.10.18",
"@opentelemetry/api": "^1.4.0",
"@opentelemetry/core": "^2.0.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.0.0",
"@opentelemetry/sdk-trace-base": "^2.0.0",
"@opentelemetry/sdk-trace-web": "^2.0.0",
"@opentelemetry/semantic-conventions": "^1.25.1",
"@playwright/test": "^1.57.0",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-slider": "^1.1.2",
@@ -94,6 +87,7 @@
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-deprecate": "^0.8.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^61.5.0",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "2.1.0",
"eslint-plugin-react": "^7.29.4",

View File

@@ -67,7 +67,6 @@ const CONFIG_JSON = {
/**
* Set the Element Call URL in the dev tool settings using `window.mxSettingsStore` via `page.evaluate`.
* @param page
*/
const setDevToolElementCallDevUrl = process.env.USE_DOCKER
? async (page: Page): Promise<void> => {

View File

@@ -1,157 +0,0 @@
/*
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 {
type SpanProcessor,
type ReadableSpan,
type Span,
} from "@opentelemetry/sdk-trace-base";
import { hrTimeToMilliseconds } from "@opentelemetry/core";
import { logger } from "matrix-js-sdk/lib/logger";
import { PosthogAnalytics } from "./PosthogAnalytics";
interface PrevCall {
callId: string;
hangupTs: number;
}
/**
* The maximum time between hanging up and joining the same call that we would
* consider a 'rejoin' on the user's part.
*/
const maxRejoinMs = 2 * 60 * 1000; // 2 minutes
/**
* Span processor that extracts certain metrics from spans to send to PostHog
*/
export class PosthogSpanProcessor implements SpanProcessor {
public async forceFlush(): Promise<void> {}
public onStart(span: Span): void {
// Hack: Yield to allow attributes to be set before processing
try {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipStart(span);
return;
case "matrix.groupCallMembership.summaryReport":
this.onSummaryReportStart(span);
return;
}
} catch (e) {
// log to avoid tripping @typescript-eslint/no-unused-vars
logger.debug(e);
}
}
public onEnd(span: ReadableSpan): void {
switch (span.name) {
case "matrix.groupCallMembership":
this.onGroupCallMembershipEnd(span);
return;
}
}
private get prevCall(): PrevCall | null {
// This is stored in localStorage so we can remember the previous call
// across app restarts
const data = localStorage.getItem("matrix-prev-call");
if (data === null) return null;
try {
return JSON.parse(data);
} catch (e) {
logger.warn("Invalid prev call data", data, "error:", e);
return null;
}
}
private set prevCall(data: PrevCall | null) {
localStorage.setItem("matrix-prev-call", JSON.stringify(data));
}
private onGroupCallMembershipStart(span: ReadableSpan): void {
const prevCall = this.prevCall;
const newCallId = span.attributes["matrix.confId"] as string;
// If the user joined the same call within a short time frame, log this as a
// rejoin. This is interesting as a call quality metric, since rejoins may
// indicate that users had to intervene to make the product work.
if (prevCall !== null && newCallId === prevCall.callId) {
const duration = hrTimeToMilliseconds(span.startTime) - prevCall.hangupTs;
if (duration <= maxRejoinMs) {
PosthogAnalytics.instance.trackEvent({
eventName: "Rejoin",
callId: prevCall.callId,
rejoinDuration: duration,
});
}
}
}
private onGroupCallMembershipEnd(span: ReadableSpan): void {
this.prevCall = {
callId: span.attributes["matrix.confId"] as string,
hangupTs: hrTimeToMilliseconds(span.endTime),
};
}
private onSummaryReportStart(span: ReadableSpan): void {
// Searching for an event like this:
// matrix.stats.summary
// matrix.stats.summary.percentageReceivedAudioMedia: 0.75
// matrix.stats.summary.percentageReceivedMedia: 1
// matrix.stats.summary.percentageReceivedVideoMedia: 0.75
// matrix.stats.summary.maxJitter: 100
// matrix.stats.summary.maxPacketLoss: 20
const event = span.events.find((e) => e.name === "matrix.stats.summary");
if (event !== undefined) {
const attributes = event.attributes;
if (attributes) {
const mediaReceived = `${attributes["matrix.stats.summary.percentageReceivedMedia"]}`;
const videoReceived = `${attributes["matrix.stats.summary.percentageReceivedVideoMedia"]}`;
const audioReceived = `${attributes["matrix.stats.summary.percentageReceivedAudioMedia"]}`;
const maxJitter = `${attributes["matrix.stats.summary.maxJitter"]}`;
const maxPacketLoss = `${attributes["matrix.stats.summary.maxPacketLoss"]}`;
const peerConnections = `${attributes["matrix.stats.summary.peerConnections"]}`;
const percentageConcealedAudio = `${attributes["matrix.stats.summary.percentageConcealedAudio"]}`;
const opponentUsersInCall = `${attributes["matrix.stats.summary.opponentUsersInCall"]}`;
const opponentDevicesInCall = `${attributes["matrix.stats.summary.opponentDevicesInCall"]}`;
const diffDevicesToPeerConnections = `${attributes["matrix.stats.summary.diffDevicesToPeerConnections"]}`;
const ratioPeerConnectionToDevices = `${attributes["matrix.stats.summary.ratioPeerConnectionToDevices"]}`;
PosthogAnalytics.instance.trackEvent(
{
eventName: "MediaReceived",
callId: span.attributes["matrix.confId"] as string,
mediaReceived: mediaReceived,
audioReceived: audioReceived,
videoReceived: videoReceived,
maxJitter: maxJitter,
maxPacketLoss: maxPacketLoss,
peerConnections: peerConnections,
percentageConcealedAudio: percentageConcealedAudio,
opponentUsersInCall: opponentUsersInCall,
opponentDevicesInCall: opponentDevicesInCall,
diffDevicesToPeerConnections: diffDevicesToPeerConnections,
ratioPeerConnectionToDevices: ratioPeerConnectionToDevices,
},
// Send instantly because the window might be closing
{ send_instantly: true },
);
}
}
}
/**
* Shutdown the processor.
*/
public async shutdown(): Promise<void> {
return Promise.resolve();
}
}

View File

@@ -1,135 +0,0 @@
/*
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 { type AttributeValue, type Attributes } from "@opentelemetry/api";
import { hrTimeToMicroseconds } from "@opentelemetry/core";
import {
type SpanProcessor,
type ReadableSpan,
type Span,
} from "@opentelemetry/sdk-trace-base";
const dumpAttributes = (
attr: Attributes,
): {
key: string;
type:
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
value: AttributeValue | undefined;
}[] =>
Object.entries(attr).map(([key, value]) => ({
key,
type: typeof value,
value,
}));
/**
* Exports spans on demand to the Jaeger JSON format, which can be attached to
* rageshakes and loaded into analysis tools like Jaeger and Stalk.
*/
export class RageshakeSpanProcessor implements SpanProcessor {
private readonly spans: ReadableSpan[] = [];
public async forceFlush(): Promise<void> {}
public onStart(span: Span): void {
this.spans.push(span);
}
public onEnd(): void {}
/**
* Dumps the spans collected so far as Jaeger-compatible JSON.
*/
public dump(): string {
const now = Date.now() * 1000; // Jaeger works in microseconds
const traces = new Map<string, ReadableSpan[]>();
// Organize spans by their trace IDs
for (const span of this.spans) {
const traceId = span.spanContext().traceId;
let trace = traces.get(traceId);
if (trace === undefined) {
trace = [];
traces.set(traceId, trace);
}
trace.push(span);
}
const processId = "p1";
const processes = {
[processId]: {
serviceName: "element-call",
tags: [],
},
warnings: null,
};
return JSON.stringify({
// Honestly not sure what some of these fields mean, I just know that
// they're present in Jaeger JSON exports
total: 0,
limit: 0,
offset: 0,
errors: null,
data: [...traces.entries()].map(([traceId, spans]) => ({
traceID: traceId,
warnings: null,
processes,
spans: spans.map((span) => {
const ctx = span.spanContext();
const startTime = hrTimeToMicroseconds(span.startTime);
// If the span has not yet ended, pretend that it ends now
const duration =
span.duration[0] === -1
? now - startTime
: hrTimeToMicroseconds(span.duration);
return {
traceID: traceId,
spanID: ctx.spanId,
operationName: span.name,
processID: processId,
warnings: null,
startTime,
duration,
references:
span.parentSpanContext?.spanId === undefined
? []
: [
{
refType: "CHILD_OF",
traceID: traceId,
spanID: span.parentSpanContext?.spanId,
},
],
tags: dumpAttributes(span.attributes),
logs: span.events.map((event) => ({
timestamp: hrTimeToMicroseconds(event.time),
// The name of the event is in the "event" field, aparently.
fields: [
...dumpAttributes(event.attributes ?? {}),
{ key: "event", type: "string", value: event.name },
],
})),
};
}),
})),
});
}
public async shutdown(): Promise<void> {}
}

View File

@@ -34,8 +34,8 @@ const getRoomSharedKeyLocalStorageKey = (roomId: string): string =>
`room-shared-key-${roomId}`;
/**
* An upto-date shared key for the room. Either from local storage or the value from `setInitialValue`.
* @param roomId
* An up-to-date shared key for the room. Either from local storage or the value from `setInitialValue`.
* @param roomId The room ID we want the shared key for.
* @param setInitialValue The value we get from the URL. The hook will overwrite the local storage value with this.
* @returns [roomSharedKey, setRoomSharedKey] like a react useState hook.
*/

View File

@@ -26,7 +26,6 @@ import {
import { getUrlParams } from "./UrlParams";
import { Config } from "./config/Config";
import { ElementCallOpenTelemetry } from "./otel/otel";
import { platform } from "./Platform";
import { isFailure } from "./utils/fetch";
@@ -101,7 +100,6 @@ enum LoadState {
class DependencyLoadStates {
public config: LoadState = LoadState.None;
public sentry: LoadState = LoadState.None;
public openTelemetry: LoadState = LoadState.None;
public allDepsAreLoaded(): boolean {
return !Object.values(this).some((s) => s !== LoadState.Loaded);
@@ -266,15 +264,6 @@ export class Initializer {
this.loadStates.sentry = LoadState.Loaded;
}
// OpenTelemetry (also only after config loaded)
if (
this.loadStates.openTelemetry === LoadState.None &&
this.loadStates.config === LoadState.Loaded
) {
ElementCallOpenTelemetry.globalInit();
this.loadStates.openTelemetry = LoadState.Loaded;
}
if (this.loadStates.allDepsAreLoaded()) {
// resolve if there is no dependency that is not loaded
resolve();

View File

@@ -166,7 +166,11 @@ interface StereoPanAudioTrackProps {
* It main purpose is to remount the AudioTrack component when switching from
* audioContext to normal audio playback.
* As of now the AudioTrack component does not support adding audio nodes while being mounted.
* @param param0
* @param props The component props
* @param props.trackRef The track reference
* @param props.muted If the track should be muted
* @param props.audioContext The audio context to use
* @param props.audioNodes The audio nodes to use
* @returns
*/
function AudioTrackWithAudioNodes({

View File

@@ -63,9 +63,9 @@ export type OpenIDClientParts = Pick<
* Gets a bearer token from the homeserver and then use it to authenticate
* to the matrix RTC backend in order to get acces to the SFU.
* It has built-in retry for calls to the homeserver with a backoff policy.
* @param client
* @param serviceUrl
* @param matrixRoomId
* @param client The Matrix client
* @param serviceUrl The URL of the livekit SFU service
* @param matrixRoomId The Matrix room ID for which to get the SFU config
* @returns Object containing the token information
* @throws FailToGetOpenIdToken
*/

View File

@@ -1,188 +0,0 @@
/*
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 { type Span } from "@opentelemetry/api";
import { type MatrixCall } from "matrix-js-sdk";
import { CallEvent } from "matrix-js-sdk/lib/webrtc/call";
import {
type TransceiverStats,
type CallFeedStats,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { ObjectFlattener } from "./ObjectFlattener";
import { ElementCallOpenTelemetry } from "./otel";
import { type OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
import { OTelCallTransceiverMediaStreamSpan } from "./OTelCallTransceiverMediaStreamSpan";
import { OTelCallFeedMediaStreamSpan } from "./OTelCallFeedMediaStreamSpan";
type StreamId = string;
type MID = string;
/**
* Tracks an individual call within a group call, either to a full-mesh peer or a focus
*/
export class OTelCall {
private readonly trackFeedSpan = new Map<
StreamId,
OTelCallAbstractMediaStreamSpan
>();
private readonly trackTransceiverSpan = new Map<
MID,
OTelCallAbstractMediaStreamSpan
>();
public constructor(
public userId: string,
public deviceId: string,
public call: MatrixCall,
public span: Span,
) {
if (call.peerConn) {
this.addCallPeerConnListeners();
} else {
this.call.once(
CallEvent.PeerConnectionCreated,
this.addCallPeerConnListeners,
);
}
}
public dispose(): void {
this.call.peerConn?.removeEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged,
);
this.call.peerConn?.removeEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged,
);
this.call.peerConn?.removeEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged,
);
this.call.peerConn?.removeEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged,
);
this.call.peerConn?.removeEventListener(
"icecandidateerror",
this.onIceCandidateError,
);
}
private addCallPeerConnListeners = (): void => {
this.call.peerConn?.addEventListener(
"connectionstatechange",
this.onCallConnectionStateChanged,
);
this.call.peerConn?.addEventListener(
"signalingstatechange",
this.onCallSignalingStateChanged,
);
this.call.peerConn?.addEventListener(
"iceconnectionstatechange",
this.onIceConnectionStateChanged,
);
this.call.peerConn?.addEventListener(
"icegatheringstatechange",
this.onIceGatheringStateChanged,
);
this.call.peerConn?.addEventListener(
"icecandidateerror",
this.onIceCandidateError,
);
};
public onCallConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.callConnectionStateChange", {
callConnectionState: this.call.peerConn?.connectionState,
});
};
public onCallSignalingStateChanged = (): void => {
this.span.addEvent("matrix.call.callSignalingStateChange", {
callSignalingState: this.call.peerConn?.signalingState,
});
};
public onIceConnectionStateChanged = (): void => {
this.span.addEvent("matrix.call.iceConnectionStateChange", {
iceConnectionState: this.call.peerConn?.iceConnectionState,
});
};
public onIceGatheringStateChanged = (): void => {
this.span.addEvent("matrix.call.iceGatheringStateChange", {
iceGatheringState: this.call.peerConn?.iceGatheringState,
});
};
public onIceCandidateError = (ev: Event): void => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(ev, flatObject, "error.", 0);
this.span.addEvent("matrix.call.iceCandidateError", flatObject);
};
public onCallFeedStats(callFeeds: CallFeedStats[]): void {
let prvFeeds: StreamId[] = [...this.trackFeedSpan.keys()];
callFeeds.forEach((feed) => {
if (!this.trackFeedSpan.has(feed.stream)) {
this.trackFeedSpan.set(
feed.stream,
new OTelCallFeedMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
feed,
),
);
}
this.trackFeedSpan.get(feed.stream)?.update(feed);
prvFeeds = prvFeeds.filter((prvStreamId) => prvStreamId !== feed.stream);
});
prvFeeds.forEach((prvStreamId) => {
this.trackFeedSpan.get(prvStreamId)?.end();
this.trackFeedSpan.delete(prvStreamId);
});
}
public onTransceiverStats(transceiverStats: TransceiverStats[]): void {
let prvTransSpan: MID[] = [...this.trackTransceiverSpan.keys()];
transceiverStats.forEach((transStats) => {
if (!this.trackTransceiverSpan.has(transStats.mid)) {
this.trackTransceiverSpan.set(
transStats.mid,
new OTelCallTransceiverMediaStreamSpan(
ElementCallOpenTelemetry.instance,
this.span,
transStats,
),
);
}
this.trackTransceiverSpan.get(transStats.mid)?.update(transStats);
prvTransSpan = prvTransSpan.filter(
(prvStreamId) => prvStreamId !== transStats.mid,
);
});
prvTransSpan.forEach((prvMID) => {
this.trackTransceiverSpan.get(prvMID)?.end();
this.trackTransceiverSpan.delete(prvMID);
});
}
public end(): void {
this.trackFeedSpan.forEach((feedSpan) => feedSpan.end());
this.trackTransceiverSpan.forEach((transceiverSpan) =>
transceiverSpan.end(),
);
this.span.end();
}
}

View File

@@ -1,69 +0,0 @@
/*
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 opentelemetry, { type Span } from "@opentelemetry/api";
import { type TrackStats } from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { type ElementCallOpenTelemetry } from "./otel";
import { OTelCallMediaStreamTrackSpan } from "./OTelCallMediaStreamTrackSpan";
type TrackId = string;
export abstract class OTelCallAbstractMediaStreamSpan {
protected readonly trackSpans = new Map<
TrackId,
OTelCallMediaStreamTrackSpan
>();
public readonly span;
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
protected readonly type: string,
) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
callSpan,
);
const options = {
links: [
{
context: callSpan.spanContext(),
},
],
};
this.span = oTel.tracer.startSpan(this.type, options, ctx);
}
protected upsertTrackSpans(tracks: TrackStats[]): void {
let prvTracks: TrackId[] = [...this.trackSpans.keys()];
tracks.forEach((t) => {
if (!this.trackSpans.has(t.id)) {
this.trackSpans.set(
t.id,
new OTelCallMediaStreamTrackSpan(this.oTel, this.span, t),
);
}
this.trackSpans.get(t.id)?.update(t);
prvTracks = prvTracks.filter((prvTrackId) => prvTrackId !== t.id);
});
prvTracks.forEach((prvTrackId) => {
this.trackSpans.get(prvTrackId)?.end();
this.trackSpans.delete(prvTrackId);
});
}
public abstract update(data: object): void;
public end(): void {
this.trackSpans.forEach((tSpan) => {
tSpan.end();
});
this.span.end();
}
}

View File

@@ -1,64 +0,0 @@
/*
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 { type Span } from "@opentelemetry/api";
import {
type CallFeedStats,
type TrackStats,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { type ElementCallOpenTelemetry } from "./otel";
import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
export class OTelCallFeedMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: { isAudioMuted: boolean; isVideoMuted: boolean };
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
callFeed: CallFeedStats,
) {
const postFix =
callFeed.type === "local" && callFeed.prefix === "from-call-feed"
? "(clone)"
: "";
super(oTel, callSpan, `matrix.call.feed.${callFeed.type}${postFix}`);
this.span.setAttribute("feed.streamId", callFeed.stream);
this.span.setAttribute("feed.type", callFeed.type);
this.span.setAttribute("feed.readFrom", callFeed.prefix);
this.span.setAttribute("feed.purpose", callFeed.purpose);
this.prev = {
isAudioMuted: callFeed.isAudioMuted,
isVideoMuted: callFeed.isVideoMuted,
};
this.span.addEvent("matrix.call.feed.initState", this.prev);
}
public update(callFeed: CallFeedStats): void {
if (this.prev.isAudioMuted !== callFeed.isAudioMuted) {
this.span.addEvent("matrix.call.feed.audioMuted", {
isAudioMuted: callFeed.isAudioMuted,
});
this.prev.isAudioMuted = callFeed.isAudioMuted;
}
if (this.prev.isVideoMuted !== callFeed.isVideoMuted) {
this.span.addEvent("matrix.call.feed.isVideoMuted", {
isVideoMuted: callFeed.isVideoMuted,
});
this.prev.isVideoMuted = callFeed.isVideoMuted;
}
const trackStats: TrackStats[] = [];
if (callFeed.video) {
trackStats.push(callFeed.video);
}
if (callFeed.audio) {
trackStats.push(callFeed.audio);
}
this.upsertTrackSpans(trackStats);
}
}

View File

@@ -1,69 +0,0 @@
/*
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 { type TrackStats } from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import opentelemetry, { type Span } from "@opentelemetry/api";
import { type ElementCallOpenTelemetry } from "./otel";
export class OTelCallMediaStreamTrackSpan {
private readonly span: Span;
private prev: TrackStats;
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly streamSpan: Span,
data: TrackStats,
) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
streamSpan,
);
const options = {
links: [
{
context: streamSpan.spanContext(),
},
],
};
const type = `matrix.call.track.${data.label}.${data.kind}`;
this.span = oTel.tracer.startSpan(type, options, ctx);
this.span.setAttribute("track.trackId", data.id);
this.span.setAttribute("track.kind", data.kind);
this.span.setAttribute("track.constrainDeviceId", data.constrainDeviceId);
this.span.setAttribute("track.settingDeviceId", data.settingDeviceId);
this.span.setAttribute("track.label", data.label);
this.span.addEvent("matrix.call.track.initState", {
readyState: data.readyState,
muted: data.muted,
enabled: data.enabled,
});
this.prev = data;
}
public update(data: TrackStats): void {
if (this.prev.muted !== data.muted) {
this.span.addEvent("matrix.call.track.muted", { muted: data.muted });
}
if (this.prev.enabled !== data.enabled) {
this.span.addEvent("matrix.call.track.enabled", {
enabled: data.enabled,
});
}
if (this.prev.readyState !== data.readyState) {
this.span.addEvent("matrix.call.track.readyState", {
readyState: data.readyState,
});
}
this.prev = data;
}
public end(): void {
this.span.end();
}
}

View File

@@ -1,61 +0,0 @@
/*
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 { type Span } from "@opentelemetry/api";
import {
type TrackStats,
type TransceiverStats,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { type ElementCallOpenTelemetry } from "./otel";
import { OTelCallAbstractMediaStreamSpan } from "./OTelCallAbstractMediaStreamSpan";
export class OTelCallTransceiverMediaStreamSpan extends OTelCallAbstractMediaStreamSpan {
private readonly prev: {
direction: string;
currentDirection: string;
};
public constructor(
protected readonly oTel: ElementCallOpenTelemetry,
protected readonly callSpan: Span,
stats: TransceiverStats,
) {
super(oTel, callSpan, `matrix.call.transceiver.${stats.mid}`);
this.span.setAttribute("transceiver.mid", stats.mid);
this.prev = {
direction: stats.direction,
currentDirection: stats.currentDirection,
};
this.span.addEvent("matrix.call.transceiver.initState", this.prev);
}
public update(stats: TransceiverStats): void {
if (this.prev.currentDirection !== stats.currentDirection) {
this.span.addEvent("matrix.call.transceiver.currentDirection", {
currentDirection: stats.currentDirection,
});
this.prev.currentDirection = stats.currentDirection;
}
if (this.prev.direction !== stats.direction) {
this.span.addEvent("matrix.call.transceiver.direction", {
direction: stats.direction,
});
this.prev.direction = stats.direction;
}
const trackStats: TrackStats[] = [];
if (stats.sender) {
trackStats.push(stats.sender);
}
if (stats.receiver) {
trackStats.push(stats.receiver);
}
this.upsertTrackSpans(trackStats);
}
}

View File

@@ -1,477 +0,0 @@
/*
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 opentelemetry, {
type Span,
type Attributes,
type Context,
} from "@opentelemetry/api";
import {
type GroupCall,
type MatrixClient,
type MatrixEvent,
type RoomMember,
} from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import {
type CallError,
type CallState,
type MatrixCall,
type VoipEvent,
} from "matrix-js-sdk/lib/webrtc/call";
import {
type CallsByUserAndDevice,
type GroupCallError,
GroupCallEvent,
type GroupCallStatsReport,
} from "matrix-js-sdk/lib/webrtc/groupCall";
import {
type ConnectionStatsReport,
type ByteSentStatsReport,
type SummaryStatsReport,
type CallFeedReport,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { ElementCallOpenTelemetry } from "./otel";
import { ObjectFlattener } from "./ObjectFlattener";
import { OTelCall } from "./OTelCall";
/**
* Represent the span of time which we intend to be joined to a group call
*/
export class OTelGroupCallMembership {
private callMembershipSpan?: Span;
private groupCallContext?: Context;
private myUserId = "unknown";
private myDeviceId: string;
private myMember?: RoomMember;
private callsByCallId = new Map<string, OTelCall>();
private statsReportSpan: {
span: Span | undefined;
stats: OTelStatsReportEvent[];
};
private readonly speakingSpans = new Map<RoomMember, Map<string, Span>>();
public constructor(
private groupCall: GroupCall,
client: MatrixClient,
) {
const clientId = client.getUserId();
if (clientId) {
this.myUserId = clientId;
const myMember = groupCall.room.getMember(clientId);
if (myMember) {
this.myMember = myMember;
}
}
this.myDeviceId = client.getDeviceId() || "unknown";
this.statsReportSpan = { span: undefined, stats: [] };
this.groupCall.on(GroupCallEvent.CallsChanged, this.onCallsChanged);
}
public dispose(): void {
this.groupCall.removeListener(
GroupCallEvent.CallsChanged,
this.onCallsChanged,
);
}
public onJoinCall(): void {
if (!ElementCallOpenTelemetry.instance) return;
if (this.callMembershipSpan !== undefined) {
logger.warn("Call membership span is already started");
return;
}
// Create the main span that tracks the time we intend to be in the call
this.callMembershipSpan =
ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.groupCallMembership",
);
this.callMembershipSpan.setAttribute(
"matrix.confId",
this.groupCall.groupCallId,
);
this.callMembershipSpan.setAttribute("matrix.userId", this.myUserId);
this.callMembershipSpan.setAttribute("matrix.deviceId", this.myDeviceId);
this.callMembershipSpan.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name",
);
this.groupCallContext = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
this.callMembershipSpan,
);
this.callMembershipSpan?.addEvent("matrix.joinCall");
}
public onLeaveCall(): void {
if (this.callMembershipSpan === undefined) {
logger.warn("Call membership span is already ended");
return;
}
this.callMembershipSpan.addEvent("matrix.leaveCall");
// and end the span to indicate we've left
this.callMembershipSpan.end();
this.callMembershipSpan = undefined;
this.groupCallContext = undefined;
}
public onUpdateRoomState(event: MatrixEvent): void {
if (
!event ||
(!event.getType().startsWith("m.call") &&
!event.getType().startsWith("org.matrix.msc3401.call"))
) {
return;
}
this.callMembershipSpan?.addEvent(
`matrix.roomStateEvent_${event.getType()}`,
ObjectFlattener.flattenVoipEvent(event.getContent()),
);
}
public onCallsChanged(calls: CallsByUserAndDevice): void {
for (const [userId, userCalls] of calls.entries()) {
for (const [deviceId, call] of userCalls.entries()) {
if (!this.callsByCallId.has(call.callId)) {
if (ElementCallOpenTelemetry.instance) {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
`matrix.call`,
undefined,
this.groupCallContext,
);
// XXX: anonymity
span.setAttribute("matrix.call.target.userId", userId);
span.setAttribute("matrix.call.target.deviceId", deviceId);
const displayName =
this.groupCall.room.getMember(userId)?.name ?? "unknown";
span.setAttribute("matrix.call.target.displayName", displayName);
this.callsByCallId.set(
call.callId,
new OTelCall(userId, deviceId, call, span),
);
}
}
}
}
for (const callTrackingInfo of this.callsByCallId.values()) {
const userCalls = calls.get(callTrackingInfo.userId);
if (
!userCalls ||
!userCalls.has(callTrackingInfo.deviceId) ||
userCalls.get(callTrackingInfo.deviceId)?.callId !==
callTrackingInfo.call.callId
) {
callTrackingInfo.end();
this.callsByCallId.delete(callTrackingInfo.call.callId);
}
}
}
public onCallStateChange(call: MatrixCall, newState: CallState): void {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got call state change for unknown call ID ${call.callId}`);
return;
}
callTrackingInfo.span.addEvent("matrix.call.stateChange", {
state: newState,
});
}
public onSendEvent(call: MatrixCall, event: VoipEvent): void {
const eventType = event.eventType as string;
if (
!eventType.startsWith("m.call") &&
!eventType.startsWith("org.matrix.call")
)
return;
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got call send event for unknown call ID ${call.callId}`);
return;
}
if (event.type === "toDevice") {
callTrackingInfo.span.addEvent(
`matrix.sendToDeviceEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event),
);
} else if (event.type === "sendEvent") {
callTrackingInfo.span.addEvent(
`matrix.sendToRoomEvent_${event.eventType}`,
ObjectFlattener.flattenVoipEvent(event),
);
}
}
public onReceivedVoipEvent(event: MatrixEvent): void {
// These come straight from CallEventHandler so don't have
// a call already associated (in principle we could receive
// events for calls we don't know about).
const callId = event.getContent().call_id;
if (!callId) {
this.callMembershipSpan?.addEvent("matrix.receive_voip_event_no_callid", {
"sender.userId": event.getSender(),
});
logger.error("Received call event with no call ID!");
return;
}
const call = this.callsByCallId.get(callId);
if (!call) {
this.callMembershipSpan?.addEvent(
"matrix.receive_voip_event_unknown_callid",
{
"sender.userId": event.getSender(),
},
);
logger.error("Received call event for unknown call ID " + callId);
return;
}
call.span.addEvent("matrix.receive_voip_event", {
"sender.userId": event.getSender(),
...ObjectFlattener.flattenVoipEvent(event.getContent()),
});
}
public onToggleMicrophoneMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleMicMuted", {
"matrix.microphone.muted": newValue,
});
}
public onSetMicrophoneMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setMicMuted", {
"matrix.microphone.muted": setMuted,
});
}
public onToggleLocalVideoMuted(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.toggleVidMuted", {
"matrix.video.muted": newValue,
});
}
public onSetLocalVideoMuted(setMuted: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.video.muted": setMuted,
});
}
public onToggleScreensharing(newValue: boolean): void {
this.callMembershipSpan?.addEvent("matrix.setVidMuted", {
"matrix.screensharing.enabled": newValue,
});
}
public onSpeaking(
member: RoomMember,
deviceId: string,
speaking: boolean,
): void {
if (speaking) {
// Ensure that there's an audio activity span for this speaker
let deviceMap = this.speakingSpans.get(member);
if (deviceMap === undefined) {
deviceMap = new Map();
this.speakingSpans.set(member, deviceMap);
}
if (!deviceMap.has(deviceId)) {
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
"matrix.audioActivity",
undefined,
this.groupCallContext,
);
span.setAttribute("matrix.userId", member.userId);
span.setAttribute("matrix.displayName", member.rawDisplayName);
deviceMap.set(deviceId, span);
}
} else {
// End the audio activity span for this speaker, if any
const deviceMap = this.speakingSpans.get(member);
deviceMap?.get(deviceId)?.end();
deviceMap?.delete(deviceId);
if (deviceMap?.size === 0) this.speakingSpans.delete(member);
}
}
public onCallError(error: CallError, call: MatrixCall): void {
const callTrackingInfo = this.callsByCallId.get(call.callId);
if (!callTrackingInfo) {
logger.error(`Got error for unknown call ID ${call.callId}`);
return;
}
callTrackingInfo.span.recordException(error);
}
public onGroupCallError(error: GroupCallError): void {
this.callMembershipSpan?.recordException(error);
}
public onUndecryptableToDevice(event: MatrixEvent): void {
this.callMembershipSpan?.addEvent("matrix.toDevice.undecryptable", {
"sender.userId": event.getSender(),
});
}
public onCallFeedStatsReport(
report: GroupCallStatsReport<CallFeedReport>,
): void {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report.report?.callId;
if (callId) {
call = this.callsByCallId.get(callId);
}
if (!call) {
this.callMembershipSpan?.addEvent(
OTelStatsReportType.CallFeedReport + "_unknown_callId",
{
"call.callId": callId,
"call.opponentMemberId": report.report?.opponentMemberId
? report.report?.opponentMemberId
: "unknown",
},
);
logger.error(
`Received ${OTelStatsReportType.CallFeedReport} with unknown call ID: ${callId}`,
);
return;
} else {
call.onCallFeedStats(report.report.callFeeds);
call.onTransceiverStats(report.report.transceiver);
}
}
public onConnectionStatsReport(
statsReport: GroupCallStatsReport<ConnectionStatsReport>,
): void {
this.buildCallStatsSpan(
OTelStatsReportType.ConnectionReport,
statsReport.report,
);
}
public onByteSentStatsReport(
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
): void {
this.buildCallStatsSpan(
OTelStatsReportType.ByteSentReport,
statsReport.report,
);
}
public buildCallStatsSpan(
type: OTelStatsReportType,
report: ByteSentStatsReport | ConnectionStatsReport,
): void {
if (!ElementCallOpenTelemetry.instance) return;
let call: OTelCall | undefined;
const callId = report?.callId;
if (callId) {
call = this.callsByCallId.get(callId);
}
if (!call) {
this.callMembershipSpan?.addEvent(type + "_unknown_callid", {
"call.callId": callId,
"call.opponentMemberId": report.opponentMemberId
? report.opponentMemberId
: "unknown",
});
logger.error(`Received ${type} with unknown call ID: ${callId}`);
return;
}
const data = ObjectFlattener.flattenReportObject(type, report);
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
call.span,
);
const options = {
links: [
{
context: call.span.spanContext(),
},
],
};
const span = ElementCallOpenTelemetry.instance.tracer.startSpan(
type,
options,
ctx,
);
span.setAttribute("matrix.callId", callId ?? "unknown");
span.setAttribute(
"matrix.opponentMemberId",
report.opponentMemberId ? report.opponentMemberId : "unknown",
);
span.addEvent("matrix.call.connection_stats_event", data);
span.end();
}
public onSummaryStatsReport(
statsReport: GroupCallStatsReport<SummaryStatsReport>,
): void {
if (!ElementCallOpenTelemetry.instance) return;
const type = OTelStatsReportType.SummaryReport;
const data = ObjectFlattener.flattenSummaryStatsReportObject(statsReport);
if (this.statsReportSpan.span === undefined && this.callMembershipSpan) {
const ctx = opentelemetry.trace.setSpan(
opentelemetry.context.active(),
this.callMembershipSpan,
);
const span = ElementCallOpenTelemetry.instance?.tracer.startSpan(
"matrix.groupCallMembership.summaryReport",
undefined,
ctx,
);
if (span === undefined) {
return;
}
span.setAttribute("matrix.confId", this.groupCall.groupCallId);
span.setAttribute("matrix.userId", this.myUserId);
span.setAttribute(
"matrix.displayName",
this.myMember ? this.myMember.name : "unknown-name",
);
span.addEvent(type, data);
span.end();
}
}
}
interface OTelStatsReportEvent {
type: OTelStatsReportType;
data: Attributes;
}
enum OTelStatsReportType {
ConnectionReport = "matrix.call.stats.connection",
ByteSentReport = "matrix.call.stats.byteSent",
SummaryReport = "matrix.stats.summary",
CallFeedReport = "matrix.stats.call_feed",
}

View File

@@ -1,266 +0,0 @@
/*
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 { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall";
import {
type AudioConcealment,
type ByteSentStatsReport,
type ConnectionStatsReport,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
import { describe, expect, it } from "vitest";
import { ObjectFlattener } from "../../src/otel/ObjectFlattener";
describe("ObjectFlattener", () => {
const noConcealment: AudioConcealment = {
concealedAudio: 0,
totalAudioDuration: 0,
};
const statsReport: GroupCallStatsReport<ConnectionStatsReport> = {
report: {
callId: "callId",
opponentMemberId: "opponentMemberId",
bandwidth: { upload: 426, download: 0 },
bitrate: {
upload: 426,
download: 0,
audio: {
upload: 124,
download: 0,
},
video: {
upload: 302,
download: 0,
},
},
packetLoss: {
total: 0,
download: 0,
upload: 0,
},
framerate: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", 0],
["LOCAL_VIDEO_TRACK_ID", 30],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", 0],
["REMOTE_VIDEO_TRACK_ID", 60],
]),
},
resolution: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["LOCAL_VIDEO_TRACK_ID", { height: 460, width: 780 }],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", { height: -1, width: -1 }],
["REMOTE_VIDEO_TRACK_ID", { height: 960, width: 1080 }],
]),
},
jitter: new Map([
["REMOTE_AUDIO_TRACK_ID", 2],
["REMOTE_VIDEO_TRACK_ID", 50],
]),
codec: {
local: new Map([
["LOCAL_AUDIO_TRACK_ID", "opus"],
["LOCAL_VIDEO_TRACK_ID", "v8"],
]),
remote: new Map([
["REMOTE_AUDIO_TRACK_ID", "opus"],
["REMOTE_VIDEO_TRACK_ID", "v9"],
]),
},
transport: [
{
ip: "ff11::5fa:abcd:999c:c5c5:50000",
type: "udp",
localIp: "2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
isFocus: true,
localCandidateType: "host",
remoteCandidateType: "host",
networkType: "ethernet",
rtt: NaN,
},
{
ip: "10.10.10.2:22222",
type: "tcp",
localIp: "10.10.10.100:33333",
isFocus: true,
localCandidateType: "srfx",
remoteCandidateType: "srfx",
networkType: "ethernet",
rtt: 0,
},
],
audioConcealment: new Map([
["REMOTE_AUDIO_TRACK_ID", noConcealment],
["REMOTE_VIDEO_TRACK_ID", noConcealment],
]),
totalAudioConcealment: noConcealment,
},
};
describe("on flattenObjectRecursive", () => {
it("should flatter an Map object", () => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report.resolution,
flatObject,
"matrix.call.stats.connection.resolution.",
0,
);
expect(flatObject).toEqual({
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
});
});
it("should flatter an Array object", () => {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report.transport,
flatObject,
"matrix.call.stats.connection.transport.",
0,
);
expect(flatObject).toEqual({
"matrix.call.stats.connection.transport.0.ip":
"ff11::5fa:abcd:999c:c5c5:50000",
"matrix.call.stats.connection.transport.0.type": "udp",
"matrix.call.stats.connection.transport.0.localIp":
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
"matrix.call.stats.connection.transport.0.isFocus": true,
"matrix.call.stats.connection.transport.0.localCandidateType": "host",
"matrix.call.stats.connection.transport.0.remoteCandidateType": "host",
"matrix.call.stats.connection.transport.0.networkType": "ethernet",
"matrix.call.stats.connection.transport.0.rtt": "NaN",
"matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222",
"matrix.call.stats.connection.transport.1.type": "tcp",
"matrix.call.stats.connection.transport.1.localIp":
"10.10.10.100:33333",
"matrix.call.stats.connection.transport.1.isFocus": true,
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
"matrix.call.stats.connection.transport.1.rtt": 0,
});
});
});
describe("on flattenReportObject Connection Stats", () => {
it("should flatten a Report to otel Attributes Object", () => {
expect(
ObjectFlattener.flattenReportObject(
"matrix.call.stats.connection",
statsReport.report,
),
).toEqual({
"matrix.call.stats.connection.callId": "callId",
"matrix.call.stats.connection.opponentMemberId": "opponentMemberId",
"matrix.call.stats.connection.bandwidth.download": 0,
"matrix.call.stats.connection.bandwidth.upload": 426,
"matrix.call.stats.connection.bitrate.audio.download": 0,
"matrix.call.stats.connection.bitrate.audio.upload": 124,
"matrix.call.stats.connection.bitrate.download": 0,
"matrix.call.stats.connection.bitrate.upload": 426,
"matrix.call.stats.connection.bitrate.video.download": 0,
"matrix.call.stats.connection.bitrate.video.upload": 302,
"matrix.call.stats.connection.codec.local.LOCAL_AUDIO_TRACK_ID": "opus",
"matrix.call.stats.connection.codec.local.LOCAL_VIDEO_TRACK_ID": "v8",
"matrix.call.stats.connection.codec.remote.REMOTE_AUDIO_TRACK_ID":
"opus",
"matrix.call.stats.connection.codec.remote.REMOTE_VIDEO_TRACK_ID": "v9",
"matrix.call.stats.connection.framerate.local.LOCAL_AUDIO_TRACK_ID": 0,
"matrix.call.stats.connection.framerate.local.LOCAL_VIDEO_TRACK_ID": 30,
"matrix.call.stats.connection.framerate.remote.REMOTE_AUDIO_TRACK_ID": 0,
"matrix.call.stats.connection.framerate.remote.REMOTE_VIDEO_TRACK_ID": 60,
"matrix.call.stats.connection.jitter.REMOTE_AUDIO_TRACK_ID": 2,
"matrix.call.stats.connection.jitter.REMOTE_VIDEO_TRACK_ID": 50,
"matrix.call.stats.connection.packetLoss.download": 0,
"matrix.call.stats.connection.packetLoss.total": 0,
"matrix.call.stats.connection.packetLoss.upload": 0,
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.height": 460,
"matrix.call.stats.connection.resolution.local.LOCAL_VIDEO_TRACK_ID.width": 780,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.height":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_AUDIO_TRACK_ID.width":
-1,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.height": 960,
"matrix.call.stats.connection.resolution.remote.REMOTE_VIDEO_TRACK_ID.width": 1080,
"matrix.call.stats.connection.transport.0.ip":
"ff11::5fa:abcd:999c:c5c5:50000",
"matrix.call.stats.connection.transport.0.type": "udp",
"matrix.call.stats.connection.transport.0.localIp":
"2aaa:9999:2aaa:999:8888:2aaa:2aaa:7777:50000",
"matrix.call.stats.connection.transport.0.isFocus": true,
"matrix.call.stats.connection.transport.0.localCandidateType": "host",
"matrix.call.stats.connection.transport.0.remoteCandidateType": "host",
"matrix.call.stats.connection.transport.0.networkType": "ethernet",
"matrix.call.stats.connection.transport.0.rtt": "NaN",
"matrix.call.stats.connection.transport.1.ip": "10.10.10.2:22222",
"matrix.call.stats.connection.transport.1.type": "tcp",
"matrix.call.stats.connection.transport.1.localIp":
"10.10.10.100:33333",
"matrix.call.stats.connection.transport.1.isFocus": true,
"matrix.call.stats.connection.transport.1.localCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.remoteCandidateType": "srfx",
"matrix.call.stats.connection.transport.1.networkType": "ethernet",
"matrix.call.stats.connection.transport.1.rtt": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.concealedAudio": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_AUDIO_TRACK_ID.totalAudioDuration": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.concealedAudio": 0,
"matrix.call.stats.connection.audioConcealment.REMOTE_VIDEO_TRACK_ID.totalAudioDuration": 0,
"matrix.call.stats.connection.totalAudioConcealment.concealedAudio": 0,
"matrix.call.stats.connection.totalAudioConcealment.totalAudioDuration": 0,
});
});
});
describe("on flattenByteSendStatsReportObject", () => {
const byteSentStatsReport = new Map<
string,
number
>() as ByteSentStatsReport;
byteSentStatsReport.callId = "callId";
byteSentStatsReport.opponentMemberId = "opponentMemberId";
byteSentStatsReport.set("4aa92608-04c6-428e-8312-93e17602a959", 132093);
byteSentStatsReport.set("a08e4237-ee30-4015-a932-b676aec894b1", 913448);
it("should flatten a Report to otel Attributes Object", () => {
expect(
ObjectFlattener.flattenReportObject(
"matrix.call.stats.bytesSend",
byteSentStatsReport,
),
).toEqual({
"matrix.call.stats.bytesSend.4aa92608-04c6-428e-8312-93e17602a959": 132093,
"matrix.call.stats.bytesSend.a08e4237-ee30-4015-a932-b676aec894b1": 913448,
});
expect(byteSentStatsReport.callId).toEqual("callId");
expect(byteSentStatsReport.opponentMemberId).toEqual("opponentMemberId");
});
});
});

View File

@@ -1,100 +0,0 @@
/*
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 { type Attributes } from "@opentelemetry/api";
import { type VoipEvent } from "matrix-js-sdk/lib/webrtc/call";
import { type GroupCallStatsReport } from "matrix-js-sdk/lib/webrtc/groupCall";
import {
type ByteSentStatsReport,
type ConnectionStatsReport,
type SummaryStatsReport,
} from "matrix-js-sdk/lib/webrtc/stats/statsReport";
export class ObjectFlattener {
public static flattenReportObject(
prefix: string,
report: ConnectionStatsReport | ByteSentStatsReport,
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(report, flatObject, `${prefix}.`, 0);
return flatObject;
}
public static flattenByteSentStatsReportObject(
statsReport: GroupCallStatsReport<ByteSentStatsReport>,
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.bytesSent.",
0,
);
return flatObject;
}
public static flattenSummaryStatsReportObject(
statsReport: GroupCallStatsReport<SummaryStatsReport>,
): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
statsReport.report,
flatObject,
"matrix.stats.summary.",
0,
);
return flatObject;
}
/* Flattens out an object into a single layer with components
* of the key separated by dots
*/
public static flattenVoipEvent(event: VoipEvent): Attributes {
const flatObject = {};
ObjectFlattener.flattenObjectRecursive(
event as unknown as Record<string, unknown>, // XXX Types
flatObject,
"matrix.event.",
0,
);
return flatObject;
}
public static flattenObjectRecursive(
obj: object,
flatObject: Attributes,
prefix: string,
depth: number,
): void {
if (depth > 10)
throw new Error(
"Depth limit exceeded: aborting VoipEvent recursion. Prefix is " +
prefix,
);
let entries;
if (obj instanceof Map) {
entries = obj.entries();
} else {
entries = Object.entries(obj);
}
for (const [k, v] of entries) {
if (["string", "number", "boolean"].includes(typeof v) || v === null) {
let value;
value = v === null ? "null" : v;
value = typeof v === "number" && Number.isNaN(v) ? "NaN" : value;
flatObject[prefix + k] = value;
} else if (typeof v === "object") {
ObjectFlattener.flattenObjectRecursive(
v,
flatObject,
prefix + k + ".",
depth + 1,
);
}
}
}
}

View File

@@ -1,79 +0,0 @@
/*
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 {
expect,
describe,
it,
vi,
beforeEach,
beforeAll,
afterAll,
} from "vitest";
import { ElementCallOpenTelemetry } from "./otel";
import { mockConfig } from "../utils/test";
describe("ElementCallOpenTelemetry", () => {
describe("embedded package", () => {
beforeAll(() => {
vi.stubEnv("VITE_PACKAGE", "embedded");
});
beforeEach(() => {
mockConfig({});
});
afterAll(() => {
vi.unstubAllEnvs();
});
it("does not create instance without config value", () => {
ElementCallOpenTelemetry.globalInit();
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false);
});
it("ignores config value and does not create instance", () => {
mockConfig({
opentelemetry: {
collector_url: "https://collector.example.com.localhost",
},
});
ElementCallOpenTelemetry.globalInit();
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false);
});
});
describe("full package", () => {
beforeAll(() => {
vi.stubEnv("VITE_PACKAGE", "full");
});
beforeEach(() => {
mockConfig({});
});
afterAll(() => {
vi.unstubAllEnvs();
});
it("does not create instance without config value", () => {
ElementCallOpenTelemetry.globalInit();
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(false);
});
it("creates instance with config value", () => {
mockConfig({
opentelemetry: {
collector_url: "https://collector.example.com.localhost",
},
});
ElementCallOpenTelemetry.globalInit();
expect(ElementCallOpenTelemetry.instance?.isOtlpEnabled).toBe(true);
});
});
});

View File

@@ -1,117 +0,0 @@
/*
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 {
SimpleSpanProcessor,
type SpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import opentelemetry, { type Tracer } from "@opentelemetry/api";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { logger } from "matrix-js-sdk/lib/logger";
import { PosthogSpanProcessor } from "../analytics/PosthogSpanProcessor";
import { Config } from "../config/Config";
import { RageshakeSpanProcessor } from "../analytics/RageshakeSpanProcessor";
import { getRageshakeSubmitUrl } from "../settings/submit-rageshake";
const SERVICE_NAME = "element-call";
let sharedInstance: ElementCallOpenTelemetry;
export class ElementCallOpenTelemetry {
private _provider: WebTracerProvider;
private _tracer: Tracer;
private otlpExporter?: OTLPTraceExporter;
public readonly rageshakeProcessor?: RageshakeSpanProcessor;
public static globalInit(): void {
// this is only supported in the full package as the is currently no support for passing in the collector URL from the widget host
const collectorUrl =
import.meta.env.VITE_PACKAGE === "full"
? Config.get().opentelemetry?.collector_url
: undefined;
// we always enable opentelemetry in general. We only enable the OTLP
// collector if a URL is defined (and in future if another setting is defined)
// Posthog reporting is enabled or disabled
// within the posthog code.
const shouldEnableOtlp = Boolean(collectorUrl);
if (!sharedInstance || sharedInstance.isOtlpEnabled !== shouldEnableOtlp) {
logger.info("(Re)starting OpenTelemetry debug reporting");
sharedInstance?.dispose();
sharedInstance = new ElementCallOpenTelemetry(
collectorUrl,
getRageshakeSubmitUrl(),
);
}
}
public static get instance(): ElementCallOpenTelemetry {
return sharedInstance;
}
private constructor(
collectorUrl: string | undefined,
rageshakeUrl: string | undefined,
) {
const spanProcessors: SpanProcessor[] = [];
if (collectorUrl) {
logger.info("Enabling OTLP collector with URL " + collectorUrl);
this.otlpExporter = new OTLPTraceExporter({
url: collectorUrl,
});
spanProcessors.push(new SimpleSpanProcessor(this.otlpExporter));
} else {
logger.info("OTLP collector disabled");
}
if (rageshakeUrl) {
this.rageshakeProcessor = new RageshakeSpanProcessor();
spanProcessors.push(this.rageshakeProcessor);
}
spanProcessors.push(new PosthogSpanProcessor());
this._provider = new WebTracerProvider({
resource: resourceFromAttributes({
// This is how we can make Jaeger show a reasonable service in the dropdown on the left.
[ATTR_SERVICE_NAME]: SERVICE_NAME,
}),
spanProcessors,
});
opentelemetry.trace.setGlobalTracerProvider(this._provider);
this._tracer = opentelemetry.trace.getTracer(
// This is not the serviceName shown in jaeger
"my-element-call-otl-tracer",
);
}
public dispose(): void {
opentelemetry.trace.disable();
this._provider?.shutdown().catch((e) => {
logger.error("Failed to shutdown OpenTelemetry", e);
});
}
public get isOtlpEnabled(): boolean {
return Boolean(this.otlpExporter);
}
public get tracer(): Tracer {
return this._tracer;
}
public get provider(): WebTracerProvider {
return this._provider;
}
}

View File

@@ -135,10 +135,10 @@ export class ReactionsReader {
}
/**
* Fetchest any hand wave reactions by the given sender on the given
* Fetches any hand wave reactions by the given sender on the given
* membership event.
* @param membershipEventId
* @param expectedSender
* @param membershipEventId - The user membership event id.
* @param expectedSender - The expected sender of the reaction.
* @returns A MatrixEvent if one was found.
*/
private getLastReactionEvent(

View File

@@ -48,7 +48,6 @@ import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts";
import { ElementWidgetActions, widget } from "../widget";
import styles from "./InCallView.module.css";
import { GridTile } from "../tile/GridTile";
import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership";
import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal";
import { useRageshakeRequestModal } from "../settings/submit-rageshake";
import { RageshakeRequestModal } from "./RageshakeRequestModal";
@@ -189,7 +188,6 @@ export interface InCallViewProps {
matrixRoom: MatrixRoom;
muteStates: MuteStates;
header: HeaderStyle;
otelGroupCallMembership?: OTelGroupCallMembership;
onShareClick: (() => void) | null;
}

View File

@@ -106,22 +106,18 @@ async function joinRoomAfterInvite(
export class CallTerminatedMessage extends Error {
/**
* Creates a new CallTerminatedMessage.
*
* @param icon The icon to display with the message
* @param messageTitle The title of the call ended screen message (translated)
* @param messageBody The message explaining the kind of termination
* (kick, ban, knock reject, etc.) (translated)
* @param reason The user-provided reason for the termination (kick/ban)
*/
public constructor(
/**
* The icon to display with the message.
*/
public readonly icon: ComponentType<SVGAttributes<SVGElement>>,
messageTitle: string,
/**
* The message explaining the kind of termination (kick, ban, knock reject,
* etc.) (translated)
*/
public readonly messageBody: string,
/**
* The user-provided reason for the termination (kick/ban)
*/
public readonly reason?: string,
) {
super(messageTitle);

View File

@@ -99,7 +99,7 @@ class ConsoleLogger extends EventEmitter {
/**
* Returns the log lines to flush to disk and empties the internal log buffer
* @return {string} \n delimited log lines
* @return \n delimited log lines
*/
public popLogs(): string {
const logsToFlush = this.logs;
@@ -109,7 +109,7 @@ class ConsoleLogger extends EventEmitter {
/**
* Returns lines currently in the log buffer without removing them
* @return {string} \n delimited log lines
* @return \n delimited log lines
*/
public peekLogs(): string {
return this.logs;
@@ -139,7 +139,7 @@ class IndexedDBLogStore {
}
/**
* @return {Promise} Resolves when the store is ready.
* @return Resolves when the store is ready.
*/
public async connect(): Promise<void> {
const req = this.indexedDB.open("logs");
@@ -219,7 +219,7 @@ class IndexedDBLogStore {
* This guarantees that we will always eventually do a flush when flush() is
* called.
*
* @return {Promise} Resolved when the logs have been flushed.
* @return Resolved when the logs have been flushed.
*/
public flush = async (): Promise<void> => {
// check if a flush() operation is ongoing
@@ -270,7 +270,7 @@ class IndexedDBLogStore {
* returned are deleted at the same time, so this can be called at startup
* to do house-keeping to keep the logs from growing too large.
*
* @return {Promise<Object[]>} Resolves to an array of objects. The array is
* @return Resolves to an array of objects. The array is
* sorted in time (oldest first) based on when the log file was created (the
* log ID). The objects have said log ID in an "id" field and "lines" which
* is a big string with all the new-line delimited logs.
@@ -421,12 +421,12 @@ class IndexedDBLogStore {
/**
* Helper method to collect results from a Cursor and promiseify it.
* @param {ObjectStore|Index} store The store to perform openCursor on.
* @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor.
* @param {Function} resultMapper A function which is repeatedly called with a
* @param store - The store to perform openCursor on.
* @param keyRange - Optional key range to apply on the cursor.
* @param resultMapper - A function which is repeatedly called with a
* Cursor.
* Return the data you want to keep.
* @return {Promise<T[]>} Resolves to an array of whatever you returned from
* @return Resolves to an array of whatever you returned from
* resultMapper.
*/
async function selectQuery<T>(
@@ -464,9 +464,7 @@ declare global {
/**
* Configure rage shaking support for sending bug reports.
* Modifies globals.
* @param {boolean} setUpPersistence When true (default), the persistence will
* be set up immediately for the logs.
* @return {Promise} Resolves when set up.
* @return Resolves when set up.
*/
export async function init(): Promise<void> {
global.mx_rage_logger = new ConsoleLogger();
@@ -503,7 +501,7 @@ export async function init(): Promise<void> {
/**
* Try to start up the rageshake storage for logs. If not possible (client unsupported)
* then this no-ops.
* @return {Promise} Resolves when complete.
* @return Resolves when complete.
*/
async function tryInitStorage(): Promise<void> {
if (global.mx_rage_initStoragePromise) {
@@ -536,7 +534,7 @@ async function tryInitStorage(): Promise<void> {
/**
* Get a recent snapshot of the logs, ready for attaching to a bug report
*
* @return {LogEntry[]} list of log data
* @return list of log data
*/
export async function getLogsForReport(): Promise<LogEntry[]> {
if (!global.mx_rage_logger) {

View File

@@ -17,7 +17,6 @@ import { type CryptoApi } from "matrix-js-sdk/lib/crypto-api";
import { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext";
import { Config } from "../config/Config";
import { ElementCallOpenTelemetry } from "../otel/otel";
import { type RageshakeRequestModal } from "../room/RageshakeRequestModal";
import { getUrlParams } from "../UrlParams";
@@ -274,14 +273,6 @@ export function useSubmitRageshake(
for (const entry of logs) {
body.append("compressed-log", await gzip(entry.lines), entry.id);
}
body.append(
"file",
await gzip(
ElementCallOpenTelemetry.instance.rageshakeProcessor!.dump(),
),
"traces.json.gz",
);
}
if (opts.rageshakeRequestId) {

View File

@@ -81,7 +81,7 @@ export interface Props {
localUser: { deviceId: string; userId: string };
}
/**
* @returns {callPickupState$, autoLeave$}
* @returns two observables:
* `callPickupState$` The current call pickup state of the call.
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
* Then we can conclude if we were the first one to join or not.

View File

@@ -138,7 +138,16 @@ interface Props {
* We want
* - a publisher
* -
* @param param0
* @param props The properties required to create the local membership.
* @param props.scope The observable scope to use.
* @param props.connectionManager The connection manager to get connections from.
* @param props.createPublisherFactory Factory to create a publisher once we have a connection.
* @param props.joinMatrixRTC Callback to join the matrix RTC session once we have a transport.
* @param props.homeserverConnected The homeserver connected state.
* @param props.localTransport$ The local transport to use for publishing.
* @param props.logger The logger to use.
* @param props.muteStates The mute states for video and audio.
* @param props.matrixRTCSession The matrix RTC session to join.
* @returns
* - publisher: The handle to create tracks and publish them to the room.
* - connected$: the current connection state. Including matrix server and livekit server connection. (only considering the livekit server we are using for our own media publication)
@@ -676,9 +685,11 @@ interface EnterRTCSessionOptions {
* - Delay events management
* - Handles retries (fails only after several attempts)
*
* @param rtcSession
* @param transport
* @param options
* @param rtcSession - The MatrixRTCSession to join.
* @param transport - The LivekitTransport to use for this session.
* @param options - Options for entering the RTC session.
* @param options.encryptMedia - Whether to encrypt media.
* @param options.matrixRTCMode - The Matrix RTC mode to use.
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
*/
// Exported for unit testing

View File

@@ -143,7 +143,7 @@ export class Publisher {
this.logger.debug("createAndSetupTracks called");
const lkRoom = this.connection.livekitRoom;
// Observe mute state changes and update LiveKit microphone/camera states accordingly
this.observeMuteStates(this.scope);
this.observeMuteStates();
// Check if audio and/or video is enabled. We only create tracks if enabled,
// because it could prompt for permission, and we don't want to do that unnecessarily.
@@ -356,10 +356,9 @@ export class Publisher {
/**
* Observe changes in the mute states and update the LiveKit room accordingly.
* @param scope
* @private
*/
private observeMuteStates(scope: ObservableScope): void {
private observeMuteStates(): void {
const lkRoom = this.connection.livekitRoom;
this.muteStates.audio.setHandler(async (enable) => {
try {

View File

@@ -218,7 +218,7 @@ export class Connection {
*
* @param opts - Connection options {@link ConnectionOpts}.
*
* @param logger
* @param logger - The logger to use.
*/
public constructor(opts: ConnectionOpts, logger: Logger) {
this.logger = logger.getChild("[Connection]");

View File

@@ -43,11 +43,11 @@ export class ECConnectionFactory implements ConnectionFactory {
* @param client - The OpenID client parts for authentication, needed to get openID and JWT tokens.
* @param devices - Used for video/audio out/in capture options.
* @param processorState$ - Effects like background blur (only for publishing connection?)
* @param livekitKeyProvider
* @param livekitKeyProvider - Optional key provider for end-to-end encryption.
* @param controlledAudioDevices - Option to indicate whether audio output device is controlled externally (native mobile app).
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
* @param echoCancellation - Whether to enable echo cancellation for audio capture.
* @param noiseSuppression - Whether to enable noise suppression for audio capture.
* @param livekitRoomFactory - Optional factory function (for testing) to create LivekitRoom instances. If not provided, a default factory is used.
*/
public constructor(
private client: OpenIDClientParts,

View File

@@ -76,9 +76,11 @@ export interface IConnectionManager {
/**
* Crete a `ConnectionManager`
* @param scope the observable scope used by this object.
* @param connectionFactory used to create new connections.
* @param _transportsSubscriptions$ A list of Behaviors each containing a LIST of LivekitTransport.
* @param props - Configuration object
* @param props.scope - The observable scope used by this object
* @param props.connectionFactory - Used to create new connections
* @param props.inputTransports$ - A list of Behaviors each containing a LIST of LivekitTransport.
* @param props.logger - The logger to use
* Each of these behaviors can be interpreted as subscribed list of transports.
*
* Using `registerTransports` independent external modules can control what connections

View File

@@ -22,9 +22,12 @@ import * as controls from "./controls";
* Play a sound though a given AudioContext. Will take
* care of connecting the correct buffer and gating
* through gain.
* @param volume The volume to play at.
* @param ctx The context to play through.
* @param buffer The buffer to play.
* @param volume The volume to play at.
* @param stereoPan The stereo pan to apply.
* @param delayS Delay in seconds before starting playing.
* @param abort Optional AbortController that can be used to stop playback.
* @returns A promise that resolves when the sound has finished playing.
*/
async function playSound(
@@ -55,9 +58,11 @@ async function playSound(
* Play a sound though a given AudioContext, looping until stopped. Will take
* care of connecting the correct buffer and gating
* through gain.
* @param volume The volume to play at.
* @param ctx The context to play through.
* @param buffer The buffer to play.
* @param volume The volume to play at.
* @param stereoPan The stereo pan to apply.
* @param delayS Delay in seconds between each loop.
* @returns A function used to end the sound. This function will return a promise when the sound has stopped.
*/
function playSoundLooping(
@@ -120,7 +125,7 @@ interface UseAudioContext<S extends string> {
/**
* Add an audio context which can be used to play
* a set of preloaded sounds.
* @param props
* @param props The properties for the audio context.
* @returns Either an instance that can be used to play sounds, or null if not ready.
*/
export function useAudioContext<S extends string>(

View File

@@ -77,6 +77,13 @@ export function shouldDisambiguate(
);
}
/**
* Calculates a display name for a member, optionally disambiguating it.
* @param member - The member to calculate the display name for.
* @param member.rawDisplayName - The raw display name of the member
* @param member.userId - The user ID of the member
* @param disambiguate - Whether to disambiguate the display name.
*/
export function calculateDisplayName(
member: { rawDisplayName?: string; userId: string },
disambiguate: boolean,

View File

@@ -57,9 +57,16 @@ export class ElementCallError extends Error {
}
}
/**
* Configuration problem due to no MatrixRTC backend/SFU is exposed via .well-known and no fallback configured.
*/
export class MatrixRTCTransportMissingError extends ElementCallError {
public domain: string;
/**
* Creates an instance of MatrixRTCTransportMissingError.
* @param domain - The domain where the MatrixRTC transport is missing.
*/
public constructor(domain: string) {
super(
t("error.call_is_not_supported"),
@@ -75,6 +82,9 @@ export class MatrixRTCTransportMissingError extends ElementCallError {
}
}
/**
* Error indicating that the connection to the call was lost and could not be re-established.
*/
export class ConnectionLostError extends ElementCallError {
public constructor() {
super(
@@ -86,7 +96,16 @@ export class ConnectionLostError extends ElementCallError {
}
}
/**
* Error indicating a failure in the membership manager causing the join call
* operation to fail.
*/
export class MembershipManagerError extends ElementCallError {
/**
* Creates an instance of MembershipManagerError.
*
* @param error - The underlying error that caused the membership manager failure.
*/
public constructor(error: Error) {
super(
t("error.membership_manager"),
@@ -98,6 +117,9 @@ export class MembershipManagerError extends ElementCallError {
}
}
/**
* Error indicating that end-to-end encryption is not supported in the current environment.
*/
export class E2EENotSupportedError extends ElementCallError {
public constructor() {
super(
@@ -109,7 +131,14 @@ export class E2EENotSupportedError extends ElementCallError {
}
}
/**
* Error indicating an unknown issue occurred during a call operation.
*/
export class UnknownCallError extends ElementCallError {
/**
* Creates an instance of UnknownCallError.
* @param error - The underlying error that caused the unknown issue.
*/
public constructor(error: Error) {
super(
t("error.generic"),
@@ -122,7 +151,14 @@ export class UnknownCallError extends ElementCallError {
}
}
/**
* Error indicating a failure to obtain an OpenID token.
*/
export class FailToGetOpenIdToken extends ElementCallError {
/**
* Creates an instance of FailToGetOpenIdToken.
* @param error - The underlying error that caused the failure.
*/
public constructor(error: Error) {
super(
t("error.generic"),
@@ -135,7 +171,14 @@ export class FailToGetOpenIdToken extends ElementCallError {
}
}
/**
* Error indicating a failure to start publishing on a LiveKit connection.
*/
export class FailToStartLivekitConnection extends ElementCallError {
/**
* Creates an instance of FailToStartLivekitConnection.
* @param e - An optional error message providing additional context.
*/
public constructor(e?: string) {
super(
t("error.failed_to_start_livekit"),
@@ -146,6 +189,9 @@ export class FailToStartLivekitConnection extends ElementCallError {
}
}
/**
* Error indicating that a LiveKit's server has hit its track limits.
*/
export class InsufficientCapacityError extends ElementCallError {
public constructor() {
super(
@@ -157,6 +203,10 @@ export class InsufficientCapacityError extends ElementCallError {
}
}
/**
* Error indicating that room creation is restricted by the SFU.
* Only authorized users can create rooms, so the room must exist before connecting (done by the auth jwt service)
*/
export class SFURoomCreationRestrictedError extends ElementCallError {
public constructor() {
super(

View File

@@ -188,7 +188,6 @@ function fullAliasFromRoomName(roomName: string, client: MatrixClient): string {
* Applies some basic sanitisation to a room name that the user
* has given us
* @param input The room name from the user
* @param client A matrix client object
*/
export function sanitiseRoomNameInput(input: string): string {
// check to see if the user has entered a fully qualified room
@@ -304,8 +303,9 @@ export async function createRoom(
/**
* Returns an absolute URL to that will load Element Call with the given room
* @param roomId ID of the room
* @param roomName Name of the room
* @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
* @param roomName Name of the room
* @param viaServers Optional list of servers to include as 'via' parameters in the URL
*/
export function getAbsoluteRoomUrl(
roomId: string,
@@ -321,8 +321,9 @@ export function getAbsoluteRoomUrl(
/**
* Returns a relative URL to that will load Element Call with the given room
* @param roomId ID of the room
* @param roomName Name of the room
* @param encryptionSystem what encryption (or EncryptionSystem.Unencrypted) the room uses
* @param roomName Name of the room
* @param viaServers Optional list of servers to include as 'via' parameters in the URL
*/
export function getRelativeRoomUrl(
roomId: string,

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
/**
* Finds a media device with label matching 'deviceName'
* @param deviceName The label of the device to look for
* @param kind The kind of media device to look for
* @param devices The list of devices to search
* @returns A matching media device or undefined if no matching device was found
*/

View File

@@ -135,7 +135,6 @@ interface ItemHandle<Data, Item> {
* requested at a later time, and destroyed (have their scope ended) when the
* key is no longer requested.
*
* @param input$ The input value to be mapped.
* @param generator A generator function yielding a tuple of keys and the
* currently associated data for each item that it wants to exist.
* @param factory A function constructing an individual item, given the item's key,

490
yarn.lock
View File

@@ -2430,6 +2430,26 @@ __metadata:
languageName: node
linkType: hard
"@es-joy/jsdoccomment@npm:~0.76.0":
version: 0.76.0
resolution: "@es-joy/jsdoccomment@npm:0.76.0"
dependencies:
"@types/estree": "npm:^1.0.8"
"@typescript-eslint/types": "npm:^8.46.0"
comment-parser: "npm:1.4.1"
esquery: "npm:^1.6.0"
jsdoc-type-pratt-parser: "npm:~6.10.0"
checksum: 10c0/8fe4edec7d60562787ea8c77193ebe8737a9e28ec3143d383506b63890d0ffd45a2813e913ad1f00f227cb10e3a1fb913e5a696b33d499dc564272ff1a6f3fdb
languageName: node
linkType: hard
"@es-joy/resolve.exports@npm:1.2.0":
version: 1.2.0
resolution: "@es-joy/resolve.exports@npm:1.2.0"
checksum: 10c0/7e4713471f5eccb17a925a12415a2d9e372a42376813a19f6abd9c35e8d01ab1403777265817da67c6150cffd4f558d9ad51e26a8de6911dad89d9cb7eedacd8
languageName: node
linkType: hard
"@esbuild/aix-ppc64@npm:0.25.1":
version: 0.25.1
resolution: "@esbuild/aix-ppc64@npm:0.25.1"
@@ -2874,6 +2894,15 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.7.3":
version: 1.7.3
resolution: "@floating-ui/core@npm:1.7.3"
dependencies:
"@floating-ui/utils": "npm:^0.2.10"
checksum: 10c0/edfc23800122d81df0df0fb780b7328ae6c5f00efbb55bd48ea340f4af8c5b3b121ceb4bb81220966ab0f87b443204d37105abdd93d94846468be3243984144c
languageName: node
linkType: hard
"@floating-ui/dom@npm:1.6.13, @floating-ui/dom@npm:^1.0.0":
version: 1.6.13
resolution: "@floating-ui/dom@npm:1.6.13"
@@ -2884,6 +2913,16 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/dom@npm:1.7.4":
version: 1.7.4
resolution: "@floating-ui/dom@npm:1.7.4"
dependencies:
"@floating-ui/core": "npm:^1.7.3"
"@floating-ui/utils": "npm:^0.2.10"
checksum: 10c0/da6166c25f9b0729caa9f498685a73a0e28251613b35d27db8de8014bc9d045158a23c092b405321a3d67c2064909b6e2a7e6c1c9cc0f62967dca5779f5aef30
languageName: node
linkType: hard
"@floating-ui/react-dom@npm:^2.0.0, @floating-ui/react-dom@npm:^2.1.2":
version: 2.1.2
resolution: "@floating-ui/react-dom@npm:2.1.2"
@@ -2910,6 +2949,13 @@ __metadata:
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.2.10":
version: 0.2.10
resolution: "@floating-ui/utils@npm:0.2.10"
checksum: 10c0/e9bc2a1730ede1ee25843937e911ab6e846a733a4488623cd353f94721b05ec2c9ec6437613a2ac9379a94c2fd40c797a2ba6fa1df2716f5ce4aa6ddb1cf9ea4
languageName: node
linkType: hard
"@floating-ui/utils@npm:^0.2.9":
version: 0.2.9
resolution: "@floating-ui/utils@npm:0.2.9"
@@ -3167,7 +3213,7 @@ __metadata:
languageName: node
linkType: hard
"@livekit/components-core@npm:0.12.11, @livekit/components-core@npm:^0.12.0":
"@livekit/components-core@npm:0.12.11":
version: 0.12.11
resolution: "@livekit/components-core@npm:0.12.11"
dependencies:
@@ -3181,6 +3227,20 @@ __metadata:
languageName: node
linkType: hard
"@livekit/components-core@npm:^0.12.0":
version: 0.12.12
resolution: "@livekit/components-core@npm:0.12.12"
dependencies:
"@floating-ui/dom": "npm:1.7.4"
loglevel: "npm:1.9.1"
rxjs: "npm:7.8.2"
peerDependencies:
livekit-client: ^2.15.14
tslib: ^2.6.2
checksum: 10c0/788ae01fa6c58a0edbd629f4195f2f3a7bc94660d2fb729af8b27cab2b151abe36cd0a666989811c6187e51d32c847119853010a82be55844750ab3978079c38
languageName: node
linkType: hard
"@livekit/components-react@npm:^2.0.0":
version: 2.9.16
resolution: "@livekit/components-react@npm:2.9.16"
@@ -3434,146 +3494,6 @@ __metadata:
languageName: node
linkType: hard
"@opentelemetry/api-logs@npm:0.208.0":
version: 0.208.0
resolution: "@opentelemetry/api-logs@npm:0.208.0"
dependencies:
"@opentelemetry/api": "npm:^1.3.0"
checksum: 10c0/dc1fbee6219df4166509f43b74ea936bb18b6d594565b0bcf56b654a1c958b50d6046b8739dc36c98149fe890c02150ff3814e963f5ea439a07ff3c562555b99
languageName: node
linkType: hard
"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.4.0":
version: 1.9.0
resolution: "@opentelemetry/api@npm:1.9.0"
checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add
languageName: node
linkType: hard
"@opentelemetry/core@npm:2.2.0, @opentelemetry/core@npm:^2.0.0":
version: 2.2.0
resolution: "@opentelemetry/core@npm:2.2.0"
dependencies:
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.10.0"
checksum: 10c0/f618b63f2f560d052791d2406b1411722aa4b0585031242e6906f869f0a707ffe725c4b29bf18aed1f202e1ab5dfc3a9f769c517ac8521338b33ac8c4265fba9
languageName: node
linkType: hard
"@opentelemetry/exporter-trace-otlp-http@npm:^0.208.0":
version: 0.208.0
resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.208.0"
dependencies:
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/otlp-exporter-base": "npm:0.208.0"
"@opentelemetry/otlp-transformer": "npm:0.208.0"
"@opentelemetry/resources": "npm:2.2.0"
"@opentelemetry/sdk-trace-base": "npm:2.2.0"
peerDependencies:
"@opentelemetry/api": ^1.3.0
checksum: 10c0/5e901388febb6e797aa7ed1705373df322df4ba47eaf545a85bb4e1e3b3056993a455d9d6a68e94dc57e6c8112d580129b374bf2982595244edf664663b93e66
languageName: node
linkType: hard
"@opentelemetry/otlp-exporter-base@npm:0.208.0":
version: 0.208.0
resolution: "@opentelemetry/otlp-exporter-base@npm:0.208.0"
dependencies:
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/otlp-transformer": "npm:0.208.0"
peerDependencies:
"@opentelemetry/api": ^1.3.0
checksum: 10c0/c2b2014da16e2a2be0ebe525b1a62b3e64e286fc9c2575444e4c75bbe0060a83762172180dc7a97cdaaaa8c6765076073edea30340459fc1820cd43468ff98b0
languageName: node
linkType: hard
"@opentelemetry/otlp-transformer@npm:0.208.0":
version: 0.208.0
resolution: "@opentelemetry/otlp-transformer@npm:0.208.0"
dependencies:
"@opentelemetry/api-logs": "npm:0.208.0"
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/resources": "npm:2.2.0"
"@opentelemetry/sdk-logs": "npm:0.208.0"
"@opentelemetry/sdk-metrics": "npm:2.2.0"
"@opentelemetry/sdk-trace-base": "npm:2.2.0"
protobufjs: "npm:^7.3.0"
peerDependencies:
"@opentelemetry/api": ^1.3.0
checksum: 10c0/70c04b2a52f0b2f8aece25ad21401c32ed3136ccd6e82b767d570a24d5456a5ded206ed4cc60ebc09eac08a4aa9c03bc8dcbf10730e491f1af3e7768c361ac12
languageName: node
linkType: hard
"@opentelemetry/resources@npm:2.2.0, @opentelemetry/resources@npm:^2.0.0":
version: 2.2.0
resolution: "@opentelemetry/resources@npm:2.2.0"
dependencies:
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
peerDependencies:
"@opentelemetry/api": ">=1.3.0 <1.10.0"
checksum: 10c0/f08fa69ccccb6d14b6932fabe6f8e097c0dfc41ae8f4c0f6c54fb04bc3d9c04e742da3e22d7240d74b585287101126d97a0da192b493a9724dc07a56ca1b77e0
languageName: node
linkType: hard
"@opentelemetry/sdk-logs@npm:0.208.0":
version: 0.208.0
resolution: "@opentelemetry/sdk-logs@npm:0.208.0"
dependencies:
"@opentelemetry/api-logs": "npm:0.208.0"
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/resources": "npm:2.2.0"
peerDependencies:
"@opentelemetry/api": ">=1.4.0 <1.10.0"
checksum: 10c0/a167ee7d2818e435ff7480836461f94543e4e39f0e8e8013d462c635def9b960dcf1a29e5536743946b51ef13b764f518d9edb511e89bc1e8995acc96f54241f
languageName: node
linkType: hard
"@opentelemetry/sdk-metrics@npm:2.2.0":
version: 2.2.0
resolution: "@opentelemetry/sdk-metrics@npm:2.2.0"
dependencies:
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/resources": "npm:2.2.0"
peerDependencies:
"@opentelemetry/api": ">=1.9.0 <1.10.0"
checksum: 10c0/a2668f9ef937123552a5ab96ec23675931ae7d3223ec7a31c8aac95fbbfb0b03a54a873f17f2356b04db7031421e7e3d7e3bf9d96d9069a0b97c680a2c158bc4
languageName: node
linkType: hard
"@opentelemetry/sdk-trace-base@npm:2.2.0, @opentelemetry/sdk-trace-base@npm:^2.0.0":
version: 2.2.0
resolution: "@opentelemetry/sdk-trace-base@npm:2.2.0"
dependencies:
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/resources": "npm:2.2.0"
"@opentelemetry/semantic-conventions": "npm:^1.29.0"
peerDependencies:
"@opentelemetry/api": ">=1.3.0 <1.10.0"
checksum: 10c0/a67715b71d7253cd61ea79954f56491796ac7a660d03d5381fd81defd4546042bb465b27e1b6eee4b1ed32c00305a5349a16d04fd44314c9a1d371a0a638107a
languageName: node
linkType: hard
"@opentelemetry/sdk-trace-web@npm:^2.0.0":
version: 2.2.0
resolution: "@opentelemetry/sdk-trace-web@npm:2.2.0"
dependencies:
"@opentelemetry/core": "npm:2.2.0"
"@opentelemetry/sdk-trace-base": "npm:2.2.0"
peerDependencies:
"@opentelemetry/api": ">=1.0.0 <1.10.0"
checksum: 10c0/002296bb929d4992575415ea93c2a68411cd5018c6e7fa27b7cd48e78741b7eddfb87eb0903e7ca42740acb04ac4e8508d00f7651bdc63b433055af955201d31
languageName: node
linkType: hard
"@opentelemetry/semantic-conventions@npm:^1.25.1, @opentelemetry/semantic-conventions@npm:^1.29.0":
version: 1.38.0
resolution: "@opentelemetry/semantic-conventions@npm:1.38.0"
checksum: 10c0/ae93e39ac18bf47df2b11d43e9a0dc1673b9d33e5f1e7f357c92968e6329fb9a67cf8a447e9a7150948ee3f8178b38274db365b8fa775a8c54802e0c6ccdd2ca
languageName: node
linkType: hard
"@oxc-resolver/binding-android-arm-eabi@npm:11.16.2":
version: 11.16.2
resolution: "@oxc-resolver/binding-android-arm-eabi@npm:11.16.2"
@@ -3878,79 +3798,6 @@ __metadata:
languageName: node
linkType: hard
"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2":
version: 1.1.2
resolution: "@protobufjs/aspromise@npm:1.1.2"
checksum: 10c0/a83343a468ff5b5ec6bff36fd788a64c839e48a07ff9f4f813564f58caf44d011cd6504ed2147bf34835bd7a7dd2107052af755961c6b098fd8902b4f6500d0f
languageName: node
linkType: hard
"@protobufjs/base64@npm:^1.1.2":
version: 1.1.2
resolution: "@protobufjs/base64@npm:1.1.2"
checksum: 10c0/eec925e681081af190b8ee231f9bad3101e189abbc182ff279da6b531e7dbd2a56f1f306f37a80b1be9e00aa2d271690d08dcc5f326f71c9eed8546675c8caf6
languageName: node
linkType: hard
"@protobufjs/codegen@npm:^2.0.4":
version: 2.0.4
resolution: "@protobufjs/codegen@npm:2.0.4"
checksum: 10c0/26ae337c5659e41f091606d16465bbcc1df1f37cc1ed462438b1f67be0c1e28dfb2ca9f294f39100c52161aef82edf758c95d6d75650a1ddf31f7ddee1440b43
languageName: node
linkType: hard
"@protobufjs/eventemitter@npm:^1.1.0":
version: 1.1.0
resolution: "@protobufjs/eventemitter@npm:1.1.0"
checksum: 10c0/1eb0a75180e5206d1033e4138212a8c7089a3d418c6dfa5a6ce42e593a4ae2e5892c4ef7421f38092badba4040ea6a45f0928869989411001d8c1018ea9a6e70
languageName: node
linkType: hard
"@protobufjs/fetch@npm:^1.1.0":
version: 1.1.0
resolution: "@protobufjs/fetch@npm:1.1.0"
dependencies:
"@protobufjs/aspromise": "npm:^1.1.1"
"@protobufjs/inquire": "npm:^1.1.0"
checksum: 10c0/cda6a3dc2d50a182c5865b160f72077aac197046600091dbb005dd0a66db9cce3c5eaed6d470ac8ed49d7bcbeef6ee5f0bc288db5ff9a70cbd003e5909065233
languageName: node
linkType: hard
"@protobufjs/float@npm:^1.0.2":
version: 1.0.2
resolution: "@protobufjs/float@npm:1.0.2"
checksum: 10c0/18f2bdede76ffcf0170708af15c9c9db6259b771e6b84c51b06df34a9c339dbbeec267d14ce0bddd20acc142b1d980d983d31434398df7f98eb0c94a0eb79069
languageName: node
linkType: hard
"@protobufjs/inquire@npm:^1.1.0":
version: 1.1.0
resolution: "@protobufjs/inquire@npm:1.1.0"
checksum: 10c0/64372482efcba1fb4d166a2664a6395fa978b557803857c9c03500e0ac1013eb4b1aacc9ed851dd5fc22f81583670b4f4431bae186f3373fedcfde863ef5921a
languageName: node
linkType: hard
"@protobufjs/path@npm:^1.1.2":
version: 1.1.2
resolution: "@protobufjs/path@npm:1.1.2"
checksum: 10c0/cece0a938e7f5dfd2fa03f8c14f2f1cf8b0d6e13ac7326ff4c96ea311effd5fb7ae0bba754fbf505312af2e38500250c90e68506b97c02360a43793d88a0d8b4
languageName: node
linkType: hard
"@protobufjs/pool@npm:^1.1.0":
version: 1.1.0
resolution: "@protobufjs/pool@npm:1.1.0"
checksum: 10c0/eda2718b7f222ac6e6ad36f758a92ef90d26526026a19f4f17f668f45e0306a5bd734def3f48f51f8134ae0978b6262a5c517c08b115a551756d1a3aadfcf038
languageName: node
linkType: hard
"@protobufjs/utf8@npm:^1.1.0":
version: 1.1.0
resolution: "@protobufjs/utf8@npm:1.1.0"
checksum: 10c0/a3fe31fe3fa29aa3349e2e04ee13dc170cc6af7c23d92ad49e3eeaf79b9766264544d3da824dba93b7855bd6a2982fb40032ef40693da98a136d835752beb487
languageName: node
linkType: hard
"@radix-ui/number@npm:1.1.1":
version: 1.1.1
resolution: "@radix-ui/number@npm:1.1.1"
@@ -5403,6 +5250,13 @@ __metadata:
languageName: node
linkType: hard
"@sindresorhus/base62@npm:^1.0.0":
version: 1.0.0
resolution: "@sindresorhus/base62@npm:1.0.0"
checksum: 10c0/9a14df0f058fdf4731c30f0f05728a4822144ee42236030039d7fa5a1a1072c2879feba8091fd4a17c8922d1056bc07bada77c31fddc3e15836fc05a266fd918
languageName: node
linkType: hard
"@stylistic/eslint-plugin@npm:^3.0.0":
version: 3.1.0
resolution: "@stylistic/eslint-plugin@npm:3.1.0"
@@ -5684,7 +5538,7 @@ __metadata:
languageName: node
linkType: hard
"@types/estree@npm:1.0.8":
"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.8":
version: 1.0.8
resolution: "@types/estree@npm:1.0.8"
checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
@@ -5767,7 +5621,7 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=13.7.0":
"@types/node@npm:*":
version: 25.0.3
resolution: "@types/node@npm:25.0.3"
dependencies:
@@ -6006,7 +5860,7 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.51.0":
"@typescript-eslint/types@npm:8.51.0, @typescript-eslint/types@npm:^8.46.0, @typescript-eslint/types@npm:^8.51.0":
version: 8.51.0
resolution: "@typescript-eslint/types@npm:8.51.0"
checksum: 10c0/eb3473d0bb71eb886438f35887b620ffadae7853b281752a40c73158aee644d136adeb82549be7d7c30f346fe888b2e979dff7e30e67b35377e8281018034529
@@ -6372,6 +6226,15 @@ __metadata:
languageName: node
linkType: hard
"acorn@npm:^8.15.0":
version: 8.15.0
resolution: "acorn@npm:8.15.0"
bin:
acorn: bin/acorn
checksum: 10c0/dec73ff59b7d6628a01eebaece7f2bdb8bb62b9b5926dcad0f8931f2b8b79c2be21f6c68ac095592adb5adb15831a3635d9343e6a91d028bbe85d564875ec3ec
languageName: node
linkType: hard
"acorn@npm:^8.9.0":
version: 8.12.1
resolution: "acorn@npm:8.12.1"
@@ -6481,6 +6344,13 @@ __metadata:
languageName: node
linkType: hard
"are-docs-informative@npm:^0.0.2":
version: 0.0.2
resolution: "are-docs-informative@npm:0.0.2"
checksum: 10c0/f0326981bd699c372d268b526b170a28f2e1aec2cf99d7de0686083528427ecdf6ae41fef5d9988e224a5616298af747ad8a76e7306b0a7c97cc085a99636d60
languageName: node
linkType: hard
"argparse@npm:^2.0.1":
version: 2.0.1
resolution: "argparse@npm:2.0.1"
@@ -7561,6 +7431,13 @@ __metadata:
languageName: node
linkType: hard
"comment-parser@npm:1.4.1":
version: 1.4.1
resolution: "comment-parser@npm:1.4.1"
checksum: 10c0/d6c4be3f5be058f98b24f2d557f745d8fe1cc9eb75bebbdccabd404a0e1ed41563171b16285f593011f8b6a5ec81f564fb1f2121418ac5cbf0f49255bf0840dd
languageName: node
linkType: hard
"common-tags@npm:^1.8.0":
version: 1.8.2
resolution: "common-tags@npm:1.8.2"
@@ -7971,6 +7848,18 @@ __metadata:
languageName: node
linkType: hard
"debug@npm:^4.4.3":
version: 4.4.3
resolution: "debug@npm:4.4.3"
dependencies:
ms: "npm:^2.1.3"
peerDependenciesMeta:
supports-color:
optional: true
checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6
languageName: node
linkType: hard
"decamelize@npm:^1.2.0":
version: 1.2.0
resolution: "decamelize@npm:1.2.0"
@@ -8302,13 +8191,6 @@ __metadata:
"@livekit/protocol": "npm:^1.42.2"
"@livekit/track-processors": "npm:^0.6.0 || ^0.7.1"
"@mediapipe/tasks-vision": "npm:^0.10.18"
"@opentelemetry/api": "npm:^1.4.0"
"@opentelemetry/core": "npm:^2.0.0"
"@opentelemetry/exporter-trace-otlp-http": "npm:^0.208.0"
"@opentelemetry/resources": "npm:^2.0.0"
"@opentelemetry/sdk-trace-base": "npm:^2.0.0"
"@opentelemetry/sdk-trace-web": "npm:^2.0.0"
"@opentelemetry/semantic-conventions": "npm:^1.25.1"
"@playwright/test": "npm:^1.57.0"
"@radix-ui/react-dialog": "npm:^1.0.4"
"@radix-ui/react-slider": "npm:^1.1.2"
@@ -8347,6 +8229,7 @@ __metadata:
eslint-config-prettier: "npm:^10.0.0"
eslint-plugin-deprecate: "npm:^0.8.2"
eslint-plugin-import: "npm:^2.26.0"
eslint-plugin-jsdoc: "npm:^61.5.0"
eslint-plugin-jsx-a11y: "npm:^6.5.1"
eslint-plugin-matrix-org: "npm:2.1.0"
eslint-plugin-react: "npm:^7.29.4"
@@ -9010,6 +8893,30 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-jsdoc@npm:^61.5.0":
version: 61.5.0
resolution: "eslint-plugin-jsdoc@npm:61.5.0"
dependencies:
"@es-joy/jsdoccomment": "npm:~0.76.0"
"@es-joy/resolve.exports": "npm:1.2.0"
are-docs-informative: "npm:^0.0.2"
comment-parser: "npm:1.4.1"
debug: "npm:^4.4.3"
escape-string-regexp: "npm:^4.0.0"
espree: "npm:^10.4.0"
esquery: "npm:^1.6.0"
html-entities: "npm:^2.6.0"
object-deep-merge: "npm:^2.0.0"
parse-imports-exports: "npm:^0.2.4"
semver: "npm:^7.7.3"
spdx-expression-parse: "npm:^4.0.0"
to-valid-identifier: "npm:^1.0.0"
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
checksum: 10c0/fabb04f6efe58a167a0839d3c05676a76080c6e91d98a269fa768c1bfd835aa0ded5822d400da2874216177044d2d227ebe241d73e923f3fe1c08bafd19cfd3d
languageName: node
linkType: hard
"eslint-plugin-jsx-a11y@npm:^6.5.1":
version: 6.10.2
resolution: "eslint-plugin-jsx-a11y@npm:6.10.2"
@@ -9244,6 +9151,17 @@ __metadata:
languageName: node
linkType: hard
"espree@npm:^10.4.0":
version: 10.4.0
resolution: "espree@npm:10.4.0"
dependencies:
acorn: "npm:^8.15.0"
acorn-jsx: "npm:^5.3.2"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10c0/c63fe06131c26c8157b4083313cb02a9a54720a08e21543300e55288c40e06c3fc284bdecf108d3a1372c5934a0a88644c98714f38b6ae8ed272b40d9ea08d6b
languageName: node
linkType: hard
"espree@npm:^9.6.0, espree@npm:^9.6.1":
version: 9.6.1
resolution: "espree@npm:9.6.1"
@@ -10137,6 +10055,13 @@ __metadata:
languageName: node
linkType: hard
"html-entities@npm:^2.6.0":
version: 2.6.0
resolution: "html-entities@npm:2.6.0"
checksum: 10c0/7c8b15d9ea0cd00dc9279f61bab002ba6ca8a7a0f3c36ed2db3530a67a9621c017830d1d2c1c65beb9b8e3436ea663e9cf8b230472e0e413359399413b27c8b7
languageName: node
linkType: hard
"html-escaper@npm:^2.0.0":
version: 2.0.2
resolution: "html-escaper@npm:2.0.2"
@@ -10931,6 +10856,13 @@ __metadata:
languageName: node
linkType: hard
"jsdoc-type-pratt-parser@npm:~6.10.0":
version: 6.10.0
resolution: "jsdoc-type-pratt-parser@npm:6.10.0"
checksum: 10c0/8ea395df0cae0e41d4bdba5f8d81b8d3e467fe53d1e4182a5d4e653235a5f17d60ed137343d68dbc74fa10e767f1c58fb85b1f6d5489c2cf16fc7216cc6d3e1a
languageName: node
linkType: hard
"jsdom@npm:^26.0.0":
version: 26.1.0
resolution: "jsdom@npm:26.1.0"
@@ -11245,13 +11177,6 @@ __metadata:
languageName: node
linkType: hard
"long@npm:^5.0.0":
version: 5.3.1
resolution: "long@npm:5.3.1"
checksum: 10c0/8726994c6359bb7162fb94563e14c3f9c0f0eeafd90ec654738f4f144a5705756d36a873c442f172ee2a4b51e08d14ab99765b49aa1fb994c5ba7fe12057bca2
languageName: node
linkType: hard
"loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
@@ -11878,6 +11803,13 @@ __metadata:
languageName: node
linkType: hard
"object-deep-merge@npm:^2.0.0":
version: 2.0.0
resolution: "object-deep-merge@npm:2.0.0"
checksum: 10c0/69e8741131ad49fa8720fb96007a3c82dca1119b5d874151d2ecbcc3b44ccd46e8553c7a30b0abcba752c099ba361bbba97f33a68c9ae54c57eed7be116ffc97
languageName: node
linkType: hard
"object-inspect@npm:^1.13.3":
version: 1.13.3
resolution: "object-inspect@npm:1.13.3"
@@ -12212,6 +12144,15 @@ __metadata:
languageName: node
linkType: hard
"parse-imports-exports@npm:^0.2.4":
version: 0.2.4
resolution: "parse-imports-exports@npm:0.2.4"
dependencies:
parse-statements: "npm:1.0.11"
checksum: 10c0/51b729037208abdf65c4a1f8e9ed06f4e7ccd907c17c668a64db54b37d95bb9e92081f8b16e4133e14102af3cb4e89870975b6ad661b4d654e9ec8f4fb5c77d6
languageName: node
linkType: hard
"parse-json@npm:^5.0.0, parse-json@npm:^5.2.0":
version: 5.2.0
resolution: "parse-json@npm:5.2.0"
@@ -12224,6 +12165,13 @@ __metadata:
languageName: node
linkType: hard
"parse-statements@npm:1.0.11":
version: 1.0.11
resolution: "parse-statements@npm:1.0.11"
checksum: 10c0/48960e085019068a5f5242e875fd9d21ec87df2e291acf5ad4e4887b40eab6929a8c8d59542acb85a6497e870c5c6a24f5ab7f980ef5f907c14cc5f7984a93f3
languageName: node
linkType: hard
"parse5-htmlparser2-tree-adapter@npm:^7.0.0":
version: 7.1.0
resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0"
@@ -12948,26 +12896,6 @@ __metadata:
languageName: node
linkType: hard
"protobufjs@npm:^7.3.0":
version: 7.4.0
resolution: "protobufjs@npm:7.4.0"
dependencies:
"@protobufjs/aspromise": "npm:^1.1.2"
"@protobufjs/base64": "npm:^1.1.2"
"@protobufjs/codegen": "npm:^2.0.4"
"@protobufjs/eventemitter": "npm:^1.1.0"
"@protobufjs/fetch": "npm:^1.1.0"
"@protobufjs/float": "npm:^1.0.2"
"@protobufjs/inquire": "npm:^1.1.0"
"@protobufjs/path": "npm:^1.1.2"
"@protobufjs/pool": "npm:^1.1.0"
"@protobufjs/utf8": "npm:^1.1.0"
"@types/node": "npm:>=13.7.0"
long: "npm:^5.0.0"
checksum: 10c0/a5460a63fe596523b9a067cbce39a6b310d1a71750fda261f076535662aada97c24450e18c5bc98a27784f70500615904ff1227e1742183509f0db4fdede669b
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0":
version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0"
@@ -13476,6 +13404,13 @@ __metadata:
languageName: node
linkType: hard
"reserved-identifiers@npm:^1.0.0":
version: 1.2.0
resolution: "reserved-identifiers@npm:1.2.0"
checksum: 10c0/b82651b12e6c608e80463c3753d275bc20fd89294d0415f04e670aeec3611ae3582ddc19e8fedd497e7d0bcbfaddab6a12823ec86e855b1e6a245e0a734eb43d
languageName: node
linkType: hard
"resolve-from@npm:^4.0.0":
version: 4.0.0
resolution: "resolve-from@npm:4.0.0"
@@ -13936,6 +13871,15 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^7.7.3":
version: 7.7.3
resolution: "semver@npm:7.7.3"
bin:
semver: bin/semver.js
checksum: 10c0/4afe5c986567db82f44c8c6faef8fe9df2a9b1d98098fc1721f57c696c4c21cebd572f297fc21002f81889492345b8470473bc6f4aff5fb032a6ea59ea2bc45e
languageName: node
linkType: hard
"set-blocking@npm:^2.0.0":
version: 2.0.0
resolution: "set-blocking@npm:2.0.0"
@@ -14204,6 +14148,16 @@ __metadata:
languageName: node
linkType: hard
"spdx-expression-parse@npm:^4.0.0":
version: 4.0.0
resolution: "spdx-expression-parse@npm:4.0.0"
dependencies:
spdx-exceptions: "npm:^2.1.0"
spdx-license-ids: "npm:^3.0.0"
checksum: 10c0/965c487e77f4fb173f1c471f3eef4eb44b9f0321adc7f93d95e7620da31faa67d29356eb02523cd7df8a7fc1ec8238773cdbf9e45bd050329d2b26492771b736
languageName: node
linkType: hard
"spdx-license-ids@npm:^3.0.0":
version: 3.0.20
resolution: "spdx-license-ids@npm:3.0.20"
@@ -14711,6 +14665,16 @@ __metadata:
languageName: node
linkType: hard
"to-valid-identifier@npm:^1.0.0":
version: 1.0.0
resolution: "to-valid-identifier@npm:1.0.0"
dependencies:
"@sindresorhus/base62": "npm:^1.0.0"
reserved-identifiers: "npm:^1.0.0"
checksum: 10c0/569b49f43b5aaaa20677e67f0f1cdcff344855149934cfb80c793c7ac7c30e191b224bc81cab40fb57641af9ca73795c78053c164a2addc617671e2d22c13a4a
languageName: node
linkType: hard
"toggle-selection@npm:^1.0.6":
version: 1.0.6
resolution: "toggle-selection@npm:1.0.6"
@@ -14950,7 +14914,7 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^5.0.4, typescript@npm:^5.8.3":
"typescript@npm:^5.0.4":
version: 5.9.3
resolution: "typescript@npm:5.9.3"
bin:
@@ -14960,7 +14924,17 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin<compat/typescript>":
"typescript@npm:^5.8.3":
version: 5.8.3
resolution: "typescript@npm:5.8.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.0.4#optional!builtin<compat/typescript>":
version: 5.9.3
resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5"
bin:
@@ -14970,6 +14944,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin<compat/typescript>":
version: 5.8.3
resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=5786d5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb
languageName: node
linkType: hard
"unbox-primitive@npm:^1.1.0":
version: 1.1.0
resolution: "unbox-primitive@npm:1.1.0"