Commit 100dc177 authored by Markus Handell's avatar Markus Handell Committed by Commit Bot

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/+/1893855Reviewed-by: default avatarGuido Urdaneta <guidou@chromium.org>
Reviewed-by: default avatarHenrik Boström <hbos@chromium.org>
Commit-Queue: Markus Handell <handellm@google.com>
Cr-Commit-Position: refs/heads/master@{#732487}
parent 9d23a33b
......@@ -193,12 +193,6 @@ MediaRecorder::MediaRecorder(ExecutionContext* context,
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 @@ void MediaRecorder::start(int time_slice, ExceptionState& exception_state) {
"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 @@ void MediaRecorder::WriteData(const char* data,
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 @@ void MediaRecorder::StopRecording() {
// 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();
......
......@@ -103,11 +103,9 @@ class MODULES_EXPORT MediaRecorder
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_;
};
......
......@@ -3300,12 +3300,12 @@ crbug.com/626703 external/wpt/css/css-text/letter-spacing/letter-spacing-nesting
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 ]
......
......@@ -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);
......
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.
<!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 @@ function createAudioVideoStream(t) {
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 @@ for (const mimeType of [
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 sets a nonempty mimeType on 'onstart' for audio");
promise_test(async t => {
const recorder = new MediaRecorder(createVideoStream(t));
recorder.start();
await new Promise(r => recorder.onstart = r);
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();
await new Promise(r => recorder.onstart = r);
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_not_equals(recorder.mimeType, "",
"MediaRecorder has a MIMEtype after 'start' for audio");
assert_regexp_match(recorder.mimeType, /^audio\//,
"MIMEtype has an expected media type");
"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");
"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();
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");
"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");
"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();
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");
"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 two codecs");
}, "MediaRecorder formats mimeType well after 'start' for audio/video");
</script>
</body>
</html>
<!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>
<!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>
......@@ -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 => {
......
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.
<!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>
/**
* @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); });
}
}
......@@ -22,11 +22,10 @@ var checkStreamTracks = function(stream, has_video, has_audio) {
}
};
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 @@ var makeAsyncTest = function(value, expected) {
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 @@ var makeAsyncTest = function(value, expected) {
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)})`);
}
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"}],
["empty-audio-only",
{video: false, audio: true, mimeType: "audio/webm;codecs=opus"}],
["empty-audio-video",
{video: true, audio: true, mimeType: "video/webm;codecs=vp8,opus"}]]);
generate_tests(makeAsyncTest,
[["video-only",
generate_tests(makeNonEmptyDataTest,
[["nonempty-video-only",
{video: true, audio: false, mimeType: "video/webm;codecs=vp8"}],
["audio-only",
["nonempty-audio-only",
{video: false, audio: true, mimeType: "audio/webm;codecs=opus"}],
["audio-video",
["nonempty-audio-video",
{video: true, audio: true, mimeType: "video/webm;codecs=vp8,opus"}]]);
</script>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment