diff options
Diffstat (limited to 'testing/web-platform/tests/webrtc-extensions')
15 files changed, 1741 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc-extensions/META.yml b/testing/web-platform/tests/webrtc-extensions/META.yml new file mode 100644 index 0000000000..be8cb028f0 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/META.yml @@ -0,0 +1,3 @@ +spec: https://w3c.github.io/webrtc-extensions/ +suggested_reviewers: + - hbos diff --git a/testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html b/testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html new file mode 100644 index 0000000000..63e92c6d08 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCOAuthCredential.html @@ -0,0 +1,44 @@ +<!doctype html> +<meta charset=utf-8> +<title>RTCConfiguration iceServers with OAuth credentials</title> +<script src='/resources/testharness.js'></script> +<script src='/resources/testharnessreport.js'></script> +<script src='../webrtc/RTCConfiguration-helper.js'></script> +<script> + 'use strict'; + +// These tests are based on +// https://w3c.github.io/webrtc-extensions/#rtcoauthcredential-dictionary + +/* + 4.3.2. To set a configuration + 11.6. If scheme name is turn or turns, and server.credentialType is "oauth", + and server.credential is not an RTCOAuthCredential, then throw an + InvalidAccessError and abort these steps. +*/ +config_test(makePc => { + assert_throws_dom('InvalidAccessError', () => + makePc({ iceServers: [{ + urls: 'turns:turn.example.org', + credentialType: 'oauth', + username: 'user', + credential: 'cred' + }] })); +}, 'with turns server, credentialType oauth, and string credential should throw InvalidAccessError'); + +config_test(makePc => { + const pc = makePc({ iceServers: [{ + urls: 'turns:turn2.example.net', + username: '22BIjxU93h/IgwEb', + credential: { + macKey: 'WmtzanB3ZW9peFhtdm42NzUzNG0=', + accessToken: 'AAwg3kPHWPfvk9bDFL936wYvkoctMADzQ5VhNDgeMR3+ZlZ35byg972fW8QjpEl7bx91YLBPFsIhsxloWcXPhA==' + }, + credentialType: 'oauth' + }]}); + const { iceServers } = pc.getConfiguration(); + const server = iceServers[0]; + assert_equals(server.credentialType, 'oauth'); +}, 'with turns server, credential type and credential from spec should not throw'); + +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html new file mode 100644 index 0000000000..8a7a8b6ba6 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-adaptivePtime.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>RTCRtpEncodingParameters adaptivePtime property</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + 'use strict'; + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const { sender } = pc.addTransceiver('audio', { + sendEncodings: [{adaptivePtime: true}], + }); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assert_true(encoding.adaptivePtime); + + encoding.adaptivePtime = false; + await sender.setParameters(param); + param = sender.getParameters(); + encoding = param.encodings[0]; + + assert_false(encoding.adaptivePtime); + + }, `Setting adaptivePtime should be accepted`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const { sender } = pc.addTransceiver('audio', { sendEncodings: [{}] }); + + const param = sender.getParameters(); + const encoding = param.encodings[0]; + + assert_false(encoding.adaptivePtime); + + }, `adaptivePtime should be default false`); + +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html new file mode 100644 index 0000000000..5c81349b15 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-codec.html @@ -0,0 +1,422 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>RTCRtpEncodingParameters codec property</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../webrtc/RTCPeerConnection-helper.js"></script> +<script src="../webrtc/third_party/sdp/sdp.js"></script> +<script src="../webrtc/simulcast/simulcast.js"></script> +<script> + 'use strict'; + + function findFirstCodec(name) { + return RTCRtpSender.getCapabilities(name.split('/')[0]).codecs.filter(c => c.mimeType.localeCompare(name, undefined, { sensitivity: 'base' }) === 0)[0]; + } + + function codecsNotMatching(mimeType) { + return RTCRtpSender.getCapabilities(mimeType.split('/')[0]).codecs.filter(c => c.mimeType.localeCompare(mimeType, undefined, {sensitivity: 'base'}) !== 0); + } + + function assertCodecEquals(a, b) { + assert_equals(a.mimeType, b.mimeType); + assert_equals(a.clockRate, b.clockRate); + assert_equals(a.channels, b.channels); + assert_equals(a.sdpFmtpLine, b.sdpFmtpLine); + } + + async function codecsForSender(sender) { + const rids = sender.getParameters().encodings.map(e => e.rid); + const stats = await sender.getStats(); + const codecs = [...stats] + .filter(([k, v]) => v.type === 'outbound-rtp') + .sort(([k, v], [k2, v2]) => rids.indexOf(v.rid) - rids.indexOf(v2.rid)) + .map(([k, v]) => stats.get(v.codecId).mimeType); + return codecs; + } + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const { sender } = pc.addTransceiver('audio'); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assert_equals(encoding.codec, undefined); + }, `Codec should be undefined by default on audio encodings`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const { sender } = pc.addTransceiver('video'); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assert_equals(encoding.codec, undefined); + }, `Codec should be undefined by default on video encodings`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const opus = findFirstCodec('audio/opus'); + + const { sender } = pc.addTransceiver('audio', { + sendEncodings: [{codec: opus}], + }); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assertCodecEquals(opus, encoding.codec); + }, `Creating an audio sender with addTransceiver and codec should work`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const vp8 = findFirstCodec('video/VP8'); + + const { sender } = pc.addTransceiver('video', { + sendEncodings: [{codec: vp8}], + }); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assertCodecEquals(vp8, encoding.codec); + }, `Creating a video sender with addTransceiver and codec should work`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const opus = findFirstCodec('audio/opus'); + + const { sender } = pc.addTransceiver('audio'); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = opus; + await sender.setParameters(param); + param = sender.getParameters(); + encoding = param.encodings[0]; + + assertCodecEquals(opus, encoding.codec); + + delete encoding.codec; + await sender.setParameters(param); + param = sender.getParameters(); + encoding = param.encodings[0]; + + assert_equals(encoding.codec, undefined); + }, `Setting codec on an audio sender with setParameters should work`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const vp8 = findFirstCodec('video/VP8'); + + const { sender } = pc.addTransceiver('video'); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = vp8; + await sender.setParameters(param); + param = sender.getParameters(); + encoding = param.encodings[0]; + + assertCodecEquals(vp8, encoding.codec); + + delete encoding.codec; + await sender.setParameters(param); + param = sender.getParameters(); + encoding = param.encodings[0]; + + assert_equals(encoding.codec, undefined); + }, `Setting codec on a video sender with setParameters should work`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const newCodec = { + mimeType: "audio/newCodec", + clockRate: 90000, + channel: 2, + }; + + assert_throws_dom('OperationError', () => pc.addTransceiver('video', { + sendEncodings: [{codec: newCodec}], + })); + }, `Creating an audio sender with addTransceiver and non-existing codec should throw OperationError`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const newCodec = { + mimeType: "video/newCodec", + clockRate: 90000, + }; + + assert_throws_dom('OperationError', () => pc.addTransceiver('video', { + sendEncodings: [{codec: newCodec}], + })); + }, `Creating a video sender with addTransceiver and non-existing codec should throw OperationError`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const newCodec = { + mimeType: "audio/newCodec", + clockRate: 90000, + channel: 2, + }; + + const { sender } = pc.addTransceiver('audio'); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = newCodec; + await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); + }, `Setting a non-existing codec on an audio sender with setParameters should throw InvalidModificationError`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const newCodec = { + mimeType: "video/newCodec", + clockRate: 90000, + }; + + const { sender } = pc.addTransceiver('video'); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = newCodec; + await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); + }, `Setting a non-existing codec on a video sender with setParameters should throw InvalidModificationError`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const opus = findFirstCodec('audio/opus'); + const nonOpus = codecsNotMatching(opus.mimeType); + + const transceiver = pc.addTransceiver('audio'); + const sender = transceiver.sender; + + transceiver.setCodecPreferences(nonOpus); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = opus; + await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); + }, `Setting a non-preferred codec on an audio sender with setParameters should throw InvalidModificationError`); + + promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const vp8 = findFirstCodec('video/VP8'); + const nonVP8 = codecsNotMatching(vp8.mimeType); + + const transceiver = pc.addTransceiver('video'); + const sender = transceiver.sender; + + transceiver.setCodecPreferences(nonVP8); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = vp8; + await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); + }, `Setting a non-preferred codec on a video sender with setParameters should throw InvalidModificationError`); + + promise_test(async (t) => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + const opus = findFirstCodec('audio/opus'); + const nonOpus = codecsNotMatching(opus.mimeType); + + const transceiver = pc1.addTransceiver('audio'); + const sender = transceiver.sender; + + transceiver.setCodecPreferences(nonOpus); + + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = opus; + await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); + }, `Setting a non-negotiated codec on an audio sender with setParameters should throw InvalidModificationError`); + + promise_test(async (t) => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + const vp8 = findFirstCodec('video/VP8'); + const nonVP8 = codecsNotMatching(vp8.mimeType); + + const transceiver = pc1.addTransceiver('video'); + const sender = transceiver.sender; + + transceiver.setCodecPreferences(nonVP8); + + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + encoding.codec = vp8; + await promise_rejects_dom(t, "InvalidModificationError", sender.setParameters(param)); + }, `Setting a non-negotiated codec on a video sender with setParameters should throw InvalidModificationError`); + + promise_test(async (t) => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + const opus = findFirstCodec('audio/opus'); + const nonOpus = codecsNotMatching(opus.mimeType); + + const transceiver = pc1.addTransceiver('audio', { + sendEncodings: [{codec: opus}], + }); + const sender = transceiver.sender; + + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assertCodecEquals(opus, encoding.codec); + + transceiver.setCodecPreferences(nonOpus); + await exchangeOfferAnswer(pc1, pc2); + + param = sender.getParameters(); + encoding = param.encodings[0]; + + assert_equals(encoding.codec, undefined); + }, `Codec should be undefined after negotiating away the currently set codec on an audio sender`); + + promise_test(async (t) => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + + const vp8 = findFirstCodec('video/VP8'); + const nonVP8 = codecsNotMatching(vp8.mimeType); + + const transceiver = pc1.addTransceiver('video', { + sendEncodings: [{codec: vp8}], + }); + const sender = transceiver.sender; + + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + + assertCodecEquals(vp8, encoding.codec); + + transceiver.setCodecPreferences(nonVP8); + await exchangeOfferAnswer(pc1, pc2); + + param = sender.getParameters(); + encoding = param.encodings[0]; + + assert_equals(encoding.codec, undefined); + }, `Codec should be undefined after negotiating away the currently set codec on a video sender`); + + promise_test(async (t) => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + const stream = await getNoiseStream({audio:true}); + t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); + + const opus = findFirstCodec('audio/opus'); + const nonOpus = codecsNotMatching(opus.mimeType); + + const transceiver = pc1.addTransceiver(stream.getTracks()[0]); + const sender = transceiver.sender; + + transceiver.setCodecPreferences(nonOpus.concat([opus])); + + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + + let codecs = await codecsForSender(sender); + assert_not_equals(codecs[0], opus.mimeType); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + encoding.codec = opus; + + await sender.setParameters(param); + + codecs = await codecsForSender(sender); + assert_array_equals(codecs, [opus.mimeType]); + }, `Stats output-rtp should match the selected codec in non-simulcast usecase on an audio sender`); + + promise_test(async (t) => { + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + t.add_cleanup(() => pc2.close()); + const stream = await getNoiseStream({video:true}); + t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); + + const vp8 = findFirstCodec('video/VP8'); + const nonVP8 = codecsNotMatching(vp8.mimeType); + + const transceiver = pc1.addTransceiver(stream.getTracks()[0]); + const sender = transceiver.sender; + + transceiver.setCodecPreferences(nonVP8.concat([vp8])); + + exchangeIceCandidates(pc1, pc2); + await exchangeOfferAnswer(pc1, pc2); + + let codecs = await codecsForSender(sender); + assert_not_equals(codecs[0], vp8.mimeType); + + let param = sender.getParameters(); + let encoding = param.encodings[0]; + encoding.codec = vp8; + + await sender.setParameters(param); + + codecs = await codecsForSender(sender); + assert_array_equals(codecs, [vp8.mimeType]); + }, `Stats output-rtp should match the selected codec in non-simulcast usecase on a video sender`); +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html new file mode 100644 index 0000000000..3e348f0d14 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpParameters-maxFramerate.html @@ -0,0 +1,101 @@ +<!doctype html> +<meta charset=utf-8> +<title>RTCRtpParameters encodings</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/webrtc/dictionary-helper.js"></script> +<script src="/webrtc/RTCRtpParameters-helper.js"></script> +<script> +'use strict'; + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + assert_throws_js(RangeError, () => pc.addTransceiver('video', { + sendEncodings: [{ + maxFramerate: -10 + }] + })); +}, `addTransceiver() with sendEncoding.maxFramerate field set to less than 0 should reject with RangeError`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + let {sender} = pc.addTransceiver('audio', { + sendEncodings: [{ + maxFramerate: -10 + }] + }); + let encodings = sender.getParameters().encodings; + assert_equals(encodings.length, 1); + assert_not_own_property(encodings[0], "maxFramerate"); + + sender = pc.addTransceiver('audio', { + sendEncodings: [{ + maxFramerate: 10 + }] + }).sender; + encodings = sender.getParameters().encodings; + assert_equals(encodings.length, 1); + assert_not_own_property(encodings[0], "maxFramerate"); +}, `addTransceiver('audio') with sendEncoding.maxFramerate should succeed, but remove the maxFramerate, even if it is invalid`); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {sender} = pc.addTransceiver('audio'); + let params = sender.getParameters(); + assert_equals(params.encodings.length, 1); + params.encodings[0].maxFramerate = 20; + await sender.setParameters(params); + const {encodings} = sender.getParameters(); + assert_equals(encodings.length, 1); + assert_not_own_property(encodings[0], "maxFramerate"); +}, `setParameters with maxFramerate on an audio sender should succeed, but remove the maxFramerate`); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {sender} = pc.addTransceiver('audio'); + let params = sender.getParameters(); + assert_equals(params.encodings.length, 1); + params.encodings[0].maxFramerate = -1; + await sender.setParameters(params); + const {encodings} = sender.getParameters(); + assert_equals(encodings.length, 1); + assert_not_own_property(encodings[0], "maxFramerate"); +}, `setParameters with an invalid maxFramerate on an audio sender should succeed, but remove the maxFramerate`); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const { sender } = pc.addTransceiver('video'); + await doOfferAnswerExchange(t, pc); + + const param = sender.getParameters(); + const encoding = param.encodings[0]; + assert_not_own_property(encoding, "maxFramerate"); + + encoding.maxFramerate = -10; + return promise_rejects_js(t, RangeError, + sender.setParameters(param)); +}, `setParameters() with encoding.maxFramerate field set to less than 0 should reject with RangeError`); + +// It would be great if we could test to see whether maxFramerate is actually +// honored. +test_modified_encoding('video', 'maxFramerate', 24, 16, + 'setParameters() with maxFramerate 24->16 should succeed'); + +test_modified_encoding('video', 'maxFramerate', undefined, 16, + 'setParameters() with maxFramerate undefined->16 should succeed'); + +test_modified_encoding('video', 'maxFramerate', 24, undefined, + 'setParameters() with maxFramerate 24->undefined should succeed'); + +test_modified_encoding('video', 'maxFramerate', 0, 16, + 'setParameters() with maxFramerate 0->16 should succeed'); + +test_modified_encoding('video', 'maxFramerate', 24, 0, + 'setParameters() with maxFramerate 24->0 should succeed'); + +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html new file mode 100644 index 0000000000..33f71800bd --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget-stats.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<title>Tests RTCRtpReceiver-jitterBufferTarget verified with stats</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/webrtc/RTCPeerConnection-helper.js"></script> +<body> +<script> +'use strict' + +function async_promise_test(func, name, properties) { + async_test(t => { + Promise.resolve(func(t)) + .catch(t.step_func(e => { throw e; })) + .then(() => t.done()); + }, name, properties); +} + +async_promise_test(t => applyJitterBufferTarget(t, "video", 4000), + "measure raising and lowering video jitterBufferTarget"); +async_promise_test(t => applyJitterBufferTarget(t, "audio", 4000), + "measure raising and lowering audio jitterBufferTarget"); + +async function applyJitterBufferTarget(t, kind, target) { + const caller = new RTCPeerConnection(); + t.add_cleanup(() => caller.close()); + const callee = new RTCPeerConnection(); + t.add_cleanup(() => callee.close()); + + const stream = await getNoiseStream({[kind]:true}); + t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); + caller.addTransceiver(stream.getTracks()[0], {streams: [stream]}); + caller.addTransceiver(stream.getTracks()[0], {streams: [stream]}); + + exchangeIceCandidates(caller, callee); + await exchangeOffer(caller, callee); + const [unconstrainedReceiver, constrainedReceiver] = callee.getReceivers(); + const haveRtp = Promise.all([ + new Promise(r => constrainedReceiver.track.onunmute = r), + new Promise(r => unconstrainedReceiver.track.onunmute = r) + ]); + await exchangeAnswer(caller, callee); + const chromeTimeout = new Promise(r => t.step_timeout(r, 1000)); // crbug.com/1295295 + await Promise.race([haveRtp, chromeTimeout]); + + // Allow some data to be processed to let the jitter buffer to stabilize a bit before measuring + await new Promise(r => t.step_timeout(r, 5000)); + + t.step(() => assert_equals(constrainedReceiver.jitterBufferTarget, null, + `jitterBufferTarget supported for ${kind}`)); + + constrainedReceiver.jitterBufferTarget = target; + t.step(() => assert_equals(constrainedReceiver.jitterBufferTarget, target, + `jitterBufferTarget increase target for ${kind}`)); + + const [increased, base] = await Promise.all([ + measureDelayFromStats(t, constrainedReceiver, 20), + measureDelayFromStats(t, unconstrainedReceiver, 20) + ]); + + t.step(() => assert_greater_than(increased , base, + `${kind} increased delay ${increased} ` + + ` greater than base delay ${base}`)); + + constrainedReceiver.jitterBufferTarget = 0; + + // Allow the jitter buffer to stabilize a bit before measuring + await new Promise(r => t.step_timeout(r, 5000)); + t.step(() => assert_equals(constrainedReceiver.jitterBufferTarget, 0, + `jitterBufferTarget decrease target for ${kind}`)); + + const decreased = await measureDelayFromStats(t, constrainedReceiver, 20); + + t.step(() => assert_less_than(decreased, increased, + `${kind} decreasedDelay ${decreased} ` + + `less than increased delay ${increased}`)); +} + +async function measureDelayFromStats(t, receiver, cycles) { + + let statsReport = await receiver.getStats(); + const oldInboundStats = [...statsReport.values()].find(({type}) => type == "inbound-rtp"); + + await new Promise(r => t.step_timeout(r, 1000 * cycles)); + + statsReport = await receiver.getStats(); + const inboundStats = [...statsReport.values()].find(({type}) => type == "inbound-rtp"); + + const delay = ((inboundStats.jitterBufferDelay - oldInboundStats.jitterBufferDelay) / + (inboundStats.jitterBufferEmittedCount - oldInboundStats.jitterBufferEmittedCount) * 1000); + + return delay; +} +</script> +</body> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html new file mode 100644 index 0000000000..448162d3a2 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpReceiver-jitterBufferTarget.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>Tests for RTCRtpReceiver-jitterBufferTarget attribute</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> +<script> +'use strict' + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); +}, 'audio jitterBufferTarget is null by default'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 500; + assert_equals(receiver.jitterBufferTarget, 500); +}, 'audio jitterBufferTarget accepts posititve values'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 4000; + assert_throws_js(RangeError, () => { + receiver.jitterBufferTarget = 4001; + }, 'audio jitterBufferTarget doesn\'t accept values greater than 4000 milliseconds'); + assert_equals(receiver.jitterBufferTarget, 4000); +}, 'audio jitterBufferTarget accepts values up to 4000 milliseconds'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 700; + assert_throws_js(RangeError, () => { + receiver.jitterBufferTarget = -500; + }, 'audio jitterBufferTarget doesn\'t accept negative values'); + assert_equals(receiver.jitterBufferTarget, 700); +}, 'audio jitterBufferTarget returns last valid value on throw'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 0; + assert_equals(receiver.jitterBufferTarget, 0); +}, 'audio jitterBufferTarget allows zero value'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('audio', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 500; + assert_equals(receiver.jitterBufferTarget, 500); + receiver.jitterBufferTarget = null; + assert_equals(receiver.jitterBufferTarget, null); +}, 'audio jitterBufferTarget allows to reset value to null'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('video', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); +}, 'video jitterBufferTarget is null by default'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('video', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 500; + assert_equals(receiver.jitterBufferTarget, 500); +}, 'video jitterBufferTarget accepts posititve values'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('video', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 4000; + assert_throws_js(RangeError, () => { + receiver.jitterBufferTarget = 4001; + }, 'video jitterBufferTarget doesn\'t accept values greater than 4000 milliseconds'); + assert_equals(receiver.jitterBufferTarget, 4000); +}, 'video jitterBufferTarget accepts values up to 4000 milliseconds'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('video', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 700; + assert_throws_js(RangeError, () => { + receiver.jitterBufferTarget = -500; + }, 'video jitterBufferTarget doesn\'t accept negative values'); + assert_equals(receiver.jitterBufferTarget, 700); +}, 'video jitterBufferTarget returns last valid value'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('video', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 0; + assert_equals(receiver.jitterBufferTarget, 0); +}, 'video jitterBufferTarget allows zero value'); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const {receiver} = pc.addTransceiver('video', {direction:'recvonly'}); + assert_equals(receiver.jitterBufferTarget, null); + receiver.jitterBufferTarget = 500; + assert_equals(receiver.jitterBufferTarget, 500); + receiver.jitterBufferTarget = null; + assert_equals(receiver.jitterBufferTarget, null); +}, 'video jitterBufferTarget allows to reset value to null'); +</script> +</body> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html new file mode 100644 index 0000000000..60b4ed0a74 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-captureTimestamp.html @@ -0,0 +1,94 @@ +<!doctype html> +<meta charset=utf-8> +<!-- This file contains a test that waits for 2 seconds. --> +<meta name="timeout" content="long"> +<title>captureTimestamp attribute in RTCRtpSynchronizationSource</title> +<div><video id="remote" width="124" height="124" autoplay></video></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/webrtc/RTCPeerConnection-helper.js"></script> +<script src="/webrtc/RTCStats-helper.js"></script> +<script src="/webrtc-extensions/RTCRtpSynchronizationSource-helper.js"></script> +<script> +'use strict'; + +function listenForCaptureTimestamp(t, receiver) { + return new Promise((resolve) => { + function listen() { + const ssrcs = receiver.getSynchronizationSources(); + assert_true(ssrcs != undefined); + if (ssrcs.length > 0) { + assert_equals(ssrcs.length, 1); + if (ssrcs[0].captureTimestamp != undefined) { + resolve(ssrcs[0].captureTimestamp); + return true; + } + } + return false; + }; + t.step_wait(listen, 'No abs-capture-time capture time header extension.'); + }); +} + +// Passes if `getSynchronizationSources()` contains `captureTimestamp` if and +// only if expected. +for (const kind of ['audio', 'video']) { + promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false, + /* absCaptureTimeAnswered= */false); + const receiver = callee.getReceivers()[0]; + + for (const ssrc of await listenForSSRCs(t, receiver)) { + assert_equals(typeof ssrc.captureTimestamp, 'undefined'); + } + }, '[' + kind + '] getSynchronizationSources() should not contain ' + + 'captureTimestamp if absolute capture time RTP header extension is not ' + + 'offered'); + + promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false, + /* absCaptureTimeAnswered= */false); + const receiver = callee.getReceivers()[0]; + + for (const ssrc of await listenForSSRCs(t, receiver)) { + assert_equals(typeof ssrc.captureTimestamp, 'undefined'); + } + }, '[' + kind + '] getSynchronizationSources() should not contain ' + + 'captureTimestamp if absolute capture time RTP header extension is ' + + 'offered, but not answered'); + + promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */true, + /* absCaptureTimeAnswered= */true); + const receiver = callee.getReceivers()[0]; + await listenForCaptureTimestamp(t, receiver); + }, '[' + kind + '] getSynchronizationSources() should contain ' + + 'captureTimestamp if absolute capture time RTP header extension is ' + + 'negotiated'); +} + +// Passes if `captureTimestamp` for audio and video are comparable, which is +// expected since the test creates a local peer connection between `caller` and +// `callee`. +promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{audio: true, video: true}, + /* absCaptureTimeOffered= */true, /* absCaptureTimeAnswered= */true); + const receivers = callee.getReceivers(); + assert_equals(receivers.length, 2); + + let captureTimestamps = [undefined, undefined]; + const t0 = performance.now(); + for (let i = 0; i < 2; ++i) { + captureTimestamps[i] = await listenForCaptureTimestamp(t, receivers[i]); + } + const t1 = performance.now(); + assert_less_than(Math.abs(captureTimestamps[0] - captureTimestamps[1]), + t1 - t0); +}, 'Audio and video RTCRtpSynchronizationSource.captureTimestamp are ' + + 'comparable'); + +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js new file mode 100644 index 0000000000..10cfd65155 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-helper.js @@ -0,0 +1,140 @@ +'use strict'; + +// This file depends on `webrtc/RTCPeerConnection-helper.js` +// which should be loaded from the main HTML file. + +var kAbsCaptureTime = + 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time'; + +function addHeaderExtensionToSdp(sdp, uri) { + const extmap = new RegExp('a=extmap:(\\d+)'); + let sdpLines = sdp.split('\r\n'); + + // This assumes at most one audio m= section and one video m= section. + // If more are present, only the first section of each kind is munged. + for (const section of ['audio', 'video']) { + let found_section = false; + let maxId = undefined; + let maxIdLine = undefined; + let extmapAllowMixed = false; + + // find the largest header extension id for section. + for (let i = 0; i < sdpLines.length; ++i) { + if (!found_section) { + if (sdpLines[i].startsWith('m=' + section)) { + found_section = true; + } + continue; + } else { + if (sdpLines[i].startsWith('m=')) { + // end of section + break; + } + } + + if (sdpLines[i] === 'a=extmap-allow-mixed') { + extmapAllowMixed = true; + } + let result = sdpLines[i].match(extmap); + if (result && result.length === 2) { + if (maxId == undefined || result[1] > maxId) { + maxId = parseInt(result[1]); + maxIdLine = i; + } + } + } + + if (maxId == 14 && !extmapAllowMixed) { + // Reaching the limit of one byte header extension. Adding two byte header + // extension support. + sdpLines.splice(maxIdLine + 1, 0, 'a=extmap-allow-mixed'); + } + if (maxIdLine !== undefined) { + sdpLines.splice(maxIdLine + 1, 0, + 'a=extmap:' + (maxId + 1).toString() + ' ' + uri); + } + } + return sdpLines.join('\r\n'); +} + +// TODO(crbug.com/1051821): Use RTP header extension API instead of munging +// when the RTP header extension API is implemented. +async function addAbsCaptureTimeAndExchangeOffer(caller, callee) { + let offer = await caller.createOffer(); + + // Absolute capture time header extension may not be offered by default, + // in such case, munge the SDP. + offer.sdp = addHeaderExtensionToSdp(offer.sdp, kAbsCaptureTime); + + await caller.setLocalDescription(offer); + return callee.setRemoteDescription(offer); +} + +// TODO(crbug.com/1051821): Use RTP header extension API instead of munging +// when the RTP header extension API is implemented. +async function checkAbsCaptureTimeAndExchangeAnswer(caller, callee, + absCaptureTimeAnswered) { + let answer = await callee.createAnswer(); + + const extmap = new RegExp('a=extmap:\\d+ ' + kAbsCaptureTime + '\r\n', 'g'); + if (answer.sdp.match(extmap) == null) { + // We expect that absolute capture time RTP header extension is answered. + // But if not, there is no need to proceed with the test. + assert_false(absCaptureTimeAnswered, 'Absolute capture time RTP ' + + 'header extension is not answered'); + } else { + if (!absCaptureTimeAnswered) { + // We expect that absolute capture time RTP header extension is not + // answered, but it is, then we munge the answer to remove it. + answer.sdp = answer.sdp.replace(extmap, ''); + } + } + + await callee.setLocalDescription(answer); + return caller.setRemoteDescription(answer); +} + +async function exchangeOfferAndListenToOntrack(t, caller, callee, + absCaptureTimeOffered) { + const ontrackPromise = addEventListenerPromise(t, callee, 'track'); + // Absolute capture time header extension is expected not offered by default, + // and thus munging is needed to enable it. + await absCaptureTimeOffered + ? addAbsCaptureTimeAndExchangeOffer(caller, callee) + : exchangeOffer(caller, callee); + return ontrackPromise; +} + +async function initiateSingleTrackCall(t, cap, absCaptureTimeOffered, + absCaptureTimeAnswered) { + const caller = new RTCPeerConnection(); + t.add_cleanup(() => caller.close()); + const callee = new RTCPeerConnection(); + t.add_cleanup(() => callee.close()); + + const stream = await getNoiseStream(cap); + stream.getTracks().forEach(track => { + caller.addTrack(track, stream); + t.add_cleanup(() => track.stop()); + }); + + // TODO(crbug.com/988432): `getSynchronizationSources() on the audio side + // needs a hardware sink for the returned dictionary entries to get updated. + const remoteVideo = document.getElementById('remote'); + + callee.ontrack = e => { + remoteVideo.srcObject = e.streams[0]; + } + + exchangeIceCandidates(caller, callee); + + await exchangeOfferAndListenToOntrack(t, caller, callee, + absCaptureTimeOffered); + + // Exchange answer and check whether the absolute capture time RTP header + // extension is answered. + await checkAbsCaptureTimeAndExchangeAnswer(caller, callee, + absCaptureTimeAnswered); + + return [caller, callee]; +} diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html new file mode 100644 index 0000000000..63ad9bf888 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpSynchronizationSource-senderCaptureTimeOffset.html @@ -0,0 +1,92 @@ +<!doctype html> +<meta charset=utf-8> +<!-- This file contains a test that waits for 2 seconds. --> +<meta name="timeout" content="long"> +<title>senderCaptureTimeOffset attribute in RTCRtpSynchronizationSource</title> +<div><video id="remote" width="124" height="124" autoplay></video></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/webrtc/RTCPeerConnection-helper.js"></script> +<script src="/webrtc/RTCStats-helper.js"></script> +<script src="/webrtc-extensions/RTCRtpSynchronizationSource-helper.js"></script> +<script> +'use strict'; + +function listenForSenderCaptureTimeOffset(t, receiver) { + return new Promise((resolve) => { + function listen() { + const ssrcs = receiver.getSynchronizationSources(); + assert_true(ssrcs != undefined); + if (ssrcs.length > 0) { + assert_equals(ssrcs.length, 1); + if (ssrcs[0].captureTimestamp != undefined) { + resolve(ssrcs[0].senderCaptureTimeOffset); + return true; + } + } + return false; + }; + t.step_wait(listen, 'No abs-capture-time capture time header extension.'); + }); +} + +// Passes if `getSynchronizationSources()` contains `senderCaptureTimeOffset` if +// and only if expected. +for (const kind of ['audio', 'video']) { + promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false, + /* absCaptureTimeAnswered= */false); + const receiver = callee.getReceivers()[0]; + + for (const ssrc of await listenForSSRCs(t, receiver)) { + assert_equals(typeof ssrc.senderCaptureTimeOffset, 'undefined'); + } + }, '[' + kind + '] getSynchronizationSources() should not contain ' + + 'senderCaptureTimeOffset if absolute capture time RTP header extension ' + + 'is not offered'); + + promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */false, + /* absCaptureTimeAnswered= */false); + const receiver = callee.getReceivers()[0]; + + for (const ssrc of await listenForSSRCs(t, receiver)) { + assert_equals(typeof ssrc.senderCaptureTimeOffset, 'undefined'); + } + }, '[' + kind + '] getSynchronizationSources() should not contain ' + + 'senderCaptureTimeOffset if absolute capture time RTP header extension ' + + 'is offered, but not answered'); + + promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{[kind]: true}, /* absCaptureTimeOffered= */true, + /* absCaptureTimeAnswered= */true); + const receiver = callee.getReceivers()[0]; + let senderCaptureTimeOffset = await listenForSenderCaptureTimeOffset( + t, receiver); + assert_true(senderCaptureTimeOffset != undefined); + }, '[' + kind + '] getSynchronizationSources() should contain ' + + 'senderCaptureTimeOffset if absolute capture time RTP header extension ' + + 'is negotiated'); +} + +// Passes if `senderCaptureTimeOffset` is zero, which is expected since the test +// creates a local peer connection between `caller` and `callee`. +promise_test(async t => { + const [caller, callee] = await initiateSingleTrackCall( + t, /* caps= */{audio: true, video: true}, + /* absCaptureTimeOffered= */true, /* absCaptureTimeAnswered= */true); + const receivers = callee.getReceivers(); + assert_equals(receivers.length, 2); + + for (let i = 0; i < 2; ++i) { + let senderCaptureTimeOffset = await listenForSenderCaptureTimeOffset( + t, receivers[i]); + assert_equals(senderCaptureTimeOffset, 0); + } +}, 'Audio and video RTCRtpSynchronizationSource.senderCaptureTimeOffset must ' + + 'be zero'); + +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html b/testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html new file mode 100644 index 0000000000..79eba02727 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/RTCRtpTransceiver-headerExtensionControl.html @@ -0,0 +1,295 @@ +<!doctype html> +<meta charset=utf-8> +<title>RTCRtpParameters encodings</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/webrtc/dictionary-helper.js"></script> +<script src="/webrtc/RTCRtpParameters-helper.js"></script> +<script src="/webrtc/third_party/sdp/sdp.js"></script> +<script> +'use strict'; + +async function negotiate(pc1, pc2) { + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); +} + +['audio', 'video'].forEach(kind => { + test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver(kind); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + const capability = capabilities.find((capability) => { + return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + assert_not_equals(capability, undefined); + assert_equals(capability.direction, 'sendrecv'); + }, `the ${kind} transceiver.getHeaderExtensionsToNegotiate() includes mandatory extensions`); +}); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('audio'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + capabilities[0].uri = ''; + assert_throws_js(TypeError, () => { + transceiver.setHeaderExtensionsToNegotiate(capabilities); + }, 'transceiver should throw TypeError when setting an empty URI'); +}, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing URI`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('audio'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + capabilities[0].direction = ''; + assert_throws_js(TypeError, () => { + transceiver.setHeaderExtensionsToNegotiate(capabilities); + }, 'transceiver should throw TypeError when setting an empty direction'); +}, `setHeaderExtensionsToNegotiate throws TypeError on encountering missing direction`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('audio'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + capabilities[0].uri = '4711'; + assert_throws_dom('InvalidModificationError', () => { + transceiver.setHeaderExtensionsToNegotiate(capabilities); + }, 'transceiver should throw InvalidModificationError when setting an unknown URI'); +}, `setHeaderExtensionsToNegotiate throws InvalidModificationError on encountering unknown URI`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('video'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate().filter(capability => { + return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + assert_throws_dom('InvalidModificationError', () => { + transceiver.setHeaderExtensionsToNegotiate(capabilities); + }, 'transceiver should throw InvalidModificationError when removing elements from the list'); +}, `setHeaderExtensionsToNegotiate throws InvalidModificationError when removing elements from the list`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('video'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + capabilities.push({ + uri: '4711', + direction: 'recvonly', + }); + assert_throws_dom('InvalidModificationError', () => { + transceiver.setHeaderExtensionsToNegotiate(capabilities); + }, 'transceiver should throw InvalidModificationError when adding elements to the list'); +}, `setHeaderExtensionsToNegotiate throws InvalidModificationError when adding elements to the list`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('audio'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + let capability = capabilities.find((capability) => { + return capability.uri === 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + ['sendonly', 'recvonly', 'inactive', 'stopped'].map(direction => { + capability.direction = direction; + assert_throws_dom('InvalidModificationError', () => { + transceiver.setHeaderExtensionsToNegotiate(capabilities); + }, `transceiver should throw InvalidModificationError when setting a mandatory header extension\'s direction to ${direction}`); + }); +}, `setHeaderExtensionsToNegotiate throws InvalidModificationError when setting a mandatory header extension\'s direction to something else than "sendrecv"`); + +test(t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('audio'); + let capabilities = transceiver.getHeaderExtensionsToNegotiate(); + let selected_capability = capabilities.find((capability) => { + return capability.direction === 'sendrecv' && + capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + selected_capability.direction = 'stopped'; + const offered_capabilities = transceiver.getHeaderExtensionsToNegotiate(); + let altered_capability = capabilities.find((capability) => { + return capability.uri === selected_capability.uri && + capability.direction === 'stopped'; + }); + assert_not_equals(altered_capability, undefined); +}, `modified direction set by setHeaderExtensionsToNegotiate is visible in subsequent getHeaderExtensionsToNegotiate`); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('video'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + const offer = await pc.createOffer(); + const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:') + .map(line => SDPUtils.parseExtmap(line)); + for (const capability of capabilities) { + if (capability.direction === 'stopped') { + assert_equals(undefined, extensions.find(e => e.uri === capability.uri)); + } else { + assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri)); + } + } +}, `Unstopped extensions turn up in offer`); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + const transceiver = pc.addTransceiver('video'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + const selected_capability = capabilities.find((capability) => { + return capability.direction === 'sendrecv' && + capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + selected_capability.direction = 'stopped'; + transceiver.setHeaderExtensionsToNegotiate(capabilities); + const offer = await pc.createOffer(); + const extensions = SDPUtils.matchPrefix(SDPUtils.splitSections(offer.sdp)[1], 'a=extmap:') + .map(line => SDPUtils.parseExtmap(line)); + for (const capability of capabilities) { + if (capability.direction === 'stopped') { + assert_equals(undefined, extensions.find(e => e.uri === capability.uri)); + } else { + assert_not_equals(undefined, extensions.find(e => e.uri === capability.uri)); + } + } +}, `Stopped extensions do not turn up in offers`); + +promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + + // Disable a non-mandatory extension before first negotiation. + const transceiver = pc1.addTransceiver('video'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + const selected_capability = capabilities.find((capability) => { + return capability.direction === 'sendrecv' && + capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + selected_capability.direction = 'stopped'; + transceiver.setHeaderExtensionsToNegotiate(capabilities); + + await negotiate(pc1, pc2); + const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); + + assert_equals(capabilities.length, negotiated_capabilites.length); +}, `The set of negotiated extensions has the same size as the set of extensions to negotiate`); + +promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + + // Disable a non-mandatory extension before first negotiation. + const transceiver = pc1.addTransceiver('video'); + const capabilities = transceiver.getHeaderExtensionsToNegotiate(); + const selected_capability = capabilities.find((capability) => { + return capability.direction === 'sendrecv' && + capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'; + }); + selected_capability.direction = 'stopped'; + transceiver.setHeaderExtensionsToNegotiate(capabilities); + + await negotiate(pc1, pc2); + const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); + + // Attempt enabling the extension. + selected_capability.direction = 'sendrecv'; + + // The enabled extension should not be part of the negotiated set. + transceiver.setHeaderExtensionsToNegotiate(capabilities); + await negotiate(pc1, pc2); + assert_not_equals( + transceiver.getNegotiatedHeaderExtensions().find(capability => { + return capability.uri === selected_capability.uri && + capability.direction === 'sendrecv'; + }), undefined); +}, `Header extensions can be reactivated in subsequent offers`); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const t1 = pc.addTransceiver('video'); + const t2 = pc.addTransceiver('video'); + const extensionUri = 'urn:3gpp:video-orientation'; + + assert_true(!!t1.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri)); + const ext1 = t1.getHeaderExtensionsToNegotiate(); + ext1.find(ext => ext.uri === extensionUri).direction = 'stopped'; + t1.setHeaderExtensionsToNegotiate(ext1); + + assert_true(!!t2.getHeaderExtensionsToNegotiate().find(ext => ext.uri === extensionUri)); + const ext2 = t2.getHeaderExtensionsToNegotiate(); + ext2.find(ext => ext.uri === extensionUri).direction = 'sendrecv'; + t2.setHeaderExtensionsToNegotiate(ext2); + + const offer = await pc.createOffer(); + const sections = SDPUtils.splitSections(offer.sdp); + sections.shift(); + const extensions = sections.map(section => { + return SDPUtils.matchPrefix(section, 'a=extmap:') + .map(SDPUtils.parseExtmap); + }); + assert_equals(extensions.length, 2); + assert_false(!!extensions[0].find(extension => extension.uri === extensionUri)); + assert_true(!!extensions[1].find(extension => extension.uri === extensionUri)); +}, 'Header extensions can be deactivated on a per-mline basis'); + +promise_test(async t => { + const pc1 = new RTCPeerConnection(); + t.add_cleanup(() => pc1.close()); + const pc2 = new RTCPeerConnection(); + t.add_cleanup(() => pc2.close()); + + const t1 = pc1.addTransceiver('video'); + + await pc1.setLocalDescription(); + await pc2.setRemoteDescription(pc1.localDescription); + // Get the transceiver after it is created by SRD. + const t2 = pc2.getTransceivers()[0]; + const t2_capabilities = t2.getHeaderExtensionsToNegotiate(); + const t2_capability_to_stop = t2_capabilities + .find(capability => capability.uri !== 'urn:ietf:params:rtp-hdrext:sdes:mid'); + assert_not_equals(undefined, t2_capability_to_stop); + t2_capability_to_stop.direction = 'stopped'; + t2.setHeaderExtensionsToNegotiate(t2_capabilities); + + await pc2.setLocalDescription(); + await pc1.setRemoteDescription(pc2.localDescription); + + const t1_negotiated = t1.getNegotiatedHeaderExtensions() + .find(extension => extension.uri === t2_capability_to_stop.uri); + assert_not_equals(undefined, t1_negotiated); + assert_equals(t1_negotiated.direction, 'stopped'); + const t1_capability = t1.getHeaderExtensionsToNegotiate() + .find(extension => extension.uri === t2_capability_to_stop.uri); + assert_not_equals(undefined, t1_capability); + assert_equals(t1_capability.direction, 'sendrecv'); +}, 'Extensions not negotiated by the peer are `stopped` in getNegotiatedHeaderExtensions'); + +promise_test(async t => { + const pc = new RTCPeerConnection(); + t.add_cleanup(() => pc.close()); + + const transceiver = pc.addTransceiver('video'); + const negotiated_capabilites = transceiver.getNegotiatedHeaderExtensions(); + assert_equals(negotiated_capabilites.length, + transceiver.getHeaderExtensionsToNegotiate().length); + for (const capability of negotiated_capabilites) { + assert_equals(capability.direction, 'stopped'); + } +}, 'Prior to negotiation, getNegotiatedHeaderExtensions() returns `stopped` for all extensions.'); + +</script> diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html new file mode 100644 index 0000000000..625fee4fe1 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.https.html @@ -0,0 +1,83 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script> + <script> +async function createConnections(test, firstConnectionCallback, secondConnectionCallback) +{ + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + test.add_cleanup(() => pc1.close()); + test.add_cleanup(() => pc2.close()); + + pc1.onicecandidate = (e) => pc2.addIceCandidate(e.candidate); + pc2.onicecandidate = (e) => pc1.addIceCandidate(e.candidate); + + firstConnectionCallback(pc1); + + const offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(offer); + + secondConnectionCallback(pc2); + + const answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(answer); +} + +async function waitForMessage(receiver, data) +{ + while (true) { + const received = await new Promise(resolve => receiver.onmessage = (event) => resolve(event.data)); + if (data === received) + return; + } +} + +promise_test(async (test) => { + let frame; + const scope = 'resources/'; + const script = 'transfer-datachannel-service-worker.js'; + + await service_worker_unregister(test, scope); + const registration = await navigator.serviceWorker.register(script, {scope}); + test.add_cleanup(async () => { + return service_worker_unregister(test, scope); + }); + const worker = registration.installing; + + const messageChannel = new MessageChannel(); + + let localChannel; + let remoteChannel; + + await new Promise((resolve, reject) => { + createConnections(test, (firstConnection) => { + localChannel = firstConnection.createDataChannel('sendDataChannel'); + worker.postMessage({channel: localChannel, port: messageChannel.port2}, [localChannel, messageChannel.port2]); + }, (secondConnection) => { + secondConnection.ondatachannel = (event) => { + remoteChannel = event.channel; + remoteChannel.onopen = resolve; + }; + }); + }); + + const promise = waitForMessage(messageChannel.port1, "OK"); + remoteChannel.send("OK"); + await promise; + + const data = new Promise(resolve => remoteChannel.onmessage = (event) => resolve(event.data)); + messageChannel.port1.postMessage({message: "OK2"}); + assert_equals(await data, "OK2"); +}, "offerer data channel in service worker"); + </script> + </body> +</html> diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js new file mode 100644 index 0000000000..c1919d0b9a --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-service-worker.js @@ -0,0 +1,15 @@ +let channel; +let port; +onmessage = (e) => { + if (e.data.port) { + port = e.data.port; + port.onmessage = (event) => channel.send(event.data.message); + } + if (e.data.channel) { + channel = e.data.channel; + channel.onopen = () => port.postMessage("opened"); + channel.onerror = () => port.postMessage("errored"); + channel.onclose = () => port.postMessage("closed"); + channel.onmessage = (event) => port.postMessage(event.data); + } +}; diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js new file mode 100644 index 0000000000..10d71f68f0 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel-worker.js @@ -0,0 +1,19 @@ +let channel; +onmessage = (event) => { + if (event.data.channel) { + channel = event.data.channel; + channel.onopen = () => self.postMessage("opened"); + channel.onerror = () => self.postMessage("errored"); + channel.onclose = () => self.postMessage("closed"); + channel.onmessage = event => self.postMessage(event.data); + } + if (event.data.message) { + if (channel) + channel.send(event.data.message); + } + if (event.data.close) { + if (channel) + channel.close(); + } +}; +self.postMessage("registered"); diff --git a/testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html new file mode 100644 index 0000000000..9759a67a24 --- /dev/null +++ b/testing/web-platform/tests/webrtc-extensions/transfer-datachannel.html @@ -0,0 +1,165 @@ +<!doctype html> +<html> + <head> + <meta charset="utf-8"> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + </head> + <body> + <script src="../service-workers/service-worker/resources/test-helpers.sub.js"></script> + <script> +async function createConnections(test, firstConnectionCallback, secondConnectionCallback) +{ + const pc1 = new RTCPeerConnection(); + const pc2 = new RTCPeerConnection(); + + test.add_cleanup(() => pc1.close()); + test.add_cleanup(() => pc2.close()); + + pc1.onicecandidate = (e) => pc2.addIceCandidate(e.candidate); + pc2.onicecandidate = (e) => pc1.addIceCandidate(e.candidate); + + firstConnectionCallback(pc1); + + const offer = await pc1.createOffer(); + await pc1.setLocalDescription(offer); + await pc2.setRemoteDescription(offer); + + secondConnectionCallback(pc2); + + const answer = await pc2.createAnswer(); + await pc2.setLocalDescription(answer); + await pc1.setRemoteDescription(answer); +} + +async function waitForMessage(receiver, data) +{ + while (true) { + const received = await new Promise(resolve => receiver.onmessage = (event) => resolve(event.data)); + if (data === received) + return; + } +} + +promise_test(async (test) => { + let localChannel; + let remoteChannel; + + const worker = new Worker('transfer-datachannel-worker.js'); + let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + assert_equals(await data, "registered"); + + await new Promise((resolve, reject) => { + createConnections(test, (firstConnection) => { + localChannel = firstConnection.createDataChannel('sendDataChannel'); + worker.postMessage({channel: localChannel}, [localChannel]); + data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + }, (secondConnection) => { + secondConnection.ondatachannel = (event) => { + remoteChannel = event.channel; + remoteChannel.onopen = resolve; + }; + }); + }); + + assert_equals(await data, "opened"); + + data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + remoteChannel.send("OK"); + assert_equals(await data, "OK"); + + data = new Promise(resolve => remoteChannel.onmessage = (event) => resolve(event.data)); + worker.postMessage({message: "OK2"}); + assert_equals(await data, "OK2"); +}, "offerer data channel in workers"); + + +promise_test(async (test) => { + let localChannel; + let remoteChannel; + + const worker = new Worker('transfer-datachannel-worker.js'); + let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + assert_equals(await data, "registered"); + + data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + await new Promise((resolve, reject) => { + createConnections(test, (firstConnection) => { + localChannel = firstConnection.createDataChannel('sendDataChannel'); + localChannel.onopen = resolve; + }, (secondConnection) => { + secondConnection.ondatachannel = (event) => { + remoteChannel = event.channel; + worker.postMessage({channel: remoteChannel}, [remoteChannel]); + }; + }); + }); + assert_equals(await data, "opened"); + + data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + localChannel.send("OK"); + assert_equals(await data, "OK"); + + data = new Promise(resolve => localChannel.onmessage = (event) => resolve(event.data)); + worker.postMessage({message: "OK2"}); + assert_equals(await data, "OK2"); +}, "answerer data channel in workers"); + +promise_test(async (test) => { + let localChannel; + let remoteChannel; + + const worker = new Worker('transfer-datachannel-worker.js'); + let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + assert_equals(await data, "registered"); + + data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + await new Promise((resolve, reject) => { + createConnections(test, (firstConnection) => { + localChannel = firstConnection.createDataChannel('sendDataChannel'); + worker.postMessage({channel: localChannel}, [localChannel]); + + }, (secondConnection) => { + secondConnection.ondatachannel = (event) => { + remoteChannel = event.channel; + remoteChannel.onopen = resolve; + }; + }); + }); + assert_equals(await data, "opened"); + + data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + remoteChannel.close(); + assert_equals(await data, "closed"); + +}, "data channel close event in worker"); + +promise_test(async (test) => { + let localChannel; + let remoteChannel; + + const worker = new Worker('transfer-datachannel-worker.js'); + let data = new Promise(resolve => worker.onmessage = (event) => resolve(event.data)); + assert_equals(await data, "registered"); + + await new Promise((resolve, reject) => { + createConnections(test, (firstConnection) => { + localChannel = firstConnection.createDataChannel('sendDataChannel'); + }, (secondConnection) => { + secondConnection.ondatachannel = (event) => { + remoteChannel = event.channel; + test.step_timeout(() => { + try { + worker.postMessage({channel: remoteChannel}, [remoteChannel]); + reject("postMessage ok"); + } catch(e) { + resolve(); + } + }, 0); + }; + }); + }); +}, "Failing to transfer a data channel"); + </script> + </body> +</html> |