mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
fix(voice): record why a screen share stopped (#2664)
This commit is contained in:
1 parent
5da256df12
commit
3a862f1484
11 files changed
+594
-36
No files matched your search
+81
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
type VoiceEngineV2AppScreenShareControllerGateway,
|
||||
VoiceEngineV2AppScreenShareControllerRouting,
|
||||
} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareControllerRouting';
|
||||
import type {VoiceEngineV2AppScreenShareExecutionAdapter} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter';
|
||||
import {
|
||||
getRecentScreenShares,
|
||||
recordScreenShareStarted,
|
||||
resetRecentScreenSharesForTests,
|
||||
} from '@app/features/voice/utils/ScreenShareLifecycleLog';
|
||||
import {beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
|
||||
vi.mock('@app/features/voice/engine/voice_screen_share_manager/shared', () => ({
|
||||
logger: {debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined},
|
||||
}));
|
||||
|
||||
vi.mock('@app/features/voice/state/LocalVoiceState', () => ({
|
||||
default: {getSelfStream: () => true},
|
||||
}));
|
||||
|
||||
const setEnabled = vi.fn(async () => undefined);
|
||||
|
||||
function createAdapter(): VoiceEngineV2AppScreenShareExecutionAdapter {
|
||||
return {liveKitFlows: {setEnabled}} as unknown as VoiceEngineV2AppScreenShareExecutionAdapter;
|
||||
}
|
||||
|
||||
function createGateway(
|
||||
overrides: Partial<VoiceEngineV2AppScreenShareControllerGateway> = {},
|
||||
): VoiceEngineV2AppScreenShareControllerGateway {
|
||||
return {
|
||||
isScreenCommandRoutable: () => true,
|
||||
hasScreenPublication: () => true,
|
||||
hasScreenDesired: () => false,
|
||||
clearScreenDesired: () => undefined,
|
||||
executingScreenOperationId: () => null,
|
||||
isScreenOperationPending: () => true,
|
||||
publishScreen: async () => undefined,
|
||||
unpublishScreen: async () => undefined,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('routed screen share stop attribution', () => {
|
||||
beforeEach(() => {
|
||||
resetRecentScreenSharesForTests();
|
||||
setEnabled.mockClear();
|
||||
});
|
||||
|
||||
it('keeps a stop the user asked for as a user stop when the controller carries it out', async () => {
|
||||
const routing = new VoiceEngineV2AppScreenShareControllerRouting(createAdapter());
|
||||
let executingOperationId: number | null = null;
|
||||
routing.setGateway(
|
||||
createGateway({
|
||||
executingScreenOperationId: () => executingOperationId,
|
||||
unpublishScreen: async (onPlanned) => {
|
||||
onPlanned([7]);
|
||||
executingOperationId = 7;
|
||||
await routing.unpublishViaLiveKitFlows(null);
|
||||
},
|
||||
}),
|
||||
);
|
||||
recordScreenShareStarted();
|
||||
|
||||
await routing.setEnabled(null, false, {sendUpdate: true, playSound: true});
|
||||
|
||||
expect(setEnabled).toHaveBeenCalledTimes(1);
|
||||
expect(getRecentScreenShares()[0].stopTrigger).toBe('user');
|
||||
});
|
||||
|
||||
it('records an unpublish the app never asked for as a gateway echo', async () => {
|
||||
const routing = new VoiceEngineV2AppScreenShareControllerRouting(createAdapter());
|
||||
routing.setGateway(createGateway());
|
||||
recordScreenShareStarted();
|
||||
|
||||
await routing.unpublishViaLiveKitFlows(null);
|
||||
|
||||
expect(getRecentScreenShares()[0].stopTrigger).toBe('gateway-echo');
|
||||
});
|
||||
});
|
||||
+2
@@ -4,6 +4,7 @@ import assert from 'node:assert/strict';
|
||||
import type {VoiceEngineV2AppScreenShareExecutionAdapter} from '@app/features/voice/engine/v2/VoiceEngineV2AppScreenShareExecutionAdapter';
|
||||
import {logger} from '@app/features/voice/engine/voice_screen_share_manager/shared';
|
||||
import LocalVoiceState from '@app/features/voice/state/LocalVoiceState';
|
||||
import {recordScreenShareStopped} from '@app/features/voice/utils/ScreenShareLifecycleLog';
|
||||
import type {VoiceEngineV2ScreenOptions} from '@fluxer/voice_engine_v2';
|
||||
import type {Room, ScreenShareCaptureOptions, TrackPublishOptions, VideoCodec} from 'livekit-client';
|
||||
|
||||
@@ -445,6 +446,7 @@ export class VoiceEngineV2AppScreenShareControllerRouting {
|
||||
|
||||
async unpublishViaLiveKitFlows(roomFromPort: Room | null): Promise<void> {
|
||||
const request = this.takeStopRequest(this.executingScreenOperationId());
|
||||
recordScreenShareStopped(request !== null ? 'user' : 'gateway-echo');
|
||||
const room = request !== null ? request.room : roomFromPort;
|
||||
try {
|
||||
await this.adapter.liveKitFlows.setEnabled(room, false, {
|
||||
|
||||
+39
-4
@@ -78,6 +78,15 @@ import {
|
||||
disarmNativeAudio,
|
||||
reconfigureLinuxNativeAudioRouting,
|
||||
} from '@app/features/voice/utils/NativeAudioCaptureBridge';
|
||||
import {
|
||||
recordScreenShareEncoderVerification,
|
||||
recordScreenShareEndedModal,
|
||||
recordScreenShareRequestedCodec,
|
||||
recordScreenShareStartError,
|
||||
recordScreenShareStarted,
|
||||
recordScreenShareStopped,
|
||||
type ScreenShareEndedModal,
|
||||
} from '@app/features/voice/utils/ScreenShareLifecycleLog';
|
||||
import {SCREEN_SHARE_DEGRADATION_PREFERENCE} from '@app/features/voice/utils/ScreenShareOptions';
|
||||
import {ScreenShareRollbackIncompleteError} from '@app/features/voice/utils/ScreenShareRollbackIncompleteError';
|
||||
import {handleScreenShareError} from '@app/features/voice/utils/ScreenShareUtils';
|
||||
@@ -400,7 +409,8 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
this.transitionScreenShareLifecycleInternal({type: 'share.encoderVerification.cleared'});
|
||||
}
|
||||
|
||||
showScreenShareEndedModalInternal(description: string): void {
|
||||
showScreenShareEndedModalInternal(description: string, modal: ScreenShareEndedModal): void {
|
||||
recordScreenShareEndedModal(modal);
|
||||
ModalCommands.pushWithKey(
|
||||
ModalCommands.modal(() => (
|
||||
<GenericErrorModal
|
||||
@@ -491,9 +501,11 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
i18n._(
|
||||
cause === 'stalled' ? SCREEN_SHARE_ENCODER_FAILED_DESCRIPTOR : SCREEN_SHARE_CODEC_POLICY_FAILED_DESCRIPTOR,
|
||||
),
|
||||
cause === 'stalled' ? 'encoder-failed' : 'codec-policy-failed',
|
||||
);
|
||||
if (this.endedScreenShareStopInFlight) return;
|
||||
if (!this.isScreenShareTrackPublishedInternal(participant, track)) return;
|
||||
recordScreenShareStopped('codec-republish-failed');
|
||||
this.transitionScreenShareLifecycleInternal({type: 'share.endedStop.start'});
|
||||
const stopPromise = this.setScreenShareEnabled(room, false, {sendUpdate: true, playSound: true})
|
||||
.catch((error) => {
|
||||
@@ -516,6 +528,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
): void {
|
||||
this.cancelEncoderVerificationInternal();
|
||||
if (!codec) return;
|
||||
recordScreenShareRequestedCodec(codec);
|
||||
const publication = preferredTrack ? undefined : participant.getTrackPublication(Track.Source.ScreenShare);
|
||||
const track = preferredTrack ?? (publication?.videoTrack as LocalVideoTrack | undefined);
|
||||
const sender = track?.sender;
|
||||
@@ -530,8 +543,10 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
const action = resolveScreenShareEncoderVerificationAction(failure);
|
||||
switch (action.kind) {
|
||||
case 'ignore-repeated-stall':
|
||||
recordScreenShareEncoderVerification('ignore-repeated-stall', null);
|
||||
return;
|
||||
case 'recover-stalled':
|
||||
recordScreenShareEncoderVerification('recover-stalled', null);
|
||||
logger.warn('Screen share encoder verification failed', {
|
||||
codec: action.codec,
|
||||
failureReason: 'screen-share-encode-stalled',
|
||||
@@ -546,12 +561,14 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
);
|
||||
return;
|
||||
case 'accept-negotiated':
|
||||
recordScreenShareEncoderVerification('accept-negotiated', action.negotiated);
|
||||
logger.info('Screen share publisher negotiated a different codec inside the publish policy', {
|
||||
requested: action.requested,
|
||||
negotiated: action.negotiated,
|
||||
});
|
||||
return;
|
||||
case 'correct-negotiated':
|
||||
recordScreenShareEncoderVerification('correct-negotiated', action.negotiated);
|
||||
logger.warn('Screen share is sending a codec outside the publish policy', {
|
||||
requested: action.requested,
|
||||
negotiated: action.negotiated,
|
||||
@@ -599,7 +616,8 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
trigger,
|
||||
readyState: mediaStreamTrack.readyState,
|
||||
});
|
||||
this.showScreenShareEndedModalInternal(i18n._(SCREEN_SHARE_SOURCE_STOPPED_DESCRIPTOR));
|
||||
this.showScreenShareEndedModalInternal(i18n._(SCREEN_SHARE_SOURCE_STOPPED_DESCRIPTOR), 'source-stopped');
|
||||
recordScreenShareStopped('media-track-ended');
|
||||
this.transitionScreenShareLifecycleInternal({type: 'share.endedStop.start'});
|
||||
const stopPromise = this.setScreenShareEnabled(room, false, {sendUpdate: true, playSound: true})
|
||||
.catch((error) => {
|
||||
@@ -663,6 +681,7 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
}
|
||||
|
||||
handleLocalScreenShareTrackUnpublished(room: Room, playSound: boolean, publication?: LocalTrackPublication): void {
|
||||
recordScreenShareStopped(this.isScreenSharePending ? 'user' : 'server-unpublish');
|
||||
this.clearScreenShareKeepAliveSinkInternal();
|
||||
this.cleanupActiveScreenShareEndListenerInternal();
|
||||
const participant = room.localParticipant;
|
||||
@@ -866,7 +885,17 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
publishOptions?: TrackPublishOptions,
|
||||
): Promise<void> {
|
||||
assert.equal(typeof enabled, 'boolean');
|
||||
await this.controllerRouting.setEnabled(room, enabled, options, publishOptions);
|
||||
if (enabled) {
|
||||
recordScreenShareStarted();
|
||||
}
|
||||
try {
|
||||
await this.controllerRouting.setEnabled(room, enabled, options, publishOptions);
|
||||
} catch (error) {
|
||||
if (enabled) {
|
||||
recordScreenShareStartError(error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!enabled && options?.preserveStreamAudioPreferences !== true) {
|
||||
VoiceSettingsCommands.update({
|
||||
shareAppAudio: true,
|
||||
@@ -908,7 +937,13 @@ class VoiceEngineV2AppScreenShareExecutionAdapter extends Store {
|
||||
options?: DeviceScreenShareCaptureOptions,
|
||||
publishOptions?: TrackPublishOptions,
|
||||
): Promise<void> {
|
||||
await this.liveKitFlows.startDeviceScreenShare(room, options, publishOptions);
|
||||
recordScreenShareStarted();
|
||||
try {
|
||||
await this.liveKitFlows.startDeviceScreenShare(room, options, publishOptions);
|
||||
} catch (error) {
|
||||
recordScreenShareStartError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async replaceActiveDisplayScreenShare(
|
||||
|
||||
Reference in new issue
Block a user