fix: Microphone should still work after a view change (#1835)
- Closes #1810
This commit is contained in:
@@ -20,12 +20,7 @@ import { localize } from '../localize/localize';
|
|||||||
import { FrigateCardError } from '../types';
|
import { FrigateCardError } from '../types';
|
||||||
import { desparsifyArrays } from '../utils/basic';
|
import { desparsifyArrays } from '../utils/basic';
|
||||||
import { isCompanionApp } from '../utils/companion';
|
import { isCompanionApp } from '../utils/companion';
|
||||||
import { CardConditionAPI, KeysState } from './types';
|
import { CardConditionAPI, KeysState, MicrophoneState } from './types';
|
||||||
|
|
||||||
interface MicrophoneConditionState {
|
|
||||||
connected?: boolean;
|
|
||||||
muted?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConditionState {
|
interface ConditionState {
|
||||||
view?: string;
|
view?: string;
|
||||||
@@ -37,7 +32,7 @@ interface ConditionState {
|
|||||||
displayMode?: ViewDisplayMode;
|
displayMode?: ViewDisplayMode;
|
||||||
triggered?: Set<string>;
|
triggered?: Set<string>;
|
||||||
interaction?: boolean;
|
interaction?: boolean;
|
||||||
microphone?: MicrophoneConditionState;
|
microphone?: MicrophoneState;
|
||||||
user?: CurrentUser;
|
user?: CurrentUser;
|
||||||
keys?: KeysState;
|
keys?: KeysState;
|
||||||
user_agent?: string;
|
user_agent?: string;
|
||||||
|
|||||||
@@ -1,36 +1,33 @@
|
|||||||
import { errorToConsole } from '../utils/basic';
|
import { errorToConsole } from '../utils/basic';
|
||||||
import { Timer } from '../utils/timer';
|
import { Timer } from '../utils/timer';
|
||||||
import { CardMicrophoneAPI } from './types';
|
import { CardMicrophoneAPI, MicrophoneState } from './types';
|
||||||
|
|
||||||
export type MicrophoneManagerListenerChange = 'muted' | 'unmuted';
|
export class MicrophoneManager {
|
||||||
type MicrophoneManagerListener = (change: MicrophoneManagerListenerChange) => void;
|
|
||||||
|
|
||||||
export interface ReadonlyMicrophoneManager {
|
|
||||||
getStream(): MediaStream | undefined;
|
|
||||||
addListener(listener: MicrophoneManagerListener): void;
|
|
||||||
removeListener(listener: MicrophoneManagerListener): void;
|
|
||||||
isConnected(): boolean;
|
|
||||||
isForbidden(): boolean;
|
|
||||||
isMuted(): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|
||||||
protected _api: CardMicrophoneAPI;
|
protected _api: CardMicrophoneAPI;
|
||||||
protected _stream?: MediaStream | null;
|
protected _stream?: MediaStream | null;
|
||||||
protected _timer = new Timer();
|
protected _timer = new Timer();
|
||||||
protected _listeners: MicrophoneManagerListener[] = [];
|
|
||||||
|
|
||||||
// We keep mute state separate from the stream state so that mute/unmute can
|
protected _state: MicrophoneState = {
|
||||||
// be expressed before the stream is created -- and when it's create it will
|
connected: false,
|
||||||
// have the right mute status.
|
muted: true,
|
||||||
protected _mute = 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) {
|
constructor(api: CardMicrophoneAPI) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getState(): MicrophoneState {
|
||||||
|
return this._state;
|
||||||
|
}
|
||||||
|
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
this._setConditionState();
|
this._setState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public shouldConnectOnInitialization(): boolean {
|
public shouldConnectOnInitialization(): boolean {
|
||||||
@@ -63,11 +60,11 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
|
|
||||||
this._stream = null;
|
this._stream = null;
|
||||||
this._api.getCardElementManager().update();
|
this._setState();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this._setMute();
|
this._setDesiredMuteOnStream();
|
||||||
this._setConditionState();
|
this._setState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +72,7 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
this._stream?.getTracks().forEach((track) => track.stop());
|
this._stream?.getTracks().forEach((track) => track.stop());
|
||||||
|
|
||||||
this._stream = undefined;
|
this._stream = undefined;
|
||||||
this._setConditionState();
|
this._setState();
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStream(): MediaStream | undefined {
|
public getStream(): MediaStream | undefined {
|
||||||
@@ -84,15 +80,9 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public mute(): void {
|
||||||
const wasMuted = this.isMuted();
|
this._desireMute = true;
|
||||||
|
this._setDesiredMuteOnStream();
|
||||||
this._mute = true;
|
this._setState();
|
||||||
this._setMute();
|
|
||||||
this._setConditionState();
|
|
||||||
|
|
||||||
if (!wasMuted) {
|
|
||||||
this._callListeners('muted');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
public async unmute(): Promise<void> {
|
||||||
@@ -100,29 +90,14 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wasUnmuted = !this.isMuted();
|
this._desireMute = false;
|
||||||
|
|
||||||
const unmute = (): void => {
|
|
||||||
this._mute = false;
|
|
||||||
this._setMute();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this.isConnected() && !this.isForbidden()) {
|
if (!this.isConnected() && !this.isForbidden()) {
|
||||||
// The connect() call is async and make take an arbitrary amount of
|
// Connecting will automatically set the desired mute.
|
||||||
// time for the user to grant access to their microphone. With a
|
|
||||||
// momentary microphone button the mute call (on mouse release) may
|
|
||||||
// arrive before the connection is even granted, so we unmute first
|
|
||||||
// before the connection is made, so the mute call on release will not
|
|
||||||
// be 'overwritten' incorrectly.
|
|
||||||
unmute();
|
|
||||||
await this.connect();
|
await this.connect();
|
||||||
} else if (this.isConnected()) {
|
} else if (this.isConnected()) {
|
||||||
unmute();
|
this._setDesiredMuteOnStream();
|
||||||
}
|
this._setState();
|
||||||
|
|
||||||
this._setConditionState();
|
|
||||||
if (!wasUnmuted) {
|
|
||||||
this._callListeners('unmuted');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,32 +111,19 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
|
|
||||||
public isMuted(): boolean {
|
public isMuted(): boolean {
|
||||||
// For safety, this function always returns the stream mute status directly
|
// For safety, this function always returns the stream mute status directly
|
||||||
// (rather the internal state).
|
// (rather the desired internal state).
|
||||||
return !this._stream || this._stream.getTracks().every((track) => !track.enabled);
|
return !this._stream || this._stream.getTracks().every((track) => !track.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addListener(listener: MicrophoneManagerListener): void {
|
protected _setDesiredMuteOnStream(): void {
|
||||||
this._listeners.push(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeListener(listener: MicrophoneManagerListener): void {
|
|
||||||
this._listeners = this._listeners.filter((l) => l !== listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _callListeners(change: MicrophoneManagerListenerChange): void {
|
|
||||||
this._listeners.forEach((listener) => listener(change));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _setMute(): void {
|
|
||||||
this._stream?.getTracks().forEach((track) => {
|
this._stream?.getTracks().forEach((track) => {
|
||||||
track.enabled = !this._mute;
|
track.enabled = !this._desireMute;
|
||||||
});
|
});
|
||||||
|
|
||||||
this._startTimer();
|
this._startDisconnectTimer();
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _startTimer(): void {
|
protected _startDisconnectTimer(): void {
|
||||||
const microphoneConfig = this._api.getConfigManager().getConfig()?.live.microphone;
|
const microphoneConfig = this._api.getConfigManager().getConfig()?.live.microphone;
|
||||||
|
|
||||||
if (microphoneConfig?.always_connected) {
|
if (microphoneConfig?.always_connected) {
|
||||||
@@ -177,12 +139,16 @@ export class MicrophoneManager implements ReadonlyMicrophoneManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _setConditionState(): void {
|
protected _setState(): void {
|
||||||
|
this._state = {
|
||||||
|
stream: this._stream,
|
||||||
|
connected: this.isConnected(),
|
||||||
|
muted: this.isMuted(),
|
||||||
|
forbidden: this.isForbidden(),
|
||||||
|
};
|
||||||
this._api.getConditionsManager().setState({
|
this._api.getConditionsManager().setState({
|
||||||
microphone: {
|
microphone: this._state,
|
||||||
muted: this.isMuted(),
|
|
||||||
connected: this.isConnected(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,14 @@ export interface KeysState {
|
|||||||
meta: boolean;
|
meta: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MicrophoneState {
|
||||||
|
stream?: MediaStream | null;
|
||||||
|
connected: boolean;
|
||||||
|
muted: boolean;
|
||||||
|
forbidden: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface TaggedAutomation extends Automation {
|
interface TaggedAutomation extends Automation {
|
||||||
tag?: unknown;
|
tag?: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -405,7 +405,7 @@ class FrigateCard extends LitElement {
|
|||||||
.getConditionsManager()
|
.getConditionsManager()
|
||||||
?.getEpoch()}
|
?.getEpoch()}
|
||||||
.hide=${!!this._controller.getMessageManager().hasMessage()}
|
.hide=${!!this._controller.getMessageManager().hasMessage()}
|
||||||
.microphoneManager=${this._controller.getMicrophoneManager()}
|
.microphoneState=${this._controller.getMicrophoneManager().getState()}
|
||||||
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
||||||
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
||||||
: undefined}
|
: undefined}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import {
|
import { MicrophoneState } from '../card-controller/types.js';
|
||||||
MicrophoneManagerListenerChange,
|
|
||||||
ReadonlyMicrophoneManager,
|
|
||||||
} from '../card-controller/microphone-manager.js';
|
|
||||||
import {
|
import {
|
||||||
AutoMuteCondition,
|
AutoMuteCondition,
|
||||||
AutoPauseCondition,
|
AutoPauseCondition,
|
||||||
@@ -20,7 +17,7 @@ export interface MediaActionsControllerOptions {
|
|||||||
autoPauseConditions?: readonly AutoPauseCondition[];
|
autoPauseConditions?: readonly AutoPauseCondition[];
|
||||||
autoMuteConditions?: readonly AutoMuteCondition[];
|
autoMuteConditions?: readonly AutoMuteCondition[];
|
||||||
|
|
||||||
microphoneManager?: ReadonlyMicrophoneManager;
|
microphoneState?: MicrophoneState;
|
||||||
microphoneMuteSeconds?: number;
|
microphoneMuteSeconds?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,12 +51,14 @@ export class MediaActionsController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
public setOptions(options: MediaActionsControllerOptions): void {
|
public setOptions(options: MediaActionsControllerOptions): void {
|
||||||
this._options = options;
|
if (this._options?.microphoneState !== options.microphoneState) {
|
||||||
|
this._microphoneStateChangeHandler(
|
||||||
if (this._options?.microphoneManager) {
|
this._options?.microphoneState,
|
||||||
this._options.microphoneManager.removeListener(this._microphoneChangeHandler);
|
options.microphoneState,
|
||||||
this._options.microphoneManager.addListener(this._microphoneChangeHandler);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasRoot(): boolean {
|
public hasRoot(): boolean {
|
||||||
@@ -75,7 +74,6 @@ export class MediaActionsController {
|
|||||||
this._target = null;
|
this._target = null;
|
||||||
this._mutationObserver.disconnect();
|
this._mutationObserver.disconnect();
|
||||||
this._intersectionObserver.disconnect();
|
this._intersectionObserver.disconnect();
|
||||||
this._options?.microphoneManager?.removeListener(this._microphoneChangeHandler);
|
|
||||||
document.removeEventListener('visibilitychange', this._visibilityHandler);
|
document.removeEventListener('visibilitychange', this._visibilityHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,13 +253,20 @@ export class MediaActionsController {
|
|||||||
await this._muteAllIfConfigured('hidden');
|
await this._muteAllIfConfigured('hidden');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
protected _microphoneChangeHandler = async (
|
|
||||||
change: MicrophoneManagerListenerChange,
|
protected async _microphoneStateChangeHandler(
|
||||||
): Promise<void> => {
|
oldState?: MicrophoneState,
|
||||||
if (change === 'unmuted') {
|
newState?: MicrophoneState,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!oldState || !newState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldState.muted && !newState.muted) {
|
||||||
await this._unmuteTargetIfConfigured('microphone');
|
await this._unmuteTargetIfConfigured('microphone');
|
||||||
} else if (
|
} else if (
|
||||||
change === 'muted' &&
|
!oldState.muted &&
|
||||||
|
newState.muted &&
|
||||||
this._options?.autoMuteConditions?.includes('microphone')
|
this._options?.autoMuteConditions?.includes('microphone')
|
||||||
) {
|
) {
|
||||||
this._microphoneMuteTimer.start(
|
this._microphoneMuteTimer.start(
|
||||||
@@ -271,5 +276,5 @@ export class MediaActionsController {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
ConditionsManagerEpoch,
|
ConditionsManagerEpoch,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
} from '../../card-controller/conditions-manager.js';
|
} from '../../card-controller/conditions-manager.js';
|
||||||
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
import { MediaActionsController } from '../../components-lib/media-actions-controller.js';
|
||||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||||
@@ -88,7 +88,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneManager?: ReadonlyMicrophoneManager;
|
public microphoneState?: MicrophoneState;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public viewFilterCameraID?: string;
|
public viewFilterCameraID?: string;
|
||||||
@@ -139,7 +139,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (
|
if (
|
||||||
changedProps.has('microphoneManager') ||
|
changedProps.has('microphoneState') ||
|
||||||
changedProps.has('overriddenLiveConfig')
|
changedProps.has('overriddenLiveConfig')
|
||||||
) {
|
) {
|
||||||
this._mediaActionsController.setOptions({
|
this._mediaActionsController.setOptions({
|
||||||
@@ -158,7 +158,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}),
|
}),
|
||||||
...((this.overriddenLiveConfig?.auto_unmute ||
|
...((this.overriddenLiveConfig?.auto_unmute ||
|
||||||
this.overriddenLiveConfig?.auto_mute) && {
|
this.overriddenLiveConfig?.auto_mute) && {
|
||||||
microphoneManager: this.microphoneManager,
|
microphoneState: this.microphoneState,
|
||||||
microphoneMuteSeconds:
|
microphoneMuteSeconds:
|
||||||
this.overriddenLiveConfig.microphone.mute_after_microphone_mute_seconds,
|
this.overriddenLiveConfig.microphone.mute_after_microphone_mute_seconds,
|
||||||
}),
|
}),
|
||||||
@@ -302,8 +302,8 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
<div class="embla__slide">
|
<div class="embla__slide">
|
||||||
<frigate-card-live-provider
|
<frigate-card-live-provider
|
||||||
?load=${!liveConfig.lazy_load}
|
?load=${!liveConfig.lazy_load}
|
||||||
.microphoneStream=${view?.camera === cameraID
|
.microphoneState=${view?.camera === cameraID
|
||||||
? this.microphoneManager?.getStream()
|
? this.microphoneState
|
||||||
: undefined}
|
: undefined}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
.cameraEndpoints=${guard(
|
.cameraEndpoints=${guard(
|
||||||
@@ -429,7 +429,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
.loop=${hasMultipleCameras}
|
.loop=${hasMultipleCameras}
|
||||||
.dragEnabled=${hasMultipleCameras && this.overriddenLiveConfig?.draggable}
|
.dragEnabled=${hasMultipleCameras && this.overriddenLiveConfig?.draggable}
|
||||||
.plugins=${guard(
|
.plugins=${guard(
|
||||||
[this.cameraManager, this.overriddenLiveConfig, this.microphoneManager],
|
[this.cameraManager, this.overriddenLiveConfig],
|
||||||
this._getPlugins.bind(this),
|
this._getPlugins.bind(this),
|
||||||
)}
|
)}
|
||||||
.selected=${this._getSelectedCameraIndex()}
|
.selected=${this._getSelectedCameraIndex()}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
||||||
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
|
||||||
import { CardWideConfig, LiveConfig, Overrides } from '../../config/types.js';
|
import { CardWideConfig, LiveConfig, Overrides } from '../../config/types.js';
|
||||||
@@ -46,7 +46,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
|||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneManager?: ReadonlyMicrophoneManager;
|
public microphoneState?: MicrophoneState;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public triggeredCameraIDs?: Set<string>;
|
public triggeredCameraIDs?: Set<string>;
|
||||||
@@ -67,7 +67,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
|||||||
.overrides=${this.overrides}
|
.overrides=${this.overrides}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.microphoneManager=${this.microphoneManager}
|
.microphoneState=${this.microphoneState}
|
||||||
?triggered=${triggeredCameraID &&
|
?triggered=${triggeredCameraID &&
|
||||||
!!this.triggeredCameraIDs?.has(triggeredCameraID)}
|
!!this.triggeredCameraIDs?.has(triggeredCameraID)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
import { ConditionsManagerEpoch } from '../../card-controller/conditions-manager.js';
|
||||||
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||||
import { LiveController } from '../../components-lib/live/live-controller.js';
|
import { LiveController } from '../../components-lib/live/live-controller.js';
|
||||||
import { CardWideConfig, LiveConfig, Overrides } from '../../config/types.js';
|
import { CardWideConfig, LiveConfig, Overrides } from '../../config/types.js';
|
||||||
@@ -38,7 +38,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneManager?: ReadonlyMicrophoneManager;
|
public microphoneState?: MicrophoneState;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public triggeredCameraIDs?: Set<string>;
|
public triggeredCameraIDs?: Set<string>;
|
||||||
@@ -67,7 +67,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
.overrides=${this.overrides}
|
.overrides=${this.overrides}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.microphoneManager=${this.microphoneManager}
|
.microphoneState=${this.microphoneState}
|
||||||
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
||||||
>
|
>
|
||||||
</frigate-card-live-grid>
|
</frigate-card-live-grid>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { guard } from 'lit/directives/guard.js';
|
import { guard } from 'lit/directives/guard.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
|
import { MicrophoneState } from '../../card-controller/types.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||||
import {
|
import {
|
||||||
@@ -64,7 +65,7 @@ export class FrigateCardLiveProvider
|
|||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneStream?: MediaStream;
|
public microphoneState?: MicrophoneState;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public zoomSettings?: PartialZoomSettings | null;
|
public zoomSettings?: PartialZoomSettings | null;
|
||||||
@@ -351,7 +352,7 @@ export class FrigateCardLiveProvider
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${this.cameraConfig}
|
.cameraConfig=${this.cameraConfig}
|
||||||
.cameraEndpoints=${this.cameraEndpoints}
|
.cameraEndpoints=${this.cameraEndpoints}
|
||||||
.microphoneStream=${this.microphoneStream}
|
.microphoneState=${this.microphoneState}
|
||||||
.microphoneConfig=${this.liveConfig.microphone}
|
.microphoneConfig=${this.liveConfig.microphone}
|
||||||
?controls=${this.liveConfig.controls.builtin}
|
?controls=${this.liveConfig.controls.builtin}
|
||||||
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
@frigate-card:live:error=${() => this._providerErrorHandler()}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
||||||
|
import { MicrophoneState } from '../../../../card-controller/types.js';
|
||||||
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
|
import { CameraConfig, MicrophoneConfig } from '../../../../config/types.js';
|
||||||
import { localize } from '../../../../localize/localize.js';
|
import { localize } from '../../../../localize/localize.js';
|
||||||
@@ -44,7 +45,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
public cameraEndpoints?: CameraEndpoints;
|
public cameraEndpoints?: CameraEndpoints;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneStream?: MediaStream;
|
public microphoneState?: MicrophoneState;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneConfig?: MicrophoneConfig;
|
public microphoneConfig?: MicrophoneConfig;
|
||||||
@@ -147,7 +148,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
|
|
||||||
this._player = new VideoRTC();
|
this._player = new VideoRTC();
|
||||||
this._player.containingPlayer = this;
|
this._player.containingPlayer = this;
|
||||||
this._player.microphoneStream = this.microphoneStream ?? null;
|
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||||
this._player.src = address;
|
this._player.src = address;
|
||||||
this._player.visibilityCheck = false;
|
this._player.visibilityCheck = false;
|
||||||
this._player.controls = this.controls;
|
this._player.controls = this.controls;
|
||||||
@@ -172,14 +173,16 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
this._player.controls = this.controls;
|
this._player.controls = this.controls;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._player && changedProps.has('microphoneStream')) {
|
if (
|
||||||
if (this._player?.microphoneStream !== this.microphoneStream) {
|
this._player &&
|
||||||
this._player.microphoneStream = this.microphoneStream ?? null;
|
changedProps.has('microphoneState') &&
|
||||||
|
this._player?.microphoneStream !== this.microphoneState?.stream
|
||||||
|
) {
|
||||||
|
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||||
|
|
||||||
// Need to force a reconnect if the microphone stream changes since
|
// Need to force a reconnect if the microphone stream changes since
|
||||||
// WebRTC cannot introduce a new stream after the offer is already made.
|
// WebRTC cannot introduce a new stream after the offer is already made.
|
||||||
this._player.reconnect();
|
this._player.reconnect();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,9 +394,10 @@ export class VideoRTC extends HTMLElement {
|
|||||||
|
|
||||||
this.pcState = WebSocket.CLOSED;
|
this.pcState = WebSocket.CLOSED;
|
||||||
if (this.pc) {
|
if (this.pc) {
|
||||||
this.pc.getSenders().forEach((sender) => {
|
// Do not close the (microphone) track attached to the peer connection as
|
||||||
if (sender.track) sender.track.stop();
|
// that is controlled by MicrophoneManager.
|
||||||
});
|
// See: https://github.com/dermotduffy/frigate-hass-card/issues/1810
|
||||||
|
|
||||||
this.pc.close();
|
this.pc.close();
|
||||||
this.pc = null;
|
this.pc = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js';
|
import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js';
|
||||||
import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js';
|
import { MicrophoneState } from '../card-controller/types.js';
|
||||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||||
import {
|
import {
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
@@ -60,7 +60,7 @@ export class FrigateCardViews extends LitElement {
|
|||||||
public hide?: boolean;
|
public hide?: boolean;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public microphoneManager?: ReadonlyMicrophoneManager;
|
public microphoneState?: MicrophoneState;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public triggeredCameraIDs?: Set<string>;
|
public triggeredCameraIDs?: Set<string>;
|
||||||
@@ -243,7 +243,7 @@ export class FrigateCardViews extends LitElement {
|
|||||||
.overrides=${this.overriddenConfig.overrides}
|
.overrides=${this.overriddenConfig.overrides}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.microphoneManager=${this.microphoneManager}
|
.microphoneState=${this.microphoneState}
|
||||||
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
.triggeredCameraIDs=${this.triggeredCameraIDs}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||||
|
import { MicrophoneState } from '../../src/card-controller/types';
|
||||||
import { createCardAPI, createConfig } from '../test-utils';
|
import { createCardAPI, createConfig } from '../test-utils';
|
||||||
|
|
||||||
const navigatorMock: Navigator = {
|
const navigatorMock: Navigator = {
|
||||||
@@ -162,7 +163,7 @@ describe('MicrophoneManager', () => {
|
|||||||
expect(manager.isConnected()).toBeTruthy();
|
expect(manager.isConnected()).toBeTruthy();
|
||||||
expect(manager.isMuted()).toBeFalsy();
|
expect(manager.isMuted()).toBeFalsy();
|
||||||
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
expect(api.getCardElementManager().update).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disconnect', async () => {
|
it('should disconnect', async () => {
|
||||||
@@ -296,95 +297,82 @@ describe('MicrophoneManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect listeners', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(
|
|
||||||
createMockStream(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const listener = vi.fn();
|
|
||||||
manager.addListener(listener);
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
expect(listener).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
manager.mute();
|
|
||||||
expect(listener).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await manager.unmute();
|
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
|
||||||
expect(listener).toHaveBeenLastCalledWith('unmuted');
|
|
||||||
|
|
||||||
await manager.unmute();
|
|
||||||
expect(listener).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
manager.mute();
|
|
||||||
expect(listener).toHaveBeenCalledTimes(2);
|
|
||||||
expect(listener).toHaveBeenLastCalledWith('muted');
|
|
||||||
|
|
||||||
manager.removeListener(listener);
|
|
||||||
|
|
||||||
await manager.unmute();
|
|
||||||
expect(listener).toHaveBeenCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should initialize', () => {
|
it('should initialize', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new MicrophoneManager(api);
|
const manager = new MicrophoneManager(api);
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
expect(api.getConditionsManager().setState).toBeCalledWith({
|
||||||
microphone: { connected: false, muted: true },
|
microphone: { connected: false, muted: true, forbidden: false, stream: undefined },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set condition state', async () => {
|
it('should set state', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new MicrophoneManager(api);
|
const manager = new MicrophoneManager(api);
|
||||||
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(
|
const stream = createMockStream();
|
||||||
createMockStream(),
|
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(stream);
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
expect(api.getConditionsManager().setState).not.toBeCalled();
|
||||||
|
|
||||||
await manager.connect();
|
await manager.connect();
|
||||||
|
|
||||||
|
let expectedState: MicrophoneState = {
|
||||||
|
forbidden: false,
|
||||||
|
stream: stream,
|
||||||
|
connected: true,
|
||||||
|
muted: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: {
|
microphone: expectedState,
|
||||||
connected: true,
|
|
||||||
muted: true,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await manager.unmute();
|
await manager.unmute();
|
||||||
|
|
||||||
|
expectedState = {
|
||||||
|
forbidden: false,
|
||||||
|
stream: stream,
|
||||||
|
connected: true,
|
||||||
|
muted: false,
|
||||||
|
};
|
||||||
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: {
|
microphone: expectedState,
|
||||||
connected: true,
|
|
||||||
muted: false,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
manager.mute();
|
manager.mute();
|
||||||
|
|
||||||
|
expectedState = {
|
||||||
|
forbidden: false,
|
||||||
|
stream: stream,
|
||||||
|
connected: true,
|
||||||
|
muted: true,
|
||||||
|
};
|
||||||
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: {
|
microphone: expectedState,
|
||||||
connected: true,
|
|
||||||
muted: true,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
manager.disconnect();
|
manager.disconnect();
|
||||||
|
|
||||||
|
expectedState = {
|
||||||
|
forbidden: false,
|
||||||
|
stream: undefined,
|
||||||
|
connected: false,
|
||||||
|
muted: true,
|
||||||
|
};
|
||||||
|
expect(manager.getState()).toEqual(expectedState);
|
||||||
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
expect(api.getConditionsManager().setState).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
microphone: {
|
microphone: expectedState,
|
||||||
connected: false,
|
|
||||||
muted: true,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import { MicrophoneState } from '../../src/card-controller/types';
|
||||||
MicrophoneManagerListenerChange,
|
|
||||||
ReadonlyMicrophoneManager,
|
|
||||||
} from '../../src/card-controller/microphone-manager';
|
|
||||||
import {
|
import {
|
||||||
MediaActionsController,
|
MediaActionsController,
|
||||||
MediaActionsControllerOptions,
|
MediaActionsControllerOptions,
|
||||||
@@ -17,7 +14,6 @@ import {
|
|||||||
flushPromises,
|
flushPromises,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
import { callVisibilityHandler, createTestSlideNodes } from '../utils/embla/test-utils';
|
import { callVisibilityHandler, createTestSlideNodes } from '../utils/embla/test-utils';
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
|
|
||||||
const getPlayer = (
|
const getPlayer = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
@@ -50,15 +46,6 @@ const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
|||||||
return divs;
|
return divs;
|
||||||
};
|
};
|
||||||
|
|
||||||
const callMicrophoneListener = (
|
|
||||||
microphoneManager: ReadonlyMicrophoneManager,
|
|
||||||
action: MicrophoneManagerListenerChange,
|
|
||||||
n = 0,
|
|
||||||
): void => {
|
|
||||||
const mock = vi.mocked(microphoneManager.addListener).mock;
|
|
||||||
mock.calls[n][0](action);
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('MediaActionsController', () => {
|
describe('MediaActionsController', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -541,7 +528,7 @@ describe('MediaActionsController', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should take action on microphone changes', () => {
|
describe('should take action on microphone state changes', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
});
|
});
|
||||||
@@ -550,14 +537,24 @@ describe('MediaActionsController', () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createMicrophoneState = (
|
||||||
|
state?: Partial<MicrophoneState>,
|
||||||
|
): MicrophoneState => {
|
||||||
|
return {
|
||||||
|
muted: true,
|
||||||
|
forbidden: false,
|
||||||
|
connected: false,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
it('should unmute when microphone unmuted', async () => {
|
it('should unmute when microphone unmuted', async () => {
|
||||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
|
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoUnmuteConditions: ['microphone' as const],
|
autoUnmuteConditions: ['microphone' as const],
|
||||||
playerSelector: 'video',
|
playerSelector: 'video',
|
||||||
microphoneManager: microphoneManager,
|
microphoneState: createMicrophoneState({ muted: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
@@ -565,19 +562,22 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
callMicrophoneListener(microphoneManager, 'unmuted');
|
controller.setOptions({
|
||||||
|
autoUnmuteConditions: ['microphone' as const],
|
||||||
|
playerSelector: 'video',
|
||||||
|
microphoneState: createMicrophoneState({ muted: false }),
|
||||||
|
});
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.unmute).toBeCalled();
|
expect(getPlayer(children[0], 'video')?.unmute).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should re-mute after delay after microphone unmuted', async () => {
|
it('should mute after delay after microphone muted', async () => {
|
||||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
|
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoMuteConditions: ['microphone' as const],
|
autoMuteConditions: ['microphone' as const],
|
||||||
playerSelector: 'video',
|
playerSelector: 'video',
|
||||||
microphoneManager: microphoneManager,
|
microphoneState: createMicrophoneState({ muted: false }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
@@ -585,21 +585,24 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
callMicrophoneListener(microphoneManager, 'muted');
|
controller.setOptions({
|
||||||
|
autoMuteConditions: ['microphone' as const],
|
||||||
|
playerSelector: 'video',
|
||||||
|
microphoneState: createMicrophoneState({ muted: true }),
|
||||||
|
});
|
||||||
|
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
|
expect(getPlayer(children[0], 'video')?.mute).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not re-mute after delay after microphone unmuted', async () => {
|
it('should not mute after delay after microphone muted', async () => {
|
||||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
|
||||||
const controller = new MediaActionsController();
|
const controller = new MediaActionsController();
|
||||||
|
|
||||||
controller.setOptions({
|
controller.setOptions({
|
||||||
autoMuteConditions: [],
|
autoMuteConditions: [],
|
||||||
playerSelector: 'video',
|
playerSelector: 'video',
|
||||||
microphoneManager: microphoneManager,
|
microphoneState: createMicrophoneState({ muted: false }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const children = createPlayerSlideNodes();
|
const children = createPlayerSlideNodes();
|
||||||
@@ -607,7 +610,11 @@ describe('MediaActionsController', () => {
|
|||||||
|
|
||||||
await controller.setTarget(0, true);
|
await controller.setTarget(0, true);
|
||||||
|
|
||||||
callMicrophoneListener(microphoneManager, 'muted');
|
controller.setOptions({
|
||||||
|
autoMuteConditions: ['microphone' as const],
|
||||||
|
playerSelector: 'video',
|
||||||
|
microphoneState: createMicrophoneState({ muted: true }),
|
||||||
|
});
|
||||||
|
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user