RTCPeerConnection-perfect-negotiation-helper.js (6096B)
1 'use strict' 2 3 function peer(other, polite, fail = null) { 4 const send = (tgt, msg) => tgt.postMessage(JSON.parse(JSON.stringify(msg)), 5 "*"); 6 if (!fail) fail = e => send(window.parent, {error: `${e.name}: ${e.message}`}); 7 const pc = new RTCPeerConnection(); 8 9 if (!window.assert_equals) { 10 window.assert_equals = (a, b, msg) => a === b || 11 fail(new Error(`${msg} expected ${b} but got ${a}`)); 12 } 13 14 const commands = { 15 async addTransceiver() { 16 const transceiver = pc.addTransceiver("video"); 17 await new Promise(r => pc.addEventListener("negotiated", r, {once: true})); 18 if (!transceiver.currentDirection) { 19 // Might have just missed the negotiation train. Catch next one. 20 await new Promise(r => pc.addEventListener("negotiated", r, {once: true})); 21 } 22 assert_equals(transceiver.currentDirection, "sendonly", "have direction"); 23 return pc.getTransceivers().length; 24 }, 25 async simpleConnect() { 26 const p = commands.addTransceiver(); 27 await new Promise(r => pc.oniceconnectionstatechange = 28 () => pc.iceConnectionState == "connected" && r()); 29 return await p; 30 }, 31 async getNumTransceivers() { 32 return pc.getTransceivers().length; 33 }, 34 }; 35 36 try { 37 pc.addEventListener("icecandidate", ({candidate}) => send(other, 38 {candidate})); 39 let makingOffer = false, ignoreIceCandidateFailures = false; 40 let srdAnswerPending = false; 41 pc.addEventListener("negotiationneeded", async () => { 42 try { 43 assert_equals(pc.signalingState, "stable", "negotiationneeded always fires in stable state"); 44 assert_equals(makingOffer, false, "negotiationneeded not already in progress"); 45 makingOffer = true; 46 await pc.setLocalDescription(); 47 assert_equals(pc.signalingState, "have-local-offer", "negotiationneeded not racing with onmessage"); 48 assert_equals(pc.localDescription.type, "offer", "negotiationneeded SLD worked"); 49 send(other, {description: pc.localDescription}); 50 } catch (e) { 51 fail(e); 52 } finally { 53 makingOffer = false; 54 } 55 }); 56 window.onmessage = async ({data: {description, candidate, run}}) => { 57 try { 58 if (description) { 59 // If we have a setRemoteDescription() answer operation pending, then 60 // we will be "stable" by the time the next setRemoteDescription() is 61 // executed, so we count this being stable when deciding whether to 62 // ignore the offer. 63 let isStable = 64 pc.signalingState == "stable" || 65 (pc.signalingState == "have-local-offer" && srdAnswerPending); 66 const ignoreOffer = description.type == "offer" && !polite && 67 (makingOffer || !isStable); 68 if (ignoreOffer) { 69 ignoreIceCandidateFailures = true; 70 return; 71 } 72 if (description.type == "answer") 73 srdAnswerPending = true; 74 await pc.setRemoteDescription(description); 75 ignoreIceCandidateFailures = false; 76 srdAnswerPending = false; 77 if (description.type == "offer") { 78 assert_equals(pc.signalingState, "have-remote-offer", "Remote offer"); 79 assert_equals(pc.remoteDescription.type, "offer", "SRD worked"); 80 await pc.setLocalDescription(); 81 assert_equals(pc.signalingState, "stable", "onmessage not racing with negotiationneeded"); 82 assert_equals(pc.localDescription.type, "answer", "onmessage SLD worked"); 83 send(other, {description: pc.localDescription}); 84 } else { 85 assert_equals(pc.remoteDescription.type, "answer", "Answer was set"); 86 assert_equals(pc.signalingState, "stable", "answered"); 87 pc.dispatchEvent(new Event("negotiated")); 88 } 89 } else if (candidate) { 90 try { 91 await pc.addIceCandidate(candidate); 92 } catch (e) { 93 if (!ignoreIceCandidateFailures) throw e; 94 } 95 } else if (run) { 96 send(window.parent, {[run.id]: await commands[run.cmd]() || 0}); 97 } 98 } catch (e) { 99 fail(e); 100 } 101 }; 102 } catch (e) { 103 fail(e); 104 } 105 return pc; 106 } 107 108 async function setupPeerIframe(t, polite) { 109 const iframe = document.createElement("iframe"); 110 t.add_cleanup(() => iframe.remove()); 111 iframe.srcdoc = 112 `<html\><script\>(${peer.toString()})(window.parent, ${polite});</script\></html\>`; 113 document.documentElement.appendChild(iframe); 114 115 const failCatcher = t.step_func(({data}) => 116 ("error" in data) && assert_unreached(`Error in iframe: ${data.error}`)); 117 window.addEventListener("message", failCatcher); 118 t.add_cleanup(() => window.removeEventListener("message", failCatcher)); 119 await new Promise(r => iframe.onload = r); 120 return iframe; 121 } 122 123 function setupPeerTopLevel(t, other, polite) { 124 const pc = peer(other, polite, t.step_func(e => { throw e; })); 125 t.add_cleanup(() => { pc.close(); window.onmessage = null; }); 126 } 127 128 let counter = 0; 129 async function run(target, cmd) { 130 const id = `result${counter++}`; 131 target.postMessage({run: {cmd, id}}, "*"); 132 return new Promise(r => window.addEventListener("message", 133 function listen({data}) { 134 if (!(id in data)) return; 135 window.removeEventListener("message", listen); 136 r(data[id]); 137 })); 138 } 139 140 let iframe; 141 async function setupAB(t, politeA, politeB) { 142 iframe = await setupPeerIframe(t, politeB); 143 return setupPeerTopLevel(t, iframe.contentWindow, politeA); 144 } 145 const runA = cmd => run(window, cmd); 146 const runB = cmd => run(iframe.contentWindow, cmd); 147 const runBoth = (cmdA, cmdB = cmdA) => Promise.all([runA(cmdA), runB(cmdB)]); 148 149 async function promise_test_both_roles(f, name) { 150 promise_test(async t => f(t, await setupAB(t, true, false)), name); 151 promise_test(async t => f(t, await setupAB(t, false, true)), 152 `${name} with roles reversed`); 153 }