Skip to content

remotion: Add effectsOutputSize to all effect-capable components - #12178

Open
alec-watts wants to merge 10 commits into
remotion-dev:mainfrom
alec-watts:codex/effects-output-size-all-components
Open

alec-watts wants to merge 10 commits into
remotion-dev:mainfrom
alec-watts:codex/effects-output-size-all-components

Conversation

@alec-watts

@alec-watts alec-watts commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #11436 with the cross-component implementation requested by the maintainer. GitHub rejected reopening that closed PR.

Effect sizing previously applied only to <Video>. This revision exposes effectsOutputSize={{width, height}} on every effect-capable component, as requested in review, and runs it through the existing shared effect pipeline.

The source is resized into the selected effect coordinate space, effects run with matching scratch-canvas dimensions, and the result is composited into the component's existing output canvas. Layout, cropping, and final composition dimensions remain unchanged. Omission preserves the existing path; empty and disabled chains avoid resizing. Numeric size changes rebuild the effect state, while equivalent inline objects reuse it. Video previews redraw a paused frame without restarting their decoder.

Supports Video, Img, CanvasImage, AnimatedImage, Gif, HtmlInCanvas, Solid, RemotionRiveCanvas, and all shapes using the existing RenderSvg wrapper. Documentation and a caller example live on the shared effects API page. Current main is merged; the old Video-only resolver, player callbacks, test fixtures, and screenshots are replaced by the shared implementation. Original contributor commits remain in the branch history.

Verification

Actual upstream-source rendering in hidden Electron with public Mediabunny 1.61.3:

  • An authored nine-component scene exercises Video, Img, CanvasImage, AnimatedImage, Gif, HtmlInCanvas, Solid, Rect, and Rive. Lossless PNG pixels and effect callback dimensions verify the selected 32×24 input and scratch-canvas size in all nine paths.
  • A three-frame MP4 changes the size from 32×24 to 48×36, then passes a new object with the same values. FFmpeg decoding verifies all 27 binary state markers. Exactly 18 effect setups and 18 cleanups confirm one rebuild per component and reuse on the same-value frame.
  • Empty-effect output with and without an explicit size has identical full decoded RGBA bytes.
  • Real pixelate/WebGL output preserves top/bottom orientation after resizing. ImageBitmap uploads require the bitmap's own flip orientation; the new resize path accounts for this before upload.
  • A real paused Player redraws after a size update. Native VideoDecoder configuration count stays at one across the change and an equivalent new size object.
  • The focused ownership regression confirms an asynchronously resized bitmap is closed when its effect chain is cancelled. Removing only the release makes that regression fail; restoration passes.
  • Existing effect allocation/cleanup coverage and the new resource guard pass: nine core tests across the effect and animated-image files. The old test that reimplemented the disabled-effect filter was removed instead of retaining a tautological check.
  • Core declaration compilation and focused media/Gif/Rive/shapes typechecks pass with upstream's TypeScript 5.9.3. Changed-file ESLint has zero errors and two pre-existing TODO warnings; formatting and git diff --check pass. React Doctor reports no diagnostics for the changed sources, although its aggregate score is inconsistent with that empty result.

These are focused runtime and source checks. Full upstream CI remains a separate check; no CI, workspace dependency, or temporary harness configuration changes ship.

Why each file changes

