Skip to content

Commit

Permalink
feat: add additional data in progress reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
listlessbird committed Nov 7, 2024
1 parent 70dcbc4 commit 4e961a7
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 47 deletions.
27 changes: 18 additions & 9 deletions renderer/src/controllers/video.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<ProgressData | null> {
if (progressQueue.length > 0) {
return Promise.resolve(progressQueue.shift()!);
}

return new Promise<number>((resolve) => {
return new Promise<ProgressData | null>((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;
Expand All @@ -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,
});
}
}
Expand Down
27 changes: 24 additions & 3 deletions renderer/src/services/video.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export class VideoService {
progressCallback
);

await progressCallback({
progress: 100,
stage: "COMPLETE",
});

logger.info(
{ configId: data.configId, outputPath },
"Video generation completed"
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -133,7 +141,7 @@ export class VideoService {
await renderMedia({
composition,
serveUrl: bundled,
codec: "h264",
codec: "h265",
outputLocation,
inputProps,
timeoutInMilliseconds: 30 * 1000,
Expand All @@ -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,
},
});
},
});

Expand Down
15 changes: 14 additions & 1 deletion renderer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,24 @@ import { GeneratedAssetSchema } from "./schema";
import { t, type Static } from "elysia";

export type VideoGenerationRequest = Static<typeof GeneratedAssetSchema>;
export type ProgressCallback = (progress: number) => Promise<void>;
export type ProgressCallback = (data: ProgressData) => Promise<void>;

export interface HealthCheckResponse {
status: "ok";
timestamp: string;
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;
};
};
103 changes: 69 additions & 34 deletions web/src/app/(history)/history/(item)/[id]/generate.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -15,53 +15,72 @@ 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;
recoverable?: boolean;
}

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<ProgressData>({
progress: 0,
stage: "STARTING",
});
const [status, setStatus] = useState("");
const [url, setUrl] = useState("");
const [error, setError] = useState<GenerationError | null>(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);
Expand All @@ -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;
Expand All @@ -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 (
Expand All @@ -111,7 +146,7 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) {
</CardHeader>
<CardContent>
<div className="space-y-4">
{isPending || progress > 0 ? (
{isPending || progress.progress > 0 ? (
<p>
Video generation is in progress. Please be patient, as this may
take several minutes.
Expand All @@ -122,7 +157,7 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) {

<Button
onClick={() => handleGeneration(false)}
disabled={isPending || progress === 100 || isRetrying}
disabled={isPending || progress.progress === 100 || isRetrying}
className="w-full"
>
{isPending ? (
Expand All @@ -135,13 +170,13 @@ export function Generate({ asset }: { asset: GeneratedAssetType }) {
)}
</Button>

{(status || progress > 0) && (
{(status || progress.progress > 0) && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>{status}</span>
<span>{progress}%</span>
<span>{progress.progress}%</span>
</div>
<Progress value={progress} className="w-full" />
<Progress value={progress.progress} className="w-full" />
</div>
)}

Expand Down

0 comments on commit 4e961a7

Please sign in to comment.