Allow the inbound audio to unmute when the microphone is unmuted.

This commit is contained in:
Dermot Duffy
2024-01-28 20:30:09 -08:00
parent a0dc1a1625
commit d3260d516c
22 changed files with 1313 additions and 582 deletions
+48 -12
View File
@@ -2,10 +2,23 @@ import { errorToConsole } from '../utils/basic';
import { Timer } from '../utils/timer';
import { CardMicrophoneAPI } from './types';
export class MicrophoneManager {
export type MicrophoneManagerListenerChange = 'muted' | 'unmuted';
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 _stream?: MediaStream | null;
protected _timer = new Timer();
protected _listeners: MicrophoneManagerListener[] = [];
// We keep mute state separate from the stream state so that mute/unmute can
// be expressed before the stream is created -- and when it's create it will
@@ -44,21 +57,20 @@ export class MicrophoneManager {
return this._stream ?? undefined;
}
protected _setMute(): void {
this._stream?.getTracks().forEach((track) => {
track.enabled = !this._mute;
});
this._startTimer();
this._api.getCardElementManager().update();
}
public mute(): void {
const wasMuted = this.isMuted();
this._mute = true;
this._setMute();
if (!wasMuted) {
this._callListeners('muted');
}
}
public async unmute(): Promise<void> {
const wasUnmuted = !this.isMuted();
const unmute = (): void => {
this._mute = false;
this._setMute();
@@ -76,6 +88,10 @@ export class MicrophoneManager {
} else if (this.isConnected()) {
unmute();
}
if (!wasUnmuted) {
this._callListeners('unmuted');
}
}
public isConnected(): boolean {
@@ -92,9 +108,29 @@ export class MicrophoneManager {
return !this._stream || this._stream.getTracks().every((track) => !track.enabled);
}
public addListener(listener: MicrophoneManagerListener): 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) => {
track.enabled = !this._mute;
});
this._startTimer();
this._api.getCardElementManager().update();
}
protected _startTimer(): void {
const microphoneConfig = this._api.getConfigManager().getConfig()
?.live.microphone;
const microphoneConfig = this._api.getConfigManager().getConfig()?.live.microphone;
if (microphoneConfig?.always_connected) {
return;
+1 -1
View File
@@ -306,7 +306,7 @@ class FrigateCard extends LitElement {
.getConditionsManager()
?.getEpoch()}
.hide=${!!this._controller.getMessageManager().hasMessage()}
.microphoneStream=${this._controller.getMicrophoneManager()?.getStream()}
.microphoneManager=${this._controller.getMicrophoneManager()}
.triggeredCameraIDs=${this._config?.view.scan.show_trigger_status
? this._controller.getTriggersManager().getTriggeredCameraIDs()
: undefined}
+19 -12
View File
@@ -18,6 +18,7 @@ import {
ConditionsManagerEpoch,
getOverriddenConfig,
} from '../../card-controller/conditions-manager.js';
import { ReadonlyMicrophoneManager } from '../../card-controller/microphone-manager.js';
import { MediaGridSelected } from '../../components-lib/media-grid-controller.js';
import {
CameraConfig,
@@ -113,7 +114,7 @@ export class FrigateCardLive extends LitElement {
public cardWideConfig?: CardWideConfig;
@property({ attribute: false })
public microphoneStream?: MediaStream;
public microphoneManager?: ReadonlyMicrophoneManager;
@property({ attribute: false })
public triggeredCameraIDs?: Set<string>;
@@ -222,7 +223,7 @@ export class FrigateCardLive extends LitElement {
.liveOverrides=${this.liveOverrides}
.cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager}
.microphoneStream=${this.microphoneStream}
.microphoneManager=${this.microphoneManager}
.triggeredCameraIDs=${this.triggeredCameraIDs}
@frigate-card:message=${(ev: CustomEvent<Message>) => {
this._renderKey++;
@@ -286,7 +287,7 @@ export class FrigateCardLiveGrid extends LitElement {
public cameraManager?: CameraManager;
@property({ attribute: false })
public microphoneStream?: MediaStream;
public microphoneManager?: ReadonlyMicrophoneManager;
@property({ attribute: false })
public triggeredCameraIDs?: Set<string>;
@@ -306,7 +307,7 @@ export class FrigateCardLiveGrid extends LitElement {
.liveOverrides=${this.liveOverrides}
.cardWideConfig=${this.cardWideConfig}
.cameraManager=${this.cameraManager}
.microphoneStream=${this.microphoneStream}
.microphoneManager=${this.microphoneManager}
?triggered=${triggeredCameraID &&
!!this.triggeredCameraIDs?.has(triggeredCameraID)}
>
@@ -394,7 +395,7 @@ export class FrigateCardLiveCarousel extends LitElement {
public cameraManager?: CameraManager;
@property({ attribute: false })
public microphoneStream?: MediaStream;
public microphoneManager?: ReadonlyMicrophoneManager;
@property({ attribute: false })
public viewFilterCameraID?: string;
@@ -431,7 +432,7 @@ export class FrigateCardLiveCarousel extends LitElement {
lazyLoadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('load', index, slide),
}),
lazyUnloadCondition: this.overriddenLiveConfig?.lazy_unload,
lazyUnloadConditions: this.overriddenLiveConfig?.lazy_unload,
lazyUnloadCallback: (index, slide) =>
this._lazyloadOrUnloadSlide('unload', index, slide),
}),
@@ -439,16 +440,22 @@ export class FrigateCardLiveCarousel extends LitElement {
AutoMediaActions({
playerSelector: FRIGATE_CARD_LIVE_PROVIDER,
...(this.overriddenLiveConfig?.auto_play && {
autoPlayCondition: this.overriddenLiveConfig.auto_play,
autoPlayConditions: this.overriddenLiveConfig.auto_play,
}),
...(this.overriddenLiveConfig?.auto_pause && {
autoPauseCondition: this.overriddenLiveConfig.auto_pause,
autoPauseConditions: this.overriddenLiveConfig.auto_pause,
}),
...(this.overriddenLiveConfig?.auto_mute && {
autoMuteCondition: this.overriddenLiveConfig.auto_mute,
autoMuteConditions: this.overriddenLiveConfig.auto_mute,
}),
...(this.overriddenLiveConfig?.auto_unmute && {
autoUnmuteCondition: this.overriddenLiveConfig.auto_unmute,
autoUnmuteConditions: this.overriddenLiveConfig.auto_unmute,
}),
...((this.overriddenLiveConfig?.auto_unmute ||
this.overriddenLiveConfig?.auto_mute) && {
microphoneManager: this.microphoneManager,
microphoneMuteSeconds:
this.overriddenLiveConfig.microphone.mute_after_microphone_mute_seconds,
}),
}),
AutoSize(),
@@ -571,7 +578,7 @@ export class FrigateCardLiveCarousel extends LitElement {
<frigate-card-live-provider
?load=${!config.lazy_load}
.microphoneStream=${this.view?.camera === cameraID
? this.microphoneStream
? this.microphoneManager?.getStream()
: undefined}
.cameraConfig=${cameraConfig}
.cameraEndpoints=${guard(
@@ -658,7 +665,7 @@ export class FrigateCardLiveCarousel extends LitElement {
.loop=${hasMultipleCameras}
.dragEnabled=${hasMultipleCameras && this.overriddenLiveConfig?.draggable}
.plugins=${guard(
[this.cameraManager, this.overriddenLiveConfig],
[this.cameraManager, this.overriddenLiveConfig, this.microphoneManager],
this._getPlugins.bind(this),
)}
.selected=${this._getSelectedCameraIndex()}
+4 -4
View File
@@ -272,16 +272,16 @@ export class FrigateCardViewerCarousel extends LitElement {
AutoMediaActions({
playerSelector: FRIGATE_CARD_VIEWER_PROVIDER,
...(this.viewerConfig?.auto_play && {
autoPlayCondition: this.viewerConfig.auto_play,
autoPlayConditions: this.viewerConfig.auto_play,
}),
...(this.viewerConfig?.auto_pause && {
autoPauseCondition: this.viewerConfig.auto_pause,
autoPauseConditions: this.viewerConfig.auto_pause,
}),
...(this.viewerConfig?.auto_mute && {
autoMuteCondition: this.viewerConfig.auto_mute,
autoMuteConditions: this.viewerConfig.auto_mute,
}),
...(this.viewerConfig?.auto_unmute && {
autoUnmuteCondition: this.viewerConfig.auto_unmute,
autoUnmuteConditions: this.viewerConfig.auto_unmute,
}),
}),
AutoSize(),
+3 -2
View File
@@ -13,6 +13,7 @@ import {
ConditionsManagerEpoch,
getOverridesByKey,
} from '../card-controller/conditions-manager.js';
import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js';
import {
CardWideConfig,
FrigateCardConfig,
@@ -60,7 +61,7 @@ export class FrigateCardViews extends LitElement {
public hide?: boolean;
@property({ attribute: false })
public microphoneStream?: MediaStream;
public microphoneManager?: ReadonlyMicrophoneManager;
@property({ attribute: false })
public triggeredCameraIDs?: Set<string>;
@@ -241,7 +242,7 @@ export class FrigateCardViews extends LitElement {
)}
.cameraManager=${this.cameraManager}
.cardWideConfig=${this.cardWideConfig}
.microphoneStream=${this.microphoneStream}
.microphoneManager=${this.microphoneManager}
.triggeredCameraIDs=${this.triggeredCameraIDs}
class="${classMap(liveClasses)}"
>
+31
View File
@@ -485,4 +485,35 @@ const UPGRADES = [
},
),
upgradeMoveToWithOverrides('view.timeout_seconds', CONF_VIEW_INTERACTION_SECONDS),
upgradeWithOverrides('live.lazy_unload', (data) =>
data === 'all' ? ['unselected', 'hidden'] : data === 'never' ? null : arrayify(data),
),
upgradeWithOverrides('live.auto_play', (data) =>
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
),
upgradeWithOverrides('live.auto_pause', (data) =>
data === 'all' ? ['unselected', 'hidden'] : data === 'never' ? null : arrayify(data),
),
upgradeWithOverrides('live.auto_mute', (data) =>
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
),
upgradeWithOverrides('live.auto_unmute', (data) =>
data === 'all'
? ['selected', 'visible', 'microphone']
: data === 'never'
? null
: arrayify(data),
),
upgradeWithOverrides('media_viewer.auto_play', (data) =>
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
),
upgradeWithOverrides('media_viewer.auto_pause', (data) =>
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
),
upgradeWithOverrides('media_viewer.auto_mute', (data) =>
data === 'all' ? null : data === 'never' ? [] : arrayify(data),
),
upgradeWithOverrides('media_viewer.auto_unmute', (data) =>
data === 'all' ? ['selected', 'visible'] : data === 'never' ? null : arrayify(data),
),
];
+41 -26
View File
@@ -46,24 +46,23 @@ const FRIGATE_CARD_VIEWS = [
export type FrigateCardView = (typeof FRIGATE_CARD_VIEWS)[number];
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
const MEDIA_ACTION_NEGATIVE_CONDITIONS = [
'all',
'unselected',
'hidden',
'never',
] as const;
export const MEDIA_ACTION_NEGATIVE_CONDITIONS = ['unselected', 'hidden'] as const;
export type LazyUnloadCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
export type AutoMuteCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
export type AutoPauseCondition = (typeof MEDIA_ACTION_NEGATIVE_CONDITIONS)[number];
const MEDIA_ACTION_POSITIVE_CONDITIONS = [
'all',
'selected',
'visible',
'never',
] as const;
export type AutoUnmuteCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
export const MEDIA_ACTION_POSITIVE_CONDITIONS = ['selected', 'visible'] as const;
export type AutoPlayCondition = (typeof MEDIA_ACTION_POSITIVE_CONDITIONS)[number];
export const MEDIA_UNMUTE_CONDITIONS = [
...MEDIA_ACTION_POSITIVE_CONDITIONS,
'microphone',
] as const;
export type AutoUnmuteCondition = (typeof MEDIA_UNMUTE_CONDITIONS)[number];
export const MEDIA_MUTE_CONDITIONS = [
...MEDIA_ACTION_NEGATIVE_CONDITIONS,
'microphone',
] as const;
export type AutoMuteCondition = (typeof MEDIA_MUTE_CONDITIONS)[number];
const PTZ_BASE_ACTIONS = ['left', 'right', 'up', 'down', 'zoom_in', 'zoom_out'] as const;
@@ -738,6 +737,7 @@ export type LiveProvider = (typeof LIVE_PROVIDERS)[number];
const microphoneConfigDefault = {
always_connected: false,
disconnect_seconds: 60,
mute_after_microphone_mute_seconds: 60,
};
const microphoneConfigSchema = z
@@ -747,6 +747,10 @@ const microphoneConfigSchema = z
.number()
.min(0)
.default(microphoneConfigDefault.disconnect_seconds),
mute_after_microphone_mute_seconds: z
.number()
.min(0)
.default(microphoneConfigDefault.mute_after_microphone_mute_seconds),
})
.default(microphoneConfigDefault);
export type MicrophoneConfig = z.infer<typeof microphoneConfigSchema>;
@@ -860,13 +864,13 @@ const liveThumbnailControlsDefaults = {
};
const liveConfigDefault = {
auto_play: 'all' as const,
auto_pause: 'never' as const,
auto_mute: 'all' as const,
auto_unmute: 'never' as const,
auto_play: [...MEDIA_ACTION_POSITIVE_CONDITIONS],
auto_pause: [],
auto_mute: [...MEDIA_MUTE_CONDITIONS],
auto_unmute: ['microphone' as const],
preload: false,
lazy_load: true,
lazy_unload: 'never' as const,
lazy_unload: [],
draggable: true,
zoomable: true,
transition_effect: 'slide' as const,
@@ -929,23 +933,27 @@ const liveOverridableConfigSchema = z
const liveConfigSchema = liveOverridableConfigSchema
.extend({
// Non-overrideable parameters.
auto_play: z
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_play),
auto_pause: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_pause),
auto_mute: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.enum(MEDIA_MUTE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_mute),
auto_unmute: z
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
.enum(MEDIA_UNMUTE_CONDITIONS)
.array()
.default(liveConfigDefault.auto_unmute),
preload: z.boolean().default(liveConfigDefault.preload),
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
lazy_unload: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array()
.default(liveConfigDefault.lazy_unload),
draggable: z.boolean().default(liveConfigDefault.draggable),
transition_effect: transitionEffectConfigSchema.default(
@@ -1296,10 +1304,10 @@ export type MenuConfig = z.infer<typeof menuConfigSchema>;
// *************************************************************************
const viewerConfigDefault = {
auto_play: 'all' as const,
auto_pause: 'all' as const,
auto_mute: 'all' as const,
auto_unmute: 'never' as const,
auto_play: [...MEDIA_ACTION_POSITIVE_CONDITIONS],
auto_pause: [...MEDIA_ACTION_NEGATIVE_CONDITIONS],
auto_mute: [...MEDIA_ACTION_NEGATIVE_CONDITIONS],
auto_unmute: [],
lazy_load: true,
draggable: true,
zoomable: true,
@@ -1330,15 +1338,22 @@ const viewerConfigSchema = z
.object({
auto_play: z
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
.array()
.default(viewerConfigDefault.auto_play),
auto_pause: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array()
.default(viewerConfigDefault.auto_pause),
// Don't use MEDIA_UNMUTE_CONDITIONS and MEDIA_MUTE_CONDITIONS here, since
// it includes 'microphone' which doesn't make sense for viewer media.
auto_mute: z
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
.array()
.default(viewerConfigDefault.auto_mute),
auto_unmute: z
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
.array()
.default(viewerConfigDefault.auto_unmute),
lazy_load: z.boolean().default(viewerConfigDefault.lazy_load),
draggable: z.boolean().default(viewerConfigDefault.draggable),
+4 -1
View File
@@ -75,7 +75,8 @@ export const CONF_VIEW_INTERACTION_SECONDS = `${CONF_VIEW}.interaction_seconds`
export const CONF_VIEW_UPDATE_CYCLE_CAMERA = `${CONF_VIEW}.update_cycle_camera` as const;
export const CONF_VIEW_UPDATE_FORCE = `${CONF_VIEW}.update_force` as const;
export const CONF_VIEW_UPDATE_SECONDS = `${CONF_VIEW}.update_seconds` as const;
export const CONF_VIEW_RESET_AFTER_INTERACTION = `${CONF_VIEW}.reset_after_interaction` as const;
export const CONF_VIEW_RESET_AFTER_INTERACTION =
`${CONF_VIEW}.reset_after_interaction` as const;
export const CONF_VIEW_SCAN = `${CONF_VIEW}.scan` as const;
export const CONF_VIEW_SCAN_ENABLED = `${CONF_VIEW_SCAN}.enabled` as const;
export const CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS =
@@ -236,6 +237,8 @@ export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD =
`${CONF_LIVE}.show_image_during_load` as const;
export const CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS =
`${CONF_LIVE}.microphone.disconnect_seconds` as const;
export const CONF_LIVE_MICROPHONE_MUTE_AFTER_MICROPHONE_MUTE_SECONDS =
`${CONF_LIVE}.microphone.mute_after_microphone_mute_seconds` as const;
export const CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED =
`${CONF_LIVE}.microphone.always_connected` as const;
export const CONF_LIVE_ZOOMABLE = `${CONF_LIVE}.zoomable` as const;
+51 -6
View File
@@ -1,3 +1,5 @@
// TODO: menu in hover mode, hold down microphone, should be momentary, appears to stick on?
import {
fireEvent,
HomeAssistant,
@@ -113,6 +115,7 @@ import {
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED,
CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS,
CONF_LIVE_MICROPHONE_MUTE_AFTER_MICROPHONE_MUTE_SECONDS,
CONF_LIVE_PRELOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
CONF_LIVE_TRANSITION_EFFECT,
@@ -519,18 +522,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
protected _mediaActionNegativeConditions: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'all', label: localize('config.common.media_action_conditions.all') },
{
value: 'unselected',
label: localize('config.common.media_action_conditions.unselected'),
},
{ value: 'hidden', label: localize('config.common.media_action_conditions.hidden') },
{ value: 'never', label: localize('config.common.media_action_conditions.never') },
];
protected _mediaActionPositiveConditions: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'all', label: localize('config.common.media_action_conditions.all') },
{
value: 'selected',
label: localize('config.common.media_action_conditions.selected'),
@@ -539,7 +539,22 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
value: 'visible',
label: localize('config.common.media_action_conditions.visible'),
},
{ value: 'never', label: localize('config.common.media_action_conditions.never') },
];
protected _mediaLiveUnmuteConditions: EditorSelectOption[] = [
...this._mediaActionPositiveConditions,
{
value: 'microphone',
label: localize('config.common.media_action_conditions.microphone_unmute'),
},
];
protected _mediaLiveMuteConditions: EditorSelectOption[] = [
...this._mediaActionNegativeConditions,
{
value: 'microphone',
label: localize('config.common.media_action_conditions.microphone_mute'),
},
];
protected _layoutFits: EditorSelectOption[] = [
@@ -2011,22 +2026,37 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(
CONF_LIVE_LAZY_UNLOAD,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_PLAY,
this._mediaActionPositiveConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_PAUSE,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_MUTE,
this._mediaActionNegativeConditions,
this._mediaLiveMuteConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_AUTO_UNMUTE,
this._mediaActionPositiveConditions,
this._mediaLiveUnmuteConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_LIVE_TRANSITION_EFFECT,
@@ -2153,6 +2183,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_MICROPHONE_ALWAYS_CONNECTED,
this._defaults.live.microphone.always_connected,
)}
${this._renderNumberInput(
CONF_LIVE_MICROPHONE_MUTE_AFTER_MICROPHONE_MUTE_SECONDS,
)}
`,
)}
</div>
@@ -2182,18 +2215,30 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_PLAY,
this._mediaActionPositiveConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_PAUSE,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_MUTE,
this._mediaActionNegativeConditions,
{
multiple: true,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_AUTO_UNMUTE,
this._mediaActionPositiveConditions,
{
multiple: true,
},
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_DRAGGABLE,
+4 -1
View File
@@ -182,6 +182,8 @@
"media_action_conditions": {
"all": "All opportunities",
"hidden": "On browser/tab hiding",
"microphone_mute": "On microphone mute",
"microphone_unmute": "On microphone unmute",
"never": "Never",
"selected": "On selection",
"unselected": "On unselection",
@@ -267,7 +269,8 @@
"always_connected": "Always keep the microphone connected",
"disconnect_seconds": "Seconds after which to disconnect microphone (0=never)",
"editor_label": "Microphone",
"enabled": "Microphone enabled"
"enabled": "Microphone enabled",
"mute_after_microphone_mute_seconds": "Seconds after microphone mute to mute inbound audio"
},
"preload": "Preload live view in the background",
"show_image_during_load": "Show still image while the live stream is loading",
+4 -1
View File
@@ -182,6 +182,8 @@
"media_action_conditions": {
"all": "Tutte le opportunità",
"hidden": "Sul browser/nascondere le schede",
"microphone_mute": "",
"microphone_unmute": "",
"never": "Mai",
"selected": "Sulla selezione",
"unselected": "Sulla non selezione",
@@ -267,7 +269,8 @@
"always_connected": "",
"disconnect_seconds": "",
"editor_label": "",
"enabled": ""
"enabled": "",
"mute_after_microphone_mute_seconds": ""
},
"preload": "Precarica Live View in background",
"show_image_during_load": "Mostra un'immagine fissa durante il caricamento del live streaming",
+4 -1
View File
@@ -182,6 +182,8 @@
"media_action_conditions": {
"all": "Todas as oportunidades",
"hidden": "Ao ocultar o navegador/aba",
"microphone_mute": "",
"microphone_unmute": "",
"never": "Nunca",
"selected": "Ao selecionar",
"unselected": "Ao desselecionar",
@@ -267,7 +269,8 @@
"always_connected": "",
"disconnect_seconds": "",
"editor_label": "",
"enabled": ""
"enabled": "",
"mute_after_microphone_mute_seconds": ""
},
"preload": "Pré-carregar a visualização ao vivo em segundo plano",
"show_image_during_load": "Mostrar imagem estática enquanto a transmissão ao vivo está carregando",
+4 -1
View File
@@ -182,6 +182,8 @@
"media_action_conditions": {
"all": "Todas as oportunidades",
"hidden": "Ao ocultar o navegador/aba",
"microphone_mute": "",
"microphone_unmute": "",
"never": "Nunca",
"selected": "Ao selecionar",
"unselected": "Ao desselecionar",
@@ -260,7 +262,8 @@
"always_connected": "",
"disconnect_seconds": "",
"editor_label": "",
"enabled": ""
"enabled": "",
"mute_after_microphone_mute_seconds": ""
},
"preload": "Pré-carregar a visualização ao vivo em segundo plano",
"show_image_during_load": "Mostar imagem durante o carregamento",
@@ -14,7 +14,7 @@ type OptionsType = CreateOptionsType<{
// Number of slides to lazyload left/right of selected (0 == only selected
// slide).
lazyLoadCount: number;
lazyUnloadCondition?: LazyUnloadCondition;
lazyUnloadConditions?: readonly LazyUnloadCondition[];
lazyLoadCallback?: (index: number, slide: HTMLElement) => void;
lazyUnloadCallback?: (index: number, slide: HTMLElement) => void;
@@ -55,8 +55,7 @@ export function AutoLazyLoad(
}
if (
options.lazyUnloadCallback &&
options.lazyUnloadCondition &&
['all', 'unselected'].includes(options.lazyUnloadCondition)
options.lazyUnloadConditions?.includes('unselected')
) {
unloadEvents.forEach((evt) => emblaApi.on(evt, lazyUnloadPreviousHandler));
}
@@ -76,8 +75,7 @@ export function AutoLazyLoad(
function visibilityHandler(): void {
if (
document.visibilityState === 'hidden' &&
options.lazyUnloadCondition &&
['all', 'hidden'].includes(options.lazyUnloadCondition)
options.lazyUnloadConditions?.includes('hidden')
) {
lazyUnloadAllHandler();
} else if (document.visibilityState === 'visible' && options.lazyLoadCallback) {
@@ -2,6 +2,10 @@ import { EmblaCarouselType } from 'embla-carousel';
import { CreateOptionsType } from 'embla-carousel/components/Options.js';
import { OptionsHandlerType } from 'embla-carousel/components/OptionsHandler.js';
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins.js';
import {
MicrophoneManagerListenerChange,
ReadonlyMicrophoneManager,
} from '../../../../card-controller/microphone-manager.js';
import {
AutoMuteCondition,
AutoPauseCondition,
@@ -9,6 +13,7 @@ import {
AutoUnmuteCondition,
} from '../../../../config/types.js';
import { FrigateCardMediaPlayer } from '../../../../types.js';
import { Timer } from '../../../timer.js';
declare module 'embla-carousel/components/Plugins' {
interface EmblaPluginsType {
@@ -19,10 +24,13 @@ declare module 'embla-carousel/components/Plugins' {
type OptionsType = CreateOptionsType<{
playerSelector?: string;
autoPlayCondition?: AutoPlayCondition;
autoUnmuteCondition?: AutoUnmuteCondition;
autoPauseCondition?: AutoPauseCondition;
autoMuteCondition?: AutoMuteCondition;
autoPlayConditions?: readonly AutoPlayCondition[];
autoUnmuteConditions?: readonly AutoUnmuteCondition[];
autoPauseConditions?: readonly AutoPauseCondition[];
autoMuteConditions?: readonly AutoMuteCondition[];
microphoneManager?: ReadonlyMicrophoneManager;
microphoneMuteSeconds?: number;
}>;
export type AutoMediaActionsOptionsType = Partial<OptionsType>;
@@ -43,6 +51,7 @@ export function AutoMediaActions(
let emblaApi: EmblaCarouselType;
let slides: HTMLElement[];
let hadInitialIntersectionCall: boolean | null = false;
const microphoneMuteTimer = new Timer();
const intersectionObserver: IntersectionObserver = new IntersectionObserver(
intersectionHandler,
@@ -59,35 +68,23 @@ export function AutoMediaActions(
slides = emblaApi.slideNodes();
if (
options.autoPlayCondition &&
['all', 'selected'].includes(options.autoPlayCondition)
) {
if (options.autoPlayConditions?.includes('selected')) {
// Auto play when the media loads not necessarily when the slide is
// selected (to allow for lazyloading).
emblaApi.containerNode().addEventListener('frigate-card:media:loaded', play);
}
if (
options.autoUnmuteCondition &&
['all', 'selected'].includes(options.autoUnmuteCondition)
) {
if (options.autoUnmuteConditions?.includes('selected')) {
// Auto unmute when the media loads not necessarily when the slide is
// selected (to allow for lazyloading).
emblaApi.containerNode().addEventListener('frigate-card:media:loaded', unmute);
}
if (
options.autoPauseCondition &&
['all', 'unselected'].includes(options.autoPauseCondition)
) {
if (options.autoPauseConditions?.includes('unselected')) {
emblaApi.on('select', pausePrevious);
}
if (
options.autoMuteCondition &&
['all', 'unselected'].includes(options.autoMuteCondition)
) {
if (options.autoMuteConditions?.includes('unselected')) {
emblaApi.on('select', mutePrevious);
}
@@ -96,34 +93,54 @@ export function AutoMediaActions(
document.addEventListener('visibilitychange', visibilityHandler);
intersectionObserver.observe(emblaApi.containerNode());
if (
options.autoUnmuteConditions?.includes('microphone') ||
options.autoMuteConditions?.includes('microphone')
) {
// For some reason mergeOptions() appears to break mock objects passed in,
// so unittesting doesn't work when using options (vs userOptions where it
// does).
userOptions.microphoneManager?.addListener(microphoneChangeHandler);
// Stop the microphone mute timer if the media changes.
emblaApi
.containerNode()
.addEventListener('frigate-card:media:loaded', stopMicrophoneTimer);
}
}
function stopMicrophoneTimer(): void {
microphoneMuteTimer.stop();
}
function microphoneChangeHandler(change: MicrophoneManagerListenerChange): void {
if (change === 'unmuted' && options.autoUnmuteConditions?.includes('microphone')) {
unmute();
} else if (
change === 'muted' &&
options.autoMuteConditions?.includes('microphone')
) {
microphoneMuteTimer.start(options.microphoneMuteSeconds ?? 60, () => {
mute();
});
}
}
function destroy(): void {
if (
options.autoPlayCondition &&
['all', 'selected'].includes(options.autoPlayCondition)
) {
if (options.autoPlayConditions?.includes('selected')) {
emblaApi.containerNode().removeEventListener('frigate-card:media:loaded', play);
}
if (
options.autoUnmuteCondition &&
['all', 'selected'].includes(options.autoUnmuteCondition)
) {
if (options.autoUnmuteConditions?.includes('selected')) {
emblaApi.containerNode().removeEventListener('frigate-card:media:loaded', unmute);
}
if (
options.autoPauseCondition &&
['all', 'unselected'].includes(options.autoPauseCondition)
) {
if (options.autoPauseConditions?.includes('unselected')) {
emblaApi.off('select', pausePrevious);
}
if (
options.autoMuteCondition &&
['all', 'unselected'].includes(options.autoMuteCondition)
) {
if (options.autoMuteConditions?.includes('unselected')) {
emblaApi.off('select', mutePrevious);
}
@@ -132,33 +149,31 @@ export function AutoMediaActions(
document.removeEventListener('visibilitychange', visibilityHandler);
intersectionObserver.disconnect();
if (
options.autoUnmuteConditions?.includes('microphone') ||
options.autoMuteConditions?.includes('microphone')
) {
userOptions.microphoneManager?.removeListener(microphoneChangeHandler);
emblaApi
.containerNode()
.removeEventListener('frigate-card:media:loaded', stopMicrophoneTimer);
}
}
function actOnVisibilityChange(visible: boolean): void {
if (visible) {
if (
options.autoPlayCondition &&
['all', 'visible'].includes(options.autoPlayCondition)
) {
if (options.autoPlayConditions?.includes('visible')) {
play();
}
if (
options.autoUnmuteCondition &&
['all', 'visible'].includes(options.autoUnmuteCondition)
) {
if (options.autoUnmuteConditions?.includes('visible')) {
unmute();
}
} else {
if (
options.autoPauseCondition &&
['all', 'hidden'].includes(options.autoPauseCondition)
) {
if (options.autoPauseConditions?.includes('hidden')) {
pauseAll();
}
if (
options.autoMuteCondition &&
['all', 'hidden'].includes(options.autoMuteCondition)
) {
if (options.autoMuteConditions?.includes('hidden')) {
muteAll();
}
}