Replacing Video Tracks Without Renegotiation

This guide is part of the Audio/Video Track Management guide, and it answers one practitioner question precisely: how do you switch a sender from camera to screen share — and back — without generating a new offer/answer exchange, and when is renegotiation unavoidable anyway?

Context & Trade-offs

RTCRtpSender.replaceTrack(newTrack) swaps the media source feeding an existing sender while keeping the SSRC, the negotiated codec, and the transceiver’s m-line untouched. Because nothing in the SDP changes, no negotiationneeded event fires and the remote keeps decoding the same conceptual stream — the <video> element on the far side never rebinds srcObject and never flickers. This is the difference between a sub-50 ms source swap and a full signaling round trip that, on a busy path, can stall media for the 200–800 ms it takes to exchange and apply a new offer/answer.

The trade-off is that replaceTrack only works when the new track fits the already-negotiated parameters. A camera-to-screen swap within the same codec family and a compatible resolution range succeeds transparently; the encoder reconfigures on the fly and may emit a single keyframe burst. But renegotiation is still required when: the track kind changes (you cannot replace a video track with audio); you need to add a stream where no sender or compatible transceiver exists; you change the transceiver direction (e.g. from recvonly to sendrecv); or the new source demands a codec the SDP never negotiated. Screen share at a much higher resolution than the camera will still go through replaceTrack, but the encoder’s content hint should switch from motion to detail so text stays sharp — coordinate this with VP8 vs H.264 vs AV1 Codec Selection since screen content favours different codec tuning than camera motion.

A practical guideline: reach for replaceTrack for every same-kind source change, and only fall back to the offer/answer flow described in SDP Offer/Answer Lifecycle when the swap genuinely alters the session’s structure. Keep a try/catch so a rejecting replaceTrack degrades gracefully into renegotiation rather than dropping the call.

It is worth being precise about why this avoids renegotiation. The SDP m-line describes the codec, the RTP header extensions, the payload types, and the SSRC binding — none of which replaceTrack mutates. The browser simply re-points the encoder input at a new raw frame source behind the existing pipeline. A camera at 720p30 and a screen at 1080p15 both encode as, say, VP8 on the same payload type and the same SSRC; the encoder reconfigures its internal resolution and rate-control state, emits a keyframe so the decoder can resync, and carries on. The remote’s depacketiser and jitter buffer never even notice the source changed. This is why the swap is measured in tens of milliseconds rather than the 200–800 ms a signaling round trip would cost, and why it composes cleanly with simulcast: each RID encoding keeps its layer, and only the underlying frames change. The one thing to watch is the keyframe burst — on a constrained uplink, an unsolicited keyframe right after the swap can momentarily spike well above your steady-state bitrate, so size your encoder’s maxBitrate headroom accordingly rather than pinning it at the exact average.

Minimal Runnable Implementation

// Toggle a single video sender between camera and screen share with no SDP exchange.
let cameraStream = null;
let sharingScreen = false;

async function toggleScreenShare(pc) {
  const sender = pc.getSenders().find(s => s.track?.kind === 'video');
  if (!sender) throw new Error('no video sender to replace');

  if (!sharingScreen) {
    // Cache the camera track so we can restore it on the same sender later.
    cameraStream = new MediaStream([sender.track]);
    const display = await navigator.mediaDevices.getDisplayMedia({ video: true });
    const screenTrack = display.getVideoTracks()[0];

    // Hint the encoder that this is detail-heavy content, not motion.
    if ('contentHint' in screenTrack) screenTrack.contentHint = 'detail';

    try {
      await sender.replaceTrack(screenTrack); // SSRC kept, NO renegotiation
      sharingScreen = true;
    } catch (err) {
      // replaceTrack rejected → fall back to a full offer/answer exchange.
      console.warn('replaceTrack failed, renegotiating instead:', err.name);
      screenTrack.stop();
      return;
    }

    // When the user clicks the browser "Stop sharing" chrome, restore the camera.
    screenTrack.addEventListener('ended', async () => {
      const cam = cameraStream.getVideoTracks()[0];
      await sender.replaceTrack(cam); // back to camera, still no SDP exchange
      sharingScreen = false;
    });
  } else {
    const cam = cameraStream.getVideoTracks()[0];
    await sender.replaceTrack(cam);
    sharingScreen = false;
  }
}