File Reason
packages/core/src/HtmlInCanvas.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/Img.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/animated-image/AnimatedImage.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/animated-image/canvas.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/animated-image/props.ts Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/canvas-image/CanvasImage.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/canvas-image/props.ts Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/effects/Solid.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/core/src/effects/effect-types.ts Define the shared public size type.
packages/core/src/effects/index.ts Re-export the shared size type through the existing effect API.
packages/core/src/effects/run-effect-chain.ts Resize once before the existing chain, preserve backend orientation, and release the owned bitmap after completion or cancellation.
packages/core/src/effects/use-effect-chain-state.ts Validate dimensions and reuse or rebuild the existing effect state by numeric size.
packages/core/src/index.ts Expose the size type from remotion.
packages/core/src/test/animated-image.test.tsx Supply the new required nullable size input to the existing hidden-frame retention regression.
packages/core/src/test/effect-internals.test.ts Guard asynchronous bitmap release on cancellation; remove a filter-mirroring test.
packages/docs/docs/effects/index.mdx Document the shared API, supported components, layout contract, and cost.
packages/gif/src/Gif.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/gif/src/GifForDevelopment.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/gif/src/GifForRendering.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/gif/src/canvas.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/gif/src/props.ts Type, consume, and forward the shared size prop through the existing component path.
packages/media/src/video/props.ts Type, consume, and forward the shared size prop through the existing component path.
packages/media/src/video/video-for-preview.tsx Use the shared size state and redraw on numeric changes without recreating the player.
packages/media/src/video/video-for-rendering.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/media/src/video/video.tsx Type, consume, and forward the shared size prop through the existing component path.
packages/rive/src/RemotionRiveCanvas.tsx Pass Rive effect sizing to the shared state owner.
packages/shapes/src/components/render-svg.tsx Forward sizing for every shape through the existing HtmlInCanvas wrapper.

Diff cleanup found no parallel effect runner, player, decoder, canvas pool, or cache. The private resize boundary owns its bitmap while calling the existing chain; other production behavior remains in that incumbent.

Shared CI test repair

Includes the one-line caption-inspector track selector fix from #12179. Current main fails this test before it can exercise caption persistence. Both caption-inspector E2E cases pass in the actual Studio with this repair; restoring the old selector reproduces the failure.

File Reason
packages/example/e2e/captions-inspector.test.mts Repair the existing main-branch selector so shared CI can exercise caption persistence.

Paused-preview review verification: HTML-in-canvas and Rect both repaint when only the numeric processing size changes, with stable effects/children/frame; a new object with the same size produces no extra repaint. Fixed an animated-image dependency issue found in review: size changes now redraw without recreating its native ImageDecoder (1 decoder before and after, versus 2 before the correction). The nine component export probes, WebGL orientation probe, video decoder-retention probe, nine core tests, typechecks, and lint pass after this correction.

Reverse-audio CI test synchronization

The media regression test cleared its Web Audio spy before React applied the new global playback rate in a passive effect, allowing forward-playback scheduling during that transition to count against the reverse-silence assertion. It now waits until the Player is actually reversing, clears the spy, and observes another reverse interval before checking silence. Production audio code is unchanged.

Validation on current main with CI’s pinned Playwright Chromium: all 152 media tests across 47 files pass. Removing the production reverse-audio guard still fails this test with a negative start offset, so the original regression remains covered.

File Reason
packages/media/src/test/player-muted-video.test.tsx Observe reverse silence after the asynchronous Player rate transition, while preserving the regression guard.

Final-head preflight (b3c9045ac): a clean checkout with bun install --frozen-lockfile, actual package builds, and CI’s pinned Playwright Chromium passes all 721 core tests (77 files) and all 152 media tests (47 files). The checkout is clean.

dukeeagle and others added 6 commits September 17, 2026 10:25
Avoid redraws from inline size objects and make internal absence explicit with nullable parameters.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the nullable default on the before composition so the comparison remains accurate.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…view

# Conflicts:
#	packages/media/src/audio/audio-for-preview.tsx
#	packages/media/src/media-player.ts
#	packages/media/src/test/media-player.test.ts
#	packages/media/src/test/premount-arm-kicks-scheduler.test.ts
#	packages/media/src/test/trim-change-seek.test.ts
#	packages/media/src/test/video-iterator-manager.test.ts
#	packages/media/src/test/video-with-no-frames-in-beginning.test.ts
#	packages/media/src/video-iterator-manager.ts
#	packages/media/src/video/props.ts
#	packages/media/src/video/video-for-preview.tsx
#	packages/media/src/video/video.tsx
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
bugs Ready Ready Preview Oct 9, 2026 9:45pm UTC
remotion Ready Ready Preview Oct 9, 2026 9:45pm UTC

Request Review

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Shapes and <HtmlInCanvas> do not repaint when only effectsOutputSize changes, so the Studio preview keeps the previous effect resolution for those components until an unrelated paint fires. Details below; one minor dependency nit is inline.

