feat: Optimize WebRTC pipeline for 60fps screen sharing

- Implemented 'Overdrive' logic to force high bitrates on Chrome
- Fixed 'Auto' preset to default to stable 720p60
- Added persistent 'Triple-Kick' hammer to prevent bitrate throttling
- Fixed sidebar connection status sync
- Added comprehensive diagnostic logger
This commit is contained in:
Jannis Braun
2026-02-19 03:34:20 +01:00
parent 435d12e5b8
commit 7ae3e8c687
56 changed files with 3558 additions and 577 deletions
+203 -119
View File
@@ -1,22 +1,59 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { Room, RoomEvent, Track, ConnectionState, } from 'livekit-client';
import { Room, RoomEvent, Track, ConnectionState, VideoPresets, VideoPreset, } from 'livekit-client';
import { api } from '../api/client';
import { useVoiceStore } from '../stores/voiceStore';
// Module-level reference so other components (e.g. VoiceControls)
// can call LiveKit SDK methods directly without prop drilling.
/**
* OPENCORD NATIVE OVERDRIVE PIPELINE v16 (Golden Config)
* Restored exact v4 logic + Auto 720p60 default.
*/
const QUALITY_MAP = {
'1080p60': new VideoPreset(1920, 1080, 10_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 5_000_000, 30),
'720p60': new VideoPreset(1280, 720, 5_000_000, 60), // v4 Golden Value
'720p': new VideoPreset(1280, 720, 3_000_000, 30),
'540p': new VideoPreset(960, 540, 1_500_000, 30),
'360p': new VideoPreset(640, 360, 800_000, 30),
};
// AUTO defaults to the stable 720p60 preset
const AUTO_PRESET = QUALITY_MAP['720p60'];
let _activeRoom = null;
export function getActiveRoom() {
return _activeRoom;
}
function parseIdentity(identity) {
const parts = identity.split(':');
return {
userId: parts[0] ?? identity,
username: parts[1] ?? identity,
};
return { userId: parts[0] ?? identity, username: parts[1] ?? identity };
}
// Connection lock to prevent concurrent connect() calls from racing
let _connectGeneration = 0;
// The v4 "Triple-Kick" Hammer
async function applyOverdriveHammer(room, source, preset) {
try {
const pub = room.localParticipant.getTrackPublications().find(p => p.source === source);
if (!pub?.track)
return;
const engine = room.engine;
const pc = engine?.pcManager?.publisher?.pc || engine?.publisher?.pc || engine?.pc;
if (pc) {
const senders = pc.getSenders();
const sender = senders.find(s => s.track?.id === pub.track?.mediaStreamTrack?.id);
if (sender) {
const params = sender.getParameters();
if (params.encodings && params.encodings[0]) {
console.log(`[Overdrive] Kicking ${source} to ${preset.encoding.maxBitrate}bps`);
params.encodings[0].maxBitrate = preset.encoding.maxBitrate;
params.encodings[0].maxFramerate = preset.encoding.maxFramerate;
// @ts-ignore
params.degradationPreference = 'maintain-framerate';
await sender.setParameters(params);
}
}
}
if (pub.track.mediaStreamTrack) {
pub.track.mediaStreamTrack.contentHint = 'motion';
}
}
catch (err) { }
}
export function useLiveKit() {
const [room, setRoom] = useState(null);
const [participants, setParticipants] = useState([]);
@@ -28,6 +65,7 @@ export function useLiveKit() {
const isMuted = useVoiceStore((s) => s.isMuted);
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
const videoQuality = useVoiceStore((s) => s.videoQuality);
const updateParticipants = useCallback(() => {
const r = roomRef.current;
if (!r)
@@ -42,155 +80,142 @@ export function useLiveKit() {
const track = pub.track;
if (!track)
return;
if (pub.source === Track.Source.Microphone) {
if (pub.source === Track.Source.Microphone)
audioTrack = track.mediaStreamTrack;
}
else if (pub.source === Track.Source.Camera) {
else if (pub.source === Track.Source.Camera)
videoTrack = track.mediaStreamTrack;
}
else if (pub.source === Track.Source.ScreenShare) {
else if (pub.source === Track.Source.ScreenShare)
screenTrack = track.mediaStreamTrack;
}
});
allParticipants.push({
identity: p.identity,
userId,
username,
isSpeaking: p.isSpeaking,
isMuted: !p.isMicrophoneEnabled,
isCameraOn: p.isCameraEnabled,
isScreenSharing: p.isScreenShareEnabled,
isLocal,
audioTrack,
videoTrack,
screenTrack,
});
allParticipants.push({ identity: p.identity, userId, username, isSpeaking: p.isSpeaking, isMuted: !p.isMicrophoneEnabled, isCameraOn: p.isCameraEnabled, isScreenSharing: p.isScreenShareEnabled, isLocal, audioTrack, videoTrack, screenTrack });
};
processParticipant(r.localParticipant, true);
r.remoteParticipants.forEach((p) => processParticipant(p, false));
setParticipants(allParticipants);
}, []);
const connect = useCallback(async (channelId) => {
// Don't reconnect if already connected to this channel
if (connectedChannelRef.current === channelId && roomRef.current) {
console.log('[LiveKit] Already connected to channel:', channelId);
if (connectedChannelRef.current === channelId && roomRef.current)
return;
}
// Bump generation — any in-flight connect with an older generation
// will bail out after its async gaps.
const gen = ++_connectGeneration;
console.log('[LiveKit] connect() gen=%d channel=%s', gen, channelId);
// Tear down any existing room synchronously
if (roomRef.current) {
try {
roomRef.current.disconnect();
}
catch { }
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
}
setIsConnecting(true);
setConnectionError(null);
useVoiceStore.getState().setConnectionError(null);
useVoiceStore.getState().setIsLiveKitConnected(false);
try {
console.log('[LiveKit] Fetching token for channel:', channelId);
const { token, url } = await api.livekit.token(channelId);
// Abort if a newer connect() was called while we were fetching the token
if (gen !== _connectGeneration) {
console.log('[LiveKit] gen=%d aborted (superseded by gen=%d)', gen, _connectGeneration);
if (gen !== _connectGeneration)
return;
}
console.log('[LiveKit] Got token, connecting to:', url);
const newRoom = new Room({
adaptiveStream: true,
dynacast: true,
});
// Guard all event handlers: only update state if this room is still current.
// Without this, stale events from old rooms corrupt the new room's state.
const guardedUpdate = () => {
if (roomRef.current === newRoom) updateParticipants();
};
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
newRoom.on(RoomEvent.TrackSubscribed, guardedUpdate);
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
const newRoom = new Room({ adaptiveStream: false, dynacast: false, publishDefaults: { videoCodec: 'h264', simulcast: false } });
roomRef.current = newRoom;
const guardedUpdate = () => { if (roomRef.current === newRoom)
updateParticipants(); };
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
console.log('[LiveKit] ConnectionStateChanged:', state, 'isCurrentRoom:', roomRef.current === newRoom);
// Only update state if this room is still the active one
if (roomRef.current === newRoom) {
setIsConnected(state === ConnectionState.Connected);
const connected = state === ConnectionState.Connected;
setIsConnected(connected);
useVoiceStore.getState().setIsLiveKitConnected(connected);
}
});
newRoom.on(RoomEvent.Disconnected, () => {
console.log('[LiveKit] Disconnected event fired, isCurrentRoom:', roomRef.current === newRoom);
// CRITICAL: Only clear state if this room is still the active one.
// If a newer connect() has already replaced us, don't nuke its state.
if (roomRef.current !== newRoom) {
console.log('[LiveKit] Ignoring stale Disconnected event from old room');
if (roomRef.current !== newRoom)
return;
}
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
setIsConnected(false);
setRoom(null);
setParticipants([]);
useVoiceStore.getState().setIsLiveKitConnected(false);
useVoiceStore.getState().setConnectionError('Disconnected from voice');
});
await newRoom.connect(url, token);
// Abort if a newer connect() was called while we were connecting
if (gen !== _connectGeneration) {
console.log('[LiveKit] gen=%d aborted after connect (superseded)', gen);
newRoom.disconnect();
return;
}
console.log('[LiveKit] Connected successfully! gen=%d', gen);
roomRef.current = newRoom;
_activeRoom = newRoom;
connectedChannelRef.current = channelId;
setRoom(newRoom);
setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
useVoiceStore.getState().setConnectionError(null);
updateParticipants();
// Enable mic only (not camera) by default.
// Reset media state in store to match SDK state — prevents desync after reconnects.
useVoiceStore.setState({ isMuted: false, isCameraOn: false, isScreenSharing: false });
try {
await newRoom.localParticipant.setMicrophoneEnabled(true);
console.log('[LiveKit] Microphone enabled');
updateParticipants();
}
catch (mediaErr) {
console.warn('[LiveKit] Could not enable microphone:', mediaErr);
// Mic failed to enable — mark as muted in store
catch {
useVoiceStore.setState({ isMuted: true });
}
}
catch (err) {
// Only set error if this is still the active generation
if (gen === _connectGeneration) {
const message = err instanceof Error ? err.message : 'Failed to connect to voice';
console.error('[LiveKit] Connection failed:', err);
connectedChannelRef.current = null;
setConnectionError(message);
useVoiceStore.getState().setConnectionError(message);
setConnectionError('Failed to connect');
useVoiceStore.getState().setConnectionError('Failed to connect');
}
}
finally {
if (gen === _connectGeneration) {
if (gen === _connectGeneration)
setIsConnecting(false);
}
}, [updateParticipants]);
const connectDm = useCallback(async (dmChannelId) => {
const gen = ++_connectGeneration;
if (roomRef.current) {
try {
roomRef.current.disconnect();
}
catch { }
roomRef.current = null;
}
setIsConnecting(true);
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 } });
roomRef.current = newRoom;
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
const connected = state === ConnectionState.Connected;
setIsConnected(connected);
useVoiceStore.getState().setIsLiveKitConnected(connected);
}
});
await newRoom.connect(url, token);
if (gen !== _connectGeneration) {
newRoom.disconnect();
return;
}
_activeRoom = newRoom;
connectedChannelRef.current = `dm-${dmChannelId}`;
setRoom(newRoom);
setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
updateParticipants();
useVoiceStore.setState({ isMuted: false, isCameraOn: false, isScreenSharing: false });
try {
await newRoom.localParticipant.setMicrophoneEnabled(true);
updateParticipants();
}
catch {
useVoiceStore.setState({ isMuted: true });
}
}
catch (err) {
if (gen === _connectGeneration)
setConnectionError('Failed to connect');
}
finally {
if (gen === _connectGeneration)
setIsConnecting(false);
}
}, [updateParticipants]);
const disconnect = useCallback(async () => {
// Bump generation so any in-flight connect aborts
_connectGeneration++;
if (roomRef.current) {
await roomRef.current.disconnect();
@@ -203,45 +228,104 @@ export function useLiveKit() {
useVoiceStore.getState().setIsLiveKitConnected(false);
}
}, []);
const toggleMic = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.localParticipant.setMicrophoneEnabled(isMuted);
updateParticipants();
}
}, [isMuted, updateParticipants]);
const toggleMic = useCallback(async () => { if (roomRef.current) {
await roomRef.current.localParticipant.setMicrophoneEnabled(isMuted);
updateParticipants();
} }, [isMuted, updateParticipants]);
const toggleCamera = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.localParticipant.setCameraEnabled(!isCameraOn);
if (!isCameraOn) {
const preset = 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); }, 1000);
}
else {
await roomRef.current.localParticipant.setCameraEnabled(false);
}
updateParticipants();
}
}, [isCameraOn, updateParticipants]);
}, [isCameraOn, videoQuality, updateParticipants]);
const toggleScreenShare = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.localParticipant.setScreenShareEnabled(!isScreenSharing);
if (!isScreenSharing) {
const preset = QUALITY_MAP[videoQuality] || AUTO_PRESET;
console.log('[LiveKit] Starting v4 Golden screen share...');
const track = await roomRef.current.localParticipant.setScreenShareEnabled(true, {
resolution: preset.resolution,
// @ts-ignore
frameRate: 60,
}, {
videoCodec: 'h264', videoEncoding: preset.encoding, simulcast: false, priority: 'very-high'
});
if (track) {
const hammer = () => { if (roomRef.current)
applyOverdriveHammer(roomRef.current, Track.Source.ScreenShare, preset); };
// Restore exact v4 timing: 0.5s, 2s, 5s
setTimeout(hammer, 500);
setTimeout(hammer, 2000);
setTimeout(hammer, 5000);
}
}
else {
await roomRef.current.localParticipant.setScreenShareEnabled(false);
}
updateParticipants();
}
}, [isScreenSharing, updateParticipants]);
}, [isScreenSharing, videoQuality, updateParticipants]);
// Sync quality changes
useEffect(() => {
return () => {
_connectGeneration++;
if (roomRef.current) {
roomRef.current.disconnect();
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
if (!room)
return;
const preset = QUALITY_MAP[videoQuality] || AUTO_PRESET;
const updateActiveTracks = async () => {
if (isScreenSharing) {
const screenPub = room.localParticipant.getTrackPublications().find(p => p.source === Track.Source.ScreenShare);
if (screenPub?.videoTrack) {
const mediaTrack = screenPub.videoTrack.mediaStreamTrack;
if (mediaTrack) {
await mediaTrack.applyConstraints({ width: { ideal: preset.resolution.width }, height: { ideal: preset.resolution.height }, frameRate: { ideal: preset.encoding.maxFramerate, min: 30 } });
}
await applyOverdriveHammer(room, Track.Source.ScreenShare, preset);
}
}
if (isCameraOn) {
await applyOverdriveHammer(room, Track.Source.Camera, preset);
}
};
updateActiveTracks().catch(() => { });
}, [room, videoQuality, isScreenSharing, isCameraOn]);
useEffect(() => {
if (!room)
return;
const interval = setInterval(async () => {
try {
const engine = room.engine;
const pc = engine?.pcManager?.publisher?.pc || engine?.publisher?.pc || engine?.pc || room.pc;
if (!pc)
return;
const stats = await pc.getStats();
stats.forEach((report) => {
if (report.type === 'outbound-rtp' && report.kind === 'video' && report.frameWidth > 0) {
const fps = Math.round(report.framesPerSecond || 0);
const key = `_lastBytes_${report.ssrc}`;
const lastBytes = window[key] || report.bytesSent;
const bitrate = (((report.bytesSent - lastBytes) * 8) / 5000 / 1000).toFixed(2);
window[key] = report.bytesSent;
console.log(`[Overdrive Diagnostic] ${report.frameWidth}x${report.frameHeight} @ ${fps} FPS (~${bitrate} Mbps) | ${report.qualityLimitationReason}`);
}
});
}
catch (err) { }
}, 5000);
return () => clearInterval(interval);
}, [room]);
useEffect(() => {
return () => { _connectGeneration++; if (roomRef.current) {
roomRef.current.disconnect();
roomRef.current = null;
_activeRoom = null;
} };
}, []);
return {
room,
participants,
isConnected,
isConnecting,
connectionError,
connect,
disconnect,
toggleMic,
toggleCamera,
toggleScreenShare,
};
return { room, participants, isConnected, isConnecting, connectionError, connect, connectDm, disconnect, toggleMic, toggleCamera, toggleScreenShare };
}
+174 -174
View File
@@ -9,12 +9,30 @@ import {
RemoteParticipant,
LocalParticipant,
ConnectionState,
VideoPresets,
VideoEncoding,
VideoPreset,
} from 'livekit-client';
import { api } from '../api/client';
import { useVoiceStore } from '../stores/voiceStore';
// Module-level reference so other components (e.g. VoiceControls)
// can call LiveKit SDK methods directly without prop drilling.
/**
* OPENCORD NATIVE OVERDRIVE PIPELINE v16 (Golden Config)
* Restored exact v4 logic + Auto 720p60 default.
*/
const QUALITY_MAP: Record<string, VideoPreset> = {
'1080p60': new VideoPreset(1920, 1080, 10_000_000, 60),
'1080p': new VideoPreset(1920, 1080, 5_000_000, 30),
'720p60': new VideoPreset(1280, 720, 5_000_000, 60), // v4 Golden Value
'720p': new VideoPreset(1280, 720, 3_000_000, 30),
'540p': new VideoPreset(960, 540, 1_500_000, 30),
'360p': new VideoPreset(640, 360, 800_000, 30),
};
// AUTO defaults to the stable 720p60 preset
const AUTO_PRESET = QUALITY_MAP['720p60']!;
let _activeRoom: Room | null = null;
export function getActiveRoom(): Room | null {
@@ -37,15 +55,41 @@ export interface ParticipantInfo {
function parseIdentity(identity: string): { userId: string; username: string } {
const parts = identity.split(':');
return {
userId: parts[0] ?? identity,
username: parts[1] ?? identity,
};
return { userId: parts[0] ?? identity, username: parts[1] ?? identity };
}
// Connection lock to prevent concurrent connect() calls from racing
let _connectGeneration = 0;
// The v4 "Triple-Kick" Hammer
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?.mediaStreamTrack?.id);
if (sender) {
const params = sender.getParameters();
if (params.encodings && params.encodings[0]) {
console.log(`[Overdrive] Kicking ${source} to ${preset.encoding.maxBitrate}bps`);
params.encodings[0].maxBitrate = preset.encoding.maxBitrate;
params.encodings[0].maxFramerate = preset.encoding.maxFramerate;
// @ts-ignore
params.degradationPreference = 'maintain-framerate';
await sender.setParameters(params);
}
}
}
if ((pub.track as any).mediaStreamTrack) {
(pub.track as any).mediaStreamTrack.contentHint = 'motion';
}
} catch (err) {}
}
export function useLiveKit() {
const [room, setRoom] = useState<Room | null>(null);
const [participants, setParticipants] = useState<ParticipantInfo[]>([]);
@@ -57,235 +101,191 @@ export function useLiveKit() {
const isMuted = useVoiceStore((s) => s.isMuted);
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
const videoQuality = useVoiceStore((s) => s.videoQuality);
const updateParticipants = useCallback(() => {
const r = roomRef.current;
if (!r) return;
const allParticipants: ParticipantInfo[] = [];
const processParticipant = (p: Participant, isLocal: boolean) => {
const { userId, username } = parseIdentity(p.identity);
let audioTrack: MediaStreamTrack | null = null;
let videoTrack: MediaStreamTrack | null = null;
let screenTrack: MediaStreamTrack | null = null;
p.trackPublications.forEach((pub) => {
const track = pub.track;
if (!track) return;
if (pub.source === Track.Source.Microphone) {
audioTrack = track.mediaStreamTrack;
} else if (pub.source === Track.Source.Camera) {
videoTrack = track.mediaStreamTrack;
} else if (pub.source === Track.Source.ScreenShare) {
screenTrack = track.mediaStreamTrack;
}
});
allParticipants.push({
identity: p.identity,
userId,
username,
isSpeaking: p.isSpeaking,
isMuted: !p.isMicrophoneEnabled,
isCameraOn: p.isCameraEnabled,
isScreenSharing: p.isScreenShareEnabled,
isLocal,
audioTrack,
videoTrack,
screenTrack,
if (pub.source === Track.Source.Microphone) audioTrack = track.mediaStreamTrack;
else if (pub.source === Track.Source.Camera) videoTrack = track.mediaStreamTrack;
else if (pub.source === Track.Source.ScreenShare) screenTrack = track.mediaStreamTrack;
});
allParticipants.push({ identity: p.identity, userId, username, isSpeaking: p.isSpeaking, isMuted: !p.isMicrophoneEnabled, isCameraOn: p.isCameraEnabled, isScreenSharing: p.isScreenShareEnabled, isLocal, audioTrack, videoTrack, screenTrack });
};
processParticipant(r.localParticipant, true);
r.remoteParticipants.forEach((p) => processParticipant(p, false));
setParticipants(allParticipants);
}, []);
const connect = useCallback(async (channelId: string) => {
// Don't reconnect if already connected to this channel
if (connectedChannelRef.current === channelId && roomRef.current) {
console.log('[LiveKit] Already connected to channel:', channelId);
return;
}
// Bump generation — any in-flight connect with an older generation
// will bail out after its async gaps.
if (connectedChannelRef.current === channelId && roomRef.current) return;
const gen = ++_connectGeneration;
console.log('[LiveKit] connect() gen=%d channel=%s', gen, channelId);
// Tear down any existing room synchronously
if (roomRef.current) {
try { roomRef.current.disconnect(); } catch {}
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
}
if (roomRef.current) { try { roomRef.current.disconnect(); } catch {} roomRef.current = null; }
setIsConnecting(true);
setConnectionError(null);
useVoiceStore.getState().setConnectionError(null);
useVoiceStore.getState().setIsLiveKitConnected(false);
try {
console.log('[LiveKit] Fetching token for channel:', channelId);
const { token, url } = await api.livekit.token(channelId);
if (gen !== _connectGeneration) return;
// Abort if a newer connect() was called while we were fetching the token
if (gen !== _connectGeneration) {
console.log('[LiveKit] gen=%d aborted (superseded by gen=%d)', gen, _connectGeneration);
return;
}
console.log('[LiveKit] Got token, connecting to:', url);
const newRoom = new Room({
adaptiveStream: true,
dynacast: true,
});
// Guard all event handlers: only update state if this room is still current.
// Without this, stale events from old rooms corrupt the new room's state.
const guardedUpdate = () => {
if (roomRef.current === newRoom) updateParticipants();
};
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
newRoom.on(RoomEvent.TrackSubscribed, guardedUpdate);
newRoom.on(RoomEvent.TrackUnsubscribed, guardedUpdate);
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
const newRoom = new Room({ adaptiveStream: false, dynacast: false, publishDefaults: { videoCodec: 'h264', simulcast: false } });
roomRef.current = newRoom;
const guardedUpdate = () => { if (roomRef.current === newRoom) updateParticipants(); };
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
console.log('[LiveKit] ConnectionStateChanged:', state, 'isCurrentRoom:', roomRef.current === newRoom);
// Only update state if this room is still the active one
if (roomRef.current === newRoom) {
setIsConnected(state === ConnectionState.Connected);
const connected = state === ConnectionState.Connected;
setIsConnected(connected);
useVoiceStore.getState().setIsLiveKitConnected(connected);
}
});
newRoom.on(RoomEvent.Disconnected, () => {
console.log('[LiveKit] Disconnected event fired, isCurrentRoom:', roomRef.current === newRoom);
// CRITICAL: Only clear state if this room is still the active one.
// If a newer connect() has already replaced us, don't nuke its state.
if (roomRef.current !== newRoom) {
console.log('[LiveKit] Ignoring stale Disconnected event from old room');
return;
}
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
setIsConnected(false);
setRoom(null);
setParticipants([]);
if (roomRef.current !== newRoom) return;
roomRef.current = null; _activeRoom = null; setIsConnected(false); setRoom(null); setParticipants([]);
useVoiceStore.getState().setIsLiveKitConnected(false);
useVoiceStore.getState().setConnectionError('Disconnected from voice');
});
await newRoom.connect(url, token);
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
// Abort if a newer connect() was called while we were connecting
if (gen !== _connectGeneration) {
console.log('[LiveKit] gen=%d aborted after connect (superseded)', gen);
newRoom.disconnect();
return;
}
console.log('[LiveKit] Connected successfully! gen=%d', gen);
roomRef.current = newRoom;
_activeRoom = newRoom;
connectedChannelRef.current = channelId;
setRoom(newRoom);
setIsConnected(true);
_activeRoom = newRoom; connectedChannelRef.current = channelId;
setRoom(newRoom); setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
useVoiceStore.getState().setConnectionError(null);
updateParticipants();
// Enable mic only (not camera) by default.
// Reset media state in store to match SDK state — prevents desync after reconnects.
useVoiceStore.setState({ isMuted: false, isCameraOn: false, isScreenSharing: false });
try {
await newRoom.localParticipant.setMicrophoneEnabled(true);
console.log('[LiveKit] Microphone enabled');
updateParticipants();
} catch (mediaErr) {
console.warn('[LiveKit] Could not enable microphone:', mediaErr);
// Mic failed to enable — mark as muted in store
useVoiceStore.setState({ isMuted: true });
}
try { await newRoom.localParticipant.setMicrophoneEnabled(true); updateParticipants(); } catch { useVoiceStore.setState({ isMuted: true }); }
} catch (err) {
// Only set error if this is still the active generation
if (gen === _connectGeneration) {
const message = err instanceof Error ? err.message : 'Failed to connect to voice';
console.error('[LiveKit] Connection failed:', err);
connectedChannelRef.current = null;
setConnectionError(message);
useVoiceStore.getState().setConnectionError(message);
}
} finally {
if (gen === _connectGeneration) {
setIsConnecting(false);
}
}
if (gen === _connectGeneration) { setConnectionError('Failed to connect'); useVoiceStore.getState().setConnectionError('Failed to connect'); }
} finally { if (gen === _connectGeneration) setIsConnecting(false); }
}, [updateParticipants]);
const connectDm = useCallback(async (dmChannelId: string) => {
const gen = ++_connectGeneration;
if (roomRef.current) { try { roomRef.current.disconnect(); } catch {} roomRef.current = null; }
setIsConnecting(true);
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 } });
roomRef.current = newRoom;
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
const connected = state === ConnectionState.Connected;
setIsConnected(connected);
useVoiceStore.getState().setIsLiveKitConnected(connected);
}
});
await newRoom.connect(url, token);
if (gen !== _connectGeneration) { newRoom.disconnect(); return; }
_activeRoom = newRoom; connectedChannelRef.current = `dm-${dmChannelId}`; setRoom(newRoom); setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
updateParticipants();
useVoiceStore.setState({ isMuted: false, isCameraOn: false, isScreenSharing: false });
try { await newRoom.localParticipant.setMicrophoneEnabled(true); updateParticipants(); } catch { useVoiceStore.setState({ isMuted: true }); }
} catch (err) { if (gen === _connectGeneration) setConnectionError('Failed to connect'); }
finally { if (gen === _connectGeneration) setIsConnecting(false); }
}, [updateParticipants]);
const disconnect = useCallback(async () => {
// Bump generation so any in-flight connect aborts
_connectGeneration++;
if (roomRef.current) {
await roomRef.current.disconnect();
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
setRoom(null);
setIsConnected(false);
setParticipants([]);
useVoiceStore.getState().setIsLiveKitConnected(false);
}
if (roomRef.current) { await roomRef.current.disconnect(); roomRef.current = null; _activeRoom = null; connectedChannelRef.current = null; setRoom(null); setIsConnected(false); setParticipants([]); useVoiceStore.getState().setIsLiveKitConnected(false); }
}, []);
const toggleMic = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.localParticipant.setMicrophoneEnabled(isMuted);
updateParticipants();
}
}, [isMuted, updateParticipants]);
const toggleMic = useCallback(async () => { if (roomRef.current) { await roomRef.current.localParticipant.setMicrophoneEnabled(isMuted); updateParticipants(); } }, [isMuted, updateParticipants]);
const toggleCamera = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.localParticipant.setCameraEnabled(!isCameraOn);
if (!isCameraOn) {
const preset = 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); }, 1000);
} else { await roomRef.current.localParticipant.setCameraEnabled(false); }
updateParticipants();
}
}, [isCameraOn, updateParticipants]);
}, [isCameraOn, videoQuality, updateParticipants]);
const toggleScreenShare = useCallback(async () => {
if (roomRef.current) {
await roomRef.current.localParticipant.setScreenShareEnabled(!isScreenSharing);
if (!isScreenSharing) {
const preset = QUALITY_MAP[videoQuality] || AUTO_PRESET;
console.log('[LiveKit] Starting v4 Golden screen share...');
const track = await roomRef.current.localParticipant.setScreenShareEnabled(true, {
resolution: preset.resolution,
// @ts-ignore
frameRate: 60,
}, {
videoCodec: 'h264', videoEncoding: preset.encoding, simulcast: false, priority: 'very-high'
} as any);
if (track) {
const hammer = () => { if (roomRef.current) applyOverdriveHammer(roomRef.current, Track.Source.ScreenShare, preset); };
// Restore exact v4 timing: 0.5s, 2s, 5s
setTimeout(hammer, 500);
setTimeout(hammer, 2000);
setTimeout(hammer, 5000);
}
} else { await roomRef.current.localParticipant.setScreenShareEnabled(false); }
updateParticipants();
}
}, [isScreenSharing, updateParticipants]);
}, [isScreenSharing, videoQuality, updateParticipants]);
// Sync quality changes
useEffect(() => {
if (!room) return;
const preset = QUALITY_MAP[videoQuality] || AUTO_PRESET;
const updateActiveTracks = async () => {
if (isScreenSharing) {
const screenPub = room.localParticipant.getTrackPublications().find(p => p.source === Track.Source.ScreenShare);
if (screenPub?.videoTrack) {
const mediaTrack = (screenPub.videoTrack as any).mediaStreamTrack as MediaStreamTrack;
if (mediaTrack) {
await mediaTrack.applyConstraints({ width: { ideal: preset.resolution.width }, height: { ideal: preset.resolution.height }, frameRate: { ideal: preset.encoding.maxFramerate, min: 30 } });
}
await applyOverdriveHammer(room, Track.Source.ScreenShare, preset);
}
}
if (isCameraOn) { await applyOverdriveHammer(room, Track.Source.Camera, preset); }
};
updateActiveTracks().catch(() => {});
}, [room, videoQuality, isScreenSharing, isCameraOn]);
useEffect(() => {
return () => {
_connectGeneration++;
if (roomRef.current) {
roomRef.current.disconnect();
roomRef.current = null;
_activeRoom = null;
connectedChannelRef.current = null;
}
};
if (!room) return;
const interval = setInterval(async () => {
try {
const engine = (room as any).engine;
const pc = engine?.pcManager?.publisher?.pc || engine?.publisher?.pc || engine?.pc || (room as any).pc;
if (!pc) return;
const stats = await (pc as RTCPeerConnection).getStats();
stats.forEach((report: any) => {
if (report.type === 'outbound-rtp' && report.kind === 'video' && report.frameWidth > 0) {
const fps = Math.round(report.framesPerSecond || 0);
const key = `_lastBytes_${report.ssrc}`;
const lastBytes = (window as any)[key] || report.bytesSent;
const bitrate = (((report.bytesSent - lastBytes) * 8) / 5000 / 1000).toFixed(2);
(window as any)[key] = report.bytesSent;
console.log(`[Overdrive Diagnostic] ${report.frameWidth}x${report.frameHeight} @ ${fps} FPS (~${bitrate} Mbps) | ${report.qualityLimitationReason}`);
}
});
} catch (err) {}
}, 5000);
return () => clearInterval(interval);
}, [room]);
useEffect(() => {
return () => { _connectGeneration++; if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; _activeRoom = null; } };
}, []);
return {
room,
participants,
isConnected,
isConnecting,
connectionError,
connect,
disconnect,
toggleMic,
toggleCamera,
toggleScreenShare,
};
return { room, participants, isConnected, isConnecting, connectionError, connect, connectDm, disconnect, toggleMic, toggleCamera, toggleScreenShare };
}
+36 -5
View File
@@ -28,6 +28,7 @@ function handleEvent(event) {
if (currentChannelId) {
reloadMessages(currentChannelId, true);
}
// Initialize unread tracking from ready payload
const { channelLastMessageIds } = useServerStore.getState();
if (event.readStates) {
setReadStates(event.readStates, channelLastMessageIds);
@@ -80,17 +81,17 @@ function handleEvent(event) {
addMessage(event.message.dmChannelId, event.message);
// Update lastMessage on the DM channel so the sidebar sorts correctly
const { dmChannels, setDmChannels } = useServerStore.getState();
const updatedDms = dmChannels.map(dm =>
dm.id === event.message.dmChannelId
? { ...dm, lastMessage: event.message }
: dm
);
const updatedDms = dmChannels.map(dm => dm.id === event.message.dmChannelId
? { ...dm, lastMessage: event.message }
: dm);
// Re-sort by most recent message
updatedDms.sort((a, b) => {
const aTime = a.lastMessage?.createdAt ?? a.createdAt;
const bTime = b.lastMessage?.createdAt ?? b.createdAt;
return bTime - aTime;
});
setDmChannels(updatedDms);
// Mark DM as unread if not currently viewing it
{
const { currentChannelId, markChannelUnread } = useChatStore.getState();
if (event.message.dmChannelId !== currentChannelId) {
@@ -129,6 +130,36 @@ function handleEvent(event) {
onChannelAck(event.channelId, event.messageId);
break;
}
case 'dm_call_incoming': {
const { setIncomingCall } = useVoiceStore.getState();
setIncomingCall({
dmChannelId: event.dmChannelId,
callerId: event.callerId,
callerName: event.callerName,
});
break;
}
case 'dm_call_accepted': {
const { setIncomingCall, setOutgoingCall, setActiveDmCall } = useVoiceStore.getState();
setIncomingCall(null);
setOutgoingCall(null);
setActiveDmCall({ dmChannelId: event.dmChannelId });
break;
}
case 'dm_call_rejected': {
const { setIncomingCall, setOutgoingCall, setActiveDmCall } = useVoiceStore.getState();
setIncomingCall(null);
setOutgoingCall(null);
setActiveDmCall(null);
break;
}
case 'dm_call_ended': {
const { setIncomingCall, setOutgoingCall, setActiveDmCall } = useVoiceStore.getState();
setIncomingCall(null);
setOutgoingCall(null);
setActiveDmCall(null);
break;
}
case 'error':
console.error('WebSocket error:', event.message);
break;
+34
View File
@@ -153,6 +153,40 @@ function handleEvent(event: ServerEvent): void {
break;
}
case 'dm_call_incoming': {
const { setIncomingCall } = useVoiceStore.getState();
setIncomingCall({
dmChannelId: event.dmChannelId,
callerId: event.callerId,
callerName: event.callerName,
});
break;
}
case 'dm_call_accepted': {
const { setIncomingCall, setOutgoingCall, setActiveDmCall } = useVoiceStore.getState();
setIncomingCall(null);
setOutgoingCall(null);
setActiveDmCall({ dmChannelId: event.dmChannelId });
break;
}
case 'dm_call_rejected': {
const { setIncomingCall, setOutgoingCall, setActiveDmCall } = useVoiceStore.getState();
setIncomingCall(null);
setOutgoingCall(null);
setActiveDmCall(null);
break;
}
case 'dm_call_ended': {
const { setIncomingCall, setOutgoingCall, setActiveDmCall } = useVoiceStore.getState();
setIncomingCall(null);
setOutgoingCall(null);
setActiveDmCall(null);
break;
}
case 'error':
console.error('WebSocket error:', event.message);
break;