RTCRtpEncodingParameters-scaleResolutionDownTo.https.html (14065B)
1 <!DOCTYPE html> 2 <meta charset="utf-8"> 3 <title>RTCRtpEncodingParameters scaleResolutionDownTo attribute</title> 4 <meta name="timeout" content="long"> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="../webrtc/simulcast/simulcast.js"></script> 8 <script src="../webrtc/third_party/sdp/sdp.js"></script> 9 <script> 10 'use strict'; 11 12 // Creates a track that can be resized with `track.resize(w,h)`. 13 function createResizableTrack(width, height) { 14 // Draw to a canvas with a 30 fps interval. 15 const canvas = Object.assign( 16 document.createElement('canvas'), {width, height}); 17 const ctx = canvas.getContext('2d'); 18 ctx.fillStyle = 'rgb(255,0,0)'; 19 const interval = setInterval(() => { 20 ctx.fillRect(0, 0, canvas.width, canvas.height); 21 }, 1000 / 30); 22 // Capture the canvas and add/modify reize() and stop() methods. 23 const stream = canvas.captureStream(); 24 const [track] = stream.getTracks(); 25 track.resize = (w, h) => { 26 canvas.width = w; 27 canvas.height = h; 28 }; 29 const nativeStop = track.stop; 30 track.stop = () => { 31 clearInterval(interval); 32 nativeStop.apply(track); 33 }; 34 return track; 35 } 36 37 async function getLastEncodedResolution(pc, rid = null) { 38 const report = await pc.getStats(); 39 for (const stats of report.values()) { 40 if (stats.type != 'outbound-rtp') { 41 continue; 42 } 43 if (rid && stats.rid != rid) { 44 continue; 45 } 46 if (!stats.frameWidth || !stats.frameWidth) { 47 // The resolution is missing until the first frame has been encoded. 48 break; 49 } 50 return { width: stats.frameWidth, height: stats.frameHeight }; 51 } 52 return { width: null, height: null }; 53 } 54 55 async function waitForFrameWithResolution(t, pc, width, height, rid = null) { 56 let resolution; 57 do { 58 resolution = await getLastEncodedResolution(pc, rid); 59 await new Promise(r => t.step_timeout(r, 50)); 60 } while (resolution.width != width || resolution.height != height); 61 } 62 63 promise_test(async t => { 64 const pc = new RTCPeerConnection(); 65 t.add_cleanup(() => pc.close()); 66 67 const track = createResizableTrack(120, 60); 68 t.add_cleanup(() => track.stop()); 69 assert_throws_dom('OperationError', () => { 70 pc.addTransceiver(track, { 71 sendEncodings:[{ 72 scaleResolutionDownTo: { maxWidth: 0, maxHeight: 0 } 73 }] 74 }); 75 }); 76 }, `addTransceiver: Invalid scaling throws`); 77 78 promise_test(async t => { 79 const pc = new RTCPeerConnection(); 80 t.add_cleanup(() => pc.close()); 81 82 const track = createResizableTrack(120, 60); 83 t.add_cleanup(() => track.stop()); 84 const {sender} = pc.addTransceiver(track, {sendEncodings:[{}]}); 85 86 const params = sender.getParameters(); 87 params.encodings[0].scaleResolutionDownTo = { maxWidth: 0, maxHeight: 0 }; 88 const p = sender.setParameters(params); 89 90 promise_rejects_dom(t, 'InvalidModificationError', p); 91 }, `setParameters: Invalid scaling throws`); 92 93 promise_test(async t => { 94 const pc = new RTCPeerConnection(); 95 t.add_cleanup(() => pc.close()); 96 97 const track = createResizableTrack(120, 60); 98 t.add_cleanup(() => track.stop()); 99 assert_throws_dom('OperationError', () => { 100 pc.addTransceiver(track, { 101 sendEncodings:[{ 102 scaleResolutionDownTo: undefined, 103 }, { 104 scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 } 105 }] 106 }); 107 }); 108 }, `addTransceiver: Specifying scaling on a subset of active encodings throws`); 109 110 promise_test(async t => { 111 const pc = new RTCPeerConnection(); 112 t.add_cleanup(() => pc.close()); 113 114 const track = createResizableTrack(120, 60); 115 t.add_cleanup(() => track.stop()); 116 pc.addTransceiver(track, { 117 sendEncodings:[{ 118 active: true, 119 scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 }, 120 }, { 121 active: false, 122 scaleResolutionDownTo: undefined 123 }] 124 }); 125 }, `addTransceiver: Specifying scaling on inactive encodings is optional`); 126 127 promise_test(async t => { 128 const pc = new RTCPeerConnection(); 129 t.add_cleanup(() => pc.close()); 130 131 const track = createResizableTrack(120, 60); 132 t.add_cleanup(() => track.stop()); 133 const {sender} = pc.addTransceiver(track, {sendEncodings:[{},{}]}); 134 135 const params = sender.getParameters(); 136 params.encodings[0].scaleResolutionDownTo = undefined; 137 params.encodings[1].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 }; 138 const p = sender.setParameters(params); 139 140 promise_rejects_dom(t, 'InvalidModificationError', p); 141 }, `setParameters: Specifying scaling on a subset of active encodings throws`); 142 143 promise_test(async t => { 144 const pc = new RTCPeerConnection(); 145 t.add_cleanup(() => pc.close()); 146 147 const track = createResizableTrack(120, 60); 148 t.add_cleanup(() => track.stop()); 149 const {sender} = pc.addTransceiver(track, {sendEncodings:[{},{}]}); 150 151 const params = sender.getParameters(); 152 params.encodings[0].active = true; 153 params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 }; 154 params.encodings[1].active = false; 155 params.encodings[1].scaleResolutionDownTo = undefined; 156 await sender.setParameters(params); 157 }, `setParameters: Specifying scaling on inactive encodings is optional`); 158 159 promise_test(async t => { 160 const pc1 = new RTCPeerConnection(); 161 t.add_cleanup(() => pc1.close()); 162 const pc2 = new RTCPeerConnection(); 163 t.add_cleanup(() => pc2.close()); 164 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 165 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 166 167 const track = createResizableTrack(120, 60); 168 t.add_cleanup(() => track.stop()); 169 170 let {sender} = pc1.addTransceiver(track, { 171 sendEncodings:[{ 172 scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 } 173 }] 174 }); 175 176 // Get the initial value set. 177 let params = sender.getParameters(); 178 assert_equals(params.encodings[0].scaleResolutionDownTo?.maxWidth, 120); 179 assert_equals(params.encodings[0].scaleResolutionDownTo?.maxHeight, 60); 180 // Set parameters and get the result. 181 params.encodings[0].scaleResolutionDownTo = { maxWidth: 60, maxHeight: 30 }; 182 await sender.setParameters(params); 183 params = sender.getParameters(); 184 assert_equals(params.encodings[0].scaleResolutionDownTo?.maxWidth, 60); 185 assert_equals(params.encodings[0].scaleResolutionDownTo?.maxHeight, 30); 186 }, `getParameters reflect the current scaleResolutionDownTo`); 187 188 promise_test(async t => { 189 const pc1 = new RTCPeerConnection(); 190 t.add_cleanup(() => pc1.close()); 191 const pc2 = new RTCPeerConnection(); 192 t.add_cleanup(() => pc2.close()); 193 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 194 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 195 196 const track = createResizableTrack(120, 60); 197 t.add_cleanup(() => track.stop()); 198 199 pc1.addTransceiver(track, { 200 sendEncodings:[{ 201 scaleResolutionDownBy: 2.0, 202 scaleResolutionDownTo: { maxWidth: 120, maxHeight: 60 } 203 }] 204 }); 205 206 await pc1.setLocalDescription(); 207 await pc2.setRemoteDescription(pc1.localDescription); 208 await pc2.setLocalDescription(); 209 await pc1.setRemoteDescription(pc2.localDescription); 210 211 await waitForFrameWithResolution(t, pc1, 120, 60); 212 }, `addTransceiver: scaleResolutionDownBy is ignored when ` + 213 `scaleResolutionDownTo is specified`); 214 215 promise_test(async t => { 216 const pc1 = new RTCPeerConnection(); 217 t.add_cleanup(() => pc1.close()); 218 const pc2 = new RTCPeerConnection(); 219 t.add_cleanup(() => pc2.close()); 220 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 221 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 222 223 const track = createResizableTrack(120, 60); 224 t.add_cleanup(() => track.stop()); 225 const {sender} = pc1.addTransceiver(track); 226 227 const params = sender.getParameters(); 228 params.encodings[0].scaleResolutionDownBy = 2.0; 229 params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 }; 230 const p = sender.setParameters(params); 231 232 await pc1.setLocalDescription(); 233 await pc2.setRemoteDescription(pc1.localDescription); 234 await pc2.setLocalDescription(); 235 await pc1.setRemoteDescription(pc2.localDescription); 236 237 await waitForFrameWithResolution(t, pc1, 120, 60); 238 }, `setParameters: scaleResolutionDownBy is ignored when ` + 239 `scaleResolutionDownTo is specified`); 240 241 promise_test(async t => { 242 const pc1 = new RTCPeerConnection(); 243 t.add_cleanup(() => pc1.close()); 244 const pc2 = new RTCPeerConnection(); 245 t.add_cleanup(() => pc2.close()); 246 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 247 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 248 249 const track = createResizableTrack(120, 60); 250 t.add_cleanup(() => track.stop()); 251 const {sender} = pc1.addTransceiver(track, { 252 sendEncodings: [{ 253 scaleResolutionDownTo: { maxWidth: 60, maxHeight: 30 } 254 }] 255 }); 256 257 await pc1.setLocalDescription(); 258 await pc2.setRemoteDescription(pc1.localDescription); 259 await pc2.setLocalDescription(); 260 await pc1.setRemoteDescription(pc2.localDescription); 261 262 await waitForFrameWithResolution(t, pc1, 60, 30); 263 }, `addTransceiver: scaleResolutionDownTo with half resolution`); 264 265 promise_test(async t => { 266 const pc1 = new RTCPeerConnection(); 267 t.add_cleanup(() => pc1.close()); 268 const pc2 = new RTCPeerConnection(); 269 t.add_cleanup(() => pc2.close()); 270 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 271 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 272 273 const track = createResizableTrack(120, 60); 274 t.add_cleanup(() => track.stop()); 275 const {sender} = pc1.addTransceiver(track); 276 277 await pc1.setLocalDescription(); 278 await pc2.setRemoteDescription(pc1.localDescription); 279 await pc2.setLocalDescription(); 280 await pc1.setRemoteDescription(pc2.localDescription); 281 282 // Request full resolution. 283 let params = sender.getParameters(); 284 params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 }; 285 await sender.setParameters(params); 286 await waitForFrameWithResolution(t, pc1, 120, 60); 287 288 // Request half resolution. 289 params = sender.getParameters(); 290 params.encodings[0].scaleResolutionDownTo = { maxWidth: 60, maxHeight: 30 }; 291 await sender.setParameters(params); 292 await waitForFrameWithResolution(t, pc1, 60, 30); 293 294 // Request full resolution again. 295 params = sender.getParameters(); 296 params.encodings[0].scaleResolutionDownTo = { maxWidth: 120, maxHeight: 60 }; 297 await sender.setParameters(params); 298 await waitForFrameWithResolution(t, pc1, 120, 60); 299 }, `setParameters: Modify scaleResolutionDownTo while sending`); 300 301 promise_test(async t => { 302 const pc1 = new RTCPeerConnection(); 303 t.add_cleanup(() => pc1.close()); 304 const pc2 = new RTCPeerConnection(); 305 t.add_cleanup(() => pc2.close()); 306 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 307 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 308 309 const track = createResizableTrack(80, 40); 310 t.add_cleanup(() => track.stop()); 311 const {sender} = pc1.addTransceiver(track); 312 313 await pc1.setLocalDescription(); 314 await pc2.setRemoteDescription(pc1.localDescription); 315 await pc2.setLocalDescription(); 316 await pc1.setRemoteDescription(pc2.localDescription); 317 318 // scaleTo is portrait, track is landscape, but no scaling should happen due 319 // to orientation agnosticism. 320 let params = sender.getParameters(); 321 params.encodings[0].scaleResolutionDownTo = { maxWidth: 40, maxHeight: 80 }; 322 await sender.setParameters(params); 323 await waitForFrameWithResolution(t, pc1, 80, 40); 324 325 // Change orientation of the track: still no downscale, but encoder begins to 326 // produce the new orientation. 327 track.resize(40, 80); 328 await waitForFrameWithResolution(t, pc1, 40, 80); 329 330 // scaleTo in landscape, reducing to half size. Verify track, which is 331 // portrait, is scaled down by 2. 332 params = sender.getParameters(); 333 params.encodings[0].scaleResolutionDownTo = { maxWidth: 40, maxHeight: 20 }; 334 await sender.setParameters(params); 335 await waitForFrameWithResolution(t, pc1, 20, 40); 336 }, `scaleResolutionDownTo is orientation agnostic`); 337 338 promise_test(async t => { 339 const pc1 = new RTCPeerConnection(); 340 t.add_cleanup(() => pc1.close()); 341 const pc2 = new RTCPeerConnection(); 342 t.add_cleanup(() => pc2.close()); 343 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 344 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 345 346 const track = createResizableTrack(120, 60); 347 t.add_cleanup(() => track.stop()); 348 const {sender} = pc1.addTransceiver(track); 349 350 await pc1.setLocalDescription(); 351 await pc2.setRemoteDescription(pc1.localDescription); 352 await pc2.setLocalDescription(); 353 await pc1.setRemoteDescription(pc2.localDescription); 354 355 // Restrict to 60x60. This results in 60x30 due to maintaining aspect ratio. 356 let params = sender.getParameters(); 357 params.encodings[0].scaleResolutionDownTo = { maxWidth: 60, maxHeight: 60 }; 358 await sender.setParameters(params); 359 await waitForFrameWithResolution(t, pc1, 60, 30); 360 }, `scaleResolutionDownTo does not change aspect ratio`); 361 362 promise_test(async t => { 363 const pc1 = new RTCPeerConnection(); 364 t.add_cleanup(() => pc1.close()); 365 const pc2 = new RTCPeerConnection(); 366 t.add_cleanup(() => pc2.close()); 367 pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate); 368 pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate); 369 370 const track = createResizableTrack(160, 80); 371 t.add_cleanup(() => track.stop()); 372 const {sender} = pc1.addTransceiver(track, { 373 sendEncodings: [{ 374 rid: '0', 375 scaleResolutionDownTo: { maxWidth: 40, maxHeight: 20 } 376 }, { 377 rid: '1', 378 scaleResolutionDownTo: { maxWidth: 80, maxHeight: 40 } 379 }, { 380 rid: '2', 381 scaleResolutionDownTo: { maxWidth: 160, maxHeight: 80 } 382 }] 383 }); 384 385 // Negotiate with simulcast tweaks. 386 await doOfferToSendSimulcastAndAnswer(pc1, pc2, ['0', '1', '2']); 387 388 await waitForFrameWithResolution(t, pc1, 40, 20, '0'); 389 await waitForFrameWithResolution(t, pc1, 80, 40, '1'); 390 await waitForFrameWithResolution(t, pc1, 160, 80, '2'); 391 }, `scaleResolutionDownTo with simulcast`); 392 </script>