RTCRtpSender.replaceTrack added behind flag.

Wires up replaceTrack[1] from the content layer implementation[2],
exposing it in JavaScript behind RuntimeEnabled experimental feature
"RTCRtpSenderReplaceTrack".

The meat of this CL are the tests. This includes making sure the track
is set asynchronously (tested with WPT) and that replacing a video
track causes different video to be sent (tested with browser_tests
instead of WPT due to bug https://crbug.com/793808).

Design doc:
https://docs.google.com/document/d/1bpsYLC35cHAJnlnmiBJ3bhUQXfUBgKbTv7A6nkEs6OA/edit?usp=sharing

[1] https://w3c.github.io/webrtc-pc/#dom-rtcrtpsender-replacetrack
[2] https://chromium-review.googlesource.com/c/chromium/src/+/833870/

Bug: 790007
Change-Id: If8c3499c78fe664a496c5d90cfbfa7b48c1c036f
Reviewed-on: https://chromium-review.googlesource.com/810765
Reviewed-by: Philip Jägenstedt <foolip@chromium.org>
Reviewed-by: Harald Alvestrand <hta@chromium.org>
Reviewed-by: Taylor Brandstetter <deadbeef@chromium.org>
Commit-Queue: Henrik Boström <hbos@chromium.org>
Cr-Commit-Position: refs/heads/master@{#528071}
diff --git a/chrome/browser/media/webrtc/webrtc_rtp_browsertest.cc b/chrome/browser/media/webrtc/webrtc_rtp_browsertest.cc
index 41dec54..4ccfad8 100644
--- a/chrome/browser/media/webrtc/webrtc_rtp_browsertest.cc
+++ b/chrome/browser/media/webrtc/webrtc_rtp_browsertest.cc
@@ -22,15 +22,24 @@
 
   void SetUpCommandLine(base::CommandLine* command_line) override {
     command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream);
+    command_line->AppendSwitchASCII(switches::kEnableBlinkFeatures,
+                                    "RTCRtpSenderReplaceTrack");
     // Required by |CollectGarbage|.
     command_line->AppendSwitchASCII(switches::kJavaScriptFlags, "--expose-gc");
   }
 
  protected:
+  void StartServer() { ASSERT_TRUE(embedded_test_server()->Start()); }
+
+  void OpenTab(content::WebContents** tab) {
+    // TODO(hbos): Just open the tab, don't "AndGetUserMediaInNewTab".
+    *tab = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage);
+  }
+
   void StartServerAndOpenTabs() {
-    ASSERT_TRUE(embedded_test_server()->Start());
-    left_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage);
-    right_tab_ = OpenTestPageAndGetUserMediaInNewTab(kMainWebrtcTestHtmlPage);
+    StartServer();
+    OpenTab(&left_tab_);
+    OpenTab(&right_tab_);
   }
 
   const TrackEvent* FindTrackEvent(const std::vector<TrackEvent>& track_events,
@@ -397,3 +406,12 @@
   StartServerAndOpenTabs();
   EXPECT_EQ("ok", ExecuteJavascript("trackAddedToSecondStream()", left_tab_));
 }
