| <!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> |