Skip to content

fix: start processing after buffered MP4 recordings finish uploading#1911

Merged
richiemcilroy merged 3 commits into
CapSoftware:mainfrom
ManthanNimodiya:fix/buffered-raw-recording-processing-trigger
Jun 12, 2026
Merged

fix: start processing after buffered MP4 recordings finish uploading#1911
richiemcilroy merged 3 commits into
CapSoftware:mainfrom
ManthanNimodiya:fix/buffered-raw-recording-processing-trigger

Conversation

@ManthanNimodiya

@ManthanNimodiya ManthanNimodiya commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #1902

Problem

The in-browser recorder has two upload pipelines: streaming-webm and buffered-raw.

The buffered-raw path never called anything to start server-side processing after the upload finished. As a result:

  • videoUploads.phase stayed "uploading" forever, even though uploaded === total.
  • getVideoStatus() bails out early whenever a videoUploads row exists (regardless of phase), so transcription/AI summaries never ran.
  • The share page polls GetUploadProgress every second indefinitely, since getStalledProcessingMessage only handles processing | generating_thumbnail | complete, not uploading.

This reproduces on any browser using the MP4 fallback recorder (Safari/Firefox), which is what the reporter hit on their self-hosted instance.

Fix

  • Added triggerInstantRecordingProcessing server action: verifies ownership, derives rawFileKey = ${ownerId}/${videoId}/result.mp4 , and calls startVideoProcessingWorkflow.
  • useWebRecorder.ts now calls this right after the buffered-raw upload completes, with a non-fatal warning toast on failure (matching the existing pattern for the streaming path).

Greptile Summary

Fixes the buffered-raw (MP4) recording path on Safari/Firefox, where server-side processing was never triggered after upload, leaving videoUploads.phase stuck at "uploading" forever.

  • Adds triggerInstantRecordingProcessing server action that transitions the upload phase to "processing" and starts processVideoWorkflow with rawFileKey = result.mp4.
  • Calls this action in useWebRecorder.ts immediately after the buffered-raw upload completes, with a non-fatal toast on failure so thumbnail upload is unaffected.
  • Guards cleanupRawUpload in process-video.ts so it is skipped when rawFileKey equals the output key (result.mp4), preventing the final video file from being deleted after transcoding.

Confidence Score: 5/5

Safe to merge. The three-part fix is coherent: the cleanup guard, the new server action, and the call site all work together correctly without introducing new failure modes.

The previously flagged deletion-of-result.mp4 bug is resolved by the rawFileKey !== outputKey guard. The new server action correctly threads through transitionVideoToProcessing (which updates rawFileKey in the DB before the workflow starts), so validateProcessingRequest's rawFileKey match check passes. The input/output sharing the same S3 key is safe because the media server fully downloads the source before writing output. Error handling in useWebRecorder.ts is non-fatal and consistent with the streaming path.

No files require special attention.

Important Files Changed

Filename Overview
apps/web/actions/video/trigger-instant-recording-processing.ts New server action that validates ownership and triggers processing for buffered-raw MP4 recordings; logic is straightforward and correctly uses the existing startVideoProcessingWorkflow infrastructure.
apps/web/workflows/process-video.ts Adds a guard so cleanupRawUpload is skipped when rawFileKey equals the outputKey (result.mp4), preventing deletion of the final video file when the buffered-raw path re-uses the same key for input and output.
apps/web/app/(org)/dashboard/caps/components/web-recorder-dialog/useWebRecorder.ts Calls triggerInstantRecordingProcessing immediately after the buffered-raw upload completes; error is caught non-fatally and continues with thumbnail upload, matching the existing pattern for the streaming path.

Reviews (3): Last reviewed commit: "fix(process-video): skip raw upload clea..." | Re-trigger Greptile

@superagent-security

Copy link
Copy Markdown

Superagent didn't find any vulnerabilities or security issues in this PR.

if (!video) throw new Error("Video not found");
if (video.ownerId !== user.id) throw new Error("Unauthorized");

const rawFileKey = `${video.ownerId}/${videoId}/result.mp4`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The action re-derives rawFileKey from video.ownerId but video.ownerId is redundant here — user.id is already in scope and is guaranteed to equal video.ownerId after the ownership check. Using video.ownerId is consistent with the rest of the codebase (e.g. upload.ts line 277), but mixing both variables makes the source of truth less clear. Using user.id directly removes the dependency on the DB field and makes the equivalence self-evident.