Reproduction Steps & Debugging Log Patterns

  1. Establish a connected RTCPeerConnection sending one camera video track.
  2. Attach a negotiationneeded listener that logs — you should see it fire zero times during the swap.
  3. Call toggleScreenShare(pc) and confirm the remote <video> keeps playing without a flash or reload.
  4. Poll getStats(): the outbound-rtp report’s ssrc must be unchanged while frameWidth/frameHeight shift to the screen resolution and framesEncoded keeps climbing.
  5. Click the browser’s native “Stop sharing” banner and verify the ended handler restores the camera on the same sender.

Expected console output:

// Healthy path — note the absence of any negotiationneeded log:
// [stats] ssrc=2954812233 frameWidth=1920 frameHeight=1080 framesEncoded climbing
// (no "negotiationneeded fired" line)

// Failure path that legitimately needs renegotiation:
// replaceTrack failed, renegotiating instead: InvalidModificationError

When Renegotiation Is Unavoidable

It helps to enumerate the cases that genuinely force an offer/answer so you do not waste time fighting replaceTrack against them. First, a kind change: you cannot replace a video track with an audio track — the m-line’s media type is fixed at negotiation, and Firefox rejects synchronously with TypeError. Second, adding a stream where no compatible sender or free transceiver exists: the first time a participant turns on their camera in a previously audio-only call, addTrack (or promoting an inactive transceiver to sendrecv) is the only path, and both fire negotiationneeded. Third, a direction change on the transceiver itself — going from recvonly to sendrecv to begin sending is an SDP-level operation regardless of whether a track is attached. Fourth, a codec requirement the original negotiation never included: if your screen-share source can only be encoded with a profile absent from the negotiated SDP, the encoder cannot proceed on the existing m-line and you must renegotiate to add the codec.

Designing around these cases is mostly about pre-negotiating capacity. If you know a call may add screen share later, negotiate the video m-line up front (even inactive) so the slot and codec already exist, turning the eventual switch into a pure replaceTrack. The same pre-allocation strategy underpins large conferences, where reserving transceiver slots before participants join keeps every renegotiation a structural no-op — a technique covered in depth in SDP Renegotiation Without Dropping Streams. When you do have to renegotiate, do it on a quiet beat — not in the middle of a layer switch or a network handoff — so the offer/answer and any keyframe burst do not stack on top of existing recovery work.

Common Implementation Mistakes

FAQ

When is renegotiation genuinely required despite using replaceTrack?

When the track kind changes, when no compatible sender or transceiver exists yet, when you alter the transceiver direction, or when the new source needs a codec that was never negotiated. Same-kind, compatible-codec source swaps never need it.

Does the remote need to do anything when I replace a track?

No. The SSRC and m-line are unchanged, so the remote keeps rendering the same MediaStreamTrack with no ontrack and no SDP processing. Only a real renegotiation would require remote-side handling.

Will replaceTrack reject if the new resolution is much larger than the old one?

In current Chrome and Firefox it resolves — the encoder reconfigures on the fly. Older or more conservative implementations may reject with InvalidModificationError if the new track cannot be encoded within the negotiated parameters; that is exactly the case your try/catch fallback to a fresh offer/answer is there to catch. The swap will still preserve the SSRC when it succeeds.

Does replaceTrack(null) need renegotiation to stop sending?

No. Passing null clears the media source on the sender while leaving the transceiver and m-line intact, so RTP simply stops without any SDP exchange. It is the cheapest way to pause an outbound video stream you intend to resume on the same sender.

Related: return to Audio/Video Track Management, and compare with Managing Audio Focus & Echo Cancellation Across Devices and SDP Renegotiation Without Dropping Streams.