67 lines
2.7 KiB
TypeScript
67 lines
2.7 KiB
TypeScript
import { NextResponse, type NextRequest } from 'next/server';
|
|
import { apiError } from '@/lib/server/api-response';
|
|
import { proxyFetch } from '@/lib/server/proxy-fetch';
|
|
import { resolveRenderServiceUrl } from '@/lib/server/render-service';
|
|
import { createLogger } from '@/lib/logger';
|
|
|
|
const log = createLogger('ExportVideo Download API');
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
/**
|
|
* Stream the rendered MP4 back to the browser.
|
|
*
|
|
* Uses `redirect: 'manual'` so that if the render service's artifact store
|
|
* returns a `302` (a presigned object-storage URL, in a demo-scale deployment)
|
|
* we pass that redirect straight through and the browser downloads directly
|
|
* from storage — bypassing this proxy. The OSS default (local-disk artifacts)
|
|
* streams the bytes through here.
|
|
*/
|
|
export async function GET(req: NextRequest, context: { params: Promise<{ jobId: string }> }) {
|
|
const { jobId } = await context.params;
|
|
const resolved = resolveRenderServiceUrl();
|
|
if ('error' in resolved) {
|
|
return apiError('PROVIDER_DISABLED', 501, 'Render service is not configured');
|
|
}
|
|
|
|
// Bound only the time to obtain the response headers — NOT the body stream.
|
|
// A total-duration timeout would truncate a large MP4 over a slow connection,
|
|
// so we abort just the initial fetch and clear the timer once headers arrive.
|
|
const controller = new AbortController();
|
|
const headerTimeout = setTimeout(() => controller.abort(), 30_000);
|
|
try {
|
|
const upstream = await proxyFetch(
|
|
`${resolved.url}/render/${encodeURIComponent(jobId)}/download`,
|
|
{ method: 'GET', redirect: 'manual', signal: controller.signal },
|
|
);
|
|
clearTimeout(headerTimeout);
|
|
|
|
// Presigned-URL artifact store: hand the redirect to the browser.
|
|
if (upstream.status === 302 || upstream.status === 301) {
|
|
const location = upstream.headers.get('location');
|
|
if (location) return NextResponse.redirect(location, 302);
|
|
}
|
|
|
|
if (!upstream.ok || !upstream.body) {
|
|
const status = upstream.status === 404 || upstream.status === 409 ? upstream.status : 502;
|
|
return apiError('UPSTREAM_ERROR', status, 'Render output not available');
|
|
}
|
|
|
|
return new NextResponse(upstream.body, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'video/mp4',
|
|
...(upstream.headers.get('content-length')
|
|
? { 'Content-Length': upstream.headers.get('content-length')! }
|
|
: {}),
|
|
'Content-Disposition': `attachment; filename="${jobId}.mp4"`,
|
|
'Cache-Control': 'private, no-store',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
clearTimeout(headerTimeout);
|
|
log.error(`Failed to download render output ${jobId}:`, error);
|
|
return apiError('UPSTREAM_ERROR', 502, 'Failed to reach render service');
|
|
}
|
|
}
|