diff options
Diffstat (limited to '')
-rw-r--r-- | dom/media/webrtc/tests/mochitests/sdpUtils.js | 398 |
1 files changed, 398 insertions, 0 deletions
diff --git a/dom/media/webrtc/tests/mochitests/sdpUtils.js b/dom/media/webrtc/tests/mochitests/sdpUtils.js new file mode 100644 index 0000000000..51cae10dba --- /dev/null +++ b/dom/media/webrtc/tests/mochitests/sdpUtils.js @@ -0,0 +1,398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var sdputils = { + // Finds the codec id / payload type given a codec format + // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of + // multiple matches. + findCodecId(sdp, format, offset = 0) { + let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi"); + let match; + for (let i = 0; i <= offset; ++i) { + match = regex.exec(sdp); + if (!match) { + throw new Error( + "Couldn't find offset " + + i + + " of codec " + + format + + " while looking for offset " + + offset + + " in sdp:\n" + + sdp + ); + } + } + // match[0] is the full matched string + // match[1] is the first parenthesis group + return match[1]; + }, + + // Returns a list of all payload types, excluding rtx, in an sdp. + getPayloadTypes(sdp) { + const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim; + const pts = []; + for (const [line, pt] of sdp.matchAll(regex)) { + pts.push(pt); + } + return pts; + }, + + // Finds all the extmap ids in the given sdp. Note that this does NOT + // consider m-sections, so a more generic version would need to + // look at each m-section separately. + findExtmapIds(sdp) { + var sdpExtmapIds = []; + extmapRegEx = /^a=extmap:([0-9+])/gm; + // must call exec on the regex to get each match in the string + while ((searchResults = extmapRegEx.exec(sdp)) !== null) { + // returned array has the matched text as the first item, + // and then one item for each capturing parenthesis that + // matched containing the text that was captured. + sdpExtmapIds.push(searchResults[1]); + } + return sdpExtmapIds; + }, + + findExtmapIdsUrnsDirections(sdp) { + var sdpExtmap = []; + extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:\-\/\.]+)/gm; + // must call exec on the regex to get each match in the string + while ((searchResults = extmapRegEx.exec(sdp)) !== null) { + // returned array has the matched text as the first item, + // and then one item for each capturing parenthesis that + // matched containing the text that was captured. + var idUrn = []; + idUrn.push(searchResults[1]); + idUrn.push(searchResults[3]); + idUrn.push(searchResults[2].slice(1)); + sdpExtmap.push(idUrn); + } + return sdpExtmap; + }, + + verify_unique_extmap_ids(sdp) { + const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp); + + return sdpExtmapIds.reduce(function (result, item, index) { + const [id, urn, dir] = item; + ok( + !(id in result) || (result[id][0] === urn && result[id][1] === dir), + "ID " + id + " is unique ID for " + urn + " and direction " + dir + ); + result[id] = [urn, dir]; + return result; + }, {}); + }, + + getMSections(sdp) { + return sdp + .split(new RegExp("^m=", "gm")) + .slice(1) + .map(s => "m=" + s); + }, + + getAudioMSections(sdp) { + return this.getMSections(sdp).filter(section => + section.startsWith("m=audio") + ); + }, + + getVideoMSections(sdp) { + return this.getMSections(sdp).filter(section => + section.startsWith("m=video") + ); + }, + + checkSdpAfterEndOfTrickle(description, testOptions, label) { + info("EOC-SDP: " + JSON.stringify(description)); + + const checkForTransportAttributes = msection => { + info("Checking msection: " + msection); + ok( + msection.includes("a=end-of-candidates"), + label + ": SDP contains end-of-candidates" + ); + + if (!msection.startsWith("m=application")) { + if (testOptions.rtcpmux) { + ok( + msection.includes("a=rtcp-mux"), + label + ": SDP contains rtcp-mux" + ); + } else { + ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port"); + } + } + }; + + const hasOwnTransport = msection => { + const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection); + if (port0Check) { + return false; + } + const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection); + if (!midMatch) { + return true; + } + const mid = midMatch[1]; + const bundleGroupMatch = new RegExp( + "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+" + ).exec(description.sdp); + return bundleGroupMatch == null; + }; + + const msectionsWithOwnTransports = this.getMSections( + description.sdp + ).filter(hasOwnTransport); + + ok( + msectionsWithOwnTransports.length, + "SDP should contain at least one msection with a transport" + ); + msectionsWithOwnTransports.forEach(checkForTransportAttributes); + + if (testOptions.ssrc) { + ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc"); + } else { + ok( + !description.sdp.includes("a=ssrc"), + label + ": SDP does not contain a=ssrc" + ); + } + }, + + // Note, we don't bother removing the fmtp lines, which makes a good test + // for some SDP parsing issues. + removeCodec(sdp, codec) { + var updated_sdp = sdp.replace( + new RegExp("a=rtpmap:" + codec + ".*\\/90000\\r\\n", ""), + "" + ); + updated_sdp = updated_sdp.replace( + new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""), + "$1$3" + ); + updated_sdp = updated_sdp.replace( + new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""), + "" + ); + updated_sdp = updated_sdp.replace( + new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""), + "" + ); + updated_sdp = updated_sdp.replace( + new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""), + "" + ); + return updated_sdp; + }, + + removeAllButPayloadType(sdp, pt) { + return sdp.replace( + new RegExp("m=(\\w+ \\w+) UDP/TLS/RTP/SAVPF .*" + pt + ".*\\r\\n", "gi"), + "m=$1 UDP/TLS/RTP/SAVPF " + pt + "\r\n" + ); + }, + + removeRtpMapForPayloadType(sdp, pt) { + return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), ""); + }, + + removeRtcpMux(sdp) { + return sdp.replace(/a=rtcp-mux\r\n/g, ""); + }, + + removeSSRCs(sdp) { + return sdp.replace(/a=ssrc.*\r\n/g, ""); + }, + + removeBundle(sdp) { + return sdp.replace(/a=group:BUNDLE .*\r\n/g, ""); + }, + + reduceAudioMLineToPcmuPcma(sdp) { + return sdp.replace( + /m=audio .*\r\n/g, + "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n" + ); + }, + + setAllMsectionsInactive(sdp) { + return sdp + .replace(/\r\na=sendrecv/g, "\r\na=inactive") + .replace(/\r\na=sendonly/g, "\r\na=inactive") + .replace(/\r\na=recvonly/g, "\r\na=inactive"); + }, + + removeAllRtpMaps(sdp) { + return sdp.replace(/a=rtpmap:.*\r\n/g, ""); + }, + + reduceAudioMLineToDynamicPtAndOpus(sdp) { + return sdp.replace( + /m=audio .*\r\n/g, + "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n" + ); + }, + + addTiasBps(sdp, bps) { + return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n"); + }, + + removeSimulcastProperties(sdp) { + return sdp + .replace(/a=simulcast:.*\r\n/g, "") + .replace(/a=rid:.*\r\n/g, "") + .replace( + /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g, + "" + ) + .replace( + /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g, + "" + ); + }, + + transferSimulcastProperties(offer_sdp, answer_sdp) { + if (!offer_sdp.includes("a=simulcast:")) { + return answer_sdp; + } + ok( + offer_sdp.includes("a=simulcast:send "), + "Offer contains simulcast attribute" + ); + var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i); + var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n"; + ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute"); + var o_rids = offer_sdp.match(/a=rid:(.*)/gi); + o_rids.forEach(o_rid => { + new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n"; + }); + var extmap_id = offer_sdp.match( + "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id" + ); + ok(extmap_id != null, "Offer contains RID RTP header extension"); + new_answer_sdp = + new_answer_sdp + + "a=extmap:" + + extmap_id[1] + + "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n"; + var extmap_id = offer_sdp.match( + "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id" + ); + if (extmap_id != null) { + new_answer_sdp = + new_answer_sdp + + "a=extmap:" + + extmap_id[1] + + "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n"; + } + + return new_answer_sdp; + }, + + verifySdp( + desc, + expectedType, + offerConstraintsList, + offerOptions, + testOptions + ) { + info("Examining this SessionDescription: " + JSON.stringify(desc)); + info("offerConstraintsList: " + JSON.stringify(offerConstraintsList)); + info("offerOptions: " + JSON.stringify(offerOptions)); + ok(desc, "SessionDescription is not null"); + is(desc.type, expectedType, "SessionDescription type is " + expectedType); + ok(desc.sdp.length > 10, "SessionDescription body length is plausible"); + ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP"); + ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP"); + ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP"); + //TODO: update this for loopback support bug 1027350 + ok( + !desc.sdp.includes(LOOPBACK_ADDR), + "loopback interface is absent from SDP" + ); + var requiresTrickleIce = !desc.sdp.includes("a=candidate"); + if (requiresTrickleIce) { + info("No ICE candidate in SDP -> requiring trickle ICE"); + } else { + info("at least one ICE candidate is present in SDP"); + } + + //TODO: how can we check for absence/presence of m=application? + + var audioTracks = + sdputils.countTracksInConstraint("audio", offerConstraintsList) || + (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0); + + info("expected audio tracks: " + audioTracks); + if (audioTracks == 0) { + ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP"); + } else { + ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP"); + is( + testOptions.opus, + desc.sdp.includes("a=rtpmap:109 opus/48000/2"), + "OPUS codec is present in SDP" + ); + //TODO: ideally the rtcp-mux should be for the m=audio, and not just + // anywhere in the SDP (JS SDP parser bug 1045429) + is( + testOptions.rtcpmux, + desc.sdp.includes("a=rtcp-mux"), + "RTCP Mux is offered in SDP" + ); + } + + var videoTracks = + sdputils.countTracksInConstraint("video", offerConstraintsList) || + (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0); + + info("expected video tracks: " + videoTracks); + if (videoTracks == 0) { + ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP"); + } else { + ok(desc.sdp.includes("m=video"), "video m-line is present in SDP"); + if (testOptions.h264) { + ok( + desc.sdp.includes("a=rtpmap:126 H264/90000") || + desc.sdp.includes("a=rtpmap:97 H264/90000"), + "H.264 codec is present in SDP" + ); + } else { + ok( + desc.sdp.includes("a=rtpmap:120 VP8/90000") || + desc.sdp.includes("a=rtpmap:121 VP9/90000"), + "VP8 or VP9 codec is present in SDP" + ); + } + is( + testOptions.rtcpmux, + desc.sdp.includes("a=rtcp-mux"), + "RTCP Mux is offered in SDP" + ); + is( + testOptions.ssrc, + desc.sdp.includes("a=ssrc"), + "a=ssrc signaled in SDP" + ); + } + + return requiresTrickleIce; + }, + + /** + * Counts the amount of audio tracks in a given media constraint. + * + * @param constraints + * The contraint to be examined. + */ + countTracksInConstraint(type, constraints) { + if (!Array.isArray(constraints)) { + return 0; + } + return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0); + }, +}; |