135 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|