feat: introduce a comprehensive LiveKit-based voice and video chat system with state management and screen sharing capabilities.
All checks were successful
Build and Release / build-and-release (push) Successful in 10m6s

This commit is contained in:
Bryan1029384756
2026-02-12 18:49:31 -06:00
parent 751f428adc
commit 2201c56cb2
16 changed files with 791 additions and 270 deletions

View File

@@ -0,0 +1,118 @@
import { useState, useEffect, useRef } from 'react';
import { RoomEvent } from 'livekit-client';
export const VideoRenderer = ({ track, style }) => {
const videoRef = useRef(null);
useEffect(() => {
const el = videoRef.current;
if (el && track) {
track.attach(el);
return () => {
track.detach(el);
};
}
}, [track]);
return (
<video
ref={videoRef}
style={{ width: '100%', height: '100%', objectFit: 'contain', backgroundColor: 'black', ...style }}
/>
);
};
export function findTrackPubs(participant) {
let cameraPub = null;
let screenSharePub = null;
let screenShareAudioPub = null;
const trackMap = participant.tracks || participant.trackPublications;
if (trackMap) {
for (const pub of trackMap.values()) {
const source = pub.source ? pub.source.toString().toLowerCase() : '';
const name = pub.trackName ? pub.trackName.toLowerCase() : '';
if (source === 'screen_share_audio' || name === 'screen_share_audio') {
screenShareAudioPub = pub;
} else if (source === 'screenshare' || source === 'screen_share' || name.includes('screen')) {
screenSharePub = pub;
} else if (source === 'camera' || name.includes('camera')) {
cameraPub = pub;
} else if (pub.kind === 'video' && !screenSharePub) {
screenSharePub = pub;
}
}
}
// Fallback for older SDKs
if ((!screenSharePub && !cameraPub) && participant.getTracks) {
try {
for (const pub of participant.getTracks()) {
const source = pub.source ? pub.source.toString().toLowerCase() : '';
const name = pub.trackName ? pub.trackName.toLowerCase() : '';
if (source === 'screen_share_audio' || name === 'screen_share_audio') {
screenShareAudioPub = pub;
} else if (source === 'screenshare' || source === 'screen_share' || name.includes('screen')) {
screenSharePub = pub;
} else if (source === 'camera' || name.includes('camera')) {
cameraPub = pub;
} else if (pub.kind === 'video' && !screenSharePub) {
screenSharePub = pub;
}
}
} catch (e) { /* ignore */ }
}
return { cameraPub, screenSharePub, screenShareAudioPub };
}
export function useParticipantTrack(participant, source) {
const [track, setTrack] = useState(null);
useEffect(() => {
if (!participant) { setTrack(null); return; }
const resolve = () => {
const { cameraPub, screenSharePub } = findTrackPubs(participant);
const pub = source === 'camera' ? cameraPub : screenSharePub;
if (pub && !pub.isSubscribed && source === 'camera') {
pub.setSubscribed(true);
}
setTrack(pub?.track || null);
};
resolve();
const interval = setInterval(resolve, 1000);
const timeout = setTimeout(() => clearInterval(interval), 10000);
const onPub = () => resolve();
const onUnpub = () => resolve();
participant.on(RoomEvent.TrackPublished, onPub);
participant.on(RoomEvent.TrackUnpublished, onUnpub);
participant.on(RoomEvent.TrackSubscribed, onPub);
participant.on(RoomEvent.TrackUnsubscribed, onUnpub);
participant.on(RoomEvent.TrackMuted, onPub);
participant.on(RoomEvent.TrackUnmuted, onPub);
participant.on('localTrackPublished', onPub);
participant.on('localTrackUnpublished', onUnpub);
return () => {
clearInterval(interval);
clearTimeout(timeout);
participant.off(RoomEvent.TrackPublished, onPub);
participant.off(RoomEvent.TrackUnpublished, onUnpub);
participant.off(RoomEvent.TrackSubscribed, onPub);
participant.off(RoomEvent.TrackUnsubscribed, onUnpub);
participant.off(RoomEvent.TrackMuted, onPub);
participant.off(RoomEvent.TrackUnmuted, onPub);
participant.off('localTrackPublished', onPub);
participant.off('localTrackUnpublished', onUnpub);
};
}, [participant, source]);
return track;
}

View File

@@ -0,0 +1,23 @@
export function getUserPref(userId, key, defaultValue) {
if (!userId) return defaultValue;
try {
const raw = localStorage.getItem(`userPrefs_${userId}`);
if (!raw) return defaultValue;
const prefs = JSON.parse(raw);
return prefs[key] !== undefined ? prefs[key] : defaultValue;
} catch {
return defaultValue;
}
}
export function setUserPref(userId, key, value) {
if (!userId) return;
try {
const raw = localStorage.getItem(`userPrefs_${userId}`);
const prefs = raw ? JSON.parse(raw) : {};
prefs[key] = value;
localStorage.setItem(`userPrefs_${userId}`, JSON.stringify(prefs));
} catch {
// Silently fail on corrupt data or full storage
}
}