diff --git a/renderer/src/controllers/video.controller.ts b/renderer/src/controllers/video.controller.ts index 94d5568..b731397 100644 --- a/renderer/src/controllers/video.controller.ts +++ b/renderer/src/controllers/video.controller.ts @@ -4,6 +4,7 @@ import { logger } from "../logger"; import { GeneratedAssetSchema } from "../schema"; import { AppError, handleError } from "../utils/error"; import Stream from "@elysiajs/stream"; +import type { ProgressData } from "../types"; export function setupVideoRoutes(app: Elysia) { const videoService = new VideoService(); @@ -21,32 +22,33 @@ export function setupVideoRoutes(app: Elysia) { return `data: ${JSON.stringify(data)}\n\n`; } - const progressQueue: number[] = []; + const progressQueue: ProgressData[] = []; let done = false; let error: Error | null = null; - function getNextProgress() { + function getNextProgress(): Promise { if (progressQueue.length > 0) { return Promise.resolve(progressQueue.shift()!); } - return new Promise((resolve) => { + return new Promise((resolve) => { function checkQueue() { if (progressQueue.length > 0) { resolve(progressQueue.shift()!); } else if (done) { - resolve(-1); + resolve(null); } else { setTimeout(checkQueue, 1000); } } + checkQueue(); }); } videoService - .renderVideo(body, async (progress) => { - progressQueue.push(progress); + .renderVideo(body, async (progressData) => { + progressQueue.push(progressData); }) .then(() => { done = true; @@ -58,11 +60,18 @@ export function setupVideoRoutes(app: Elysia) { try { while (!done) { - const progress = await getNextProgress(); + const progressData = await getNextProgress(); - if (progress >= 0) { + if (progressData && progressData.details) { + yield formatSSE({ + progress: Math.round(progressData.progress), + stage: progressData.stage, + details: progressData.details, + }); + } else if (progressData) { yield formatSSE({ - progress: Math.round(progress), + progress: Math.round(progressData.progress), + stage: progressData.stage, }); } } diff --git a/renderer/src/services/video.service.ts b/renderer/src/services/video.service.ts index ac8f2bc..423d43d 100644 --- a/renderer/src/services/video.service.ts +++ b/renderer/src/services/video.service.ts @@ -34,6 +34,11 @@ export class VideoService { progressCallback ); + await progressCallback({ + progress: 100, + stage: "COMPLETE", + }); + logger.info( { configId: data.configId, outputPath }, "Video generation completed" @@ -80,7 +85,10 @@ export class VideoService { entryPoint: path.join(process.cwd(), "./src/remotion/index.ts"), onProgress: async (progress: number) => { logger.debug({ progress }, "Bundling progress"); - await progressCallback(progress / 2); + await progressCallback({ + progress: progress / 2, + stage: "STARTING", + }); }, }); } catch (error) { @@ -133,7 +141,7 @@ export class VideoService { await renderMedia({ composition, serveUrl: bundled, - codec: "h264", + codec: "h265", outputLocation, inputProps, timeoutInMilliseconds: 30 * 1000, @@ -157,7 +165,20 @@ export class VideoService { "Rendering progress" ); - await progressCallback(50 + progress * 50); + const stage = stitchStage === "encoding" ? "RENDERING" : "ENCODING"; + + await progressCallback({ + progress: 50 + progress * 50, + stage, + details: { + renderedFrames, + encodedFrames, + // @ts-ignore + renderedDoneIn, + // @ts-ignore + encodedDoneIn, + }, + }); }, }); diff --git a/renderer/src/types.ts b/renderer/src/types.ts index e2615bc..e5b6484 100644 --- a/renderer/src/types.ts +++ b/renderer/src/types.ts @@ -3,7 +3,7 @@ import { GeneratedAssetSchema } from "./schema"; import { t, type Static } from "elysia"; export type VideoGenerationRequest = Static; -export type ProgressCallback = (progress: number) => Promise; +export type ProgressCallback = (data: ProgressData) => Promise; export interface HealthCheckResponse { status: "ok"; @@ -11,3 +11,16 @@ export interface HealthCheckResponse { version: string; uptime: number; } + +export type ProgressStage = "STARTING" | "RENDERING" | "ENCODING" | "COMPLETE"; + +export type ProgressData = { + progress: number; + stage: ProgressStage; + details?: { + renderedFrames: number; + encodedFrames?: number; + renderedDoneIn?: number; + encodedDoneIn?: number; + }; +}; diff --git a/web/src/app/(history)/history/(item)/[id]/generate.tsx b/web/src/app/(history)/history/(item)/[id]/generate.tsx index 3c1ba93..217b83b 100644 --- a/web/src/app/(history)/history/(item)/[id]/generate.tsx +++ b/web/src/app/(history)/history/(item)/[id]/generate.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useTransition, useEffect } from "react"; +import { useState, useTransition } from "react"; import { startGeneration } from "@/app/(history)/history/(item)/[id]/action"; import { Button } from "@/components/ui/button"; import { Progress } from "@/components/ui/progress"; @@ -15,6 +15,19 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Loader2, AlertCircle, CheckCircle2, RefreshCcw } from "lucide-react"; import { GeneratedAssetType } from "@/types"; +type ProgressStage = "STARTING" | "RENDERING" | "ENCODING" | "COMPLETE"; + +type ProgressData = { + progress: number; + stage: ProgressStage; + details?: { + renderedFrames: number; + encodedFrames?: number; + renderedDoneIn?: number; + encodedDoneIn?: number; + }; +}; + interface GenerationError { error: string; details?: string; @@ -22,46 +35,52 @@ interface GenerationError { } const statusMessages = { - starting: "Preparing to start video generation...", + STARTING: "Preparing to start video generation...", + RENDERING: (frames: number) => + `Rendering video... (${frames} frames rendered)`, + ENCODING: (frames: number) => `Encoding video... (${frames} frames encoded)`, + COMPLETE: "Video generation completed!", retrying: (count: number) => `Attempt ${count} of 3 to retry generation...`, - rendering: (frames: number) => `Rendering video... (${frames} frames)`, - encoding: (frames: number) => `Encoding video... (${frames} frames)`, - complete: "Video generation completed!", - processing: "Processing...", error: "An error occurred during generation.", }; export function Generate({ asset }: { asset: GeneratedAssetType }) { const [isPending, startTransition] = useTransition(); - const [progress, setProgress] = useState(0); + const [progress, setProgress] = useState({ + progress: 0, + stage: "STARTING", + }); const [status, setStatus] = useState(""); const [url, setUrl] = useState(""); const [error, setError] = useState(null); const [retryCount, setRetryCount] = useState(0); - function setStatusMessage(stage: string, details?: any) { + function setStatusMessage(data: ProgressData) { + const { stage, details } = data; switch (stage) { case "STARTING": - setStatus(statusMessages.starting); + setStatus(statusMessages.STARTING); break; case "RENDERING": - setStatus(statusMessages.rendering(details?.renderedFrames)); + setStatus(statusMessages.RENDERING(details?.renderedFrames || 0)); break; case "ENCODING": - setStatus(statusMessages.encoding(details?.encodedFrames)); + setStatus(statusMessages.ENCODING(details?.encodedFrames || 0)); break; case "COMPLETE": - setStatus(statusMessages.complete); - setUrl(details?.signedUrl || ""); + setStatus(statusMessages.COMPLETE); break; - default: - setStatus(statusMessages.processing); } } async function handleGeneration(isRetry = false) { - if (!isRetry) resetState(); - else { + if (!isRetry) { + setProgress({ progress: 0, stage: "STARTING" }); + setStatus(statusMessages.STARTING); + setError(null); + setUrl(""); + setRetryCount(0); + } else { setRetryCount((prev) => prev + 1); setStatus(statusMessages.retrying(retryCount + 1)); setError(null); @@ -77,9 +96,33 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) { if (done) break; const data = JSON.parse(new TextDecoder().decode(value)); - console.table({ data }); - setProgress(data.progress ?? progress); - setStatusMessage(data.stage, data.details); + + if (data.error) { + throw { + error: data.error, + details: data.details, + recoverable: data.recoverable, + }; + } + + if (data.signedUrl) { + setUrl(data.signedUrl); + } + + setProgress((prev) => ({ + progress: data.progress ?? prev.progress, + stage: data.stage ?? prev.stage, + details: { + ...prev.details, + ...data.details, + }, + })); + + setStatusMessage({ + progress: data.progress, + stage: data.stage, + details: data.details, + }); } } catch (err) { const errorDetails = err as GenerationError; @@ -93,15 +136,7 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) { }); } - function resetState() { - setProgress(0); - setStatus(statusMessages.starting); - setError(null); - setUrl(""); - setRetryCount(0); - } - - const canRetry = error?.recoverable || retryCount < 3; + const canRetry = error?.recoverable && retryCount < 3; const isRetrying = error?.recoverable && retryCount < 3; return ( @@ -111,7 +146,7 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) {
- {isPending || progress > 0 ? ( + {isPending || progress.progress > 0 ? (

Video generation is in progress. Please be patient, as this may take several minutes. @@ -122,7 +157,7 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) { - {(status || progress > 0) && ( + {(status || progress.progress > 0) && (

{status} - {progress}% + {progress.progress}%
- +
)}