+
+IN_PROC_BROWSER_TEST_F(WebRtcRtpBrowserTest,
+                       RTCRtpSenderReplaceTrackSendsNewVideoTrack) {
+  StartServer();
+  OpenTab(&left_tab_);
+  EXPECT_EQ("test-passed",
+            ExecuteJavascript(
+                "testRTCRtpSenderReplaceTrackSendsNewVideoTrack()", left_tab_));
+}
diff --git a/chrome/test/data/webrtc/peerconnection_replacetrack.js b/chrome/test/data/webrtc/peerconnection_replacetrack.js
new file mode 100644
index 0000000..ad45521
--- /dev/null
+++ b/chrome/test/data/webrtc/peerconnection_replacetrack.js
@@ -0,0 +1,173 @@
+/**
+ * Copyright 2017 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+/**
+ * The resolver has a |promise| that can be resolved or rejected using |resolve|
+ * or |reject|.
+ */
+// TODO(hbos): Remove when no longer needed. https://crbug.com/793808
+class Resolver {
+  constructor() {
+    let promiseResolve;
+    let promiseReject;
+    this.promise = new Promise(function(resolve, reject) {
+      promiseResolve = resolve;
+      promiseReject = reject;
+    });
+    this.resolve = promiseResolve;
+    this.reject = promiseReject;
+  }
+}
+
+// TODO(hbos): Make this an external/wpt/webrtc/ test when video elements are
+// updated by received webrtc streams in content_shell. https://crbug.com/793808
+async function testRTCRtpSenderReplaceTrackSendsNewVideoTrack() {
+  const redCanvas = document.getElementById('redCanvas');
+  const redCanvasStream = redCanvas.captureStream(10);
+  const blueCanvas = document.getElementById('blueCanvas');
+  const blueCanvasStream = blueCanvas.captureStream(10);
+  const remoteVideo = document.getElementById('remote-view');
+  const remoteVideoCanvas = document.getElementById('whiteCanvas');
+
+  const caller = new RTCPeerConnection();
+  const callee = new RTCPeerConnection();
+
+  // Connect and send "redCanvas" to callee.
+  const sender = caller.addTrack(redCanvasStream.getTracks()[0],
+                                 redCanvasStream);
+  const connectPromise = connect(caller, callee);
+  const trackEvent = await eventAsAsyncFunction(callee, 'ontrack');
+  remoteVideo.srcObject = trackEvent.streams[0];
+  await connectPromise;
+
+  // Ensure a red frame is sent by redrawing the canvas while polling.
+  function fillRedCanvas() { fillCanvas(redCanvas, 'red'); }
+  let receivedColor = await pollNextVideoColor(
+      fillRedCanvas, remoteVideo, remoteVideoCanvas);
+  if (receivedColor != 'red')
+    throw failTest('Expected red, but received: ' + receivedColor);
+
+  // Send "blueCanvas" to callee using the existing sender.
+  await sender.replaceTrack(blueCanvasStream.getTracks()[0]);
+
+  // Ensure a blue frame is sent by redrawing the canvas while polling.
+  function fillBlueCanvas() { fillCanvas(blueCanvas, 'blue'); }
+  receivedColor = await pollNextVideoColor(
+      fillBlueCanvas, remoteVideo, remoteVideoCanvas);
+  if (receivedColor != 'blue')
+    throw failTest('Expected blue, but received: ' + receivedColor);
+
+  returnToTest('test-passed');
+}
+
+// Internals.
+
+/** @private */
+async function connect(caller, callee) {
+  caller.onicecandidate = (e) => {
+    if (e.candidate)
+      callee.addIceCandidate(new RTCIceCandidate(e.candidate));
+  }
+  callee.onicecandidate = (e) => {
+    if (e.candidate)
+      caller.addIceCandidate(new RTCIceCandidate(e.candidate));
+  }
+  let offer = await caller.createOffer();
+  await caller.setLocalDescription(offer);
+  await callee.setRemoteDescription(offer);
+  let answer = await callee.createAnswer();
+  await callee.setLocalDescription(answer);
+  return caller.setRemoteDescription(answer);
+}
+
+/**
+ * Makes the next |object[eventname]| event resolve the returned promise with
+ * the event argument and resets the event handler to null.
+ * @private
+ */
+async function eventAsAsyncFunction(object, eventname) {
+  const resolver = new Resolver();
+  object[eventname] = e => {
+    object[eventname] = null;
+    resolver.resolve(e);
+  }
+  return resolver.promise;
+}
+
+/**
+ * Updates the canvas, filling it with |color|, e.g. 'red', 'lime' or 'blue'.
+ * @private
+ */
+function fillCanvas(canvas, color) {
+  const canvasContext = canvas.getContext('2d');
+  canvasContext.fillStyle = color;
+  canvasContext.fillRect(0, 0, canvas.width, canvas.height);
+}
+
+/**
+ * Gets the dominant color of the center of the canvas, meaning the color that
+ * is closest to that pixel's color amongst: 'black', 'white', 'red', 'lime' and
+ * 'blue'.
+ * @private
+ */
+function getDominantCanvasColor(canvas) {
+  const colorData = canvas.getContext('2d').getImageData(
+      Math.floor(canvas.width / 2), Math.floor(canvas.height / 2), 1, 1).data;
+
+  const dominantColors = [
+    { name: 'black', colorData: [0, 0, 0] },
+    { name: 'white', colorData: [255, 255, 255] },
+    { name: 'red', colorData: [255, 0, 0] },
+    { name: 'lime', colorData: [0, 255, 0] },
+    { name: 'blue', colorData: [0, 0, 255] },
+  ];
+  function getColorDistanceSquared(colorData1, colorData2) {
+    const colorDiff = [ colorData2[0] - colorData1[0],
+                        colorData2[1] - colorData1[1],
+                        colorData2[2] - colorData1[2] ];
+    return colorDiff[0] * colorDiff[0] +
+           colorDiff[1] * colorDiff[1] +
+           colorDiff[2] * colorDiff[2];
+  }
+  let dominantColor = dominantColors[0];
+  let dominantColorDistanceSquared =
+      getColorDistanceSquared(dominantColor.colorData, colorData);
+  for (let i = 1; i < dominantColors.length; ++i) {
+    const colorDistanceSquared =
+        getColorDistanceSquared(dominantColors[i].colorData, colorData);
+    if (colorDistanceSquared < dominantColorDistanceSquared) {
+      dominantColor = dominantColors[i];
+      dominantColorDistanceSquared = colorDistanceSquared;
+    }
+  }
+  return dominantColor.name;
+}
+
+/**
+ * Polls the video's dominant color (see getDominantCanvasColor()) until a color
+ * different than the initial color is retrieved, resolving the returned promise
+ * with the new color name. The video color is read by drawing the video onto a
+ * canvas and reading the color of the canvas. Before each time the color is
+ * polled, doWhilePolling() is invoked.
+ * @private
+ */
+async function pollNextVideoColor(doWhilePolling, video, canvas) {
+  canvas.getContext('2d').drawImage(video, 0, 0);
+  const initialColor = getDominantCanvasColor(canvas);
+  const resolver = new Resolver();
+  function checkColor() {
+    doWhilePolling();
+    canvas.getContext('2d').drawImage(video, 0, 0);
+    const color = getDominantCanvasColor(canvas);
+    if (color != initialColor) {
+      resolver.resolve(color);
+      return;
+    }
+    setTimeout(checkColor, 0);
+  }
+  setTimeout(checkColor, 0);
+  return resolver.promise;
+}
diff --git a/chrome/test/data/webrtc/webrtc_jsep01_test.html b/chrome/test/data/webrtc/webrtc_jsep01_test.html
index 00e69e1..4b087180 100644
--- a/chrome/test/data/webrtc/webrtc_jsep01_test.html
+++ b/chrome/test/data/webrtc/webrtc_jsep01_test.html
@@ -12,12 +12,16 @@
   <script type="text/javascript" src="indexeddb.js"></script>
   <script type="text/javascript" src="peerconnection_getstats.js"></script>
   <script type="text/javascript" src="peerconnection_rtp.js"></script>
