fix: Release the microphone to the browser when a call ends (#2685)
The microphone is connected when a call needs it and released the moment that call ends, so the browser stops reporting it as in use at hangup rather than `disconnect_seconds` later. Existing configurations are migrated automatically by the visual editor. - Closes: #2681 BREAKING CHANGE: `live.microphone.disconnect_seconds` is removed. The microphone is released when a call ends, so there is no idle countdown to configure. Use `live.microphone.always_connected` to hold it open instead. BREAKING CHANGE: The `microphone_connect` and `microphone_disconnect` actions are removed. The card owns the microphone lifecycle; `microphone_mute` and `microphone_unmute` remain. BREAKING CHANGE: `call` is removed from `live.microphone.auto_mute`, whose default is now `[]`. The microphone is muted when a call ends regardless of this option. BREAKING CHANGE: `microphone_unmute` has no effect outside a call. Nothing carries the audio at any other time, so the request is ignored rather than opening the microphone.
This commit is contained in:
@@ -309,7 +309,7 @@ describe('MicrophoneActionsController', () => {
|
||||
});
|
||||
|
||||
describe('on call answered state change', () => {
|
||||
it('should unmute on call answer when call is a configured unmute condition', () => {
|
||||
it('should unmute on call answer when call is a configured unmute condition', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
@@ -317,13 +317,13 @@ describe('MicrophoneActionsController', () => {
|
||||
autoUnmuteConditions: ['call' as const],
|
||||
});
|
||||
|
||||
controller.setCallAnswered(false);
|
||||
controller.setCallAnswered(true);
|
||||
await controller.setCallAnswered(false);
|
||||
await controller.setCallAnswered(true);
|
||||
|
||||
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should unmute when the call is already answered on first notification', () => {
|
||||
it('should unmute when the call is already answered on first notification', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
@@ -334,41 +334,42 @@ describe('MicrophoneActionsController', () => {
|
||||
// `setCallAnswered(true)` is the first call-state signal, with no
|
||||
// preceding `false` -- as for a live view that mounts while a call is
|
||||
// already answered. The initial state must not be swallowed as a baseline.
|
||||
controller.setCallAnswered(true);
|
||||
await controller.setCallAnswered(true);
|
||||
|
||||
expect(microphoneManager.unmute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should mute on call end when call is a configured mute condition', () => {
|
||||
it('should not mute on call end', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
microphoneManager,
|
||||
autoMuteConditions: ['call' as const],
|
||||
});
|
||||
|
||||
controller.setCallAnswered(true);
|
||||
controller.setCallAnswered(false);
|
||||
|
||||
expect(microphoneManager.mute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not act on the initial call state', () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
microphoneManager,
|
||||
autoMuteConditions: ['call' as const],
|
||||
autoUnmuteConditions: ['call' as const],
|
||||
});
|
||||
|
||||
controller.setCallAnswered(false);
|
||||
await controller.setCallAnswered(true);
|
||||
await controller.setCallAnswered(false);
|
||||
|
||||
// The microphone manager mutes and releases the microphone itself when
|
||||
// the call ends.
|
||||
expect(microphoneManager.mute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not act on the initial call state', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
microphoneManager,
|
||||
autoUnmuteConditions: ['call' as const],
|
||||
});
|
||||
|
||||
await controller.setCallAnswered(false);
|
||||
|
||||
expect(microphoneManager.mute).not.toHaveBeenCalled();
|
||||
expect(microphoneManager.unmute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not act on call answer when call is not a configured condition', () => {
|
||||
it('should not act on call answer when call is not a configured condition', async () => {
|
||||
const microphoneManager = createMicrophoneManager();
|
||||
const controller = new MicrophoneActionsController();
|
||||
controller.setOptions({
|
||||
@@ -376,8 +377,8 @@ describe('MicrophoneActionsController', () => {
|
||||
autoUnmuteConditions: [],
|
||||
});
|
||||
|
||||
controller.setCallAnswered(false);
|
||||
controller.setCallAnswered(true);
|
||||
await controller.setCallAnswered(false);
|
||||
await controller.setCallAnswered(true);
|
||||
|
||||
expect(microphoneManager.unmute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user