Source code
Revision control
Copy as Markdown
Other Tools
Test Info: Warnings
- This test has a WPT meta file that expects 13 subtest issues.
- This WPT test may be referenced by the following Test IDs:
- /webrtc-extensions/RTCRtpEncodingParameters-scaleResolutionDownTo.https.html - WPT Dashboard Interop Dashboard
<!DOCTYPE html>
<meta charset="utf-8">
<title>RTCRtpEncodingParameters scaleResolutionDownTo attribute</title>
<meta name="timeout" content="long">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../webrtc/simulcast/simulcast.js"></script>
<script src="../webrtc/third_party/sdp/sdp.js"></script>
<script>
'use strict';
// Creates a track that can be resized with `track.resize(w,h)`.
function createResizableTrack(width, height) {
// Draw to a canvas with a 30 fps interval.
const canvas = Object.assign(
document.createElement('canvas'), {width, height});
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'rgb(255,0,0)';
const interval = setInterval(() => {
ctx.fillRect(0, 0, canvas.width, canvas.height);
}, 1000 / 30);
// Capture the canvas and add/modify reize() and stop() methods.
const stream = canvas.captureStream();
const [track] = stream.getTracks();
track.resize = (w, h) => {
canvas.width = w;
canvas.height = h;
};
const nativeStop = track.stop;
track.stop = () => {
clearInterval(interval);
nativeStop.apply(track);
};
return track;
}
async function getLastEncodedResolution(pc, rid = null) {
const report = await pc.getStats();
for (const stats of report.values()) {
if (stats.type != 'outbound-rtp') {
continue;
}
if (rid && stats.rid != rid) {
continue;
}
if (!stats.frameWidth || !stats.frameWidth) {
// The resolution is missing until the first frame has been encoded.
break;
}
return { width: stats.frameWidth, height: stats.frameHeight };
}
return { width: null, height: null };
}
async function waitForFrameWithResolution(t, pc, width, height, rid = null) {
let resolution;
do {
resolution = await getLastEncodedResolution(pc, rid);
await new Promise(r => t.step_timeout(r, 50));
} while (resolution.width != width || resolution.height != height);
}
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
assert_throws_dom('OperationError', () => {
pc.addTransceiver(track, {
sendEncodings:[{
scaleResolutionDownTo: { maxWidth: 0, maxHeight: 0 }
}]
});
});
}, `addTransceiver: Invalid scaling throws`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc.addTransceiver(track, {sendEncodings:[{}]});
const params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 0, maxHeight: 0 };
const p = sender.setParameters(params);
promise_rejects_dom(t, 'InvalidModificationError', p);
}, `setParameters: Invalid scaling throws`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
assert_throws_dom('OperationError', () => {
pc.addTransceiver(track, {
sendEncodings:[{
scaleResolutionDownTo: undefined,
}, {
scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 }
}]
});
});
}, `addTransceiver: Specifying scaling on a subset of active encodings throws`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
pc.addTransceiver(track, {
sendEncodings:[{
active: true,
scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 },
}, {
active: false,
scaleResolutionDownTo: undefined
}]
});
}, `addTransceiver: Specifying scaling on inactive encodings is optional`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc.addTransceiver(track, {sendEncodings:[{},{}]});
const params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = undefined;
params.encodings[1].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 };
const p = sender.setParameters(params);
promise_rejects_dom(t, 'InvalidModificationError', p);
}, `setParameters: Specifying scaling on a subset of active encodings throws`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc.addTransceiver(track, {sendEncodings:[{},{}]});
const params = sender.getParameters();
params.encodings[0].active = true;
params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 };
params.encodings[1].active = false;
params.encodings[1].scaleResolutionDownTo = undefined;
await sender.setParameters(params);
}, `setParameters: Specifying scaling on inactive encodings is optional`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
let {sender} = pc1.addTransceiver(track, {
sendEncodings:[{
scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 }
}]
});
// Get the initial value set.
let params = sender.getParameters();
assert_equals(params.encodings[0].scaleResolutionDownTo?.maxWidth, 120);
assert_equals(params.encodings[0].scaleResolutionDownTo?.maxHeight, 60);
// Set parameters and get the result.
params.encodings[0].scaleResolutionDownTo = { maxWidth: 60, maxHeight: 30 };
await sender.setParameters(params);
params = sender.getParameters();
assert_equals(params.encodings[0].scaleResolutionDownTo?.maxWidth, 60);
assert_equals(params.encodings[0].scaleResolutionDownTo?.maxHeight, 30);
}, `getParameters reflect the current scaleResolutionDownTo`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
pc1.addTransceiver(track, {
sendEncodings:[{
scaleResolutionDownBy: 2.0,
scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 }
}]
});
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await waitForFrameWithResolution(t, pc1, 120, 60);
}, `addTransceiver: scaleResolutionDownBy is ignored when ` +
`scaleResolutionDownTo is specified`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc1.addTransceiver(track);
const params = sender.getParameters();
params.encodings[0].scaleResolutionDownBy = 2.0;
params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 };
const p = sender.setParameters(params);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await waitForFrameWithResolution(t, pc1, 120, 60);
}, `setParameters: scaleResolutionDownBy is ignored when ` +
`scaleResolutionDownTo is specified`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc1.addTransceiver(track, {
sendEncodings: [{
scaleResolutionDownTo: { maxWidth: 60, maxHeight: 30 }
}]
});
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
await waitForFrameWithResolution(t, pc1, 60, 30);
}, `addTransceiver: scaleResolutionDownTo with half resolution`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc1.addTransceiver(track);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
// Request full resolution.
let params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 };
await sender.setParameters(params);
await waitForFrameWithResolution(t, pc1, 120, 60);
// Request half resolution.
params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 60, maxHeight: 30 };
await sender.setParameters(params);
await waitForFrameWithResolution(t, pc1, 60, 30);
// Request full resolution again.
params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 };
await sender.setParameters(params);
await waitForFrameWithResolution(t, pc1, 120, 60);
}, `setParameters: Modify scaleResolutionDownTo while sending`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(80, 40);
t.add_cleanup(() => track.stop());
const {sender} = pc1.addTransceiver(track);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
// scaleTo is portrait, track is landscape, but no scaling should happen due
// to orientation agnosticism.
let params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 40, maxHeight: 80 };
await sender.setParameters(params);
await waitForFrameWithResolution(t, pc1, 80, 40);
// Change orientation of the track: still no downscale, but encoder begins to
// produce the new orientation.
track.resize(40, 80);
await waitForFrameWithResolution(t, pc1, 40, 80);
// scaleTo in landscape, reducing to half size. Verify track, which is
// portrait, is scaled down by 2.
params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 40, maxHeight: 20 };
await sender.setParameters(params);
await waitForFrameWithResolution(t, pc1, 20, 40);
}, `scaleResolutionDownTo is orientation agnostic`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(120, 60);
t.add_cleanup(() => track.stop());
const {sender} = pc1.addTransceiver(track);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
// Restrict to 60x60. This results in 60x30 due to maintaining aspect ratio.
let params = sender.getParameters();
params.encodings[0].scaleResolutionDownTo = { maxWidth: 60, maxHeight: 60 };
await sender.setParameters(params);
await waitForFrameWithResolution(t, pc1, 60, 30);
}, `scaleResolutionDownTo does not change aspect ratio`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
const track = createResizableTrack(160, 80);
t.add_cleanup(() => track.stop());
const {sender} = pc1.addTransceiver(track, {
sendEncodings: [{
rid: '0',
scaleResolutionDownTo: { maxWidth: 40, maxHeight: 20 }
}, {
rid: '1',
scaleResolutionDownTo: { maxWidth: 80, maxHeight: 40 }
}, {
rid: '2',
scaleResolutionDownTo: { maxWidth: 160, maxHeight: 80 }
}]
});
// Negotiate with simulcast tweaks.
await doOfferToSendSimulcastAndAnswer(pc1, pc2, ['0', '1', '2']);
await waitForFrameWithResolution(t, pc1, 40, 20, '0');
await waitForFrameWithResolution(t, pc1, 80, 40, '1');
await waitForFrameWithResolution(t, pc1, 160, 80, '2');
}, `scaleResolutionDownTo with simulcast`);
</script>