Skip to content

Commit 8f73005

Browse files
author
Miriad
committed
fix: decouple Remotion render from webhook to avoid 60s timeout
Split the monolithic pipeline into two phases: Phase 1 (webhook, <55s): validate → TTS → upload → B-roll → start renders → store IDs Phase 2 (check-renders cron): poll render progress → download → upload → video_gen New endpoint: /api/cron/check-renders - Queries docs with status "rendering" - Checks Remotion Lambda progress - Downloads and uploads completed videos to Sanity New schema field: renderData (stores render IDs for tracking)
1 parent c6d5923 commit 8f73005

5 files changed

Lines changed: 409 additions & 204 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
export const fetchCache = 'force-no-store';
2+
3+
import { type NextRequest } from 'next/server';
4+
import { createClient } from 'next-sanity';
5+
import { apiVersion, dataset, projectId } from '@/sanity/lib/api';
6+
import { checkBothRenders } from '@/lib/services/remotion';
7+
import { uploadVideoToSanity } from '@/lib/services/sanity-upload';
8+
9+
// --- Types ---
10+
11+
interface RenderingDoc {
12+
_id: string;
13+
title?: string;
14+
renderData: {
15+
mainRenderId: string;
16+
shortRenderId: string;
17+
bucketName: string;
18+
startedAt: string;
19+
};
20+
}
21+
22+
interface ProcessResult {
23+
id: string;
24+
title?: string;
25+
status: 'completed' | 'rendering' | 'error';
26+
mainProgress?: number;
27+
shortProgress?: number;
28+
error?: string;
29+
}
30+
31+
// --- Sanity Write Client ---
32+
33+
function getSanityWriteClient() {
34+
const token = process.env.SANITY_API_TOKEN || process.env.SANITY_API_WRITE_TOKEN;
35+
if (!token) {
36+
throw new Error('[CHECK-RENDERS] Missing SANITY_API_TOKEN environment variable');
37+
}
38+
return createClient({ projectId, dataset, apiVersion, token, useCdn: false });
39+
}
40+
41+
// --- Route Handler ---
42+
43+
/**
44+
* Cron endpoint: Check Remotion Lambda render progress for all documents
45+
* in "rendering" status. When renders complete, download videos, upload
46+
* to Sanity, and advance status to "video_gen".
47+
*
48+
* Triggered hourly by Vercel cron, or manually via curl.
49+
*/
50+
export async function GET(request: NextRequest) {
51+
// Auth check
52+
const authHeader = request.headers.get('authorization');
53+
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
54+
console.error('[CHECK-RENDERS] Unauthorized request');
55+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
56+
}
57+
58+
console.log('[CHECK-RENDERS] Starting render progress check...');
59+
60+
const client = getSanityWriteClient();
61+
62+
// Find all docs in "rendering" status with render data
63+
const renderingDocs = await client.fetch<RenderingDoc[]>(
64+
`*[_type == "automatedVideo" && status == "rendering" && defined(renderData)]{
65+
_id, title, renderData
66+
}`
67+
);
68+
69+
if (renderingDocs.length === 0) {
70+
console.log('[CHECK-RENDERS] No documents in "rendering" status');
71+
return Response.json({ processed: 0, results: [] });
72+
}
73+
74+
console.log(`[CHECK-RENDERS] Found ${renderingDocs.length} document(s) to check`);
75+
76+
const results: ProcessResult[] = [];
77+
78+
for (const doc of renderingDocs) {
79+
try {
80+
console.log(`[CHECK-RENDERS] Checking renders for "${doc.title || doc._id}"...`);
81+
82+
const progress = await checkBothRenders(
83+
doc.renderData.mainRenderId,
84+
doc.renderData.shortRenderId,
85+
doc.renderData.bucketName
86+
);
87+
88+
// Check for render errors
89+
if (progress.main.errors || progress.short.errors) {
90+
const errorMsg = [progress.main.errors, progress.short.errors]
91+
.filter(Boolean)
92+
.join('; ');
93+
console.error(`[CHECK-RENDERS] Render error for ${doc._id}: ${errorMsg}`);
94+
95+
await client.patch(doc._id).set({
96+
status: 'flagged',
97+
flaggedReason: `Remotion render failed: ${errorMsg}`,
98+
}).commit();
99+
100+
results.push({ id: doc._id, title: doc.title, status: 'error', error: errorMsg });
101+
continue;
102+
}
103+
104+
if (progress.allDone) {
105+
console.log(`[CHECK-RENDERS] Both renders done for "${doc.title || doc._id}", downloading...`);
106+
107+
// Download rendered videos from Remotion S3
108+
const [mainVideoResponse, shortVideoResponse] = await Promise.all([
109+
fetch(progress.main.outputUrl!),
110+
fetch(progress.short.outputUrl!),
111+
]);
112+
113+
if (!mainVideoResponse.ok) {
114+
throw new Error(`Failed to download main video: ${mainVideoResponse.status}`);
115+
}
116+
if (!shortVideoResponse.ok) {
117+
throw new Error(`Failed to download short video: ${shortVideoResponse.status}`);
118+
}
119+
120+
const [mainVideoBuffer, shortVideoBuffer] = await Promise.all([
121+
Buffer.from(await mainVideoResponse.arrayBuffer()),
122+
Buffer.from(await shortVideoResponse.arrayBuffer()),
123+
]);
124+
125+
console.log(
126+
`[CHECK-RENDERS] Downloaded — main: ${mainVideoBuffer.length} bytes, short: ${shortVideoBuffer.length} bytes`
127+
);
128+
129+
// Upload to Sanity
130+
const [mainUploadResult, shortUploadResult] = await Promise.all([
131+
uploadVideoToSanity(mainVideoBuffer, `${doc._id}-main.mp4`),
132+
uploadVideoToSanity(shortVideoBuffer, `${doc._id}-short.mp4`),
133+
]);
134+
135+
console.log(
136+
`[CHECK-RENDERS] Uploaded — main: ${mainUploadResult.url}, short: ${shortUploadResult.url}`
137+
);
138+
139+
// Update Sanity document with video URLs and advance to video_gen
140+
await client.patch(doc._id).set({
141+
status: 'video_gen',
142+
videoUrl: mainUploadResult.url,
143+
videoFile: {
144+
_type: 'file',
145+
asset: { _type: 'reference', _ref: mainUploadResult.assetId },
146+
},
147+
shortUrl: shortUploadResult.url,
148+
shortFile: {
149+
_type: 'file',
150+
asset: { _type: 'reference', _ref: shortUploadResult.assetId },
151+
},
152+
}).commit();
153+
154+
console.log(`[CHECK-RENDERS] ✅ ${doc._id} → video_gen`);
155+
results.push({ id: doc._id, title: doc.title, status: 'completed' });
156+
} else {
157+
console.log(
158+
`[CHECK-RENDERS] Still rendering "${doc.title || doc._id}" — ` +
159+
`main: ${progress.main.progress}%, short: ${progress.short.progress}%`
160+
);
161+
results.push({
162+
id: doc._id,
163+
title: doc.title,
164+
status: 'rendering',
165+
mainProgress: progress.main.progress,
166+
shortProgress: progress.short.progress,
167+
});
168+
}
169+
} catch (error) {
170+
const errorMessage = error instanceof Error ? error.message : String(error);
171+
console.error(`[CHECK-RENDERS] ❌ Error processing ${doc._id}: ${errorMessage}`);
172+
173+
// Flag the document so it doesn't get stuck
174+
try {
175+
await client.patch(doc._id).set({
176+
status: 'flagged',
177+
flaggedReason: `check-renders error: ${errorMessage}`,
178+
}).commit();
179+
} catch (patchError) {
180+
console.error(`[CHECK-RENDERS] Failed to flag ${doc._id}:`, patchError);
181+
}
182+
183+
results.push({ id: doc._id, title: doc.title, status: 'error', error: errorMessage });
184+
}
185+
}
186+
187+
console.log(`[CHECK-RENDERS] Done. Processed ${results.length} document(s).`);
188+
return Response.json({ processed: results.length, results });
189+
}

0 commit comments

Comments
 (0)