MediaRecorder: update mimeType on onstart.

This change brings the MediaRecorder onstart behavior to spec
compliance, as per https://github.com/w3c/mediacapture-record/pull/190.

The change carries updates to web tests related to the following:
- PeerConnection MediaRecorder tests have been added to cover the
both spec wording related to avoiding transcoding and the normal
transcoding use cases.
- A test that shows that audio recording cannot happen without a sink
tag has been added.
- With this change, Chrome passes all the mime type tests.

Bug: 1013590
Change-Id: I0001673fce20e995a3fe67336a7c9e87d70adc0b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1893855
Reviewed-by: Guido Urdaneta <guidou@chromium.org>
Reviewed-by: Henrik Boström <hbos@chromium.org>
Commit-Queue: Markus Handell <handellm@google.com>
Cr-Commit-Position: refs/heads/master@{#732487}
diff --git a/third_party/blink/renderer/modules/mediarecorder/media_recorder.cc b/third_party/blink/renderer/modules/mediarecorder/media_recorder.cc
index 1996adb..96c70129 100644
--- a/third_party/blink/renderer/modules/mediarecorder/media_recorder.cc
+++ b/third_party/blink/renderer/modules/mediarecorder/media_recorder.cc
@@ -193,12 +193,6 @@
             mime_type_ + ") is not supported.");
     return;
   }
-  // If the user requested no mimeType, query |recorder_handler_|.
-  if (options->mimeType().IsEmpty()) {
-    const String actual_mime_type = recorder_handler_->ActualMimeType();
-    if (!actual_mime_type.IsEmpty())
-      mime_type_ = actual_mime_type;
-  }
   stopped_ = false;
 }
 
@@ -241,7 +235,6 @@
         "There was an error starting the MediaRecorder.");
     return;
   }
-  ScheduleDispatchEvent(Event::Create(event_type_names::kStart));
 }
 
 void MediaRecorder::stop(ExceptionState& exception_state) {
@@ -361,9 +354,20 @@
                               size_t length,
                               bool last_in_slice,
                               double timecode) {
+  // Update mime_type_ when "onstart" is sent by the MediaRecorder. This method
+  // is used also from StopRecording, with a zero length. If we never wrote
+  // anything we don't want to send start or associated actions (update the mime
+  // type in that case).
+  if (!first_write_received_ && length) {
+    mime_type_ = recorder_handler_->ActualMimeType();
+  }
   if (stopped_ && !last_in_slice) {
     stopped_ = false;
     ScheduleDispatchEvent(Event::Create(event_type_names::kStart));
+    first_write_received_ = true;
+  } else if (!first_write_received_ && length) {
+    ScheduleDispatchEvent(Event::Create(event_type_names::kStart));
+    first_write_received_ = true;
   }
 
   if (!blob_data_) {
@@ -410,6 +414,8 @@
     // have ended leading to a call to OnAllTracksEnded.
     return;
   }
+  // Make sure that starting the recorder again yields an onstart event.
+  first_write_received_ = false;
   state_ = State::kInactive;
 
   recorder_handler_->Stop();
diff --git a/third_party/blink/renderer/modules/mediarecorder/media_recorder.h b/third_party/blink/renderer/modules/mediarecorder/media_recorder.h
index 84b583f..5ebbfde 100644
--- a/third_party/blink/renderer/modules/mediarecorder/media_recorder.h
+++ b/third_party/blink/renderer/modules/mediarecorder/media_recorder.h
@@ -103,11 +103,9 @@
   int video_bits_per_second_;
 
   State state_;
-
+  bool first_write_received_ = false;
   std::unique_ptr<BlobData> blob_data_;
-
   Member<MediaRecorderHandler> recorder_handler_;
-
   HeapVector<Member<Event>> scheduled_events_;
 };
 
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 1cc6c737..2cebd57 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -3300,12 +3300,12 @@
 crbug.com/626703 [ Win ] external/wpt/css/css-text/word-break/word-break-keep-all-005.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-text/white-space/trailing-ideographic-space-001.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-text/white-space/trailing-ideographic-space-003.html [ Failure ]
-crbug.com/626703 external/wpt/mediacapture-record/MediaRecorder-stop.html [ Timeout ]
 crbug.com/626703 [ Win ] external/wpt/css/css-text/word-break/word-break-keep-all-006.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-text/letter-spacing/letter-spacing-nesting-001.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-text/letter-spacing/letter-spacing-bidi-001.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-text/letter-spacing/letter-spacing-end-of-line-001.html [ Failure ]
 crbug.com/626703 external/wpt/mediacapture-record/MediaRecorder-destroy-script-execution.html [ Timeout ]