Suggested change
const rawFileKey = `${video.ownerId}/${videoId}/result.mp4`;
const rawFileKey = `${user.id}/${videoId}/result.mp4`;
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/video/trigger-instant-recording-processing.ts
Line: 26

Comment:
The action re-derives `rawFileKey` from `video.ownerId` but `video.ownerId` is redundant here — `user.id` is already in scope and is guaranteed to equal `video.ownerId` after the ownership check. Using `video.ownerId` is consistent with the rest of the codebase (e.g. `upload.ts` line 277), but mixing both variables makes the source of truth less clear. Using `user.id` directly removes the dependency on the DB field and makes the equivalence self-evident.

```suggestion
	const rawFileKey = `${user.id}/${videoId}/result.mp4`;
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

Comment on lines +28 to +36
await startVideoProcessingWorkflow({
videoId,
userId: user.id,
rawFileKey,
bucketId: video.bucket ?? null,
processingMessage: "Starting video processing...",
startFailureMessage: "Video uploaded, but processing could not start.",
mode: "singlepart",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 processVideoWorkflow deletes result.mp4 after processing

startVideoProcessingWorkflow calls processVideoWorkflow, which finishes with cleanupRawUpload(videoId, rawFileKey) — an unconditional bucket.deleteObject(rawFileKey). For the streaming-webm path rawFileKey is raw-upload.webm, a distinct intermediary that should be cleaned up. Here rawFileKey = "${user.id}/${videoId}/result.mp4", which is also the hardcoded outputKey written by the media server (${userId}/${videoId}/result.mp4 in process-video.ts line 255). After the media server successfully writes the transcoded output to result.mp4, cleanupRawUpload immediately deletes that same key, leaving the video unplayable.

The existing admin reprocess path (adminReprocessVideoWorkflow) handles exactly this case — it calls the media server in the same way but has no cleanup step. Either route through that workflow, or guard the cleanup in processVideoWorkflow so it skips deletion when rawFileKey equals the output key.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/video/trigger-instant-recording-processing.ts
Line: 28-36

Comment:
**`processVideoWorkflow` deletes `result.mp4` after processing**

`startVideoProcessingWorkflow` calls `processVideoWorkflow`, which finishes with `cleanupRawUpload(videoId, rawFileKey)` — an unconditional `bucket.deleteObject(rawFileKey)`. For the streaming-webm path `rawFileKey` is `raw-upload.webm`, a distinct intermediary that should be cleaned up. Here `rawFileKey = "${user.id}/${videoId}/result.mp4"`, which is also the hardcoded `outputKey` written by the media server (`${userId}/${videoId}/result.mp4` in `process-video.ts` line 255). After the media server successfully writes the transcoded output to `result.mp4`, `cleanupRawUpload` immediately deletes that same key, leaving the video unplayable.

The existing admin reprocess path (`adminReprocessVideoWorkflow`) handles exactly this case — it calls the media server in the same way but has no cleanup step. Either route through that workflow, or guard the cleanup in `processVideoWorkflow` so it skips deletion when `rawFileKey` equals the output key.

How can I resolve this? If you propose a fix, please make it concise.

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@greptileai please re-review

@richiemcilroy

Copy link
Copy Markdown
Member

Hey @ManthanNimodiya, have you tested this locally fully?

@ManthanNimodiya

Copy link
Copy Markdown
Contributor Author

@richiemcilroy,
tested it locally, full stack (mysql/minio/media-server/dev server), both via a script hitting the real upload+process flow and an actual browser recording (Safari UA spoofed to hit the buffered-MP4 path where this bug applies).

Video processed fully both times and result.mp4 survived afterward whereas before the fix, it got deleted right after processing.

@richiemcilroy richiemcilroy merged commit 0c5848b into CapSoftware:main Jun 12, 2026
15 of 16 checks passed
@richiemcilroy

Copy link
Copy Markdown
Member

/tip $40

@algora-pbc

algora-pbc Bot commented Jun 12, 2026

Copy link
Copy Markdown

Please visit Algora to complete your tip via Stripe.

@algora-pbc

algora-pbc Bot commented Jun 12, 2026

Copy link
Copy Markdown

🎉🎈 @ManthanNimodiya has been awarded $40 by Cap! 🎈🎊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Video upload never completes on self-hosted Cap

2 participants