{"record":{"id":"fc0246d55e8ae157","repo":"remotion-dev/remotion","slug":"writeaudiountil-timestamps-must-be-monotonically","errorCode":null,"errorMessage":"writeAudioUntil() timestamps must be monotonically increasing.","messagePattern":"writeAudioUntil\\(\\) timestamps must be monotonically increasing\\.","errorType":"validation","errorClass":"RangeError","httpStatus":null,"severity":"error","filePath":"packages/video-matting/src/prepare-audio.ts","lineNumber":242,"sourceCode":"\t\t\t\tif (primed || finished || canceled) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tprimed = true;\n\t\t\t\tawait writePacketsUntil({\n\t\t\t\t\toutputTimestamp: 0,\n\t\t\t\t\twriteAtLeastOne: true,\n\t\t\t\t});\n\t\t\t},\n\t\t\twriteAudioUntil: async (outputTimestamp) => {\n\t\t\t\tif (!Number.isFinite(outputTimestamp) || outputTimestamp < 0) {\n\t\t\t\t\tthrow new TypeError(\n\t\t\t\t\t\t'outputTimestamp must be a finite, non-negative number.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (outputTimestamp < lastWriteTimestamp) {\n\t\t\t\t\tthrow new RangeError(\n\t\t\t\t\t\t'writeAudioUntil() timestamps must be monotonically increasing.',\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tif (finished || canceled) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlastWriteTimestamp = outputTimestamp;\n\t\t\t\tawait writePacketsUntil({\n\t\t\t\t\toutputTimestamp: Math.min(\n\t\t\t\t\t\toutputTimestamp,\n\t\t\t\t\t\tvideoEndTimestamp - videoStartTimestamp,\n\t\t\t\t\t),\n\t\t\t\t\twriteAtLeastOne: false,\n\t\t\t\t});\n\t\t\t},\n\t\t\tfinishAudio: async () => {","sourceCodeStart":224,"sourceCodeEnd":260,"githubUrl":"https://github.com/remotion-dev/remotion/blob/b2f4e34732f3c6c222ea029413e22dc402218cd7/packages/video-matting/src/prepare-audio.ts#L224-L260","documentation":"writeAudioUntil() enforces that each successive outputTimestamp is greater than or equal to the previously written timestamp. Media sinks cannot seek backwards while streaming, so a regressing timestamp raises a RangeError to protect the Opus encoder's monotonic timestamp contract.","triggerScenarios":"Calling writeAudioUntil(5) after writeAudioUntil(10); restarting a render loop without re-calling prepareAudio; replaying a frame sequence with decreasing frame indices.","commonSituations":"Re-rendering a timeline segment in-place and rewinding the clock; off-by-one in a loop that passes the previous frame's timestamp; reusing a prepared audio pipeline across two renders.","solutions":["Ensure each call passes a timestamp >= the last one (monotonic, equal is allowed).","Create a fresh pipeline with prepareAudio() when you need to restart from an earlier timestamp.","Track lastTimestamp in the caller and skip or clamp calls that would go backwards."],"exampleFix":"// before\nawait audio.writeAudioUntil(currentFrame / fps); // currentFrame can decrease\n// after\nconst t = Math.max(lastT, currentFrame / fps);\nawait audio.writeAudioUntil(t);\nlastT = t;","handlingStrategy":"validation","validationCode":"if (t < lastWritten) t = lastWritten; // or skip the call\nawait audio.writeAudioUntil(t); lastWritten = t;","typeGuard":"null","tryCatchPattern":"try { await audio.writeAudioUntil(t); } catch (e) { if (e instanceof RangeError && e.message.includes('monotonically')) { lastWritten = 0; /* recreate pipeline */ } else throw e; }","preventionTips":["Track the last written timestamp in a variable and only move forward","Recreate the pipeline on seek instead of rewinding","Route all writes through a single serialized writer"],"tags":["rangeerror","webcodecs","stateful-api"],"backgroundTag":"value-out-of-range","analyzedSha":"b2f4e34732f3c6c222ea029413e22dc402218cd7","analyzedAt":"2026-09-09T13:27:51.281Z","contentChangedAt":"2026-09-09T13:27:51.281Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}