fix(webapp): log recording upload failures and restore 6s clips

This commit is contained in:
2026-04-15 11:15:00 +01:00
parent 995cecd4ac
commit 9dc202ce03

View File

@@ -34,7 +34,7 @@ const MOTION_DETECTION_PROFILES = {
releaseThreshold: 0.05, releaseThreshold: 0.05,
consecutiveTriggerFrames: 3, consecutiveTriggerFrames: 3,
cooldownMs: 12000, cooldownMs: 12000,
minimumEventMs: 9000 minimumEventMs: 6000
}, },
balanced: { balanced: {
profile: 'balanced', profile: 'balanced',
@@ -46,7 +46,7 @@ const MOTION_DETECTION_PROFILES = {
releaseThreshold: 0.04, releaseThreshold: 0.04,
consecutiveTriggerFrames: 3, consecutiveTriggerFrames: 3,
cooldownMs: 9000, cooldownMs: 9000,
minimumEventMs: 8000 minimumEventMs: 6000
}, },
responsive: { responsive: {
profile: 'responsive', profile: 'responsive',
@@ -58,7 +58,7 @@ const MOTION_DETECTION_PROFILES = {
releaseThreshold: 0.035, releaseThreshold: 0.035,
consecutiveTriggerFrames: 2, consecutiveTriggerFrames: 2,
cooldownMs: 7000, cooldownMs: 7000,
minimumEventMs: 7000 minimumEventMs: 6000
} }
}; };
@@ -1478,6 +1478,24 @@ const startOfferToClient = async (streamSessionId, requesterDeviceId) => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const summarizeFetchFailure = async (response) => {
if (!response) {
return 'no response returned';
}
const parts = [`status ${response.status}`];
try {
const bodyText = (await response.text()).trim();
if (bodyText) {
parts.push(bodyText.slice(0, 240));
}
} catch {
// Ignore body parsing failures for diagnostics.
}
return parts.join(' · ');
};
const finalizeRecordingForStream = async (streamSessionId, captureResult) => { const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
const currentDevice = getAppState().device; const currentDevice = getAppState().device;
if (!currentDevice?.id) { if (!currentDevice?.id) {
@@ -1496,6 +1514,8 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
if (!captureResult?.blob || captureResult.blob.size === 0) { if (!captureResult?.blob || captureResult.blob.size === 0) {
throw new Error('No captured video blob to upload'); throw new Error('No captured video blob to upload');
} }
addActivity('Recording', `Preparing upload for stream ${streamSessionId}`);
const compressedBlob = await compressRecordingBlob(captureResult.blob); const compressedBlob = await compressRecordingBlob(captureResult.blob);
const uploadMeta = await api.request('/videos/upload-url', { const uploadMeta = await api.request('/videos/upload-url', {
@@ -1507,6 +1527,26 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
recordingId: recording.id recordingId: recording.id
}) })
}); });
const uploadOrigin = (() => {
try {
return new URL(uploadMeta.uploadUrl).origin;
} catch {
return 'invalid upload URL';
}
})();
console.info('[recording.upload] upload URL issued', {
recordingId: recording.id,
streamSessionId,
objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket,
uploadOrigin,
blobSize: compressedBlob.size,
blobType: compressedBlob.type || 'video/webm'
});
addActivity(
'Recording',
`Upload URL ready for ${uploadMeta.objectKey} via ${uploadOrigin}`
);
const uploadResponse = await fetch(uploadMeta.uploadUrl, { const uploadResponse = await fetch(uploadMeta.uploadUrl, {
method: 'PUT', method: 'PUT',
@@ -1515,9 +1555,19 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
}); });
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`); 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,
sizeBytes: compressedBlob.size
});
addActivity('Recording', `Upload completed for ${uploadMeta.objectKey}`);
await api.events.finalizeRecording(recording.id, { await api.events.finalizeRecording(recording.id, {
objectKey: uploadMeta.objectKey, objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket, bucket: uploadMeta.bucket,
@@ -1525,14 +1575,29 @@ const finalizeRecordingForStream = async (streamSessionId, captureResult) => {
sizeBytes: compressedBlob.size sizeBytes: compressedBlob.size
}); });
console.info('[recording.upload] recording finalized', {
recordingId: recording.id,
streamSessionId,
objectKey: uploadMeta.objectKey,
durationSeconds: captureResult.durationSeconds,
sizeBytes: compressedBlob.size
});
addActivity('Recording', 'Recording uploaded and finalized'); addActivity('Recording', 'Recording uploaded and finalized');
return true; return true;
} catch (error) { } catch (error) {
console.error('Recording upload failed, falling back to simulated key', error); console.error('[recording.upload] stream upload failed, falling back to simulated key', {
recordingId: recording.id,
streamSessionId,
error: error instanceof Error ? error.message : String(error)
});
addActivity(
'Recording',
`Upload failed for stream ${streamSessionId}: ${error instanceof Error ? error.message : 'unknown error'}`
);
const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`; const fallbackObjectKey = `sim/${streamSessionId}/${Date.now()}.webm`;
await api.events.finalizeRecording(recording.id, { await api.events.finalizeRecording(recording.id, {
objectKey: fallbackObjectKey, objectKey: fallbackObjectKey,
durationSeconds: captureResult?.durationSeconds ?? 15, durationSeconds: captureResult?.durationSeconds ?? 6,
sizeBytes: captureResult?.blob?.size ?? 5000000 sizeBytes: captureResult?.blob?.size ?? 5000000
}); });
addActivity('Recording', 'Upload failed; finalized with simulator fallback'); addActivity('Recording', 'Upload failed; finalized with simulator fallback');
@@ -1560,6 +1625,7 @@ const uploadStandaloneMotionRecording = async (captureResult) => {
} }
try { try {
addActivity('Recording', 'Preparing standalone motion clip upload');
const compressedBlob = await compressRecordingBlob(captureResult.blob); const compressedBlob = await compressRecordingBlob(captureResult.blob);
const uploadMeta = await api.request('/videos/upload-url', { const uploadMeta = await api.request('/videos/upload-url', {
method: 'POST', method: 'POST',
@@ -1570,6 +1636,23 @@ const uploadStandaloneMotionRecording = async (captureResult) => {
eventId: lastMotionEventId eventId: lastMotionEventId
}) })
}); });
const uploadOrigin = (() => {
try {
return new URL(uploadMeta.uploadUrl).origin;
} catch {
return 'invalid upload URL';
}
})();
console.info('[recording.upload] standalone upload URL issued', {
eventId: lastMotionEventId,
recordingId: uploadMeta.video?.id,
objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket,
uploadOrigin,
blobSize: compressedBlob.size,
blobType: compressedBlob.type || 'video/webm'
});
addActivity('Recording', `Standalone upload URL ready via ${uploadOrigin}`);
const uploadResponse = await fetch(uploadMeta.uploadUrl, { const uploadResponse = await fetch(uploadMeta.uploadUrl, {
method: 'PUT', method: 'PUT',
@@ -1578,9 +1661,19 @@ const uploadStandaloneMotionRecording = async (captureResult) => {
}); });
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
throw new Error(`Upload failed with status ${uploadResponse.status}`); 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,
sizeBytes: compressedBlob.size
});
addActivity('Recording', `Standalone upload completed for ${uploadMeta.objectKey}`);
await api.events.finalizeRecording(uploadMeta.video.id, { await api.events.finalizeRecording(uploadMeta.video.id, {
objectKey: uploadMeta.objectKey, objectKey: uploadMeta.objectKey,
bucket: uploadMeta.bucket, bucket: uploadMeta.bucket,
@@ -1588,11 +1681,24 @@ const uploadStandaloneMotionRecording = async (captureResult) => {
sizeBytes: compressedBlob.size sizeBytes: compressedBlob.size
}); });
console.info('[recording.upload] standalone recording finalized', {
eventId: lastMotionEventId,
recordingId: uploadMeta.video?.id,
objectKey: uploadMeta.objectKey,
durationSeconds: captureResult.durationSeconds,
sizeBytes: compressedBlob.size
});
addActivity('Recording', `Motion clip uploaded (${uploadMeta.objectKey})`); addActivity('Recording', `Motion clip uploaded (${uploadMeta.objectKey})`);
return true; return true;
} catch (error) { } catch (error) {
console.error('Standalone motion upload failed', error); console.error('[recording.upload] standalone motion upload failed', {
addActivity('Recording', 'Standalone motion upload failed'); eventId: lastMotionEventId,
error: error instanceof Error ? error.message : String(error)
});
addActivity(
'Recording',
`Standalone motion upload failed: ${error instanceof Error ? error.message : 'unknown error'}`
);
return false; return false;
} }
}; };