remotion-dev/remotion · error · Error
The time stretcher has already been finalized.
Error message
The time stretcher has already been finalized.
What it means
finalize() flushes the time stretcher and produces the final stretched output; it must run exactly once per instance. The instance tracks a `finalized` flag, and calling finalize() a second time throws this error because there is nothing left to flush and re-finalizing would produce duplicate/incorrect output.
Solutions
- Call finalize() only once per instance; guard the call with an `if (!stretcher.finalized)` check in shared cleanup code
- Deduplicate shutdown paths so end-of-stream and cleanup logic do not both finalize
- Create a fresh StreamingTimeStretcher if you need to process another stream
Example fix
// before
stream.on('end', () => stretcher.finalize());
cleanup(() => stretcher.finalize()); // throws on second call
// after
stream.on('end', () => {
if (!stretcher.finalized) stretcher.finalize();
}); Defensive patterns
Strategy: type-guard
Validate before calling
function canFinalize(s: {readonly finalized: boolean}): boolean {
return !s.finalized;
} Type guard
function isFinalizable(s: StreamingTimeStretcher): boolean {
return !s.finalized;
} Try / catch
try {
stretcher.finalize();
} catch (e) {
if ((e as Error).message.includes('already been finalized')) return; // idempotent shutdown
throw e;
} Prevention
- Wrap finalize in a small helper that checks the finalized flag first to make shutdown idempotent
- Ensure only one code path is responsible for finalizing a given stretcher
- Do not put finalize inside retry loops without an already-finalized check
When it happens
Trigger: Calling finalize() twice on the same StreamingTimeStretcher instance, e.g. finalize triggered both by an explicit call and by a stream 'end' handler, or a retry path that calls finalize again after an earlier error.
Common situations: Event-driven audio pipelines where both the end-of-stream callback and cleanup code call finalize; wrapping finalize in retry logic without idempotency; accidental duplicate shutdown hooks.
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/8d525eaac7974343.
Report an issue: GitHub.
Appendix: source
Thrown at packages/media/src/audio/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)