@@ -65,6 +65,7 @@ class LocalTracks extends EventTargetPolyfill {
65
65
super ( ) ;
66
66
Object . defineProperty ( this , 'stream' , { value : new MediaStream ( ) , writeable : false } ) ;
67
67
this . videoIsScreenshare = false ;
68
+ this . videoIsDummy = false ;
68
69
}
69
70
70
71
getTracks ( kind ) {
@@ -224,6 +225,10 @@ exports.rtc = new class {
224
225
this . _selfViewButtons = { } ;
225
226
// When grabbing both locks the audio lock must be grabbed first to avoid deadlock.
226
227
this . _trackLocks = { audio : new Mutex ( ) , video : new Mutex ( ) } ;
228
+ // for dummy canvas stream
229
+ this . _dummyCanvasStream = null ;
230
+ this . _dummyCanvas = null ;
231
+ this . _dummyCanvasTimerId = null ;
227
232
}
228
233
229
234
get enableDebugLogging ( ) { return enableDebugLogging ; }
@@ -328,7 +333,7 @@ exports.rtc = new class {
328
333
handleClientMessage_RTC_MESSAGE ( hookName , { payload : { from, data} } ) {
329
334
debug ( `(peer ${ from } ) received message` , data ) ;
330
335
const { publish, clientId, needsReply} = data ;
331
- if ( publish != null && clientId != null ) {
336
+ if ( this . _activated && publish != null && clientId != null ) {
332
337
if ( from !== this . getUserId ( ) ) {
333
338
debug ( `*add cliend id ${ clientId } to user id ${ from } ` ) ;
334
339
this . _clientIdToUserId . set ( clientId , from ) ;
@@ -493,7 +498,7 @@ exports.rtc = new class {
493
498
if ( addAudioTrack ) devices . push ( 'mic' ) ;
494
499
const addVideoTrack = updateVideo && this . _selfViewButtons . video . enabled &&
495
500
( ! this . _localTracks . stream . getVideoTracks ( ) . some ( ( t ) => t . readyState === 'live' ) ||
496
- this . _localTracks . videoIsScreenshare ) ;
501
+ this . _localTracks . videoIsScreenshare || this . _localTracks . videoIsDummy ) ;
497
502
if ( addVideoTrack ) devices . push ( 'cam' ) ;
498
503
const addScreenshareTrack = updateVideo && this . _selfViewButtons . screenshare . enabled &&
499
504
! this . _selfViewButtons . video . enabled && // Video button overrides screenshare button.
@@ -534,6 +539,9 @@ exports.rtc = new class {
534
539
for ( const track of stream . getTracks ( ) ) {
535
540
if ( track . kind === 'video' ) {
536
541
this . _localTracks . videoIsScreenshare = devices . includes ( 'screen' ) ;
542
+ this . _localTracks . videoIsDummy = false ;
543
+ // dummy video stream is no longer needed
544
+ this . disposeDummyCanvasStream ( ) ;
537
545
}
538
546
this . _localTracks . setTrack ( track . kind , track ) ;
539
547
}
@@ -565,11 +573,51 @@ exports.rtc = new class {
565
573
if ( updateVideo ) this . _trackLocks . video . unlock ( ) ;
566
574
if ( updateAudio ) this . _trackLocks . audio . unlock ( ) ;
567
575
}
576
+ // For some browsers, audio stream is not played without video stream,
577
+ // so add video stream with dummy canvas.
578
+ await this . _trackLocks . video . lock ( ) ;
579
+ if ( this . _localTracks . stream . getVideoTracks ( ) . length === 0 ) {
580
+ const dummyVideoTrack = this . createDummyCanvasStream ( ) . getVideoTracks ( ) [ 0 ] ;
581
+ this . _localTracks . setTrack ( dummyVideoTrack . kind , dummyVideoTrack ) ;
582
+ this . _localTracks . videoIsDummy = true ;
583
+ }
584
+ this . _trackLocks . video . unlock ( ) ;
568
585
// For most browsers, autoplay with audio is allowed if the user grants access to the camera or
569
586
// microphone, so unmuting auto-muted videos is likely to succeed.
570
587
await this . unmuteAndPlayAll ( ) ;
571
588
}
572
589
590
+ createDummyCanvasStream ( ) {
591
+ if ( ! this . _dummyCanvasStream ) {
592
+ this . _dummyCanvas = document . createElement ( 'canvas' ) ;
593
+ const canvasWidth = 160 ;
594
+ const canvasHeight = 120 ;
595
+ this . _dummyCanvas . width = canvasWidth ;
596
+ this . _dummyCanvas . height = canvasHeight ;
597
+ const ctx = this . _dummyCanvas . getContext ( '2d' ) ;
598
+ ctx . fillStyle = '#000000' ;
599
+ ctx . fillRect ( 0 , 0 , canvasWidth , canvasHeight ) ;
600
+ this . _dummyCanvasTimerId = setInterval ( ( ) => {
601
+ if ( this . _dummyCanvas ) {
602
+ const ctx = this . _dummyCanvas . getContext ( '2d' ) ;
603
+ ctx . fillStyle = '#000000' ;
604
+ ctx . fillRect ( 0 , 0 , this . _dummyCanvas . width , this . _dummyCanvas . height ) ;
605
+ }
606
+ } , 1000 ) ;
607
+ this . _dummyCanvasStream = this . _dummyCanvas . captureStream ( 1 ) ;
608
+ }
609
+ return this . _dummyCanvasStream ;
610
+ }
611
+
612
+ disposeDummyCanvasStream ( ) {
613
+ if ( this . _dummyCanvasTimerId ) {
614
+ clearInterval ( this . _dummyCanvasTimerId ) ;
615
+ this . _dummyCanvasTimerId = null ;
616
+ }
617
+ this . _dummyCanvasStream = null ;
618
+ this . _dummyCanvas = null ;
619
+ }
620
+
573
621
async activate ( ) {
574
622
if ( ! this . _activated ) {
575
623
this . _activated = ( async ( ) => {
@@ -640,6 +688,7 @@ exports.rtc = new class {
640
688
for ( const track of this . _localTracks . stream . getTracks ( ) ) {
641
689
this . _localTracks . setTrack ( track . kind , null ) ;
642
690
}
691
+ this . disposeDummyCanvasStream ( ) ;
643
692
} finally {
644
693
this . _trackLocks . video . unlock ( ) ;
645
694
this . _trackLocks . audio . unlock ( ) ;
@@ -673,8 +722,8 @@ exports.rtc = new class {
673
722
}
674
723
675
724
async playVideo ( $video ) {
676
- // play() will block indefinitely if there are no enabled tracks.
677
- if ( ! $video [ 0 ] . srcObject . getTracks ( ) . some ( ( t ) => t . enabled ) ) return ;
725
+ // play() will block indefinitely if there are no enabled 'video' tracks.
726
+ if ( ! $video [ 0 ] . srcObject . getVideoTracks ( ) . some ( ( t ) => t . enabled ) ) return ;
678
727
debug ( 'playing video' , $video [ 0 ] ) ;
679
728
const $videoContainer = $ ( `#container_${ $video [ 0 ] . id } ` ) ;
680
729
const $playErrorBtn = $videoContainer . find ( '.play-error-btn' ) ;
0 commit comments