Files
advanced-camera-card/src/card-controller/microphone-manager.ts
T
Dermot Duffy 8d3cf07b43 refactor: Significantly refactor how conditions work internally (#1886)
- Much improved API cleanliness and testability to allow further
extensibility in future
 - Simplified code in `live` view

This technically contains a small change in how overrides work in the
`live` view. Since that change is _closer_ to the documentation, and
since this is likely to be rarely used, this is not considered a
breaking change. Previously, overrides for a given live camera would
always render _as if_ that camera was selected, vs was actually
selected. Now, overrides will only apply in the live view when the
camera is _actually_ selected. If this is an issue for you in practice,
lets discuss.
2025-02-11 20:07:55 -08:00

155 lines
3.8 KiB
TypeScript

import { errorToConsole } from '../utils/basic';
import { Timer } from '../utils/timer';
import { CardMicrophoneAPI, MicrophoneState } from './types';
export class MicrophoneManager {
protected _api: CardMicrophoneAPI;
protected _stream?: MediaStream | null;
protected _timer = new Timer();
protected _state: MicrophoneState = {
connected: false,
muted: true,
forbidden: false,
};
// We keep desired mute state separate from the overall state so that
// mute/unmute can be expressed before the stream is even created -- and when
// it's created it will have the right mute status.
protected _desireMute = true;
constructor(api: CardMicrophoneAPI) {
this._api = api;
}
public getState(): MicrophoneState {
return this._state;
}
public initialize(): void {
this._setState();
}
public shouldConnectOnInitialization(): boolean {
return (
!!this._api.getConfigManager().getConfig()?.live.microphone?.always_connected &&
// If it won't be possible to connect the microphone at all, we do not
// block the initialization of the card (the microphone just won't work)
this.isSupported()
);
}
public isSupported(): boolean {
// Some browsers will have mediaDevices/getUserMedia as undefined if
// accessed over http.
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1543
return !!navigator.mediaDevices?.getUserMedia;
}
public async connect(): Promise<boolean> {
if (!this.isSupported()) {
return false;
}
try {
this._stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: false,
});
} catch (e: unknown) {
errorToConsole(e as Error);
this._stream = null;
this._setState();
return false;
}
this._setDesiredMuteOnStream();
this._setState();
return true;
}
public disconnect(): void {
this._stream?.getTracks().forEach((track) => track.stop());
this._stream = undefined;
this._setState();
}
public getStream(): MediaStream | undefined {
return this._stream ?? undefined;
}
public mute(): void {
this._desireMute = true;
this._setDesiredMuteOnStream();
this._setState();
}
public async unmute(): Promise<void> {
if (!this.isSupported()) {
return;
}
this._desireMute = false;
if (!this.isConnected() && !this.isForbidden()) {
// Connecting will automatically set the desired mute.
await this.connect();
} else if (this.isConnected()) {
this._setDesiredMuteOnStream();
this._setState();
}
}
public isConnected(): boolean {
return !!this._stream;
}
public isForbidden(): boolean {
return this._stream === null;
}
public isMuted(): boolean {
// For safety, this function always returns the stream mute status directly
// (rather the desired internal state).
return !this._stream || this._stream.getTracks().every((track) => !track.enabled);
}
protected _setDesiredMuteOnStream(): void {
this._stream?.getTracks().forEach((track) => {
track.enabled = !this._desireMute;
});
this._startDisconnectTimer();
}
protected _startDisconnectTimer(): void {
const microphoneConfig = this._api.getConfigManager().getConfig()?.live.microphone;
if (microphoneConfig?.always_connected) {
return;
}
const disconnectSeconds = microphoneConfig?.disconnect_seconds ?? 0;
if (disconnectSeconds) {
this._timer.start(disconnectSeconds, () => {
this.disconnect();
});
}
}
protected _setState(): void {
this._state = {
stream: this._stream,
connected: this.isConnected(),
muted: this.isMuted(),
forbidden: this.isForbidden(),
};
this._api.getConditionStateManager().setState({
microphone: this._state,
});
this._api.getCardElementManager().update();
}
}