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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
|
'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.
*/
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 =
sections[1].match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/)[0];
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 =
sections[1].match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/)[0];
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);
}
function swapRidAndMidExtensionsInSimulcastOffer(offer, rids) {
return ridToMid(offer, rids);
}
function swapRidAndMidExtensionsInSimulcastAnswer(answer, localDescription, rids) {
return midToRid(answer, localDescription, rids);
}
async function negotiateSimulcastAndWaitForVideo(t, rids, pc1, pc2, codec) {
exchangeIceCandidates(pc1, pc2);
const metadataToBeLoaded = [];
pc2.ontrack = (e) => {
const stream = e.streams[0];
const v = document.createElement('video');
v.autoplay = true;
v.srcObject = stream;
v.id = stream.id
metadataToBeLoaded.push(new Promise((resolve) => {
v.addEventListener('loadedmetadata', () => {
resolve();
});
}));
};
const sendEncodings = rids.map(rid => ({rid}));
// Use a 2X downscale factor between each layer. To improve ramp-up time, the
// top layer is scaled down by a factor 2. Smaller layer comes first. For
// example if MediaStreamTrack is 720p and we want to send three layers we'll
// get {90p, 180p, 360p}.
let scaleResolutionDownBy = 2;
for (let i = sendEncodings.length - 1; i >= 0; --i) {
sendEncodings[i].scaleResolutionDownBy = scaleResolutionDownBy;
scaleResolutionDownBy *= 2;
}
// Use getUserMedia as getNoiseStream does not have enough entropy to ramp-up.
await setMediaPermission();
const stream = await navigator.mediaDevices.getUserMedia({video: {width: 1280, height: 720}});
t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
const transceiver = pc1.addTransceiver(stream.getVideoTracks()[0], {
streams: [stream],
sendEncodings: sendEncodings,
});
if (codec) {
preferCodec(transceiver, codec.mimeType, codec.sdpFmtpLine);
}
const offer = await pc1.createOffer();
await pc1.setLocalDescription(offer),
await pc2.setRemoteDescription({
type: 'offer',
sdp: swapRidAndMidExtensionsInSimulcastOffer(offer, rids),
});
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription({
type: 'answer',
sdp: swapRidAndMidExtensionsInSimulcastAnswer(answer, pc1.localDescription, rids),
});
assert_equals(metadataToBeLoaded.length, rids.length);
return Promise.all(metadataToBeLoaded);
}
|