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

  1. Do not call getWriter() on the stream you pass, or call writer.releaseLock() before invoking separateVideoLayers()
  2. Pass a fresh or TransformStream-derived writable that no other consumer has locked
  3. 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

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


AI-assisted analysis of remotion-dev/remotion@b2f4e34732 (2026-09-09). Data as JSON: /api/errors/c23bd105510f10d2. Report an issue: GitHub.