feat(media): wire Recorder upload to /api/media/audio; fix multer TS types; add S3 download helper

This commit is contained in:
Ender 2025-10-23 21:24:52 +02:00
parent c94461b460
commit 4ad9c311a2
3 changed files with 52 additions and 4 deletions

View File

@ -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>
);
}

View File

@ -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 {

View File

@ -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 };
}