remotion-dev/remotion · error · TypeError
outputs.${layer}.outputWritable must not already be locked.
Error message
outputs.${layer}.outputWritable must not already be locked. What it means
A WritableStream can be locked (e.g. via getWriter() or getReader on its pipe counterpart). separateVideoLayers() needs exclusive access to write encoded output, so passing an already-locked stream is rejected with this TypeError.
Source
Thrown at packages/video-matting/src/separate-video-layers.ts:148
output.outputWritable !== undefined
) {
throw new TypeError(
`outputs.${layer} cannot specify both outputTarget and outputWritable.`,
);
}
if (output.outputWritable !== undefined) {
if (
typeof WritableStream === 'undefined' ||
!(output.outputWritable instanceof WritableStream)
) {
throw new TypeError(
`outputs.${layer}.outputWritable must be a WritableStream.`,
);
}
if (output.outputWritable.locked) {
throw new TypeError(
`outputs.${layer}.outputWritable must not already be locked.`,
);
}
}
};
const validateOptions = (options: SeparateVideoLayersOptions) => {
if (!options || typeof options !== 'object') {
throw new TypeError('separateVideoLayers() expects an options object.');
}
const isBlob = typeof Blob !== 'undefined' && options.src instanceof Blob;
const isUrl = options.src instanceof URL;
if (typeof options.src !== 'string' && !isUrl && !isBlob) {
throw new TypeError('src must be a string, URL, or Blob.');
}
if (typeof options.src === 'string' && options.src.length === 0) {View on GitHub (pinned to b2f4e34732)
Solutions
- Do not call getWriter() on the stream you pass, or call writer.releaseLock() before invoking separateVideoLayers()
- Pass a fresh or TransformStream-derived writable that no other consumer has locked
- Create two separate streams if you need both base and foreground output
Example fix
// before
const writer = writable.getWriter();
await separateVideoLayers({ src, outputs: { base: { outputWritable: writable } } });
// after
const writer = writable.getWriter();
writer.releaseLock();
await separateVideoLayers({ src, outputs: { base: { outputWritable: writable } } }); Defensive patterns
Strategy: validation
Validate before calling
function assertNotLocked(s) { if (s && s.locked) throw new TypeError('outputWritable is already locked; release the writer first'); } Type guard
const isUnlockedWritable = (v) => v instanceof WritableStream && !v.locked;
Try / catch
try { await separateVideoLayers(opts); } catch (e) { if (e instanceof TypeError && e.message.includes('must not already be locked')) { writer?.releaseLock(); /* retry */ } else throw e; } Prevention
- Avoid getWriter() on streams you hand to the library
- Call releaseLock() as soon as you're done writing
- Never share one stream across pipeline consumers
When it happens
Trigger: Calling output.outputWritable.getWriter() before passing the stream; passing the writable side of a TransformStream that already has a writer attached; piping the stream elsewhere before handing it to separateVideoLayers().
Common situations: Acquiring a writer for logging/teeing and forgetting to releaseLock(); reusing the same stream across two pipeline stages; sharing one stream between base and foreground outputs (see sibling error).
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
- outputs.${layer}.outputWritable must be a WritableStream.
- outputs.base and outputs.foreground must not use the same ou
- Emoji ${emoji} not found. Available emojis: ${emojis.map((e)
- The start and end values must be of the same type. Start val
- Non-animatable values cannot be interpolated. Start value: $
AI-assisted analysis of remotion-dev/remotion@b2f4e34732 (2026-09-09).
Data as JSON: /api/errors/c23bd105510f10d2.
Report an issue: GitHub.