+  <script type="text/javascript" src="peerconnection_replacetrack.js"></script>
 </head>
 <body>
   <table border="0">
     <tr>
       <td><video id="local-view" autoplay style="display:none"></video></td>
       <td><video id="remote-view" autoplay style="display:none"></video></td>
+      <td><canvas id="redCanvas" width="10" height="10" style="background-color:#FF0000"/></td>
+      <td><canvas id="blueCanvas" width="10" height="10" style="background-color:#0000FF"/></td>
+      <td><canvas id="whiteCanvas" width="10" height="10" style="background-color:#FFFFFF"/></td>
     </tr>
   </table>
 </body>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https-expected.txt b/third_party/WebKit/LayoutTests/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https-expected.txt
new file mode 100644
index 0000000..76822af
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https-expected.txt
@@ -0,0 +1,9 @@
+This is a testharness.js-based test.
+PASS replaceTrack() sets the track attribute to a new track.
+PASS replaceTrack() sets the track attribute to null.
+PASS replaceTrack() does not set the track synchronously.
+PASS replaceTrack() rejects when the peer connection is closed.
+PASS replaceTrack() rejects when invoked after removeTrack().
+FAIL replaceTrack() rejects after a subsequent removeTrack(). assert_unreached: Expected replaceTrack() to be rejected with InvalidModificationError but the promise was resolved. Reached unreachable code
+Harness: the test ran to completion.
+
diff --git a/third_party/WebKit/LayoutTests/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html b/third_party/WebKit/LayoutTests/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html
new file mode 100644
index 0000000..8a40f9a4
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/external/wpt/webrtc/RTCPeerConnection-setRemoteDescription-replaceTrack.https.html
@@ -0,0 +1,139 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>RTCPeerConnection.prototype.setRemoteDescription - replaceTrack</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="RTCPeerConnection-helper.js"></script>
+<script>
+  'use strict';
+
+  // The following helper functions are called from RTCPeerConnection-helper.js:
+  //   getUserMediaTracksAndStreams
+
+  async_test(t => {
+    const caller = new RTCPeerConnection();
+    return getUserMediaTracksAndStreams(2)
+    .then(t.step_func(([tracks, streams]) => {
+      const sender = caller.addTrack(tracks[0]);
+      return sender.replaceTrack(tracks[1])
+      .then(t.step_func(() => {
+        assert_equals(sender.track, tracks[1]);
+        t.done();
+      }));
+    }))
+    .catch(t.step_func(reason => {
+      assert_unreached(reason);
+    }));
+  }, 'replaceTrack() sets the track attribute to a new track.');
+
+  async_test(t => {
+    const caller = new RTCPeerConnection();
+    return getUserMediaTracksAndStreams(1)
+    .then(t.step_func(([tracks, streams]) => {
+      const sender = caller.addTrack(tracks[0]);
+      return sender.replaceTrack(null)
+      .then(t.step_func(() => {
+        assert_equals(sender.track, null);
+        t.done();
+      }));
+    }))
+    .catch(t.step_func(reason => {
+      assert_unreached(reason);
+    }));
+  }, 'replaceTrack() sets the track attribute to null.');
+
+  async_test(t => {
+    const caller = new RTCPeerConnection();
+    return getUserMediaTracksAndStreams(2)
+    .then(t.step_func(([tracks, streams]) => {
+      const sender = caller.addTrack(tracks[0]);
+      assert_equals(sender.track, tracks[0]);
+      sender.replaceTrack(tracks[1]);
+      // replaceTrack() is asynchronous, there should be no synchronously
+      // observable effects.
+      assert_equals(sender.track, tracks[0]);
+      t.done();
+    }))
+    .catch(t.step_func(reason => {
+      assert_unreached(reason);
+    }));
+  }, 'replaceTrack() does not set the track synchronously.');
+
+  async_test(t => {
+    const expectedException = 'InvalidStateError';
+    const caller = new RTCPeerConnection();
+    return getUserMediaTracksAndStreams(2)
+    .then(t.step_func(([tracks, streams]) => {
+      const sender = caller.addTrack(tracks[0]);
+      caller.close();
+      return sender.replaceTrack(tracks[1])
+      .then(t.step_func(() => {
+        assert_unreached('Expected replaceTrack() to be rejected with ' +
+                         expectedException + ' but the promise was resolved.');
+      }),
+      t.step_func(e => {
+        assert_equals(e.name, expectedException);
+        t.done();
+      }));
+    }))
+    .catch(t.step_func(reason => {
+      assert_unreached(reason);
+    }));
+  }, 'replaceTrack() rejects when the peer connection is closed.');
+
+  async_test(t => {
+    const expectedException = 'InvalidModificationError';
+    const caller = new RTCPeerConnection();
+    return getUserMediaTracksAndStreams(2)
+    .then(t.step_func(([tracks, streams]) => {
+      const sender = caller.addTrack(tracks[0]);
+      caller.removeTrack(sender);
+      // replaceTrack() should fail because the sender should be inactive after
+      // removeTrack().
+      return sender.replaceTrack(tracks[1])
+      .then(t.step_func(() => {
+        assert_unreached('Expected replaceTrack() to be rejected with ' +
+                         expectedException + ' but the promise was resolved.');
+      }),
+      t.step_func(e => {
+        assert_equals(e.name, expectedException);
+        t.done();
+      }));
+    }))
+    .catch(t.step_func(reason => {
+      assert_unreached(reason);
+    }));
+  }, 'replaceTrack() rejects when invoked after removeTrack().');
+
+  async_test(t => {
+    const expectedException = 'InvalidModificationError';
+    const caller = new RTCPeerConnection();
+    return getUserMediaTracksAndStreams(2)
+    .then(t.step_func(([tracks, streams]) => {
+      const sender = caller.addTrack(tracks[0]);
+      let p = sender.replaceTrack(tracks[1])
+      caller.removeTrack(sender);
+      // replaceTrack() should fail because it executes steps in parallel and
+      // queues a task to execute after removeTrack() has occurred. The sender
+      // should be inactive. If this can be racy, update or remove the test.
+      // https://github.com/w3c/webrtc-pc/issues/1728
+      return p.then(t.step_func(() => {
+        assert_unreached('Expected replaceTrack() to be rejected with ' +
+                         expectedException + ' but the promise was resolved.');
+      }),
+      t.step_func(e => {
+        assert_equals(e.name, expectedException);
+        t.done();
+      }));
+    }))
+    .catch(t.step_func(reason => {
+      assert_unreached(reason);
+    }));
+  }, 'replaceTrack() rejects after a subsequent removeTrack().');
+
+  // TODO(hbos): Verify that replaceTrack() changes what media is received on
+  // the remote end of two connected peer connections. For video tracks, this
+  // requires Chromium's video tag to update on receiving frames when running
+  // content_shell. https://crbug.com/793808
+
+</script>
diff --git a/third_party/WebKit/LayoutTests/external/wpt/webrtc/interfaces.https-expected.txt b/third_party/WebKit/LayoutTests/external/wpt/webrtc/interfaces.https-expected.txt
index ed35f4b..abc09ac7 100644
--- a/third_party/WebKit/LayoutTests/external/wpt/webrtc/interfaces.https-expected.txt
+++ b/third_party/WebKit/LayoutTests/external/wpt/webrtc/interfaces.https-expected.txt
@@ -245,7 +245,7 @@
 FAIL RTCRtpSender interface: operation getCapabilities(DOMString) assert_own_property: interface object missing static operation expected property "getCapabilities" missing
 FAIL RTCRtpSender interface: operation setParameters(RTCRtpParameters) assert_own_property: interface prototype object missing non-static operation expected property "setParameters" missing
 FAIL RTCRtpSender interface: operation getParameters() assert_own_property: interface prototype object missing non-static operation expected property "getParameters" missing
