feat(media): wire Recorder upload to /api/media/audio; fix multer TS types; add S3 download helper
This commit is contained in:
parent
c94461b460
commit
4ad9c311a2
@ -6,6 +6,8 @@ export default function Recorder() {
|
||||
const chunksRef = useRef<Blob[]>([]);
|
||||
const [recording, setRecording] = useState(false);
|
||||
const [audioUrl, setAudioUrl] = useState<string | null>(null);
|
||||
const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
|
||||
const [uploadKey, setUploadKey] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const requestStream = async (): Promise<MediaStream | null> => {
|
||||
@ -39,6 +41,7 @@ export default function Recorder() {
|
||||
if (prev) URL.revokeObjectURL(prev);
|
||||
return url;
|
||||
});
|
||||
setAudioBlob(blob);
|
||||
// stop all tracks to release mic
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
};
|
||||
@ -52,6 +55,31 @@ export default function Recorder() {
|
||||
setRecording(false);
|
||||
};
|
||||
|
||||
const uploadAudio = async () => {
|
||||
try {
|
||||
setError('');
|
||||
setUploadKey(null);
|
||||
if (!audioBlob) {
|
||||
setError('No audio to upload');
|
||||
return;
|
||||
}
|
||||
const form = new FormData();
|
||||
form.append('audio', audioBlob, 'recording.webm');
|
||||
const res = await fetch('/api/media/audio', {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const txt = await res.text();
|
||||
throw new Error(`Upload failed: ${res.status} ${txt}`);
|
||||
}
|
||||
const data = await res.json();
|
||||
setUploadKey(data.key || 'uploaded');
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Upload failed');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (audioUrl) URL.revokeObjectURL(audioUrl);
|
||||
@ -64,6 +92,7 @@ export default function Recorder() {
|
||||
<Stack direction="row" spacing={2} sx={{ mb: 2 }}>
|
||||
<Button variant="contained" disabled={recording} onClick={startRecording}>Start</Button>
|
||||
<Button variant="outlined" disabled={!recording} onClick={stopRecording}>Stop</Button>
|
||||
<Button variant="text" disabled={!audioBlob} onClick={uploadAudio}>Upload</Button>
|
||||
</Stack>
|
||||
{error && <Typography color="error" sx={{ mb: 2 }}>{error}</Typography>}
|
||||
{audioUrl && (
|
||||
@ -71,6 +100,11 @@ export default function Recorder() {
|
||||
<audio controls src={audioUrl} />
|
||||
</Box>
|
||||
)}
|
||||
{uploadKey && (
|
||||
<Typography variant="body2" sx={{ mt: 1 }}>
|
||||
Uploaded as key: {uploadKey}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import express from 'express';
|
||||
import multer from 'multer';
|
||||
import type { File as MulterFile } from 'multer';
|
||||
import crypto from 'crypto';
|
||||
import { uploadBuffer } from './storage/s3';
|
||||
|
||||
@ -8,7 +7,7 @@ const router = express.Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
router.post('/audio', upload.single('audio'), async (
|
||||
req: express.Request & { file?: MulterFile },
|
||||
req: express.Request,
|
||||
res: express.Response
|
||||
) => {
|
||||
try {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||
|
||||
function getS3Client() {
|
||||
export function getS3Client() {
|
||||
const endpoint = process.env.S3_ENDPOINT; // e.g. http://<VPS_IP>:9000
|
||||
const region = process.env.S3_REGION || 'us-east-1';
|
||||
const accessKeyId = process.env.S3_ACCESS_KEY || '';
|
||||
@ -34,3 +34,18 @@ export async function uploadBuffer(params: {
|
||||
await s3.send(cmd);
|
||||
return { bucket: params.bucket, key: params.key };
|
||||
}
|
||||
|
||||
export async function downloadObject(params: { bucket: string; key: string }): Promise<{ buffer: Buffer; contentType: string }> {
|
||||
const s3 = getS3Client();
|
||||
const cmd = new GetObjectCommand({ Bucket: params.bucket, Key: params.key });
|
||||
const res = await s3.send(cmd);
|
||||
const contentType = res.ContentType || 'application/octet-stream';
|
||||
const body = res.Body as unknown as NodeJS.ReadableStream;
|
||||
const chunks: Buffer[] = [];
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
body.on('data', (c: Buffer) => chunks.push(c));
|
||||
body.on('end', resolve);
|
||||
body.on('error', reject);
|
||||
});
|
||||
return { buffer: Buffer.concat(chunks), contentType };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user