{"record":{"id":"f8f96016ab1d976a","repo":"remotion-dev/remotion","slug":"the-time-stretcher-has-already-been-finalized-f8f960","errorCode":null,"errorMessage":"The time stretcher has already been finalized.","messagePattern":"The time stretcher has already been finalized\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/web-renderer/src/streaming-pitch-shifter.ts","lineNumber":321,"sourceCode":"\t\tthis.outputLength -= finalizedLength;\n\t\tthis.synthesisPosition -= finalizedLength;\n\n\t\tconst inputFramesToDiscard = Math.max(\n\t\t\t0,\n\t\t\tMath.floor(this.analysisPosition) - this.searchRadius,\n\t\t);\n\t\tfor (let channel = 0; channel < this.numberOfChannels; channel++) {\n\t\t\tthis.input[channel].copyWithin(0, inputFramesToDiscard, this.inputLength);\n\t\t}\n\n\t\tthis.inputLength -= inputFramesToDiscard;\n\t\tthis.analysisPosition -= inputFramesToDiscard;\n\t\treturn result;\n\t}\n\n\tpublic finalize() {\n\t\tif (this.finalized) {\n\t\t\tthrow new Error('The time stretcher has already been finalized.');\n\t\t}\n\n\t\tthis.finalized = true;\n\t\tconst targetLength = Math.round(this.totalInputFrames * this.factor);\n\t\tconst padding = makePlanarAudio(\n\t\t\tthis.numberOfChannels,\n\t\t\tthis.windowSize + this.searchRadius * 2,\n\t\t);\n\t\tthis.input = ensurePlanarCapacity({\n\t\t\tbuffers: this.input,\n\t\t\trequiredLength: this.inputLength + padding[0].length,\n\t\t});\n\t\tfor (let channel = 0; channel < this.numberOfChannels; channel++) {\n\t\t\tthis.input[channel].set(padding[channel], this.inputLength);\n\t\t}\n\n\t\tthis.inputLength += padding[0].length;\n\t\tthis.process();","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/remotion-dev/remotion/blob/46a3a6bf13ca367376ff18bbb06dae2e87e0d068/packages/web-renderer/src/streaming-pitch-shifter.ts#L303-L339","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst audio = shifter.finalize();\ncleanup(() => shifter.finalize()); // second call throws\n\n// after\nlet finalized = false;\nfunction finish() {\n  if (finalized) return;\n  finalized = true;\n  return shifter.finalize();\n}\nconst audio = finish();\ncleanup(finish);","handlingStrategy":"validation","validationCode":"// idempotent finalize wrapper\nlet finalizedOnce = false;\nfunction finalizeOnce(shifter: StreamingPitchShifter) {\n  if (finalizedOnce) return null;\n  finalizedOnce = true;\n  return shifter.finalize();\n}","typeGuard":null,"tryCatchPattern":"try {\n  return shifter.finalize();\n} catch (err) {\n  if (err instanceof Error && err.message === 'The time stretcher has already been finalized.') {\n    return null; // already flushed\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["audio","streaming","lifecycle","double-call"],"backgroundTag":"invalid-state-transition","analyzedSha":"46a3a6bf13ca367376ff18bbb06dae2e87e0d068","analyzedAt":"2026-09-18T00:12:20.256Z","contentChangedAt":"2026-09-18T00:12:20.256Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}