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 (
+
+
+ {isRecording && {formatElapsed(elapsedMs)}}
+
+ );
+};
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 (
+
+
+ {isRecording && {formatElapsed(elapsedMs)}}
+
+ );
+};