+crbug.com/626703 external/wpt/mediacapture-record/MediaRecorder-no-sink.https.html [ Timeout ]
 crbug.com/626703 external/wpt/css/css-text/white-space/trailing-ideographic-space-002.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-text/white-space/trailing-ideographic-space-004.html [ Failure ]
 crbug.com/626703 external/wpt/css/css-fonts/variations/font-descriptor-range-reversed.html [ Failure ]
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-disabled-tracks.https.html b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-disabled-tracks.https.html
index 52e0010..b98aed2 100644
--- a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-disabled-tracks.https.html
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-disabled-tracks.https.html
@@ -30,7 +30,7 @@
       const recorderOnError = test.unreached_func('Recording error.');
 
       const gotStream = test.step_func(function(stream) {
-        for (track in stream.getTracks())
+        for (track of stream.getTracks())
           track.enabled = false;
 
         recorder = new MediaRecorder(stream);
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype-expected.txt b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype-expected.txt
deleted file mode 100644
index 264518f..0000000
--- a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype-expected.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-This is a testharness.js-based test.
-FAIL MediaRecorder sets no default MIMEType in the constructor for audio assert_equals: MediaRecorder has no default MIMEtype expected "" but got "audio/webm;codecs=opus"
-FAIL MediaRecorder sets no default MIMEType in the constructor for video assert_equals: MediaRecorder has no default MIMEtype expected "" but got "video/webm;codecs=vp8"
-FAIL MediaRecorder sets no default MIMEType in the constructor for audio/video assert_equals: MediaRecorder has no default MIMEtype expected "" but got "video/webm;codecs=vp8,opus"
-PASS MediaRecorder invalid audio MIMEType throws
-PASS MediaRecorder invalid audio MIMEType is unsupported
-PASS MediaRecorder invalid video MIMEType throws
-PASS MediaRecorder invalid video MIMEType is unsupported
-PASS Unsupported MIMEType audio/mp4 throws
-PASS Unsupported MIMEType video/mp4 throws
-PASS Unsupported MIMEType audio/ogg throws
-PASS Unsupported MIMEType audio/ogg; codecs="vorbis" throws
-PASS Unsupported MIMEType audio/ogg; codecs="opus" throws
-PASS Supported MIMEType audio/webm is set immediately after constructing
-PASS Unsupported MIMEType audio/webm; codecs="vorbis" throws
-PASS Supported MIMEType audio/webm; codecs="opus" is set immediately after constructing
-PASS Supported MIMEType video/webm is set immediately after constructing
-PASS Supported MIMEType video/webm; codecs="vp8" is set immediately after constructing
-PASS Unsupported MIMEType video/webm; codecs="vp8, vorbis" throws
-PASS Supported MIMEType video/webm; codecs="vp8, opus" is set immediately after constructing
-PASS Supported MIMEType video/webm; codecs="vp9" is set immediately after constructing
-PASS Unsupported MIMEType video/webm; codecs="vp9, vorbis" throws
-PASS Supported MIMEType video/webm; codecs="vp9, opus" is set immediately after constructing
-PASS Unsupported MIMEType video/webm; codecs="av1" throws
-PASS Unsupported MIMEType video/webm; codecs="av1, opus" throws
-FAIL MediaRecorder sets a MIMEType after 'start' for audio assert_equals: MediaRecorder has no MIMEtype after start() for audio expected "" but got "audio/webm;codecs=opus"
-FAIL MediaRecorder sets a MIMEType after 'start' for video assert_equals: MediaRecorder has no MIMEtype after start() for video expected "" but got "video/webm;codecs=vp8"
-FAIL MediaRecorder sets a MIMEType after 'start' for audio/video assert_equals: MediaRecorder has no MIMEtype after start() for audio/video expected "" but got "video/webm;codecs=vp8,opus"
-Harness: the test ran to completion.
-
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype.html b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype.html
index e90bbcc..06841aea 100644
--- a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype.html
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-mimetype.html
@@ -1,8 +1,8 @@
 <!doctype html>
 <html>
 <head>
-  <title>MediaRecorder MIMEType</title>
-  <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimetype">
+  <title>MediaRecorder mimeType</title>
+  <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
   <script src="/resources/testharness.js"></script>
   <script src="/resources/testharnessreport.js"></script>
 </head>
@@ -47,39 +47,39 @@
 test(t => {
   const recorder = new MediaRecorder(createAudioStream(t));
   assert_equals(recorder.mimeType, "",
-    "MediaRecorder has no default MIMEtype");
-}, "MediaRecorder sets no default MIMEType in the constructor for audio");
+    "MediaRecorder has no default mimeType");
+}, "MediaRecorder sets no default mimeType in the constructor for audio");
 
 test(t => {
   const recorder = new MediaRecorder(createVideoStream(t));
   assert_equals(recorder.mimeType, "",
-    "MediaRecorder has no default MIMEtype");
-}, "MediaRecorder sets no default MIMEType in the constructor for video");
+    "MediaRecorder has no default mimeType");
+}, "MediaRecorder sets no default mimeType in the constructor for video");
 
 test(t => {
   const stream = createAudioVideoStream(t);
   const recorder = new MediaRecorder(stream);
   assert_equals(recorder.mimeType, "",
-    "MediaRecorder has no default MIMEtype");
-}, "MediaRecorder sets no default MIMEType in the constructor for audio/video");
+    "MediaRecorder has no default mimeType");
+}, "MediaRecorder sets no default mimeType in the constructor for audio/video");
 
 test(t => {
   assert_throws("NotSupportedError",
     () => new MediaRecorder(new MediaStream(), {mimeType: "audio/banana"}));
-}, "MediaRecorder invalid audio MIMEType throws");
+}, "MediaRecorder invalid audio mimeType throws");
 
 test(t => {
   assert_false(MediaRecorder.isTypeSupported("audio/banana"));
-}, "MediaRecorder invalid audio MIMEType is unsupported");
+}, "MediaRecorder invalid audio mimeType is unsupported");
 
 test(t => {
   assert_throws("NotSupportedError",
     () => new MediaRecorder(new MediaStream(), {mimeType: "video/pineapple"}));
-}, "MediaRecorder invalid video MIMEType throws");
+}, "MediaRecorder invalid video mimeType throws");
 
 test(t => {
   assert_false(MediaRecorder.isTypeSupported("video/pineapple"));
-}, "MediaRecorder invalid video MIMEType is unsupported");
+}, "MediaRecorder invalid video mimeType is unsupported");
 
 // New MIME types could be added to this list as needed.
 for (const mimeType of [
@@ -104,66 +104,138 @@
   if (MediaRecorder.isTypeSupported(mimeType)) {
     test(t => {
       const recorder = new MediaRecorder(new MediaStream(), {mimeType});
-      assert_equals(recorder.mimeType, mimeType, "Supported MIMEType is set");
-    }, `Supported MIMEType ${mimeType} is set immediately after constructing`);
+      assert_equals(recorder.mimeType, mimeType, "Supported mimeType is set");
+    }, `Supported mimeType ${mimeType} is set immediately after constructing`);
   } else {
     test(t => {
       assert_throws("NotSupportedError",
         () => new MediaRecorder(new MediaStream(), {mimeType}));
-    }, `Unsupported MIMEType ${mimeType} throws`);
+    }, `Unsupported mimeType ${mimeType} throws`);
   }
 }
 
 promise_test(async t => {
   const recorder = new MediaRecorder(createAudioStream(t));
   recorder.start();
-  assert_equals(recorder.mimeType, "",
-    "MediaRecorder has no MIMEtype after start() for audio");
-
   await new Promise(r => recorder.onstart = r);
-  assert_not_equals(recorder.mimeType, "",
-    "MediaRecorder has a MIMEtype after 'start' for audio");
-  assert_regexp_match(recorder.mimeType, /^audio\//,
-    "MIMEtype has an expected media type");
-  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
-    "MIMEtype has a container subtype");
-  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+; codecs=[^,]+$/,
-    "MIMEtype has one codec");
-}, "MediaRecorder sets a MIMEType after 'start' for audio");
+  assert_not_equals(recorder.mimeType, "");
+}, "MediaRecorder sets a nonempty mimeType on 'onstart' for audio");
 
 promise_test(async t => {
   const recorder = new MediaRecorder(createVideoStream(t));
   recorder.start();
-  assert_equals(recorder.mimeType, "",
-    "MediaRecorder has no MIMEtype after start() for video");
-
   await new Promise(r => recorder.onstart = r);
-  assert_not_equals(recorder.mimeType, "",
-    "MediaRecorder has a MIMEtype after 'start' for video");
-  assert_regexp_match(recorder.mimeType, /^video\//,
-    "MIMEtype has an expected media type");
-  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
-    "MIMEtype has a container subtype");
-  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+; codecs=[^,]+$/,
-    "MIMEtype has one codec");
-}, "MediaRecorder sets a MIMEType after 'start' for video");
+  assert_not_equals(recorder.mimeType, "");
+}, "MediaRecorder sets a nonempty mimeType on 'onstart' for video");
 
 promise_test(async t => {
   const recorder = new MediaRecorder(createAudioVideoStream(t));
   recorder.start();
-  assert_equals(recorder.mimeType, "",
-    "MediaRecorder has no MIMEtype after start() for audio/video");
-
   await new Promise(r => recorder.onstart = r);
-  assert_not_equals(recorder.mimeType, "",
-    "MediaRecorder has a MIMEtype after 'start' for audio/video");
-  assert_regexp_match(recorder.mimeType, /^video\//,
-    "MIMEtype has an expected media type");
+  assert_not_equals(recorder.mimeType, "");
+}, "MediaRecorder sets a nonempty mimeType on 'onstart' for audio/video");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createAudioStream(t));
+  recorder.start();
+  assert_equals(recorder.mimeType, "");
+}, "MediaRecorder mimeType is not set before 'onstart' for audio");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createVideoStream(t));
+  recorder.start();
+  assert_equals(recorder.mimeType, "");
+}, "MediaRecorder mimeType is not set before 'onstart' for video");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createAudioVideoStream(t));
+  recorder.start();
+  assert_equals(recorder.mimeType, "");
+}, "MediaRecorder mimeType is not set before 'onstart' for audio/video");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createAudioStream(t));
+  const onstartPromise = new Promise(resolve => {
+    recorder.onstart = () => {
+      recorder.onstart = () => t.step_func(() => {
+        assert_not_reached("MediaRecorder doesn't fire 'onstart' twice");
+      });
+      resolve();
+    }
+  });
+  recorder.start();
+  await onstartPromise;
+  await new Promise(r => t.step_timeout(r, 1000));
+}, "MediaRecorder doesn't fire 'onstart' multiple times for audio");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createVideoStream(t));
+  const onstartPromise = new Promise(resolve => {
+    recorder.onstart = () => {
+      recorder.onstart = () => t.step_func(() => {
+        assert_not_reached("MediaRecorder doesn't fire 'onstart' twice");
+      });
+      resolve();
+    }
+  });
+  recorder.start();
+  await onstartPromise;
+  await new Promise(r => t.step_timeout(r, 1000));
+}, "MediaRecorder doesn't fire 'onstart' multiple times for video");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createAudioVideoStream(t));
+  const onstartPromise = new Promise(resolve => {
+    recorder.onstart = () => {
+      recorder.onstart = () => t.step_func(() => {
+        assert_not_reached("MediaRecorder doesn't fire 'onstart' twice");
+      });
+      resolve();
+    }
+  });
+  recorder.start();
+  await onstartPromise;
+  await new Promise(r => t.step_timeout(r, 1000));
+}, "MediaRecorder doesn't fire 'onstart' multiple times for audio/video");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createAudioStream(t));
+  recorder.start();
+  await new Promise(r => recorder.onstart = r);
+  assert_regexp_match(recorder.mimeType, /^audio\//,
+    "mimeType has an expected media type");
   assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
