fix: Improve 2-way audio detection (#2293)

- For Frigate cameras, you must be running at least [integration
v5.12.0](https://github.com/blakeblackshear/frigate-hass-integration/releases/tag/v5.12.0).
 - Closes: #2191 

Includes a significant refactor of cameras, and the introduction of a
`2-way-audio` capability that is dynamically fetched from `go2rtc`. One
gotcha is if you previously had a substream with 2-way audio, you may
need to modify

```yaml
 - camera_entity: camera.foo
    capabilities:
      disable_except:
        - substream
```

... to ...

```yaml
 - camera_entity: camera.foo
    capabilities:
      disable_except:
        - substream
        - 2-way-audio
```
This commit is contained in:
Dermot Duffy
2025-12-20 13:06:09 -08:00
committed by GitHub
parent 588ed6c98a
commit 1e821f09c6
47 changed files with 2240 additions and 958 deletions
@@ -961,19 +961,23 @@ describe('MenuButtonController', () => {
});
describe('should have microphone button', () => {
it('with suitable loaded media', () => {
it('when camera has 2-way-audio capability', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(false);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
}),
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
@@ -997,19 +1001,18 @@ describe('MenuButtonController', () => {
});
});
it('without suitable loaded media', () => {
it('when camera does not have 2-way-audio capability', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(false);
vi.mocked(microphoneManager.isMuted).mockReturnValue(false);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const cameraManager = createCameraManager(
createStore([{ cameraID: 'camera-1', capabilities: createCapabilities() }]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: false,
},
}),
});
expect(buttons).not.toEqual(
@@ -1021,13 +1024,17 @@ describe('MenuButtonController', () => {
const microphoneManager = mock<MicrophoneManager>();
vi.mocked(microphoneManager.isForbidden).mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
}),
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
@@ -1046,13 +1053,17 @@ describe('MenuButtonController', () => {
vi.mocked(microphoneManager.isMuted).mockReturnValue(true);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
}),
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
@@ -1079,13 +1090,17 @@ describe('MenuButtonController', () => {
vi.mocked(microphoneManager.isMuted).mockReturnValue(true);
vi.mocked(microphoneManager.isSupported).mockReturnValue(false);
const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
}),
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
});
expect(buttons).toContainEqual({
@@ -1104,13 +1119,17 @@ describe('MenuButtonController', () => {
vi.mocked(microphoneManager.isMuted).mockReturnValue(true);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
}),
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
config: createConfig({
menu: { buttons: { microphone: { type: 'toggle' } } },
}),
@@ -1136,13 +1155,17 @@ describe('MenuButtonController', () => {
vi.mocked(microphoneManager.isMuted).mockReturnValue(false);
vi.mocked(microphoneManager.isSupported).mockReturnValue(true);
const buttons = calculateButtons(controller, {
microphoneManager: microphoneManager,
currentMediaLoadedInfo: createMediaLoadedInfo({
capabilities: {
supports2WayAudio: true,
const cameraManager = createCameraManager(
createStore([
{
cameraID: 'camera-1',
capabilities: createCapabilities({ '2-way-audio': true }),
},
}),
]),
);
const buttons = calculateButtons(controller, {
cameraManager,
microphoneManager: microphoneManager,
config: createConfig({
menu: { buttons: { microphone: { type: 'toggle' } } },
}),