demo · v130

timestamp-shifted forwarding

One of the chromestatus motivation's use cases by name: "Altering the timestamp of a frame to introduce a delay." Picture an SFU that needs to keep video and external IP-camera audio in lip-sync, or a real-time translator that emits the dubbed audio a few hundred ms after the original. Build an encoded transform that adds a constant offset to every frame's RTP timestamp before forwarding.

requires WebRTC Encoded Transform + Chrome 130 modify-metadata The constructor for RTCEncodedVideoFrame/RTCEncodedAudioFrame with custom metadata is what Chrome 130 ships. The probe below shows whether the constructor accepts the new metadata dictionary. The simulation runs regardless.
+120 ms
source frame
camera/mic
transform
read frame.metadata()
build new metadata
new RTCEncodedVideoFrame(...)
downstream
peer connection / SFU
#original RTP tsshifted RTP tsdelta

other modify-metadata use cases

SSRC re-keying

Forward a track from one PeerConnection to another with a new SSRC. The downstream side never sees that the frame originated from a different stream.

mime-type rewriting

Wrap an H.264 baseline frame as VP8 (or vice versa) after transcoding — the transform owns the metadata, not just the payload.

frame dropping with continuity

Skip every Nth B-frame to reduce bandwidth, then rewrite the remaining frames' presentation timestamps so the receiver doesn't see gaps.

lipsync to external audio

Pin video frames to the audio clock by shifting timestamps to match the original sample boundaries the transcribed audio was generated from.

the code

// In the worker that owns the RTCRtpScriptTransform.
self.onmessage = (e) => {
  const transformer = new TransformStream({
    transform(frame, controller) {
      // Read metadata (this part worked pre-Chrome 130).
      const meta = frame.getMetadata();

      // Chrome 130: construct a new frame with mutated metadata.
      const shifted = new RTCEncodedVideoFrame(frame, {
        metadata: {
          ...meta,
          rtpTimestamp: meta.rtpTimestamp + delaySamples,
        },
      });
      controller.enqueue(shifted);
    },
  });
  e.data.readable.pipeThrough(transformer).pipeTo(e.data.writable);
};

// Pre-Chrome 130: you could mutate the payload but not the metadata —
// rtpTimestamp / mimeType / contributingSources were read-only on the
// frame and the constructor did not take a metadata dictionary. The
// only way to "shift" a frame's timestamp was to drop it and forge a
// new one — which broke the contributingSources chain.

see also