diff --git a/client/src/components/controls.vue b/client/src/components/controls.vue index 9307a12bc..5ec91a7de 100644 --- a/client/src/components/controls.vue +++ b/client/src/components/controls.vue @@ -53,6 +53,24 @@ @click.stop.prevent="toggleMedia" /> +
  • + +
  • diff --git a/client/src/locale/en-us.ts b/client/src/locale/en-us.ts index edc7f2bb0..47f3c47ef 100644 --- a/client/src/locale/en-us.ts +++ b/client/src/locale/en-us.ts @@ -52,6 +52,9 @@ export const controls = { unlock: 'Unlock Controls', has: 'You have control', hasnot: 'You do not have control', + mic_on: 'Enable Microphone', + mic_off: 'Disable Microphone', + mic_error: 'Microphone Error', } export const locks = { diff --git a/client/src/neko/base.ts b/client/src/neko/base.ts index 12828595d..8ea87659f 100644 --- a/client/src/neko/base.ts +++ b/client/src/neko/base.ts @@ -28,6 +28,9 @@ export abstract class BaseClient extends EventEmitter { protected _state: RTCIceConnectionState = 'disconnected' protected _id = '' protected _candidates: RTCIceCandidate[] = [] + protected _micStream?: MediaStream + protected _micSender?: RTCRtpSender + protected _micActive = false get id() { return this._id @@ -128,11 +131,59 @@ export abstract class BaseClient extends EventEmitter { this._peer = undefined } + this.disableMicrophone() + this._state = 'disconnected' this._displayname = undefined this._id = '' } + get microphoneActive() { + return this._micActive + } + + public async enableMicrophone(): Promise { + if (!this._peer) { + this.emit('warn', 'attempting to enable microphone with no peer connection') + return + } + + if (this._micActive) { + this.emit('debug', 'microphone already active') + return + } + + try { + this._micStream = await navigator.mediaDevices.getUserMedia({ audio: true }) + const audioTrack = this._micStream.getAudioTracks()[0] + this._micSender = this._peer.addTrack(audioTrack, this._micStream) + this._micActive = true + this.emit('info', `microphone enabled: ${audioTrack.label}`) + } catch (err: any) { + this.emit('error', err) + throw err + } + } + + public disableMicrophone(): void { + if (this._micSender && this._peer) { + try { + this._peer.removeTrack(this._micSender) + } catch (err) { + this.emit('warn', 'failed to remove mic track from peer', err) + } + this._micSender = undefined + } + + if (this._micStream) { + this._micStream.getTracks().forEach((t) => t.stop()) + this._micStream = undefined + } + + this._micActive = false + this.emit('info', 'microphone disabled') + } + public sendData(event: 'wheel' | 'mousemove', data: { x: number; y: number }): void public sendData(event: 'mousedown' | 'mouseup' | 'keydown' | 'keyup', data: { key: number }): void public sendData(event: string, data: any) { diff --git a/server/internal/webrtc/manager.go b/server/internal/webrtc/manager.go index 541675f2f..d6b6d9db0 100644 --- a/server/internal/webrtc/manager.go +++ b/server/internal/webrtc/manager.go @@ -1,7 +1,9 @@ package webrtc import ( + "errors" "fmt" + "io" "net" "strings" "sync" @@ -459,7 +461,10 @@ func (manager *WebRTCManagerCtx) CreatePeer(session types.Session) (*webrtc.Sess for { i, _, err := track.Read(buf) if err != nil { - logger.Warn().Err(err).Msg("failed read from remote track") + // if the error is not io.EOF, log it. Otherwise, it's a normal closure of the track. + if !errors.Is(err, io.EOF) { + logger.Warn().Err(err).Msg("failed read from remote track") + } break }