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>