fix: Prevent microphone auto-disconnect during calls and restore the removed microphone connected condition (#2597)
- Closes: #2590
This commit is contained in:
@@ -1386,6 +1386,75 @@ describe('answer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('microphone usage', () => {
|
||||
it('should mark the microphone in use for the duration of the call', async () => {
|
||||
const api = createAPI({ view: createView({ camera: 'camera.office' }) });
|
||||
const manager = new CallManager(api);
|
||||
|
||||
expect(await manager.start()).toBe(true);
|
||||
|
||||
expect(api.getMicrophoneManager().startUsing).toBeCalledTimes(1);
|
||||
expect(api.getMicrophoneManager().stopUsing).not.toBeCalled();
|
||||
|
||||
manager.end();
|
||||
|
||||
expect(api.getMicrophoneManager().stopUsing).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not mark the microphone in use when the start is aborted', async () => {
|
||||
const api = createAPI({
|
||||
view: createView({ camera: 'camera.office' }),
|
||||
microphoneConnected: false,
|
||||
});
|
||||
vi.mocked(api.getMicrophoneManager().connect).mockRejectedValue(new Error());
|
||||
|
||||
expect(await new CallManager(api).start()).toBe(false);
|
||||
|
||||
expect(api.getMicrophoneManager().startUsing).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should keep the microphone in use when a call supersedes another', async () => {
|
||||
const api = createAPI({
|
||||
view: createView({ camera: 'camera.office' }),
|
||||
store: createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({ live: true, '2-way-audio': true }),
|
||||
},
|
||||
{
|
||||
cameraID: 'camera.garage',
|
||||
capabilities: createCapabilities({ live: true, '2-way-audio': true }),
|
||||
},
|
||||
]),
|
||||
});
|
||||
const manager = new CallManager(api);
|
||||
|
||||
expect(await manager.start()).toBe(true);
|
||||
expect(await manager.start({ cameraID: 'camera.garage' })).toBe(true);
|
||||
|
||||
expect(api.getMicrophoneManager().startUsing).toBeCalledTimes(2);
|
||||
expect(api.getMicrophoneManager().stopUsing).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should mark the microphone unused on uninitialization', async () => {
|
||||
const api = createAPI({ view: createView({ camera: 'camera.office' }) });
|
||||
const manager = new CallManager(api);
|
||||
|
||||
expect(await manager.start()).toBe(true);
|
||||
manager.uninitialize();
|
||||
|
||||
expect(api.getMicrophoneManager().stopUsing).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not mark the microphone unused when uninitializing without a call', () => {
|
||||
const api = createAPI({ view: createView({ camera: 'camera.office' }) });
|
||||
|
||||
new CallManager(api).uninitialize();
|
||||
|
||||
expect(api.getMicrophoneManager().stopUsing).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// Ringtone integration: started only for inbound + unanswered + a configured
|
||||
// ringtone other than 'none'; stopped on end / uninitialize.
|
||||
describe('ringtone', () => {
|
||||
@@ -1610,7 +1679,7 @@ describe('session end during setState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('uninitialize during in-flight start', () => {
|
||||
describe('state changes during in-flight start', () => {
|
||||
it('should not install a session or ring when uninitialized mid-await', async () => {
|
||||
const api = createAPI({
|
||||
view: createView({ camera: 'camera.office' }),
|
||||
@@ -1660,6 +1729,46 @@ describe('uninitialize during in-flight start', () => {
|
||||
expect(manager.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it('should supersede a session installed by another start mid-await', async () => {
|
||||
const api = createAPI({
|
||||
view: createView({ camera: 'camera.office' }),
|
||||
microphoneConnected: false,
|
||||
store: createStore([
|
||||
{
|
||||
cameraID: 'camera.office',
|
||||
capabilities: createCapabilities({ live: true, '2-way-audio': true }),
|
||||
},
|
||||
{
|
||||
cameraID: 'camera.garage',
|
||||
capabilities: createCapabilities({ live: true, '2-way-audio': true }),
|
||||
},
|
||||
]),
|
||||
});
|
||||
let resolveConnect: () => void = () => {};
|
||||
vi.mocked(api.getMicrophoneManager().connect).mockReturnValue(
|
||||
new Promise<void>((resolve) => {
|
||||
resolveConnect = resolve;
|
||||
}),
|
||||
);
|
||||
const manager = new CallManager(api);
|
||||
manager.initialize();
|
||||
|
||||
// Both requests read the (absent) session before either can install one.
|
||||
const first = manager.start();
|
||||
const second = manager.start({ cameraID: 'camera.garage' });
|
||||
resolveConnect();
|
||||
|
||||
expect(await first).toBe(true);
|
||||
expect(await second).toBe(true);
|
||||
|
||||
// The second request must end the first request's session rather than
|
||||
// overwrite it, leaving exactly one live session and no stranded microphone
|
||||
// marking.
|
||||
expect(manager.getCall()?.cameraID).toBe('camera.garage');
|
||||
expect(api.getMicrophoneManager().startUsing).toBeCalledTimes(2);
|
||||
expect(api.getMicrophoneManager().stopUsing).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should suppress the microphone-failure notification when uninitialized mid-await', async () => {
|
||||
const api = createAPI({
|
||||
view: createView({ camera: 'camera.office' }),
|
||||
|
||||
@@ -245,6 +245,159 @@ describe('MicrophoneManager', () => {
|
||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('should stay connected while in use', () => {
|
||||
const disconnectSeconds = 10;
|
||||
|
||||
const createManagerWithDisconnectSeconds = (): MicrophoneManager => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(
|
||||
createMockStream(),
|
||||
);
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
live: {
|
||||
microphone: {
|
||||
always_connected: false,
|
||||
disconnect_seconds: disconnectSeconds,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
return new MicrophoneManager(api);
|
||||
};
|
||||
|
||||
it('should not automatically disconnect while a user is registered', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
await manager.connect();
|
||||
manager.startUsing();
|
||||
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
||||
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not automatically disconnect when a user is registered before connection', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
manager.startUsing();
|
||||
await manager.connect();
|
||||
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
||||
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not automatically disconnect on mute or unmute while in use', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
await manager.connect();
|
||||
manager.startUsing();
|
||||
|
||||
await manager.unmute();
|
||||
manager.mute();
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
||||
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should restart the countdown in full when use stops', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
await manager.connect();
|
||||
manager.startUsing();
|
||||
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
||||
manager.stopUsing();
|
||||
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000 - 1);
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(manager.isConnected()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should become idle on a single stop no matter how often use was started', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
await manager.connect();
|
||||
manager.startUsing();
|
||||
manager.startUsing();
|
||||
|
||||
manager.stopUsing();
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
||||
|
||||
expect(manager.isConnected()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should restart the countdown when stopped without having been in use', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
await manager.connect();
|
||||
|
||||
vi.advanceTimersByTime((disconnectSeconds * 1000) / 2);
|
||||
manager.stopUsing();
|
||||
|
||||
vi.advanceTimersByTime((disconnectSeconds * 1000) / 2);
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
|
||||
vi.advanceTimersByTime((disconnectSeconds * 1000) / 2);
|
||||
expect(manager.isConnected()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not reconnect after an explicit disconnect while in use', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const manager = createManagerWithDisconnectSeconds();
|
||||
await manager.connect();
|
||||
manager.startUsing();
|
||||
manager.disconnect();
|
||||
|
||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
||||
|
||||
expect(manager.isConnected()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not disconnect after being explicitly disconnected and reconnected', async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const disconnectSeconds = 10;
|
||||
const api = createCardAPI();
|
||||
const manager = new MicrophoneManager(api);
|
||||
vi.mocked(navigatorMock.mediaDevices.getUserMedia).mockResolvedValue(
|
||||
createMockStream(),
|
||||
);
|
||||
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
live: {
|
||||
microphone: {
|
||||
always_connected: false,
|
||||
disconnect_seconds: disconnectSeconds,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await manager.connect();
|
||||
|
||||
vi.advanceTimersByTime((disconnectSeconds * 1000) / 2);
|
||||
manager.disconnect();
|
||||
await manager.connect();
|
||||
|
||||
// The countdown from the first connection must not survive the disconnect
|
||||
// and cut the second one short.
|
||||
vi.advanceTimersByTime((disconnectSeconds * 1000) / 2);
|
||||
expect(manager.isConnected()).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('should require initialization', async () => {
|
||||
it('should require when configured and supported', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
Reference in New Issue
Block a user