@@ -30,6 +30,8 @@ export function createPublisherHook(iceServers = []) {
30
30
view . videoApplyButton = document . getElementById ( "lex-video-apply-button" ) ;
31
31
view . button = document . getElementById ( "lex-button" ) ;
32
32
33
+ view . simulcast = document . getElementById ( "lex-simulcast" ) ;
34
+
33
35
view . audioDevices . onchange = function ( ) {
34
36
view . setupStream ( view ) ;
35
37
} ;
@@ -93,6 +95,7 @@ export function createPublisherHook(iceServers = []) {
93
95
view . audioApplyButton . disabled = true ;
94
96
view . videoApplyButton . disabled = true ;
95
97
view . bitrate . disabled = true ;
98
+ view . simulcast . disabled = true ;
96
99
} ,
97
100
98
101
enableControls ( view ) {
@@ -107,6 +110,7 @@ export function createPublisherHook(iceServers = []) {
107
110
view . audioApplyButton . disabled = false ;
108
111
view . videoApplyButton . disabled = false ;
109
112
view . bitrate . disabled = false ;
113
+ view . simulcast . disabled = false ;
110
114
} ,
111
115
112
116
async findDevices ( view ) {
@@ -198,78 +202,10 @@ export function createPublisherHook(iceServers = []) {
198
202
view . time . innerText = view . toHHMMSS ( new Date ( ) - view . startTime ) ;
199
203
200
204
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 ) ;
270
206
} , 1000 ) ;
271
207
} else if ( view . pc . connectionState === "failed" ) {
272
- view . pushEvent ( "stop-streaming" , { reason : "failed" } )
208
+ view . pushEvent ( "stop-streaming" , { reason : "failed" } ) ;
273
209
view . stopStreaming ( view ) ;
274
210
}
275
211
} ;
@@ -279,6 +215,126 @@ export function createPublisherHook(iceServers = []) {
279
215
} ;
280
216
281
217
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 ) {
282
338
view . pc . addTrack ( view . localStream . getVideoTracks ( ) [ 0 ] , view . localStream ) ;
283
339
284
340
// set max bitrate
@@ -290,11 +346,6 @@ export function createPublisherHook(iceServers = []) {
290
346
params . encodings [ 0 ] . maxBitrate = view . bitrate . value * 1024 ;
291
347
await sender . setParameters ( params ) ;
292
348
} ) ;
293
-
294
- const offer = await view . pc . createOffer ( ) ;
295
- await view . pc . setLocalDescription ( offer ) ;
296
-
297
- view . pushEventTo ( view . el , "offer" , offer ) ;
298
349
} ,
299
350
300
351
stopStreaming ( view ) {
@@ -312,6 +363,12 @@ export function createPublisherHook(iceServers = []) {
312
363
view . startTime = undefined ;
313
364
view . lastAudioReport = undefined ;
314
365
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 ;
315
372
view . audioBitrate . innerText = 0 ;
316
373
view . videoBitrate . innerText = 0 ;
317
374
view . packetLoss . innerText = 0 ;
0 commit comments