Skip to content

@remotion/renderer: imageSequencePattern option for renderFrames() #5346

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions packages/cli/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const {
hardwareAccelerationOption,
audioLatencyHintOption,
enableCrossSiteIsolationOption,
imageSequencePatternOption,
} = BrowserSafeApis.options;

declare global {
Expand Down Expand Up @@ -500,6 +501,11 @@ declare global {
* Prefer lossless audio encoding. Default: false
*/
readonly setPublicPath: (publicPath: string | null) => void;
/**
* Set the pattern for naming image sequence files. Supports [frame] and [ext] replacements.
* @param pattern The pattern string, e.g. 'frame_[frame].[ext]'.
*/
readonly setImageSequencePattern: (pattern: string | null) => void;
}
}

Expand Down Expand Up @@ -674,6 +680,7 @@ export const Config: FlatConfig = {
setBinariesDirectory: binariesDirectoryOption.setConfig,
setPreferLosslessAudio: preferLosslessOption.setConfig,
setPublicPath: publicPathOption.setConfig,
setImageSequencePattern: imageSequencePatternOption.setConfig,
setHardwareAcceleration: hardwareAccelerationOption.setConfig,
setEnableCrossSiteIsolation: enableCrossSiteIsolationOption.setConfig,
};
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/parse-command-line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type CommandLineOptions = {
typeof enableMultiprocessOnLinuxOption
>;
repro: boolean;
'image-sequence-pattern': string;
};

