mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
feat(voice): add screen share subscription debug helper (#2432)
This commit is contained in:
1 parent
6c634b686f
commit
eb4562b6a6
8 files changed
+532
-23
No files matched your search
Vendored
+2
@@ -16,6 +16,8 @@ interface FluxerDebugApi {
|
||||
getClientInfoSync?: () => string;
|
||||
getClientInfoObject?: () => Promise<unknown>;
|
||||
getClientInfoObjectSync?: () => unknown;
|
||||
getVoiceSubscriptionDebug?: () => Promise<unknown>;
|
||||
getVoiceSubscriptionDebugJson?: () => Promise<string>;
|
||||
}
|
||||
|
||||
type FluxerDebugGlobal = Record<string, unknown> & FluxerDebugApi;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import i18n from '@app/app/I18n';
|
||||
import Config from '@app/features/app/config/Config';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {getFluxerDebugObject} from '@app/features/platform/utils/FluxerDebugGlobal';
|
||||
import {getElectronAPI, isDesktop} from '@app/features/ui/utils/NativeUtils';
|
||||
import type {DesktopInfo} from '@app/types/electron.d';
|
||||
import Bowser from 'bowser';
|
||||
@@ -325,29 +326,6 @@ export async function getFormattedClientInfo(): Promise<string> {
|
||||
return formatClientBuildInfo(await getClientInfo());
|
||||
}
|
||||
|
||||
interface FluxerDebugApi {
|
||||
getClientInfo?: () => Promise<string>;
|
||||
getClientInfoSync?: () => string;
|
||||
getClientInfoObject?: () => Promise<ClientInfo>;
|
||||
getClientInfoObjectSync?: () => ClientInfo;
|
||||
}
|
||||
|
||||
function getFluxerDebugObject(): (Record<string, unknown> & FluxerDebugApi) | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const win = window as Window & {
|
||||
__FLUXER_DEBUG__?: Record<string, unknown> & FluxerDebugApi;
|
||||
};
|
||||
if (win.__FLUXER_DEBUG__ === undefined || win.__FLUXER_DEBUG__ === null) {
|
||||
win.__FLUXER_DEBUG__ = {};
|
||||
}
|
||||
if (typeof win.__FLUXER_DEBUG__ !== 'object' || Array.isArray(win.__FLUXER_DEBUG__)) {
|
||||
return null;
|
||||
}
|
||||
return win.__FLUXER_DEBUG__;
|
||||
}
|
||||
|
||||
export function installFluxerConfigDebugApi(): void {
|
||||
const config = getFluxerDebugObject();
|
||||
if (!config) {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
type FluxerDebugObject = NonNullable<Window['__FLUXER_DEBUG__']>;
|
||||
|
||||
export function getFluxerDebugObject(): FluxerDebugObject | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const existing = window.__FLUXER_DEBUG__;
|
||||
if (existing === undefined || existing === null) {
|
||||
const created: FluxerDebugObject = {};
|
||||
window.__FLUXER_DEBUG__ = created;
|
||||
return created;
|
||||
}
|
||||
if (typeof existing !== 'object' || Array.isArray(existing)) {
|
||||
return null;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {getFluxerDebugObject} from '@app/features/platform/utils/FluxerDebugGlobal';
|
||||
import {loadLazyModule} from '@app/features/platform/utils/LazyModuleLoader';
|
||||
|
||||
const logger = new Logger('VoiceSubscriptionDebugApi');
|
||||
|
||||
async function collectReport(): Promise<unknown> {
|
||||
const {collectVoiceSubscriptionDebugReport} = await loadLazyModule(
|
||||
() => import('@app/features/voice/diagnostics/VoiceSubscriptionDebugReport'),
|
||||
);
|
||||
return collectVoiceSubscriptionDebugReport();
|
||||
}
|
||||
|
||||
export function installVoiceSubscriptionDebugApi(): void {
|
||||
const debugObject = getFluxerDebugObject();
|
||||
if (!debugObject) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
debugObject.getVoiceSubscriptionDebug = collectReport;
|
||||
debugObject.getVoiceSubscriptionDebugJson = async () => JSON.stringify(await collectReport(), null, 2);
|
||||
} catch (error) {
|
||||
logger.warn('Failed to install __FLUXER_DEBUG__ voice subscription helpers', error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import Config from '@app/features/app/config/Config';
|
||||
import {
|
||||
summarizeVoiceSubscriptionDebug,
|
||||
type VoiceSubscriptionDebugMediaStreamTrack,
|
||||
type VoiceSubscriptionDebugParticipant,
|
||||
type VoiceSubscriptionDebugPublication,
|
||||
type VoiceSubscriptionDebugSubscription,
|
||||
type VoiceSubscriptionDebugSummary,
|
||||
type VoiceSubscriptionDebugTransceiver,
|
||||
} from '@app/features/voice/diagnostics/VoiceSubscriptionDebugSummary';
|
||||
import MediaEngine from '@app/features/voice/engine/MediaEngineFacade';
|
||||
import type {VoiceMediaGraphSubscriptionCommand} from '@app/features/voice/engine/VoiceMediaGraph';
|
||||
import {voiceMediaGraphStore} from '@app/features/voice/engine/VoiceMediaGraphStore';
|
||||
import type {RemoteTrackPublication, Room} from 'livekit-client';
|
||||
|
||||
interface VoiceSubscriptionDebugFailure {
|
||||
key: string;
|
||||
code: number;
|
||||
reason: string;
|
||||
reportedAt: number;
|
||||
streamKey: string | null;
|
||||
participantIdentity: string | null;
|
||||
participantSid: string | null;
|
||||
trackSid: string | null;
|
||||
source: string | null;
|
||||
generation: number | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface VoiceSubscriptionDebugReport {
|
||||
capturedAt: string;
|
||||
buildVersion: string;
|
||||
graphNowMs: number;
|
||||
connection: {
|
||||
connected: boolean;
|
||||
connecting: boolean;
|
||||
reconnecting: boolean;
|
||||
guildId: string | null;
|
||||
channelId: string | null;
|
||||
connectionId: string | null;
|
||||
voiceServerEndpoint: string | null;
|
||||
roomState: string | null;
|
||||
localIdentity: string | null;
|
||||
};
|
||||
summary: VoiceSubscriptionDebugSummary;
|
||||
publisherTransceivers: Array<VoiceSubscriptionDebugTransceiver>;
|
||||
subscriberTransportPresent: boolean;
|
||||
remoteParticipants: Array<VoiceSubscriptionDebugParticipant>;
|
||||
subscriptions: Array<VoiceSubscriptionDebugSubscription>;
|
||||
pendingSubscriptionCommands: Array<VoiceMediaGraphSubscriptionCommand>;
|
||||
watchIntent: {viewerStreamKeys: Array<string>; deferredStopKeys: Array<string>};
|
||||
watchAttempts: Array<{
|
||||
streamKey: string;
|
||||
attemptKey: string;
|
||||
startedAt: number;
|
||||
hasRenderedVideoFrame: boolean;
|
||||
generation: number;
|
||||
}>;
|
||||
deadlines: Array<{
|
||||
key: string;
|
||||
kind: string;
|
||||
streamKey: string | null;
|
||||
subscriptionKey: string | null;
|
||||
attemptKey: string | null;
|
||||
generation: number;
|
||||
dueAt: number;
|
||||
}>;
|
||||
failures: Array<VoiceSubscriptionDebugFailure>;
|
||||
}
|
||||
|
||||
function describeMediaStreamTrack(
|
||||
track: MediaStreamTrack | null | undefined,
|
||||
): VoiceSubscriptionDebugMediaStreamTrack | null {
|
||||
if (!track) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
id: track.id,
|
||||
kind: track.kind,
|
||||
enabled: track.enabled,
|
||||
muted: track.muted,
|
||||
readyState: track.readyState,
|
||||
};
|
||||
}
|
||||
|
||||
function describeTransceivers(room: Room | null): Array<VoiceSubscriptionDebugTransceiver> {
|
||||
const publisher = room?.engine?.pcManager?.publisher;
|
||||
if (!publisher) {
|
||||
return [];
|
||||
}
|
||||
return publisher.getTransceivers().map((transceiver) => ({
|
||||
mid: transceiver.mid,
|
||||
direction: transceiver.direction,
|
||||
currentDirection: transceiver.currentDirection,
|
||||
receiver: describeMediaStreamTrack(transceiver.receiver?.track),
|
||||
sender: describeMediaStreamTrack(transceiver.sender?.track),
|
||||
}));
|
||||
}
|
||||
|
||||
function describePublication(publication: RemoteTrackPublication): VoiceSubscriptionDebugPublication {
|
||||
const dimensions = publication.dimensions;
|
||||
return {
|
||||
trackSid: publication.trackSid,
|
||||
trackName: publication.trackName,
|
||||
kind: publication.kind,
|
||||
source: publication.source,
|
||||
mimeType: publication.mimeType ?? null,
|
||||
dimensions: dimensions ? {width: dimensions.width, height: dimensions.height} : null,
|
||||
simulcasted: publication.simulcasted ?? null,
|
||||
muted: publication.isMuted,
|
||||
enabled: publication.isEnabled,
|
||||
subscribed: publication.isSubscribed,
|
||||
desired: publication.isDesired,
|
||||
subscriptionStatus: publication.subscriptionStatus,
|
||||
permissionStatus: publication.permissionStatus,
|
||||
videoQuality: publication.videoQuality ?? null,
|
||||
track: describeMediaStreamTrack(publication.track?.mediaStreamTrack),
|
||||
};
|
||||
}
|
||||
|
||||
function describeRemoteParticipants(room: Room | null): Array<VoiceSubscriptionDebugParticipant> {
|
||||
if (!room) {
|
||||
return [];
|
||||
}
|
||||
return Array.from(room.remoteParticipants.values()).map((participant) => ({
|
||||
identity: participant.identity,
|
||||
sid: participant.sid,
|
||||
publications: Array.from(participant.trackPublications.values()).map(describePublication),
|
||||
}));
|
||||
}
|
||||
|
||||
function describeError(error: unknown): string | null {
|
||||
if (error == null) {
|
||||
return null;
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return `${error.name}: ${error.message}`;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
|
||||
export function collectVoiceSubscriptionDebugReport(): VoiceSubscriptionDebugReport {
|
||||
const room = MediaEngine.room;
|
||||
const snapshot = voiceMediaGraphStore.getGraphSnapshot();
|
||||
const transceivers = describeTransceivers(room);
|
||||
const remoteParticipants = describeRemoteParticipants(room);
|
||||
const subscriptions = Array.from(snapshot.subscriptionsByKey.entries()).map(([key, entry]) => ({
|
||||
key,
|
||||
participantIdentity: entry.participantIdentity,
|
||||
source: entry.source,
|
||||
desired: {
|
||||
enabled: entry.desired.enabled,
|
||||
quality: entry.desired.quality,
|
||||
context: entry.desired.context,
|
||||
isIntersecting: entry.desired.isIntersecting,
|
||||
hasObservedElement: entry.desired.observedElement != null,
|
||||
},
|
||||
actual: {
|
||||
subscribed: entry.actual.subscribed,
|
||||
enabled: entry.actual.enabled,
|
||||
quality: entry.actual.quality,
|
||||
lastCommandAt: entry.actual.lastCommandAt,
|
||||
lastError: entry.actual.lastError,
|
||||
},
|
||||
publication: {
|
||||
available: entry.publication.available,
|
||||
trackSid: entry.publication.trackSid,
|
||||
observedAt: entry.publication.observedAt,
|
||||
},
|
||||
firstFrame: {renderedAt: entry.firstFrame.renderedAt},
|
||||
subscribed: entry.subscribed,
|
||||
}));
|
||||
const pendingSubscriptionCommands = Array.from(snapshot.subscriptionCommands);
|
||||
return {
|
||||
capturedAt: new Date().toISOString(),
|
||||
buildVersion: Config.PUBLIC_BUILD_VERSION ?? 'dev',
|
||||
graphNowMs: voiceMediaGraphStore.nowMs(),
|
||||
connection: {
|
||||
connected: MediaEngine.connected,
|
||||
connecting: MediaEngine.connecting,
|
||||
reconnecting: MediaEngine.reconnecting,
|
||||
guildId: MediaEngine.guildId,
|
||||
channelId: MediaEngine.channelId,
|
||||
connectionId: MediaEngine.connectionId,
|
||||
voiceServerEndpoint: MediaEngine.voiceServerEndpoint,
|
||||
roomState: room?.state ?? null,
|
||||
localIdentity: room?.localParticipant.identity ?? null,
|
||||
},
|
||||
summary: summarizeVoiceSubscriptionDebug(
|
||||
transceivers,
|
||||
remoteParticipants,
|
||||
subscriptions,
|
||||
pendingSubscriptionCommands.length,
|
||||
),
|
||||
publisherTransceivers: transceivers,
|
||||
subscriberTransportPresent: room?.engine?.pcManager?.subscriber != null,
|
||||
remoteParticipants,
|
||||
subscriptions,
|
||||
pendingSubscriptionCommands,
|
||||
watchIntent: {
|
||||
viewerStreamKeys: Array.from(snapshot.watchIntent.viewerStreamKeys),
|
||||
deferredStopKeys: Array.from(snapshot.watchIntent.deferredStopKeys),
|
||||
},
|
||||
watchAttempts: Array.from(snapshot.attemptsByStreamKey.entries()).map(([streamKey, attempt]) => ({
|
||||
streamKey,
|
||||
attemptKey: attempt.attemptKey,
|
||||
startedAt: attempt.startedAt,
|
||||
hasRenderedVideoFrame: attempt.hasRenderedVideoFrame,
|
||||
generation: attempt.generation,
|
||||
})),
|
||||
deadlines: Array.from(snapshot.deadlinesByKey.entries()).map(([key, deadline]) => ({
|
||||
key,
|
||||
kind: deadline.kind,
|
||||
streamKey: deadline.streamKey,
|
||||
subscriptionKey: deadline.subscriptionKey,
|
||||
attemptKey: deadline.attemptKey,
|
||||
generation: deadline.generation,
|
||||
dueAt: deadline.dueAt,
|
||||
})),
|
||||
failures: Array.from(snapshot.failuresByKey.entries()).map(([key, failure]) => ({
|
||||
key,
|
||||
code: failure.code,
|
||||
reason: failure.reason,
|
||||
reportedAt: failure.reportedAt,
|
||||
streamKey: failure.streamKey ?? null,
|
||||
participantIdentity: failure.participantIdentity ?? null,
|
||||
participantSid: failure.participantSid ?? null,
|
||||
trackSid: failure.trackSid ?? null,
|
||||
source: failure.source ?? null,
|
||||
generation: failure.generation ?? null,
|
||||
error: describeError(failure.error),
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
summarizeVoiceSubscriptionDebug,
|
||||
type VoiceSubscriptionDebugParticipant,
|
||||
type VoiceSubscriptionDebugPublication,
|
||||
type VoiceSubscriptionDebugSubscription,
|
||||
type VoiceSubscriptionDebugTransceiver,
|
||||
} from '@app/features/voice/diagnostics/VoiceSubscriptionDebugSummary';
|
||||
import {describe, expect, it} from 'vitest';
|
||||
|
||||
function transceiver(overrides: Partial<VoiceSubscriptionDebugTransceiver>): VoiceSubscriptionDebugTransceiver {
|
||||
return {
|
||||
mid: '0',
|
||||
direction: 'recvonly',
|
||||
currentDirection: 'recvonly',
|
||||
receiver: {id: 'receiver', kind: 'video', enabled: true, muted: false, readyState: 'live'},
|
||||
sender: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function publication(overrides: Partial<VoiceSubscriptionDebugPublication>): VoiceSubscriptionDebugPublication {
|
||||
return {
|
||||
trackSid: 'TR_1',
|
||||
trackName: 'screen',
|
||||
kind: 'video',
|
||||
source: 'screen_share',
|
||||
mimeType: 'video/VP9',
|
||||
dimensions: {width: 1920, height: 1080},
|
||||
simulcasted: false,
|
||||
muted: false,
|
||||
enabled: true,
|
||||
subscribed: false,
|
||||
desired: true,
|
||||
subscriptionStatus: 'desired',
|
||||
permissionStatus: 'allowed',
|
||||
videoQuality: 2,
|
||||
track: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function participant(publications: Array<VoiceSubscriptionDebugPublication>): VoiceSubscriptionDebugParticipant {
|
||||
return {identity: 'user_1_connection-a', sid: 'PA_1', publications};
|
||||
}
|
||||
|
||||
function subscription(overrides: Partial<VoiceSubscriptionDebugSubscription>): VoiceSubscriptionDebugSubscription {
|
||||
return {
|
||||
key: 'user_1_connection-a:screen_share',
|
||||
participantIdentity: 'user_1_connection-a',
|
||||
source: 'screen_share',
|
||||
desired: {enabled: true, quality: 'high', context: 'focused', isIntersecting: true, hasObservedElement: false},
|
||||
actual: {subscribed: null, enabled: null, quality: null, lastCommandAt: null, lastError: null},
|
||||
publication: {available: true, trackSid: 'TR_1', observedAt: 1000},
|
||||
firstFrame: {renderedAt: null},
|
||||
subscribed: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('summarizeVoiceSubscriptionDebug', () => {
|
||||
it('reports zero issued commands when the client never asked for the stream', () => {
|
||||
const summary = summarizeVoiceSubscriptionDebug([], [participant([publication({})])], [subscription({})], 0);
|
||||
expect(summary.remoteScreenSharePublications).toBe(1);
|
||||
expect(summary.screenShareSubscriptions).toBe(1);
|
||||
expect(summary.screenShareSubscriptionsWithCommandIssued).toBe(0);
|
||||
expect(summary.videoReceiveTransceivers).toBe(0);
|
||||
expect(summary.negotiatedVideoReceiveTransceivers).toBe(0);
|
||||
expect(summary.screenSharePublicationsWithTrack).toBe(0);
|
||||
});
|
||||
|
||||
it('counts a negotiated receive transceiver once the answer applies it', () => {
|
||||
const summary = summarizeVoiceSubscriptionDebug(
|
||||
[transceiver({}), transceiver({mid: '1', currentDirection: null})],
|
||||
[participant([publication({subscribed: true, track: null})])],
|
||||
[subscription({actual: {subscribed: true, enabled: true, quality: 'high', lastCommandAt: 5, lastError: null}})],
|
||||
2,
|
||||
);
|
||||
expect(summary.videoReceiveTransceivers).toBe(2);
|
||||
expect(summary.negotiatedVideoReceiveTransceivers).toBe(1);
|
||||
expect(summary.subscribedScreenSharePublications).toBe(1);
|
||||
expect(summary.screenShareSubscriptionsWithCommandIssued).toBe(1);
|
||||
expect(summary.pendingSubscriptionCommands).toBe(2);
|
||||
});
|
||||
|
||||
it('ignores send-only video, audio receivers, and non-screen-share sources', () => {
|
||||
const summary = summarizeVoiceSubscriptionDebug(
|
||||
[
|
||||
transceiver({direction: 'sendonly', currentDirection: 'sendonly'}),
|
||||
transceiver({
|
||||
mid: '1',
|
||||
receiver: {id: 'audio', kind: 'audio', enabled: true, muted: false, readyState: 'live'},
|
||||
}),
|
||||
],
|
||||
[
|
||||
participant([
|
||||
publication({source: 'camera'}),
|
||||
publication({trackSid: 'TR_2', kind: 'audio', source: 'screen_share_audio'}),
|
||||
]),
|
||||
],
|
||||
[subscription({source: 'camera'})],
|
||||
0,
|
||||
);
|
||||
expect(summary.videoReceiveTransceivers).toBe(0);
|
||||
expect(summary.negotiatedVideoReceiveTransceivers).toBe(0);
|
||||
expect(summary.remoteScreenSharePublications).toBe(0);
|
||||
expect(summary.screenShareSubscriptions).toBe(0);
|
||||
});
|
||||
|
||||
it('counts a rendered first frame separately from an attached track', () => {
|
||||
const summary = summarizeVoiceSubscriptionDebug(
|
||||
[transceiver({})],
|
||||
[
|
||||
participant([
|
||||
publication({
|
||||
subscribed: true,
|
||||
track: {id: 'track', kind: 'video', enabled: true, muted: false, readyState: 'live'},
|
||||
}),
|
||||
]),
|
||||
],
|
||||
[subscription({firstFrame: {renderedAt: 42}})],
|
||||
0,
|
||||
);
|
||||
expect(summary.screenSharePublicationsWithTrack).toBe(1);
|
||||
expect(summary.screenShareSubscriptionsWithFirstFrame).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {VOICE_MEDIA_GRAPH_SCREEN_SHARE_SOURCE} from '@app/features/voice/engine/VoiceMediaGraph';
|
||||
|
||||
export interface VoiceSubscriptionDebugMediaStreamTrack {
|
||||
id: string;
|
||||
kind: string;
|
||||
enabled: boolean;
|
||||
muted: boolean;
|
||||
readyState: string;
|
||||
}
|
||||
|
||||
export interface VoiceSubscriptionDebugTransceiver {
|
||||
mid: string | null;
|
||||
direction: string;
|
||||
currentDirection: string | null;
|
||||
receiver: VoiceSubscriptionDebugMediaStreamTrack | null;
|
||||
sender: VoiceSubscriptionDebugMediaStreamTrack | null;
|
||||
}
|
||||
|
||||
export interface VoiceSubscriptionDebugPublication {
|
||||
trackSid: string;
|
||||
trackName: string;
|
||||
kind: string;
|
||||
source: string;
|
||||
mimeType: string | null;
|
||||
dimensions: {width: number; height: number} | null;
|
||||
simulcasted: boolean | null;
|
||||
muted: boolean;
|
||||
enabled: boolean;
|
||||
subscribed: boolean;
|
||||
desired: boolean;
|
||||
subscriptionStatus: string;
|
||||
permissionStatus: string;
|
||||
videoQuality: number | null;
|
||||
track: VoiceSubscriptionDebugMediaStreamTrack | null;
|
||||
}
|
||||
|
||||
export interface VoiceSubscriptionDebugParticipant {
|
||||
identity: string;
|
||||
sid: string;
|
||||
publications: Array<VoiceSubscriptionDebugPublication>;
|
||||
}
|
||||
|
||||
export interface VoiceSubscriptionDebugSubscription {
|
||||
key: string;
|
||||
participantIdentity: string;
|
||||
source: string;
|
||||
desired: {
|
||||
enabled: boolean;
|
||||
quality: string;
|
||||
context: string;
|
||||
isIntersecting: boolean;
|
||||
hasObservedElement: boolean;
|
||||
};
|
||||
actual: {
|
||||
subscribed: boolean | null;
|
||||
enabled: boolean | null;
|
||||
quality: string | null;
|
||||
lastCommandAt: number | null;
|
||||
lastError: {code: number; reason: string; at: number} | null;
|
||||
};
|
||||
publication: {available: boolean; trackSid: string | null; observedAt: number | null};
|
||||
firstFrame: {renderedAt: number | null};
|
||||
subscribed: boolean;
|
||||
}
|
||||
|
||||
export interface VoiceSubscriptionDebugSummary {
|
||||
videoReceiveTransceivers: number;
|
||||
negotiatedVideoReceiveTransceivers: number;
|
||||
remoteScreenSharePublications: number;
|
||||
subscribedScreenSharePublications: number;
|
||||
screenSharePublicationsWithTrack: number;
|
||||
screenShareSubscriptions: number;
|
||||
screenShareSubscriptionsWithCommandIssued: number;
|
||||
screenShareSubscriptionsWithFirstFrame: number;
|
||||
pendingSubscriptionCommands: number;
|
||||
}
|
||||
|
||||
function isReceiveDirection(direction: string | null): boolean {
|
||||
return direction === 'recvonly' || direction === 'sendrecv';
|
||||
}
|
||||
|
||||
export function summarizeVoiceSubscriptionDebug(
|
||||
transceivers: ReadonlyArray<VoiceSubscriptionDebugTransceiver>,
|
||||
participants: ReadonlyArray<VoiceSubscriptionDebugParticipant>,
|
||||
subscriptions: ReadonlyArray<VoiceSubscriptionDebugSubscription>,
|
||||
pendingSubscriptionCommands: number,
|
||||
): VoiceSubscriptionDebugSummary {
|
||||
const videoTransceivers = transceivers.filter((transceiver) => transceiver.receiver?.kind === 'video');
|
||||
const screenSharePublications = participants
|
||||
.flatMap((participant) => participant.publications)
|
||||
.filter(
|
||||
(publication) => publication.source === VOICE_MEDIA_GRAPH_SCREEN_SHARE_SOURCE && publication.kind === 'video',
|
||||
);
|
||||
const screenShareSubscriptions = subscriptions.filter(
|
||||
(subscription) => subscription.source === VOICE_MEDIA_GRAPH_SCREEN_SHARE_SOURCE,
|
||||
);
|
||||
return {
|
||||
videoReceiveTransceivers: videoTransceivers.filter((transceiver) => isReceiveDirection(transceiver.direction))
|
||||
.length,
|
||||
negotiatedVideoReceiveTransceivers: videoTransceivers.filter((transceiver) =>
|
||||
isReceiveDirection(transceiver.currentDirection),
|
||||
).length,
|
||||
remoteScreenSharePublications: screenSharePublications.length,
|
||||
subscribedScreenSharePublications: screenSharePublications.filter((publication) => publication.subscribed).length,
|
||||
screenSharePublicationsWithTrack: screenSharePublications.filter((publication) => publication.track != null).length,
|
||||
screenShareSubscriptions: screenShareSubscriptions.length,
|
||||
screenShareSubscriptionsWithCommandIssued: screenShareSubscriptions.filter(
|
||||
(subscription) => subscription.actual.lastCommandAt != null,
|
||||
).length,
|
||||
screenShareSubscriptionsWithFirstFrame: screenShareSubscriptions.filter(
|
||||
(subscription) => subscription.firstFrame.renderedAt != null,
|
||||
).length,
|
||||
pendingSubscriptionCommands,
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
} from '@app/features/platform/utils/ClientInfo';
|
||||
import {loadLazyModule} from '@app/features/platform/utils/LazyModuleLoader';
|
||||
import {scheduleNonLatinScriptFaces} from '@app/features/theme/fonts/ScriptFontLoader';
|
||||
import {installVoiceSubscriptionDebugApi} from '@app/features/voice/diagnostics/VoiceSubscriptionDebugApi';
|
||||
import {i18n} from '@lingui/core';
|
||||
import {I18nProvider} from '@lingui/react';
|
||||
import {configure} from 'mobx';
|
||||
@@ -48,6 +49,7 @@ if (typeof window !== 'undefined' && window.history) {
|
||||
}
|
||||
|
||||
installFluxerConfigDebugApi();
|
||||
installVoiceSubscriptionDebugApi();
|
||||
|
||||
function createRoot(): ReactDOM.Root {
|
||||
const container = document.getElementById('root');
|
||||
|
||||
Reference in new issue
Block a user