133 lines
4.5 KiB
TypeScript
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 };
|
|
}
|