-FAIL RTCRtpSender interface: operation replaceTrack(MediaStreamTrack) assert_own_property: interface prototype object missing non-static operation expected property "replaceTrack" missing
+PASS RTCRtpSender interface: operation replaceTrack(MediaStreamTrack)
 FAIL RTCRtpSender interface: operation getStats() assert_own_property: interface prototype object missing non-static operation expected property "getStats" missing
 FAIL RTCRtpSender interface: attribute dtmf assert_true: The prototype object must have a property "dtmf" expected true got false
 FAIL RTCRtpSender must be primary interface of new RTCPeerConnection().addTransceiver('audio').sender assert_equals: Unexpected exception when evaluating object expected null but got object "TypeError: (intermediate value).addTransceiver is not a function"
diff --git a/third_party/WebKit/LayoutTests/webexposed/global-interface-listing-expected.txt b/third_party/WebKit/LayoutTests/webexposed/global-interface-listing-expected.txt
index 21d7ed6..773df45db 100644
--- a/third_party/WebKit/LayoutTests/webexposed/global-interface-listing-expected.txt
+++ b/third_party/WebKit/LayoutTests/webexposed/global-interface-listing-expected.txt
@@ -5294,6 +5294,7 @@
     attribute @@toStringTag
     getter track
     method constructor
