Allow the inbound audio to unmute when the microphone is unmuted.
This commit is contained in:
@@ -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
@@ -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
@@ -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()}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)}"
|
||||
>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user