Files
openscreen/src/hooks/useScreenRecorder.ts
T
2025-10-17 20:05:17 -07:00

133 lines
4.5 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import { fixWebmDuration } from "@fix-webm-duration/fix";
type UseScreenRecorderReturn = {
recording: boolean;
toggleRecording: () => void;
};
export function useScreenRecorder(): UseScreenRecorderReturn {
const [recording, setRecording] = useState(false);
const mediaRecorder = useRef<MediaRecorder | null>(null);
const stream = useRef<MediaStream | null>(null);
const chunks = useRef<Blob[]>([]);
const startTime = useRef<number>(0);
const stopRecording = useRef(() => {
if (mediaRecorder.current?.state === "recording") {
if (stream.current) {
stream.current.getTracks().forEach(track => track.stop());
}
mediaRecorder.current.stop();
setRecording(false);
window.electronAPI.stopMouseTracking();
window.electronAPI?.setRecordingState(false);
}
});
useEffect(() => {
let cleanup: (() => void) | undefined;
if (window.electronAPI?.onStopRecordingFromTray) {
cleanup = window.electronAPI.onStopRecordingFromTray(() => {
stopRecording.current();
});
}
return () => {
if (cleanup) cleanup();
if (mediaRecorder.current?.state === "recording") {
mediaRecorder.current.stop();
}
if (stream.current) {
stream.current.getTracks().forEach(track => track.stop());
stream.current = null;
}
};
}, []);
const startRecording = async () => {
try {
const selectedSource = await window.electronAPI.getSelectedSource();
if (!selectedSource) {
alert("Please select a source to record");
return;
}
await window.electronAPI.startMouseTracking();
const mediaStream = await (navigator.mediaDevices as any).getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: selectedSource.id,
},
},
});
stream.current = mediaStream;
if (!stream.current) {
throw new Error("Media stream is not available.");
}
const videoTrack = stream.current.getVideoTracks()[0];
const { width = 1920, height = 1080 } = videoTrack.getSettings();
const totalPixels = width * height;
let bitrate = 150_000_000;
if (totalPixels > 1920 * 1080 && totalPixels <= 2560 * 1440) {
bitrate = 250_000_000;
} else if (totalPixels > 2560 * 1440) {
bitrate = 400_000_000;
}
chunks.current = [];
const mimeType = "video/webm;codecs=vp9";
const recorder = new MediaRecorder(stream.current, { mimeType, videoBitsPerSecond: bitrate });
mediaRecorder.current = recorder;
recorder.ondataavailable = e => {
if (e.data && e.data.size > 0) chunks.current.push(e.data);
};
recorder.onstop = async () => {
stream.current = null;
if (chunks.current.length === 0) return;
const duration = Date.now() - startTime.current;
const buggyBlob = new Blob(chunks.current, { type: mimeType });
const timestamp = Date.now();
const videoFileName = `recording-${timestamp}.webm`;
const trackingFileName = `recording-${timestamp}_tracking.json`;
try {
const videoBlob = await fixWebmDuration(buggyBlob, duration);
const arrayBuffer = await videoBlob.arrayBuffer();
const videoResult = await window.electronAPI.storeRecordedVideo(arrayBuffer, videoFileName);
if (!videoResult.success) {
console.error('Failed to store video:', videoResult.message);
return;
}
const trackingResult = await window.electronAPI.storeMouseTrackingData(trackingFileName);
if (!trackingResult.success) {
console.warn('Failed to store mouse tracking:', trackingResult.message);
}
await window.electronAPI.switchToEditor();
} catch (error) {
console.error('Error saving recording:', error);
}
};
recorder.onerror = () => setRecording(false);
recorder.start(1000);
startTime.current = Date.now();
setRecording(true);
window.electronAPI?.setRecordingState(true);
} catch (error) {
console.error('Failed to start recording:', error);
setRecording(false);
if (stream.current) {
stream.current.getTracks().forEach(track => track.stop());
stream.current = null;
}
}
};
const toggleRecording = () => {
recording ? stopRecording.current() : startRecording();
};
return { recording, toggleRecording };
}