-    "MIMEtype has a container subtype");
-  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+; codecs=[^,]+,[^,]+$/,
-    "MIMEtype has two codecs");
-}, "MediaRecorder sets a MIMEType after 'start' for audio/video");
+    "mimeType has a container subtype");
+  assert_regexp_match(
+    recorder.mimeType, /^[a-z]+\/[a-z]+;[ ]*codecs=[^,]+$/,
+    "mimeType has one codec a");
+}, "MediaRecorder formats mimeType well after 'start' for audio");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createVideoStream(t));
+  recorder.start();
+  await new Promise(r => recorder.onstart = r);
+  assert_regexp_match(recorder.mimeType, /^video\//,
+    "mimeType has an expected media type");
+  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
+    "mimeType has a container subtype");
+  assert_regexp_match(
+    recorder.mimeType, /^[a-z]+\/[a-z]+;[ ]*codecs=[^,]+$/,
+    "mimeType has one codec a");
+}, "MediaRecorder formats mimeType well after 'start' for video");
+
+promise_test(async t => {
+  const recorder = new MediaRecorder(createAudioVideoStream(t));
+  recorder.start();
+  await new Promise(r => recorder.onstart = r);
+  assert_regexp_match(recorder.mimeType, /^video\//,
+    "mimeType has an expected media type");
+  assert_regexp_match(recorder.mimeType, /^[a-z]+\/[a-z]+/,
+    "mimeType has a container subtype");
+  assert_regexp_match(
+    recorder.mimeType, /^[a-z]+\/[a-z]+;[ ]*codecs=[^,]+,[^,]+$/,
+    "mimeType has two codecs");
+}, "MediaRecorder formats mimeType well after 'start' for audio/video");
 </script>
 </body>
 </html>
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-no-sink.https.html b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-no-sink.https.html
new file mode 100644
index 0000000..7f5ff93
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-no-sink.https.html
@@ -0,0 +1,42 @@
+<!doctype html>
+<html>
+
+<head>
+  <title>MediaRecorder peer connection</title>
+  <link rel="help"
+        href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="utils/peerconnection.js"></script>
+</head>
+
+<body>
+  <script>
+
+[{name: "audio",        kind: {audio: true, video: false}},
+ {name: "video",        kind: {audio: false, video: true}},
+ {name: "audio/video",  kind: {audio: true, video: true}}].forEach(args => {
+  promise_test(async t => {
+    const [localPc, remotePc, stream] = await startConnection(
+        t, args.kind.audio, args.kind.video);
+    const recorder = new MediaRecorder(stream);
+    // Set an arbitrary timeslice interval so the ondataavailable event
+    // handler gets invoked repeatedly. Without it, the test would
+    // deadlock as it's currently written.
+    recorder.start(100);
+    let combinedSize = 0;
+    const dataPromise = new Promise(r => recorder.ondataavailable = e => {
+      // Wait for an arbitrary amount of data to appear before we resolve.
+      combinedSize += e.data.size;
+      if (combinedSize > 4711) r();
+    });
+    await dataPromise;
+    recorder.stop();
+  }, "PeerConnection MediaRecorder records " + args.name +
+     " from PeerConnection without sinks");
+ });
+
+  </script>
+</body>
+
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-peerconnection.https.html b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-peerconnection.https.html
new file mode 100644
index 0000000..88e9da7
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-peerconnection.https.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<html>
+<meta name="timeout" content="long">
+
+<head>
+  <title>MediaRecorder peer connection</title>
+  <link rel="help"
+        href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="utils/peerconnection.js"></script>
+</head>
+
+<body>
+  <video id="remote" autoplay width="240" />
+  <script>
+
+[{ name: "video", kind: { video: true, audio: false }, mimeType: "" },
+ { name: "audio", kind: { video: false, audio: true }, mimeType: "" },
+ { name: "audio/video", kind: { video: true, audio: true }, mimeType: "" },
+ { name: "audio", kind: { video: false, audio: true }, mimeType: "video/webm;codecs=vp8" },
+ { name: "video", kind: { video: true, audio: false }, mimeType: "video/webm;codecs=vp8" },
+ { name: "audio/video", kind: { video: true, audio: true }, mimeType: "video/webm;codecs=vp8" },
+ { name: "audio", kind: { video: false, audio: true }, mimeType: "video/webm;codecs=vp9" },
+ { name: "video", kind: { video: true, audio: false }, mimeType: "video/webm;codecs=vp9" },
+ { name: "audio/video", kind: { video: true, audio: true }, mimeType: "video/webm;codecs=vp9" }]
+  .forEach(args => {
+    const formatString = JSON.stringify(args.kind) +
+        " with format " + (args.mimeType ? args.mimeType : "[passthrough]") + ".";
+    promise_test(async t => {
+      const [localPc, remotePc, stream] = await startConnection(
+          t, args.kind.audio, args.kind.video);
+      const recorder = new MediaRecorder(stream, { mimeType: args.mimeType });
+      let combinedSize = 0;
+      const dataPromise = new Promise(r => {
+        recorder.onstart = () => {
+          recorder.ondataavailable = e => {
+            // Wait for an arbitrary amount of data to appear before we resolve.
+            combinedSize += e.data.size;
+            if (combinedSize > 4711) r();
+          }
+        }
+      });
+      recorder.start(100);
+      await dataPromise;
+      recorder.stop();
+    }, "PeerConnection MediaRecorder receives data after onstart, " +
+          formatString);
+
+    promise_test(async t => {
+      const [localPc, remotePc, stream] = await startConnection(
+          t, args.kind.audio, args.kind.video);
+      const recorder = new MediaRecorder(stream, { mimeType: args.mimeType });
+      const stopPromise = new Promise(r => recorder.onstop = r);
+      const dataPromise = new Promise(r => recorder.ondataavailable = r);
+      recorder.start();
+      await waitForReceivedFrames(
+        t, remotePc, args.kind.audio, args.kind.video, 10);
+      for (transceiver of remotePc.getTransceivers())
+        transceiver.receiver.track.stop();
+      // As the tracks ended, we'd like to see data from the recorder.
+      // For details:
+      // https://www.w3.org/TR/mediastream-recording/#mediarecorder-methods.
+      await dataPromise;
+      await stopPromise;
+    }, "PeerConnection MediaRecorder gets ondata on stopping recorded " +
+            "tracks " + formatString);
+  });
+
+  </script>
+</body>
+
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-stop.html b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-stop.html
index 1cc18f4..bda5d87 100644
--- a/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-stop.html
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/MediaRecorder-stop.html
@@ -24,6 +24,20 @@
         return arr;
     }
 
