From 2a06e7b4d77d5f41a7835e45dad8cf1154c84774 Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Mon, 16 Feb 2026 17:59:30 +0100 Subject: [PATCH 01/36] @remotion/studio: Show render queue in read-only mode when browser rendering is enabled --- .../studio/src/components/OptionsPanel.tsx | 7 ++++--- .../studio/src/components/RenderButton.tsx | 21 ++++++++++++++++++- .../src/components/RenderQueue/index.tsx | 3 ++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/studio/src/components/OptionsPanel.tsx b/packages/studio/src/components/OptionsPanel.tsx index bd87886b93b..359b99c9d46 100644 --- a/packages/studio/src/components/OptionsPanel.tsx +++ b/packages/studio/src/components/OptionsPanel.tsx @@ -11,6 +11,7 @@ import type {_InternalTypes} from 'remotion'; import {Internals} from 'remotion'; import {BACKGROUND} from '../helpers/colors'; import {useMobileLayout} from '../helpers/mobile-layout'; +import {SHOW_BROWSER_RENDERING} from '../helpers/show-browser-rendering'; import {VisualControlsTabActivatedContext} from '../visual-controls/VisualControls'; import {GlobalPropsEditorUpdateButton} from './GlobalPropsEditorUpdateButton'; import {DataEditor} from './RenderModal/DataEditor'; @@ -25,7 +26,7 @@ type OptionsSidebarPanel = 'input-props' | 'renders' | 'visual-controls'; const localStorageKey = 'remotion.sidebarPanel'; const getSelectedPanel = (readOnlyStudio: boolean): OptionsSidebarPanel => { - if (readOnlyStudio) { + if (readOnlyStudio && !SHOW_BROWSER_RENDERING) { return 'input-props'; } @@ -207,7 +208,7 @@ export const OptionsPanel: React.FC<{ ) : null} ) : null} - {readOnlyStudio ? null : ( + {readOnlyStudio && !SHOW_BROWSER_RENDERING ? null : ( ) : panel === 'visual-controls' && visualControlsTabActivated ? ( - ) : readOnlyStudio ? null : ( + ) : readOnlyStudio && !SHOW_BROWSER_RENDERING ? null : ( )} diff --git a/packages/studio/src/components/RenderButton.tsx b/packages/studio/src/components/RenderButton.tsx index e9b92430c08..1d25cad87b4 100644 --- a/packages/studio/src/components/RenderButton.tsx +++ b/packages/studio/src/components/RenderButton.tsx @@ -9,7 +9,14 @@ import type { } from '@remotion/renderer'; import type {RenderStillOnWebImageFormat} from '@remotion/web-renderer'; import type {SVGProps} from 'react'; -import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import ReactDOM from 'react-dom'; import type {_InternalTypes} from 'remotion'; import {Internals} from 'remotion'; @@ -178,6 +185,18 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ const connectionStatus = useContext(StudioServerConnectionCtx) .previewServerState.type; + + // auto-fallback to client-render when server disconnects and browser rendering is available + useEffect(() => { + if ( + connectionStatus === 'disconnected' && + SHOW_BROWSER_RENDERING && + renderType === 'server-render' + ) { + setRenderType('client-render'); + } + }, [connectionStatus, renderType]); + const shortcut = areKeyboardShortcutsDisabled() ? '' : '(R)'; const tooltip = connectionStatus === 'connected' diff --git a/packages/studio/src/components/RenderQueue/index.tsx b/packages/studio/src/components/RenderQueue/index.tsx index 3c3257dc434..ef0f927e836 100644 --- a/packages/studio/src/components/RenderQueue/index.tsx +++ b/packages/studio/src/components/RenderQueue/index.tsx @@ -2,6 +2,7 @@ import React, {useContext, useEffect, useMemo} from 'react'; import {Internals} from 'remotion'; import {StudioServerConnectionCtx} from '../../helpers/client-id'; import {BACKGROUND, BORDER_COLOR, LIGHT_TEXT} from '../../helpers/colors'; +import {SHOW_BROWSER_RENDERING} from '../../helpers/show-browser-rendering'; import {VERTICAL_SCROLLBAR_CLASSNAME} from '../Menu/is-menu-item'; import {Spacing} from '../layout'; import {RenderQueueItem} from './RenderQueueItem'; @@ -78,7 +79,7 @@ export const RenderQueue: React.FC = () => { return -1; }, [canvasContent, jobs]); - if (connectionStatus === 'disconnected') { + if (connectionStatus === 'disconnected' && !SHOW_BROWSER_RENDERING) { return (
From 154c03281a6da736c704c5454bb9a32f2cbdd09c Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Mon, 16 Feb 2026 18:13:39 +0100 Subject: [PATCH 02/36] @remotion/docs: Update deploy-static.mdx to reflect client-side rendering availability --- packages/docs/docs/studio/deploy-static.mdx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/docs/docs/studio/deploy-static.mdx b/packages/docs/docs/studio/deploy-static.mdx index 2711d3e389e..27c6ac36e80 100644 --- a/packages/docs/docs/studio/deploy-static.mdx +++ b/packages/docs/docs/studio/deploy-static.mdx @@ -11,7 +11,8 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; You can deploy the Remotion Studio as a static site, for example to Vercel or Netlify. -While the Render button will be disabled, it may be used as a [Serve URL](/docs/terminology/serve-url) to pass to rendering APIs. +Server-side rendering will not be available, but if you enable [client-side rendering](/docs/client-side-rendering), you can render videos directly in the browser. +The deployed URL may also be used as a [Serve URL](/docs/terminology/serve-url) to pass to rendering APIs. Make sure you are on at least v4.0.97 to use this feature - use `npx remotion upgrade` to upgrade. ## Export the Remotion Studio as a static site From 541f47a5d0a0944cdf14a1e8e1f55dffe26530d0 Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Mon, 16 Feb 2026 18:15:20 +0100 Subject: [PATCH 03/36] @remotion/studio: Fix render button appearing disabled in read-only mode when client-side rendering is enabled --- packages/studio/src/components/RenderButton.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/studio/src/components/RenderButton.tsx b/packages/studio/src/components/RenderButton.tsx index 1d25cad87b4..10d567e52ea 100644 --- a/packages/studio/src/components/RenderButton.tsx +++ b/packages/studio/src/components/RenderButton.tsx @@ -199,7 +199,7 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ const shortcut = areKeyboardShortcutsDisabled() ? '' : '(R)'; const tooltip = - connectionStatus === 'connected' + connectionStatus === 'connected' || SHOW_BROWSER_RENDERING ? 'Export the current composition ' + shortcut : 'Connect to the Studio server to render'; @@ -421,11 +421,13 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ }, [dropdownOpened, size, spaceToBottom, spaceToTop]); const containerStyle = useMemo((): React.CSSProperties => { + const isDisabled = + connectionStatus !== 'connected' && !SHOW_BROWSER_RENDERING; return { ...splitButtonContainer, borderColor: INPUT_BORDER_COLOR_UNHOVERED, - opacity: connectionStatus !== 'connected' ? 0.7 : 1, - cursor: connectionStatus !== 'connected' ? 'inherit' : 'pointer', + opacity: isDisabled ? 0.7 : 1, + cursor: isDisabled ? 'inherit' : 'pointer', }; }, [connectionStatus]); From 5a5f4180b4a5cecfcf81d2b0916b0df0d348531e Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Mon, 16 Feb 2026 18:56:34 +0100 Subject: [PATCH 04/36] @remotion/studio: Show only encoding progress in web renderer and fix 0/0 display --- .../RenderModal/ClientRenderProgress.tsx | 26 +------------------ .../ClientRenderQueueProcessor.tsx | 1 - .../RenderQueue/RenderQueueItemStatus.tsx | 4 +-- .../RenderQueueProgressMessage.tsx | 4 ++- .../RenderQueue/client-side-render-types.ts | 1 - .../src/components/RenderQueue/context.tsx | 2 +- packages/studio/src/helpers/document-title.ts | 4 +-- 7 files changed, 9 insertions(+), 33 deletions(-) diff --git a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx index 9ee8a1d5bac..43a9a203e38 100644 --- a/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx +++ b/packages/studio/src/components/RenderModal/ClientRenderProgress.tsx @@ -26,26 +26,6 @@ const right: React.CSSProperties = { flex: 1, }; -const RenderingProgress: React.FC<{ - readonly renderedFrames: number; - readonly totalFrames: number; -}> = ({renderedFrames, totalFrames}) => { - const done = renderedFrames === totalFrames; - const progress = totalFrames > 0 ? renderedFrames / totalFrames : 0; - - return ( -
- {done ? : } - -
- {done - ? `Rendered ${totalFrames} frames` - : `Rendering ${renderedFrames} / ${totalFrames} frames`} -
-
- ); -}; - const EncodingProgress: React.FC<{ readonly encodedFrames: number; readonly totalFrames: number; @@ -112,15 +92,11 @@ export const ClientRenderProgress: React.FC<{ ); } - const {renderedFrames, encodedFrames, totalFrames} = job.progress; + const {encodedFrames, totalFrames} = job.progress; return (
- {job.type === 'client-video' && ( { signal, onProgress: (progress) => { onProgress(job.id, { - renderedFrames: progress.renderedFrames, encodedFrames: progress.encodedFrames, totalFrames, }); diff --git a/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx b/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx index a96525aa453..251ece350ab 100644 --- a/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx +++ b/packages/studio/src/components/RenderQueue/RenderQueueItemStatus.tsx @@ -94,8 +94,8 @@ export const RenderQueueItemStatus: React.FC<{ if (job.status === 'running') { let progressValue: number; if (isClientJob) { - const {renderedFrames, totalFrames} = job.progress; - progressValue = totalFrames > 0 ? renderedFrames / totalFrames : 0; + const {encodedFrames, totalFrames} = job.progress; + progressValue = totalFrames > 0 ? encodedFrames / totalFrames : 0; } else { progressValue = job.progress.value; } diff --git a/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx b/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx index 00a9906b06a..d9f9b7725dc 100644 --- a/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx +++ b/packages/studio/src/components/RenderQueue/RenderQueueProgressMessage.tsx @@ -29,7 +29,9 @@ export const RenderQueueProgressMessage: React.FC<{ }, [job.id, setSelectedModal]); const message = isClientJob - ? `Rendering frame ${job.progress.renderedFrames}/${job.progress.totalFrames}` + ? job.progress.totalFrames === 0 + ? 'Getting composition' + : `Encoding frame ${job.progress.encodedFrames}/${job.progress.totalFrames}` : job.progress.message; return ( 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 8c2ba721934..a17ddd29785 100644 --- a/packages/studio/src/components/RenderQueue/client-side-render-types.ts +++ b/packages/studio/src/components/RenderQueue/client-side-render-types.ts @@ -9,7 +9,6 @@ import type { import type {LogLevel} from 'remotion'; export type ClientRenderJobProgress = { - renderedFrames: number; encodedFrames: number; totalFrames: number; }; diff --git a/packages/studio/src/components/RenderQueue/context.tsx b/packages/studio/src/components/RenderQueue/context.tsx index b98a7ed9ad6..7061c97b9a4 100644 --- a/packages/studio/src/components/RenderQueue/context.tsx +++ b/packages/studio/src/components/RenderQueue/context.tsx @@ -144,7 +144,7 @@ export const RenderQueueContextProvider: React.FC<{ ? ({ ...job, status: 'running', - progress: {renderedFrames: 0, encodedFrames: 0, totalFrames: 0}, + progress: {encodedFrames: 0, totalFrames: 0}, } as ClientRenderJob) : job, ), diff --git a/packages/studio/src/helpers/document-title.ts b/packages/studio/src/helpers/document-title.ts index 27601eae535..ab72cf6dbda 100644 --- a/packages/studio/src/helpers/document-title.ts +++ b/packages/studio/src/helpers/document-title.ts @@ -70,9 +70,9 @@ const getProgressInBrackets = ( let progInPercent: number; if (isClientRenderJob(currentRender)) { - const {renderedFrames, totalFrames} = currentRender.progress; + const {encodedFrames, totalFrames} = currentRender.progress; progInPercent = - totalFrames > 0 ? Math.ceil((renderedFrames / totalFrames) * 100) : 0; + totalFrames > 0 ? Math.ceil((encodedFrames / totalFrames) * 100) : 0; } else { progInPercent = Math.ceil(currentRender.progress.value * 100); } From 70631565ba2bb5bca02bd6e4c62af82184f9e3a6 Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Mon, 16 Feb 2026 19:07:51 +0100 Subject: [PATCH 05/36] `@remotion/studio`: Replace useEffect with derived state for render type fallback --- .../studio/src/components/RenderButton.tsx | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/studio/src/components/RenderButton.tsx b/packages/studio/src/components/RenderButton.tsx index 10d567e52ea..9fd00912676 100644 --- a/packages/studio/src/components/RenderButton.tsx +++ b/packages/studio/src/components/RenderButton.tsx @@ -12,7 +12,6 @@ import type {SVGProps} from 'react'; import React, { useCallback, useContext, - useEffect, useMemo, useRef, useState, @@ -127,8 +126,8 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ }) => { const {inFrame, outFrame} = useTimelineInOutFramePosition(); const {setSelectedModal} = useContext(ModalsContext); - const [renderType, setRenderType] = useState(() => - getInitialRenderType(readOnlyStudio), + const [preferredRenderType, setPreferredRenderType] = useState( + () => getInitialRenderType(readOnlyStudio), ); const [dropdownOpened, setDropdownOpened] = useState(false); const dropdownRef = useRef(null); @@ -186,16 +185,17 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ const connectionStatus = useContext(StudioServerConnectionCtx) .previewServerState.type; - // auto-fallback to client-render when server disconnects and browser rendering is available - useEffect(() => { - if ( - connectionStatus === 'disconnected' && - SHOW_BROWSER_RENDERING && - renderType === 'server-render' - ) { - setRenderType('client-render'); + const renderType: RenderType = useMemo(() => { + if (connectionStatus === 'disconnected' && SHOW_BROWSER_RENDERING) { + return 'client-render'; } - }, [connectionStatus, renderType]); + + if (!SHOW_BROWSER_RENDERING) { + return 'server-render'; + } + + return preferredRenderType; + }, [connectionStatus, preferredRenderType]); const shortcut = areKeyboardShortcutsDisabled() ? '' : '(R)'; const tooltip = @@ -334,7 +334,7 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ const handleRenderTypeChange = useCallback( (newType: RenderType) => { - setRenderType(newType); + setPreferredRenderType(newType); try { localStorage.setItem(RENDER_TYPE_STORAGE_KEY, newType); } catch { From 8f1b4e5af8a68007036d693f2a9ac78b3b1de6f1 Mon Sep 17 00:00:00 2001 From: Igor Samokhovets Date: Mon, 16 Feb 2026 19:27:17 +0100 Subject: [PATCH 06/36] simplify --- .../studio/src/components/OptionsPanel.tsx | 14 +++++----- .../studio/src/components/RenderButton.tsx | 26 ++++++++----------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/studio/src/components/OptionsPanel.tsx b/packages/studio/src/components/OptionsPanel.tsx index 359b99c9d46..f6952d04148 100644 --- a/packages/studio/src/components/OptionsPanel.tsx +++ b/packages/studio/src/components/OptionsPanel.tsx @@ -25,8 +25,8 @@ type OptionsSidebarPanel = 'input-props' | 'renders' | 'visual-controls'; const localStorageKey = 'remotion.sidebarPanel'; -const getSelectedPanel = (readOnlyStudio: boolean): OptionsSidebarPanel => { - if (readOnlyStudio && !SHOW_BROWSER_RENDERING) { +const getSelectedPanel = (renderingAvailable: boolean): OptionsSidebarPanel => { + if (!renderingAvailable) { return 'input-props'; } @@ -64,6 +64,8 @@ export const OptionsPanel: React.FC<{ ); const [saving, setSaving] = useState(false); + const renderingAvailable = !readOnlyStudio || SHOW_BROWSER_RENDERING; + const isMobileLayout = useMobileLayout(); const visualControlsTabActivated = useContext( @@ -83,7 +85,7 @@ export const OptionsPanel: React.FC<{ ); const [panel, setPanel] = useState(() => - getSelectedPanel(readOnlyStudio), + getSelectedPanel(renderingAvailable), ); const onPropsSelected = useCallback(() => { setPanel('input-props'); @@ -208,12 +210,12 @@ export const OptionsPanel: React.FC<{ ) : null} ) : null} - {readOnlyStudio && !SHOW_BROWSER_RENDERING ? null : ( + {renderingAvailable ? ( - )} + ) : null}
{panel === `input-props` && composition ? ( @@ -230,7 +232,7 @@ export const OptionsPanel: React.FC<{ /> ) : panel === 'visual-controls' && visualControlsTabActivated ? ( - ) : readOnlyStudio && !SHOW_BROWSER_RENDERING ? null : ( + ) : !renderingAvailable ? null : ( )}
diff --git a/packages/studio/src/components/RenderButton.tsx b/packages/studio/src/components/RenderButton.tsx index 9fd00912676..6465994eb2a 100644 --- a/packages/studio/src/components/RenderButton.tsx +++ b/packages/studio/src/components/RenderButton.tsx @@ -185,6 +185,9 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ const connectionStatus = useContext(StudioServerConnectionCtx) .previewServerState.type; + const canRender = + connectionStatus === 'connected' || SHOW_BROWSER_RENDERING; + const renderType: RenderType = useMemo(() => { if (connectionStatus === 'disconnected' && SHOW_BROWSER_RENDERING) { return 'client-render'; @@ -198,10 +201,9 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ }, [connectionStatus, preferredRenderType]); const shortcut = areKeyboardShortcutsDisabled() ? '' : '(R)'; - const tooltip = - connectionStatus === 'connected' || SHOW_BROWSER_RENDERING - ? 'Export the current composition ' + shortcut - : 'Connect to the Studio server to render'; + const tooltip = canRender + ? 'Export the current composition ' + shortcut + : 'Connect to the Studio server to render'; const iconStyle: SVGProps = useMemo(() => { return { @@ -421,15 +423,13 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({ }, [dropdownOpened, size, spaceToBottom, spaceToTop]); const containerStyle = useMemo((): React.CSSProperties => { - const isDisabled = - connectionStatus !== 'connected' && !SHOW_BROWSER_RENDERING; return { ...splitButtonContainer, borderColor: INPUT_BORDER_COLOR_UNHOVERED, - opacity: isDisabled ? 0.7 : 1, - cursor: isDisabled ? 'inherit' : 'pointer', + opacity: canRender ? 1 : 0.7, + cursor: canRender ? 'pointer' : 'inherit', }; - }, [connectionStatus]); + }, [canRender]); const renderLabel = renderType === 'server-render' ? 'Render' : 'Render on web'; @@ -457,9 +457,7 @@ export const RenderButton: React.FC<{readonly readOnlyStudio: boolean}> = ({