fix: retry failed recording uploads via backend proxy
This commit is contained in:
@@ -36,6 +36,8 @@ export const createControllerMediaModule = ({
|
|||||||
let cameraVideoElement = null;
|
let cameraVideoElement = null;
|
||||||
|
|
||||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
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) => {
|
const updateMotionDetectionRuntime = (updates) => {
|
||||||
patchAppState((state) => ({
|
patchAppState((state) => ({
|
||||||
@@ -148,6 +150,62 @@ export const createControllerMediaModule = ({
|
|||||||
return parts.join(' · ');
|
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 () => {
|
const refreshCameraInputDevices = async () => {
|
||||||
if (!navigator.mediaDevices?.enumerateDevices) {
|
if (!navigator.mediaDevices?.enumerateDevices) {
|
||||||
setAppState({ cameraInputDevices: [], selectedCameraInputId: '' });
|
setAppState({ cameraInputDevices: [], selectedCameraInputId: '' });
|
||||||
@@ -566,23 +624,19 @@ export const createControllerMediaModule = ({
|
|||||||
'Recording',
|
'Recording',
|
||||||
`Upload URL ready for ${uploadMeta.objectKey} via ${uploadOrigin}`
|
`Upload URL ready for ${uploadMeta.objectKey} via ${uploadOrigin}`
|
||||||
);
|
);
|
||||||
|
const uploadResult = await uploadRecordingBlobToStorage({
|
||||||
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
|
uploadMeta,
|
||||||
method: 'PUT',
|
blob: compressedBlob,
|
||||||
headers: { 'Content-Type': compressedBlob.type || 'video/webm' },
|
streamSessionId
|
||||||
body: compressedBlob
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
|
||||||
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info('[recording.upload] object uploaded', {
|
console.info('[recording.upload] object uploaded', {
|
||||||
recordingId: recording.id,
|
recordingId: recording.id,
|
||||||
streamSessionId,
|
streamSessionId,
|
||||||
objectKey: uploadMeta.objectKey,
|
objectKey: uploadMeta.objectKey,
|
||||||
bucket: uploadMeta.bucket,
|
bucket: uploadMeta.bucket,
|
||||||
status: uploadResponse.status,
|
mode: uploadResult.mode,
|
||||||
|
status: uploadResult.status,
|
||||||
sizeBytes: compressedBlob.size
|
sizeBytes: compressedBlob.size
|
||||||
});
|
});
|
||||||
addActivity('Recording', `Upload completed for ${uploadMeta.objectKey}`);
|
addActivity('Recording', `Upload completed for ${uploadMeta.objectKey}`);
|
||||||
@@ -604,7 +658,7 @@ export const createControllerMediaModule = ({
|
|||||||
addActivity('Recording', 'Recording uploaded and finalized');
|
addActivity('Recording', 'Recording uploaded and finalized');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} 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,
|
recordingId: recording.id,
|
||||||
streamSessionId,
|
streamSessionId,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error)
|
||||||
@@ -613,14 +667,9 @@ export const createControllerMediaModule = ({
|
|||||||
'Recording',
|
'Recording',
|
||||||
`Upload failed for stream ${streamSessionId}: ${error instanceof Error ? error.message : 'unknown error'}`
|
`Upload failed for stream ${streamSessionId}: ${error instanceof Error ? error.message : 'unknown error'}`
|
||||||
);
|
);
|
||||||
const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`;
|
addActivity('Recording', uploadFailureHint);
|
||||||
await api.events.finalizeRecording(recording.id, {
|
pushToast('Recording upload failed before reaching storage.', 'error');
|
||||||
objectKey: fallbackObjectKey,
|
return false;
|
||||||
durationSeconds: captureResult?.durationSeconds ?? 6,
|
|
||||||
sizeBytes: captureResult?.blob?.size ?? 5000000
|
|
||||||
});
|
|
||||||
addActivity('Recording', 'Upload failed; finalized with simulator fallback');
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,23 +721,19 @@ export const createControllerMediaModule = ({
|
|||||||
blobType: compressedBlob.type || 'video/webm'
|
blobType: compressedBlob.type || 'video/webm'
|
||||||
});
|
});
|
||||||
addActivity('Recording', `Standalone upload URL ready via ${uploadOrigin}`);
|
addActivity('Recording', `Standalone upload URL ready via ${uploadOrigin}`);
|
||||||
|
const uploadResult = await uploadRecordingBlobToStorage({
|
||||||
const uploadResponse = await fetch(uploadMeta.uploadUrl, {
|
uploadMeta,
|
||||||
method: 'PUT',
|
blob: compressedBlob,
|
||||||
headers: { 'Content-Type': compressedBlob.type || 'video/webm' },
|
eventId: lastMotionEventId
|
||||||
body: compressedBlob
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!uploadResponse.ok) {
|
|
||||||
throw new Error(`Upload failed: ${await summarizeFetchFailure(uploadResponse)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info('[recording.upload] standalone object uploaded', {
|
console.info('[recording.upload] standalone object uploaded', {
|
||||||
eventId: lastMotionEventId,
|
eventId: lastMotionEventId,
|
||||||
recordingId: uploadMeta.video?.id,
|
recordingId: uploadMeta.video?.id,
|
||||||
objectKey: uploadMeta.objectKey,
|
objectKey: uploadMeta.objectKey,
|
||||||
bucket: uploadMeta.bucket,
|
bucket: uploadMeta.bucket,
|
||||||
status: uploadResponse.status,
|
mode: uploadResult.mode,
|
||||||
|
status: uploadResult.status,
|
||||||
sizeBytes: compressedBlob.size
|
sizeBytes: compressedBlob.size
|
||||||
});
|
});
|
||||||
addActivity('Recording', `Standalone upload completed for ${uploadMeta.objectKey}`);
|
addActivity('Recording', `Standalone upload completed for ${uploadMeta.objectKey}`);
|
||||||
@@ -718,6 +763,8 @@ export const createControllerMediaModule = ({
|
|||||||
'Recording',
|
'Recording',
|
||||||
`Standalone motion upload failed: ${error instanceof Error ? error.message : 'unknown error'}`
|
`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;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user