1.0.60
All checks were successful
Build and Release / build-and-release (push) Successful in 12m29s

This commit is contained in:
Bryan1029384756
2026-04-14 20:03:54 -05:00
parent b7a4cf4ce8
commit 965048f7d2
47 changed files with 2558 additions and 135 deletions

View File

@@ -4,6 +4,7 @@ import { LiveKitRoom, RoomAudioRenderer } from '@livekit/components-react';
import { useQuery, useConvex } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
import { findTrackPubs } from '../utils/streamUtils.jsx';
import { VoiceRecorder } from '../utils/voiceRecorder';
import { usePlatform } from '../platform';
import '@livekit/components-styles';
@@ -58,7 +59,8 @@ function playSoundUrl(url) {
}
export const VoiceProvider = ({ children }) => {
const { idle, voiceService } = usePlatform();
const platform = usePlatform();
const { idle, voiceService } = platform;
const [activeChannelId, setActiveChannelId] = useState(null);
const [activeChannelName, setActiveChannelName] = useState(null);
const [connectionState, setConnectionState] = useState('disconnected');
@@ -85,6 +87,16 @@ export const VoiceProvider = ({ children }) => {
const [isReconnecting, setIsReconnecting] = useState(false);
const [connectionQualities, setConnectionQualities] = useState({});
// Voice recording — crash-safe per-participant audio capture.
// Only available on Electron (`platform.features.hasRecording`).
// `recordingError` is surfaced once per failure so UI can toast
// it; consumers should clear it after reading.
const [isRecording, setIsRecording] = useState(false);
const [recordingStartedAt, setRecordingStartedAt] = useState(null);
const [recordingSessionId, setRecordingSessionId] = useState(null);
const [recordingError, setRecordingError] = useState(null);
const voiceRecorderRef = useRef(null);
const convex = useConvex();
// Stream watching state (lifted from VoiceStage so PiP can persist across navigation)
@@ -953,6 +965,94 @@ export const VoiceProvider = ({ children }) => {
}
}, [room]);
// ── Voice recording ─────────────────────────────────────────
// Starts a `VoiceRecorder` bound to the current room. Returns
// the session metadata on success so the UI can show a confirm
// indicator. Gracefully no-ops (with a console warning) on
// platforms that don't support recording.
const startRecording = useCallback(async () => {
if (!platform?.features?.hasRecording || !platform.recording) {
console.warn('Recording is not available on this platform.');
return null;
}
if (!room) {
console.warn('Cannot start recording — not connected to a voice channel.');
return null;
}
if (voiceRecorderRef.current) {
return {
sessionId: voiceRecorderRef.current.sessionId,
startedAt: voiceRecorderRef.current.startedAt,
};
}
try {
// Resolve the user-preferred recording folder (settings
// writes to localStorage under `voiceSettings`, same
// bucket the mic/AGC toggles live in).
let rootDir = null;
try {
const raw = localStorage.getItem('voiceSettings');
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed.recordingDir === 'string') {
rootDir = parsed.recordingDir;
}
}
} catch {}
const recorder = new VoiceRecorder({
platform,
room,
channelId: activeChannelId,
channelName: activeChannelName,
rootDir,
onError: (err) => {
console.error('Voice recorder error:', err);
setRecordingError(err.message || 'Recording error');
},
});
await recorder.start();
voiceRecorderRef.current = recorder;
setRecordingSessionId(recorder.sessionId);
setRecordingStartedAt(recorder.startedAt);
setIsRecording(true);
setRecordingError(null);
return { sessionId: recorder.sessionId, startedAt: recorder.startedAt };
} catch (err) {
console.error('Failed to start recording:', err);
setRecordingError(err?.message || 'Failed to start recording');
voiceRecorderRef.current = null;
setIsRecording(false);
setRecordingSessionId(null);
setRecordingStartedAt(null);
return null;
}
}, [platform, room, activeChannelId, activeChannelName]);
const stopRecording = useCallback(async () => {
const recorder = voiceRecorderRef.current;
if (!recorder) return;
voiceRecorderRef.current = null;
try {
await recorder.stop();
} catch (err) {
console.error('Failed to stop recording cleanly:', err);
setRecordingError(err?.message || 'Failed to stop recording');
} finally {
setIsRecording(false);
setRecordingSessionId(null);
setRecordingStartedAt(null);
}
}, []);
// Auto-stop the recorder if the user disconnects from the
// voice channel — we don't want an orphaned VoiceRecorder
// holding references to a destroyed LiveKit room.
useEffect(() => {
if (!room && voiceRecorderRef.current) {
void stopRecording();
}
}, [room, stopRecording]);
return (
<VoiceContext.Provider value={{
@@ -993,6 +1093,14 @@ export const VoiceProvider = ({ children }) => {
isReceivingScreenShareAudio,
isReconnecting,
connectionQualities,
// Voice recording
isRecording,
recordingStartedAt,
recordingSessionId,
recordingError,
startRecording,
stopRecording,
clearRecordingError: () => setRecordingError(null),
}}>
{children}
{room && (