Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/web/actions/video/trigger-instant-recording-processing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use server";

import { db } from "@cap/database";
import { getCurrentUser } from "@cap/database/auth/session";
import { videos } from "@cap/database/schema";
import type { Video } from "@cap/web-domain";
import { eq } from "drizzle-orm";
import { startVideoProcessingWorkflow } from "@/lib/video-processing";

export async function triggerInstantRecordingProcessing({
videoId,
}: {
videoId: Video.VideoId;
}): Promise<{ success: boolean }> {
const user = await getCurrentUser();
if (!user) throw new Error("Unauthorized");

const [video] = await db()
.select()
.from(videos)
.where(eq(videos.id, videoId));

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

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

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",
});
Comment on lines +28 to +36

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.


return { success: true };
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Cause, Exit, Option } from "effect";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { triggerInstantRecordingProcessing } from "@/actions/video/trigger-instant-recording-processing";
import { createVideoAndGetUploadUrl } from "@/actions/video/upload";
import { useEffectMutation, useRpcClient } from "@/lib/EffectRuntime";
import { ThumbnailRequest } from "@/lib/Requests/ThumbnailRequest";
Expand Down Expand Up @@ -1298,6 +1299,17 @@ export const useWebRecorder = ({
setUploadStatus,
);

try {
await triggerInstantRecordingProcessing({
videoId: creationResult.id,
});
} catch (processingError) {
console.error("Failed to start video processing", processingError);
toast.warning(
"Recording uploaded. Processing did not start yet, but the original recording is available.",
);
}

if (thumbnailBlob) {
try {
const screenshotData = await createVideoAndGetUploadUrl({
Expand Down
6 changes: 5 additions & 1 deletion apps/web/workflows/process-video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export async function processVideoWorkflow(
);

await saveMetadataAndComplete(videoId, result.metadata);
await cleanupRawUpload(videoId, rawFileKey);

const outputKey = `${userId}/${videoId}/result.mp4`;
if (rawFileKey !== outputKey) {
await cleanupRawUpload(videoId, rawFileKey);
}

return {
success: true,
Expand Down
Loading