+    // This function is used to check that elements of |actual| is a sub
+    // sequence in the |expected| sequence.
+    function assertSequenceIn(actual, expected) {
+      let i = 0;
+      for (event of actual) {
+        const j = expected.slice(i).indexOf(event);
+        assert_greater_than_equal(
+            j, 0, "Sequence element " + event + " is not included in " +
+            expected.slice(i));
+        i = j;
+      }
+      return true;
+    }
+
     promise_test(async t => {
         let video = createVideoStream();
         let recorder = new MediaRecorder(video);
@@ -40,8 +54,10 @@
         assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
         assert_equals(recorder.state, "inactive", "MediaRecorder is inactive after stop event");
 
-        assert_array_equals(events, ["start", "dataavailable", "stop"],
-            "Should have gotten expected events");
+        // As the test is written, it's not guaranteed that
+        // onstart/ondataavailable is invoked, but it's fine if they are.
+        // The stop element is guaranteed to be in events when we get here.
+        assertSequenceIn(events, ["start", "dataavailable", "stop"]);
     }, "MediaRecorder will stop recording and fire a stop event when all tracks are ended");
 
     promise_test(async t => {
@@ -59,8 +75,10 @@
         assert_true(event.isTrusted, "isTrusted should be true when the event is created by C++");
         assert_equals(recorder.state, "inactive", "MediaRecorder is inactive after stop event");
 
-        assert_array_equals(events, ["start", "dataavailable", "stop"],
-            "Should have gotten expected events");
+        // As the test is written, it's not guaranteed that
+        // onstart/ondataavailable is invoked, but it's fine if they are.
+        // The stop element is guaranteed to be in events when we get here.
+        assertSequenceIn(events, ["start", "dataavailable", "stop"]);
     }, "MediaRecorder will stop recording and fire a stop event when stop() is called");
 
     promise_test(async t => {
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/passthrough/MediaRecorder-passthrough.https-expected.txt b/third_party/blink/web_tests/external/wpt/mediacapture-record/passthrough/MediaRecorder-passthrough.https-expected.txt
new file mode 100644
index 0000000..6240f63
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/passthrough/MediaRecorder-passthrough.https-expected.txt
@@ -0,0 +1,8 @@
+This is a testharness.js-based test.
+PASS PeerConnection passthrough MediaRecorder receives VP8 after onstart with a video stream.
+PASS PeerConnection passthrough MediaRecorder receives VP8 after onstart with a audio/video stream.
+PASS PeerConnection passthrough MediaRecorder receives VP9 after onstart with a video stream.
+PASS PeerConnection passthrough MediaRecorder receives VP9 after onstart with a audio/video stream.
+FAIL PeerConnection passthrough MediaRecorder should be prepared to handle the codec switching from VP8 to VP9 assert_unreached: MediaRecorder should be prepared to handle codec switches Reached unreachable code
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html b/third_party/blink/web_tests/external/wpt/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html
new file mode 100644
index 0000000..655b7a1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/passthrough/MediaRecorder-passthrough.https.html
@@ -0,0 +1,61 @@
+<!doctype html>
+<html>
+
+<head>
+  <title>MediaRecorder peer connection</title>
+  <link rel="help"
+        href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType">
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../utils/peerconnection.js"></script>
+</head>
+
+<body>
+  <video id="remote" autoplay width="240" />
+  <script>
+
+[{kind: "video", audio: false, codecPreference: "VP8", codecRegex: /.*vp8.*/},
+ {kind: "audio/video", audio: true, codecPreference: "VP8", codecRegex: /.*vp8.*/},
+ {kind: "video", audio: false, codecPreference: "VP9", codecRegex: /.*vp9.*/},
+ {kind: "audio/video", audio: true, codecPreference: "VP9", codecRegex: /.*vp9.*/}]
+  .forEach(args => {
+    promise_test(async t => {
+      const [localPc, remotePc, stream] = await startConnection(
+          t, args.audio, /*video=*/true, args.codecPreference);
+      const recorder = new MediaRecorder(stream);  // Passthrough.
+      const onstartPromise = new Promise(resolve => {
+        recorder.onstart = t.step_func(() => {
+          assert_regexp_match(
+                recorder.mimeType, args.codecRegex,
+                "mimeType is matching " + args.codecPreference +
+                " in case of passthrough.");
+          resolve();
+        });
+      });
+      recorder.start();
+      await(onstartPromise);
+    }, "PeerConnection passthrough MediaRecorder receives " +
+          args.codecPreference + " after onstart with a " + args.kind +
+          " stream.");
+  });
+
+promise_test(async t => {
+  const [localPc, remotePc, stream, transceivers] = await startConnection(
+      t, /*audio=*/false, /*video=*/true, /*videoCodecPreference=*/"VP8");
+  const recorder = new MediaRecorder(stream);  // Possibly passthrough.
+  recorder.start();
+  await waitForReceivedFrames(t, remotePc, false, true, 10);
+
+  // Switch codec to VP9; we expect onerror to not be invoked.
+  recorder.onerror = t.step_func(() => assert_unreached(
+      "MediaRecorder should be prepared to handle codec switches"));
+  setTransceiverCodecPreference(transceivers.video, "VP9");
+  exchangeOfferAnswer(localPc, remotePc);
+  await waitForReceivedCodec(t, remotePc, "VP9");
+}, "PeerConnection passthrough MediaRecorder should be prepared to handle " +
+        "the codec switching from VP8 to VP9");
+
+</script>
+</body>
+
+</html>
diff --git a/third_party/blink/web_tests/external/wpt/mediacapture-record/utils/peerconnection.js b/third_party/blink/web_tests/external/wpt/mediacapture-record/utils/peerconnection.js
new file mode 100644
index 0000000..817095e
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/mediacapture-record/utils/peerconnection.js
@@ -0,0 +1,156 @@
+/**
+ * @fileoverview Utility functions for tests utilizing PeerConnections
+ */
+
+/**
+ * Exchanges offers and answers between two peer connections.
+ *
+ * pc1's offer is set as local description in pc1 and
+ * remote description in pc2. After that, pc2's answer
+ * is set as it's local description and remote description in pc1.
+ *
+ * @param {!RTCPeerConnection} pc1 The first peer connection.
+ * @param {!RTCPeerConnection} pc2 The second peer connection.
+ */
+async function exchangeOfferAnswer(pc1, pc2) {
+  await pc1.setLocalDescription(await pc1.createOffer());
+  await pc2.setRemoteDescription(pc1.localDescription);
+  await pc2.setLocalDescription(await pc2.createAnswer());
+  await pc1.setRemoteDescription(pc2.localDescription);
+}
+
+/**
+ * Sets the specified codec preference if it's included in the transceiver's
+ * list of supported codecs.
+ * @param {!RTCRtpTransceiver} transceiver The RTP transceiver.
+ * @param {string} codecPreference The codec preference.
+ */
+function setTransceiverCodecPreference(transceiver, codecPreference) {
+  for (let codec of RTCRtpSender.getCapabilities('video').codecs) {
+    if (codec.mimeType.includes(codecPreference)) {
+      transceiver.setCodecPreferences([codec]);
+      return;
+    }
+  }
+}
+
+/**
+ * Starts a connection between two peer connections, using a audio and/or video
+ * stream.
+ * @param {*} t Test instance.
+ * @param {boolean} useAudio True if audio should be used.
+ * @param {boolean} useVideo True if video should be used.
+ * @param {string} [videoCodecPreference] String containing the codec preference.
+ * @returns an array with the two connected peer connections, the remote stream,
+ * and the list of transceivers.
+ */
+async function startConnection(t, useAudio, useVideo, videoCodecPreference) {
+  const stream = await navigator.mediaDevices.getUserMedia({
+    audio: useAudio, video: useVideo
+  });
+  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
+  const pc1 = new RTCPeerConnection();
+  t.add_cleanup(() => pc1.close());
+  const pc2 = new RTCPeerConnection();
+  t.add_cleanup(() => pc2.close());
+  let transceivers = {};
+  stream.getTracks().forEach(track => {
+    const transceiver = pc1.addTransceiver(track);
+    transceivers[track.kind] = transceiver;
+    if (videoCodecPreference && track.kind == 'video') {
+      setTransceiverCodecPreference(transceiver, videoCodecPreference);
+    }
+  });
+  function doExchange(localPc, remotePc) {
+    localPc.addEventListener('icecandidate', event => {
+      const { candidate } = event;
+      if (candidate && remotePc.signalingState !== 'closed') {
+        remotePc.addIceCandidate(candidate);
+      }
+    });
+  }
+  doExchange(pc1, pc2);
+  doExchange(pc2, pc1);
+  exchangeOfferAnswer(pc1, pc2);
+  const remoteStream = await new Promise(resolve => {
+    let tracks = [];
+    pc2.ontrack = e => {
+      tracks.push(e.track)
+      if (tracks.length < useAudio + useVideo) return;
+      const stream = new MediaStream(tracks);
+      // The srcObject sink is needed for the tests to get exercised in Chrome.
+      const remoteVideo = document.getElementById('remote');
+      if (remoteVideo) {
+        remoteVideo.srcObject = stream;
+      }
+      resolve(stream)
+    }
+  });
+  return [pc1, pc2, remoteStream, transceivers]
+}
+
+/**
+ * Given a peer connection, return after at least numFramesOrPackets
+ * frames (video) or packets (audio) have been received.
+ * @param {*} t Test instance.
+ * @param {!RTCPeerConnection} pc The peer connection.
+ * @param {boolean} lookForAudio True if audio packets should be waited for.
+ * @param {boolean} lookForVideo True if video packets should be waited for.
+ * @param {int} numFramesOrPackets Number of frames (video) and packets (audio)
+ * to wait for.
+ */
+async function waitForReceivedFrames(
+    t, pc, lookForAudio, lookForVideo, numFramesOrPackets) {
+  let initialAudioPackets = 0;
+  let initialVideoFrames = 0;
+  while (lookForAudio || lookForVideo) {
+    const report = await pc.getStats();
+    report.forEach(stats => {
+      if (stats.type && stats.type == 'inbound-rtp') {
+        if (lookForAudio && stats.kind == 'audio') {
+          if (!initialAudioPackets) {
+            initialAudioPackets = stats.packetsReceived
+          } else if (stats.packetsReceived > initialAudioPackets +
+                     numFramesOrPackets) {
+            lookForAudio = false;
+          }
+        }
+        if (lookForVideo && stats.kind == 'video') {
+          if (!initialVideoFrames) {
+            initialVideoFrames = stats.framesDecoded;
+          } else if (stats.framesDecoded > initialVideoFrames +
+                     numFramesOrPackets) {
+            lookForVideo = false;
+          }
+        }
+      }
+    });
+    await new Promise(r => { t.step_timeout(r, 100); });
+  }
+}
+
+/**
+ * Given a peer connection, return after one of its inbound RTP connections
+ * includes use of the specified codec.
+ * @param {*} t Test instance.
+ * @param {!RTCPeerConnection} pc The peer connection.
+ * @param {string} codecToLookFor The waited-for codec.
+ */
+async function waitForReceivedCodec(t, pc, codecToLookFor) {
+  let currentCodecId;
+  for (;;) {
+    const report = await pc.getStats();
+    report.forEach(stats => {
+      if (stats.id) {
+        if (stats.type == 'inbound-rtp' && stats.kind == 'video') {
+          currentCodecId = stats.codecId;
+        } else if (currentCodecId && stats.id == currentCodecId &&
+                   stats.mimeType.toLowerCase().includes(
+                      codecToLookFor.toLowerCase())) {
+          return;
+        }
+      }
+    });
+    await new Promise(r => { t.step_timeout(r, 100); });
+  }
+}
diff --git a/third_party/blink/web_tests/fast/mediarecorder/MediaRecorder-requestData.html b/third_party/blink/web_tests/fast/mediarecorder/MediaRecorder-requestData.html
index c4f418e1..01aaba3 100644
--- a/third_party/blink/web_tests/fast/mediarecorder/MediaRecorder-requestData.html
+++ b/third_party/blink/web_tests/fast/mediarecorder/MediaRecorder-requestData.html
@@ -22,11 +22,10 @@
   }
 };
 
