Screensharing rework

Split Linux screensharing audio tracks, Rework screensharing functionality and layout
This will need some refactoring soon
This commit is contained in:
2026-03-08 06:33:27 +01:00
parent d20509566d
commit 7a4c4ede8c
42 changed files with 4998 additions and 475 deletions

View File

@@ -35,6 +35,7 @@ import {
JoinedServerInfo,
VoiceStateSnapshot,
LatencyProfile,
ScreenShareStartOptions,
SIGNALING_TYPE_IDENTIFY,
SIGNALING_TYPE_JOIN_SERVER,
SIGNALING_TYPE_VIEW_SERVER,
@@ -47,6 +48,8 @@ import {
SIGNALING_TYPE_USER_JOINED,
SIGNALING_TYPE_USER_LEFT,
DEFAULT_DISPLAY_NAME,
P2P_TYPE_SCREEN_SHARE_REQUEST,
P2P_TYPE_SCREEN_SHARE_STOP,
P2P_TYPE_VOICE_STATE,
P2P_TYPE_SCREEN_STATE
} from './webrtc';
@@ -69,6 +72,9 @@ export class WebRTCService implements OnDestroy {
/** Maps each remote peer ID to the server they were discovered from. */
private readonly peerServerMap = new Map<string, string>();
private readonly serviceDestroyed$ = new Subject<void>();
private remoteScreenShareRequestsEnabled = false;
private readonly desiredRemoteScreenSharePeers = new Set<string>();
private readonly activeRemoteScreenSharePeers = new Set<string>();
private readonly _localPeerId = signal<string>(uuidv4());
private readonly _isSignalingConnected = signal(false);
@@ -204,11 +210,37 @@ export class WebRTCService implements OnDestroy {
// Signaling → heartbeat → broadcast states
this.signalingManager.heartbeatTick$.subscribe(() => this.peerManager.broadcastCurrentStates());
// Internal control-plane messages for on-demand screen-share delivery.
this.peerManager.messageReceived$.subscribe((event) => this.handlePeerControlMessage(event));
// Peer manager → connected peers signal
this.peerManager.connectedPeersChanged$.subscribe((peers: string[]) =>
this._connectedPeers.set(peers)
);
// If we are already sharing when a new peer connection finishes, push the
// current screen-share tracks to that peer and renegotiate.
this.peerManager.peerConnected$.subscribe((peerId) => {
if (!this.screenShareManager.getIsScreenActive()) {
if (this.remoteScreenShareRequestsEnabled && this.desiredRemoteScreenSharePeers.has(peerId)) {
this.requestRemoteScreenShares([peerId]);
}
return;
}
this.screenShareManager.syncScreenShareToPeer(peerId);
if (this.remoteScreenShareRequestsEnabled && this.desiredRemoteScreenSharePeers.has(peerId)) {
this.requestRemoteScreenShares([peerId]);
}
});
this.peerManager.peerDisconnected$.subscribe((peerId) => {
this.activeRemoteScreenSharePeers.delete(peerId);
this.screenShareManager.clearScreenShareRequest(peerId);
});
// Media manager → voice connected signal
this.mediaManager.voiceConnected$.subscribe(() => {
this._isVoiceConnected.set(true);
@@ -544,6 +576,31 @@ export class WebRTCService implements OnDestroy {
this.peerManager.sendToPeer(peerId, event);
}
syncRemoteScreenShareRequests(peerIds: string[], enabled: boolean): void {
const nextDesiredPeers = new Set(
peerIds.filter((peerId): peerId is string => !!peerId)
);
if (!enabled) {
this.remoteScreenShareRequestsEnabled = false;
this.desiredRemoteScreenSharePeers.clear();
this.stopRemoteScreenShares([...this.activeRemoteScreenSharePeers]);
return;
}
this.remoteScreenShareRequestsEnabled = true;
for (const activePeerId of [...this.activeRemoteScreenSharePeers]) {
if (!nextDesiredPeers.has(activePeerId)) {
this.stopRemoteScreenShares([activePeerId]);
}
}
this.desiredRemoteScreenSharePeers.clear();
nextDesiredPeers.forEach((peerId) => this.desiredRemoteScreenSharePeers.add(peerId));
this.requestRemoteScreenShares([...nextDesiredPeers]);
}
/**
* Send a {@link ChatEvent} to a peer with back-pressure awareness.
*
@@ -569,6 +626,29 @@ export class WebRTCService implements OnDestroy {
return this.peerManager.remotePeerStreams.get(peerId) ?? null;
}
/**
* Get the remote voice-only stream for a connected peer.
*
* @param peerId - The remote peer whose voice stream to retrieve.
* @returns The stream, or `null` if the peer has no active voice audio.
*/
getRemoteVoiceStream(peerId: string): MediaStream | null {
return this.peerManager.remotePeerVoiceStreams.get(peerId) ?? null;
}
/**
* Get the remote screen-share stream for a connected peer.
*
* This contains the screen video track and any audio track that belongs to
* the screen share itself, not the peer's normal voice-chat audio.
*
* @param peerId - The remote peer whose screen-share stream to retrieve.
* @returns The stream, or `null` if the peer has no active screen share.
*/
getRemoteScreenShareStream(peerId: string): MediaStream | null {
return this.peerManager.remotePeerScreenShareStreams.get(peerId) ?? null;
}
/**
* Get the current local media stream (microphone audio).
*
@@ -715,11 +795,11 @@ export class WebRTCService implements OnDestroy {
/**
* Start sharing the screen (or a window) with all connected peers.
*
* @param includeAudio - Whether to capture and mix system audio.
* @param options - Screen-share capture options.
* @returns The screen-capture {@link MediaStream}.
*/
async startScreenShare(includeAudio = false): Promise<MediaStream> {
const stream = await this.screenShareManager.startScreenShare(includeAudio);
async startScreenShare(options: ScreenShareStartOptions): Promise<MediaStream> {
const stream = await this.screenShareManager.startScreenShare(options);
this._isScreenSharing.set(true);
this._screenStreamSignal.set(stream);
@@ -755,6 +835,9 @@ export class WebRTCService implements OnDestroy {
private fullCleanup(): void {
this.voiceServerId = null;
this.peerServerMap.clear();
this.remoteScreenShareRequestsEnabled = false;
this.desiredRemoteScreenSharePeers.clear();
this.activeRemoteScreenSharePeers.clear();
this.peerManager.closeAllPeers();
this._connectedPeers.set([]);
this.mediaManager.disableVoice();
@@ -782,6 +865,50 @@ export class WebRTCService implements OnDestroy {
return connState === 'connected' && dcState === 'open';
}
private handlePeerControlMessage(event: ChatEvent): void {
if (!event.fromPeerId) {
return;
}
if (event.type === P2P_TYPE_SCREEN_SHARE_REQUEST) {
this.screenShareManager.requestScreenShareForPeer(event.fromPeerId);
return;
}
if (event.type === P2P_TYPE_SCREEN_SHARE_STOP) {
this.screenShareManager.stopScreenShareForPeer(event.fromPeerId);
}
}
private requestRemoteScreenShares(peerIds: string[]): void {
const connectedPeerIds = new Set(this.peerManager.getConnectedPeerIds());
for (const peerId of peerIds) {
if (!connectedPeerIds.has(peerId) || this.activeRemoteScreenSharePeers.has(peerId)) {
continue;
}
this.peerManager.sendToPeer(peerId, { type: P2P_TYPE_SCREEN_SHARE_REQUEST });
this.activeRemoteScreenSharePeers.add(peerId);
}
}
private stopRemoteScreenShares(peerIds: string[]): void {
const connectedPeerIds = new Set(this.peerManager.getConnectedPeerIds());
for (const peerId of peerIds) {
if (!this.activeRemoteScreenSharePeers.has(peerId)) {
continue;
}
if (connectedPeerIds.has(peerId)) {
this.peerManager.sendToPeer(peerId, { type: P2P_TYPE_SCREEN_SHARE_STOP });
}
this.activeRemoteScreenSharePeers.delete(peerId);
}
}
ngOnDestroy(): void {
this.disconnect();
this.serviceDestroyed$.complete();