+    method replaceTrack
 interface RTCSessionDescription
     attribute @@toStringTag
     getter sdp
diff --git a/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.cpp b/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.cpp
index 3788369a1..7404bcc0 100644
--- a/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.cpp
+++ b/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.cpp
@@ -1274,7 +1274,7 @@
         DCHECK(track);
       }
       RTCRtpSender* rtp_sender =
-          new RTCRtpSender(std::move(web_rtp_senders[i]), track);
+          new RTCRtpSender(this, std::move(web_rtp_senders[i]), track);
       rtp_senders_.insert(id, rtp_sender);
       rtp_senders[i] = rtp_sender;
     }
@@ -1324,7 +1324,8 @@
 
   uintptr_t id = web_rtp_sender->Id();
   DCHECK(rtp_senders_.find(id) == rtp_senders_.end());
-  RTCRtpSender* rtp_sender = new RTCRtpSender(std::move(web_rtp_sender), track);
+  RTCRtpSender* rtp_sender =
+      new RTCRtpSender(this, std::move(web_rtp_sender), track);
   tracks_.insert(track->Component(), track);
   rtp_senders_.insert(id, rtp_sender);
   return rtp_sender;
diff --git a/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.h b/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.h
index 92412b0..969abbf 100644
--- a/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.h
+++ b/third_party/WebKit/Source/modules/peerconnection/RTCPeerConnection.h
@@ -172,6 +172,7 @@
 
   RTCDTMFSender* createDTMFSender(MediaStreamTrack*, ExceptionState&);
 
