'use strict' function peer(other, polite, fail = null) { const send = (tgt, msg) => tgt.postMessage(JSON.parse(JSON.stringify(msg)), "*"); if (!fail) fail = e => send(window.parent, {error: `${e.name}: ${e.message}`}); const pc = new RTCPeerConnection(); if (!window.assert_equals) { window.assert_equals = (a, b, msg) => a === b || fail(new Error(`${msg} expected ${b} but got ${a}`)); } const commands = { async addTransceiver() { const transceiver = pc.addTransceiver("video"); await new Promise(r => pc.addEventListener("negotiated", r, {once: true})); if (!transceiver.currentDirection) { // Might have just missed the negotiation train. Catch next one. await new Promise(r => pc.addEventListener("negotiated", r, {once: true})); } assert_equals(transceiver.currentDirection, "sendonly", "have direction"); return pc.getTransceivers().length; }, async simpleConnect() { const p = commands.addTransceiver(); await new Promise(r => pc.oniceconnectionstatechange = () => pc.iceConnectionState == "connected" && r()); return await p; }, async getNumTransceivers() { return pc.getTransceivers().length; }, }; try { pc.addEventListener("icecandidate", ({candidate}) => send(other, {candidate})); let makingOffer = false, ignoreIceCandidateFailures = false; let srdAnswerPending = false; pc.addEventListener("negotiationneeded", async () => { try { assert_equals(pc.signalingState, "stable", "negotiationneeded always fires in stable state"); assert_equals(makingOffer, false, "negotiationneeded not already in progress"); makingOffer = true; await pc.setLocalDescription(); assert_equals(pc.signalingState, "have-local-offer", "negotiationneeded not racing with onmessage"); assert_equals(pc.localDescription.type, "offer", "negotiationneeded SLD worked"); send(other, {description: pc.localDescription}); } catch (e) { fail(e); } finally { makingOffer = false; } }); window.onmessage = async ({data: {description, candidate, run}}) => { try { if (description) { // If we have a setRemoteDescription() answer operation pending, then // we will be "stable" by the time the next setRemoteDescription() is // executed, so we count this being stable when deciding whether to // ignore the offer. let isStable = pc.signalingState == "stable" || (pc.signalingState == "have-local-offer" && srdAnswerPending); const ignoreOffer = description.type == "offer" && !polite && (makingOffer || !isStable); if (ignoreOffer) { ignoreIceCandidateFailures = true; return; } if (description.type == "answer") srdAnswerPending = true; await pc.setRemoteDescription(description); ignoreIceCandidateFailures = false; srdAnswerPending = false; if (description.type == "offer") { assert_equals(pc.signalingState, "have-remote-offer", "Remote offer"); assert_equals(pc.remoteDescription.type, "offer", "SRD worked"); await pc.setLocalDescription(); assert_equals(pc.signalingState, "stable", "onmessage not racing with negotiationneeded"); assert_equals(pc.localDescription.type, "answer", "onmessage SLD worked"); send(other, {description: pc.localDescription}); } else { assert_equals(pc.remoteDescription.type, "answer", "Answer was set"); assert_equals(pc.signalingState, "stable", "answered"); pc.dispatchEvent(new Event("negotiated")); } } else if (candidate) { try { await pc.addIceCandidate(candidate); } catch (e) { if (!ignoreIceCandidateFailures) throw e; } } else if (run) { send(window.parent, {[run.id]: await commands[run.cmd]() || 0}); } } catch (e) { fail(e); } }; } catch (e) { fail(e); } return pc; } async function setupPeerIframe(t, polite) { const iframe = document.createElement("iframe"); t.add_cleanup(() => iframe.remove()); iframe.srcdoc = `(${peer.toString()})(window.parent, ${polite});`; document.documentElement.appendChild(iframe); const failCatcher = t.step_func(({data}) => ("error" in data) && assert_unreached(`Error in iframe: ${data.error}`)); window.addEventListener("message", failCatcher); t.add_cleanup(() => window.removeEventListener("message", failCatcher)); await new Promise(r => iframe.onload = r); return iframe; } function setupPeerTopLevel(t, other, polite) { const pc = peer(other, polite, t.step_func(e => { throw e; })); t.add_cleanup(() => { pc.close(); window.onmessage = null; }); } let counter = 0; async function run(target, cmd) { const id = `result${counter++}`; target.postMessage({run: {cmd, id}}, "*"); return new Promise(r => window.addEventListener("message", function listen({data}) { if (!(id in data)) return; window.removeEventListener("message", listen); r(data[id]); })); } let iframe; async function setupAB(t, politeA, politeB) { iframe = await setupPeerIframe(t, politeB); return setupPeerTopLevel(t, iframe.contentWindow, politeA); } const runA = cmd => run(window, cmd); const runB = cmd => run(iframe.contentWindow, cmd); const runBoth = (cmdA, cmdB = cmdA) => Promise.all([runA(cmdA), runB(cmdB)]); async function promise_test_both_roles(f, name) { promise_test(async t => f(t, await setupAB(t, true, false)), name); promise_test(async t => f(t, await setupAB(t, false, true)), `${name} with roles reversed`); }