fix: retry failed recording uploads via backend proxy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user