+  bool IsClosed() { return closed_; }
   void close();
 
   // We allow getStats after close, but not other calls or callbacks.
diff --git a/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.cpp b/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.cpp
index 98102b12..d9c458e 100644
--- a/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.cpp
+++ b/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.cpp
@@ -4,43 +4,92 @@
 
 #include "modules/peerconnection/RTCRtpSender.h"
 
+#include "bindings/core/v8/ScriptPromiseResolver.h"
+#include "core/dom/DOMException.h"
 #include "modules/mediastream/MediaStreamTrack.h"
+#include "modules/peerconnection/RTCPeerConnection.h"
+#include "platform/peerconnection/RTCVoidRequest.h"
 
 namespace blink {
 
 namespace {
 
-bool TrackEquals(MediaStreamTrack* track,
-                 const WebMediaStreamTrack& web_track) {
-  if (track)
-    return track->Component() == static_cast<MediaStreamComponent*>(web_track);
-  return web_track.IsNull();
-}
+class ReplaceTrackRequest : public RTCVoidRequest {
+ public:
+  ReplaceTrackRequest(RTCRtpSender* sender,
+                      MediaStreamTrack* with_track,
+                      ScriptPromiseResolver* resolver)
+      : sender_(sender), with_track_(with_track), resolver_(resolver) {}
+  ~ReplaceTrackRequest() override {}
+
+  void RequestSucceeded() override {
+    sender_->SetTrack(with_track_);
+    resolver_->Resolve();
+  }
+
+  // TODO(hbos): Surface RTCError instead of hard-coding which exception type to
+  // use. This requires the webrtc layer sender interface to be updated.
+  // https://crbug.com/webrtc/8690
+  void RequestFailed(const String& error) override {
+    resolver_->Reject(DOMException::Create(kInvalidModificationError, error));
+  }
+
+  void Trace(blink::Visitor* visitor) override {
+    visitor->Trace(sender_);
+    visitor->Trace(with_track_);
+    visitor->Trace(resolver_);
+    RTCVoidRequest::Trace(visitor);
+  }
+
+ private:
+  Member<RTCRtpSender> sender_;
+  Member<MediaStreamTrack> with_track_;
+  Member<ScriptPromiseResolver> resolver_;
+};
 
 }  // namespace
 
-RTCRtpSender::RTCRtpSender(std::unique_ptr<WebRTCRtpSender> sender,
+RTCRtpSender::RTCRtpSender(RTCPeerConnection* pc,
+                           std::unique_ptr<WebRTCRtpSender> sender,
                            MediaStreamTrack* track)
-    : sender_(std::move(sender)), track_(track) {
+    : pc_(pc), sender_(std::move(sender)), track_(track) {
+  DCHECK(pc_);
   DCHECK(sender_);
   DCHECK(track_);
 }
 
 MediaStreamTrack* RTCRtpSender::track() {
-  DCHECK(TrackEquals(track_, sender_->Track()));
   return track_;
 }
 
+ScriptPromise RTCRtpSender::replaceTrack(ScriptState* script_state,
+                                         MediaStreamTrack* with_track) {
+  ScriptPromiseResolver* resolver = ScriptPromiseResolver::Create(script_state);
+  ScriptPromise promise = resolver->Promise();
+  if (pc_->IsClosed()) {
+    resolver->Reject(DOMException::Create(kInvalidStateError,
+                                          "The peer connection is closed."));
+    return promise;
+  }
+  WebMediaStreamTrack web_track;
+  if (with_track)
+    web_track = with_track->Component();
+  ReplaceTrackRequest* request =
+      new ReplaceTrackRequest(this, with_track, resolver);
+  sender_->ReplaceTrack(web_track, request);
+  return promise;
+}
+
 WebRTCRtpSender* RTCRtpSender::web_sender() {
   return sender_.get();
 }
 
 void RTCRtpSender::SetTrack(MediaStreamTrack* track) {
-  DCHECK(TrackEquals(track, sender_->Track()));
   track_ = track;
 }
 
 void RTCRtpSender::Trace(blink::Visitor* visitor) {
+  visitor->Trace(pc_);
   visitor->Trace(track_);
   ScriptWrappable::Trace(visitor);
 }
diff --git a/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.h b/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.h
index 4823065..9885541 100644
--- a/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.h
+++ b/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.h
@@ -5,6 +5,7 @@
 #ifndef RTCRtpSender_h
 #define RTCRtpSender_h
 
+#include "bindings/core/v8/ScriptPromise.h"
 #include "platform/bindings/ScriptWrappable.h"
 #include "platform/heap/GarbageCollected.h"
 #include "platform/heap/Member.h"
@@ -15,15 +16,21 @@
 namespace blink {
 
 class MediaStreamTrack;
+class RTCPeerConnection;
 
 // https://w3c.github.io/webrtc-pc/#rtcrtpsender-interface
 class RTCRtpSender final : public ScriptWrappable {
   DEFINE_WRAPPERTYPEINFO();
 
  public:
-  RTCRtpSender(std::unique_ptr<WebRTCRtpSender>, MediaStreamTrack*);
+  // TODO(hbos): Get rid of sender's reference to RTCPeerConnection?
+  // https://github.com/w3c/webrtc-pc/issues/1712
+  RTCRtpSender(RTCPeerConnection*,
+               std::unique_ptr<WebRTCRtpSender>,
+               MediaStreamTrack*);
 
   MediaStreamTrack* track();
+  ScriptPromise replaceTrack(ScriptState*, MediaStreamTrack*);
 
   WebRTCRtpSender* web_sender();
   // Sets the track. This must be called when the |WebRTCRtpSender| has its
@@ -33,6 +40,7 @@
   virtual void Trace(blink::Visitor*);
 
  private:
+  Member<RTCPeerConnection> pc_;
   std::unique_ptr<WebRTCRtpSender> sender_;
   Member<MediaStreamTrack> track_;
 };
diff --git a/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.idl b/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.idl
index ddbd638..8f74522 100644
--- a/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.idl
+++ b/third_party/WebKit/Source/modules/peerconnection/RTCRtpSender.idl
@@ -5,5 +5,6 @@
 // https://w3c.github.io/webrtc-pc/#rtcrtpsender-interface
 interface RTCRtpSender {
     readonly attribute MediaStreamTrack? track;
+    [RuntimeEnabled=RTCRtpSenderReplaceTrack, Measure, CallWith=ScriptState] Promise<void> replaceTrack(MediaStreamTrack? withTrack);
     // TODO(hbos): Implement the rest of RTCRtpSender, https://crbug.com/700916.
 };
diff --git a/third_party/WebKit/Source/platform/runtime_enabled_features.json5 b/third_party/WebKit/Source/platform/runtime_enabled_features.json5
index 67b6bec0..e4e7fdd 100644
--- a/third_party/WebKit/Source/platform/runtime_enabled_features.json5
+++ b/third_party/WebKit/Source/platform/runtime_enabled_features.json5
@@ -902,6 +902,10 @@
       name: "RootLayerScrolling",
     },
     {
+      name: "RTCRtpSenderReplaceTrack",
+      status: "experimental",
+    },
+    {
       name: "RTCUnifiedPlan",
       status: "experimental",
     },
diff --git a/third_party/WebKit/public/platform/web_feature.mojom b/third_party/WebKit/public/platform/web_feature.mojom
index acbde07..e8dce48 100644
--- a/third_party/WebKit/public/platform/web_feature.mojom
+++ b/third_party/WebKit/public/platform/web_feature.mojom
@@ -1829,6 +1829,7 @@
   kFilterAsContainingBlockMayChangeOutput = 2320,
   kDispatchMouseUpDownEventOnDisabledFormControl = 2321,
   kCSSSelectorPseudoMatches = 2322,
+  kV8RTCRtpSender_ReplaceTrack_Method = 2323,
 
   // Add new features immediately above this line. Don't change assigned
   // numbers of any item, and don't reuse removed slots.
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index cf613d7..7f930c0 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -17371,6 +17371,7 @@
   <int value="2320" label="FilterAsContainingBlockMayChangeOutput"/>
   <int value="2321" label="DispatchMouseUpDownEventOnDisabledFormControl"/>
   <int value="2322" label="CSSSelectorPseudoMatches"/>
+  <int value="2323" label="V8RTCRtpSender_ReplaceTrack_Method"/>
 </enum>
 
 <enum name="FeedbackSource">