tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

sdpUtils.js (13418B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 var sdputils = {
      6  // Finds the codec id / payload type given a codec format
      7  // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of
      8  // multiple matches.
      9  findCodecId(sdp, format, offset = 0) {
     10    let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi");
     11    let match;
     12    for (let i = 0; i <= offset; ++i) {
     13      match = regex.exec(sdp);
     14      if (!match) {
     15        throw new Error(
     16          "Couldn't find offset " +
     17            i +
     18            " of codec " +
     19            format +
     20            " while looking for offset " +
     21            offset +
     22            " in sdp:\n" +
     23            sdp
     24        );
     25      }
     26    }
     27    // match[0] is the full matched string
     28    // match[1] is the first parenthesis group
     29    return match[1];
     30  },
     31 
     32  // Returns a list of all payload types, excluding rtx, in an sdp.
     33  getPayloadTypes(sdp) {
     34    const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim;
     35    const pts = [];
     36    for (const [line, pt] of sdp.matchAll(regex)) {
     37      pts.push(pt);
     38    }
     39    return pts;
     40  },
     41 
     42  // Finds all the extmap ids in the given sdp.  Note that this does NOT
     43  // consider m-sections, so a more generic version would need to
     44  // look at each m-section separately.
     45  findExtmapIds(sdp) {
     46    var sdpExtmapIds = [];
     47    extmapRegEx = /^a=extmap:([0-9+])/gm;
     48    // must call exec on the regex to get each match in the string
     49    while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
     50      // returned array has the matched text as the first item,
     51      // and then one item for each capturing parenthesis that
     52      // matched containing the text that was captured.
     53      sdpExtmapIds.push(searchResults[1]);
     54    }
     55    return sdpExtmapIds;
     56  },
     57 
     58  findExtmapIdsUrnsDirections(sdp) {
     59    var sdpExtmap = [];
     60    extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:#\-\/\.]+)/gm;
     61    // must call exec on the regex to get each match in the string
     62    while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
     63      // returned array has the matched text as the first item,
     64      // and then one item for each capturing parenthesis that
     65      // matched containing the text that was captured.
     66      var idUrn = [];
     67      idUrn.push(searchResults[1]);
     68      idUrn.push(searchResults[3]);
     69      idUrn.push(searchResults[2].slice(1));
     70      sdpExtmap.push(idUrn);
     71    }
     72    return sdpExtmap;
     73  },
     74 
     75  verify_unique_extmap_ids(sdp) {
     76    const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp);
     77 
     78    return sdpExtmapIds.reduce(function (result, item, index) {
     79      const [id, urn, dir] = item;
     80      ok(
     81        !(id in result) || (result[id][0] === urn && result[id][1] === dir),
     82        "ID " + id + " is unique ID for " + urn + " and direction " + dir
     83      );
     84      result[id] = [urn, dir];
     85      return result;
     86    }, {});
     87  },
     88 
     89  getMSections(sdp) {
     90    return sdp
     91      .split(new RegExp("^m=", "gm"))
     92      .slice(1)
     93      .map(s => "m=" + s);
     94  },
     95 
     96  getAudioMSections(sdp) {
     97    return this.getMSections(sdp).filter(section =>
     98      section.startsWith("m=audio")
     99    );
    100  },
    101 
    102  getVideoMSections(sdp) {
    103    return this.getMSections(sdp).filter(section =>
    104      section.startsWith("m=video")
    105    );
    106  },
    107 
    108  checkSdpAfterEndOfTrickle(description, testOptions, label) {
    109    info("EOC-SDP: " + JSON.stringify(description));
    110 
    111    const checkForTransportAttributes = msection => {
    112      info("Checking msection: " + msection);
    113      ok(
    114        msection.includes("a=end-of-candidates"),
    115        label + ": SDP contains end-of-candidates"
    116      );
    117 
    118      if (!msection.startsWith("m=application")) {
    119        if (testOptions.rtcpmux) {
    120          ok(
    121            msection.includes("a=rtcp-mux"),
    122            label + ": SDP contains rtcp-mux"
    123          );
    124        } else {
    125          ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port");
    126        }
    127      }
    128    };
    129 
    130    const hasOwnTransport = msection => {
    131      const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection);
    132      if (port0Check) {
    133        return false;
    134      }
    135      const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection);
    136      if (!midMatch) {
    137        return true;
    138      }
    139      const mid = midMatch[1];
    140      const bundleGroupMatch = new RegExp(
    141        "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+"
    142      ).exec(description.sdp);
    143      return bundleGroupMatch == null;
    144    };
    145 
    146    const msectionsWithOwnTransports = this.getMSections(
    147      description.sdp
    148    ).filter(hasOwnTransport);
    149 
    150    ok(
    151      msectionsWithOwnTransports.length,
    152      "SDP should contain at least one msection with a transport"
    153    );
    154    msectionsWithOwnTransports.forEach(checkForTransportAttributes);
    155 
    156    if (testOptions.ssrc) {
    157      ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc");
    158    } else {
    159      ok(
    160        !description.sdp.includes("a=ssrc"),
    161        label + ": SDP does not contain a=ssrc"
    162      );
    163    }
    164  },
    165 
    166  // Note, we don't bother removing the fmtp lines, which makes a good test
    167  // for some SDP parsing issues.
    168  removeCodec(sdp, codec) {
    169    var updated_sdp = sdp.replace(
    170      new RegExp("a=rtpmap:" + codec + ".*\\/[0-9/]+\\r\\n", ""),
    171      ""
    172    );
    173    updated_sdp = updated_sdp.replace(
    174      new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""),
    175      "$1$3"
    176    );
    177    updated_sdp = updated_sdp.replace(
    178      new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""),
    179      ""
    180    );
    181    updated_sdp = updated_sdp.replace(
    182      new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""),
    183      ""
    184    );
    185    updated_sdp = updated_sdp.replace(
    186      new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""),
    187      ""
    188    );
    189    return updated_sdp;
    190  },
    191 
    192  removeCodecs(sdp, codecs) {
    193    var updated_sdp = sdp;
    194    codecs.forEach(codec => {
    195      updated_sdp = this.removeCodec(updated_sdp, codec);
    196    });
    197    return updated_sdp;
    198  },
    199 
    200  // Returns an array of objects with keys {pt, codec}.
    201  enumerateCodecs(sdp) {
    202    const codecs = [];
    203    const regex = /a=rtpmap:(\d+)\s+([^\/]+)\/[0-9\/]+/gi;
    204    let match;
    205    while ((match = regex.exec(sdp))) {
    206      if (match.length < 3) {
    207        continue;
    208      }
    209      const pt = parseInt(match[1]);
    210      if (isNaN(pt)) {
    211        continue;
    212      }
    213      const codec = match[2];
    214      codecs.push({ pt, codec });
    215    }
    216    return codecs;
    217  },
    218 
    219  removeAllButPayloadType(sdp, payloadType) {
    220    const codecs = this.enumerateCodecs(sdp);
    221    const pts = codecs.map(({ pt }) => pt).filter(pt => pt != payloadType);
    222    return this.removeCodecs(sdp, pts);
    223  },
    224 
    225  removeAllButCodec(sdp, codecToKeep) {
    226    const codecs = this.enumerateCodecs(sdp);
    227    const pts = codecs
    228      .filter(({ codec }) => codec.toLowerCase() != codecToKeep.toLowerCase())
    229      .map(({ pt }) => pt);
    230    return this.removeCodecs(sdp, pts);
    231  },
    232 
    233  removeRtpMapForPayloadType(sdp, pt) {
    234    return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), "");
    235  },
    236 
    237  removeRtcpMux(sdp) {
    238    return sdp.replace(/a=rtcp-mux\r\n/g, "");
    239  },
    240 
    241  removeSSRCs(sdp) {
    242    return sdp.replace(/a=ssrc.*\r\n/g, "");
    243  },
    244 
    245  removeBundle(sdp) {
    246    return sdp.replace(/a=group:BUNDLE .*\r\n/g, "");
    247  },
    248 
    249  reduceAudioMLineToPcmuPcma(sdp) {
    250    return sdp.replace(
    251      /m=audio .*\r\n/g,
    252      "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
    253    );
    254  },
    255 
    256  setAllMsectionsInactive(sdp) {
    257    return sdp
    258      .replace(/\r\na=sendrecv/g, "\r\na=inactive")
    259      .replace(/\r\na=sendonly/g, "\r\na=inactive")
    260      .replace(/\r\na=recvonly/g, "\r\na=inactive");
    261  },
    262 
    263  removeAllRtpMaps(sdp) {
    264    return sdp.replace(/a=rtpmap:.*\r\n/g, "");
    265  },
    266 
    267  reduceAudioMLineToDynamicPtAndOpus(sdp) {
    268    return sdp.replace(
    269      /m=audio .*\r\n/g,
    270      "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n"
    271    );
    272  },
    273 
    274  addTiasBps(sdp, bps) {
    275    return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n");
    276  },
    277 
    278  removeSimulcastProperties(sdp) {
    279    return sdp
    280      .replace(/a=simulcast:.*\r\n/g, "")
    281      .replace(/a=rid:.*\r\n/g, "")
    282      .replace(
    283        /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g,
    284        ""
    285      )
    286      .replace(
    287        /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g,
    288        ""
    289      );
    290  },
    291 
    292  transferSimulcastProperties(offer_sdp, answer_sdp) {
    293    if (!offer_sdp.includes("a=simulcast:")) {
    294      return answer_sdp;
    295    }
    296    ok(
    297      offer_sdp.includes("a=simulcast:send "),
    298      "Offer contains simulcast attribute"
    299    );
    300    var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i);
    301    var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n";
    302    ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute");
    303    var o_rids = offer_sdp.match(/a=rid:(.*)/gi);
    304    o_rids.forEach(o_rid => {
    305      new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n";
    306    });
    307    var extmap_id = offer_sdp.match(
    308      "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
    309    );
    310    ok(extmap_id != null, "Offer contains RID RTP header extension");
    311    new_answer_sdp =
    312      new_answer_sdp +
    313      "a=extmap:" +
    314      extmap_id[1] +
    315      "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n";
    316    var extmap_id = offer_sdp.match(
    317      "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
    318    );
    319    if (extmap_id != null) {
    320      new_answer_sdp =
    321        new_answer_sdp +
    322        "a=extmap:" +
    323        extmap_id[1] +
    324        "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n";
    325    }
    326 
    327    return new_answer_sdp;
    328  },
    329 
    330  verifySdp(
    331    desc,
    332    expectedType,
    333    offerConstraintsList,
    334    offerOptions,
    335    testOptions
    336  ) {
    337    info("Examining this SessionDescription: " + JSON.stringify(desc));
    338    info("offerConstraintsList: " + JSON.stringify(offerConstraintsList));
    339    info("offerOptions: " + JSON.stringify(offerOptions));
    340    info("testOptions: " + JSON.stringify(testOptions));
    341    ok(desc, "SessionDescription is not null");
    342    is(desc.type, expectedType, "SessionDescription type is " + expectedType);
    343    ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
    344    ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP");
    345    ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP");
    346    ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP");
    347    var requiresTrickleIce = !desc.sdp.includes("a=candidate");
    348    if (requiresTrickleIce) {
    349      info("No ICE candidate in SDP -> requiring trickle ICE");
    350    } else {
    351      info("at least one ICE candidate is present in SDP");
    352    }
    353 
    354    //TODO: how can we check for absence/presence of m=application?
    355 
    356    var audioTracks =
    357      sdputils.countTracksInConstraint("audio", offerConstraintsList) ||
    358      (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0);
    359 
    360    info("expected audio tracks: " + audioTracks);
    361    if (audioTracks == 0) {
    362      ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP");
    363    } else {
    364      ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP");
    365      is(
    366        testOptions.opus,
    367        desc.sdp.includes("a=rtpmap:109 opus/48000/2"),
    368        "OPUS codec is present in SDP"
    369      );
    370      //TODO: ideally the rtcp-mux should be for the m=audio, and not just
    371      //      anywhere in the SDP (JS SDP parser bug 1045429)
    372      is(
    373        testOptions.rtcpmux,
    374        desc.sdp.includes("a=rtcp-mux"),
    375        "RTCP Mux is offered in SDP"
    376      );
    377    }
    378 
    379    var videoTracks =
    380      sdputils.countTracksInConstraint("video", offerConstraintsList) ||
    381      (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0);
    382 
    383    info("expected video tracks: " + videoTracks);
    384    if (videoTracks == 0) {
    385      ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP");
    386    } else {
    387      ok(desc.sdp.includes("m=video"), "video m-line is present in SDP");
    388      if (testOptions.h264) {
    389        ok(
    390          desc.sdp.includes("a=rtpmap:126 H264/90000") ||
    391            desc.sdp.includes("a=rtpmap:97 H264/90000") ||
    392            desc.sdp.includes("a=rtpmap:103 H264/90000") ||
    393            desc.sdp.includes("a=rtpmap:105 H264/90000"),
    394          "H.264 codec is present in SDP"
    395        );
    396      }
    397      if (testOptions.av1) {
    398        ok(
    399          desc.sdp.includes("a=rtpmap:99 AV1/90000"),
    400          "AV1 codec is present in SDP"
    401        );
    402      }
    403      if (!testOptions.h264 && !testOptions.av1) {
    404        ok(
    405          desc.sdp.includes("a=rtpmap:120 VP8/90000") ||
    406            desc.sdp.includes("a=rtpmap:121 VP9/90000"),
    407          "VP8 or VP9 codec is present in SDP"
    408        );
    409      }
    410      is(
    411        testOptions.rtcpmux,
    412        desc.sdp.includes("a=rtcp-mux"),
    413        "RTCP Mux is offered in SDP"
    414      );
    415      is(
    416        testOptions.ssrc,
    417        desc.sdp.includes("a=ssrc"),
    418        "a=ssrc signaled in SDP"
    419      );
    420    }
    421 
    422    return requiresTrickleIce;
    423  },
    424 
    425  /**
    426   * Counts the amount of audio tracks in a given media constraint.
    427   *
    428   * @param constraints
    429   *        The contraint to be examined.
    430   */
    431  countTracksInConstraint(type, constraints) {
    432    if (!Array.isArray(constraints)) {
    433      return 0;
    434    }
    435    return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0);
    436  },
    437 };