Skip to content

Commit 7efebc9

Browse files
committed
Add support for simulcast
1 parent 31e4cc7 commit 7efebc9

File tree

4 files changed

+333
-186
lines changed

4 files changed

+333
-186
lines changed

assets/player.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
export function createPlayerHook(iceServers = []) {
22
return {
33
async mounted() {
4+
this.videoQuality = document.getElementById("lexp-video-quality");
5+
this.videoQuality.onchange = () => {
6+
this.pushEventTo(this.el, "layer", this.videoQuality.value);
7+
};
8+
49
this.pc = new RTCPeerConnection({ iceServers: iceServers });
510

611
this.pc.onicecandidate = (ev) => {

assets/publisher.js

Lines changed: 132 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function createPublisherHook(iceServers = []) {
3030
view.videoApplyButton = document.getElementById("lex-video-apply-button");
3131
view.button = document.getElementById("lex-button");
3232

33+
view.simulcast = document.getElementById("lex-simulcast");
34+
3335
view.audioDevices.onchange = function () {
3436
view.setupStream(view);
3537
};
@@ -93,6 +95,7 @@ export function createPublisherHook(iceServers = []) {
9395
view.audioApplyButton.disabled = true;
9496
view.videoApplyButton.disabled = true;
9597
view.bitrate.disabled = true;
98+
view.simulcast.disabled = true;
9699
},
97100

98101
enableControls(view) {
@@ -107,6 +110,7 @@ export function createPublisherHook(iceServers = []) {
107110
view.audioApplyButton.disabled = false;
108111
view.videoApplyButton.disabled = false;
109112
view.bitrate.disabled = false;
113+
view.simulcast.disabled = false;
110114
},
111115

112116
async findDevices(view) {
@@ -198,78 +202,10 @@ export function createPublisherHook(iceServers = []) {
198202
view.time.innerText = view.toHHMMSS(new Date() - view.startTime);
199203

200204
const stats = await view.pc.getStats(null);
201-
let bitrate;
202-
203-
stats.forEach((report) => {
204-
if (report.type === "outbound-rtp" && report.kind === "video") {
205-
if (!view.lastVideoReport) {
206-
bitrate = (report.bytesSent * 8) / 1000;
207-
} else {
208-
const timeDiff =
209-
(report.timestamp - view.lastVideoReport.timestamp) / 1000;
210-
if (timeDiff == 0) {
211-
// this should never happen as we are getting stats every second
212-
bitrate = 0;
213-
} else {
214-
bitrate =
215-
((report.bytesSent - view.lastVideoReport.bytesSent) *
216-
8) /
217-
timeDiff;
218-
}
219-
}
220-
221-
view.videoBitrate.innerText = (bitrate / 1000).toFixed();
222-
view.lastVideoReport = report;
223-
} else if (
224-
report.type === "outbound-rtp" &&
225-
report.kind === "audio"
226-
) {
227-
if (!view.lastAudioReport) {
228-
bitrate = report.bytesSent;
229-
} else {
230-
const timeDiff =
231-
(report.timestamp - view.lastAudioReport.timestamp) / 1000;
232-
if (timeDiff == 0) {
233-
// this should never happen as we are getting stats every second
234-
bitrate = 0;
235-
} else {
236-
bitrate =
237-
((report.bytesSent - view.lastAudioReport.bytesSent) *
238-
8) /
239-
timeDiff;
240-
}
241-
}
242-
243-
view.audioBitrate.innerText = (bitrate / 1000).toFixed();
244-
view.lastAudioReport = report;
245-
}
246-
});
247-
248-
// calculate packet loss
249-
if (!view.lastAudioReport || !view.lastVideoReport) {
250-
view.packetLoss.innerText = 0;
251-
} else {
252-
const packetsSent =
253-
view.lastVideoReport.packetsSent +
254-
view.lastAudioReport.packetsSent;
255-
const rtxPacketsSent =
256-
view.lastVideoReport.retransmittedPacketsSent +
257-
view.lastAudioReport.retransmittedPacketsSent;
258-
const nackReceived =
259-
view.lastVideoReport.nackCount + view.lastAudioReport.nackCount;
260-
261-
if (nackReceived == 0) {
262-
view.packetLoss.innerText = 0;
263-
} else {
264-
view.packetLoss.innerText = (
265-
(nackReceived / (packetsSent - rtxPacketsSent)) *
266-
100
267-
).toFixed();
268-
}
269-
}
205+
view.processStats(view, stats);
270206
}, 1000);
271207
} else if (view.pc.connectionState === "failed") {
272-
view.pushEvent("stop-streaming", {reason: "failed"})
208+
view.pushEvent("stop-streaming", { reason: "failed" });
273209
view.stopStreaming(view);
274210
}
275211
};
@@ -279,6 +215,126 @@ export function createPublisherHook(iceServers = []) {
279215
};
280216

281217
view.pc.addTrack(view.localStream.getAudioTracks()[0], view.localStream);
218+
219+
if (view.simulcast.checked === true) {
220+
view.addSimulcastVideo(view);
221+
} else {
222+
view.addNormalVideo(view);
223+
}
224+
225+
const offer = await view.pc.createOffer();
226+
await view.pc.setLocalDescription(offer);
227+
228+
view.pushEventTo(view.el, "offer", offer);
229+
},
230+
231+
processStats(view, stats) {
232+
let videoBytesSent = 0;
233+
let videoPacketsSent = 0;
234+
let videoNack = 0;
235+
let audioBytesSent = 0;
236+
let audioPacketsSent = 0;
237+
let audioNack = 0;
238+
239+
let statsTimestamp;
240+
stats.forEach((report) => {
241+
console.log(report);
242+
if (!statsTimestamp) statsTimestamp = report.timestamp;
243+
244+
if (report.type === "outbound-rtp" && report.kind === "video") {
245+
videoBytesSent += report.bytesSent;
246+
videoPacketsSent += report.packetsSent;
247+
videoNack += report.nackCount;
248+
} else if (report.type === "outbound-rtp" && report.kind === "audio") {
249+
audioBytesSent += report.bytesSent;
250+
audioPacketsSent += report.packetsSent;
251+
audioNack += report.nackCount;
252+
}
253+
});
254+
255+
const timeDiff = (statsTimestamp - view.lastStatsTimestamp) / 1000;
256+
257+
let bitrate;
258+
259+
if (!view.lastVideoBytesSent) {
260+
bitrate = (videoBytesSent * 8) / 1000;
261+
} else {
262+
if (timeDiff == 0) {
263+
// this should never happen as we are getting stats every second
264+
bitrate = 0;
265+
} else {
266+
bitrate = ((videoBytesSent - view.lastVideoBytesSent) * 8) / timeDiff;
267+
}
268+
}
269+
270+
view.videoBitrate.innerText = (bitrate / 1000).toFixed();
271+
272+
if (!view.lastAudioBytesSent) {
273+
bitrate = (audioBytesSent * 8) / 1000;
274+
} else {
275+
if (timeDiff == 0) {
276+
// this should never happen as we are getting stats every second
277+
bitrate = 0;
278+
} else {
279+
bitrate = ((audioBytesSent - view.lastAudioBytesSent) * 8) / timeDiff;
280+
}
281+
}
282+
283+
view.audioBitrate.innerText = (bitrate / 1000).toFixed();
284+
285+
// calculate packet loss
286+
if (!view.lastAudioPacketsSent || !view.lastVideoPacketsSent) {
287+
view.packetLoss.innerText = 0;
288+
} else {
289+
const packetsSent =
290+
videoPacketsSent +
291+
audioPacketsSent -
292+
view.lastAudioPacketsSent -
293+
view.lastVideoPacketsSent;
294+
295+
const nack =
296+
videoNack + audioNack - view.lastVideoNack - view.lastAudioNack;
297+
298+
if (packetsSent == 0 || timeDiff == 0) {
299+
view.packetLoss.innerText = 0;
300+
} else {
301+
view.packetLoss.innerText = (
302+
((nack / packetsSent) * 100) /
303+
timeDiff
304+
).toFixed(2);
305+
}
306+
}
307+
308+
view.lastVideoBytesSent = videoBytesSent;
309+
view.lastVideoPacketsSent = videoPacketsSent;
310+
view.lastVideoNack = videoNack;
311+
view.lastAudioBytesSent = audioBytesSent;
312+
view.lastAudioPacketsSent = audioPacketsSent;
313+
view.lastAudioNack = audioNack;
314+
view.lastStatsTimestamp = statsTimestamp;
315+
},
316+
317+
addSimulcastVideo(view) {
318+
const maxTotalBitrate = view.bitrate.value * 1024;
319+
// we do a very simple calculation: maxTotalBitrate = x + 1/4x + 1/16x
320+
// x - bitrate for base resolution
321+
// 1/4x- bitrate for resolution scaled down by 2 - we decrese total number of pixels by 4 (width/2*height/2)
322+
// 1/16x- bitrate for resolution scaled down by 4 - we decrese total number of pixels by 16 (width/4*height/4)
323+
const maxHBitrate = Math.floor((16 * maxTotalBitrate) / 21);
324+
const maxMBitrate = Math.floor(maxHBitrate / 4);
325+
const maxLBitrate = Math.floor(maxHBitrate / 16);
326+
327+
view.pc.addTransceiver(view.localStream.getVideoTracks()[0], {
328+
streams: [view.localStream],
329+
sendEncodings: [
330+
{ rid: "h", maxBitrate: maxHBitrate },
331+
{ rid: "m", scaleResolutionDownBy: 2, maxBitrate: maxMBitrate },
332+
{ rid: "l", scaleResolutionDownBy: 4, maxBitrate: maxLBitrate },
333+
],
334+
});
335+
},
336+
337+
addNormalVideo(view) {
282338
view.pc.addTrack(view.localStream.getVideoTracks()[0], view.localStream);
283339

284340
// set max bitrate
@@ -290,11 +346,6 @@ export function createPublisherHook(iceServers = []) {
290346
params.encodings[0].maxBitrate = view.bitrate.value * 1024;
291347
await sender.setParameters(params);
292348
});
293-
294-
const offer = await view.pc.createOffer();
295-
await view.pc.setLocalDescription(offer);
296-
297-
view.pushEventTo(view.el, "offer", offer);
298349
},
299350

300351
stopStreaming(view) {
@@ -312,6 +363,12 @@ export function createPublisherHook(iceServers = []) {
312363
view.startTime = undefined;
313364
view.lastAudioReport = undefined;
314365
view.lastVideoReport = undefined;
366+
view.lastVideoBytesSent = 0;
367+
view.lastVideoPacketsSent = 0;
368+
view.lastVideoNack = 0;
369+
view.lastAudioBytesSent = 0;
370+
view.lastAudioPacketsSent = 0;
371+
view.lastAudioNack = 0;
315372
view.audioBitrate.innerText = 0;
316373
view.videoBitrate.innerText = 0;
317374
view.packetLoss.innerText = 0;

0 commit comments

Comments
 (0)