chore: remove 62 tsc emit artifacts from src/, add noEmit to tsconfig

tsc was emitting compiled .js files directly into packages/web/src/
alongside the .tsx source files because noEmit was not set. These
artifacts were never used — Vite compiles from .tsx source directly.

- Add noEmit: true to packages/web/tsconfig.json (tsc = type-check only)
- Delete all 62 orphaned .js files from src/ (-6,129 lines)
- Add packages/web/src/**/*.js to .gitignore as safeguard
This commit is contained in:
Jannis Braun
2026-02-23 00:54:51 +01:00
parent 5747267a6b
commit 176f4db27e
64 changed files with 3 additions and 6129 deletions
@@ -1,88 +0,0 @@
import { useRef, useEffect } from 'react';
import { AudioManager } from '../audio/AudioManager';
/**
* Hybrid audio pipeline for a single remote audio track.
*
* Architecture:
* 1. A MUTED <audio> element keeps Chrome's WebRTC audio pipeline alive
* for the track. Chrome requires an HTML media element consuming a
* WebRTC MediaStreamTrack or it stops processing it. The element is
* always muted (volume=0, muted=true) — it never produces audible output.
*
* 2. A Web Audio pipeline handles ALL actual audio output:
* MediaStreamTrack -> MediaStream -> MediaStreamAudioSourceNode -> GainNode -> ctx.destination
*
* This gives us:
* - Chrome compatibility (muted <audio> keep-alive)
* - No ducking (all elements are muted, only Web Audio produces sound)
* - Clean mixing (single ctx.destination for all tracks)
* - Full volume range (0.0 4.0+) via GainNode
* - Smooth transitions via setTargetAtTime (no clicks/pops)
*/
export function useAudioTrackPlayer(opts) {
const { track, volume, muted } = opts;
const audioRef = useRef(null);
const sourceRef = useRef(null);
const gainRef = useRef(null);
// Keep current volume/muted in refs so Effect 1 can read them
// for the initial ramp without depending on them
const volumeRef = useRef(volume);
const mutedRef = useRef(muted);
volumeRef.current = volume;
mutedRef.current = muted;
// Effect 1: Track attachment (keep-alive) + Web Audio pipeline build
useEffect(() => {
const audioEl = audioRef.current;
// Tear down previous Web Audio graph
if (sourceRef.current) {
sourceRef.current.disconnect();
sourceRef.current = null;
}
if (gainRef.current) {
gainRef.current.disconnect();
gainRef.current = null;
}
if (!track) {
if (audioEl)
audioEl.srcObject = null;
return;
}
// --- Keep-alive: attach track to <audio> element (always muted) ---
// Chrome needs an HTML element consuming the WebRTC track or it
// stops the audio pipeline for that track entirely.
if (audioEl) {
audioEl.srcObject = new MediaStream([track]);
audioEl.muted = true;
audioEl.volume = 0;
audioEl.play().catch(() => { });
}
// --- Web Audio pipeline for actual output ---
const ctx = AudioManager.getInstance().ensureContext();
const stream = new MediaStream([track]);
const source = ctx.createMediaStreamSource(stream);
const gain = ctx.createGain();
// Start gain at 0 to prevent pop, then ramp to target
gain.gain.setValueAtTime(0, ctx.currentTime);
const targetGain = mutedRef.current ? 0 : volumeRef.current;
gain.gain.setTargetAtTime(targetGain, ctx.currentTime, 0.015);
source.connect(gain);
gain.connect(AudioManager.getInstance().getMasterOutput());
sourceRef.current = source;
gainRef.current = gain;
return () => {
source.disconnect();
gain.disconnect();
sourceRef.current = null;
gainRef.current = null;
};
}, [track]);
// Effect 2: Update gain when volume or muted changes (no graph rebuild)
useEffect(() => {
if (!gainRef.current)
return;
const ctx = AudioManager.getInstance().ensureContext();
const targetGain = muted ? 0 : volume;
gainRef.current.gain.setTargetAtTime(targetGain, ctx.currentTime, 0.015);
}, [volume, muted]);
return audioRef;
}
-20
View File
@@ -1,20 +0,0 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/authStore';
export function useAuth() {
const token = useAuthStore((s) => s.token);
const user = useAuthStore((s) => s.user);
const isLoading = useAuthStore((s) => s.isLoading);
const loadUser = useAuthStore((s) => s.loadUser);
const navigate = useNavigate();
useEffect(() => {
if (!token) {
navigate('/login');
return;
}
if (!user && !isLoading) {
loadUser();
}
}, [token, user, isLoading, loadUser, navigate]);
return { user, isLoading, isAuthenticated: !!token };
}
-671
View File
@@ -1,671 +0,0 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { Room, RoomEvent, Track, ConnectionState, VideoPresets, VideoPreset, } from 'livekit-client';
import { api } from '../api/client';
import { useVoiceStore } from '../stores/voiceStore';
import { AudioManager } from '../audio/AudioManager';
/**
* OPENCORD NATIVE OVERDRIVE PIPELINE v32
*/
const QUALITY_MAP = {
'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'];
let _activeRoom = null;
export function getActiveRoom() {
return _activeRoom;
}
export function deriveGridTiles(participants) {
const tiles = [];
for (const p of participants) {
tiles.push({
kind: 'user',
key: p.identity,
participant: p,
videoTrack: (p.isCameraOn && p.videoTrack?.readyState === 'live') ? p.videoTrack : null,
audioTrack: p.audioTrack,
});
if (p.isScreenSharing) {
tiles.push({
kind: 'stream',
key: `${p.identity}:stream`,
participant: p,
screenTrack: p.screenTrack,
screenAudioTrack: p.screenAudioTrack,
});
}
}
return tiles;
}
export function setStreamSubscription(room, targetIdentity, subscribed) {
if (!room)
return;
const rp = room.remoteParticipants.get(targetIdentity);
if (!rp)
return;
rp.trackPublications.forEach((pub) => {
if (pub.source === Track.Source.ScreenShare || pub.source === Track.Source.ScreenShareAudio) {
pub.setSubscribed(subscribed);
}
});
}
function parseIdentity(identity) {
const parts = identity.split(':');
return { userId: parts[0] ?? identity, username: parts[1] ?? identity };
}
let _connectGeneration = 0;
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]) {
params.encodings[0].maxBitrate = preset.encoding.maxBitrate;
params.encodings[0].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(null);
const [participants, setParticipants] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const [connectionState, setConnectionState] = useState(ConnectionState.Disconnected);
const [connectedChannelId, setConnectedChannelId] = useState(null);
const [connectionError, setConnectionError] = useState(null);
const roomRef = useRef(null);
const connectedChannelRef = useRef(null);
const isMuted = useVoiceStore((s) => s.isMuted);
const isDeafened = useVoiceStore((s) => s.isDeafened);
const isCameraOn = useVoiceStore((s) => s.isCameraOn);
const isScreenSharing = useVoiceStore((s) => s.isScreenSharing);
const videoQuality = useVoiceStore((s) => s.videoQuality);
const voiceUserStates = useVoiceStore((s) => s.voiceUserStates);
const inputVolume = useVoiceStore((s) => s.inputVolume);
const inputDeviceId = useVoiceStore((s) => s.inputDeviceId);
const echoCancellation = useVoiceStore((s) => s.echoCancellation);
const noiseSuppression = useVoiceStore((s) => s.noiseSuppression);
const autoGainControl = useVoiceStore((s) => s.autoGainControl);
const rnnoiseEnabled = useVoiceStore((s) => s.rnnoiseEnabled);
const lastMicGenRef = useRef(0);
const updateParticipants = useCallback(() => {
const r = roomRef.current;
if (!r)
return;
const allParticipants = [];
const processParticipant = (p, isLocal) => {
if (!p.identity)
return;
const { userId, username } = parseIdentity(p.identity);
let audioTrack = null;
let videoTrack = null;
let screenTrack = null;
let screenAudioTrack = null;
let hasScreenSharePublication = false;
p.trackPublications.forEach((pub) => {
// Detect screen share publication even if unsubscribed
if (pub.source === Track.Source.ScreenShare)
hasScreenSharePublication = true;
const track = pub.track;
if (!track)
return;
// Strict check: Track must be subscribed AND not muted to be considered "active"
if (pub.isMuted)
return;
if (!isLocal && !pub.isSubscribed)
return;
const mt = track.mediaStreamTrack;
if (!mt || mt.readyState !== 'live')
return;
if (pub.source === Track.Source.Microphone)
audioTrack = mt;
else if (pub.source === Track.Source.Camera && p.isCameraEnabled)
videoTrack = mt;
else if (pub.source === Track.Source.ScreenShare)
screenTrack = mt;
else if (pub.source === Track.Source.ScreenShareAudio)
screenAudioTrack = mt;
});
const userState = useVoiceStore.getState().voiceUserStates.get(userId);
let isPartDeafened = false;
let isPartMuted = !p.isMicrophoneEnabled;
if (isLocal) {
isPartDeafened = useVoiceStore.getState().isDeafened;
isPartMuted = useVoiceStore.getState().isMuted;
}
else {
isPartDeafened = userState?.isDeafened ?? useVoiceStore.getState().deafenedUserIds.has(userId);
if (userState)
isPartMuted = userState.isMuted;
}
allParticipants.push({
identity: p.identity,
userId,
username,
isSpeaking: p.isSpeaking,
isMuted: isPartMuted,
isDeafened: isPartDeafened,
isCameraOn: !!videoTrack,
isScreenSharing: hasScreenSharePublication, // True even when unsubscribed
isLocal,
audioTrack,
videoTrack,
screenTrack,
screenAudioTrack,
});
};
processParticipant(r.localParticipant, true);
r.remoteParticipants.forEach((p) => processParticipant(p, false));
setParticipants(allParticipants);
}, []);
const handleDataReceived = useCallback((payload, participant) => {
try {
const text = new TextDecoder().decode(payload);
const msg = JSON.parse(text);
if (msg.type === 'deafen' && participant) {
const { userId } = parseIdentity(participant.identity);
useVoiceStore.getState().setUserDeafened(userId, msg.deafened === true);
updateParticipants();
}
}
catch { }
}, [updateParticipants]);
// Handle Input Device & Mute Logic via AudioManager
// Mute uses setMicrophoneEnabled(false) to keep the track published (silence frames)
// instead of unpublishTrack() which tears down the WebRTC transport.
useEffect(() => {
const r = roomRef.current;
if (!r || !isConnected)
return;
const syncMic = async () => {
try {
const audioManager = AudioManager.getInstance();
audioManager.setVoiceProcessing({ echoCancellation, noiseSuppression, autoGainControl });
await audioManager.setRnnoiseEnabled(rnnoiseEnabled);
audioManager.setScreenShareActive(isScreenSharing);
const micPub = r.localParticipant.getTrackPublications()
.find(p => p.source === Track.Source.Microphone);
// If muted or deafened, mute the track in-place (keep it published)
if (isMuted || isDeafened) {
if (micPub?.track && !micPub.isMuted) {
await r.localParticipant.setMicrophoneEnabled(false);
}
return;
}
// Not muted — ensure mic is published and live
await audioManager.setInputDevice(inputDeviceId);
audioManager.setInputVolume(inputVolume);
const currentGen = audioManager.getStreamGeneration();
if (micPub?.track) {
// Track already published — check if it's still current
if (micPub.track.mediaStreamTrack?.readyState === 'live' && lastMicGenRef.current === currentGen) {
// Current and live — just unmute if needed
if (micPub.isMuted) {
await r.localParticipant.setMicrophoneEnabled(true);
}
return;
}
// Track is stale (device or constraint change) — replace it
await r.localParticipant.unpublishTrack(micPub.track);
}
// Publish fresh track from AudioManager pipeline
const audioTrack = audioManager.getFreshTrack();
if (!audioTrack)
return;
console.log('[LiveKit] Publishing fresh microphone track (gen:', currentGen, ')');
await r.localParticipant.publishTrack(audioTrack, {
name: 'microphone',
source: Track.Source.Microphone,
});
lastMicGenRef.current = currentGen;
}
catch (err) {
console.error('[LiveKit] Failed to sync mic state:', err);
}
};
syncMic();
const unsubscribe = AudioManager.getInstance().onResumed(() => {
syncMic();
});
return () => {
unsubscribe();
};
}, [isMuted, isDeafened, inputDeviceId, inputVolume, isConnected, echoCancellation, noiseSuppression, autoGainControl, rnnoiseEnabled, isScreenSharing]);
const connect = useCallback(async (channelId) => {
if (connectedChannelRef.current === channelId && roomRef.current?.state === ConnectionState.Connected)
return;
const gen = ++_connectGeneration;
// Ensure AudioContext is created and resumed before tracks arrive
await AudioManager.getInstance().resumeContext();
// 1. Reset state immediately to reflect "Loading/Switching" in UI
setRoom(null);
setParticipants([]);
setIsConnected(false);
setIsConnecting(true);
setConnectionState(ConnectionState.Connecting);
setConnectionError(null);
setConnectedChannelId(null); // Clear this so AppLayout knows we are transitioning
useVoiceStore.getState().setIsLiveKitConnected(false);
// 2. Strictly disconnect previous room (Local Ref OR Global Ref)
// This handles cases where AppLayout might have remounted, losing roomRef but leaving _activeRoom alive.
const roomToDisconnect = roomRef.current || _activeRoom;
if (roomToDisconnect) {
try {
console.log('[LiveKit] Disconnecting previous room:', roomToDisconnect.name);
await roomToDisconnect.disconnect();
}
catch (err) {
console.warn('Error disconnecting from previous room:', err);
}
roomRef.current = null;
_activeRoom = null;
}
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 } });
roomRef.current = newRoom;
const guardedUpdate = () => { if (roomRef.current === newRoom)
updateParticipants(); };
// ... existing event listeners ...
newRoom.on(RoomEvent.ParticipantConnected, (participant) => {
guardedUpdate();
if (useVoiceStore.getState().isDeafened) {
const encoder = new TextEncoder();
newRoom.localParticipant.publishData(encoder.encode(JSON.stringify({ type: 'deafen', deafened: true })), { reliable: true }).catch(() => { });
}
});
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
newRoom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
// LiveKit auto-attaches a hidden <audio> element for subscribed audio tracks.
// GlobalAudioRenderer is the sole audio playback path with volume/attenuation/boost.
// Detach LiveKit's internal element to prevent double-playback.
if (track.kind === Track.Kind.Audio) {
track.detach();
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackUnsubscribed, (track) => {
if (track.kind === Track.Kind.Audio) {
track.detach();
}
guardedUpdate();
});
newRoom.on(RoomEvent.LocalTrackPublished, (publication) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
useVoiceStore.getState().watchStream(userId);
}
guardedUpdate();
});
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
useVoiceStore.getState().unwatchStream(userId);
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.ParticipantMetadataChanged, guardedUpdate);
newRoom.on(RoomEvent.TrackPublished, (publication, participant) => {
if (publication.source === Track.Source.ScreenShare ||
publication.source === Track.Source.ScreenShareAudio) {
publication.setSubscribed(false);
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackUnpublished, (publication, participant) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(participant.identity);
const state = useVoiceStore.getState();
state.unwatchStream(userId);
state.clearStreamVolume(userId);
state.clearStreamMute(userId);
}
guardedUpdate();
});
newRoom.on(RoomEvent.DataReceived, handleDataReceived);
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
setConnectionState(state);
const connected = state === ConnectionState.Connected;
const connecting = state === ConnectionState.Connecting || state === ConnectionState.Reconnecting;
setIsConnected(connected);
setIsConnecting(connecting);
useVoiceStore.getState().setIsLiveKitConnected(connected);
if (connected) {
updateParticipants();
}
}
});
newRoom.on(RoomEvent.Disconnected, () => {
if (roomRef.current !== newRoom)
return;
setConnectionState(ConnectionState.Disconnected);
setConnectedChannelId(null);
roomRef.current = null;
_activeRoom = null;
setIsConnected(false);
setRoom(null);
setParticipants([]);
useVoiceStore.getState().setIsLiveKitConnected(false);
});
await newRoom.connect(url, token);
if (gen !== _connectGeneration) {
newRoom.disconnect();
return;
}
_activeRoom = newRoom;
connectedChannelRef.current = channelId;
setConnectedChannelId(channelId);
setRoom(newRoom);
setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
updateParticipants();
// Unsubscribe from any remote screen share tracks that auto-subscribed during connect
newRoom.remoteParticipants.forEach((rp) => {
rp.trackPublications.forEach((pub) => {
if ((pub.source === Track.Source.ScreenShare || pub.source === Track.Source.ScreenShareAudio) &&
pub.isSubscribed) {
pub.setSubscribed(false);
}
});
});
// Initial mute state check
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
if (wasDeafened) {
newRoom.remoteParticipants.forEach((p) => p.setVolume(0));
}
updateParticipants();
}
catch (err) {
if (gen === _connectGeneration)
setConnectionError('Failed to connect');
}
finally {
if (gen === _connectGeneration)
setIsConnecting(false);
}
}, [updateParticipants, handleDataReceived]);
const connectDm = useCallback(async (dmChannelId) => {
const gen = ++_connectGeneration;
// Ensure AudioContext is created and resumed before tracks arrive
await AudioManager.getInstance().resumeContext();
// 1. Reset state immediately
setRoom(null);
setParticipants([]);
setIsConnected(false);
setIsConnecting(true);
setConnectionState(ConnectionState.Connecting);
setConnectionError(null);
setConnectedChannelId(null);
// 2. Strictly disconnect previous room (Local Ref OR Global Ref)
const roomToDisconnect = roomRef.current || _activeRoom;
if (roomToDisconnect) {
try {
console.log('[LiveKit] Disconnecting previous room (DM):', roomToDisconnect.name);
await roomToDisconnect.disconnect();
}
catch (err) {
console.warn('Error disconnecting from previous room:', err);
}
roomRef.current = null;
_activeRoom = null;
}
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;
const guardedUpdate = () => { if (roomRef.current === newRoom)
updateParticipants(); };
newRoom.on(RoomEvent.ParticipantConnected, guardedUpdate);
newRoom.on(RoomEvent.ParticipantDisconnected, guardedUpdate);
newRoom.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind === Track.Kind.Audio) {
track.detach();
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackUnsubscribed, (track) => {
if (track.kind === Track.Kind.Audio) {
track.detach();
}
guardedUpdate();
});
newRoom.on(RoomEvent.LocalTrackPublished, (publication) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
useVoiceStore.getState().watchStream(userId);
}
guardedUpdate();
});
newRoom.on(RoomEvent.LocalTrackUnpublished, (publication) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(newRoom.localParticipant.identity);
useVoiceStore.getState().unwatchStream(userId);
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackMuted, guardedUpdate);
newRoom.on(RoomEvent.TrackUnmuted, guardedUpdate);
newRoom.on(RoomEvent.ActiveSpeakersChanged, guardedUpdate);
newRoom.on(RoomEvent.TrackPublished, (publication, participant) => {
if (publication.source === Track.Source.ScreenShare ||
publication.source === Track.Source.ScreenShareAudio) {
publication.setSubscribed(false);
}
guardedUpdate();
});
newRoom.on(RoomEvent.TrackUnpublished, (publication, participant) => {
if (publication.source === Track.Source.ScreenShare) {
const { userId } = parseIdentity(participant.identity);
const state = useVoiceStore.getState();
state.unwatchStream(userId);
state.clearStreamVolume(userId);
state.clearStreamMute(userId);
}
guardedUpdate();
});
newRoom.on(RoomEvent.ConnectionStateChanged, (state) => {
if (roomRef.current === newRoom) {
setConnectionState(state);
const connected = state === ConnectionState.Connected;
const connecting = state === ConnectionState.Connecting || state === ConnectionState.Reconnecting;
setIsConnected(connected);
setIsConnecting(connecting);
useVoiceStore.getState().setIsLiveKitConnected(connected);
if (connected) {
updateParticipants();
}
}
});
await newRoom.connect(url, token);
if (gen !== _connectGeneration) {
newRoom.disconnect();
return;
}
const fullId = `dm-${dmChannelId}`;
_activeRoom = newRoom;
connectedChannelRef.current = fullId;
setConnectedChannelId(fullId);
setRoom(newRoom);
setIsConnected(true);
useVoiceStore.getState().setIsLiveKitConnected(true);
updateParticipants();
// Unsubscribe from any remote screen share tracks that auto-subscribed during connect
newRoom.remoteParticipants.forEach((rp) => {
rp.trackPublications.forEach((pub) => {
if ((pub.source === Track.Source.ScreenShare || pub.source === Track.Source.ScreenShareAudio) &&
pub.isSubscribed) {
pub.setSubscribed(false);
}
});
});
const { isMuted: wasMuted, isDeafened: wasDeafened } = useVoiceStore.getState();
useVoiceStore.setState({ isCameraOn: false, isScreenSharing: false });
if (wasDeafened) {
newRoom.remoteParticipants.forEach((p) => p.setVolume(0));
}
updateParticipants();
}
catch (err) {
if (gen === _connectGeneration)
setConnectionError('Failed to connect');
}
finally {
if (gen === _connectGeneration)
setIsConnecting(false);
}
}, [updateParticipants, handleDataReceived]);
const disconnect = useCallback(async () => {
_connectGeneration++;
connectedChannelRef.current = null;
setConnectedChannelId(null);
if (roomRef.current) {
await roomRef.current.disconnect();
roomRef.current = null;
_activeRoom = null;
setRoom(null);
setIsConnected(false);
setIsConnecting(false);
setConnectionState(ConnectionState.Disconnected);
setParticipants([]);
useVoiceStore.getState().setIsLiveKitConnected(false);
}
}, []);
const toggleMic = useCallback(async () => {
await AudioManager.getInstance().resumeContext();
useVoiceStore.getState().toggleMic();
}, []);
const toggleCamera = useCallback(async () => {
if (roomRef.current) {
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); }, 2000);
}
else {
await roomRef.current.localParticipant.setCameraEnabled(false);
}
updateParticipants();
}
}, [isCameraOn, videoQuality, updateParticipants]);
const toggleScreenShare = useCallback(async () => {
if (roomRef.current) {
if (!isScreenSharing) {
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'
});
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);
}
}
else {
await roomRef.current.localParticipant.setScreenShareEnabled(false);
}
updateParticipants();
}
}, [isScreenSharing, videoQuality, updateParticipants]);
useEffect(() => {
updateParticipants();
}, [voiceUserStates, isMuted, isDeafened, updateParticipants]);
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.mediaStreamTrack;
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);
}
}
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(`[Soft-Launch 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, connectionState, connectedChannelId, connectionError, connect, connectDm, disconnect, toggleMic, toggleCamera, toggleScreenShare };
}
-333
View File
@@ -1,333 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { useAuthStore } from '../stores/authStore';
import { useServerStore } from '../stores/serverStore';
import { useChatStore } from '../stores/chatStore';
import { useVoiceStore } from '../stores/voiceStore';
import { useSocialStore } from '../stores/socialStore';
let globalWs = null;
let reconnectAttempts = 0;
let reconnectTimer;
let currentToken = null;
let isInitialized = false;
function handleEvent(event) {
const { setUser } = useAuthStore.getState();
const { populateFromReady, loadServerDetail, currentServerId, updateMemberPresence, addMember, removeMember, addDmChannel, removeDmChannel } = useServerStore.getState();
const { addMessage, addRealtimeMessage, updateMessage, removeMessage, setTyping, onReactionAdded, onReactionRemoved } = useChatStore.getState();
const { addVoiceUser, removeVoiceUser, clearAllVoiceUsers, setVoiceUsers, setVoiceUserStatus, clearVoiceUserStatus } = useVoiceStore.getState();
switch (event.type) {
case 'ready':
setUser(event.user);
populateFromReady(event.servers, event.folders, event.dmChannels);
if (currentServerId) {
loadServerDetail(currentServerId);
}
// Only force-reload the current channel on reconnect; other channels keep their cache
{
const { loadMessages: reloadMessages, currentChannelId, setReadStates } = useChatStore.getState();
if (currentChannelId) {
reloadMessages(currentChannelId, true);
}
// Initialize unread tracking from ready payload
const { channelLastMessageIds } = useServerStore.getState();
if (event.readStates) {
setReadStates(event.readStates, channelLastMessageIds);
}
}
// Clear stale voice state, then populate from server truth
clearAllVoiceUsers();
if (event.voiceStates) {
for (const [channelId, userIds] of Object.entries(event.voiceStates)) {
setVoiceUsers(channelId, userIds);
}
}
// Populate voice user statuses (mute/deafen) from server
if (event.voiceUserStates) {
for (const [uid, status] of Object.entries(event.voiceUserStates)) {
setVoiceUserStatus(uid, status.isMuted, status.isDeafened);
}
}
// Re-register in voice channel if we're still connected to LiveKit
// (WebSocket reconnect causes server to drop our voice tracking)
{
const { currentVoiceChannelId, isMuted: curMuted, isDeafened: curDeafened } = useVoiceStore.getState();
if (currentVoiceChannelId) {
console.log('[WebSocket] Re-syncing voice status on reconnect:', { currentVoiceChannelId, curMuted, curDeafened });
wsSend({ type: 'voice_join', channelId: currentVoiceChannelId });
wsSend({ type: 'voice_status', isMuted: curMuted, isDeafened: curDeafened });
}
}
break;
case 'message_created':
addRealtimeMessage(event.message.channelId, event.message);
{
const { currentChannelId, markChannelUnread } = useChatStore.getState();
if (event.message.channelId !== currentChannelId) {
markChannelUnread(event.message.channelId);
}
}
break;
case 'message_updated':
updateMessage(event.message);
break;
case 'message_deleted':
removeMessage(event.messageId, event.channelId);
break;
case 'typing':
setTyping(event.channelId, event.userId, event.username);
break;
case 'presence_update':
updateMemberPresence(event.userId, event.status);
useSocialStore.getState().updateFriendPresence(event.userId, event.status);
break;
case 'voice_state_update':
if (event.action === 'join') {
addVoiceUser(event.channelId, event.userId);
}
else {
removeVoiceUser(event.channelId, event.userId);
}
break;
case 'voice_status_update':
setVoiceUserStatus(event.userId, event.isMuted, event.isDeafened);
break;
case 'member_joined':
addMember(event.member);
break;
case 'member_left':
removeMember(event.userId);
break;
case 'dm_message_created': {
addRealtimeMessage(event.message.dmChannelId, event.message);
// If DM channel is unknown (first-ever message safety net), add a minimal one
const { dmChannels: currentDmChannels, setDmChannels: setDms, addDmChannel: addDmCh } = useServerStore.getState();
const knownDm = currentDmChannels.find(dm => dm.id === event.message.dmChannelId);
if (!knownDm) {
// Construct a minimal DmChannel from the message so the sidebar shows it
addDmCh({
id: event.message.dmChannelId,
createdAt: event.message.createdAt,
members: event.message.user ? [event.message.user] : [],
lastMessage: event.message,
});
} else {
// Update lastMessage on the DM channel so the sidebar sorts correctly
const updatedDms = currentDmChannels.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;
});
setDms(updatedDms);
}
// Mark DM as unread if not currently viewing it
{
const { currentChannelId, markChannelUnread } = useChatStore.getState();
if (event.message.dmChannelId !== currentChannelId) {
markChannelUnread(event.message.dmChannelId);
}
}
break;
}
case 'dm_message_updated':
updateMessage(event.message);
break;
case 'dm_message_deleted':
removeMessage(event.messageId, event.dmChannelId);
break;
case 'dm_typing':
setTyping(event.dmChannelId, event.userId, event.username);
break;
case 'reaction_added':
onReactionAdded(event.messageId, event.reaction);
break;
case 'reaction_removed':
onReactionRemoved(event.messageId, event.userId, event.emoji);
break;
case 'friend_request_received': {
const { addIncomingRequest } = useSocialStore.getState();
addIncomingRequest(event.request);
break;
}
case 'friend_request_accepted': {
const { addFriendFromAccepted } = useSocialStore.getState();
addFriendFromAccepted(event.friend, event.requestId);
break;
}
case 'channel_ack': {
const { onChannelAck } = useChatStore.getState();
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 'dm_channel_created':
addDmChannel(event.dmChannel);
break;
case 'dm_channel_closed':
removeDmChannel(event.dmChannelId);
break;
case 'friend_removed': {
const { removeFriendLocally } = useSocialStore.getState();
removeFriendLocally(event.userId);
break;
}
case 'channel_created': {
const { currentServerId: curServerId, channels: curChannels, setChannels } = useServerStore.getState();
if (event.serverId === curServerId) {
if (!curChannels.find(c => c.id === event.channel.id)) {
setChannels([...curChannels, event.channel].sort((a, b) => a.position - b.position));
}
}
break;
}
case 'channel_updated': {
const { currentServerId: curServerId2, channels: curChannels2, setChannels: setChannels2 } = useServerStore.getState();
if (event.serverId === curServerId2) {
setChannels2(curChannels2.map(c => c.id === event.channel.id ? event.channel : c).sort((a, b) => a.position - b.position));
}
break;
}
case 'channel_deleted': {
const { currentServerId: curServerId3, channels: curChannels3, setChannels: setChannels3 } = useServerStore.getState();
if (event.serverId === curServerId3) {
setChannels3(curChannels3.filter(c => c.id !== event.channelId));
}
{
const { currentChannelId } = useChatStore.getState();
if (currentChannelId === event.channelId) {
const { channels: remainingChannels } = useServerStore.getState();
const firstText = remainingChannels.find(c => c.type === 'text');
if (firstText) {
useChatStore.getState().setCurrentChannel(firstText.id);
} else {
useChatStore.getState().setCurrentChannel(null);
}
}
}
break;
}
case 'server_updated': {
const { servers: currentServers, setServers } = useServerStore.getState();
setServers(currentServers.map(s => s.id === event.server.id ? { ...s, ...event.server } : s));
break;
}
case 'error':
console.error('WebSocket error:', event.message);
break;
}
}
function connect() {
if (!currentToken)
return;
if (globalWs && (globalWs.readyState === WebSocket.OPEN || globalWs.readyState === WebSocket.CONNECTING)) {
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
const ws = new WebSocket(wsUrl);
globalWs = ws;
ws.onopen = () => {
reconnectAttempts = 0;
ws.send(JSON.stringify({ type: 'auth', token: currentToken }));
};
ws.onmessage = (e) => {
try {
const event = JSON.parse(e.data);
handleEvent(event);
}
catch {
console.error('Failed to parse WebSocket message');
}
};
ws.onclose = () => {
globalWs = null;
if (currentToken) {
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
reconnectAttempts++;
reconnectTimer = setTimeout(connect, delay);
}
};
ws.onerror = () => {
ws.close();
};
}
function disconnect() {
currentToken = null;
isInitialized = false;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = undefined;
}
if (globalWs) {
globalWs.close();
globalWs = null;
}
}
/** Send an event over the WebSocket. Can be used outside of React components. */
export function wsSend(event) {
if (globalWs && globalWs.readyState === WebSocket.OPEN) {
globalWs.send(JSON.stringify(event));
}
}
/**
* Hook to initialize the WebSocket connection. Should only be called ONCE
* from the top-level layout component (AppLayout). Other components should
* use the exported `wsSend` function directly.
*/
export function useWebSocket() {
const token = useAuthStore((s) => s.token);
const prevToken = useRef(token);
const [isConnected, setIsConnected] = React.useState(false);
useEffect(() => {
if (token && (!isInitialized || token !== prevToken.current)) {
currentToken = token;
isInitialized = true;
connect();
}
else if (!token && isInitialized) {
disconnect();
}
prevToken.current = token;
}, [token]);
useEffect(() => {
const checkStatus = setInterval(() => {
setIsConnected(!!globalWs && globalWs.readyState === WebSocket.OPEN);
}, 500);
return () => {
clearInterval(checkStatus);
disconnect();
};
}, []);
return { send: wsSend, isConnected };
}