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(null); private pendingResolve: ((selection: ScreenShareSourceSelection) => void) | null = null; private pendingReject: ((reason?: unknown) => void) | null = null; open( sources: readonly Pick[], initialIncludeSystemAudio = loadVoiceSettingsFromStorage().includeSystemAudio ): Promise { 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((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 ): 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; } }