| <!doctype html> |
| <html> |
| <head> |
| <script src="../resources/testharness.js"></script> |
| <script src="../resources/testharnessreport.js"></script> |
| </head> |
| <body> |
| <div> |
| <video id="low" playsinline autoplay width="320"></video> |
| <video id="mid" playsinline autoplay width="320"></video> |
| <video id="high" playsinline autoplay width="320"></video> |
| </div> |
| <script> |
| // Code taken from Chrome/Firefox tests and/or simulcast playground. |
| function splitUnifiedPlanOffer(offer) { |
| let sdpLines = offer.sdp.split("\r\n"); |
| |
| mSectionStart = sdpLines.findIndex(line => line.startsWith("m=")); |
| mSection = sdpLines.splice(mSectionStart); |
| |
| let ssrcs = mSection.filter((line) => { |
| return line.startsWith("a=ssrc"); |
| }); |
| |
| let layerRIDS = mSection.filter(line => line.startsWith("a=simulcast:")).map( |
| line => line.replace("a=simulcast:send ", "").split(";") |
| )[0]; |
| |
| let midExtmapId = mSection.filter(line => line.includes("urn:ietf:params:rtp-hdrext:sdes:mid")).map(line => |
| line.replace("a=extmap:", "").split(" ")[0] |
| )[0]; |
| |
| let ridExtmapId = mSection.filter(line => line.includes("urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id")).map(line => |
| line.replace("a=extmap:", "").split(" ")[0] |
| )[0]; |
| |
| mSection = mSection.filter((line) => { |
| return !line.startsWith("a=ssrc") && !line.startsWith("a=simulcast"); |
| }); |
| |
| sdpLines = sdpLines.map(line => { |
| if (line.startsWith("a=group:BUNDLE")) |
| return "a=group:BUNDLE " + layerRIDS.join(" "); |
| |
| return line; |
| }); |
| |
| let counter = 0; |
| for (let layerName of layerRIDS) { |
| sdpLines = sdpLines.concat(mSection.map(line => { |
| if (line.match(/a=msid:/)) { |
| return "a=msid:" + layerName + " " + layerName; |
| } |
| |
| if (line.startsWith("a=mid:")) |
| return "a=mid:" + layerName; |
| |
| if (line.startsWith("a=extmap:" + midExtmapId + " ")) |
| return "a=extmap:" + midExtmapId + " urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; |
| |
| if (line.startsWith("a=extmap:" + ridExtmapId + " ")) |
| return "a=extmap:" + ridExtmapId + " urn:ietf:params:rtp-hdrext:sdes:mid"; |
| |
| if (line.startsWith("a=rid:") || line.startsWith("a=simulcast:")) |
| return null; |
| |
| return line; |
| })); |
| sdpLines = sdpLines.concat([ssrcs[counter]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 1]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 2]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 3]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 4]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 5]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 6]]); |
| sdpLines = sdpLines.concat([ssrcs[8 * counter + 4 + 7]]); |
| counter = counter + 1; |
| } |
| |
| offer.sdp = sdpLines |
| .filter(line => line && line.length > 0) |
| .join("\r\n") + "\r\n"; |
| } |
| |
| function splitUnifiedPlanAnswer(answer) { |
| let sdpLines = answer.sdp.split("\r\n"); |
| |
| let mSectionStart = sdpLines.findIndex(line => line.startsWith("m=")); |
| let mSection = sdpLines.splice(mSectionStart); |
| |
| // Remove extra m= sections |
| mSectionStart = mSection.slice(1).findIndex(line => line.startsWith("m=")); |
| if (mSectionStart != -1) |
| mSection.splice(mSectionStart); |
| |
| let midExtmapId = mSection.filter(line => line.includes("urn:ietf:params:rtp-hdrext:sdes:mid")).map(line => |
| line.replace("a=extmap:", "").split(" ")[0] |
| )[0]; |
| |
| let ridExtmapId = mSection.filter(line => line.includes("urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id")).map(line => |
| line.replace("a=extmap:", "").split(" ")[0] |
| )[0]; |
| |
| sdpLines = sdpLines.map(line => { |
| if (line.startsWith("a=group:BUNDLE")) |
| return "a=group:BUNDLE 0"; |
| |
| return line; |
| }); |
| |
| mSection = mSection.map(line => { |
| if (line.startsWith("a=mid:")) |
| return "a=mid:0"; |
| |
| if (line.startsWith("a=extmap:" + midExtmapId + " ")) |
| return "a=extmap:" + midExtmapId + " urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; |
| |
| if (line.startsWith("a=extmap:" + ridExtmapId + " ")) |
| return "a=extmap:" + ridExtmapId + " urn:ietf:params:rtp-hdrext:sdes:mid"; |
| |
| return line; |
| }); |
| |
| let params = ["0", "1", "2"]; |
| for(let r in params) { |
| mSection.push("a=rid:" + r + " recv"); |
| } |
| mSection.push("a=simulcast:recv " + params.join(";")); |
| |
| answer.sdp = sdpLines.concat(mSection) |
| .filter(line => line && line.length > 0).join("\r\n") + "\r\n"; |
| } |
| |
| function enableSimulcastThroughSDP(offer) |
| { |
| match = offer.sdp.match(/a=ssrc:(\d+) cname:(.*)\r\n/); |
| msid = offer.sdp.match(/a=ssrc:(\d+) msid:(.*)\r\n/); |
| var lines = offer.sdp.trim().split('\r\n'); |
| var removed = lines.splice(lines.length - 4, 4); |
| var videoSSRC1 = parseInt(match[1]); |
| rtxSSRC1 = offer.sdp.split('\r\n').filter((line) => { return line.startsWith('a=ssrc-group:FID ')})[0].split(' ')[2]; |
| var videoSSRC2 = videoSSRC1 + 1; |
| var rtxSSRC2 = videoSSRC1 + 2; |
| var videoSSRC3 = videoSSRC1 + 3; |
| var rtxSSRC3 = videoSSRC1 + 4; |
| lines.push(removed[0]); |
| lines.push(removed[1]); |
| lines.push('a=ssrc:' + videoSSRC2 + ' cname:' + match[2]); |
| lines.push('a=ssrc:' + videoSSRC2 + ' msid:' + msid[2]); |
| lines.push('a=ssrc:' + rtxSSRC2 + ' cname:' + match[2]); |
| lines.push('a=ssrc:' + rtxSSRC2 + ' msid:' + msid[2]); |
| |
| lines.push('a=ssrc:' + videoSSRC3 + ' cname:' + match[2]); |
| lines.push('a=ssrc:' + videoSSRC3 + ' msid:' + msid[2]); |
| lines.push('a=ssrc:' + rtxSSRC3 + ' cname:' + match[2]); |
| lines.push('a=ssrc:' + rtxSSRC3 + ' msid:' + msid[2]); |
| |
| lines.push('a=ssrc-group:FID ' + videoSSRC2 + ' ' + rtxSSRC2); |
| lines.push('a=ssrc-group:FID ' + videoSSRC3 + ' ' + rtxSSRC3); |
| lines.push('a=ssrc-group:SIM ' + videoSSRC1 + ' ' + videoSSRC2 + ' ' + videoSSRC3); |
| |
| offer.sdp = lines.join('\r\n') + '\r\n'; |
| } |
| |
| function enableSimulcastThroughSDP2(offer) |
| { |
| var lines = offer.sdp.trim().split('\r\n'); |
| |
| lines.push('a=simulcast:send 0;1;2'); |
| |
| offer.sdp = lines.join('\r\n') + '\r\n'; |
| } |
| |
| </script> |
| <script> |
| async function setupCall(pc1, pc2) |
| { |
| let pc1Offer = await pc1.createOffer(); |
| enableSimulcastThroughSDP(pc1Offer); |
| await pc1.setLocalDescription(pc1Offer); |
| |
| let pc2Offer = { |
| type: 'offer', |
| sdp: pc1.localDescription.sdp, |
| }; |
| enableSimulcastThroughSDP2(pc2Offer); |
| |
| splitUnifiedPlanOffer(pc2Offer); |
| await pc2.setRemoteDescription(pc2Offer); |
| |
| let answer = await pc2.createAnswer(); |
| await pc2.setLocalDescription(answer); |
| let pc1Answer = { |
| type: "answer", |
| sdp: pc2.localDescription.sdp, |
| } |
| splitUnifiedPlanAnswer(pc1Answer); |
| |
| await pc1.setRemoteDescription(pc1Answer).then(() => {}, (e) => console.log(e)); |
| } |
| |
| var state; |
| var finished = false; |
| |
| const pc1 = new RTCPeerConnection(); |
| const pc2 = new RTCPeerConnection(); |
| |
| promise_test(async (test) => { |
| if (window.testRunner && testRunner.timeout) { |
| setTimeout(() => { |
| if (!finished) |
| throw "test stucked in state: " + state; |
| }, testRunner.timeout * 0.9); |
| } |
| |
| state = "start"; |
| |
| pc1.onicecandidate = e => { |
| if (e.candidate) { |
| for(let layerIndex in ["0", "1", "2"]) { |
| let newCandidate = new RTCIceCandidate({ |
| candidate: e.candidate.candidate, |
| sdpMid: layerIndex, |
| sdpMLineIndex: layerIndex, |
| usernameFragment: e.candidate.usernameFragment, |
| }); |
| setTimeout(() => pc2.addIceCandidate(newCandidate), 5); |
| } |
| } else |
| setTimeout(() => pc1.addIceCandidate(e.candidate), 5); |
| }; |
| |
| pc2.onicecandidate = e => { |
| if (e.candidate) { |
| let newCandidate = new RTCIceCandidate({ |
| candidate: e.candidate.candidate, |
| sdpMid: "0", //e.candidate.sdpMid, |
| sdpMLineIndex: e.candidate.sdpMLineIndex, |
| usernameFragment: e.candidate.usernameFragment, |
| }); |
| setTimeout(() => pc1.addIceCandidate(newCandidate), 5); |
| } else |
| setTimeout(() => pc1.addIceCandidate(e.candidate), 5); |
| }; |
| |
| let counter = 0; |
| pc2.ontrack = (e) => { |
| if (counter == 0) |
| low.srcObject = new MediaStream([e.track]); |
| else if (counter == 1) |
| mid.srcObject = new MediaStream([e.track]); |
| else |
| high.srcObject = new MediaStream([e.track]); |
| ++counter; |
| } |
| |
| const localStream = await navigator.mediaDevices.getUserMedia({ video: { width: 640, height: 480 } }); |
| pc1.addTrack(localStream.getVideoTracks()[0], localStream); |
| |
| await setupCall(pc1, pc2); |
| |
| await low.play(); |
| state = "video low plays"; |
| |
| assert_equals(low.srcObject.getVideoTracks()[0].getSettings().height, 240); |
| assert_equals(low.srcObject.getVideoTracks()[0].getSettings().width, 320); |
| |
| await mid.play(); |
| state = "video mid plays"; |
| |
| assert_equals(mid.srcObject.getVideoTracks()[0].getSettings().height, 480); |
| assert_equals(mid.srcObject.getVideoTracks()[0].getSettings().width, 640); |
| |
| finished = true; |
| }, "Testing simulcast"); |
| </script> |
| </body> |
| </html> |