mirror of
https://github.com/sipeed/NanoKVM-USB.git
synced 2026-09-11 05:59:57 -05:00
Merge pull request #86 from imguoguo/add-recorder-upstream
feat: add recorder
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Recorder />
|
||||
|
||||
<Divider type="vertical" />
|
||||
|
||||
<Settings />
|
||||
<Fullscreen />
|
||||
<div
|
||||
|
||||
115
browser/src/components/menu/recorder/index.tsx
Normal file
115
browser/src/components/menu/recorder/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Video } from 'lucide-react';
|
||||
|
||||
import { camera } from '@/libs/camera';
|
||||
|
||||
export const Recorder = () => {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [elapsedMs, setElapsedMs] = useState(0);
|
||||
const mediaRecorderRef = useRef<MediaRecorder>();
|
||||
const fileWritableRef = useRef<FileSystemWritableFileStream | null>(null);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const startTimeRef = useRef<number>(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 (
|
||||
<div
|
||||
className="flex h-[28px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700/70"
|
||||
onClick={isRecording ? handleStopRecording : handleStartRecording}
|
||||
>
|
||||
<Video className={isRecording ? 'animate-pulse text-red-400' : ''} size={18} />
|
||||
{isRecording && <span className="p-1 text-xs text-red-300">{formatElapsed(elapsedMs)}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 => {
|
||||
<Keyboard />
|
||||
<Divider type="vertical" className="px-[2px]" />
|
||||
|
||||
<Recorder />
|
||||
|
||||
<Divider type="vertical" className="px-[2px]" />
|
||||
|
||||
<Fullscreen />
|
||||
<Settings />
|
||||
<div
|
||||
|
||||
115
desktop/src/renderer/src/components/menu/recorder/index.tsx
Normal file
115
desktop/src/renderer/src/components/menu/recorder/index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Video } from 'lucide-react';
|
||||
|
||||
import { camera } from '@renderer/libs/camera'
|
||||
|
||||
export const Recorder = () => {
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [elapsedMs, setElapsedMs] = useState(0);
|
||||
const mediaRecorderRef = useRef<MediaRecorder>();
|
||||
const fileWritableRef = useRef<FileSystemWritableFileStream | null>(null);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const startTimeRef = useRef<number>(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 (
|
||||
<div
|
||||
className="flex h-[28px] cursor-pointer items-center justify-center rounded text-white hover:bg-neutral-700/70"
|
||||
onClick={isRecording ? handleStopRecording : handleStartRecording}
|
||||
>
|
||||
<Video className={isRecording ? 'animate-pulse text-red-400' : ''} size={18} />
|
||||
{isRecording && <span className="p-1 text-xs text-red-300">{formatElapsed(elapsedMs)}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user