2020-11-07 12:16:07 +00:00
import {
AbstractVoiceConnection ,
VoiceConnectionStatus ,
WhisperSessionInitializer
} from "tc-shared/connection/VoiceConnection" ;
import { RecorderProfile } from "tc-shared/voice/RecorderProfile" ;
import { VoiceClient } from "tc-shared/voice/VoiceClient" ;
2020-11-28 11:58:22 +00:00
import {
kUnknownWhisperClientUniqueId ,
WhisperSession ,
WhisperSessionState ,
WhisperTarget
} from "tc-shared/voice/VoiceWhisper" ;
2020-11-28 18:33:54 +00:00
import { RTCConnection , RTCConnectionEvents , RTPConnectionState } from "tc-shared/connection/rtc/Connection" ;
2020-11-16 20:02:18 +00:00
import { AbstractServerConnection , ConnectionStatistics } from "tc-shared/connection/ConnectionBase" ;
2020-11-07 12:16:07 +00:00
import { VoicePlayerState } from "tc-shared/voice/VoicePlayer" ;
import * as log from "tc-shared/log" ;
2020-11-28 11:58:22 +00:00
import { LogCategory , logDebug , logError , logTrace , logWarn } from "tc-shared/log" ;
2020-11-28 18:33:54 +00:00
import * as aplayer from "../audio/player" ;
2020-11-07 12:16:07 +00:00
import { tr } from "tc-shared/i18n/localize" ;
2020-11-28 18:33:54 +00:00
import { RtpVoiceClient } from "tc-backend/web/voice/VoiceClient" ;
2020-11-07 12:16:07 +00:00
import { InputConsumerType } from "tc-shared/voice/RecorderBase" ;
2020-11-28 18:33:54 +00:00
import { RtpWhisperSession } from "tc-backend/web/voice/WhisperClient" ;
2020-11-07 12:16:07 +00:00
2020-11-28 11:58:22 +00:00
type CancelableWhisperTarget = WhisperTarget & { canceled : boolean } ;
2020-11-07 12:16:07 +00:00
export class RtpVoiceConnection extends AbstractVoiceConnection {
private readonly rtcConnection : RTCConnection ;
private readonly listenerRtcAudioAssignment ;
private readonly listenerRtcStateChanged ;
private listenerClientMoved ;
2020-11-17 10:27:14 +00:00
private listenerSpeakerStateChanged ;
2020-11-07 12:16:07 +00:00
private connectionState : VoiceConnectionStatus ;
private localFailedReason : string ;
private localAudioDestination : MediaStreamAudioDestinationNode ;
private currentAudioSourceNode : AudioNode ;
private currentAudioSource : RecorderProfile ;
2020-11-17 10:27:14 +00:00
private speakerMuted : boolean ;
2020-11-07 12:16:07 +00:00
private voiceClients : RtpVoiceClient [ ] = [ ] ;
2020-11-28 11:58:22 +00:00
private whisperSessionInitializer : WhisperSessionInitializer | undefined ;
private whisperSessions : RtpWhisperSession [ ] = [ ] ;
private whisperTarget : CancelableWhisperTarget | undefined ;
private whisperTargetInitialize : Promise < void > | undefined ;
2020-11-07 12:16:07 +00:00
private currentlyReplayingVoice : boolean = false ;
private readonly voiceClientStateChangedEventListener ;
private readonly whisperSessionStateChangedEventListener ;
constructor ( connection : AbstractServerConnection , rtcConnection : RTCConnection ) {
super ( connection ) ;
this . rtcConnection = rtcConnection ;
this . voiceClientStateChangedEventListener = this . handleVoiceClientStateChange . bind ( this ) ;
2020-11-28 11:58:22 +00:00
this . whisperSessionStateChangedEventListener = this . handleWhisperSessionStateChange . bind ( this ) ;
2020-11-07 12:16:07 +00:00
this . rtcConnection . getEvents ( ) . on ( "notify_audio_assignment_changed" ,
this . listenerRtcAudioAssignment = event = > this . handleAudioAssignmentChanged ( event ) ) ;
this . rtcConnection . getEvents ( ) . on ( "notify_state_changed" ,
this . listenerRtcStateChanged = event = > this . handleRtcConnectionStateChanged ( event ) ) ;
2020-11-17 10:27:14 +00:00
this . listenerSpeakerStateChanged = connection . client . events ( ) . on ( "notify_state_updated" , event = > {
if ( event . state === "speaker" ) {
this . updateSpeakerState ( ) ;
}
} ) ;
2020-11-07 12:16:07 +00:00
this . listenerClientMoved = this . rtcConnection . getConnection ( ) . command_handler_boss ( ) . register_explicit_handler ( "notifyclientmoved" , event = > {
const localClientId = this . rtcConnection . getConnection ( ) . client . getClientId ( ) ;
for ( const data of event . arguments ) {
if ( parseInt ( data [ "clid" ] ) === localClientId ) {
2021-01-04 20:28:47 +00:00
this . rtcConnection . startAudioBroadcast ( ) . catch ( error = > {
2020-12-03 16:52:20 +00:00
logError ( LogCategory . VOICE , tr ( "Failed to start voice audio broadcasting after channel switch: %o" ) , error ) ;
this . localFailedReason = tr ( "Failed to start audio broadcasting" ) ;
this . setConnectionState ( VoiceConnectionStatus . Failed ) ;
} ) . catch ( ( ) = > {
this . setConnectionState ( VoiceConnectionStatus . Connected ) ;
} ) ;
2020-11-07 12:16:07 +00:00
}
}
} ) ;
2020-11-17 10:27:14 +00:00
this . speakerMuted = connection . client . isSpeakerMuted ( ) || connection . client . isSpeakerDisabled ( ) ;
2020-11-07 12:16:07 +00:00
this . setConnectionState ( VoiceConnectionStatus . Disconnected ) ;
aplayer . on_ready ( ( ) = > {
this . localAudioDestination = aplayer . context ( ) . createMediaStreamDestination ( ) ;
if ( this . currentAudioSourceNode ) {
this . currentAudioSourceNode . connect ( this . localAudioDestination ) ;
}
} ) ;
2020-11-28 11:58:22 +00:00
this . setWhisperSessionInitializer ( undefined ) ;
2020-11-07 12:16:07 +00:00
}
destroy() {
if ( this . listenerClientMoved ) {
this . listenerClientMoved ( ) ;
this . listenerClientMoved = undefined ;
}
2020-11-17 10:27:14 +00:00
if ( this . listenerSpeakerStateChanged ) {
this . listenerSpeakerStateChanged ( ) ;
this . listenerSpeakerStateChanged = undefined ;
}
2020-11-07 12:16:07 +00:00
this . rtcConnection . getEvents ( ) . off ( "notify_audio_assignment_changed" , this . listenerRtcAudioAssignment ) ;
this . rtcConnection . getEvents ( ) . off ( "notify_state_changed" , this . listenerRtcStateChanged ) ;
this . acquireVoiceRecorder ( undefined , true ) . catch ( error = > {
log . warn ( LogCategory . VOICE , tr ( "Failed to release voice recorder: %o" ) , error ) ;
} ) . then ( ( ) = > {
for ( const client of Object . values ( this . voiceClients ) ) {
client . abortReplay ( ) ;
}
this . voiceClients = undefined ;
this . currentAudioSource = undefined ;
} ) ;
if ( Object . keys ( this . voiceClients ) . length !== 0 ) {
logWarn ( LogCategory . AUDIO , tr ( "Voice connection will be destroyed, but some voice clients are still left (%d)." ) , Object . keys ( this . voiceClients ) . length ) ;
}
/ *
const whisperSessions = Object . keys ( this . whisperSessions ) ;
whisperSessions . forEach ( session = > this . whisperSessions [ session ] . destroy ( ) ) ;
this . whisperSessions = { } ;
* /
this . events . destroy ( ) ;
}
getConnectionState ( ) : VoiceConnectionStatus {
return this . connectionState ;
}
getFailedMessage ( ) : string {
return this . localFailedReason || this . rtcConnection . getFailReason ( ) ;
}
voiceRecorder ( ) : RecorderProfile {
return this . currentAudioSource ;
}
async acquireVoiceRecorder ( recorder : RecorderProfile | undefined , enforce? : boolean ) : Promise < void > {
if ( this . currentAudioSource === recorder && ! enforce ) {
return ;
}
if ( this . currentAudioSource ) {
this . currentAudioSourceNode ? . disconnect ( this . localAudioDestination ) ;
this . currentAudioSourceNode = undefined ;
this . currentAudioSource . callback_unmount = undefined ;
await this . currentAudioSource . unmount ( ) ;
}
/* unmount our target recorder */
await recorder ? . unmount ( ) ;
this . handleRecorderStop ( ) ;
2021-01-08 20:30:48 +00:00
const oldRecorder = recorder ;
2020-11-07 12:16:07 +00:00
this . currentAudioSource = recorder ;
if ( recorder ) {
recorder . current_handler = this . connection . client ;
recorder . callback_unmount = this . handleRecorderUnmount . bind ( this ) ;
recorder . callback_start = this . handleRecorderStart . bind ( this ) ;
recorder . callback_stop = this . handleRecorderStop . bind ( this ) ;
recorder . callback_input_initialized = async input = > {
await input . setConsumer ( {
type : InputConsumerType . NODE ,
callbackDisconnect : node = > {
this . currentAudioSourceNode = undefined ;
node . disconnect ( this . localAudioDestination ) ;
} ,
callbackNode : node = > {
this . currentAudioSourceNode = node ;
if ( this . localAudioDestination ) {
node . connect ( this . localAudioDestination ) ;
}
}
} ) ;
} ;
if ( recorder . input ) {
recorder . callback_input_initialized ( recorder . input ) ;
}
if ( ! recorder . input || recorder . input . isFiltered ( ) ) {
this . handleRecorderStop ( ) ;
} else {
this . handleRecorderStart ( ) ;
}
}
2021-01-08 20:30:48 +00:00
this . events . fire ( "notify_recorder_changed" , {
oldRecorder ,
newRecorder : recorder
} ) ;
2020-11-07 12:16:07 +00:00
}
private handleRecorderStop() {
const chandler = this . connection . client ;
const ch = chandler . getClient ( ) ;
if ( ch ) ch . speaking = false ;
2020-11-28 11:58:22 +00:00
if ( ! chandler . connected ) {
2020-11-07 12:16:07 +00:00
return false ;
2020-11-28 11:58:22 +00:00
}
2020-11-07 12:16:07 +00:00
2020-11-28 11:58:22 +00:00
if ( chandler . isMicrophoneMuted ( ) ) {
2020-11-07 12:16:07 +00:00
return false ;
2020-11-28 11:58:22 +00:00
}
2020-11-07 12:16:07 +00:00
log . info ( LogCategory . VOICE , tr ( "Local voice ended" ) ) ;
this . rtcConnection . setTrackSource ( "audio" , null ) . catch ( error = > {
logError ( LogCategory . AUDIO , tr ( "Failed to set current audio track: %o" ) , error ) ;
} ) ;
2020-11-28 11:58:22 +00:00
this . rtcConnection . setTrackSource ( "audio-whisper" , null ) . catch ( error = > {
logError ( LogCategory . AUDIO , tr ( "Failed to set current audio track: %o" ) , error ) ;
} ) ;
2020-11-07 12:16:07 +00:00
}
private handleRecorderStart() {
const chandler = this . connection . client ;
if ( chandler . isMicrophoneMuted ( ) ) {
log . warn ( LogCategory . VOICE , tr ( "Received local voice started event, even thou we're muted!" ) ) ;
return ;
}
log . info ( LogCategory . VOICE , tr ( "Local voice started" ) ) ;
const ch = chandler . getClient ( ) ;
2020-11-17 10:27:14 +00:00
if ( ch ) { ch . speaking = true ; }
2020-11-28 11:58:22 +00:00
this . rtcConnection . setTrackSource ( this . whisperTarget ? "audio-whisper" : "audio" , this . localAudioDestination . stream . getAudioTracks ( ) [ 0 ] )
2020-11-07 12:16:07 +00:00
. catch ( error = > {
logError ( LogCategory . AUDIO , tr ( "Failed to set current audio track: %o" ) , error ) ;
} ) ;
}
private handleRecorderUnmount() {
log . info ( LogCategory . VOICE , "Lost recorder!" ) ;
this . currentAudioSource = undefined ;
this . acquireVoiceRecorder ( undefined , true ) ; /* we can ignore the promise because we should finish this directly */
}
isReplayingVoice ( ) : boolean {
return this . currentlyReplayingVoice ;
}
decodingSupported ( codec : number ) : boolean {
return codec === 4 || codec === 5 ;
}
encodingSupported ( codec : number ) : boolean {
return codec === 4 || codec === 5 ;
}
getEncoderCodec ( ) : number {
2020-11-28 11:58:22 +00:00
return 4 ;
2020-11-07 12:16:07 +00:00
}
2020-11-28 11:58:22 +00:00
setEncoderCodec ( codec : number ) {
/* TODO: If possible change the payload format? */
}
2020-11-07 12:16:07 +00:00
availableVoiceClients ( ) : VoiceClient [ ] {
return Object . values ( this . voiceClients ) ;
}
registerVoiceClient ( clientId : number ) {
if ( typeof this . voiceClients [ clientId ] !== "undefined" ) {
throw tr ( "voice client already registered" ) ;
}
const client = new RtpVoiceClient ( clientId ) ;
this . voiceClients [ clientId ] = client ;
2020-12-03 11:10:44 +00:00
this . voiceClients [ clientId ] . setGloballyMuted ( this . speakerMuted ) ;
2020-11-07 12:16:07 +00:00
client . events . on ( "notify_state_changed" , this . voiceClientStateChangedEventListener ) ;
return client ;
}
unregisterVoiceClient ( client : VoiceClient ) {
2020-11-17 13:27:46 +00:00
if ( ! ( client instanceof RtpVoiceClient ) ) {
2020-11-07 12:16:07 +00:00
throw "Invalid client type" ;
2020-11-17 13:27:46 +00:00
}
2020-11-07 12:16:07 +00:00
console . error ( "Destroy voice client %d" , client . getClientId ( ) ) ;
client . events . off ( "notify_state_changed" , this . voiceClientStateChangedEventListener ) ;
delete this . voiceClients [ client . getClientId ( ) ] ;
client . destroy ( ) ;
}
2020-11-28 11:58:22 +00:00
stopAllVoiceReplays() { }
2020-11-07 12:16:07 +00:00
getWhisperSessionInitializer ( ) : WhisperSessionInitializer | undefined {
2020-11-28 11:58:22 +00:00
return this . whisperSessionInitializer ;
2020-11-07 12:16:07 +00:00
}
setWhisperSessionInitializer ( initializer : WhisperSessionInitializer | undefined ) {
2020-11-28 11:58:22 +00:00
this . whisperSessionInitializer = initializer ;
if ( ! this . whisperSessionInitializer ) {
this . whisperSessionInitializer = async session = > {
logWarn ( LogCategory . VOICE , tr ( "Missing whisper session initializer. Blocking whisper from %d (%s)" ) , session . getClientId ( ) , session . getClientUniqueId ( ) ) ;
return {
clientName : session.getClientName ( ) || tr ( "Unknown client" ) ,
clientUniqueId : session.getClientUniqueId ( ) || kUnknownWhisperClientUniqueId ,
blocked : true ,
volume : 1 ,
sessionTimeout : 60 * 1000
}
}
}
2020-11-07 12:16:07 +00:00
}
getWhisperSessions ( ) : WhisperSession [ ] {
2020-11-28 11:58:22 +00:00
return this . whisperSessions ;
2020-11-07 12:16:07 +00:00
}
2020-11-28 11:58:22 +00:00
dropWhisperSession ( session : WhisperSession ) {
if ( ! ( session instanceof RtpWhisperSession ) ) {
throw tr ( "Invalid session type" ) ;
}
session . events . off ( "notify_state_changed" , this . whisperSessionStateChangedEventListener ) ;
this . whisperSessions . remove ( session ) ;
session . destroy ( ) ;
2020-11-07 12:16:07 +00:00
}
2020-11-28 11:58:22 +00:00
async startWhisper ( target : WhisperTarget ) : Promise < void > {
while ( this . whisperTargetInitialize ) {
this . whisperTarget . canceled = true ;
await this . whisperTargetInitialize ;
}
this . whisperTarget = Object . assign ( { canceled : false } , target ) ;
try {
await ( this . whisperTargetInitialize = this . doStartWhisper ( this . whisperTarget ) ) ;
} finally {
this . whisperTargetInitialize = undefined ;
}
}
private async doStartWhisper ( target : CancelableWhisperTarget ) {
if ( this . rtcConnection . getConnectionState ( ) !== RTPConnectionState . CONNECTED ) {
return ;
}
await this . rtcConnection . startWhisper ( target ) ;
if ( target . canceled ) {
return ;
}
this . handleRecorderStop ( ) ;
if ( this . currentAudioSource ? . input && ! this . currentAudioSource . input . isFiltered ( ) ) {
this . handleRecorderStart ( ) ;
}
2020-11-07 12:16:07 +00:00
}
2020-11-28 11:58:22 +00:00
getWhisperTarget ( ) : WhisperTarget | undefined {
return this . whisperTarget ;
2020-11-07 12:16:07 +00:00
}
2020-11-28 11:58:22 +00:00
stopWhisper() {
if ( this . whisperTarget ) {
this . whisperTarget . canceled = true ;
2020-11-28 14:41:44 +00:00
this . whisperTarget = undefined ;
2020-11-28 11:58:22 +00:00
this . whisperTargetInitialize = undefined ;
this . connection . send_command ( "whispersessionreset" ) . catch ( error = > {
logWarn ( LogCategory . CLIENT , tr ( "Failed to clear the whisper target: %o" ) , error ) ;
} ) ;
}
this . handleRecorderStop ( ) ;
if ( this . currentAudioSource ? . input && ! this . currentAudioSource . input . isFiltered ( ) ) {
this . handleRecorderStart ( ) ;
}
}
2020-11-07 12:16:07 +00:00
private handleVoiceClientStateChange ( /* event: VoicePlayerEvents["notify_state_changed"] */ ) {
this . updateVoiceReplaying ( ) ;
}
private handleWhisperSessionStateChange() {
this . updateVoiceReplaying ( ) ;
}
private updateVoiceReplaying() {
2020-11-28 11:58:22 +00:00
let replaying ;
2020-11-07 12:16:07 +00:00
2020-11-28 11:58:22 +00:00
{
2020-11-07 12:16:07 +00:00
let index = this . availableVoiceClients ( ) . findIndex ( client = > client . getState ( ) === VoicePlayerState . PLAYING || client . getState ( ) === VoicePlayerState . BUFFERING ) ;
replaying = index !== - 1 ;
if ( ! replaying ) {
index = this . getWhisperSessions ( ) . findIndex ( session = > session . getSessionState ( ) === WhisperSessionState . PLAYING ) ;
replaying = index !== - 1 ;
}
}
if ( this . currentlyReplayingVoice !== replaying ) {
this . currentlyReplayingVoice = replaying ;
this . events . fire_later ( "notify_voice_replay_state_change" , { replaying : replaying } ) ;
}
}
private setConnectionState ( state : VoiceConnectionStatus ) {
if ( this . connectionState === state )
return ;
const oldState = this . connectionState ;
this . connectionState = state ;
this . events . fire ( "notify_connection_status_changed" , { newStatus : state , oldStatus : oldState } ) ;
}
private handleRtcConnectionStateChanged ( event : RTCConnectionEvents [ "notify_state_changed" ] ) {
switch ( event . newState ) {
case RTPConnectionState . CONNECTED :
2021-01-04 20:28:47 +00:00
this . rtcConnection . startAudioBroadcast ( ) . then ( ( ) = > {
2020-11-07 12:16:07 +00:00
logTrace ( LogCategory . VOICE , tr ( "Local audio broadcasting has been started successfully" ) ) ;
this . setConnectionState ( VoiceConnectionStatus . Connected ) ;
} ) . catch ( error = > {
logError ( LogCategory . VOICE , tr ( "Failed to start voice audio broadcasting: %o" ) , error ) ;
this . localFailedReason = tr ( "Failed to start audio broadcasting" ) ;
this . setConnectionState ( VoiceConnectionStatus . Failed ) ;
} ) ;
2020-11-28 11:58:22 +00:00
if ( this . whisperTarget ) {
this . startWhisper ( this . whisperTarget ) . catch ( error = > {
logError ( LogCategory . VOICE , tr ( "Failed to start voice whisper on connected rtc connection: &o" ) , error ) ;
/* TODO: Somehow abort the whisper and give the user a feedback? */
} ) ;
}
2020-11-07 12:16:07 +00:00
break ;
case RTPConnectionState . CONNECTING :
this . setConnectionState ( VoiceConnectionStatus . Connecting ) ;
break ;
case RTPConnectionState . DISCONNECTED :
this . setConnectionState ( VoiceConnectionStatus . Disconnected ) ;
break ;
case RTPConnectionState . FAILED :
this . localFailedReason = undefined ;
this . setConnectionState ( VoiceConnectionStatus . Failed ) ;
break ;
2020-11-17 13:27:46 +00:00
case RTPConnectionState . NOT_SUPPORTED :
this . setConnectionState ( VoiceConnectionStatus . ServerUnsupported ) ;
break ;
2020-11-07 12:16:07 +00:00
}
}
private handleAudioAssignmentChanged ( event : RTCConnectionEvents [ "notify_audio_assignment_changed" ] ) {
2020-11-28 11:58:22 +00:00
{
let oldClient = Object . values ( this . voiceClients ) . find ( client = > client . getRtpTrack ( ) === event . track ) ;
oldClient ? . setRtpTrack ( undefined ) ;
let oldSession = this . whisperSessions . find ( e = > e . getRtpTrack ( ) === event . track ) ;
oldSession ? . setRtpTrack ( undefined ) ;
2020-11-07 12:16:07 +00:00
}
if ( event . info ) {
2020-11-28 11:58:22 +00:00
if ( typeof event . info . media === "undefined" ) {
logWarn ( LogCategory . VOICE , tr ( "Received audio assignment event with missing media type" ) ) ;
return ;
}
switch ( event . info . media ) {
case 0 : {
const newClient = this . voiceClients [ event . info . client_id ] ;
if ( newClient ) {
newClient . setRtpTrack ( event . track ) ;
} else {
logWarn ( LogCategory . AUDIO , tr ( "Received audio track assignment for unknown voice client (%o)." ) , event . info ) ;
}
break ;
}
case 1 : {
let session = this . whisperSessions . find ( e = > e . getClientId ( ) === event . info . client_id ) ;
if ( typeof session === "undefined" ) {
logDebug ( LogCategory . VOICE , tr ( "Received new whisper from %d (%s)" ) , event . info . client_id , event . info . client_name ) ;
session = new RtpWhisperSession ( event . track , event . info ) ;
session . setGloballyMuted ( this . speakerMuted ) ;
this . whisperSessions . push ( session ) ;
session . events . on ( "notify_state_changed" , this . whisperSessionStateChangedEventListener ) ;
this . whisperSessionInitializer ( session ) . then ( result = > {
try {
session . initializeFromData ( result ) ;
this . events . fire ( "notify_whisper_initialized" , { session } ) ;
} catch ( error ) {
logError ( LogCategory . VOICE , tr ( "Failed to internally initialize a voice whisper session: %o" ) , error ) ;
session . setSessionState ( WhisperSessionState . INITIALIZE_FAILED ) ;
}
} ) . catch ( error = > {
logError ( LogCategory . VOICE , tr ( "Failed to initialize whisper session: %o." ) , error ) ;
session . initializeFailed ( ) ;
} ) ;
session . events . on ( "notify_timed_out" , ( ) = > {
logTrace ( LogCategory . VOICE , tr ( "Whisper session %d timed out. Dropping session." ) , session . getClientId ( ) ) ;
this . dropWhisperSession ( session ) ;
} ) ;
this . events . fire ( "notify_whisper_created" , { session : session } ) ;
2020-11-28 18:33:54 +00:00
} else {
session . setRtpTrack ( event . track ) ;
2020-11-28 11:58:22 +00:00
}
break ;
}
default :
logWarn ( LogCategory . VOICE , tr ( "Received audio assignment event with invalid media type (%o)" ) , event . info . media ) ;
break ;
2020-11-07 12:16:07 +00:00
}
}
}
2020-11-16 20:02:18 +00:00
async getConnectionStats ( ) : Promise < ConnectionStatistics > {
const stats = await this . rtcConnection . getConnectionStatistics ( ) ;
return {
bytesReceived : stats.voiceBytesReceived ,
bytesSend : stats.voiceBytesSent
} ;
}
2020-11-17 10:27:14 +00:00
private updateSpeakerState() {
const newState = this . connection . client . isSpeakerMuted ( ) || this . connection . client . isSpeakerDisabled ( ) ;
if ( this . speakerMuted === newState ) { return ; }
this . speakerMuted = newState ;
this . voiceClients . forEach ( client = > client . setGloballyMuted ( this . speakerMuted ) ) ;
2020-11-28 11:58:22 +00:00
this . whisperSessions . forEach ( session = > session . setGloballyMuted ( this . speakerMuted ) ) ;
2020-11-17 10:27:14 +00:00
}
2020-11-17 12:10:24 +00:00
getRetryTimestamp ( ) : number | 0 {
return this . rtcConnection . getRetryTimestamp ( ) ;
}
2020-11-07 12:16:07 +00:00
}