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

  1. Call finalize() exactly once per StreamingPitchShifter instance, at the single end-of-stream point; remove duplicate calls from success and cleanup paths.
  2. If teardown may run twice (React StrictMode, double cleanup), guard the call: `if (!shifter.isFinalized()) shifter.finalize();` or track your own boolean.
  3. Create a fresh shifter per stream instead of sharing one instance across streams that each get finalized.
  4. 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

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


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)