remotion-dev/remotion · error
The time stretcher has already been finalized.
Error message
The time stretcher has already been finalized.
What it means
finalize() on StreamingPitchShifter flushes the stretcher's internal buffers, pads the input, and computes the final stretched output. It may only be called once per instance; a second call would corrupt the output length math (totalInputFrames vs targetLength), so the class tracks a `finalized` flag and throws this error on repeat calls. This is a deliberate one-shot lifecycle guard.
Solutions
- Call finalize() exactly once per StreamingPitchShifter instance, at the single end-of-stream point; remove duplicate calls from success and cleanup paths.
- If teardown may run twice (React StrictMode, double cleanup), guard the call: `if (!shifter.isFinalized()) shifter.finalize();` or track your own boolean.
- Create a fresh shifter per stream instead of sharing one instance across streams that each get finalized.
- If finalize() itself can throw mid-way and you retry, remember the flag is already set — you must construct a new instance rather than re-finalizing.
Example fix
// before
const audio = shifter.finalize();
cleanup(() => shifter.finalize()); // second call throws
// after
let finalized = false;
function finish() {
if (finalized) return;
finalized = true;
return shifter.finalize();
}
const audio = finish();
cleanup(finish); Defensive patterns
Strategy: validation
Validate before calling
// idempotent finalize wrapper
let finalizedOnce = false;
function finalizeOnce(shifter: StreamingPitchShifter) {
if (finalizedOnce) return null;
finalizedOnce = true;
return shifter.finalize();
} Try / catch
try {
return shifter.finalize();
} catch (err) {
if (err instanceof Error && err.message === 'The time stretcher has already been finalized.') {
return null; // already flushed
}
throw err;
} Prevention
- Make finalize idempotent at the call site with a local boolean so success + finally paths converge on one call.
- Finalize exactly one shifter per stream; do not share an instance across streams each needing a flush.
- In teardown hooks that can run twice (StrictMode, multiple unmounts), guard the finalize call.
- Never retry finalize after an error thrown inside finalize() — construct a new instance instead.
- Prefer an explicit close()/end() owner object so only one code path can finalize.
When it happens
Trigger: Calling pitchShifter.finalize() twice on the same instance — e.g. finalize in both a success path and a cleanup/finally block, calling finalize in a loop over multiple streams sharing one shifter, or a retry that re-invokes finalize after a partial failure.
Common situations: try/finally pipelines where finalize() is in the finally block but also called explicitly on success; components that re-render and re-run a teardown calling finalize on the same shifter; aggregation code that finalizes each sub-stream with one shared shifter instance.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot append audio after the time stretcher was finalized.
- Cannot append audio after the time stretcher was finalized.
- Cannot change the behavior for pre-mounting audio tags…
- Changing the AudioContext sample rate dynamically is not…
- `_experimentalKeepAudioContextAlive` cannot be changed…
AI-assisted analysis of remotion-dev/remotion@46a3a6bf13 (2026-09-18).
Data as JSON: /api/errors/f8f96016ab1d976a.
Report an issue: GitHub.
Appendix: source
Thrown at packages/web-renderer/src/streaming-pitch-shifter.ts:321
this.outputLength -= finalizedLength;
this.synthesisPosition -= finalizedLength;
const inputFramesToDiscard = Math.max(
0,
Math.floor(this.analysisPosition) - this.searchRadius,
);
for (let channel = 0; channel < this.numberOfChannels; channel++) {
this.input[channel].copyWithin(0, inputFramesToDiscard, this.inputLength);
}
this.inputLength -= inputFramesToDiscard;
this.analysisPosition -= inputFramesToDiscard;
return result;
}
public finalize() {
if (this.finalized) {
throw new Error('The time stretcher has already been finalized.');
}
this.finalized = true;
const targetLength = Math.round(this.totalInputFrames * this.factor);
const padding = makePlanarAudio(
this.numberOfChannels,
this.windowSize + this.searchRadius * 2,
);
this.input = ensurePlanarCapacity({
buffers: this.input,
requiredLength: this.inputLength + padding[0].length,
});
for (let channel = 0; channel < this.numberOfChannels; channel++) {
this.input[channel].set(padding[channel], this.inputLength);
}
this.inputLength += padding[0].length;
this.process();View on GitHub (pinned to 46a3a6bf13)