export const parseCommandLine = () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/render-flows/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export const renderVideoFlow = async ({
hardwareAcceleration,
chromeMode,
audioLatencyHint,
imageSequencePattern,
}: {
remotionRoot: string;
fullEntryPoint: string;
Expand Down Expand Up @@ -174,6 +175,7 @@ export const renderVideoFlow = async ({
hardwareAcceleration: HardwareAccelerationOption;
chromeMode: ChromeMode;
audioLatencyHint: AudioContextLatencyCategory | null;
imageSequencePattern: string | null;
}) => {
const isVerbose = RenderInternals.isEqualOrBelowLogLevel(logLevel, 'verbose');

Expand Down Expand Up @@ -535,9 +537,10 @@ export const renderVideoFlow = async ({
onBrowserDownload,
onArtifact,
chromeMode,
imageSequencePattern,
});

Log.info({indent, logLevel}, chalk.blue(`▶ ${absoluteOutputFile}`));
Log.info({indent, logLevel}, chalk.blue(`\n▶ ${absoluteOutputFile}`));
return;
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/render-queue/process-video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,6 @@ export const processVideoJob = async ({
chromeMode: job.chromeMode,
offthreadVideoThreads: job.offthreadVideoThreads,
audioLatencyHint: null,
imageSequencePattern: null,
});
};
5 changes: 5 additions & 0 deletions packages/cli/src/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const {
chromeModeOption,
offthreadVideoThreadsOption,
audioLatencyHintOption,
imageSequencePatternOption,
} = BrowserSafeApis.options;

export const render = async (
Expand Down Expand Up @@ -189,6 +190,9 @@ export const render = async (
const audioLatencyHint = audioLatencyHintOption.getValue({
commandLine: parsedCli,
}).value;
const imageSequencePattern = imageSequencePatternOption.getValue({
commandLine: parsedCli,
}).value;

await renderVideoFlow({
fullEntryPoint,
Expand Down Expand Up @@ -255,5 +259,6 @@ export const render = async (
chromeMode,
offthreadVideoThreads,
audioLatencyHint,
imageSequencePattern,
});
};
11 changes: 11 additions & 0 deletions packages/docs/docs/cli/render.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ Inline JSON string isn't supported on Windows shells because it removes the `"`

[`jpeg` or `png` - JPEG is faster, but doesn't support transparency.](/docs/config#setvideoimageformat) The default image format is `jpeg` since v1.1.

### `--image-sequence-pattern` <AvailableFrom v="4.0.313" />

[Pattern for naming image sequence files. Supports `[frame]` for the zero-padded frame number and `[ext]` for the file extension.]

**Example:**

```bash
npx remotion render ... --sequence --image-sequence-pattern='frame_[frame]_custom.[ext]'
# Produces: frame_0001_custom.jpeg, frame_0002_custom.jpeg, ...
```

### `--config`<AvailableFrom v="1.2.0" />

Specify a location for the Remotion config file.
Expand Down
18 changes: 18 additions & 0 deletions packages/docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -969,3 +969,21 @@ The old way is deprecated, but will work for the foreseeable future.
## See also

- [Encoding guide](/docs/encoding)

### setImageSequencePattern(pattern)

Set the pattern for naming image sequence files when rendering an image sequence.

- `[frame]` will be replaced with the zero-padded frame number (e.g. 0001, 0002, ...)
- `[ext]` will be replaced with the image format extension (e.g. jpeg, png)

**Example:**

```js
// remotion.config.ts
import {Config} from 'remotion';

Config.setImageSequencePattern('frame_[frame]_custom.[ext]');
```

This will produce files like `frame_0001_custom.jpeg`, `frame_0002_custom.jpeg`, ...
19 changes: 19 additions & 0 deletions packages/docs/docs/renderer/render-frames.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,25 @@ _optional since v4.0 - default "jpeg"_
- Choose `png` if you want your image sequence to have an alpha channel (for transparency).
- Choose `none` if you only want to render audio.

### `imageSequencePattern?` <AvailableFrom v="4.0.313" />

A string pattern for naming the output image sequence files. You can use the following magic replacements:

- `[frame]`: Will be replaced with the zero-padded frame number (e.g. 0001, 0002, ...)
- `[ext]`: Will be replaced with the image format extension (e.g. jpeg, png)

Default: `element-[frame].[ext]`

**Example:**

```js
renderFrames({
...otherOptions,
imageSequencePattern: 'frame_[frame]_custom.[ext]',
});
// Produces: frame_0001_custom.jpeg, frame_0002_custom.jpeg, ...
```

### `concurrency?`

_optional_
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/components/VideoPlayerWithControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const VideoPlayerWithControls = forwardRef<
);

useEffect(() => {
// @ts-expect-error we don't bother to type it
window.global_seek_to = (time: number) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
Expand Down
34 changes: 23 additions & 11 deletions packages/renderer/src/get-frame-padded-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import type {VideoImageFormat} from './image-format';

export type CountType = 'from-zero' | 'actual-frames';

const padIndex = ({
num,
filePadLength,
export const getFrameOutputFileNameFromPattern = ({
pattern,
frame,
ext,
}: {
num: number;
filePadLength: number;
pattern: string;
frame: string;
ext: string;
}) => {
return String(num).padStart(filePadLength, '0');
return pattern.replace(/\[frame\]/g, frame).replace(/\[ext\]/g, ext);
};

export const getFrameOutputFileName = ({
Expand All @@ -24,26 +26,36 @@ export const getFrameOutputFileName = ({
countType,
lastFrame,
totalFrames,
imageSequencePattern,
}: {
index: number;
frame: number;
imageFormat: VideoImageFormat;
countType: CountType;
lastFrame: number;
totalFrames: number;
imageSequencePattern: string | null;
}) => {
const filePadLength = getFilePadLength({lastFrame, countType, totalFrames});
const frameStr =
countType === 'actual-frames'
? String(frame).padStart(filePadLength, '0')
: String(index).padStart(filePadLength, '0');
if (imageSequencePattern) {
return getFrameOutputFileNameFromPattern({
pattern: imageSequencePattern,
frame: frameStr,
ext: imageFormat,
});
}

const prefix = 'element';

if (countType === 'actual-frames') {
const paddedIndex = padIndex({filePadLength, num: frame});
return `${prefix}-${paddedIndex}.${imageFormat}`;
return `${prefix}-${frameStr}.${imageFormat}`;
}

if (countType === 'from-zero') {
const paddedIndex = padIndex({filePadLength, num: index});
return `${prefix}-${paddedIndex}.${imageFormat}`;
return `${prefix}-${frameStr}.${imageFormat}`;
}

throw new TypeError('Unknown count type');
Expand Down
37 changes: 37 additions & 0 deletions packages/renderer/src/options/image-sequence-pattern.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type {AnyRemotionOption} from './option';

const cliFlag = 'image-sequence-pattern' as const;

let currentImageSequencePattern: string | null = null;

// Option for --image-sequence-pattern
export const imageSequencePatternOption = {
name: 'Image Sequence Pattern',
cliFlag,
ssrName: 'imageSequencePattern',
description: () => (
<>
Pattern for naming image sequence files. Supports <code>[frame]</code> for
the zero-padded frame number and <code>[ext]</code> for the file
extension.
</>
),
docLink: null,
type: 'string' as string | null,
getValue: ({commandLine}) => {
if (currentImageSequencePattern !== null) {
return {
value: currentImageSequencePattern,
source: 'config',
};
}

return {
value: commandLine[cliFlag] as string,
source: 'cli',
};
},
setConfig: (pattern: string | null) => {
currentImageSequencePattern = pattern;
},
} satisfies AnyRemotionOption<string | null>;
2 changes: 2 additions & 0 deletions packages/renderer/src/options/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {forSeamlessAacConcatenationOption} from './for-seamless-aac-concatenatio
import {glOption} from './gl';
import {hardwareAccelerationOption} from './hardware-acceleration';
import {headlessOption} from './headless';
import {imageSequencePatternOption} from './image-sequence-pattern';
import {jpegQualityOption} from './jpeg-quality';
import {audioLatencyHintOption} from './latency-hint';
import {logLevelOption} from './log-level';
Expand Down Expand Up @@ -87,6 +88,7 @@ export const allOptions = {
apiKeyOption,
audioLatencyHintOption,
enableCrossSiteIsolationOption,
imageSequencePatternOption,
};

export type AvailableOptions = keyof typeof allOptions;
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer/src/options/options-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {encodingMaxRateOption} from './encoding-max-rate';
import {enforceAudioOption} from './enforce-audio';
import {forSeamlessAacConcatenationOption} from './for-seamless-aac-concatenation';
import {hardwareAccelerationOption} from './hardware-acceleration';
import {imageSequencePatternOption} from './image-sequence-pattern';
import {jpegQualityOption} from './jpeg-quality';
import {logLevelOption} from './log-level';
import {mutedOption} from './mute';
Expand Down Expand Up @@ -95,6 +96,7 @@ export const optionsMap = {
binariesDirectory: binariesDirectoryOption,
onBrowserDownload: onBrowserDownloadOption,
chromeMode: chromeModeOption,
imageSequencePattern: imageSequencePatternOption,
},
renderMediaOnLambda: {
offthreadVideoCacheSizeInBytes: offthreadVideoCacheSizeInBytesOption,
Expand Down
5 changes: 5 additions & 0 deletions packages/renderer/src/render-frame-and-retry-target-close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const renderFrameAndRetryTargetClose = async ({
onFrameBuffer,
onFrameUpdate,
nextFrameToRender,
imageSequencePattern,
}: {
retriesLeft: number;
attempt: number;
Expand Down Expand Up @@ -88,6 +89,7 @@ export const renderFrameAndRetryTargetClose = async ({
timeToRenderInMilliseconds: number,
) => void);
nextFrameToRender: NextFrameToRender;
imageSequencePattern: string | null;
}): Promise<void> => {
const currentPool = await poolPromise;

Expand Down Expand Up @@ -129,6 +131,7 @@ export const renderFrameAndRetryTargetClose = async ({
nextFrameToRender,
frame,
page: freePage,
imageSequencePattern,
}),
new Promise((_, reject) => {
cancelSignal?.(() => {
Expand Down Expand Up @@ -211,6 +214,7 @@ export const renderFrameAndRetryTargetClose = async ({
onFrameBuffer,
onFrameUpdate,
nextFrameToRender,
imageSequencePattern,
});
}

Expand Down Expand Up @@ -264,6 +268,7 @@ export const renderFrameAndRetryTargetClose = async ({
onFrameBuffer,
onFrameUpdate,
nextFrameToRender,
imageSequencePattern,
});
}
};
3 changes: 3 additions & 0 deletions packages/renderer/src/render-frame-with-option-to-reject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const renderFrameWithOptionToReject = async ({
onFrameUpdate,
frame,
page,
imageSequencePattern,
}: {
reject: (err: Error) => void;
width: number;
Expand Down Expand Up @@ -83,6 +84,7 @@ export const renderFrameWithOptionToReject = async ({
) => void);
frame: number;
page: Page;
imageSequencePattern: string | null;
}) => {
const startTime = performance.now();

Expand Down Expand Up @@ -153,6 +155,7 @@ export const renderFrameWithOptionToReject = async ({
countType,
lastFrame,
totalFrames: framesToRender.length,
imageSequencePattern,
}),
),
jpegQuality,
Expand Down
3 changes: 3 additions & 0 deletions packages/renderer/src/render-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const renderFrame = ({
framesRenderedObj,
frame,
page,
imageSequencePattern,
}: {
attempt: number;
indent: boolean;
Expand Down Expand Up @@ -71,6 +72,7 @@ export const renderFrame = ({
nextFrameToRender: NextFrameToRender;
frame: number;
page: Page;
imageSequencePattern: string | null;
}) => {
return new Promise<void>((resolve, reject) => {
renderFrameWithOptionToReject({
Expand Down Expand Up @@ -103,6 +105,7 @@ export const renderFrame = ({
onFrameUpdate,
frame,
page,
imageSequencePattern,
})
.then(() => {
resolve();
Expand Down
Loading
Loading