diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/webrtc/tests/mochitests/simulcast.js | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/dom/media/webrtc/tests/mochitests/simulcast.js b/dom/media/webrtc/tests/mochitests/simulcast.js new file mode 100644 index 0000000000..0af36478c4 --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/simulcast.js @@ -0,0 +1,232 @@ +"use strict"; +/* Helper functions to munge SDP and split the sending track into + * separate tracks on the receiving end. This can be done in a number + * of ways, the one used here uses the fact that the MID and RID header + * extensions which are used for packet routing share the same wire + * format. The receiver interprets the rids from the sender as mids + * which allows receiving the different spatial resolutions on separate + * m-lines and tracks. + */ + +// Borrowed from wpt, with some dependencies removed. + +const ridExtensions = [ + "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", + "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", +]; + +function ridToMid(description, rids) { + const sections = SDPUtils.splitSections(description.sdp); + const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); + const ice = SDPUtils.getIceParameters(sections[1], sections[0]); + const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); + const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; + const directionValue = + description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || + "a=sendrecv"; + const mline = SDPUtils.parseMLine(sections[1]); + + // Skip mid extension; we are replacing it with the rid extmap + rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( + ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid" + ); + + for (const ext of rtpParameters.headerExtensions) { + if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") { + ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid"; + } + } + + // Filter rtx as we have no way to (re)interpret rrid. + // Not doing this makes probing use RTX, it's not understood and ramp-up is slower. + rtpParameters.codecs = rtpParameters.codecs.filter( + c => c.name.toUpperCase() !== "RTX" + ); + + if (!rids) { + rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map( + r => r[1] + ); + } + + let sdp = + SDPUtils.writeSessionBoilerplate() + + SDPUtils.writeDtlsParameters(dtls, setupValue) + + SDPUtils.writeIceParameters(ice) + + "a=group:BUNDLE " + + rids.join(" ") + + "\r\n"; + const baseRtpDescription = SDPUtils.writeRtpDescription( + mline.kind, + rtpParameters + ); + for (const rid of rids) { + sdp += + baseRtpDescription + + "a=mid:" + + rid + + "\r\n" + + "a=msid:rid-" + + rid + + " rid-" + + rid + + "\r\n"; + sdp += directionValue + "\r\n"; + } + return sdp; +} + +function midToRid(description, localDescription, rids) { + const sections = SDPUtils.splitSections(description.sdp); + const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]); + const ice = SDPUtils.getIceParameters(sections[1], sections[0]); + const rtpParameters = SDPUtils.parseRtpParameters(sections[1]); + const setupValue = description.sdp.match(/a=setup:(.*)/)[1]; + const directionValue = + description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) || + "a=sendrecv"; + const mline = SDPUtils.parseMLine(sections[1]); + + // Skip rid extensions; we are replacing them with the mid extmap + rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter( + ext => !ridExtensions.includes(ext.uri) + ); + + for (const ext of rtpParameters.headerExtensions) { + if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") { + ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; + } + } + + const localMid = localDescription + ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1]) + : "0"; + + if (!rids) { + rids = []; + for (let i = 1; i < sections.length; i++) { + rids.push(SDPUtils.getMid(sections[i])); + } + } + + let sdp = + SDPUtils.writeSessionBoilerplate() + + SDPUtils.writeDtlsParameters(dtls, setupValue) + + SDPUtils.writeIceParameters(ice) + + "a=group:BUNDLE " + + localMid + + "\r\n"; + sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters); + // Although we are converting mids to rids, we still need a mid. + // The first one will be consistent with trickle ICE candidates. + sdp += "a=mid:" + localMid + "\r\n"; + sdp += directionValue + "\r\n"; + + for (const rid of rids) { + const stringrid = String(rid); // allow integers + const choices = stringrid.split(","); + choices.forEach(choice => { + sdp += "a=rid:" + choice + " recv\r\n"; + }); + } + if (rids.length) { + sdp += "a=simulcast:recv " + rids.join(";") + "\r\n"; + } + + return sdp; +} + +async function doOfferToSendSimulcast(offerer, answerer) { + await offerer.setLocalDescription(); + + // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids, + // even if some rids have been removed or reordered. + let mids = []; + if (answerer.localDescription) { + // Renegotiation. Mids must be the same as before, because renegotiation + // can never remove or reorder mids, nor can it expand the simulcast + // envelope. + mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( + e => e[1] + ); + } else { + // First negotiation; the mids will be exactly the same as the rids + const simulcastAttr = offerer.localDescription.sdp.match( + /a=simulcast:send (.*)/ + ); + if (simulcastAttr) { + mids = simulcastAttr[1].split(";"); + } + } + + const nonSimulcastOffer = ridToMid(offerer.localDescription, mids); + await answerer.setRemoteDescription({ + type: "offer", + sdp: nonSimulcastOffer, + }); +} + +async function doAnswerToRecvSimulcast(offerer, answerer, rids) { + await answerer.setLocalDescription(); + const simulcastAnswer = midToRid( + answerer.localDescription, + offerer.localDescription, + rids + ); + await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer }); +} + +async function doOfferToRecvSimulcast(offerer, answerer, rids) { + await offerer.setLocalDescription(); + const simulcastOffer = midToRid( + offerer.localDescription, + answerer.localDescription, + rids + ); + await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer }); +} + +async function doAnswerToSendSimulcast(offerer, answerer) { + await answerer.setLocalDescription(); + + // See which mids the offerer had; it will barf if we remove or reorder them + const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map( + e => e[1] + ); + + const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids); + await offerer.setRemoteDescription({ + type: "answer", + sdp: nonSimulcastAnswer, + }); +} + +async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) { + await doOfferToSendSimulcast(offerer, answerer); + await doAnswerToRecvSimulcast(offerer, answerer, rids); +} + +async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) { + await doOfferToRecvSimulcast(offerer, answerer, rids); + await doAnswerToSendSimulcast(offerer, answerer); +} + +// This would be useful for cases other than simulcast, but we do not use it +// anywhere else right now, nor do we have a place for wpt-friendly helpers at +// the moment. +function createPlaybackElement(track) { + const elem = document.createElement(track.kind); + elem.autoplay = true; + elem.srcObject = new MediaStream([track]); + elem.id = track.id; + return elem; +} + +async function getPlaybackWithLoadedMetadata(track) { + const elem = createPlaybackElement(track); + return new Promise(resolve => { + elem.addEventListener("loadedmetadata", () => { + resolve(elem); + }); + }); +} |