demo · v141
Video Watermarker
Capture webcam video, feed it through a RTCRtpScriptTransform worker pipeline, and inject a frame counter into each encoded frame's additional data. Shows the full sender → transform → network → transform → decoder flow with live frame metadata.
Webcam + Chrome 141 required
The watermark transform requires
RTCRtpScriptTransform (Chrome 141+). The frame metadata inspector and pipeline visualiser work in all modern browsers. Camera access is requested when you click Start.
probing RTCRtpScriptTransform…
sender (raw / encoded)
receiver (decoded output)
transform pipeline
Camera
MediaStream
→
Encoder
RTCRtpSender
→
Transform
worker: inject
→
···Network···
loopback
→
Transform
worker: strip
→
Decoder
RTCRtpReceiver
frame metadata inspector
Frames inspected: 0 Watermark counter: —
V1 vs V2 API shape
V1 — Insertable Streams
RTCRtpSender.createEncodedStreams()
const { readable, writable } =
sender.createEncodedStreams();
readable.pipeThrough(
new TransformStream({ transform })
).pipeTo(writable);
// Non-standard, Chrome-only
// deprecated in Chrome 141
V2 — RTCRtpScriptTransform
RTCRtpSender.transform (Chrome 141)
const worker = new Worker("xf.js");
sender.transform =
new RTCRtpScriptTransform(worker, {
role: "sender",
watermarkId: crypto.randomUUID(),
});
// Standardised across Chrome/FF/Safari
// Options object sent to worker via
// transformer.options
the worker (xf.js)
// xf.js — sender side
self.onrtctransform = (event) => {
const { readable, writable, options } = event.transformer;
const { role, watermarkId } = options;
let counter = 0;
readable
.pipeThrough(new TransformStream({
transform(frame, controller) {
if (role === "sender") {
// Inject 8-byte trailer: 4B counter + 4B magic
const original = new Uint8Array(frame.data);
const stamped = new Uint8Array(original.length + 8);
stamped.set(original);
new DataView(stamped.buffer).setUint32(original.length, ++counter);
new DataView(stamped.buffer).setUint32(original.length + 4, 0xDEADBEEF);
frame.data = stamped.buffer;
} else {
// Strip 8-byte trailer, read counter
const view = new DataView(frame.data);
const magic = view.getUint32(frame.data.byteLength - 4);
if (magic === 0xDEADBEEF) {
const count = view.getUint32(frame.data.byteLength - 8);
self.postMessage({ type: "watermark", counter: count });
frame.data = frame.data.slice(0, -8);
}
}
controller.enqueue(frame);
}
}))
.pipeTo(writable);
};
see also
- WebRTC Encoded Transform V2 — feature index
- ChromeStatus entry
- Explainer