From d7282a04a2fa708d19f9c03cd2afdcaabc6b6a3d Mon Sep 17 00:00:00 2001 From: Guoguo Date: Wed, 3 Dec 2025 14:06:20 +0800 Subject: [PATCH] feat: add recorder --- browser/src/components/menu/index.tsx | 5 + .../src/components/menu/recorder/index.tsx | 115 ++++++++++++++++++ .../renderer/src/components/menu/index.tsx | 5 + .../src/components/menu/recorder/index.tsx | 115 ++++++++++++++++++ 4 files changed, 240 insertions(+) create mode 100644 browser/src/components/menu/recorder/index.tsx create mode 100644 desktop/src/renderer/src/components/menu/recorder/index.tsx diff --git a/browser/src/components/menu/index.tsx b/browser/src/components/menu/index.tsx index c06cb7b..e856be5 100644 --- a/browser/src/components/menu/index.tsx +++ b/browser/src/components/menu/index.tsx @@ -13,6 +13,7 @@ import { Mouse } from './mouse'; import { SerialPort } from './serial-port'; import { Settings } from './settings'; import { Video } from './video'; +import { Recorder } from './recorder'; export const Menu = () => { const serialState = useAtomValue(serialStateAtom); @@ -55,6 +56,10 @@ export const Menu = () => { + + + +
{ + const [isRecording, setIsRecording] = useState(false); + const [elapsedMs, setElapsedMs] = useState(0); + const mediaRecorderRef = useRef(); + const fileWritableRef = useRef(null); + const timerRef = useRef(null); + const startTimeRef = useRef(0); + + const stopTimer = () => { + if (timerRef.current !== null) { + window.clearInterval(timerRef.current); + timerRef.current = null; + } + }; + + const startTimer = () => { + stopTimer(); + startTimeRef.current = Date.now(); + setElapsedMs(0); + timerRef.current = window.setInterval(() => { + setElapsedMs(Date.now() - startTimeRef.current); + }, 1000); + }; + + useEffect(() => { + return () => { + stopTimer(); + }; + }, []); + + const formatElapsed = (ms: number) => { + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + }; + + const handleStartRecording = async () => { + const stream = camera.getStream(); + if (!stream) { + return; + } + + try { + const handle = await (window as any).showSaveFilePicker({ + suggestedName: `recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.webm`, + types: [ + { + description: 'Sipeed NanoKVM-USB Recorder', + accept: { 'video/webm': ['.webm'] } + } + ] + }); + + const writable = await handle.createWritable(); + fileWritableRef.current = writable; + + const recorder = new MediaRecorder(stream, { + mimeType: 'video/webm' + }); + + recorder.ondataavailable = async (event) => { + if (event.data && event.data.size > 0) { + if (fileWritableRef.current) { + await fileWritableRef.current.write(event.data); + } else { + recorder.stop(); + } + } + }; + + recorder.onstop = async () => { + if (fileWritableRef.current) { + await fileWritableRef.current.close(); + fileWritableRef.current = null; + } + stopTimer(); + setElapsedMs(0); + setIsRecording(false); + }; + + recorder.start(1000); + mediaRecorderRef.current = recorder; + setIsRecording(true); + startTimer(); + } catch (err) { + console.error(err); + } + }; + + const handleStopRecording = () => { + const recorder = mediaRecorderRef.current; + if (recorder && recorder.state !== 'inactive') { + recorder.stop(); + stopTimer(); + setElapsedMs(0); + setIsRecording(false); + } + }; + + return ( +
+
+ ); +}; diff --git a/desktop/src/renderer/src/components/menu/index.tsx b/desktop/src/renderer/src/components/menu/index.tsx index 34e930d..0140977 100644 --- a/desktop/src/renderer/src/components/menu/index.tsx +++ b/desktop/src/renderer/src/components/menu/index.tsx @@ -11,6 +11,7 @@ import { Mouse } from './mouse' import { SerialPort } from './serial-port' import { Settings } from './settings' import { Video } from './video' +import { Recorder } from './recorder' export const Menu = (): ReactElement => { const [isMenuOpen, setIsMenuOpen] = useState(false) @@ -41,6 +42,10 @@ export const Menu = (): ReactElement => { + + + +
{ + const [isRecording, setIsRecording] = useState(false); + const [elapsedMs, setElapsedMs] = useState(0); + const mediaRecorderRef = useRef(); + const fileWritableRef = useRef(null); + const timerRef = useRef(null); + const startTimeRef = useRef(0); + + const stopTimer = () => { + if (timerRef.current !== null) { + window.clearInterval(timerRef.current); + timerRef.current = null; + } + }; + + const startTimer = () => { + stopTimer(); + startTimeRef.current = Date.now(); + setElapsedMs(0); + timerRef.current = window.setInterval(() => { + setElapsedMs(Date.now() - startTimeRef.current); + }, 1000); + }; + + useEffect(() => { + return () => { + stopTimer(); + }; + }, []); + + const formatElapsed = (ms: number) => { + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; + }; + + const handleStartRecording = async () => { + const stream = camera.getStream(); + if (!stream) { + return; + } + + try { + const handle = await (window as any).showSaveFilePicker({ + suggestedName: `recording-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.webm`, + types: [ + { + description: 'Sipeed NanoKVM-USB Recorder', + accept: { 'video/webm': ['.webm'] } + } + ] + }); + + const writable = await handle.createWritable(); + fileWritableRef.current = writable; + + const recorder = new MediaRecorder(stream, { + mimeType: 'video/webm' + }); + + recorder.ondataavailable = async (event) => { + if (event.data && event.data.size > 0) { + if (fileWritableRef.current) { + await fileWritableRef.current.write(event.data); + } else { + recorder.stop(); + } + } + }; + + recorder.onstop = async () => { + if (fileWritableRef.current) { + await fileWritableRef.current.close(); + fileWritableRef.current = null; + } + stopTimer(); + setElapsedMs(0); + setIsRecording(false); + }; + + recorder.start(1000); + mediaRecorderRef.current = recorder; + setIsRecording(true); + startTimer(); + } catch (err) { + console.error(err); + } + }; + + const handleStopRecording = () => { + const recorder = mediaRecorderRef.current; + if (recorder && recorder.state !== 'inactive') { + recorder.stop(); + stopTimer(); + setElapsedMs(0); + setIsRecording(false); + } + }; + + return ( +
+
+ ); +};