Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
91287f7
@remotion/studio: Add JSON and SRT caption importing
samohovets Sep 3, 2026
5160199
@remotion/studio: Show caption import as button
samohovets Sep 3, 2026
b418cf2
Fix caption import button E2E locator
samohovets Sep 3, 2026
ab4151a
@remotion/studio: Limit caption imports to Remotion JSON
samohovets Sep 4, 2026
04d119a
@remotion/studio: Reuse existing caption save flow
samohovets Sep 4, 2026
fcd9830
@remotion/studio: Import captions without confirmation
samohovets Sep 4, 2026
1b8f2e6
Elements: Show embedded libraries in dark mode
samohovets Sep 4, 2026
5c2737b
Merge pull request #11055 from remotion-dev/captions-import-workflow
samohovets Sep 4, 2026
2c2074f
Merge pull request #11063 from remotion-dev/elements-embedded-iframe-…
samohovets Sep 4, 2026
4f74ce5
Center Elements installed into compositions
samohovets Sep 4, 2026
caa8efc
Elements: Remove product collection image fit option
samohovets Sep 4, 2026
533e253
Select Elements after installation
samohovets Sep 4, 2026
678721c
Elements: Refine product card shadows
samohovets Sep 4, 2026
8bbc84c
Merge pull request #11064 from remotion-dev/elements-center-on-install
samohovets Sep 4, 2026
5bfee9a
Merge pull request #11065 from remotion-dev/product-collection-image-fit
samohovets Sep 4, 2026
517b0f0
Fix Element installation E2E flow
samohovets Sep 4, 2026
af42389
Merge pull request #11066 from remotion-dev/auto-select-element-after…
samohovets Sep 4, 2026
ef7c6ba
`remotion`: Add `assetType` to `InteractivitySchema` (#11068)
samohovets Sep 4, 2026
dbb2c0a
Add asset types to Elements (#11069)
samohovets Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/Img.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ const CanvasImageWithPrivateProps = CanvasImage as React.ComponentType<
export const imgSchema = {
src: {
type: 'asset',
assetType: 'image',
default: undefined,
description: 'Source',
keyframable: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/animated-image/AnimatedImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {resolveAnimatedImageSource} from './resolve-image-source';
export const animatedImageSchema = {
src: {
type: 'asset',
assetType: 'image',
default: undefined,
description: 'Source',
keyframable: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/canvas-image/CanvasImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {CanvasImageCanvasProps, CanvasImageProps} from './props.js';
export const canvasImageSchema = {
src: {
type: 'asset',
assetType: 'image',
default: undefined,
description: 'Source',
keyframable: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/interactivity-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export type FontWeightFieldSchema = {

export type AssetFieldSchema = {
type: 'asset';
assetType?: 'audio' | 'video' | 'image';
default: string | undefined;
description?: string;
keyframable?: false;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/test/Img.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,7 @@ test('Img with effects renders through the canvas image path', async () => {
test('<Img> schema exposes src and crop but not fit', () => {
expect(imgSchema.src).toEqual({
type: 'asset',
assetType: 'image',
default: undefined,
description: 'Source',
keyframable: false,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/test/canvas-image.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ test('<CanvasImage> registers its canvas as the outline ref', async () => {
test('<CanvasImage> schema exposes src and non-keyframable premounting fields', () => {
expect(canvasImageSchema.src).toEqual({
type: 'asset',
assetType: 'image',
default: undefined,
description: 'Source',
keyframable: false,
Expand Down
23 changes: 23 additions & 0 deletions packages/docs/docs/interactivity-schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,29 @@ Shows an on/off control.

Shows a color control. The value is a CSS color string.

### `asset`

Shows a control for selecting an asset source.

#### `assetType?`<AvailableFrom v="4.0.521" />

Set to `'audio'`, `'video'` or `'image'` to only show matching files when replacing this field's asset in Remotion Studio.

`assetType` describes the intended content. It does not inspect or validate the current value or remote URLs.

```ts twoslash title="schema.ts"
import type {InteractivitySchema} from 'remotion';

export const schema = {
audioSrc: {
type: 'asset',
assetType: 'audio',
default: 'https://example.com/audio.mp3',
description: 'Audio source',
},
} as const satisfies InteractivitySchema;
```

### `font-family`<AvailableFrom v="4.0.485" />

Shows a font family control. The value is a CSS font family string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const mirroredAudioSpectrumSchema = {
...Interactive.baseSchema,
audioSrc: {
type: 'asset',
assetType: 'audio',
default:
'https://remotion.media/elements/remotion-made-this-picture-move.mp3',
description: 'Audio source',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const audioOscilloscopeSchema = {
...Interactive.baseSchema,
audioSrc: {
type: 'asset',
assetType: 'audio',
default:
'https://remotion.media/elements/remotion-made-this-picture-move.mp3',
description: 'Audio source',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const audioWaveformProgressSchema = {
...Interactive.baseSchema,
audioSrc: {
type: 'asset',
assetType: 'audio',
default:
'https://remotion.media/elements/remotion-made-this-picture-move.mp3',
description: 'Audio source',
Expand Down
10 changes: 10 additions & 0 deletions packages/docs/elements/captions/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,14 @@ import {ElementLibrary} from '@site/src/components/Elements/ElementLibrary';

# Captions

## Importing captions into Studio<AvailableFrom v="4.0.521" />

<Step>1</Step> Install an Element from the [Captions category](/elements/captions), or add [`Interactive.captionsSchema`](/docs/interactivity-schema#interactivecaptionsschema) to an interactive component that accepts [`Caption[]`](/docs/captions/caption).
<br />
<Step>2</Step> Select its caption layer in Studio.
<br />
<Step>3</Step> In the Captions inspector, click **Import** and choose a JSON file containing a Remotion [`Caption[]`](/docs/captions/caption).

Files are processed locally. Importing SRT and transcription service JSON is tracked in [#11062](https://github.com/remotion-dev/remotion/issues/11062).

<ElementLibrary category="captions" />
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ type ProductCardProps = InteractiveBaseProps &
readonly count: number;
readonly discount: string;
readonly image: string;
readonly imageFit: 'contain' | 'cover';
readonly index: number;
readonly originalPrice: string;
readonly price: string;
Expand All @@ -42,19 +41,11 @@ const productCardSchema = {
},
image: {
type: 'asset',
assetType: 'image',
default:
'https://remotion.media/elements/product-collection-cloudline-runner.png',
description: 'Product image',
},
imageFit: {
type: 'enum',
default: 'cover',
description: 'Image fit',
variants: {
contain: {},
cover: {},
},
},
price: {
type: 'text-content',
default: '$100',
Expand Down Expand Up @@ -85,7 +76,6 @@ const ProductCardInner = forwardRef<
count,
discount,
image,
imageFit,
index,
name,
originalPrice,
Expand Down Expand Up @@ -194,6 +184,8 @@ const ProductCardInner = forwardRef<
style={{
...style,
backgroundColor: '#ffffff',
borderRadius: 6,
boxShadow: '0 2px 6px rgba(29, 29, 25, 0.12)',
boxSizing: 'border-box',
color: '#1d1d19',
display: 'flex',
Expand All @@ -218,7 +210,7 @@ const ProductCardInner = forwardRef<
src={image}
style={{
height: '100%',
objectFit: imageFit,
objectFit: 'cover',
objectPosition: '50% 50%',
scale: interpolate(
frame,
Expand Down Expand Up @@ -426,7 +418,6 @@ export const ProductCollection = () => {
count={5}
discount="20% off"
image="https://remotion.media/elements/product-collection-cloudline-runner.png"
imageFit="cover"
index={0}
name="Cloudline Runner card"
originalPrice="$148"
Expand All @@ -438,7 +429,6 @@ export const ProductCollection = () => {
count={5}
discount=""
image="https://remotion.media/elements/product-collection-minimal-steel-watch.png"
imageFit="cover"
index={1}
name="Minimal Steel Watch card"
originalPrice=""
Expand All @@ -450,7 +440,6 @@ export const ProductCollection = () => {
count={5}
discount="Save $31"
image="https://images.unsplash.com/photo-1511499767150-a48a237f0083?fm=jpg&fit=crop&w=900&q=90"
imageFit="cover"
index={2}
name="Studio Sunglasses card"
originalPrice="$125"
Expand All @@ -462,7 +451,6 @@ export const ProductCollection = () => {
count={5}
discount="New"
image="https://images.unsplash.com/photo-1505740420928-5e560c06d30e?fm=jpg&fit=crop&w=900&q=90"
imageFit="cover"
index={3}
name="Studio Headset card"
originalPrice=""
Expand All @@ -474,7 +462,6 @@ export const ProductCollection = () => {
count={5}
discount=""
image="https://images.unsplash.com/photo-1507473885765-e6ed057f782c?fm=jpg&fit=crop&w=900&q=90"
imageFit="cover"
index={4}
name="Sculptural Table Lamp card"
originalPrice=""
Expand Down
10 changes: 9 additions & 1 deletion packages/docs/src/components/Elements/ElementPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import Head from '@docusaurus/Head';
import {
installInStudio,
type InstallInStudioErrorCode,
isInsideStudio,
setStudioDragData,
StudioProtocolInternals,
type InstallInStudioErrorCode,
} from '@remotion/studio-protocol';
import React, {
useCallback,
Expand Down Expand Up @@ -345,6 +345,14 @@ export const ElementPage: React.FC<ElementPageProps> = ({

<div className={styles.details}>
<p className={styles.description}>{description}</p>
{definition.category === 'captions' ? (
<p className={styles.description} style={{marginTop: 8}}>
Have captions?{' '}
<a href="/elements/captions/#importing-captions-into-studio">
Import Remotion Caption[] JSON from Studio.
</a>
</p>
) : null}
<dl className={styles.metadata}>
<div>
<dt>Dimensions</dt>
Expand Down
71 changes: 66 additions & 5 deletions packages/example/e2e/captions-inspector.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,78 @@ test.describe('captions inspector', () => {
}).toPass({timeout: 30_000});
await expect(defaultCaption).toBeEnabled();

await defaultCaption.fill('Editable captions');
const importCaptionsButton = page.getByRole('button', {
name: 'Import',
exact: true,
});
const importCaptionsInput = page.getByLabel('Import captions file');
await expect(importCaptionsButton).toBeVisible();

const sourceBeforeFailedImport = fs.readFileSync(
elementCaptionsFile,
'utf-8',
);
await importCaptionsInput.setInputFiles({
name: 'broken.json',
mimeType: 'application/json',
buffer: Buffer.from(
JSON.stringify([
{
text: 'Broken',
endMs: 1000,
timestampMs: 500,
confidence: null,
},
]),
),
});
await expect(
page.getByText(
/broken\.json:.*captions\[0\]\.startMs must be a finite, non-negative number/,
),
).toBeVisible();
expect(fs.readFileSync(elementCaptionsFile, 'utf-8')).toBe(
sourceBeforeFailedImport,
);

await importCaptionsInput.setInputFiles({
name: 'captions.json',
mimeType: 'application/json',
buffer: Buffer.from(
JSON.stringify([
{
text: 'Imported',
startMs: 100,
endMs: 400,
timestampMs: 250,
confidence: null,
},
{
text: ' captions',
startMs: 400,
endMs: 900,
timestampMs: 650,
confidence: null,
},
]),
),
});
await expect(defaultCaption).toHaveValue('Imported');
await expect
.poll(() => fs.readFileSync(elementCaptionsFile, 'utf-8'))
.toMatch(/text:\s*['"]Imported['"][\s\S]*startMs:\s*100/);
expect(fs.readFileSync(elementCallSiteFile, 'utf-8')).toBe(
elementCallSiteSourceBefore,
);

await defaultCaption.fill('Edited imported caption');
await defaultCaption.blur();
await expect
.poll(() => {
return /text:\s*['"]Editable captions['"]/.test(
return /text:\s*['"]Edited imported caption['"]/.test(
fs.readFileSync(elementCaptionsFile, 'utf-8'),
);
})
.toBe(true);
expect(fs.readFileSync(elementCallSiteFile, 'utf-8')).toBe(
elementCallSiteSourceBefore,
);
});
});
24 changes: 19 additions & 5 deletions packages/example/e2e/studio-protocol.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ const CloseupPlaceholder = () => {
}
});
await context.route(
'https://www.remotion.dev/elements?remotion-studio=true',
'https://www.remotion.dev/elements?remotion-studio=true&docusaurus-theme=dark',
async (route) => {
officialLibraryRequests.push(route.request().url());
await route.fulfill({
Expand All @@ -232,7 +232,7 @@ const CloseupPlaceholder = () => {
},
);
await context.route(
`${externalLibraryUrl}?remotion-studio=true`,
`${externalLibraryUrl}?remotion-studio=true&docusaurus-theme=dark`,
async (route) => {
externalLibraryRequests.push(route.request().url());
await route.fulfill({
Expand Down Expand Up @@ -311,7 +311,7 @@ const CloseupPlaceholder = () => {
);
await expect(officialElementsIframe).toBeVisible();
expect(officialLibraryRequests).toEqual([
'https://www.remotion.dev/elements?remotion-studio=true',
'https://www.remotion.dev/elements?remotion-studio=true&docusaurus-theme=dark',
]);
expect(context.pages()).toHaveLength(2);
await studioPage.keyboard.press('Escape');
Expand Down Expand Up @@ -339,7 +339,7 @@ const CloseupPlaceholder = () => {
);
await expect(elementsIframe).toHaveAttribute('credentialless', '');
expect(externalLibraryRequests).toEqual([
`${externalLibraryUrl}?remotion-studio=true`,
`${externalLibraryUrl}?remotion-studio=true&docusaurus-theme=dark`,
]);
expect(context.pages()).toHaveLength(2);
const elementsFrame = studioPage.frameLocator(
Expand Down Expand Up @@ -377,6 +377,14 @@ const CloseupPlaceholder = () => {
await expect(elementsIframe).toHaveCount(0);
expect(studioProtocolRequests).toEqual([]);
await dialog.getByRole('button', {name: /Install/}).click();
await expect(
studioPage
.getByRole('group', {name: 'Inspector source location'})
.first(),
).toContainText('Protocol Element', {timeout: 30_000});
await expect(
studioPage.getByText('Installed Protocol Element', {exact: true}),
).toBeVisible();

const elementFile = path.join(
temporaryProject,
Expand Down Expand Up @@ -502,9 +510,15 @@ const CloseupPlaceholder = () => {
await expect(studioPage).toHaveURL(/ProtocolElementScene/, {
timeout: 30_000,
});
await expect(
studioPage
.getByRole('group', {name: 'Inspector source location'})
.first(),
).toContainText('Protocol Element', {timeout: 30_000});

await studioPage.bringToFront();
await studioPage.mouse.click(500, 300);
await studioPage.keyboard.press('Escape');
await expect(browseElements).toBeVisible();
await expect
.poll(() =>
fetch(`${studioUrl}/api/studio-protocol`, {
Expand Down
1 change: 1 addition & 0 deletions packages/example/src/mirrored-spectrum.element.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const mirroredAudioSpectrumSchema = {
...Interactive.baseSchema,
audioSrc: {
type: 'asset',
assetType: 'audio',
default:
'https://remotion.media/elements/remotion-made-this-picture-move.mp3',
description: 'Audio source',
Expand Down
Loading
Loading