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

  1. Call finalize() only once per instance; guard the call with an `if (!stretcher.finalized)` check in shared cleanup code
  2. Deduplicate shutdown paths so end-of-stream and cleanup logic do not both finalize
  3. 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

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


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)