-var makeAsyncTest = function(value, expected) {
+const makeEmptyDataTest = function(value, expected) {
   async_test(function(test) {
     const recorderOnDataAvailable = test.step_func_done(function(event) {
       assert_equals(event.data.size, 0, 'Recorded data size should be == 0');
-      assert_equals(event.data.type, value['mimeType']);
       assert_not_equals(event.timecode, NaN, 'timecode');
     });
 
@@ -43,7 +42,7 @@
 
       assert_throws("InvalidStateError", function() { recorder.requestData(); },
                     "recorder throws InvalidStateError if requestData() " +
-                    "while  state is not 'recording'");
+                    "while state is not 'recording'");
 
       recorder.ondataavailable = recorderOnDataAvailable;
       recorder.onstop = recorderOnStop;
@@ -56,15 +55,48 @@
 
     const onError = test.unreached_func('Error creating MediaStream.');
     navigator.webkitGetUserMedia(value, gotStream, onError);
-  });
-};
+  }, "MediaRecorder requestData causes blobs without contained data to " +
+     `trigger ondataavailable after start (${JSON.stringify(value)})`);
+}
 
-generate_tests(makeAsyncTest,
-    [["video-only",
+const makeNonEmptyDataTest = function(value, expected) {
+  promise_test(async function(test) {
+    const recorderOnDataAvailable = test.step_func_done(function(event) {
+      assert_greater_than(event.data.size, 0,
+          'Recorded data size should be > 0');
+      assert_equals(event.data.type, value['mimeType']);
+    });
+
+    const gotStream = test.step_func(async function(stream) {
+      checkStreamTracks(stream, value['video'], value['audio']);
+      var recorder = new MediaRecorder(stream);
+      recorder.ondataavailable = recorderOnDataAvailable;
+      const onstartPromise = new Promise(r => recorder.onstart = r);
+      recorder.start();
+      await onstartPromise;
+      recorder.requestData();
+    });
+
+    const onError = test.unreached_func('Error creating MediaStream.');
+    navigator.webkitGetUserMedia(value, gotStream, onError);
+  }, "MediaRecorder requestData causes blobs with contained " +
+  `trigger ondataavailable after onstart (${JSON.stringify(value)})`);
+}
+
+generate_tests(makeEmptyDataTest,
+    [["empty-video-only",
          {video: true,  audio: false, mimeType: "video/webm;codecs=vp8"}],
-     ["audio-only",
+     ["empty-audio-only",
          {video: false, audio: true, mimeType: "audio/webm;codecs=opus"}],
-     ["audio-video",
+     ["empty-audio-video",
+         {video: true,  audio: true, mimeType: "video/webm;codecs=vp8,opus"}]]);
+
+generate_tests(makeNonEmptyDataTest,
+    [["nonempty-video-only",
+         {video: true,  audio: false, mimeType: "video/webm;codecs=vp8"}],
+     ["nonempty-audio-only",
+         {video: false, audio: true, mimeType: "audio/webm;codecs=opus"}],
+     ["nonempty-audio-video",
          {video: true,  audio: true, mimeType: "video/webm;codecs=vp8,opus"}]]);
 
 </script>