Allow the inbound audio to unmute when the microphone is unmuted.
This commit is contained in:
@@ -190,4 +190,35 @@ describe('MicrophoneManager', () => {
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should respect listeners', async () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MicrophoneManager(api);
|
||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
||||
|
||||
const listener = vi.fn();
|
||||
manager.addListener(listener);
|
||||
|
||||
await manager.connect();
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
await manager.mute();
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
||||
await manager.unmute();
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
expect(listener).toHaveBeenLastCalledWith('unmuted');
|
||||
|
||||
await manager.unmute();
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
await manager.mute();
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
expect(listener).toHaveBeenLastCalledWith('muted');
|
||||
|
||||
manager.removeListener(listener);
|
||||
|
||||
await manager.unmute();
|
||||
expect(listener).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
+771
-411
File diff suppressed because it is too large
Load Diff
@@ -58,10 +58,10 @@ describe('config defaults', () => {
|
||||
zoomable: true,
|
||||
},
|
||||
live: {
|
||||
auto_mute: 'all',
|
||||
auto_pause: 'never',
|
||||
auto_play: 'all',
|
||||
auto_unmute: 'never',
|
||||
auto_mute: ['unselected', 'hidden', 'microphone'],
|
||||
auto_pause: [],
|
||||
auto_play: ['selected', 'visible'],
|
||||
auto_unmute: ['microphone'],
|
||||
controls: {
|
||||
builtin: true,
|
||||
next_previous: {
|
||||
@@ -96,10 +96,11 @@ describe('config defaults', () => {
|
||||
},
|
||||
draggable: true,
|
||||
lazy_load: true,
|
||||
lazy_unload: 'never',
|
||||
lazy_unload: [],
|
||||
microphone: {
|
||||
always_connected: false,
|
||||
disconnect_seconds: 60,
|
||||
mute_after_microphone_mute_seconds: 60,
|
||||
},
|
||||
preload: false,
|
||||
show_image_during_load: true,
|
||||
@@ -122,10 +123,10 @@ describe('config defaults', () => {
|
||||
},
|
||||
},
|
||||
media_viewer: {
|
||||
auto_mute: 'all',
|
||||
auto_pause: 'all',
|
||||
auto_play: 'all',
|
||||
auto_unmute: 'never',
|
||||
auto_mute: ['unselected', 'hidden'],
|
||||
auto_pause: ['unselected', 'hidden'],
|
||||
auto_play: ['selected', 'visible'],
|
||||
auto_unmute: [],
|
||||
controls: {
|
||||
builtin: true,
|
||||
next_previous: {
|
||||
@@ -473,3 +474,14 @@ it('should strip trailing slashes from go2rtc url', () => {
|
||||
expect(config).toBeTruthy();
|
||||
expect(config.cameras[0].go2rtc.url).toBe('https://my-custom-go2rtc');
|
||||
});
|
||||
|
||||
it('media viewer should not support microphone based conditions', () => {
|
||||
expect(() =>
|
||||
createConfig({
|
||||
cameras: [],
|
||||
media_viewer: {
|
||||
auto_unmute: 'microphone' as const,
|
||||
},
|
||||
}),
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MEDIA_ACTION_NEGATIVE_CONDITIONS } from '../../../../../src/config/types';
|
||||
import { AutoLazyLoad } from '../../../../../src/utils/embla/plugins/auto-lazy-load/auto-lazy-load';
|
||||
import {
|
||||
callEmblaHandler,
|
||||
@@ -91,7 +92,7 @@ describe('AutoLazyLoad', () => {
|
||||
lazyLoadCallback: vi.fn(),
|
||||
lazyLoadCount: 3,
|
||||
lazyUnloadCallback: lazyUnloadCallback,
|
||||
lazyUnloadCondition: 'all',
|
||||
lazyUnloadConditions: MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
@@ -143,7 +144,7 @@ describe('AutoLazyLoad', () => {
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: vi.fn(),
|
||||
lazyUnloadCallback: lazyUnloadCallback,
|
||||
lazyUnloadCondition: 'all',
|
||||
lazyUnloadConditions: MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
});
|
||||
|
||||
const children = createTestSlideNodes();
|
||||
@@ -172,7 +173,7 @@ describe('AutoLazyLoad', () => {
|
||||
const lazyLoadCallback = vi.fn();
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyLoadCallback: lazyLoadCallback,
|
||||
lazyUnloadCondition: 'all',
|
||||
lazyUnloadConditions: MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
// No lazy unload callback.
|
||||
});
|
||||
const children = createTestSlideNodes();
|
||||
@@ -200,7 +201,7 @@ describe('AutoLazyLoad', () => {
|
||||
vi.spyOn(global.document, 'addEventListener');
|
||||
|
||||
const plugin = AutoLazyLoad({
|
||||
lazyUnloadCondition: 'all',
|
||||
lazyUnloadConditions: MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
// No callbacks provided.
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import add from 'date-fns/add';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import {
|
||||
MicrophoneManagerListenerChange,
|
||||
ReadonlyMicrophoneManager,
|
||||
} from '../../../../../src/card-controller/microphone-manager';
|
||||
import {
|
||||
MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
MEDIA_ACTION_POSITIVE_CONDITIONS,
|
||||
MEDIA_MUTE_CONDITIONS,
|
||||
MEDIA_UNMUTE_CONDITIONS,
|
||||
} from '../../../../../src/config/types';
|
||||
import { FrigateCardMediaPlayer } from '../../../../../src/types';
|
||||
import {
|
||||
AutoMediaActions,
|
||||
@@ -50,18 +62,33 @@ const createPlayerSlideNodes = (n = 10): HTMLElement[] => {
|
||||
const createPlugin = (options?: AutoMediaActionsOptionsType): AutoMediaActionsType => {
|
||||
return AutoMediaActions({
|
||||
playerSelector: 'video',
|
||||
autoPlayCondition: 'all',
|
||||
autoUnmuteCondition: 'all',
|
||||
autoPauseCondition: 'all',
|
||||
autoMuteCondition: 'all',
|
||||
autoPlayConditions: MEDIA_ACTION_POSITIVE_CONDITIONS,
|
||||
autoUnmuteConditions: MEDIA_UNMUTE_CONDITIONS,
|
||||
autoPauseConditions: MEDIA_ACTION_NEGATIVE_CONDITIONS,
|
||||
autoMuteConditions: MEDIA_MUTE_CONDITIONS,
|
||||
microphoneManager: mock<ReadonlyMicrophoneManager>(),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const callMicrophoneListener = (
|
||||
microphoneManager: ReadonlyMicrophoneManager,
|
||||
action: MicrophoneManagerListenerChange,
|
||||
n = 0,
|
||||
): void => {
|
||||
const mock = vi.mocked(microphoneManager.addListener).mock;
|
||||
mock.calls[n][0](action);
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('AutoMediaActions', () => {
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -74,8 +101,14 @@ describe('AutoMediaActions', () => {
|
||||
});
|
||||
|
||||
it('should init without any conditions', () => {
|
||||
const plugin = AutoMediaActions();
|
||||
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({
|
||||
autoPlayConditions: [],
|
||||
autoUnmuteConditions: [],
|
||||
autoPauseConditions: [],
|
||||
autoMuteConditions: [],
|
||||
microphoneManager: microphoneManager,
|
||||
});
|
||||
const parent = createParent();
|
||||
const addEventListener = vi.fn();
|
||||
parent.addEventListener = addEventListener;
|
||||
@@ -86,6 +119,7 @@ describe('AutoMediaActions', () => {
|
||||
expect(emblaApi.on).toBeCalledWith('destroy', expect.anything());
|
||||
expect(emblaApi.on).not.toBeCalledWith('select', expect.anything());
|
||||
expect(addEventListener).not.toBeCalled();
|
||||
expect(microphoneManager.addListener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should destroy', () => {
|
||||
@@ -105,7 +139,14 @@ describe('AutoMediaActions', () => {
|
||||
});
|
||||
|
||||
it('should destroy without any conditions', () => {
|
||||
const plugin = AutoMediaActions();
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({
|
||||
autoPlayConditions: [],
|
||||
autoUnmuteConditions: [],
|
||||
autoPauseConditions: [],
|
||||
autoMuteConditions: [],
|
||||
microphoneManager: microphoneManager,
|
||||
});
|
||||
const parent = createParent();
|
||||
const removeEventListener = vi.fn();
|
||||
parent.removeEventListener = removeEventListener;
|
||||
@@ -118,6 +159,7 @@ describe('AutoMediaActions', () => {
|
||||
expect(emblaApi.off).toBeCalledWith('destroy', expect.anything());
|
||||
expect(emblaApi.off).not.toBeCalledWith('select', expect.anything());
|
||||
expect(removeEventListener).not.toBeCalled();
|
||||
expect(microphoneManager.removeListener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should mute and pause on destroy', () => {
|
||||
@@ -301,4 +343,113 @@ describe('AutoMediaActions', () => {
|
||||
expect(getPlayer(child, 'video')?.mute).toBeCalled();
|
||||
}
|
||||
});
|
||||
|
||||
describe('should handle microphone triggers', () => {
|
||||
it('should unmute on microphone unmute', () => {
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({ microphoneManager: microphoneManager });
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
callMicrophoneListener(microphoneManager, 'unmuted');
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.unmute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not unmute on microphone unmute when not configured', () => {
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({
|
||||
microphoneManager: microphoneManager,
|
||||
autoUnmuteConditions: [],
|
||||
});
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
callMicrophoneListener(microphoneManager, 'unmuted');
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.unmute).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should mute on microphone mute after default delay', () => {
|
||||
const start = new Date('2024-01-28T14:42');
|
||||
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({ microphoneManager: microphoneManager });
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
vi.setSystemTime(start);
|
||||
callMicrophoneListener(microphoneManager, 'muted');
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.mute).not.toBeCalled();
|
||||
vi.setSystemTime(add(start, { seconds: 60 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
expect(getPlayer(children[5], 'video')?.mute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should mute on microphone mute after configured delay', () => {
|
||||
const start = new Date('2024-01-28T14:42');
|
||||
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({
|
||||
microphoneManager: microphoneManager,
|
||||
microphoneMuteSeconds: 30,
|
||||
});
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
vi.setSystemTime(start);
|
||||
callMicrophoneListener(microphoneManager, 'muted');
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.mute).not.toBeCalled();
|
||||
vi.setSystemTime(add(start, { seconds: 30 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
expect(getPlayer(children[5], 'video')?.mute).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not mute on microphone mute after delay when not configured', () => {
|
||||
const start = new Date('2024-01-28T14:42');
|
||||
|
||||
const microphoneManager = mock<ReadonlyMicrophoneManager>();
|
||||
const plugin = createPlugin({
|
||||
microphoneManager: microphoneManager,
|
||||
autoMuteConditions: [],
|
||||
});
|
||||
const children = createPlayerSlideNodes();
|
||||
const emblaApi = createEmblaApiInstance({
|
||||
slideNodes: children,
|
||||
selectedScrollSnap: 5,
|
||||
});
|
||||
|
||||
plugin.init(emblaApi, createTestEmblaOptionHandler());
|
||||
|
||||
vi.setSystemTime(start);
|
||||
callMicrophoneListener(microphoneManager, 'muted');
|
||||
|
||||
expect(getPlayer(children[5], 'video')?.mute).not.toBeCalled();
|
||||
vi.setSystemTime(add(start, { seconds: 60 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
expect(getPlayer(children[5], 'video')?.mute).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user