Files
Toju/src/app/core/services/screen-share-source-picker.service.ts
Myx 7b3caa0b61
All checks were successful
Queue Release Build / prepare (push) Successful in 44s
Queue Release Build / build-windows (push) Successful in 32m25s
Queue Release Build / build-linux (push) Successful in 42m57s
Queue Release Build / finalize (push) Successful in 3m41s
Fix electron windows screen and window picker for screensharing
2026-03-11 14:40:53 +01:00

135 lines
3.4 KiB
TypeScript

import {
Injectable,
computed,
signal
} from '@angular/core';
import { loadVoiceSettingsFromStorage, saveVoiceSettingsToStorage } from './voice-settings.storage';
import { ELECTRON_ENTIRE_SCREEN_SOURCE_NAME } from './webrtc/webrtc.constants';
export type ScreenShareSourceKind = 'screen' | 'window';
export interface ScreenShareSourceOption {
id: string;
kind: ScreenShareSourceKind;
name: string;
thumbnail: string;
}
export interface ScreenShareSourceSelection {
includeSystemAudio: boolean;
source: ScreenShareSourceOption;
}
interface ScreenShareSourcePickerRequest {
includeSystemAudio: boolean;
sources: readonly ScreenShareSourceOption[];
}
@Injectable({ providedIn: 'root' })
export class ScreenShareSourcePickerService {
readonly request = computed(() => this._request());
private readonly _request = signal<ScreenShareSourcePickerRequest | null>(null);
private pendingResolve: ((selection: ScreenShareSourceSelection) => void) | null = null;
private pendingReject: ((reason?: unknown) => void) | null = null;
open(
sources: readonly Pick<ScreenShareSourceOption, 'id' | 'name' | 'thumbnail'>[],
initialIncludeSystemAudio = loadVoiceSettingsFromStorage().includeSystemAudio
): Promise<ScreenShareSourceSelection> {
if (sources.length === 0) {
throw new Error('No desktop capture sources were available.');
}
this.cancelPendingRequest();
const normalizedSources = sources.map((source) => {
const kind = this.getSourceKind(source);
return {
...source,
kind,
name: this.getSourceDisplayName(source.name, kind)
};
});
this._request.set({
includeSystemAudio: initialIncludeSystemAudio,
sources: normalizedSources
});
return new Promise<ScreenShareSourceSelection>((resolve, reject) => {
this.pendingResolve = resolve;
this.pendingReject = reject;
});
}
confirm(sourceId: string, includeSystemAudio: boolean): void {
const activeRequest = this._request();
const source = activeRequest?.sources.find((entry) => entry.id === sourceId);
const resolve = this.pendingResolve;
if (!source || !resolve) {
return;
}
this.clearPendingRequest();
saveVoiceSettingsToStorage({ includeSystemAudio });
resolve({
includeSystemAudio,
source
});
}
cancel(): void {
this.cancelPendingRequest();
}
private cancelPendingRequest(): void {
const reject = this.pendingReject;
this.clearPendingRequest();
if (reject) {
reject(this.createAbortError());
}
}
private clearPendingRequest(): void {
this._request.set(null);
this.pendingResolve = null;
this.pendingReject = null;
}
private getSourceKind(
source: Pick<ScreenShareSourceOption, 'id' | 'name'>
): ScreenShareSourceKind {
return source.id.startsWith('screen') || source.name === ELECTRON_ENTIRE_SCREEN_SOURCE_NAME
? 'screen'
: 'window';
}
private getSourceDisplayName(name: string, kind: ScreenShareSourceKind): string {
const trimmedName = name.trim();
if (trimmedName) {
return trimmedName;
}
return kind === 'screen' ? 'Entire screen' : 'Window';
}
private createAbortError(): Error {
if (typeof DOMException !== 'undefined') {
return new DOMException('The user aborted a request.', 'AbortError');
}
const error = new Error('The user aborted a request.');
error.name = 'AbortError';
return error;
}
}