@@ -32,6 +32,8 @@ export function createPublisherHook(iceServers = []) {
32
32
view . videoApplyButton = document . getElementById ( "lex-video-apply-button" ) ;
33
33
view . button = document . getElementById ( "lex-button" ) ;
34
34
35
+ view . simulcast = document . getElementById ( "lex-simulcast" ) ;
36
+
35
37
view . audioDevices . onchange = function ( ) {
36
38
view . setupStream ( view ) ;
37
39
} ;
@@ -95,6 +97,7 @@ export function createPublisherHook(iceServers = []) {
95
97
view . audioApplyButton . disabled = true ;
96
98
view . videoApplyButton . disabled = true ;
97
99
view . bitrate . disabled = true ;
100
+ view . simulcast . disabled = true ;
98
101
// Button present only when Recorder is used
99
102
if ( view . recordStream ) view . recordStream . disabled = true ;
100
103
} ,
@@ -111,6 +114,7 @@ export function createPublisherHook(iceServers = []) {
111
114
view . audioApplyButton . disabled = false ;
112
115
view . videoApplyButton . disabled = false ;
113
116
view . bitrate . disabled = false ;
117
+ view . simulcast . disabled = false ;
114
118
// See above
115
119
if ( view . recordStream ) view . recordStream . disabled = false ;
116
120
} ,
@@ -204,78 +208,10 @@ export function createPublisherHook(iceServers = []) {
204
208
view . time . innerText = view . toHHMMSS ( new Date ( ) - view . startTime ) ;
205
209
206
210
const stats = await view . pc . getStats ( null ) ;
207
- let bitrate ;
208
-
209
- stats . forEach ( ( report ) => {
210
- if ( report . type === "outbound-rtp" && report . kind === "video" ) {
211
- if ( ! view . lastVideoReport ) {
212
- bitrate = ( report . bytesSent * 8 ) / 1000 ;
213
- } else {
214
- const timeDiff =
215
- ( report . timestamp - view . lastVideoReport . timestamp ) / 1000 ;
216
- if ( timeDiff == 0 ) {
217
- // this should never happen as we are getting stats every second
218
- bitrate = 0 ;
219
- } else {
220
- bitrate =
221
- ( ( report . bytesSent - view . lastVideoReport . bytesSent ) *
222
- 8 ) /
223
- timeDiff ;
224
- }
225
- }
226
-
227
- view . videoBitrate . innerText = ( bitrate / 1000 ) . toFixed ( ) ;
228
- view . lastVideoReport = report ;
229
- } else if (
230
- report . type === "outbound-rtp" &&
231
- report . kind === "audio"
232
- ) {
233
- if ( ! view . lastAudioReport ) {
234
- bitrate = report . bytesSent ;
235
- } else {
236
- const timeDiff =
237
- ( report . timestamp - view . lastAudioReport . timestamp ) / 1000 ;
238
- if ( timeDiff == 0 ) {
239
- // this should never happen as we are getting stats every second
240
- bitrate = 0 ;
241
- } else {
242
- bitrate =
243
- ( ( report . bytesSent - view . lastAudioReport . bytesSent ) *
244
- 8 ) /
245
- timeDiff ;
246
- }
247
- }
248
-
249
- view . audioBitrate . innerText = ( bitrate / 1000 ) . toFixed ( ) ;
250
- view . lastAudioReport = report ;
251
- }
252
- } ) ;
253
-
254
- // calculate packet loss
255
- if ( ! view . lastAudioReport || ! view . lastVideoReport ) {
256
- view . packetLoss . innerText = 0 ;
257
- } else {
258
- const packetsSent =
259
- view . lastVideoReport . packetsSent +
260
- view . lastAudioReport . packetsSent ;
261
- const rtxPacketsSent =
262
- view . lastVideoReport . retransmittedPacketsSent +
263
- view . lastAudioReport . retransmittedPacketsSent ;
264
- const nackReceived =
265
- view . lastVideoReport . nackCount + view . lastAudioReport . nackCount ;
266
-
267
- if ( nackReceived == 0 ) {
268
- view . packetLoss . innerText = 0 ;
269
- } else {
270
- view . packetLoss . innerText = (
271
- ( nackReceived / ( packetsSent - rtxPacketsSent ) ) *
272
- 100
273
- ) . toFixed ( ) ;
274
- }
275
- }
211
+ view . processStats ( view , stats ) ;
276
212
} , 1000 ) ;
277
213
} else if ( view . pc . connectionState === "failed" ) {
278
- view . pushEvent ( "stop-streaming" , { reason : "failed" } )
214
+ view . pushEvent ( "stop-streaming" , { reason : "failed" } ) ;
279
215
view . stopStreaming ( view ) ;
280
216
}
281
217
} ;
@@ -285,6 +221,144 @@ export function createPublisherHook(iceServers = []) {
285
221
} ;
286
222
287
223
view . pc . addTrack ( view . localStream . getAudioTracks ( ) [ 0 ] , view . localStream ) ;
224
+
225
+ if ( view . simulcast . checked === true ) {
226
+ view . addSimulcastVideo ( view ) ;
227
+ } else {
228
+ view . addNormalVideo ( view ) ;
229
+ }
230
+
231
+ const offer = await view . pc . createOffer ( ) ;
232
+ await view . pc . setLocalDescription ( offer ) ;
233
+
234
+ view . pushEventTo ( view . el , "offer" , offer ) ;
235
+ } ,
236
+
237
+ processStats ( view , stats ) {
238
+ let videoBytesSent = 0 ;
239
+ let videoPacketsSent = 0 ;
240
+ let videoNack = 0 ;
241
+ let audioBytesSent = 0 ;
242
+ let audioPacketsSent = 0 ;
243
+ let audioNack = 0 ;
244
+
245
+ let statsTimestamp ;
246
+ stats . forEach ( ( report ) => {
247
+ if ( ! statsTimestamp ) statsTimestamp = report . timestamp ;
248
+
249
+ if ( report . type === "outbound-rtp" && report . kind === "video" ) {
250
+ videoBytesSent += report . bytesSent ;
251
+ videoPacketsSent += report . packetsSent ;
252
+ videoNack += report . nackCount ;
253
+ } else if ( report . type === "outbound-rtp" && report . kind === "audio" ) {
254
+ audioBytesSent += report . bytesSent ;
255
+ audioPacketsSent += report . packetsSent ;
256
+ audioNack += report . nackCount ;
257
+ }
258
+ } ) ;
259
+
260
+ const timeDiff = ( statsTimestamp - view . lastStatsTimestamp ) / 1000 ;
261
+
262
+ let bitrate ;
263
+
264
+ if ( ! view . lastVideoBytesSent ) {
265
+ bitrate = ( videoBytesSent * 8 ) / 1000 ;
266
+ } else {
267
+ if ( timeDiff == 0 ) {
268
+ // this should never happen as we are getting stats every second
269
+ bitrate = 0 ;
270
+ } else {
271
+ bitrate = ( ( videoBytesSent - view . lastVideoBytesSent ) * 8 ) / timeDiff ;
272
+ }
273
+ }
274
+
275
+ view . videoBitrate . innerText = ( bitrate / 1000 ) . toFixed ( ) ;
276
+
277
+ if ( ! view . lastAudioBytesSent ) {
278
+ bitrate = ( audioBytesSent * 8 ) / 1000 ;
279
+ } else {
280
+ if ( timeDiff == 0 ) {
281
+ // this should never happen as we are getting stats every second
282
+ bitrate = 0 ;
283
+ } else {
284
+ bitrate = ( ( audioBytesSent - view . lastAudioBytesSent ) * 8 ) / timeDiff ;
285
+ }
286
+ }
287
+
288
+ view . audioBitrate . innerText = ( bitrate / 1000 ) . toFixed ( ) ;
289
+
290
+ // calculate packet loss
291
+ if ( ! view . lastAudioPacketsSent || ! view . lastVideoPacketsSent ) {
292
+ view . packetLoss . innerText = 0 ;
293
+ } else {
294
+ const packetsSent =
295
+ videoPacketsSent +
296
+ audioPacketsSent -
297
+ view . lastAudioPacketsSent -
298
+ view . lastVideoPacketsSent ;
299
+
300
+ const nack =
301
+ videoNack + audioNack - view . lastVideoNack - view . lastAudioNack ;
302
+
303
+ if ( packetsSent == 0 || timeDiff == 0 ) {
304
+ view . packetLoss . innerText = 0 ;
305
+ } else {
306
+ view . packetLoss . innerText = (
307
+ ( ( nack / packetsSent ) * 100 ) /
308
+ timeDiff
309
+ ) . toFixed ( 2 ) ;
310
+ }
311
+ }
312
+
313
+ view . lastVideoBytesSent = videoBytesSent ;
314
+ view . lastVideoPacketsSent = videoPacketsSent ;
315
+ view . lastVideoNack = videoNack ;
316
+ view . lastAudioBytesSent = audioBytesSent ;
317
+ view . lastAudioPacketsSent = audioPacketsSent ;
318
+ view . lastAudioNack = audioNack ;
319
+ view . lastStatsTimestamp = statsTimestamp ;
320
+ } ,
321
+
322
+ addSimulcastVideo ( view ) {
323
+ const videoTrack = view . localStream . getVideoTracks ( ) [ 0 ] ;
324
+ const settings = videoTrack . getSettings ( ) ;
325
+ const maxTotalBitrate = view . bitrate . value * 1024 ;
326
+
327
+ // This is based on:
328
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/video/config/simulcast.cc;l=79?q=simulcast.cc
329
+ let sendEncodings ;
330
+ if ( settings . width >= 960 && settings . height >= 540 ) {
331
+ // we do a very simple calculation: maxTotalBitrate = x + 1/4x + 1/16x
332
+ // x - bitrate for base resolution
333
+ // 1/4x- bitrate for resolution scaled down by 2 - we decrese total number of pixels by 4 (width/2*height/2)
334
+ // 1/16x- bitrate for resolution scaled down by 4 - we decrese total number of pixels by 16 (width/4*height/4)
335
+ const maxHBitrate = Math . floor ( ( 16 * maxTotalBitrate ) / 21 ) ;
336
+ const maxMBitrate = Math . floor ( maxHBitrate / 4 ) ;
337
+ const maxLBitrate = Math . floor ( maxHBitrate / 16 ) ;
338
+ sendEncodings = [
339
+ { rid : "h" , maxBitrate : maxHBitrate } ,
340
+ { rid : "m" , scaleResolutionDownBy : 2 , maxBitrate : maxMBitrate } ,
341
+ { rid : "l" , scaleResolutionDownBy : 4 , maxBitrate : maxLBitrate } ,
342
+ ] ;
343
+ } else if ( settings . width >= 480 && settings . height >= 270 ) {
344
+ // maxTotalBitate = x + 1/4x
345
+ const maxHBitrate = Math . floor ( ( 4 * maxTotalBitrate ) / 5 ) ;
346
+ const maxMBitrate = Math . floor ( maxHBitrate / 4 ) ;
347
+ sendEncodings = [
348
+ { rid : "h" , maxBitrate : maxHBitrate } ,
349
+ { rid : "m" , scaleResolutionDownBy : 2 , maxBitrate : maxMBitrate } ,
350
+ ] ;
351
+ } else {
352
+ sendEncodings = [ { rid : "h" , maxBitrate : maxTotalBitrate } ] ;
353
+ }
354
+
355
+ view . pc . addTransceiver ( view . localStream . getVideoTracks ( ) [ 0 ] , {
356
+ streams : [ view . localStream ] ,
357
+ sendEncodings : sendEncodings ,
358
+ } ) ;
359
+ } ,
360
+
361
+ addNormalVideo ( view ) {
288
362
view . pc . addTrack ( view . localStream . getVideoTracks ( ) [ 0 ] , view . localStream ) ;
289
363
290
364
// set max bitrate
@@ -296,11 +370,6 @@ export function createPublisherHook(iceServers = []) {
296
370
params . encodings [ 0 ] . maxBitrate = view . bitrate . value * 1024 ;
297
371
await sender . setParameters ( params ) ;
298
372
} ) ;
299
-
300
- const offer = await view . pc . createOffer ( ) ;
301
- await view . pc . setLocalDescription ( offer ) ;
302
-
303
- view . pushEventTo ( view . el , "offer" , offer ) ;
304
373
} ,
305
374
306
375
stopStreaming ( view ) {
@@ -318,6 +387,12 @@ export function createPublisherHook(iceServers = []) {
318
387
view . startTime = undefined ;
319
388
view . lastAudioReport = undefined ;
320
389
view . lastVideoReport = undefined ;
390
+ view . lastVideoBytesSent = 0 ;
391
+ view . lastVideoPacketsSent = 0 ;
392
+ view . lastVideoNack = 0 ;
393
+ view . lastAudioBytesSent = 0 ;
394
+ view . lastAudioPacketsSent = 0 ;
395
+ view . lastAudioNack = 0 ;
321
396
view . audioBitrate . innerText = 0 ;
322
397
view . videoBitrate . innerText = 0 ;
323
398
view . packetLoss . innerText = 0 ;
0 commit comments