Extended logging for mute states

This commit is contained in:
Valere
2025-12-04 16:55:03 +01:00
parent e4404e5bb1
commit cba588f0ac
6 changed files with 77 additions and 13 deletions

View File

@@ -168,6 +168,15 @@ export interface ResolvedConfigOptions extends ConfigOptions {
enable_video: boolean; enable_video: boolean;
}; };
app_prompt: boolean; app_prompt: boolean;
logging?: {
livekit_log_level?:
| "trace"
| "debug"
| "info"
| "warn"
| "error"
| "silent";
};
} }
export const DEFAULT_CONFIG: ResolvedConfigOptions = { export const DEFAULT_CONFIG: ResolvedConfigOptions = {
@@ -186,4 +195,7 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
enable_video: true, enable_video: true,
}, },
app_prompt: true, app_prompt: true,
logging: {
livekit_log_level: "info",
},
}; };

View File

@@ -16,6 +16,7 @@ import { createRoot } from "react-dom/client";
import "./index.css"; import "./index.css";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { import {
LogLevel,
setLogExtension as setLKLogExtension, setLogExtension as setLKLogExtension,
setLogLevel as setLKLogLevel, setLogLevel as setLKLogLevel,
} from "livekit-client"; } from "livekit-client";
@@ -25,6 +26,7 @@ import { init as initRageshake } from "./settings/rageshake";
import { Initializer } from "./initializer"; import { Initializer } from "./initializer";
import { AppViewModel } from "./state/AppViewModel"; import { AppViewModel } from "./state/AppViewModel";
import { globalScope } from "./state/ObservableScope"; import { globalScope } from "./state/ObservableScope";
import { Config } from "./config/Config.ts";
window.setLKLogLevel = setLKLogLevel; window.setLKLogLevel = setLKLogLevel;
@@ -32,6 +34,29 @@ initRageshake().catch((e) => {
logger.error("Failed to initialize rageshake", e); logger.error("Failed to initialize rageshake", e);
}); });
setLKLogLevel("info"); setLKLogLevel("info");
// Initialize config and set LiveKit log level accordingly
// dev/test deployments can set the log level more verbose via config to help debugging
Config.init()
.then(() => {
const LKLogsMapping = {
trace: LogLevel.trace,
debug: LogLevel.debug,
info: LogLevel.info,
warn: LogLevel.warn,
error: LogLevel.error,
silent: LogLevel.silent,
};
// const logLevelConfig = Config.get().logging?.livekit_log_level;
// DO NOT COMMIT: temporarily hardcode until we add this to config options
const logLevelConfig = "debug";
setLKLogLevel(LKLogsMapping[logLevelConfig ?? "info"] ?? LogLevel.info);
})
.catch((e) => {
logger.error("Failed to initialize config for livekit log level", e);
});
setLKLogExtension((level, msg, context) => { setLKLogExtension((level, msg, context) => {
// we pass a synthetic logger name of "livekit" to the rageshake to make it easier to read // we pass a synthetic logger name of "livekit" to the rageshake to make it easier to read
global.mx_rage_logger.log(level, "livekit", msg, context); global.mx_rage_logger.log(level, "livekit", msg, context);

View File

@@ -477,9 +477,7 @@ export function createCallViewModel$(
mediaDevices, mediaDevices,
muteStates, muteStates,
trackProcessorState$, trackProcessorState$,
logger.getChild( logger.getChild(`[${connection.transport.livekit_service_url}]`),
"[Publisher" + connection.transport.livekit_service_url + "]",
),
); );
}, },
connectionManager: connectionManager, connectionManager: connectionManager,

View File

@@ -45,14 +45,16 @@ import {
* The Publisher is also responsible for creating the media tracks. * The Publisher is also responsible for creating the media tracks.
*/ */
export class Publisher { export class Publisher {
private readonly logger: Logger;
/** /**
* Creates a new Publisher. * Creates a new Publisher.
* @param scope - The observable scope to use for managing the publisher. * @param scope - The observable scope to use for managing the publisher.
* @param connection - The connection to use for publishing. * @param connection - The connection to use for publishing.
* @param devices - The media devices to use for audio and video input. * @param devices - The media devices to use for audio and video input.
* @param muteStates - The mute states for audio and video. * @param muteStates - The mute states for audio and video.
* @param e2eeLivekitOptions - The E2EE options to use for the LiveKit room. Use to share the same key provider across connections!.
* @param trackerProcessorState$ - The processor state for the video track processor (e.g. background blur). * @param trackerProcessorState$ - The processor state for the video track processor (e.g. background blur).
* @param logger - the parent logger
*/ */
public constructor( public constructor(
private scope: ObservableScope, private scope: ObservableScope,
@@ -60,8 +62,9 @@ export class Publisher {
devices: MediaDevices, devices: MediaDevices,
private readonly muteStates: MuteStates, private readonly muteStates: MuteStates,
trackerProcessorState$: Behavior<ProcessorState>, trackerProcessorState$: Behavior<ProcessorState>,
private logger: Logger, logger: Logger,
) { ) {
this.logger = logger.getChild(`[Publisher]`);
this.logger.info("Create LiveKit room"); this.logger.info("Create LiveKit room");
const { controlledAudioDevices } = getUrlParams(); const { controlledAudioDevices } = getUrlParams();
@@ -149,6 +152,7 @@ export class Publisher {
private _publishing$ = new BehaviorSubject<boolean>(false); private _publishing$ = new BehaviorSubject<boolean>(false);
public publishing$ = this.scope.behavior(this._publishing$); public publishing$ = this.scope.behavior(this._publishing$);
/** /**
* *
* @returns * @returns
@@ -233,6 +237,7 @@ export class Publisher {
* Stops all tracks that are currently running * Stops all tracks that are currently running
*/ */
public stopTracks(): void { public stopTracks(): void {
this.logger.debug("stopTracks called");
this.tracks$.value.forEach((t) => t.stop()); this.tracks$.value.forEach((t) => t.stop());
this._tracks$.next([]); this._tracks$.next([]);
} }
@@ -337,6 +342,7 @@ export class Publisher {
private observeMuteStates(scope: ObservableScope): void { private observeMuteStates(scope: ObservableScope): void {
const lkRoom = this.connection.livekitRoom; const lkRoom = this.connection.livekitRoom;
this.muteStates.audio.setHandler(async (desired) => { this.muteStates.audio.setHandler(async (desired) => {
this.logger.debug(`Syncing LiveKit audio mute state to ${desired}`);
try { try {
await lkRoom.localParticipant.setMicrophoneEnabled(desired); await lkRoom.localParticipant.setMicrophoneEnabled(desired);
} catch (e) { } catch (e) {
@@ -345,6 +351,7 @@ export class Publisher {
return lkRoom.localParticipant.isMicrophoneEnabled; return lkRoom.localParticipant.isMicrophoneEnabled;
}); });
this.muteStates.video.setHandler(async (desired) => { this.muteStates.video.setHandler(async (desired) => {
this.logger.debug(`Syncing LiveKit video mute state to ${desired}`);
try { try {
await lkRoom.localParticipant.setCameraEnabled(desired); await lkRoom.localParticipant.setCameraEnabled(desired);
} catch (e) { } catch (e) {

View File

@@ -49,6 +49,7 @@ describe("MuteState", () => {
} as unknown as MediaDevice<DeviceLabel, SelectedDevice>; } as unknown as MediaDevice<DeviceLabel, SelectedDevice>;
const muteState = new MuteState( const muteState = new MuteState(
"test-mutestate",
testScope, testScope,
deviceStub, deviceStub,
constant(true), constant(true),

View File

@@ -52,12 +52,14 @@ export class MuteState<Label, Selected> {
private readonly handler$ = new BehaviorSubject(defaultHandler); private readonly handler$ = new BehaviorSubject(defaultHandler);
public setHandler(handler: Handler): void { public setHandler(handler: Handler): void {
logger.debug(`MuteState[${this.description}]: setting handler`);
if (this.handler$.value !== defaultHandler) if (this.handler$.value !== defaultHandler)
throw new Error("Multiple mute state handlers are not supported"); throw new Error("Multiple mute state handlers are not supported");
this.handler$.next(handler); this.handler$.next(handler);
} }
public unsetHandler(): void { public unsetHandler(): void {
logger.debug(`MuteState[${this.description}]: removing handler`);
this.handler$.next(defaultHandler); this.handler$.next(defaultHandler);
} }
@@ -77,16 +79,19 @@ export class MuteState<Label, Selected> {
this.enabledByDefault$, this.enabledByDefault$,
(canControlDevices, enabledByDefault) => { (canControlDevices, enabledByDefault) => {
logger.info( logger.info(
`MuteState: canControlDevices: ${canControlDevices}, enabled by default: ${enabledByDefault}`, `MuteState[${this.description}]: canControlDevices: ${canControlDevices}, enabled by default: ${enabledByDefault}`,
); );
if (!canControlDevices) { if (!canControlDevices) {
logger.info( logger.info(
`MuteState: devices connected: ${canControlDevices}, disabling`, `MuteState[${this.description}]: devices connected: ${canControlDevices}, disabling`,
); );
// We need to sync the mute state with the handler // We need to sync the mute state with the handler
// to ensure nothing is beeing published. // to ensure nothing is beeing published.
this.handler$.value(false).catch((err) => { this.handler$.value(false).catch((err) => {
logger.error("MuteState-disable: handler error", err); logger.error(
"MuteState[${this.description}] disable: handler error",
err,
);
}); });
return { enabled$: of(false), set: null, toggle: null }; return { enabled$: of(false), set: null, toggle: null };
} }
@@ -102,12 +107,18 @@ export class MuteState<Label, Selected> {
let syncing = false; let syncing = false;
const sync = async (): Promise<void> => { const sync = async (): Promise<void> => {
if (enabled === latestDesired) syncing = false; if (enabled === latestDesired) {
else { syncing = false;
} else {
const previouslyEnabled = enabled; const previouslyEnabled = enabled;
enabled = await firstValueFrom( enabled = await firstValueFrom(
this.handler$.pipe( this.handler$.pipe(
switchMap(async (handler) => handler(latestDesired)), switchMap(async (handler) => {
logger.debug(
`MuteState[${this.description}]: syncing to ${latestDesired}`,
);
return handler(latestDesired);
}),
), ),
); );
if (enabled === previouslyEnabled) { if (enabled === previouslyEnabled) {
@@ -117,7 +128,10 @@ export class MuteState<Label, Selected> {
syncing = true; syncing = true;
sync().catch((err) => { sync().catch((err) => {
// TODO: better error handling // TODO: better error handling
logger.error("MuteState: handler error", err); logger.error(
"MuteState[${this.description}]: handler error",
err,
);
}); });
} }
} }
@@ -129,7 +143,10 @@ export class MuteState<Label, Selected> {
syncing = true; syncing = true;
sync().catch((err) => { sync().catch((err) => {
// TODO: better error handling // TODO: better error handling
logger.error("MuteState: handler error", err); logger.error(
"MuteState[${this.description}]: handler error",
err,
);
}); });
} }
}); });
@@ -158,6 +175,8 @@ export class MuteState<Label, Selected> {
); );
public constructor( public constructor(
// A description for logging purposes
private readonly description: string,
private readonly scope: ObservableScope, private readonly scope: ObservableScope,
private readonly device: MediaDevice<Label, Selected>, private readonly device: MediaDevice<Label, Selected>,
private readonly joined$: Observable<boolean>, private readonly joined$: Observable<boolean>,
@@ -189,6 +208,7 @@ export class MuteStates {
); );
public readonly audio = new MuteState( public readonly audio = new MuteState(
"audio-mutestate",
this.scope, this.scope,
this.mediaDevices.audioInput, this.mediaDevices.audioInput,
this.joined$, this.joined$,
@@ -196,6 +216,7 @@ export class MuteStates {
constant(false), constant(false),
); );
public readonly video = new MuteState( public readonly video = new MuteState(
"video-mutestate",
this.scope, this.scope,
this.mediaDevices.videoInput, this.mediaDevices.videoInput,
this.joined$, this.joined$,