392 lines
14 KiB
HTML
392 lines
14 KiB
HTML
<!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>
|