summaryrefslogtreecommitdiffstats
path: root/dom/media/webrtc/tests/mochitests/simulcast.js
blob: 0af36478c4bf0778af1d1dcd234e483765117393 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
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);
    });
  });
}