fix: centralize screen share pipeline to fix 270p resolution ramp-up failure

Screen sharing was publishing at h360 (640x360) and never ramping to target
resolution due to stale closure in setTimeout, wrong initial quality anchor,
and 5 competing code paths with inconsistent bitrates.

- Create utils/screenShare.ts as single source of truth for all screen share ops
- Publish at target resolution from the start (not h360 → ramp)
- Read store at call time in timers (eliminates stale closure bug)
- Use maintain-resolution for screen content, maintain-framerate for camera
- Fix OS-level "Stop sharing" not resetting store or restoring AEC
- Enable dynacast for SFU quality signaling
- Reconcile QUALITY_MAP to canonical bitrates across all 7 files
This commit is contained in:
Jannis Braun
2026-02-23 03:25:52 +01:00
parent 82eee04946
commit 7e7de41718
7 changed files with 211 additions and 161 deletions
@@ -4,17 +4,7 @@ import { useServerStore } from '../../stores/serverStore';
import { useAuthStore } from '../../stores/authStore';
import { getActiveRoom } from '../../hooks/useLiveKit';
import { wsSend } from '../../hooks/useWebSocket';
import { AudioManager } from '../../audio/AudioManager';
import { VideoPresets, VideoPreset } from 'livekit-client';
const QUALITY_MAP: Record<string, any> = {
'1080p60': new VideoPreset(1920, 1080, 15_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 8_000_000, 30),
'720p60': new VideoPreset(1280, 720, 8_000_000, 60),
'720p': new VideoPreset(1280, 720, 5_000_000, 30),
'540p': new VideoPreset(960, 540, 2_000_000, 30),
'360p': new VideoPreset(640, 360, 1_000_000, 30),
};
import { SCREEN_QUALITY_MAP, startScreenShare, stopScreenShare } from '../../utils/screenShare';
export function DmCallView() {
const activeDmCall = useVoiceStore((s) => s.activeDmCall);
@@ -26,7 +16,6 @@ export function DmCallView() {
const toggleMic = useVoiceStore((s) => s.toggleMic);
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
const setActiveDmCall = useVoiceStore((s) => s.setActiveDmCall);
const leaveVoice = useVoiceStore((s) => s.leaveVoice);
const speakingParticipantIds = useVoiceStore((s) => s.speakingParticipantIds);
@@ -69,7 +58,7 @@ export function DmCallView() {
const willEnable = !isCameraOn;
if (willEnable) {
const videoQuality = useVoiceStore.getState().videoQuality;
const preset = QUALITY_MAP[videoQuality];
const preset = SCREEN_QUALITY_MAP[videoQuality];
if (preset) {
await room.localParticipant.setCameraEnabled(true,
{ resolution: preset.resolution },
@@ -93,13 +82,10 @@ export function DmCallView() {
if (!room) return;
try {
if (!isScreenSharing) {
AudioManager.getInstance().setScreenShareActive(true);
await room.localParticipant.setScreenShareEnabled(true, { audio: true });
await startScreenShare(room);
} else {
await room.localParticipant.setScreenShareEnabled(false);
AudioManager.getInstance().setScreenShareActive(false);
await stopScreenShare(room);
}
toggleScreenShare();
} catch (err) {
console.error('[DmCallView] Failed to toggle screen share:', err);
}
@@ -2,8 +2,8 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
import { Avatar } from '../ui/Avatar';
import { useVoiceStore } from '../../stores/voiceStore';
import { getActiveRoom, setStreamSubscription } from '../../hooks/useLiveKit';
import { AudioManager } from '../../audio/AudioManager';
import { VideoQualityPopover } from './VideoQualityPopover';
import { stopScreenShare, changeScreenShare } from '../../utils/screenShare';
import type { StreamTile as StreamTileType } from '../../hooks/useLiveKit';
interface StreamTileProps {
@@ -107,22 +107,14 @@ export function StreamTile({ tile, large }: StreamTileProps) {
const handleStopStreaming = useCallback(async () => {
const room = getActiveRoom();
if (room) {
await room.localParticipant.setScreenShareEnabled(false);
AudioManager.getInstance().setScreenShareActive(false);
useVoiceStore.getState().toggleScreenShare();
await stopScreenShare(room);
}
}, []);
const handleChangeStream = useCallback(async () => {
const room = getActiveRoom();
if (room) {
await room.localParticipant.setScreenShareEnabled(false);
// Small delay then re-start to re-trigger the source picker
setTimeout(async () => {
await room.localParticipant.setScreenShareEnabled(true, {
audio: true,
});
}, 200);
await changeScreenShare(room);
}
}, []);
@@ -1,7 +1,5 @@
import React, { useRef, useEffect } from 'react';
import { useVoiceStore } from '../../stores/voiceStore';
import { getActiveRoom } from '../../hooks/useLiveKit';
import { VideoPresets, VideoPreset } from 'livekit-client';
interface VideoQualityPopoverProps {
open: boolean;
@@ -10,23 +8,14 @@ interface VideoQualityPopoverProps {
}
const PRESETS = [
{ value: '1080p60' as const, label: '1080p 60fps', desc: '1920x1080, 10000 kbps' },
{ value: '1080p' as const, label: '1080p 30fps', desc: '1920x1080, 5000 kbps' },
{ value: '720p60' as const, label: '720p 60fps', desc: '1280x720, 5000 kbps' },
{ value: '720p' as const, label: '720p 30fps', desc: '1280x720, 3000 kbps' },
{ value: '540p' as const, label: '540p 30fps', desc: '960x540, 1500 kbps' },
{ value: '360p' as const, label: '360p 30fps', desc: '640x360, 800 kbps' },
{ value: '1080p60' as const, label: '1080p 60fps', desc: '1920x1080, 12000 kbps' },
{ value: '1080p' as const, label: '1080p 30fps', desc: '1920x1080, 8000 kbps' },
{ value: '720p60' as const, label: '720p 60fps', desc: '1280x720, 8000 kbps' },
{ value: '720p' as const, label: '720p 30fps', desc: '1280x720, 5000 kbps' },
{ value: '540p' as const, label: '540p 30fps', desc: '960x540, 2000 kbps' },
{ value: '360p' as const, label: '360p 30fps', desc: '640x360, 1000 kbps' },
] as const;
const QUALITY_MAP: Record<string, VideoPreset> = {
'1080p60': new VideoPreset(1920, 1080, 15_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 8_000_000, 30),
'720p60': new VideoPreset(1280, 720, 8_000_000, 60),
'720p': new VideoPreset(1280, 720, 5_000_000, 30),
'540p': new VideoPreset(960, 540, 2_000_000, 30),
'360p': new VideoPreset(640, 360, 1_000_000, 30),
};
export function VideoQualityPopover({ open, onClose, anchorRect }: VideoQualityPopoverProps) {
const popoverRef = useRef<HTMLDivElement>(null);
const videoQuality = useVoiceStore((s) => s.videoQuality);
@@ -3,18 +3,8 @@ import { useVoiceStore } from '../../stores/voiceStore';
import { useUIStore } from '../../stores/uiStore';
import { getActiveRoom } from '../../hooks/useLiveKit';
import { wsSend } from '../../hooks/useWebSocket';
import { AudioManager } from '../../audio/AudioManager';
import { VideoQualityPopover } from './VideoQualityPopover';
import { VideoPresets, VideoPreset } from 'livekit-client';
const QUALITY_MAP: Record<string, any> = {
'1080p60': new VideoPreset(1920, 1080, 15_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 8_000_000, 30),
'720p60': new VideoPreset(1280, 720, 8_000_000, 60),
'720p': new VideoPreset(1280, 720, 5_000_000, 30),
'540p': new VideoPreset(960, 540, 2_000_000, 30),
'360p': new VideoPreset(640, 360, 1_000_000, 30),
};
import { SCREEN_QUALITY_MAP, startScreenShare, stopScreenShare } from '../../utils/screenShare';
const btnBase = 'w-10 h-10 flex items-center justify-center rounded-full transition-colors';
const btnDefault = `${btnBase} bg-[#1e1f22] text-discord-text-secondary hover:bg-[#2b2d31] hover:text-discord-text-primary`;
@@ -29,7 +19,6 @@ export function VoiceControlBar() {
const toggleMic = useVoiceStore((s) => s.toggleMic);
const toggleDeafen = useVoiceStore((s) => s.toggleDeafen);
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
const voiceChatOpen = useUIStore((s) => s.voiceChatOpen);
const toggleVoiceChat = useUIStore((s) => s.toggleVoiceChat);
const voiceFullscreen = useUIStore((s) => s.voiceFullscreen);
@@ -72,7 +61,7 @@ export function VoiceControlBar() {
const willEnable = !isCameraOn;
if (willEnable) {
const videoQuality = useVoiceStore.getState().videoQuality;
const preset = QUALITY_MAP[videoQuality];
const preset = SCREEN_QUALITY_MAP[videoQuality];
if (preset) {
await room.localParticipant.setCameraEnabled(true,
{ resolution: preset.resolution },
@@ -98,13 +87,10 @@ export function VoiceControlBar() {
if (!room) return;
try {
if (!isScreenSharing) {
AudioManager.getInstance().setScreenShareActive(true);
await room.localParticipant.setScreenShareEnabled(true, { audio: true });
await startScreenShare(room);
} else {
await room.localParticipant.setScreenShareEnabled(false);
AudioManager.getInstance().setScreenShareActive(false);
await stopScreenShare(room);
}
toggleScreenShare();
} catch (err) {
console.error('[VoiceControlBar] Failed to toggle screen share:', err);
}
@@ -3,9 +3,9 @@ import { useVoiceStore } from '../../stores/voiceStore';
import { useServerStore } from '../../stores/serverStore';
import { getActiveRoom } from '../../hooks/useLiveKit';
import { wsSend } from '../../hooks/useWebSocket';
import { AudioManager } from '../../audio/AudioManager';
import { VideoQualityPopover } from './VideoQualityPopover';
import { ConnectionInfoPopover } from './ConnectionInfoPopover';
import { startScreenShare, stopScreenShare } from '../../utils/screenShare';
/**
* VoiceControls renders the voice status + button rows.
@@ -16,7 +16,6 @@ export function VoiceControls() {
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
const toggleCamera = useVoiceStore((s) => s.toggleCamera);
const toggleScreenShare = useVoiceStore((s) => s.toggleScreenShare);
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
const setRnnoiseEnabled = useVoiceStore((s) => s.setRnnoiseEnabled);
const connectionError = useVoiceStore((s) => s.connectionError);
@@ -47,13 +46,10 @@ export function VoiceControls() {
if (!room) return;
try {
if (!isScreenSharing) {
AudioManager.getInstance().setScreenShareActive(true);
await room.localParticipant.setScreenShareEnabled(true, { audio: true });
await startScreenShare(room);
} else {
await room.localParticipant.setScreenShareEnabled(false);
AudioManager.getInstance().setScreenShareActive(false);
await stopScreenShare(room);
}
toggleScreenShare();
} catch (err) {
console.error('[VoiceControls] Failed to toggle screen share:', err);
}
+24 -85
View File
@@ -18,21 +18,14 @@ import { api } from '../api/client';
import { useVoiceStore } from '../stores/voiceStore';
import { AudioManager } from '../audio/AudioManager';
import { SpeakingDetector } from '../audio/SpeakingDetector';
/**
* OPENCORD NATIVE OVERDRIVE PIPELINE v33
*/
const QUALITY_MAP: Record<string, VideoPreset> = {
'1080p60': new VideoPreset(1920, 1080, 12_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 8_000_000, 30),
'720p60': new VideoPreset(1280, 720, 8_000_000, 60),
'720p': new VideoPreset(1280, 720, 4_000_000, 30),
'540p': new VideoPreset(960, 540, 2_000_000, 30),
'360p': new VideoPreset(640, 360, 1_000_000, 30),
};
const AUTO_PRESET = QUALITY_MAP['720p60']!;
import {
SCREEN_QUALITY_MAP,
AUTO_PRESET,
applyOverdrive,
startScreenShare,
stopScreenShare,
handleScreenShareUnpublished,
} from '../utils/screenShare';
let _activeRoom: Room | null = null;
@@ -114,31 +107,6 @@ function parseIdentity(identity: string): { userId: string; username: string } {
let _connectGeneration = 0;
async function applyOverdriveHammer(room: Room, source: Track.Source, preset: VideoPreset) {
try {
const pub = room.localParticipant.getTrackPublications().find(p => p.source === source);
if (!pub?.track) return;
const engine = (room as any).engine;
const pc = engine?.pcManager?.publisher?.pc || engine?.publisher?.pc || engine?.pc;
if (pc) {
const senders = (pc as RTCPeerConnection).getSenders();
const sender = senders.find(s => s.track?.id === (pub.track as any).mediaStreamTrack?.id);
if (sender) {
const params = sender.getParameters();
if (params.encodings && params.encodings[0]) {
params.encodings[0].maxBitrate = preset.encoding.maxBitrate;
(params.encodings[0] as any).minBitrate = 2_000_000;
params.encodings[0].maxFramerate = preset.encoding.maxFramerate;
params.encodings[0].networkPriority = 'high';
// @ts-ignore
params.degradationPreference = 'maintain-framerate';
await sender.setParameters(params);
}
}
}
} catch (err) {}
}
export function useLiveKit() {
const [room, setRoom] = useState<Room | null>(null);
@@ -357,11 +325,10 @@ export function useLiveKit() {
try {
const { token, url } = await api.livekit.token(channelId);
if (gen !== _connectGeneration) return;
const newRoom = new Room({ adaptiveStream: false, dynacast: false, publishDefaults: { videoCodec: 'h264', simulcast: false } });
const newRoom = new Room({ adaptiveStream: false, dynacast: true, publishDefaults: { videoCodec: 'h264', simulcast: false } });
roomRef.current = newRoom;
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
// ... existing event listeners ...
newRoom.on(RoomEvent.ParticipantConnected, (participant) => {
guardedUpdate();
if (useVoiceStore.getState().isDeafened) {
@@ -399,6 +366,8 @@ export function useLiveKit() {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
useVoiceStore.getState().unwatchStream(userId);
// OS-level "Stop sharing" fires this without going through stopScreenShare
handleScreenShareUnpublished();
}
guardedUpdate();
});
@@ -530,7 +499,7 @@ export function useLiveKit() {
try {
const { token, url } = await api.livekit.dmToken(dmChannelId);
if (gen !== _connectGeneration) return;
const newRoom = new Room({ adaptiveStream: false, dynacast: false, publishDefaults: { videoCodec: 'h264', simulcast: false } });
const newRoom = new Room({ adaptiveStream: false, dynacast: true, publishDefaults: { videoCodec: 'h264', simulcast: false } });
roomRef.current = newRoom;
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
@@ -558,6 +527,8 @@ export function useLiveKit() {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
useVoiceStore.getState().unwatchStream(userId);
// OS-level "Stop sharing" fires this without going through stopScreenShare
handleScreenShareUnpublished();
}
guardedUpdate();
});
@@ -664,55 +635,23 @@ export function useLiveKit() {
const toggleCamera = useCallback(async () => {
if (roomRef.current) {
if (!isCameraOn) {
const preset = QUALITY_MAP[videoQuality] || VideoPresets.h720;
const preset = SCREEN_QUALITY_MAP[videoQuality] || VideoPresets.h720;
await roomRef.current.localParticipant.setCameraEnabled(true, { resolution: preset.resolution, frameRate: preset.encoding.maxFramerate }, { videoCodec: 'h264', videoEncoding: preset.encoding, simulcast: false });
setTimeout(() => { if (roomRef.current) applyOverdriveHammer(roomRef.current, Track.Source.Camera, preset); }, 2000);
setTimeout(() => { if (roomRef.current) applyOverdrive(roomRef.current, Track.Source.Camera, preset); }, 2000);
} else { await roomRef.current.localParticipant.setCameraEnabled(false); }
updateParticipants();
}
}, [isCameraOn, videoQuality, updateParticipants]);
const toggleScreenShare = useCallback(async () => {
if (roomRef.current) {
if (!isScreenSharing) {
// Notify AudioManager BEFORE enabling screen share so the mic track
// gets republished with AEC off, preventing Chrome's ducking.
AudioManager.getInstance().setScreenShareActive(true);
const preset = QUALITY_MAP[videoQuality] || AUTO_PRESET;
const track = await roomRef.current.localParticipant.setScreenShareEnabled(true, {
audio: true,
resolution: VideoPresets.h360.resolution,
// @ts-ignore
frameRate: 30,
}, {
videoCodec: 'h264', videoEncoding: VideoPresets.h360.encoding, simulcast: false, priority: 'very-high'
} as any);
if (track) {
setTimeout(async () => {
if (roomRef.current && isScreenSharing) {
const screenPub = roomRef.current.localParticipant.getTrackPublications().find(p => p.source === Track.Source.ScreenShare);
if (screenPub?.track?.mediaStreamTrack) {
await screenPub.track.mediaStreamTrack.applyConstraints({
width: { ideal: preset.resolution.width },
height: { ideal: preset.resolution.height },
frameRate: { ideal: preset.encoding.maxFramerate, min: 30 }
});
await applyOverdriveHammer(roomRef.current, Track.Source.ScreenShare, preset);
}
}
}, 2000);
setTimeout(() => applyOverdriveHammer(roomRef.current!, Track.Source.ScreenShare, preset), 5000);
}
if (!roomRef.current) return;
if (!useVoiceStore.getState().isScreenSharing) {
await startScreenShare(roomRef.current);
} else {
await roomRef.current.localParticipant.setScreenShareEnabled(false);
// Restore user's AEC preference after screen share ends
AudioManager.getInstance().setScreenShareActive(false);
await stopScreenShare(roomRef.current);
}
updateParticipants();
}
}, [isScreenSharing, videoQuality, updateParticipants]);
}, [updateParticipants]);
useEffect(() => {
updateParticipants();
@@ -720,7 +659,7 @@ export function useLiveKit() {
useEffect(() => {
if (!room) return;
const preset = QUALITY_MAP[videoQuality] || AUTO_PRESET;
const preset = SCREEN_QUALITY_MAP[videoQuality] || AUTO_PRESET;
const updateActiveTracks = async () => {
if (isScreenSharing) {
const screenPub = room.localParticipant.getTrackPublications().find(p => p.source === Track.Source.ScreenShare);
@@ -729,10 +668,10 @@ export function useLiveKit() {
if (mediaTrack) {
await mediaTrack.applyConstraints({ width: { ideal: preset.resolution.width }, height: { ideal: preset.resolution.height }, frameRate: { ideal: preset.encoding.maxFramerate } });
}
await applyOverdriveHammer(room, Track.Source.ScreenShare, preset);
await applyOverdrive(room, Track.Source.ScreenShare, preset);
}
}
if (isCameraOn) { await applyOverdriveHammer(room, Track.Source.Camera, preset); }
if (isCameraOn) { await applyOverdrive(room, Track.Source.Camera, preset); }
};
updateActiveTracks().catch(() => {});
}, [room, videoQuality, isScreenSharing, isCameraOn]);
+162
View File
@@ -0,0 +1,162 @@
import { Room, Track, VideoPreset } from 'livekit-client';
import { useVoiceStore } from '../stores/voiceStore';
import { AudioManager } from '../audio/AudioManager';
/**
* Canonical quality presets — single source of truth.
* Used by all screen share entry points, camera controls, and VideoQualityPopover.
*/
export const SCREEN_QUALITY_MAP: Record<string, VideoPreset> = {
'1080p60': new VideoPreset(1920, 1080, 12_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 8_000_000, 30),
'720p60': new VideoPreset(1280, 720, 8_000_000, 60),
'720p': new VideoPreset(1280, 720, 5_000_000, 30),
'540p': new VideoPreset(960, 540, 2_000_000, 30),
'360p': new VideoPreset(640, 360, 1_000_000, 30),
};
export const AUTO_PRESET = SCREEN_QUALITY_MAP['720p60']!;
/**
* Apply overdrive hammer to a published track — forces bitrate/resolution/framerate
* directly on the RTCRtpSender, bypassing LiveKit's conservative defaults.
*
* Screen share: uses 'maintain-resolution' (text/code readability matters more than fps)
* Camera: uses 'maintain-framerate' (smooth motion matters more than sharpness)
*/
export async function applyOverdrive(
room: Room,
source: Track.Source,
preset: VideoPreset,
): Promise<void> {
try {
const pub = room.localParticipant.getTrackPublications().find(p => p.source === source);
if (!pub?.track) return;
const engine = (room as any).engine;
const pc = engine?.pcManager?.publisher?.pc || engine?.publisher?.pc || engine?.pc;
if (!pc) return;
const senders = (pc as RTCPeerConnection).getSenders();
const sender = senders.find(s => s.track?.id === (pub.track as any).mediaStreamTrack?.id);
if (!sender) return;
const params = sender.getParameters();
if (!params.encodings?.[0]) return;
params.encodings[0].maxBitrate = preset.encoding.maxBitrate;
params.encodings[0].maxFramerate = preset.encoding.maxFramerate;
params.encodings[0].networkPriority = 'high';
const isScreenShare = source === Track.Source.ScreenShare;
if (isScreenShare) {
(params as any).degradationPreference = 'maintain-resolution';
(params.encodings[0] as any).minBitrate = 2_000_000;
} else {
(params as any).degradationPreference = 'maintain-framerate';
}
await sender.setParameters(params);
} catch (err) {
console.warn('[ScreenShare] Failed to apply overdrive:', err);
}
}
/**
* Start screen sharing at target quality from the beginning.
* Reads videoQuality from store at call time — no stale closures.
*/
export async function startScreenShare(room: Room): Promise<boolean> {
const { videoQuality } = useVoiceStore.getState();
const preset = SCREEN_QUALITY_MAP[videoQuality] || AUTO_PRESET;
// Notify AudioManager BEFORE enabling screen share so the mic track
// gets republished with AEC off, preventing Chrome's ducking.
AudioManager.getInstance().setScreenShareActive(true);
try {
const track = await room.localParticipant.setScreenShareEnabled(true, {
audio: true,
resolution: preset.resolution,
// @ts-ignore — LiveKit accepts frameRate at capture level
frameRate: preset.encoding.maxFramerate,
}, {
videoCodec: 'h264',
videoEncoding: preset.encoding,
simulcast: false,
} as any);
if (!track) {
// User cancelled the screen picker
AudioManager.getInstance().setScreenShareActive(false);
return false;
}
// Update store — screen share is now active
useVoiceStore.setState({ isScreenSharing: true });
// Overdrive at 2s — after WebRTC finishes negotiation
setTimeout(async () => {
// Read state fresh at timer fire — no stale closure
if (!useVoiceStore.getState().isScreenSharing) return;
const currentPreset = SCREEN_QUALITY_MAP[useVoiceStore.getState().videoQuality] || AUTO_PRESET;
const screenPub = room.localParticipant.getTrackPublications()
.find(p => p.source === Track.Source.ScreenShare);
if (screenPub?.track?.mediaStreamTrack) {
await screenPub.track.mediaStreamTrack.applyConstraints({
width: { ideal: currentPreset.resolution.width },
height: { ideal: currentPreset.resolution.height },
frameRate: { ideal: currentPreset.encoding.maxFramerate, min: 15 },
});
}
await applyOverdrive(room, Track.Source.ScreenShare, currentPreset);
}, 2000);
// Second overdrive at 5s — safety net for slow BWE convergence
setTimeout(async () => {
if (!useVoiceStore.getState().isScreenSharing) return;
const currentPreset = SCREEN_QUALITY_MAP[useVoiceStore.getState().videoQuality] || AUTO_PRESET;
await applyOverdrive(room, Track.Source.ScreenShare, currentPreset);
}, 5000);
return true;
} catch (err) {
console.error('[ScreenShare] Failed to start screen share:', err);
AudioManager.getInstance().setScreenShareActive(false);
return false;
}
}
/**
* Stop screen sharing, restore AEC, reset store.
*/
export async function stopScreenShare(room: Room): Promise<void> {
try {
await room.localParticipant.setScreenShareEnabled(false);
} catch (err) {
console.error('[ScreenShare] Failed to stop screen share:', err);
}
AudioManager.getInstance().setScreenShareActive(false);
useVoiceStore.setState({ isScreenSharing: false });
}
/**
* Change the screen share source — stops current stream, re-triggers picker.
*/
export async function changeScreenShare(room: Room): Promise<void> {
await room.localParticipant.setScreenShareEnabled(false);
// Small delay then re-start to re-trigger the source picker
setTimeout(async () => {
await startScreenShare(room);
}, 200);
}
/**
* Called from LocalTrackUnpublished handler to handle OS-level "Stop sharing".
* Resets store state and restores AEC without trying to unpublish (already done).
*/
export function handleScreenShareUnpublished(): void {
AudioManager.getInstance().setScreenShareActive(false);
useVoiceStore.setState({ isScreenSharing: false });
}