From d57f6041c6369e1c19f018890cdd8e9aae89c3c3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 6 Mar 2026 18:50:31 +0000 Subject: [PATCH 1/7] @remotion/web-renderer: Add render progress estimations --- .../docs/web-renderer/render-media-on-web.mdx | 19 +++++- packages/docs/docs/web-renderer/types.mdx | 4 ++ .../web-renderer/src/render-media-on-web.tsx | 64 ++++++++++++++++-- .../src/test/frame-range.test.tsx | 12 +++- .../src/test/render-media.test.tsx | 67 ++++++++++++++++++- 5 files changed, 159 insertions(+), 7 deletions(-) diff --git a/packages/docs/docs/web-renderer/render-media-on-web.mdx b/packages/docs/docs/web-renderer/render-media-on-web.mdx index 83420b3e5e6..6f2af45ffa0 100644 --- a/packages/docs/docs/web-renderer/render-media-on-web.mdx +++ b/packages/docs/docs/web-renderer/render-media-on-web.mdx @@ -104,9 +104,26 @@ React to render progress. The callback receives an object with the following pro ```tsx twoslash import type {RenderMediaOnWebProgressCallback} from '@remotion/web-renderer'; -const onProgress: RenderMediaOnWebProgressCallback = ({renderedFrames, encodedFrames}) => { +const onProgress: RenderMediaOnWebProgressCallback = ({ + renderedFrames, + encodedFrames, + renderEstimatedTime, + progress, + renderedDoneIn, + encodedDoneIn, +}) => { console.log(`Rendered ${renderedFrames} frames`); console.log(`Encoded ${encodedFrames} frames`); + console.log(`Overall progress: ${Math.round(progress * 100)}%`); + console.log(`Estimated render time remaining: ${renderEstimatedTime}ms`); + + if (renderedDoneIn !== null) { + console.log(`Finished rendering frames in ${renderedDoneIn}ms`); + } + + if (encodedDoneIn !== null) { + console.log(`Finished encoding in ${encodedDoneIn}ms`); + } }; ``` diff --git a/packages/docs/docs/web-renderer/types.mdx b/packages/docs/docs/web-renderer/types.mdx index ed19995ec04..60d99203f74 100644 --- a/packages/docs/docs/web-renderer/types.mdx +++ b/packages/docs/docs/web-renderer/types.mdx @@ -85,6 +85,10 @@ import type {RenderMediaOnWebProgress} from '@remotion/web-renderer'; - `renderedFrames`: The number of frames that have been rendered - `encodedFrames`: The number of frames that have been encoded +- `renderedDoneIn`: The time in milliseconds until all frames have been rendered, or `null` while rendering is still in progress +- `encodedDoneIn`: The time in milliseconds until all frames have been encoded, or `null` while encoding is still in progress +- `renderEstimatedTime`: Estimated time remaining in milliseconds until rendering is done +- `progress`: Overall progress as a number between `0` and `1` ## `RenderMediaOnWebProgressCallback` diff --git a/packages/web-renderer/src/render-media-on-web.tsx b/packages/web-renderer/src/render-media-on-web.tsx index bcf14b349cd..aaf22c2db12 100644 --- a/packages/web-renderer/src/render-media-on-web.tsx +++ b/packages/web-renderer/src/render-media-on-web.tsx @@ -75,10 +75,15 @@ type MandatoryRenderMediaOnWebOptions< composition: CompositionCalculateMetadataOrExplicit; }; +const MAX_RECENT_FRAME_TIMINGS = 150; + export type RenderMediaOnWebProgress = { renderedFrames: number; encodedFrames: number; - // TODO: encodedDoneIn, renderEstimatedTime, progress + renderedDoneIn: number | null; + encodedDoneIn: number | null; + renderEstimatedTime: number; + progress: number; }; export type RenderMediaOnWebResult = { @@ -335,6 +340,12 @@ const internalRenderMediaOnWeb = async < const totalFrames = realFrameRange[1] - realFrameRange[0] + 1; const durationInSeconds = totalFrames / resolved.fps; + const renderStart = Date.now(); + let renderedDoneIn: number | null = null; + let encodedDoneIn: number | null = null; + let renderEstimatedTime = 0; + const recentFrameTimings: number[] = []; + let timeOfLastFrame = renderStart; if (videoSampleSource) { outputWithCleanup.output.addVideoTrack( @@ -375,6 +386,26 @@ const internalRenderMediaOnWeb = async < const progress: RenderMediaOnWebProgress = { renderedFrames: 0, encodedFrames: 0, + renderedDoneIn: null, + encodedDoneIn: null, + renderEstimatedTime: 0, + progress: 0, + }; + const getProgressPayload = (): RenderMediaOnWebProgress => { + const overallProgress = + Math.round( + (70 * progress.renderedFrames + 30 * progress.encodedFrames) / + totalFrames, + ) / 100; + + return { + renderedFrames: progress.renderedFrames, + encodedFrames: progress.encodedFrames, + renderedDoneIn, + encodedDoneIn, + renderEstimatedTime, + progress: overallProgress, + }; }; for (let frame = realFrameRange[0]; frame <= realFrameRange[1]; frame++) { @@ -443,8 +474,29 @@ const internalRenderMediaOnWeb = async < } } + const now = Date.now(); + const timeToRenderInMilliseconds = now - timeOfLastFrame; + timeOfLastFrame = now; + progress.renderedFrames++; - throttledOnProgress?.({...progress}); + recentFrameTimings.push(timeToRenderInMilliseconds); + if (recentFrameTimings.length > MAX_RECENT_FRAME_TIMINGS) { + recentFrameTimings.shift(); + } + + const recentTimingsSum = recentFrameTimings.reduce( + (sum, time) => sum + time, + 0, + ); + const newAverage = recentTimingsSum / recentFrameTimings.length; + const remainingFrames = totalFrames - progress.renderedFrames; + renderEstimatedTime = Math.round(remainingFrames * newAverage); + + if (progress.renderedFrames === totalFrames) { + renderedDoneIn = now - renderStart; + } + + throttledOnProgress?.(getProgressPayload()); const audioCombineStart = performance.now(); const assets = collectAssets.current!.collectAssets(); @@ -487,7 +539,11 @@ const internalRenderMediaOnWeb = async < internalState.addAddSampleTime(performance.now() - addSampleStart); progress.encodedFrames++; - throttledOnProgress?.({...progress}); + if (progress.encodedFrames === totalFrames) { + encodedDoneIn = Date.now() - renderStart; + } + + throttledOnProgress?.(getProgressPayload()); if (signal?.aborted) { throw new Error('renderMediaOnWeb() was cancelled'); @@ -495,7 +551,7 @@ const internalRenderMediaOnWeb = async < } // Call progress one final time to ensure final state is reported - onProgress?.({...progress}); + onProgress?.(getProgressPayload()); videoSampleSource?.videoSampleSource.close(); audioSampleSource?.audioSampleSource.close(); diff --git a/packages/web-renderer/src/test/frame-range.test.tsx b/packages/web-renderer/src/test/frame-range.test.tsx index 33c329daf7c..e7139058891 100644 --- a/packages/web-renderer/src/test/frame-range.test.tsx +++ b/packages/web-renderer/src/test/frame-range.test.tsx @@ -57,10 +57,20 @@ test('should render with valid frame range', async (t) => { frameRange: [10, 15], licenseKey: 'free-license', }); - expect(finalProgress).toEqual({ + if (finalProgress === null) { + throw new Error('Expected final progress to be reported'); + } + + const resolvedProgress = finalProgress as RenderMediaOnWebProgress; + + expect(resolvedProgress).toMatchObject({ renderedFrames: 6, encodedFrames: 6, + renderEstimatedTime: 0, + progress: 1, }); + expect(resolvedProgress.renderedDoneIn).toBeTypeOf('number'); + expect(resolvedProgress.encodedDoneIn).toBeTypeOf('number'); }); test('frameRange starting from non-zero should produce correct duration', async (t) => { diff --git a/packages/web-renderer/src/test/render-media.test.tsx b/packages/web-renderer/src/test/render-media.test.tsx index 39c49789d5f..4de52af96c7 100644 --- a/packages/web-renderer/src/test/render-media.test.tsx +++ b/packages/web-renderer/src/test/render-media.test.tsx @@ -2,6 +2,7 @@ import {ALL_FORMATS, BlobSource, Input} from 'mediabunny'; import {interpolateColors, useCurrentFrame} from 'remotion'; import {VERSION} from 'remotion/version'; import {expect, test} from 'vitest'; +import type {RenderMediaOnWebProgress} from '../render-media-on-web'; import {renderMediaOnWeb} from '../render-media-on-web'; import '../symbol-dispose'; @@ -130,7 +131,7 @@ test('should throttle onProgress callback to 250ms', {retry: 2}, async (t) => { const progressCalls: Array<{ time: number; - progress: {renderedFrames: number; encodedFrames: number}; + progress: RenderMediaOnWebProgress; }> = []; const startTime = Date.now(); @@ -159,6 +160,10 @@ test('should throttle onProgress callback to 250ms', {retry: 2}, async (t) => { const finalCall = progressCalls[progressCalls.length - 1]; expect(finalCall.progress.renderedFrames).toBe(30); expect(finalCall.progress.encodedFrames).toBe(30); + expect(finalCall.progress.renderEstimatedTime).toBe(0); + expect(finalCall.progress.progress).toBe(1); + expect(finalCall.progress.renderedDoneIn).toBeTypeOf('number'); + expect(finalCall.progress.encodedDoneIn).toBeTypeOf('number'); // Check that calls are throttled (if we have multiple calls) if (progressCalls.length > 1) { @@ -171,6 +176,66 @@ test('should throttle onProgress callback to 250ms', {retry: 2}, async (t) => { } }); +test( + 'should provide progress estimates while rendering', + {retry: 2}, + async (t) => { + if (t.task.file.projectName === 'webkit') { + t.skip(); + return; + } + + const progressCalls: RenderMediaOnWebProgress[] = []; + const Component: React.FC = () => null; + + await renderMediaOnWeb({ + composition: { + component: Component, + id: 'progress-estimation-test', + width: 100, + height: 100, + fps: 30, + durationInFrames: 20, + }, + inputProps: {}, + onFrame: async (frame) => { + await new Promise((resolve) => { + setTimeout(resolve, 40); + }); + return frame; + }, + onProgress: (progress) => { + progressCalls.push({...progress}); + }, + }); + + expect(progressCalls.length).toBeGreaterThan(1); + + const intermediateCall = progressCalls.find((progress) => { + return progress.renderedFrames < 20; + }); + expect(intermediateCall).toBeDefined(); + expect(intermediateCall?.renderEstimatedTime).toBeGreaterThan(0); + expect(intermediateCall?.renderedDoneIn).toBeNull(); + expect(intermediateCall?.encodedDoneIn).toBeNull(); + expect(intermediateCall?.progress).toBeGreaterThan(0); + expect(intermediateCall?.progress).toBeLessThan(1); + + const finalCall = progressCalls[progressCalls.length - 1]; + expect(finalCall).toMatchObject({ + renderedFrames: 20, + encodedFrames: 20, + renderEstimatedTime: 0, + progress: 1, + }); + expect(finalCall.renderedDoneIn).toBeTypeOf('number'); + expect(finalCall.encodedDoneIn).toBeTypeOf('number'); + expect(finalCall.encodedDoneIn).toBeGreaterThanOrEqual( + finalCall.renderedDoneIn ?? 0, + ); + }, +); + test('should include "Made with Remotion" metadata', async (t) => { if (t.task.file.projectName === 'webkit') { t.skip(); From 8ecc85006934dfa062ad34dc9c3aab6f883eaea1 Mon Sep 17 00:00:00 2001 From: "pullfrog[bot]" <226033991+pullfrog[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:07:53 +0000 Subject: [PATCH 2/7] @remotion/renderer: Increase render-still test timeouts for macOS CI --- packages/renderer/src/test/render-still.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/renderer/src/test/render-still.test.ts b/packages/renderer/src/test/render-still.test.ts index 3327e789894..9d9363c4f41 100644 --- a/packages/renderer/src/test/render-still.test.ts +++ b/packages/renderer/src/test/render-still.test.ts @@ -34,7 +34,7 @@ test( ).toThrow(/not be NaN, but is NaN/); }, { - timeout: 30000, + timeout: 90000, }, ); @@ -66,7 +66,7 @@ test( expect(contentType).toBe('image/png'); }, { - timeout: 30000, + timeout: 90000, }, ); @@ -100,7 +100,7 @@ test( /Cannot use frame 200: Duration of composition is 30, therefore the highest frame that can be rendered is 29/, ); }, - {timeout: 30000}, + {timeout: 90000}, ); test( @@ -133,6 +133,6 @@ test( ).toThrow(/Image format should be one of: "png", "jpeg", "pdf", "webp"/); }, { - timeout: 30000, + timeout: 90000, }, ); From a5fe417710bb95f4bd6418b0795f50fb9ebbfd5f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 6 Mar 2026 20:00:36 +0000 Subject: [PATCH 3/7] @remotion/studio: Show client render time estimates --- .../RenderModal/ClientRenderProgress.tsx | 64 +++++++++++++++++-- .../ClientRenderQueueProcessor.tsx | 5 ++ .../RenderQueue/RenderQueueItemStatus.tsx | 3 +- .../RenderQueueProgressMessage.tsx | 5 +- .../RenderQueue/client-render-progress.ts | 37 +++++++++++ .../RenderQueue/client-side-render-types.ts | 5 ++ .../src/components/RenderQueue/context.tsx | 10 ++- .../src/test/client-render-progress.test.ts | 53 +++++++++++++++ 8 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 packages/studio/src/components/RenderQueue/client-render-progress.ts create mode 100644 packages/studio/src/test/client-render-progress.test.ts diff --git a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx index 43a9a203e38..7565f5e2d16 100644 --- a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx +++ b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {LIGHT_TEXT} from '../../helpers/colors'; import {Spacing} from '../layout'; import {CircularProgress} from '../RenderQueue/CircularProgress'; +import {formatEtaString} from '../RenderQueue/client-render-progress'; import type {ClientRenderJob} from '../RenderQueue/client-side-render-types'; import {SuccessIcon} from '../RenderQueue/SuccessIcon'; @@ -26,11 +27,41 @@ const right: React.CSSProperties = { flex: 1, }; +const RenderingProgress: React.FC<{ + readonly renderedFrames: number; + readonly totalFrames: number; + readonly renderedDoneIn: number | null; + readonly renderEstimatedTime: number; +}> = ({renderedFrames, totalFrames, renderedDoneIn, renderEstimatedTime}) => { + const done = renderedDoneIn !== null; + const progress = totalFrames > 0 ? renderedFrames / totalFrames : 0; + const etaString = + !done && renderEstimatedTime > 0 + ? `, time remaining: ${formatEtaString(renderEstimatedTime)}` + : ''; + + return ( +
+ {done ? : } + +
+ {done + ? `Rendered ${totalFrames} frames` + : `Rendered ${renderedFrames} / ${totalFrames} frames${etaString}`} +
+ {renderedDoneIn !== null ? ( +
{renderedDoneIn}ms
+ ) : null} +
+ ); +}; + const EncodingProgress: React.FC<{ readonly encodedFrames: number; readonly totalFrames: number; -}> = ({encodedFrames, totalFrames}) => { - const done = encodedFrames === totalFrames; + readonly encodedDoneIn: number | null; +}> = ({encodedFrames, totalFrames, encodedDoneIn}) => { + const done = encodedDoneIn !== null; const progress = totalFrames > 0 ? encodedFrames / totalFrames : 0; return ( @@ -42,6 +73,9 @@ const EncodingProgress: React.FC<{ ? `Encoded ${totalFrames} frames` : `Encoding ${encodedFrames} / ${totalFrames} frames`} + {encodedDoneIn !== null ? ( +
{encodedDoneIn}ms
+ ) : null} ); }; @@ -92,16 +126,32 @@ export const ClientRenderProgress: React.FC<{ ); } - const {encodedFrames, totalFrames} = job.progress; + const { + renderedFrames, + encodedFrames, + totalFrames, + renderedDoneIn, + encodedDoneIn, + renderEstimatedTime, + } = job.progress; return (
{job.type === 'client-video' && ( - + <> + + + )}
diff --git a/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx b/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx index 09281699822..c6653f186f0 100644 --- a/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx +++ b/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx @@ -135,8 +135,13 @@ export const ClientRenderQueueProcessor: React.FC = () => { signal, onProgress: (progress) => { onProgress(job.id, { + renderedFrames: progress.renderedFrames, encodedFrames: progress.encodedFrames, totalFrames, + renderedDoneIn: progress.renderedDoneIn, + encodedDoneIn: progress.encodedDoneIn, + renderEstimatedTime: progress.renderEstimatedTime, + progress: progress.progress, }); }, outputTarget: 'web-fs', diff --git a/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx b/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx index 251ece350ab..499256ca8ac 100644 --- a/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx +++ b/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx @@ -94,8 +94,7 @@ export const RenderQueueItemStatus: React.FC<{ if (job.status === 'running') { let progressValue: number; if (isClientJob) { - const {encodedFrames, totalFrames} = job.progress; - progressValue = totalFrames > 0 ? encodedFrames / totalFrames : 0; + progressValue = job.progress.progress; } else { progressValue = job.progress.value; } diff --git a/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx b/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx index d9f9b7725dc..93948773ec4 100644 --- a/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx +++ b/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx @@ -1,6 +1,7 @@ import React, {useCallback, useContext} from 'react'; import {ModalsContext} from '../../state/modals'; import {useZIndex} from '../../state/z-index'; +import {getClientRenderProgressMessage} from './client-render-progress'; import type {AnyRenderJob} from './context'; import {isClientRenderJob} from './context'; import {renderQueueItemSubtitleStyle} from './item-style'; @@ -29,9 +30,7 @@ export const RenderQueueProgressMessage: React.FC<{ }, [job.id, setSelectedModal]); const message = isClientJob - ? job.progress.totalFrames === 0 - ? 'Getting composition' - : `Encoding frame ${job.progress.encodedFrames}/${job.progress.totalFrames}` + ? getClientRenderProgressMessage(job.progress) : job.progress.message; return ( diff --git a/packages/studio/src/components/RenderQueue/client-render-progress.ts b/packages/studio/src/components/RenderQueue/client-render-progress.ts new file mode 100644 index 00000000000..718bdde11ed --- /dev/null +++ b/packages/studio/src/components/RenderQueue/client-render-progress.ts @@ -0,0 +1,37 @@ +import type {ClientRenderJobProgress} from './client-side-render-types'; + +export const formatEtaString = (timeRemainingInMilliseconds: number) => { + const remainingTime = timeRemainingInMilliseconds / 1000; + const remainingTimeHours = Math.floor(remainingTime / 3600); + const remainingTimeMinutes = Math.floor((remainingTime % 3600) / 60); + const remainingTimeSeconds = Math.floor(remainingTime % 60); + + return [ + remainingTimeHours ? `${remainingTimeHours}h` : null, + remainingTimeMinutes ? `${remainingTimeMinutes}m` : null, + `${remainingTimeSeconds}s`, + ] + .filter((value): value is string => Boolean(value)) + .join(' '); +}; + +export const getClientRenderProgressMessage = ( + progress: ClientRenderJobProgress, +) => { + if (progress.totalFrames === 0) { + return 'Getting composition'; + } + + const allRendered = progress.renderedFrames === progress.totalFrames; + + if (!allRendered) { + const etaString = + progress.renderEstimatedTime > 0 + ? `, time remaining: ${formatEtaString(progress.renderEstimatedTime)}` + : ''; + + return `Rendered ${progress.renderedFrames}/${progress.totalFrames}${etaString}`; + } + + return `Encoded ${progress.encodedFrames}/${progress.totalFrames}`; +}; diff --git a/packages/studio/src/components/RenderQueue/client-side-render-types.ts b/packages/studio/src/components/RenderQueue/client-side-render-types.ts index e9a9d374dca..1238012d1b7 100644 --- a/packages/studio/src/components/RenderQueue/client-side-render-types.ts +++ b/packages/studio/src/components/RenderQueue/client-side-render-types.ts @@ -9,8 +9,13 @@ import type { import type {LogLevel} from 'remotion'; export type ClientRenderJobProgress = { + renderedFrames: number; encodedFrames: number; totalFrames: number; + renderedDoneIn: number | null; + encodedDoneIn: number | null; + renderEstimatedTime: number; + progress: number; }; export type GetBlobCallback = () => Promise; diff --git a/packages/studio/src/components/RenderQueue/context.tsx b/packages/studio/src/components/RenderQueue/context.tsx index 8ef94544eab..c6780013ce4 100644 --- a/packages/studio/src/components/RenderQueue/context.tsx +++ b/packages/studio/src/components/RenderQueue/context.tsx @@ -144,7 +144,15 @@ export const RenderQueueContextProvider: React.FC<{ ? ({ ...job, status: 'running', - progress: {encodedFrames: 0, totalFrames: 0}, + progress: { + renderedFrames: 0, + encodedFrames: 0, + totalFrames: 0, + renderedDoneIn: null, + encodedDoneIn: null, + renderEstimatedTime: 0, + progress: 0, + }, } as ClientRenderJob) : job, ), diff --git a/packages/studio/src/test/client-render-progress.test.ts b/packages/studio/src/test/client-render-progress.test.ts new file mode 100644 index 00000000000..890e4eb9ca2 --- /dev/null +++ b/packages/studio/src/test/client-render-progress.test.ts @@ -0,0 +1,53 @@ +import {expect, test} from 'bun:test'; +import { + formatEtaString, + getClientRenderProgressMessage, +} from '../components/RenderQueue/client-render-progress'; + +test('formats ETA strings like server-side renders', () => { + expect(formatEtaString(5_000)).toBe('5s'); + expect(formatEtaString(65_000)).toBe('1m 5s'); + expect(formatEtaString(3_725_000)).toBe('1h 2m 5s'); +}); + +test('formats client render progress message while rendering', () => { + expect( + getClientRenderProgressMessage({ + renderedFrames: 12, + encodedFrames: 10, + totalFrames: 30, + renderedDoneIn: null, + encodedDoneIn: null, + renderEstimatedTime: 65_000, + progress: 0.55, + }), + ).toBe('Rendered 12/30, time remaining: 1m 5s'); +}); + +test('formats client render progress message while encoding', () => { + expect( + getClientRenderProgressMessage({ + renderedFrames: 30, + encodedFrames: 24, + totalFrames: 30, + renderedDoneIn: 4_000, + encodedDoneIn: null, + renderEstimatedTime: 0, + progress: 0.94, + }), + ).toBe('Encoded 24/30'); +}); + +test('returns getting composition before frame totals are known', () => { + expect( + getClientRenderProgressMessage({ + renderedFrames: 0, + encodedFrames: 0, + totalFrames: 0, + renderedDoneIn: null, + encodedDoneIn: null, + renderEstimatedTime: 0, + progress: 0, + }), + ).toBe('Getting composition'); +}); From 61fc9b84897677112e446bc20ccfa9938f59fc7e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 10 Mar 2026 01:01:36 +0000 Subject: [PATCH 4/7] @remotion/web-renderer: Remove render-only progress fields --- .../docs/web-renderer/render-media-on-web.mdx | 7 -- packages/docs/docs/web-renderer/types.mdx | 2 - .../components/homepage/Demo/DemoRender.tsx | 4 +- .../RenderModal/ClientRenderProgress.tsx | 80 +++++++------------ .../ClientRenderQueueProcessor.tsx | 2 - .../RenderQueue/client-render-progress.ts | 8 +- .../RenderQueue/client-side-render-types.ts | 2 - .../src/components/RenderQueue/context.tsx | 2 - .../src/test/client-render-progress.test.ts | 8 +- .../web-renderer/src/render-media-on-web.tsx | 15 +--- .../src/test/frame-range.test.tsx | 6 +- .../src/test/render-media.test.tsx | 24 +++--- 12 files changed, 47 insertions(+), 113 deletions(-) diff --git a/packages/docs/docs/web-renderer/render-media-on-web.mdx b/packages/docs/docs/web-renderer/render-media-on-web.mdx index 6f2af45ffa0..f56378fd412 100644 --- a/packages/docs/docs/web-renderer/render-media-on-web.mdx +++ b/packages/docs/docs/web-renderer/render-media-on-web.mdx @@ -105,22 +105,15 @@ React to render progress. The callback receives an object with the following pro import type {RenderMediaOnWebProgressCallback} from '@remotion/web-renderer'; const onProgress: RenderMediaOnWebProgressCallback = ({ - renderedFrames, encodedFrames, renderEstimatedTime, progress, - renderedDoneIn, encodedDoneIn, }) => { - console.log(`Rendered ${renderedFrames} frames`); console.log(`Encoded ${encodedFrames} frames`); console.log(`Overall progress: ${Math.round(progress * 100)}%`); console.log(`Estimated render time remaining: ${renderEstimatedTime}ms`); - if (renderedDoneIn !== null) { - console.log(`Finished rendering frames in ${renderedDoneIn}ms`); - } - if (encodedDoneIn !== null) { console.log(`Finished encoding in ${encodedDoneIn}ms`); } diff --git a/packages/docs/docs/web-renderer/types.mdx b/packages/docs/docs/web-renderer/types.mdx index 60d99203f74..9e39dc165ad 100644 --- a/packages/docs/docs/web-renderer/types.mdx +++ b/packages/docs/docs/web-renderer/types.mdx @@ -83,9 +83,7 @@ import type {RenderMediaOnWebProgress} from '@remotion/web-renderer'; // ^? ``` -- `renderedFrames`: The number of frames that have been rendered - `encodedFrames`: The number of frames that have been encoded -- `renderedDoneIn`: The time in milliseconds until all frames have been rendered, or `null` while rendering is still in progress - `encodedDoneIn`: The time in milliseconds until all frames have been encoded, or `null` while encoding is still in progress - `renderEstimatedTime`: Estimated time remaining in milliseconds until rendering is done - `progress`: Overall progress as a number between `0` and `1` diff --git a/packages/promo-pages/src/components/homepage/Demo/DemoRender.tsx b/packages/promo-pages/src/components/homepage/Demo/DemoRender.tsx index c2ffe15a66a..2f2549ae35f 100644 --- a/packages/promo-pages/src/components/homepage/Demo/DemoRender.tsx +++ b/packages/promo-pages/src/components/homepage/Demo/DemoRender.tsx @@ -110,10 +110,10 @@ export const RenderButton: React.FC<{ muted: typeof AudioEncoder === 'undefined', scale: 1, inputProps, - onProgress: ({renderedFrames}) => { + onProgress: ({progress}) => { setState({ type: 'progress', - progress: renderedFrames / durationInFrames, + progress, }); }, }); diff --git a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx index 7565f5e2d16..15e0d7d3237 100644 --- a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx +++ b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {LIGHT_TEXT} from '../../helpers/colors'; import {Spacing} from '../layout'; import {CircularProgress} from '../RenderQueue/CircularProgress'; -import {formatEtaString} from '../RenderQueue/client-render-progress'; +import {getClientRenderProgressMessage} from '../RenderQueue/client-render-progress'; import type {ClientRenderJob} from '../RenderQueue/client-side-render-types'; import {SuccessIcon} from '../RenderQueue/SuccessIcon'; @@ -27,52 +27,33 @@ const right: React.CSSProperties = { flex: 1, }; -const RenderingProgress: React.FC<{ - readonly renderedFrames: number; - readonly totalFrames: number; - readonly renderedDoneIn: number | null; - readonly renderEstimatedTime: number; -}> = ({renderedFrames, totalFrames, renderedDoneIn, renderEstimatedTime}) => { - const done = renderedDoneIn !== null; - const progress = totalFrames > 0 ? renderedFrames / totalFrames : 0; - const etaString = - !done && renderEstimatedTime > 0 - ? `, time remaining: ${formatEtaString(renderEstimatedTime)}` - : ''; - - return ( -
- {done ? : } - -
- {done - ? `Rendered ${totalFrames} frames` - : `Rendered ${renderedFrames} / ${totalFrames} frames${etaString}`} -
- {renderedDoneIn !== null ? ( -
{renderedDoneIn}ms
- ) : null} -
- ); -}; - -const EncodingProgress: React.FC<{ +const ProgressStatus: React.FC<{ readonly encodedFrames: number; readonly totalFrames: number; readonly encodedDoneIn: number | null; -}> = ({encodedFrames, totalFrames, encodedDoneIn}) => { + readonly renderEstimatedTime: number; + readonly progress: number; +}> = ({ + encodedFrames, + totalFrames, + encodedDoneIn, + renderEstimatedTime, + progress, +}) => { const done = encodedDoneIn !== null; - const progress = totalFrames > 0 ? encodedFrames / totalFrames : 0; + const message = getClientRenderProgressMessage({ + encodedFrames, + totalFrames, + encodedDoneIn, + renderEstimatedTime, + progress, + }); return (
{done ? : } -
- {done - ? `Encoded ${totalFrames} frames` - : `Encoding ${encodedFrames} / ${totalFrames} frames`} -
+
{message}
{encodedDoneIn !== null ? (
{encodedDoneIn}ms
) : null} @@ -127,31 +108,24 @@ export const ClientRenderProgress: React.FC<{ } const { - renderedFrames, encodedFrames, totalFrames, - renderedDoneIn, encodedDoneIn, renderEstimatedTime, + progress, } = job.progress; return (
{job.type === 'client-video' && ( - <> - - - + )}
diff --git a/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx b/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx index c6653f186f0..8cfd0c027a1 100644 --- a/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx +++ b/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx @@ -135,10 +135,8 @@ export const ClientRenderQueueProcessor: React.FC = () => { signal, onProgress: (progress) => { onProgress(job.id, { - renderedFrames: progress.renderedFrames, encodedFrames: progress.encodedFrames, totalFrames, - renderedDoneIn: progress.renderedDoneIn, encodedDoneIn: progress.encodedDoneIn, renderEstimatedTime: progress.renderEstimatedTime, progress: progress.progress, diff --git a/packages/studio/src/components/RenderQueue/client-render-progress.ts b/packages/studio/src/components/RenderQueue/client-render-progress.ts index 718bdde11ed..b1626b96cb3 100644 --- a/packages/studio/src/components/RenderQueue/client-render-progress.ts +++ b/packages/studio/src/components/RenderQueue/client-render-progress.ts @@ -22,15 +22,17 @@ export const getClientRenderProgressMessage = ( return 'Getting composition'; } - const allRendered = progress.renderedFrames === progress.totalFrames; + if (progress.encodedDoneIn !== null) { + return `Encoded ${progress.totalFrames}/${progress.totalFrames}`; + } - if (!allRendered) { + if (progress.renderEstimatedTime > 0) { const etaString = progress.renderEstimatedTime > 0 ? `, time remaining: ${formatEtaString(progress.renderEstimatedTime)}` : ''; - return `Rendered ${progress.renderedFrames}/${progress.totalFrames}${etaString}`; + return `Rendering ${Math.round(progress.progress * 100)}%${etaString}`; } return `Encoded ${progress.encodedFrames}/${progress.totalFrames}`; diff --git a/packages/studio/src/components/RenderQueue/client-side-render-types.ts b/packages/studio/src/components/RenderQueue/client-side-render-types.ts index 1238012d1b7..f13417ea45d 100644 --- a/packages/studio/src/components/RenderQueue/client-side-render-types.ts +++ b/packages/studio/src/components/RenderQueue/client-side-render-types.ts @@ -9,10 +9,8 @@ import type { import type {LogLevel} from 'remotion'; export type ClientRenderJobProgress = { - renderedFrames: number; encodedFrames: number; totalFrames: number; - renderedDoneIn: number | null; encodedDoneIn: number | null; renderEstimatedTime: number; progress: number; diff --git a/packages/studio/src/components/RenderQueue/context.tsx b/packages/studio/src/components/RenderQueue/context.tsx index c6780013ce4..1f071db5f8d 100644 --- a/packages/studio/src/components/RenderQueue/context.tsx +++ b/packages/studio/src/components/RenderQueue/context.tsx @@ -145,10 +145,8 @@ export const RenderQueueContextProvider: React.FC<{ ...job, status: 'running', progress: { - renderedFrames: 0, encodedFrames: 0, totalFrames: 0, - renderedDoneIn: null, encodedDoneIn: null, renderEstimatedTime: 0, progress: 0, diff --git a/packages/studio/src/test/client-render-progress.test.ts b/packages/studio/src/test/client-render-progress.test.ts index 890e4eb9ca2..7290d3b7d4a 100644 --- a/packages/studio/src/test/client-render-progress.test.ts +++ b/packages/studio/src/test/client-render-progress.test.ts @@ -13,24 +13,20 @@ test('formats ETA strings like server-side renders', () => { test('formats client render progress message while rendering', () => { expect( getClientRenderProgressMessage({ - renderedFrames: 12, encodedFrames: 10, totalFrames: 30, - renderedDoneIn: null, encodedDoneIn: null, renderEstimatedTime: 65_000, progress: 0.55, }), - ).toBe('Rendered 12/30, time remaining: 1m 5s'); + ).toBe('Rendering 55%, time remaining: 1m 5s'); }); test('formats client render progress message while encoding', () => { expect( getClientRenderProgressMessage({ - renderedFrames: 30, encodedFrames: 24, totalFrames: 30, - renderedDoneIn: 4_000, encodedDoneIn: null, renderEstimatedTime: 0, progress: 0.94, @@ -41,10 +37,8 @@ test('formats client render progress message while encoding', () => { test('returns getting composition before frame totals are known', () => { expect( getClientRenderProgressMessage({ - renderedFrames: 0, encodedFrames: 0, totalFrames: 0, - renderedDoneIn: null, encodedDoneIn: null, renderEstimatedTime: 0, progress: 0, diff --git a/packages/web-renderer/src/render-media-on-web.tsx b/packages/web-renderer/src/render-media-on-web.tsx index aaf22c2db12..c19befd9ec0 100644 --- a/packages/web-renderer/src/render-media-on-web.tsx +++ b/packages/web-renderer/src/render-media-on-web.tsx @@ -78,9 +78,7 @@ type MandatoryRenderMediaOnWebOptions< const MAX_RECENT_FRAME_TIMINGS = 150; export type RenderMediaOnWebProgress = { - renderedFrames: number; encodedFrames: number; - renderedDoneIn: number | null; encodedDoneIn: number | null; renderEstimatedTime: number; progress: number; @@ -341,7 +339,6 @@ const internalRenderMediaOnWeb = async < const totalFrames = realFrameRange[1] - realFrameRange[0] + 1; const durationInSeconds = totalFrames / resolved.fps; const renderStart = Date.now(); - let renderedDoneIn: number | null = null; let encodedDoneIn: number | null = null; let renderEstimatedTime = 0; const recentFrameTimings: number[] = []; @@ -383,13 +380,9 @@ const internalRenderMediaOnWeb = async < throw new Error('renderMediaOnWeb() was cancelled'); } - const progress: RenderMediaOnWebProgress = { + const progress = { renderedFrames: 0, encodedFrames: 0, - renderedDoneIn: null, - encodedDoneIn: null, - renderEstimatedTime: 0, - progress: 0, }; const getProgressPayload = (): RenderMediaOnWebProgress => { const overallProgress = @@ -399,9 +392,7 @@ const internalRenderMediaOnWeb = async < ) / 100; return { - renderedFrames: progress.renderedFrames, encodedFrames: progress.encodedFrames, - renderedDoneIn, encodedDoneIn, renderEstimatedTime, progress: overallProgress, @@ -492,10 +483,6 @@ const internalRenderMediaOnWeb = async < const remainingFrames = totalFrames - progress.renderedFrames; renderEstimatedTime = Math.round(remainingFrames * newAverage); - if (progress.renderedFrames === totalFrames) { - renderedDoneIn = now - renderStart; - } - throttledOnProgress?.(getProgressPayload()); const audioCombineStart = performance.now(); diff --git a/packages/web-renderer/src/test/frame-range.test.tsx b/packages/web-renderer/src/test/frame-range.test.tsx index e7139058891..00ebe326b1d 100644 --- a/packages/web-renderer/src/test/frame-range.test.tsx +++ b/packages/web-renderer/src/test/frame-range.test.tsx @@ -63,14 +63,12 @@ test('should render with valid frame range', async (t) => { const resolvedProgress = finalProgress as RenderMediaOnWebProgress; - expect(resolvedProgress).toMatchObject({ - renderedFrames: 6, + expect(resolvedProgress).toEqual({ encodedFrames: 6, + encodedDoneIn: expect.any(Number), renderEstimatedTime: 0, progress: 1, }); - expect(resolvedProgress.renderedDoneIn).toBeTypeOf('number'); - expect(resolvedProgress.encodedDoneIn).toBeTypeOf('number'); }); test('frameRange starting from non-zero should produce correct duration', async (t) => { diff --git a/packages/web-renderer/src/test/render-media.test.tsx b/packages/web-renderer/src/test/render-media.test.tsx index 4de52af96c7..f539f6739aa 100644 --- a/packages/web-renderer/src/test/render-media.test.tsx +++ b/packages/web-renderer/src/test/render-media.test.tsx @@ -158,12 +158,12 @@ test('should throttle onProgress callback to 250ms', {retry: 2}, async (t) => { // Final call should have all frames rendered and encoded const finalCall = progressCalls[progressCalls.length - 1]; - expect(finalCall.progress.renderedFrames).toBe(30); - expect(finalCall.progress.encodedFrames).toBe(30); - expect(finalCall.progress.renderEstimatedTime).toBe(0); - expect(finalCall.progress.progress).toBe(1); - expect(finalCall.progress.renderedDoneIn).toBeTypeOf('number'); - expect(finalCall.progress.encodedDoneIn).toBeTypeOf('number'); + expect(finalCall.progress).toEqual({ + encodedFrames: 30, + encodedDoneIn: expect.any(Number), + renderEstimatedTime: 0, + progress: 1, + }); // Check that calls are throttled (if we have multiple calls) if (progressCalls.length > 1) { @@ -212,27 +212,21 @@ test( expect(progressCalls.length).toBeGreaterThan(1); const intermediateCall = progressCalls.find((progress) => { - return progress.renderedFrames < 20; + return progress.encodedFrames < 20; }); expect(intermediateCall).toBeDefined(); expect(intermediateCall?.renderEstimatedTime).toBeGreaterThan(0); - expect(intermediateCall?.renderedDoneIn).toBeNull(); expect(intermediateCall?.encodedDoneIn).toBeNull(); expect(intermediateCall?.progress).toBeGreaterThan(0); expect(intermediateCall?.progress).toBeLessThan(1); const finalCall = progressCalls[progressCalls.length - 1]; - expect(finalCall).toMatchObject({ - renderedFrames: 20, + expect(finalCall).toEqual({ encodedFrames: 20, + encodedDoneIn: expect.any(Number), renderEstimatedTime: 0, progress: 1, }); - expect(finalCall.renderedDoneIn).toBeTypeOf('number'); - expect(finalCall.encodedDoneIn).toBeTypeOf('number'); - expect(finalCall.encodedDoneIn).toBeGreaterThanOrEqual( - finalCall.renderedDoneIn ?? 0, - ); }, ); From 309e5f8d6931e209bc08eec39c308a746dce74e1 Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Tue, 10 Mar 2026 18:06:42 +0100 Subject: [PATCH 5/7] @remotion/web-renderer: Rename and update progress fields for clarity - Changed `encodedDoneIn` to `doneIn` for consistency across components. - Updated documentation to reflect the new naming and clarify the purpose of `doneIn`. - Deprecated `renderedFrames` in favor of using `progress` for overall status updates. - Adjusted related components and tests to accommodate the new field names. --- .../docs/web-renderer/render-media-on-web.mdx | 6 ++-- packages/docs/docs/web-renderer/types.mdx | 24 ++++++++++++--- .../RenderModal/ClientRenderProgress.tsx | 29 +++++-------------- .../ClientRenderQueueProcessor.tsx | 2 +- .../RenderQueue/client-render-progress.ts | 7 ++--- .../RenderQueue/client-side-render-types.ts | 2 +- .../src/components/RenderQueue/context.tsx | 2 +- .../src/test/client-render-progress.test.ts | 6 ++-- .../web-renderer/src/render-media-on-web.tsx | 20 +++++++++---- .../src/test/frame-range.test.tsx | 3 +- .../src/test/render-media.test.tsx | 9 ++++-- 11 files changed, 62 insertions(+), 48 deletions(-) diff --git a/packages/docs/docs/web-renderer/render-media-on-web.mdx b/packages/docs/docs/web-renderer/render-media-on-web.mdx index f56378fd412..1cbbf7aa8d4 100644 --- a/packages/docs/docs/web-renderer/render-media-on-web.mdx +++ b/packages/docs/docs/web-renderer/render-media-on-web.mdx @@ -108,14 +108,14 @@ const onProgress: RenderMediaOnWebProgressCallback = ({ encodedFrames, renderEstimatedTime, progress, - encodedDoneIn, + doneIn, }) => { console.log(`Encoded ${encodedFrames} frames`); console.log(`Overall progress: ${Math.round(progress * 100)}%`); console.log(`Estimated render time remaining: ${renderEstimatedTime}ms`); - if (encodedDoneIn !== null) { - console.log(`Finished encoding in ${encodedDoneIn}ms`); + if (doneIn !== null) { + console.log(`Finished rendering in ${doneIn}ms`); } }; ``` diff --git a/packages/docs/docs/web-renderer/types.mdx b/packages/docs/docs/web-renderer/types.mdx index 9e39dc165ad..6d25a735641 100644 --- a/packages/docs/docs/web-renderer/types.mdx +++ b/packages/docs/docs/web-renderer/types.mdx @@ -83,10 +83,26 @@ import type {RenderMediaOnWebProgress} from '@remotion/web-renderer'; // ^? ``` -- `encodedFrames`: The number of frames that have been encoded -- `encodedDoneIn`: The time in milliseconds until all frames have been encoded, or `null` while encoding is still in progress -- `renderEstimatedTime`: Estimated time remaining in milliseconds until rendering is done -- `progress`: Overall progress as a number between `0` and `1` +### `renderedFrames` + +Deprecated and kept for backward compatibility. The number of frames that have been rendered. +Prefer `progress` for overall status updates. + +### `encodedFrames` + +The number of frames that have been encoded. + +### `doneIn` + +The total time in milliseconds from render start until all frames were encoded, or `null` while encoding is still in progress. + +### `renderEstimatedTime` + +Estimated time remaining in milliseconds until rendering is done. + +### `progress` + +Overall progress as a number between `0` and `1`. ## `RenderMediaOnWebProgressCallback` diff --git a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx index 15e0d7d3237..e2807e8feae 100644 --- a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx +++ b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx @@ -30,21 +30,15 @@ const right: React.CSSProperties = { const ProgressStatus: React.FC<{ readonly encodedFrames: number; readonly totalFrames: number; - readonly encodedDoneIn: number | null; + readonly doneIn: number | null; readonly renderEstimatedTime: number; readonly progress: number; -}> = ({ - encodedFrames, - totalFrames, - encodedDoneIn, - renderEstimatedTime, - progress, -}) => { - const done = encodedDoneIn !== null; +}> = ({encodedFrames, totalFrames, doneIn, renderEstimatedTime, progress}) => { + const done = doneIn !== null; const message = getClientRenderProgressMessage({ encodedFrames, totalFrames, - encodedDoneIn, + doneIn, renderEstimatedTime, progress, }); @@ -54,9 +48,7 @@ const ProgressStatus: React.FC<{ {done ? : }
{message}
- {encodedDoneIn !== null ? ( -
{encodedDoneIn}ms
- ) : null} + {doneIn !== null ?
{doneIn}ms
: null}
); }; @@ -107,13 +99,8 @@ export const ClientRenderProgress: React.FC<{ ); } - const { - encodedFrames, - totalFrames, - encodedDoneIn, - renderEstimatedTime, - progress, - } = job.progress; + const {encodedFrames, totalFrames, doneIn, renderEstimatedTime, progress} = + job.progress; return (
@@ -122,7 +109,7 @@ export const ClientRenderProgress: React.FC<{ diff --git a/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx b/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx index 8cfd0c027a1..32656eee9c9 100644 --- a/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx +++ b/packages/studio/src/components/RenderQueue/ClientRenderQueueProcessor.tsx @@ -137,7 +137,7 @@ export const ClientRenderQueueProcessor: React.FC = () => { onProgress(job.id, { encodedFrames: progress.encodedFrames, totalFrames, - encodedDoneIn: progress.encodedDoneIn, + doneIn: progress.doneIn, renderEstimatedTime: progress.renderEstimatedTime, progress: progress.progress, }); diff --git a/packages/studio/src/components/RenderQueue/client-render-progress.ts b/packages/studio/src/components/RenderQueue/client-render-progress.ts index b1626b96cb3..68429fbcd97 100644 --- a/packages/studio/src/components/RenderQueue/client-render-progress.ts +++ b/packages/studio/src/components/RenderQueue/client-render-progress.ts @@ -22,15 +22,12 @@ export const getClientRenderProgressMessage = ( return 'Getting composition'; } - if (progress.encodedDoneIn !== null) { + if (progress.doneIn !== null) { return `Encoded ${progress.totalFrames}/${progress.totalFrames}`; } if (progress.renderEstimatedTime > 0) { - const etaString = - progress.renderEstimatedTime > 0 - ? `, time remaining: ${formatEtaString(progress.renderEstimatedTime)}` - : ''; + const etaString = `, time remaining: ${formatEtaString(progress.renderEstimatedTime)}`; return `Rendering ${Math.round(progress.progress * 100)}%${etaString}`; } diff --git a/packages/studio/src/components/RenderQueue/client-side-render-types.ts b/packages/studio/src/components/RenderQueue/client-side-render-types.ts index f13417ea45d..3f5f7b8365d 100644 --- a/packages/studio/src/components/RenderQueue/client-side-render-types.ts +++ b/packages/studio/src/components/RenderQueue/client-side-render-types.ts @@ -11,7 +11,7 @@ import type {LogLevel} from 'remotion'; export type ClientRenderJobProgress = { encodedFrames: number; totalFrames: number; - encodedDoneIn: number | null; + doneIn: number | null; renderEstimatedTime: number; progress: number; }; diff --git a/packages/studio/src/components/RenderQueue/context.tsx b/packages/studio/src/components/RenderQueue/context.tsx index 1f071db5f8d..f360b7ddeac 100644 --- a/packages/studio/src/components/RenderQueue/context.tsx +++ b/packages/studio/src/components/RenderQueue/context.tsx @@ -147,7 +147,7 @@ export const RenderQueueContextProvider: React.FC<{ progress: { encodedFrames: 0, totalFrames: 0, - encodedDoneIn: null, + doneIn: null, renderEstimatedTime: 0, progress: 0, }, diff --git a/packages/studio/src/test/client-render-progress.test.ts b/packages/studio/src/test/client-render-progress.test.ts index 7290d3b7d4a..b20808a2d0d 100644 --- a/packages/studio/src/test/client-render-progress.test.ts +++ b/packages/studio/src/test/client-render-progress.test.ts @@ -15,7 +15,7 @@ test('formats client render progress message while rendering', () => { getClientRenderProgressMessage({ encodedFrames: 10, totalFrames: 30, - encodedDoneIn: null, + doneIn: null, renderEstimatedTime: 65_000, progress: 0.55, }), @@ -27,7 +27,7 @@ test('formats client render progress message while encoding', () => { getClientRenderProgressMessage({ encodedFrames: 24, totalFrames: 30, - encodedDoneIn: null, + doneIn: null, renderEstimatedTime: 0, progress: 0.94, }), @@ -39,7 +39,7 @@ test('returns getting composition before frame totals are known', () => { getClientRenderProgressMessage({ encodedFrames: 0, totalFrames: 0, - encodedDoneIn: null, + doneIn: null, renderEstimatedTime: 0, progress: 0, }), diff --git a/packages/web-renderer/src/render-media-on-web.tsx b/packages/web-renderer/src/render-media-on-web.tsx index c19befd9ec0..e2b0255d663 100644 --- a/packages/web-renderer/src/render-media-on-web.tsx +++ b/packages/web-renderer/src/render-media-on-web.tsx @@ -78,8 +78,17 @@ type MandatoryRenderMediaOnWebOptions< const MAX_RECENT_FRAME_TIMINGS = 150; export type RenderMediaOnWebProgress = { + /** + * @deprecated Kept for backward compatibility. Use `progress` for overall + * status updates. + */ + renderedFrames: number; encodedFrames: number; - encodedDoneIn: number | null; + /** + * The total time in milliseconds from render start until all frames were + * encoded, or `null` while encoding is still in progress. + */ + doneIn: number | null; renderEstimatedTime: number; progress: number; }; @@ -339,10 +348,9 @@ const internalRenderMediaOnWeb = async < const totalFrames = realFrameRange[1] - realFrameRange[0] + 1; const durationInSeconds = totalFrames / resolved.fps; const renderStart = Date.now(); - let encodedDoneIn: number | null = null; + let doneIn: number | null = null; let renderEstimatedTime = 0; const recentFrameTimings: number[] = []; - let timeOfLastFrame = renderStart; if (videoSampleSource) { outputWithCleanup.output.addVideoTrack( @@ -380,6 +388,7 @@ const internalRenderMediaOnWeb = async < throw new Error('renderMediaOnWeb() was cancelled'); } + let timeOfLastFrame = Date.now(); const progress = { renderedFrames: 0, encodedFrames: 0, @@ -392,8 +401,9 @@ const internalRenderMediaOnWeb = async < ) / 100; return { + renderedFrames: progress.renderedFrames, encodedFrames: progress.encodedFrames, - encodedDoneIn, + doneIn, renderEstimatedTime, progress: overallProgress, }; @@ -527,7 +537,7 @@ const internalRenderMediaOnWeb = async < progress.encodedFrames++; if (progress.encodedFrames === totalFrames) { - encodedDoneIn = Date.now() - renderStart; + doneIn = Date.now() - renderStart; } throttledOnProgress?.(getProgressPayload()); diff --git a/packages/web-renderer/src/test/frame-range.test.tsx b/packages/web-renderer/src/test/frame-range.test.tsx index 00ebe326b1d..402d80ebb59 100644 --- a/packages/web-renderer/src/test/frame-range.test.tsx +++ b/packages/web-renderer/src/test/frame-range.test.tsx @@ -64,8 +64,9 @@ test('should render with valid frame range', async (t) => { const resolvedProgress = finalProgress as RenderMediaOnWebProgress; expect(resolvedProgress).toEqual({ + renderedFrames: 6, encodedFrames: 6, - encodedDoneIn: expect.any(Number), + doneIn: expect.any(Number), renderEstimatedTime: 0, progress: 1, }); diff --git a/packages/web-renderer/src/test/render-media.test.tsx b/packages/web-renderer/src/test/render-media.test.tsx index f539f6739aa..cdf799813fd 100644 --- a/packages/web-renderer/src/test/render-media.test.tsx +++ b/packages/web-renderer/src/test/render-media.test.tsx @@ -159,8 +159,9 @@ test('should throttle onProgress callback to 250ms', {retry: 2}, async (t) => { // Final call should have all frames rendered and encoded const finalCall = progressCalls[progressCalls.length - 1]; expect(finalCall.progress).toEqual({ + renderedFrames: 30, encodedFrames: 30, - encodedDoneIn: expect.any(Number), + doneIn: expect.any(Number), renderEstimatedTime: 0, progress: 1, }); @@ -215,15 +216,17 @@ test( return progress.encodedFrames < 20; }); expect(intermediateCall).toBeDefined(); + expect(intermediateCall?.renderedFrames).toBeGreaterThan(0); expect(intermediateCall?.renderEstimatedTime).toBeGreaterThan(0); - expect(intermediateCall?.encodedDoneIn).toBeNull(); + expect(intermediateCall?.doneIn).toBeNull(); expect(intermediateCall?.progress).toBeGreaterThan(0); expect(intermediateCall?.progress).toBeLessThan(1); const finalCall = progressCalls[progressCalls.length - 1]; expect(finalCall).toEqual({ + renderedFrames: 20, encodedFrames: 20, - encodedDoneIn: expect.any(Number), + doneIn: expect.any(Number), renderEstimatedTime: 0, progress: 1, }); From 196e9161dc2d7980ba3bbefcfbda009a0642b03e Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Tue, 10 Mar 2026 18:18:23 +0100 Subject: [PATCH 6/7] make the studio render progress consistent with the server side renders --- .../studio/src/components/RenderQueue/client-render-progress.ts | 2 +- packages/studio/src/test/client-render-progress.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/studio/src/components/RenderQueue/client-render-progress.ts b/packages/studio/src/components/RenderQueue/client-render-progress.ts index 68429fbcd97..58f375fb43d 100644 --- a/packages/studio/src/components/RenderQueue/client-render-progress.ts +++ b/packages/studio/src/components/RenderQueue/client-render-progress.ts @@ -29,7 +29,7 @@ export const getClientRenderProgressMessage = ( if (progress.renderEstimatedTime > 0) { const etaString = `, time remaining: ${formatEtaString(progress.renderEstimatedTime)}`; - return `Rendering ${Math.round(progress.progress * 100)}%${etaString}`; + return `Rendering ${progress.encodedFrames}/${progress.totalFrames}${etaString}`; } return `Encoded ${progress.encodedFrames}/${progress.totalFrames}`; diff --git a/packages/studio/src/test/client-render-progress.test.ts b/packages/studio/src/test/client-render-progress.test.ts index b20808a2d0d..0bbd09fdddd 100644 --- a/packages/studio/src/test/client-render-progress.test.ts +++ b/packages/studio/src/test/client-render-progress.test.ts @@ -19,7 +19,7 @@ test('formats client render progress message while rendering', () => { renderEstimatedTime: 65_000, progress: 0.55, }), - ).toBe('Rendering 55%, time remaining: 1m 5s'); + ).toBe('Rendering 10/30, time remaining: 1m 5s'); }); test('formats client render progress message while encoding', () => { From a8794624f9e43e701457de4830ec812c223e7977 Mon Sep 17 00:00:00 2001 From: JonnyBurger Date: Wed, 11 Mar 2026 15:37:23 +0100 Subject: [PATCH 7/7] Update GitHub stars from 38k to 39k Co-Authored-By: Claude Opus 4.6 --- .../promo-pages/src/components/homepage/CommunityStatsItems.tsx | 2 +- packages/promo-pages/src/components/homepage/GitHubButton.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/promo-pages/src/components/homepage/CommunityStatsItems.tsx b/packages/promo-pages/src/components/homepage/CommunityStatsItems.tsx index a84d96e78d6..152efa99c13 100644 --- a/packages/promo-pages/src/components/homepage/CommunityStatsItems.tsx +++ b/packages/promo-pages/src/components/homepage/CommunityStatsItems.tsx @@ -177,7 +177,7 @@ export const GitHubStars: React.FC = () => { width="45px" /> {
GitHub{' '}
- {'38k'} + {'39k'}
);