fix: retry failed recording uploads via backend proxy

This commit is contained in:
2026-04-17 18:30:00 +01:00
parent e9f4f67eee
commit c86efa6ee5

View File

@@ -36,6 +36,8 @@ export const createControllerMediaModule = ({
let cameraVideoElement = null;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const uploadFailureHint =
'Check MinIO bucket CORS and the public upload host/certificate if browser uploads are failing.';
const updateMotionDetectionRuntime = (updates) => {
patchAppState((state) => ({
@@ -148,6 +150,62 @@ export const createControllerMediaModule = ({
return parts.join(' · ');
};
const uploadRecordingBlobToStorage = async ({ uploadMeta, blob, streamSessionId = null, eventId = null }) => {
const contentType = blob.type || 'video/webm';
const recordingId = uploadMeta?.video?.id ?? null;
try {
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: blob
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
}
return {
mode: 'direct',
status: uploadResponse.status
};
} catch (directError) {
const directErrorMessage =
directError instanceof Error ? directError.message : String(directError);
console.warn('[recording.upload] direct upload failed, retrying via backend proxy', {
recordingId,
streamSessionId,
eventId,
objectKey: uploadMeta?.objectKey,
error: directErrorMessage
});
addActivity(
'Recording',
`Direct upload failed for ${uploadMeta?.objectKey ?? 'recording'}, retrying via backend proxy`
);
if (!recordingId) {
throw directError;
}
try {
const proxyUpload = await api.uploads.uploadRecordingBlob(recordingId, blob, contentType);
return {
mode: 'proxy',
status: 201,
proxyUpload
};
} catch (proxyError) {
const proxyErrorMessage =
proxyError instanceof Error ? proxyError.message : String(proxyError);
throw new Error(
`Direct upload failed (${directErrorMessage}); backend proxy upload failed (${proxyErrorMessage})`
);
}
}
};
const refreshCameraInputDevices = async () => {
if (!navigator.mediaDevices?.enumerateDevices) {
setAppState({ cameraInputDevices: [], selectedCameraInputId: '' });
@@ -566,23 +624,19 @@ export const createControllerMediaModule = ({
'Recording',
`Upload URL ready for ${uploadMeta.objectKey} via ${uploadOrigin}`
);
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': compressedBlob.type || 'video/webm' },
body: compressedBlob
const uploadResult = await uploadRecordingBlobToStorage({
uploadMeta,
blob: compressedBlob,
streamSessionId
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
}
console.info('[recording.upload] object uploaded', {
recordingId: recording.id,
streamSessionId,
objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket,
status: uploadResponse.status,
mode: uploadResult.mode,
status: uploadResult.status,
sizeBytes: compressedBlob.size
});
addActivity('Recording', `Upload completed for ${uploadMeta.objectKey}`);
@@ -604,7 +658,7 @@ export const createControllerMediaModule = ({
addActivity('Recording', 'Recording uploaded and finalized');
return true;
} catch (error) {
console.error('[recording.upload] stream upload failed, falling back to simulated key', {
console.error('[recording.upload] stream upload failed before object reached storage', {
recordingId: recording.id,
streamSessionId,
error: error instanceof Error ? error.message : String(error)
@@ -613,14 +667,9 @@ export const createControllerMediaModule = ({
'Recording',
`Upload failed for stream ${streamSessionId}: ${error instanceof Error ? error.message : 'unknown error'}`
);
const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`;
await api.events.finalizeRecording(recording.id, {
objectKey: fallbackObjectKey,
durationSeconds: captureResult?.durationSeconds ?? 6,
sizeBytes: captureResult?.blob?.size ?? 5000000
});
addActivity('Recording', 'Upload failed; finalized with simulator fallback');
return true;
addActivity('Recording', uploadFailureHint);
pushToast('Recording upload failed before reaching storage.', 'error');
return false;
}
}
@@ -672,23 +721,19 @@ export const createControllerMediaModule = ({
blobType: compressedBlob.type || 'video/webm'
});
addActivity('Recording', `Standalone upload URL ready via ${uploadOrigin}`);
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': compressedBlob.type || 'video/webm' },
body: compressedBlob
const uploadResult = await uploadRecordingBlobToStorage({
uploadMeta,
blob: compressedBlob,
eventId: lastMotionEventId
});
if (!uploadResponse.ok) {
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
}
console.info('[recording.upload] standalone object uploaded', {
eventId: lastMotionEventId,
recordingId: uploadMeta.video?.id,
objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket,
status: uploadResponse.status,
mode: uploadResult.mode,
status: uploadResult.status,
sizeBytes: compressedBlob.size
});
addActivity('Recording', `Standalone upload completed for ${uploadMeta.objectKey}`);
@@ -718,6 +763,8 @@ export const createControllerMediaModule = ({
'Recording',
`Standalone motion upload failed: ${error instanceof Error ? error.message : 'unknown error'}`
);
addActivity('Recording', uploadFailureHint);
pushToast('Motion clip upload failed before reaching storage.', 'error');
return false;
}
};