fix: Remove the microphone selection conditions (#2686)
- Closes: #2679 BREAKING CHANGE: `selected` and `unselected` are removed from `live.microphone.auto_unmute` / `auto_mute`. A camera change ends any call and the microphone only carries audio during a call, so neither were useful.
This commit is contained in:
@@ -31,10 +31,11 @@ export const getLiveAutoUnmuteOptions = (): HASelectSelectorOption[] => [
|
||||
condition('call', 'call_unmute'),
|
||||
];
|
||||
|
||||
export const getMicrophoneMuteOptions = (): HASelectSelectorOption[] =>
|
||||
getMediaActionNegativeOptions();
|
||||
export const getMicrophoneMuteOptions = (): HASelectSelectorOption[] => [
|
||||
condition('hidden'),
|
||||
];
|
||||
|
||||
export const getMicrophoneUnmuteOptions = (): HASelectSelectorOption[] => [
|
||||
...getMediaActionPositiveOptions(),
|
||||
condition('visible'),
|
||||
condition('call', 'call_unmute'),
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { CallSession } from '../../card-controller/call/types.js';
|
||||
import type { MicrophoneManager } from '../../card-controller/microphone-manager.js';
|
||||
import type {
|
||||
MicrophoneAutoMuteCondition,
|
||||
@@ -17,16 +18,13 @@ interface MicrophoneActionsControllerOptions {
|
||||
* The microphone is a global singleton so cannot be controlled by
|
||||
* MediaActionsController without clashes between different controllers for
|
||||
* different cameras. This gives:
|
||||
* - deterministic ordering on selection change (this single owner sequences
|
||||
* 'unselected' for the leaver and 'selected' for the arriver),
|
||||
* - a single intersection observer scoped to the live root (correct
|
||||
* 'visible'/'hidden' semantics for the whole live view, not per-cell), and
|
||||
* - a single document.visibilitychange listener.
|
||||
*/
|
||||
export class MicrophoneActionsController {
|
||||
private _options: MicrophoneActionsControllerOptions | null = null;
|
||||
private _selectedCamera: string | null = null;
|
||||
private _callAnswered = false;
|
||||
private _answeredCall: CallSession | null = null;
|
||||
private _visibilityObserver: VisibilityObserver;
|
||||
|
||||
constructor() {
|
||||
@@ -40,23 +38,28 @@ export class MicrophoneActionsController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the controller of the call-answered state (outbound calls are
|
||||
* Notifies the controller of the active call session (outbound calls are
|
||||
* answered at start; inbound calls only become answered when the user
|
||||
* accepts). Acts only on a genuine transition. The initial state is treated
|
||||
* as unanswered, so a first-ever `true` counts -- the call rules apply even
|
||||
* when the live view first appears during an answered call.
|
||||
* accepts). A session that is still ringing unmutes nothing, so the user is
|
||||
* never heard before accepting.
|
||||
*
|
||||
* Call answer unmutes the microphone only if the user opted into
|
||||
* Each session is a distinct object, so a call that replaces another is
|
||||
* acted on in its own right: the microphone connection carries over, but the
|
||||
* user is muted or unmuted by the new call's own rules rather than
|
||||
* inheriting where the previous call left them.
|
||||
*
|
||||
* An answered session unmutes the microphone only if the user opted into
|
||||
* `microphone.auto_unmute: ['call']`. There is no symmetric mute: the
|
||||
* microphone manager mutes and releases the microphone itself when the call
|
||||
* ends.
|
||||
*/
|
||||
public async setCallAnswered(answered: boolean): Promise<void> {
|
||||
if (answered === this._callAnswered) {
|
||||
public async setCall(call?: CallSession): Promise<void> {
|
||||
const answeredCall = call?.answered ? call : null;
|
||||
if (answeredCall === this._answeredCall) {
|
||||
return;
|
||||
}
|
||||
this._callAnswered = answered;
|
||||
if (answered) {
|
||||
this._answeredCall = answeredCall;
|
||||
if (answeredCall) {
|
||||
await this._unmuteIfConfigured('call');
|
||||
}
|
||||
}
|
||||
@@ -66,30 +69,9 @@ export class MicrophoneActionsController {
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this._selectedCamera = null;
|
||||
this._visibilityObserver.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the controller of the currently selected camera. Called from the
|
||||
* live root whenever view.camera changes. Fires 'unselected' for the previous
|
||||
* camera (if any) and 'selected' for the new camera (if any), in that order.
|
||||
*/
|
||||
public async setSelectedCamera(camera: string | null): Promise<void> {
|
||||
if (this._selectedCamera === camera) {
|
||||
return;
|
||||
}
|
||||
const previous = this._selectedCamera;
|
||||
this._selectedCamera = camera;
|
||||
|
||||
if (previous !== null) {
|
||||
this._muteIfConfigured('unselected');
|
||||
}
|
||||
if (camera !== null) {
|
||||
await this._unmuteIfConfigured('selected');
|
||||
}
|
||||
}
|
||||
|
||||
private _changeVisibility = async (visible: boolean): Promise<void> => {
|
||||
if (visible) {
|
||||
await this._unmuteIfConfigured('visible');
|
||||
|
||||
@@ -81,26 +81,8 @@ export class AdvancedCameraCardLive extends LitElement {
|
||||
autoUnmuteConditions: this.liveConfig?.microphone.auto_unmute,
|
||||
});
|
||||
}
|
||||
if (changedProps.has('viewManagerEpoch')) {
|
||||
// The live element is also rendered (and this willUpdate runs) when
|
||||
// `live.preload` is true even while the user is on gallery/viewer/
|
||||
// timeline. `view.camera` is set across all views, so without this gate
|
||||
// `auto_unmute: ['selected']` would prompt for / open the microphone
|
||||
// from a hidden live view. Treat the live view as having no selected
|
||||
// camera unless it is the active view.
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
void this._microphoneActionsController.setSelectedCamera(
|
||||
view?.is('live') ? view.camera ?? null : null,
|
||||
);
|
||||
}
|
||||
if (changedProps.has('call')) {
|
||||
// Gate on `answered`, not mere presence, so `microphone.auto_unmute:
|
||||
// ['call']` doesn't transmit audio during the pre-answer ringing state.
|
||||
// Outbound calls are answered at construction; inbound calls only after
|
||||
// the user accepts.
|
||||
this._microphoneActionsController
|
||||
.setCallAnswered(!!this.call?.answered)
|
||||
.catch(() => {});
|
||||
this._microphoneActionsController.setCall(this.call).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1561,17 +1561,19 @@ const removeMicrophoneActionsTransform = (data: unknown): boolean => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a value from an array. The array is kept even when it empties: inside
|
||||
* Remove values from an array. The array is kept even when it empties: inside
|
||||
* an override, deleting it would restore whatever the base configuration says
|
||||
* rather than leaving nothing. An array that does not hold the value is left
|
||||
* rather than leaving nothing. An array that holds none of the values is left
|
||||
* alone.
|
||||
*/
|
||||
const removeFromArrayTransform = (removed: unknown): ((value: unknown) => unknown) => {
|
||||
const removeFromArrayTransform = (
|
||||
...removed: unknown[]
|
||||
): ((value: unknown) => unknown) => {
|
||||
return (value: unknown): unknown => {
|
||||
if (!Array.isArray(value) || !value.includes(removed)) {
|
||||
if (!Array.isArray(value) || !value.some((item) => removed.includes(item))) {
|
||||
return undefined;
|
||||
}
|
||||
return value.filter((item) => item !== removed);
|
||||
return value.filter((item) => !removed.includes(item));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1811,9 +1813,19 @@ const UPGRADES = [
|
||||
);
|
||||
},
|
||||
|
||||
// Retire microphone parameters/actions not needed with 'call'.
|
||||
// Retire microphone parameters/actions not needed with 'call'. The selection
|
||||
// conditions go with them: the card ends any call when the selected camera
|
||||
// changes, and the microphone only carries audio during a call.
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2681
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2679
|
||||
deleteWithOverrides('live.microphone.disconnect_seconds'),
|
||||
upgradeWithOverrides('live.microphone.auto_mute', removeFromArrayTransform('call')),
|
||||
upgradeWithOverrides(
|
||||
'live.microphone.auto_mute',
|
||||
removeFromArrayTransform('call', 'unselected'),
|
||||
),
|
||||
upgradeWithOverrides(
|
||||
'live.microphone.auto_unmute',
|
||||
removeFromArrayTransform('selected'),
|
||||
),
|
||||
removeMicrophoneActionsTransform,
|
||||
];
|
||||
|
||||
@@ -13,12 +13,12 @@ export const MEDIA_UNMUTE_CONDITIONS = [
|
||||
'call',
|
||||
] as const;
|
||||
|
||||
export const MICROPHONE_MUTE_CONDITIONS = [...MEDIA_ACTION_NEGATIVE_CONDITIONS] as const;
|
||||
// The microphone conditions have no 'selection' counterparts: the card ends any
|
||||
// call when the selected camera changes, and the microphone only carries audio
|
||||
// during a call.
|
||||
export const MICROPHONE_MUTE_CONDITIONS = ['hidden'] as const;
|
||||
|
||||
export const MICROPHONE_UNMUTE_CONDITIONS = [
|
||||
...MEDIA_ACTION_POSITIVE_CONDITIONS,
|
||||
'call',
|
||||
] as const;
|
||||
export const MICROPHONE_UNMUTE_CONDITIONS = ['visible', 'call'] as const;
|
||||
|
||||
export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
|
||||
export type AutoPauseCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
|
||||
|
||||
Reference in New Issue
Block a user