Log remote member participant matching and tile waiting state

A tile shows "Waiting for media" for as long as its MatrixRTC member
cannot be matched to a LiveKit participant. Log each transition of that
match and of the tile's waitingForMedia state so rageshakes can tie a
stuck tile to the LiveKit participant and track events.
This commit is contained in:
Matthew Hodgson
2026-09-03 17:07:08 +01:00
parent f8f5caaa09
commit 0b95fd5a65
3 changed files with 31 additions and 12 deletions

View File

@@ -5,7 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details. Please see LICENSE in the repository root for full details.
*/ */
import { describe, test, expect, beforeEach, afterEach } from "vitest"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
import { logger } from "matrix-js-sdk/lib/logger";
import { import {
type CallMembership, type CallMembership,
type LivekitTransport, type LivekitTransport,
@@ -77,6 +78,7 @@ function epochMeWith$<T, U>(
} }
test("should signal participant not yet connected to livekit", async () => { test("should signal participant not yet connected to livekit", async () => {
const info = vi.spyOn(logger, "info");
const mockedMemberships$ = new BehaviorSubject([bobMembership]); const mockedMemberships$ = new BehaviorSubject([bobMembership]);
const mockConnectionManagerData$ = new BehaviorSubject( const mockConnectionManagerData$ = new BehaviorSubject(
new ConnectionManagerData(), new ConnectionManagerData(),
@@ -108,6 +110,9 @@ test("should signal participant not yet connected to livekit", async () => {
return true; return true;
}, },
); );
expect(info).toHaveBeenCalledWith(
"[RemoteMatrixLivekitMembers] @bob:example.org:DEV000: LiveKit participant missing",
);
}); });
// Helper to create epoch'ed memberships$ and membershipsWithTransport$ from memberships observable. // Helper to create epoch'ed memberships$ and membershipsWithTransport$ from memberships observable.

View File

@@ -11,6 +11,7 @@ import {
type LivekitTransportConfig, type LivekitTransportConfig,
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { combineLatest, filter, map } from "rxjs"; import { combineLatest, filter, map } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../../Behavior"; import { type Behavior } from "../../Behavior";
import { type IConnectionManager } from "./ConnectionManager"; import { type IConnectionManager } from "./ConnectionManager";
@@ -133,8 +134,15 @@ export function createRemoteMatrixLivekitMembers$({
} }
}, },
// Each update where the key of the generator array do not change will result in updates to the `data$` behavior. // Each update where the key of the generator array do not change will result in updates to the `data$` behavior.
(scope, data$, userId, _deviceId, _memberId, _rtcBackendIdentity) => { (scope, data$, userId, _deviceId, _memberId, rtcBackendIdentity) => {
const { participant$, ...rest } = scope.splitBehavior(data$); const { participant$, ...rest } = scope.splitBehavior(data$);
// Log whether the member could be matched to a LiveKit participant,
// since a tile shows "waiting for media" for as long as it cannot.
participant$.pipe(scope.bind()).subscribe((p) => {
logger.info(
`[RemoteMatrixLivekitMembers] ${rtcBackendIdentity}: LiveKit participant ${p ? `matched (${p.sid})` : "missing"}`,
);
});
// will only get called once per backend identity. // will only get called once per backend identity.
// updates to data$ and as a result to displayName$ and mxcAvatarUrl$ are more frequent. // updates to data$ and as a result to displayName$ and mxcAvatarUrl$ are more frequent.
return { return {

View File

@@ -8,6 +8,7 @@ Please see LICENSE in the repository root for full details.
import { type RemoteParticipant } from "livekit-client"; import { type RemoteParticipant } from "livekit-client";
import { combineLatest, map, of, switchMap } from "rxjs"; import { combineLatest, map, of, switchMap } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../Behavior"; import { type Behavior } from "../Behavior";
import { createVolumeControls, type VolumeControls } from "../VolumeControls"; import { createVolumeControls, type VolumeControls } from "../VolumeControls";
@@ -45,6 +46,20 @@ export function createRemoteUserMedia(
statsType: "inbound-rtp", statsType: "inbound-rtp",
}); });
const waitingForMedia$ = scope.behavior(
combineLatest(
[inputs.livekitRoom$, inputs.participant$],
(livekitRoom, participant) =>
// If livekitRoom is undefined, the user is not attempting to publish on
// any transport and so we shouldn't expect a participant. (They might
// be a subscribe-only bot for example.)
livekitRoom !== undefined && participant === null,
),
);
waitingForMedia$.pipe(scope.bind()).subscribe((waiting) => {
logger.info(`[RemoteUserMedia ${inputs.id}] waitingForMedia=${waiting}`);
});
return { return {
...baseUserMedia, ...baseUserMedia,
...createVolumeControls(scope, { ...createVolumeControls(scope, {
@@ -68,15 +83,6 @@ export function createRemoteUserMedia(
), ),
), ),
), ),
waitingForMedia$: scope.behavior( waitingForMedia$,
combineLatest(
[inputs.livekitRoom$, inputs.participant$],
(livekitRoom, participant) =>
// If livekitRoom is undefined, the user is not attempting to publish on
// any transport and so we shouldn't expect a participant. (They might
// be a subscribe-only bot for example.)
livekitRoom !== undefined && participant === null,
),
),
}; };
} }