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:
@@ -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,11 +58,11 @@ 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,
|
||||
await room.localParticipant.setCameraEnabled(true,
|
||||
{ resolution: preset.resolution },
|
||||
{
|
||||
{
|
||||
videoEncoding: preset.encoding,
|
||||
simulcast: videoQuality === '1080p' || videoQuality === '720p'
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user