Reviewed changes

  • Shared size type & pipeline — Adds EffectsOutputSize (exported from remotion), threads it through useEffectChainState, and makes runEffectChain resize the source with createImageBitmap before running the existing chain, owning the bitmap until the run settles.
  • Component wiring — effectsOutputSize added to Video, Img, CanvasImage, AnimatedImage, Gif, HtmlInCanvas, Solid, RemotionRiveCanvas, and every shape via RenderSvg.
  • Preview redraws — VideoForPreview and AnimatedImage re-run their drawing effects on numeric size change; Solid, CanvasImage, Gif, and Rive already depend on the chain-state identity.
  • Docs & tests — New effects-page section with a <Video> example; new cancellation/ownership test; removed the filter-mirroring test.

⚠️ Shapes and <HtmlInCanvas> don't repaint on an effectsOutputSize-only change

effectsOutputSize reaches HtmlInCanvas through useEffectChainState (packages/core/src/HtmlInCanvas.tsx:592), whose identity changes on a numeric size change and is only consumed by onPaintCb. Re-registering that paint listener does not call requestPaint(), and the two effects that do request a paint depend on [onPaint, memoizedEffects] (HtmlInCanvas.tsx:897-909) and [width, height, …, renderFrame, …] (911-944) — neither includes effectsOutputSize/chainState. In preview (renderFrame={null}), changing only effectsOutputSize therefore leaves the canvas showing the old resolution. Every other component in this PR wires the size into its drawing effect's deps, so <HtmlInCanvas> and every shape (via RenderSvg) are the outlier.

Technical details
# Repaint on effectsOutputSize-only change

## Affected sites
- packages/core/src/HtmlInCanvas.tsx:592 — `chainState` identity changes with `effectsOutputSize`.
- packages/core/src/HtmlInCanvas.tsx:897-909 — the "repaint when the paint output changes" effect depends on `[onPaint, memoizedEffects]`, not `chainState`.
- packages/core/src/HtmlInCanvas.tsx:911-944 — the mount/paint-barrier effect depends on `width`/`height`/`canvasSizeKey`/`renderFrame`, not `effectsOutputSize` (in preview `renderFrame` is `null`).
- packages/shapes/src/components/render-svg.tsx:277 — forwards `effectsOutputSize` to `HtmlInCanvas`; inherits the same gap.

## Required outcome
- Updating `effectsOutputSize` while `width`, `height`, `effects`, `onPaint`, children, and frame stay identical must still repaint the `HtmlInCanvas` (and thus shapes) in Studio preview so the new effect resolution takes effect.

## Suggested approach
- Add `chainState` (or `effectsOutputSize?.width`/`.height`) to the deps of the effect at `HtmlInCanvas.tsx:897` so it calls `requestPaint()`, matching how `VideoForPreview` and `AnimatedImage` handle the same case.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash | 𝕏

Comment thread packages/core/src/animated-image/AnimatedImage.tsx Outdated

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ The new commits introduce no new issues. This review is non-approving only because the earlier finding from the initial review is still open.

Reviewed changes (delta since 81b31b6)

  • Animated-image test fixtures — d818c8a threads the now-required effectsOutputSize={null} into the two <Canvas> fixtures in packages/core/src/test/animated-image.test.tsx. No production code changed; the only other delta is the line shift in effect-internals.test.ts.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash | 𝕏

@alec-watts

Copy link
Copy Markdown
Contributor Author

Fixed the animated-image dependency finding in 93e5eb7. A native paused-Player probe now changes processing size from 32×24 to 48×36 while retaining one ImageDecoder; the previous code created two.

I also checked the reported HtmlInCanvas/shape repaint issue with paused Players, stable effects, stable children, and an unchanged frame. Both HtmlInCanvas and Rect redraw at 48×36; replacing the size object with identical values causes no extra repaint. The chain-state change recreates onPaintCb, and its existing initialization effect re-registers the paint listener and resets the canvas target. The nine-component export probes and WebGL orientation probe still pass.

This branch was successfully deployed

2 active deployments
Preview – remotion — b3c9045a Deployed Oct 9, 2026 by vercel[bot]
Preview – bugs — b3c9045a Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants