ddd test
This commit is contained in:
575
src/app/infrastructure/realtime/media/screen-share.manager.ts
Normal file
575
src/app/infrastructure/realtime/media/screen-share.manager.ts
Normal file
@@ -0,0 +1,575 @@
|
||||
/* eslint-disable, @typescript-eslint/no-non-null-assertion, @typescript-eslint/member-ordering, id-denylist */
|
||||
/**
|
||||
* Manages screen sharing: getDisplayMedia / Electron desktop capturer,
|
||||
* system-audio capture, and attaching screen tracks to peers.
|
||||
*/
|
||||
import { WebRTCLogger } from '../logging/webrtc-logger';
|
||||
import { ScreenShareQualityPreset, ScreenShareStartOptions } from '../screen-share.config';
|
||||
import { PeerData } from '../realtime.types';
|
||||
import {
|
||||
TRACK_KIND_AUDIO,
|
||||
TRACK_KIND_VIDEO,
|
||||
TRANSCEIVER_SEND_RECV,
|
||||
TRANSCEIVER_RECV_ONLY
|
||||
} from '../realtime.constants';
|
||||
import { DEFAULT_SCREEN_SHARE_START_OPTIONS, SCREEN_SHARE_QUALITY_PRESETS } from '../screen-share.config';
|
||||
import { BrowserScreenShareCapture } from './screen-share-platforms/browser-screen-share.capture';
|
||||
import { DesktopElectronScreenShareCapture } from './screen-share-platforms/desktop-electron-screen-share.capture';
|
||||
import { LinuxElectronScreenShareCapture } from './screen-share-platforms/linux-electron-screen-share.capture';
|
||||
import { getElectronApi } from '../../../core/platform/electron/get-electron-api';
|
||||
import { ScreenShareElectronApi } from './screen-share-platforms/shared';
|
||||
|
||||
/**
|
||||
* Callbacks the ScreenShareManager needs from the owning service.
|
||||
*/
|
||||
export interface ScreenShareCallbacks {
|
||||
getActivePeers(): Map<string, PeerData>;
|
||||
getLocalMediaStream(): MediaStream | null;
|
||||
renegotiate(peerId: string): Promise<void>;
|
||||
broadcastCurrentStates(): void;
|
||||
selectDesktopSource?(
|
||||
sources: readonly { id: string; name: string; thumbnail: string }[],
|
||||
options: { includeSystemAudio: boolean }
|
||||
): Promise<{
|
||||
includeSystemAudio: boolean;
|
||||
source: { id: string; name: string; thumbnail: string };
|
||||
}>;
|
||||
updateLocalScreenShareState?(state: LocalScreenShareState): void;
|
||||
}
|
||||
|
||||
type ScreenShareCaptureMethod = 'display-media' | 'electron-desktop' | 'linux-electron';
|
||||
|
||||
export interface LocalScreenShareState {
|
||||
active: boolean;
|
||||
captureMethod: ScreenShareCaptureMethod | null;
|
||||
includeSystemAudio: boolean;
|
||||
stream: MediaStream | null;
|
||||
suppressRemotePlayback: boolean;
|
||||
forceDefaultRemotePlaybackOutput: boolean;
|
||||
}
|
||||
|
||||
export class ScreenShareManager {
|
||||
/** The active screen-capture stream. */
|
||||
private activeScreenStream: MediaStream | null = null;
|
||||
|
||||
/** Optional system-audio stream captured alongside the screen. */
|
||||
private screenAudioStream: MediaStream | null = null;
|
||||
|
||||
/** The quality preset currently applied to the active share. */
|
||||
private activeScreenPreset: ScreenShareQualityPreset | null = null;
|
||||
|
||||
/** Remote peers that explicitly requested screen-share video. */
|
||||
private readonly requestedViewerPeerIds = new Set<string>();
|
||||
|
||||
/** Browser `getDisplayMedia` capture path. */
|
||||
private readonly browserScreenShareCapture: BrowserScreenShareCapture;
|
||||
|
||||
/** Desktop Electron capture path for non-Linux desktop builds. */
|
||||
private readonly desktopElectronScreenShareCapture: DesktopElectronScreenShareCapture;
|
||||
|
||||
/** Linux Electron screen/audio capture path with isolated audio routing. */
|
||||
private readonly linuxElectronScreenShareCapture: LinuxElectronScreenShareCapture;
|
||||
|
||||
/** Whether screen sharing is currently active. */
|
||||
private isScreenActive = false;
|
||||
|
||||
constructor(
|
||||
private readonly logger: WebRTCLogger,
|
||||
private callbacks: ScreenShareCallbacks
|
||||
) {
|
||||
this.browserScreenShareCapture = new BrowserScreenShareCapture(this.logger);
|
||||
this.desktopElectronScreenShareCapture = new DesktopElectronScreenShareCapture(this.logger, {
|
||||
getElectronApi: () => this.getElectronApi(),
|
||||
getSelectDesktopSource: () => this.callbacks.selectDesktopSource
|
||||
});
|
||||
|
||||
this.linuxElectronScreenShareCapture = new LinuxElectronScreenShareCapture(this.logger, {
|
||||
getElectronApi: () => this.getElectronApi(),
|
||||
onCaptureEnded: () => {
|
||||
if (this.isScreenActive) {
|
||||
this.stopScreenShare();
|
||||
}
|
||||
},
|
||||
startDisplayMedia: async (options, preset) =>
|
||||
await this.browserScreenShareCapture.startCapture(options, preset)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the callback set at runtime.
|
||||
* Needed because of circular initialisation between managers.
|
||||
*
|
||||
* @param nextCallbacks - The new callback interface to wire into this manager.
|
||||
*/
|
||||
setCallbacks(nextCallbacks: ScreenShareCallbacks): void {
|
||||
this.callbacks = nextCallbacks;
|
||||
}
|
||||
|
||||
/** Returns the current screen-capture stream, or `null` if inactive. */
|
||||
getScreenStream(): MediaStream | null { return this.activeScreenStream; }
|
||||
/** Whether screen sharing is currently active. */
|
||||
getIsScreenActive(): boolean { return this.isScreenActive; }
|
||||
|
||||
/**
|
||||
* Begin screen sharing.
|
||||
*
|
||||
* On Linux Electron builds, prefers a dedicated PulseAudio/PipeWire routing
|
||||
* path so remote voice playback is kept out of captured system audio.
|
||||
* On Windows Electron builds, prefers `getDisplayMedia` with system audio
|
||||
* so the separate mic `getUserMedia` stream is not disrupted; falls back to
|
||||
* Electron desktop capture only when `getDisplayMedia` fails entirely.
|
||||
* In browser contexts, uses `getDisplayMedia`.
|
||||
*
|
||||
* @param options - Screen-share capture options.
|
||||
* @returns The captured screen {@link MediaStream}.
|
||||
* @throws If both Electron and browser screen capture fail.
|
||||
*/
|
||||
async startScreenShare(options: ScreenShareStartOptions = DEFAULT_SCREEN_SHARE_START_OPTIONS): Promise<MediaStream> {
|
||||
const shareOptions = {
|
||||
...DEFAULT_SCREEN_SHARE_START_OPTIONS,
|
||||
...options
|
||||
};
|
||||
const preset = SCREEN_SHARE_QUALITY_PRESETS[shareOptions.quality];
|
||||
const electronDesktopCaptureAvailable = this.desktopElectronScreenShareCapture.isAvailable();
|
||||
|
||||
let captureMethod: ScreenShareCaptureMethod | null = null;
|
||||
|
||||
try {
|
||||
this.logger.info('startScreenShare invoked', shareOptions);
|
||||
|
||||
if (this.activeScreenStream) {
|
||||
this.stopScreenShare();
|
||||
}
|
||||
|
||||
await this.linuxElectronScreenShareCapture.awaitPendingReset();
|
||||
|
||||
this.activeScreenStream = null;
|
||||
|
||||
if (shareOptions.includeSystemAudio && this.linuxElectronScreenShareCapture.isSupported()) {
|
||||
try {
|
||||
this.activeScreenStream = await this.linuxElectronScreenShareCapture.startCapture(shareOptions, preset);
|
||||
captureMethod = 'linux-electron';
|
||||
} catch (error) {
|
||||
this.rethrowIfScreenShareAborted(error);
|
||||
this.logger.warn('Linux Electron audio routing failed; falling back to standard capture', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.activeScreenStream && shareOptions.includeSystemAudio) {
|
||||
try {
|
||||
this.activeScreenStream = await this.browserScreenShareCapture.startCapture(shareOptions, preset);
|
||||
captureMethod = 'display-media';
|
||||
|
||||
if (this.activeScreenStream.getAudioTracks().length === 0) {
|
||||
if (electronDesktopCaptureAvailable) {
|
||||
// On Windows Electron, keep the getDisplayMedia stream for video
|
||||
// rather than falling through to getUserMedia desktop audio which
|
||||
// can replace or kill the active mic stream.
|
||||
this.logger.warn(
|
||||
'getDisplayMedia did not provide system audio; '
|
||||
+ 'continuing without system audio to preserve mic stream'
|
||||
);
|
||||
|
||||
shareOptions.includeSystemAudio = false;
|
||||
} else {
|
||||
this.logger.warn('getDisplayMedia did not provide system audio; trying next capture method');
|
||||
this.activeScreenStream.getTracks().forEach((track) => track.stop());
|
||||
this.activeScreenStream = null;
|
||||
captureMethod = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.rethrowIfScreenShareAborted(error);
|
||||
this.logger.warn('getDisplayMedia with system audio failed; falling back to Electron desktop capture', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.activeScreenStream && electronDesktopCaptureAvailable) {
|
||||
try {
|
||||
const electronCapture = await this.desktopElectronScreenShareCapture.startCapture(shareOptions, preset);
|
||||
|
||||
this.activeScreenStream = electronCapture.stream;
|
||||
shareOptions.includeSystemAudio = electronCapture.includeSystemAudio;
|
||||
captureMethod = 'electron-desktop';
|
||||
} catch (error) {
|
||||
this.rethrowIfScreenShareAborted(error);
|
||||
this.logger.warn('Electron desktop capture failed; falling back to getDisplayMedia', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.activeScreenStream) {
|
||||
this.activeScreenStream = await this.browserScreenShareCapture.startCapture(shareOptions, preset);
|
||||
captureMethod = 'display-media';
|
||||
}
|
||||
|
||||
this.configureScreenStream(preset);
|
||||
this.prepareScreenAudio(shareOptions.includeSystemAudio);
|
||||
this.activeScreenPreset = preset;
|
||||
this.attachScreenTracksToPeers(preset);
|
||||
|
||||
this.isScreenActive = true;
|
||||
this.publishLocalScreenShareState(shareOptions.includeSystemAudio, captureMethod);
|
||||
this.callbacks.broadcastCurrentStates();
|
||||
|
||||
const activeScreenStream = this.activeScreenStream;
|
||||
|
||||
if (!activeScreenStream) {
|
||||
throw new Error('Screen sharing did not produce an active stream.');
|
||||
}
|
||||
|
||||
const screenVideoTrack = activeScreenStream.getVideoTracks()[0];
|
||||
|
||||
if (screenVideoTrack) {
|
||||
screenVideoTrack.onended = () => {
|
||||
this.logger.warn('Screen video track ended');
|
||||
this.stopScreenShare();
|
||||
};
|
||||
}
|
||||
|
||||
return activeScreenStream;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to start screen share', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop screen sharing and remove screen-share tracks on all peers.
|
||||
*
|
||||
* Stops all screen-capture tracks, resets screen transceivers to receive-only,
|
||||
* and triggers renegotiation.
|
||||
*/
|
||||
stopScreenShare(): void {
|
||||
if (this.activeScreenStream) {
|
||||
this.activeScreenStream.getTracks().forEach((track) => track.stop());
|
||||
this.activeScreenStream = null;
|
||||
}
|
||||
|
||||
this.linuxElectronScreenShareCapture.scheduleReset();
|
||||
|
||||
this.screenAudioStream = null;
|
||||
this.activeScreenPreset = null;
|
||||
this.isScreenActive = false;
|
||||
this.publishLocalScreenShareState(false, null);
|
||||
this.callbacks.broadcastCurrentStates();
|
||||
|
||||
this.callbacks.getActivePeers().forEach((peerData, peerId) => {
|
||||
this.detachScreenTracksFromPeer(peerData, peerId);
|
||||
});
|
||||
}
|
||||
|
||||
requestScreenShareForPeer(peerId: string): void {
|
||||
this.requestedViewerPeerIds.add(peerId);
|
||||
|
||||
if (!this.isScreenActive || !this.activeScreenPreset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerData = this.callbacks.getActivePeers().get(peerId);
|
||||
|
||||
if (!peerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attachScreenTracksToPeer(peerData, peerId, this.activeScreenPreset);
|
||||
}
|
||||
|
||||
stopScreenShareForPeer(peerId: string): void {
|
||||
this.requestedViewerPeerIds.delete(peerId);
|
||||
|
||||
const peerData = this.callbacks.getActivePeers().get(peerId);
|
||||
|
||||
if (!peerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.detachScreenTracksFromPeer(peerData, peerId);
|
||||
}
|
||||
|
||||
clearScreenShareRequest(peerId: string): void {
|
||||
this.requestedViewerPeerIds.delete(peerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach the current screen-share tracks to a newly-connected peer.
|
||||
*
|
||||
* This is needed when a peer connects after screen sharing already started,
|
||||
* because `startScreenShare()` only pushes tracks to peers that existed at
|
||||
* the time sharing began.
|
||||
*/
|
||||
syncScreenShareToPeer(peerId: string): void {
|
||||
if (
|
||||
!this.requestedViewerPeerIds.has(peerId)
|
||||
|| !this.isScreenActive
|
||||
|| !this.activeScreenStream
|
||||
|| !this.activeScreenPreset
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const peerData = this.callbacks.getActivePeers().get(peerId);
|
||||
|
||||
if (!peerData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attachScreenTracksToPeer(peerData, peerId, this.activeScreenPreset);
|
||||
}
|
||||
|
||||
/** Clean up all resources. */
|
||||
destroy(): void {
|
||||
this.stopScreenShare();
|
||||
}
|
||||
|
||||
private getElectronApi(): ScreenShareElectronApi | null {
|
||||
return getElectronApi();
|
||||
}
|
||||
|
||||
private publishLocalScreenShareState(
|
||||
includeSystemAudio: boolean,
|
||||
captureMethod: ScreenShareCaptureMethod | null
|
||||
): void {
|
||||
this.callbacks.updateLocalScreenShareState?.({
|
||||
active: this.isScreenActive,
|
||||
captureMethod: this.isScreenActive ? captureMethod : null,
|
||||
includeSystemAudio: this.isScreenActive ? includeSystemAudio : false,
|
||||
stream: this.isScreenActive ? this.activeScreenStream : null,
|
||||
suppressRemotePlayback: this.isScreenActive
|
||||
&& this.desktopElectronScreenShareCapture.shouldSuppressRemotePlaybackDuringShare(
|
||||
includeSystemAudio,
|
||||
captureMethod === 'electron-desktop'
|
||||
),
|
||||
forceDefaultRemotePlaybackOutput: this.isScreenActive
|
||||
&& includeSystemAudio
|
||||
&& captureMethod === 'linux-electron'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dedicated stream for system audio captured alongside the screen.
|
||||
*
|
||||
* @param includeSystemAudio - Whether system audio should be sent.
|
||||
*/
|
||||
private prepareScreenAudio(includeSystemAudio: boolean): void {
|
||||
const screenAudioTrack = includeSystemAudio ? (this.activeScreenStream?.getAudioTracks()[0] || null) : null;
|
||||
|
||||
if (!screenAudioTrack) {
|
||||
if (includeSystemAudio) {
|
||||
this.logger.warn('System audio was requested, but no screen audio track was captured');
|
||||
}
|
||||
|
||||
this.screenAudioStream = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.screenAudioStream = new MediaStream([screenAudioTrack]);
|
||||
this.logger.attachTrackDiagnostics(screenAudioTrack, 'screenAudio');
|
||||
this.logger.logStream('screenAudio', this.screenAudioStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach screen video and optional system-audio tracks to all
|
||||
* active peer connections, then trigger SDP renegotiation.
|
||||
*
|
||||
* @param options - Screen-share capture options.
|
||||
* @param preset - Selected quality preset for sender tuning.
|
||||
*/
|
||||
private attachScreenTracksToPeers(
|
||||
preset: ScreenShareQualityPreset
|
||||
): void {
|
||||
this.callbacks.getActivePeers().forEach((peerData, peerId) => {
|
||||
if (!this.requestedViewerPeerIds.has(peerId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attachScreenTracksToPeer(peerData, peerId, preset);
|
||||
});
|
||||
}
|
||||
|
||||
private attachScreenTracksToPeer(
|
||||
peerData: PeerData,
|
||||
peerId: string,
|
||||
preset: ScreenShareQualityPreset
|
||||
): void {
|
||||
if (!this.activeScreenStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const screenVideoTrack = this.activeScreenStream.getVideoTracks()[0];
|
||||
|
||||
if (!screenVideoTrack) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.attachTrackDiagnostics(screenVideoTrack, `screenVideo:${peerId}`);
|
||||
|
||||
let videoSender = peerData.videoSender || peerData.connection.getSenders().find((sender) => sender.track?.kind === TRACK_KIND_VIDEO);
|
||||
|
||||
if (!videoSender) {
|
||||
const videoTransceiver = peerData.connection.addTransceiver(TRACK_KIND_VIDEO, {
|
||||
direction: TRANSCEIVER_SEND_RECV
|
||||
});
|
||||
|
||||
videoSender = videoTransceiver.sender;
|
||||
peerData.videoSender = videoSender;
|
||||
} else {
|
||||
const videoTransceiver = peerData.connection.getTransceivers().find(
|
||||
(transceiver) => transceiver.sender === videoSender
|
||||
);
|
||||
|
||||
if (videoTransceiver?.direction === TRANSCEIVER_RECV_ONLY) {
|
||||
videoTransceiver.direction = TRANSCEIVER_SEND_RECV;
|
||||
}
|
||||
}
|
||||
|
||||
peerData.screenVideoSender = videoSender;
|
||||
|
||||
if (typeof videoSender.setStreams === 'function') {
|
||||
videoSender.setStreams(this.activeScreenStream);
|
||||
}
|
||||
|
||||
videoSender.replaceTrack(screenVideoTrack)
|
||||
.then(() => {
|
||||
this.logger.info('screen video replaceTrack ok', { peerId });
|
||||
void this.applyScreenShareVideoParameters(videoSender, preset, peerId);
|
||||
})
|
||||
.catch((error) => this.logger.error('screen video replaceTrack failed', error));
|
||||
|
||||
const screenAudioTrack = this.screenAudioStream?.getAudioTracks()[0] || null;
|
||||
|
||||
if (screenAudioTrack) {
|
||||
this.logger.attachTrackDiagnostics(screenAudioTrack, `screenAudio:${peerId}`);
|
||||
let screenAudioSender = peerData.screenAudioSender;
|
||||
|
||||
if (!screenAudioSender) {
|
||||
const screenAudioTransceiver = peerData.connection.addTransceiver(TRACK_KIND_AUDIO, {
|
||||
direction: TRANSCEIVER_SEND_RECV
|
||||
});
|
||||
|
||||
screenAudioSender = screenAudioTransceiver.sender;
|
||||
} else {
|
||||
const screenAudioTransceiver = peerData.connection.getTransceivers().find(
|
||||
(transceiver) => transceiver.sender === screenAudioSender
|
||||
);
|
||||
|
||||
if (screenAudioTransceiver?.direction === TRANSCEIVER_RECV_ONLY) {
|
||||
screenAudioTransceiver.direction = TRANSCEIVER_SEND_RECV;
|
||||
}
|
||||
}
|
||||
|
||||
peerData.screenAudioSender = screenAudioSender;
|
||||
|
||||
if (typeof screenAudioSender.setStreams === 'function') {
|
||||
screenAudioSender.setStreams(this.activeScreenStream);
|
||||
}
|
||||
|
||||
screenAudioSender.replaceTrack(screenAudioTrack)
|
||||
.then(() => this.logger.info('screen audio replaceTrack ok', { peerId }))
|
||||
.catch((error) => this.logger.error('screen audio replaceTrack failed', error));
|
||||
}
|
||||
|
||||
this.callbacks.renegotiate(peerId);
|
||||
}
|
||||
|
||||
private detachScreenTracksFromPeer(peerData: PeerData, peerId: string): void {
|
||||
const transceivers = peerData.connection.getTransceivers();
|
||||
const videoTransceiver = transceivers.find(
|
||||
(transceiver) => transceiver.sender === peerData.videoSender || transceiver.sender === peerData.screenVideoSender
|
||||
);
|
||||
const screenAudioTransceiver = transceivers.find(
|
||||
(transceiver) => transceiver.sender === peerData.screenAudioSender
|
||||
);
|
||||
|
||||
if (videoTransceiver) {
|
||||
videoTransceiver.sender.replaceTrack(null).catch((error) => {
|
||||
this.logger.error('Failed to clear screen video sender track', error, { peerId });
|
||||
});
|
||||
|
||||
if (videoTransceiver.direction === TRANSCEIVER_SEND_RECV) {
|
||||
videoTransceiver.direction = TRANSCEIVER_RECV_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
if (screenAudioTransceiver) {
|
||||
screenAudioTransceiver.sender.replaceTrack(null).catch((error) => {
|
||||
this.logger.error('Failed to clear screen audio sender track', error, { peerId });
|
||||
});
|
||||
|
||||
if (screenAudioTransceiver.direction === TRANSCEIVER_SEND_RECV) {
|
||||
screenAudioTransceiver.direction = TRANSCEIVER_RECV_ONLY;
|
||||
}
|
||||
}
|
||||
|
||||
peerData.screenVideoSender = undefined;
|
||||
peerData.screenAudioSender = undefined;
|
||||
|
||||
this.callbacks.renegotiate(peerId);
|
||||
}
|
||||
|
||||
private isScreenShareSelectionAborted(error: unknown): boolean {
|
||||
return error instanceof Error
|
||||
&& (error.name === 'AbortError' || error.name === 'NotAllowedError');
|
||||
}
|
||||
|
||||
private rethrowIfScreenShareAborted(error: unknown): void {
|
||||
if (this.isScreenShareSelectionAborted(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private configureScreenStream(preset: ScreenShareQualityPreset): void {
|
||||
const screenVideoTrack = this.activeScreenStream?.getVideoTracks()[0];
|
||||
|
||||
if (!screenVideoTrack) {
|
||||
throw new Error('Screen capture returned no video track.');
|
||||
}
|
||||
|
||||
if ('contentHint' in screenVideoTrack) {
|
||||
screenVideoTrack.contentHint = preset.contentHint;
|
||||
}
|
||||
|
||||
this.logger.attachTrackDiagnostics(screenVideoTrack, 'screenVideo');
|
||||
this.logger.logStream('screen', this.activeScreenStream);
|
||||
|
||||
if (typeof screenVideoTrack.applyConstraints === 'function') {
|
||||
screenVideoTrack.applyConstraints({
|
||||
width: { ideal: preset.width, max: preset.width },
|
||||
height: { ideal: preset.height, max: preset.height },
|
||||
frameRate: { ideal: preset.frameRate, max: preset.frameRate }
|
||||
}).catch((error) => {
|
||||
this.logger.warn('Failed to re-apply screen video constraints', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async applyScreenShareVideoParameters(
|
||||
sender: RTCRtpSender,
|
||||
preset: ScreenShareQualityPreset,
|
||||
peerId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
const params = sender.getParameters();
|
||||
const encodings = params.encodings?.length ? params.encodings : [{} as RTCRtpEncodingParameters];
|
||||
|
||||
params.encodings = encodings.map((encoding, index) => index === 0
|
||||
? {
|
||||
...encoding,
|
||||
maxBitrate: preset.maxBitrateBps,
|
||||
maxFramerate: preset.frameRate,
|
||||
scaleResolutionDownBy: preset.scaleResolutionDownBy ?? encoding.scaleResolutionDownBy ?? 1
|
||||
}
|
||||
: encoding);
|
||||
|
||||
(params as RTCRtpSendParameters & { degradationPreference?: string }).degradationPreference = preset.degradationPreference;
|
||||
|
||||
await sender.setParameters(params);
|
||||
this.logger.info('Applied screen-share sender parameters', {
|
||||
peerId,
|
||||
maxBitrate: preset.maxBitrateBps,
|
||||
maxFramerate: preset.frameRate
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn('Failed to apply screen-share sender parameters', error, { peerId });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user