feat: Allow 2-way audio detection to be skipped and timed (#2355)

- For #2313
This commit is contained in:
Dermot Duffy
2026-02-16 19:47:56 -08:00
committed by GitHub
parent f34c7e8ee1
commit a15376602f
14 changed files with 337 additions and 34 deletions
+5 -4
View File
@@ -46,10 +46,11 @@ cameras:
# [...] # [...]
``` ```
| Option | Default | Description | | Option | Default | Description |
| ---------------- | ------- | ---------------------------------------------------------------------------------------------------------- | | ---------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `disable` | | A list of camera capabilities to disable. By default all capabilities supported by the camera are enabled. | | `disable` | | A list of camera capabilities to disable. By default all capabilities supported by the camera are enabled. |
| `disable_except` | | A list of camera capabilities to leave enabled if supported. Everything else will be disabled. | | `disable_except` | | A list of camera capabilities to leave enabled if supported. Everything else will be disabled. |
| `force` | | A list of capabilities to force-enable instead of auto-detecting them. Currently only supports `2-way-audio`. `disable` / `disable_except` take precedence over `force`. |
### Capabilities ### Capabilities
+14 -5
View File
@@ -26,15 +26,23 @@ cameras:
# [...] # [...]
``` ```
| Option | Default | Description | | Option | Default | Description |
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `modes` | `[webrtc, mse, mp4, mjpeg]` | An ordered list of `go2rtc` modes to use. Valid values are `webrtc`, `mse`, `mp4` or `mjpeg` values. | | `metadata_fetch_timeout_seconds` | `2` | Timeout for the go2rtc stream metadata fetch (this is used to detect stream capabilities such as `2-way-audio`). |
| `stream` | Determined by camera engine (e.g. `frigate` camera name). | A valid `go2rtc` stream name. | | `modes` | `[webrtc, mse, mp4, mjpeg]` | An ordered list of `go2rtc` modes to use. Valid values are `webrtc`, `mse`, `mp4` or `mjpeg` values. |
| `url` | Determined by camera engine (e.g. the `frigate` engine will automatically generate a URL for the go2rtc backend that runs in the Frigate container). | The root `go2rtc` URL the card should stream the video from. This is only needed for non-Frigate usecases, or advanced Frigate usecases. Example: `http://my-custom-go2rtc:1984` | | `stream` | Determined by camera engine (e.g. `frigate` camera name). | A valid `go2rtc` stream name. |
| `url` | Determined by camera engine (e.g. the `frigate` engine will automatically generate a URL for the go2rtc backend that runs in the Frigate container). | The root `go2rtc` URL the card should stream the video from. This is only needed for non-Frigate usecases, or advanced Frigate usecases. Example: `http://my-custom-go2rtc:1984` |
> [!NOTE] > [!NOTE]
> If `url` is manually set and `proxy.live` is set to `auto` on the camera (the default), the video stream will automatically be proxied via the Home Assistant process if the [hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) is detected. See [proxying](./README.md?id=proxy). > If `url` is manually set and `proxy.live` is set to `auto` on the camera (the default), the video stream will automatically be proxied via the Home Assistant process if the [hass-web-proxy-integration](https://github.com/dermotduffy/hass-web-proxy-integration) is detected. See [proxying](./README.md?id=proxy).
> [!TIP]
> If you are certain your camera hardware supports 2-way audio but the
> microphone button is intermittently missing on load, try increasing
> `metadata_fetch_timeout_seconds` or use
> [`capabilities.force`](./README.md?id=capabilities) to skip metadata
> detection entirely.
## `image` ## `image`
All configuration is under: All configuration is under:
@@ -119,6 +127,7 @@ cameras:
- mjpeg - mjpeg
stream: sitting_room stream: sitting_room
url: 'https://my.custom.go2rtc.backend' url: 'https://my.custom.go2rtc.backend'
metadata_fetch_timeout_seconds: 2
- camera_entity: camera.office_jsmpeg - camera_entity: camera.office_jsmpeg
live_provider: jsmpeg live_provider: jsmpeg
jsmpeg: jsmpeg:
+7
View File
@@ -20,6 +20,11 @@ challenging.
- Only the `webrtc` mode supports 2-way audio: - Only the `webrtc` mode supports 2-way audio:
- Must have microphone menu button enabled: - Must have microphone menu button enabled:
If your setup supports 2-way audio but detection is intermittent on load:
- Increase `cameras[].go2rtc.metadata_fetch_timeout_seconds`.
- Or force the capability with `cameras[].capabilities.force: ['2-way-audio']`.
## Example configuration ## Example configuration
```yaml ```yaml
@@ -30,6 +35,8 @@ cameras:
go2rtc: go2rtc:
modes: modes:
- webrtc - webrtc
# Optional: For slower cameras increase timeout (default: 2)
metadata_fetch_timeout_seconds: 10
menu: menu:
buttons: buttons:
microphone: microphone:
+25 -7
View File
@@ -85,15 +85,13 @@ export class Camera {
): Promise<Capabilities> { ): Promise<Capabilities> {
const rawCapabilities = await this._getRawCapabilities(options); const rawCapabilities = await this._getRawCapabilities(options);
const config = this.getConfig(); const config = this.getConfig();
const has2WayAudio = await liveProviderSupports2WayAudio( const has2WayAudio = await this._has2WayAudioCapability(options.hass);
options.hass,
config,
this._getGo2RTCMetadataEndpoint(),
this.getProxyConfig(),
);
return new Capabilities( return new Capabilities(
{ ...rawCapabilities, '2-way-audio': has2WayAudio }, {
...rawCapabilities,
'2-way-audio': has2WayAudio,
},
{ {
disable: config.capabilities?.disable, disable: config.capabilities?.disable,
disableExcept: config.capabilities?.disable_except, disableExcept: config.capabilities?.disable_except,
@@ -101,6 +99,26 @@ export class Camera {
); );
} }
protected async _has2WayAudioCapability(hass: HomeAssistant): Promise<boolean> {
if (this._config.capabilities?.disable?.includes('2-way-audio')) {
return false;
}
const disableExcept = this._config.capabilities?.disable_except;
if (disableExcept?.length && !disableExcept.includes('2-way-audio')) {
return false;
}
if (this._config.capabilities?.force?.includes('2-way-audio')) {
return true;
}
return await liveProviderSupports2WayAudio(
hass,
this.getConfig(),
this.getConfig().go2rtc.metadata_fetch_timeout_seconds,
this._getGo2RTCMetadataEndpoint(),
this.getProxyConfig(),
);
}
/** /**
* Get raw capabilities for this camera. Subclasses should override * Get raw capabilities for this camera. Subclasses should override
* and call super._getRawCapabilities() to extend defaults. * and call super._getRawCapabilities() to extend defaults.
+8 -7
View File
@@ -6,18 +6,14 @@ import { errorToConsole } from '../../../utils/basic';
import { CameraProxyConfig } from '../../types'; import { CameraProxyConfig } from '../../types';
import { Go2RTCStreamInfo, go2RTCStreamInfoSchema } from './types'; import { Go2RTCStreamInfo, go2RTCStreamInfoSchema } from './types';
// Allow generous amount of time to fetch metadata (timeout matches that used in
// the Frigate frontend for the same call).
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2313
const GO2RTC_METADATA_TIMEOUT_SECONDS = 10;
const getGo2RTCStreamMetadata = async ( const getGo2RTCStreamMetadata = async (
hass: HomeAssistant, hass: HomeAssistant,
endpoint: Endpoint, endpoint: Endpoint,
timeoutSeconds: number,
): Promise<Go2RTCStreamInfo | null> => { ): Promise<Go2RTCStreamInfo | null> => {
try { try {
return await homeAssistantSignAndFetch(hass, endpoint, go2RTCStreamInfoSchema, { return await homeAssistantSignAndFetch(hass, endpoint, go2RTCStreamInfoSchema, {
timeoutSeconds: GO2RTC_METADATA_TIMEOUT_SECONDS, timeoutSeconds,
}); });
} catch (e) { } catch (e) {
errorToConsole(e as Error); errorToConsole(e as Error);
@@ -53,6 +49,7 @@ const streamSupports2WayAudio = (streamInfo: Go2RTCStreamInfo | null): boolean =
*/ */
export const supports2WayAudio = async ( export const supports2WayAudio = async (
hass: HomeAssistant, hass: HomeAssistant,
metadataFetchTimeoutSeconds: number,
go2rtcMetadataEndpoint?: Endpoint | null, go2rtcMetadataEndpoint?: Endpoint | null,
proxyConfig?: CameraProxyConfig, proxyConfig?: CameraProxyConfig,
): Promise<boolean> => { ): Promise<boolean> => {
@@ -67,6 +64,10 @@ export const supports2WayAudio = async (
{ context: 'live', openLimit: 1 }, { context: 'live', openLimit: 1 },
); );
const streamInfo = await getGo2RTCStreamMetadata(hass, endpoint); const streamInfo = await getGo2RTCStreamMetadata(
hass,
endpoint,
metadataFetchTimeoutSeconds,
);
return streamSupports2WayAudio(streamInfo); return streamSupports2WayAudio(streamInfo);
}; };
+12 -1
View File
@@ -31,6 +31,11 @@ const LIVE_PROVIDERS = [
] as const; ] as const;
export type LiveProvider = (typeof LIVE_PROVIDERS)[number]; export type LiveProvider = (typeof LIVE_PROVIDERS)[number];
const go2rtcConfigDefault = {
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2313
metadata_fetch_timeout_seconds: 2,
};
const go2rtcConfigSchema = z.object({ const go2rtcConfigSchema = z.object({
url: z url: z
.string() .string()
@@ -39,6 +44,11 @@ const go2rtcConfigSchema = z.object({
host: z.string().optional(), host: z.string().optional(),
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(), modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
stream: z.string().optional(), stream: z.string().optional(),
metadata_fetch_timeout_seconds: z
.number()
.int()
.positive()
.default(go2rtcConfigDefault.metadata_fetch_timeout_seconds),
}); });
const webrtcCardConfigSchema = z const webrtcCardConfigSchema = z
@@ -219,6 +229,7 @@ export const cameraConfigSchema = z
.object({ .object({
disable: z.enum(capabilityKeys).array().optional(), disable: z.enum(capabilityKeys).array().optional(),
disable_except: z.enum(capabilityKeys).array().optional(), disable_except: z.enum(capabilityKeys).array().optional(),
force: z.enum(['2-way-audio']).array().optional(),
}) })
.optional(), .optional(),
@@ -306,7 +317,7 @@ export const cameraConfigSchema = z
// Live provider options. // Live provider options.
live_provider: z.enum(LIVE_PROVIDERS).default(cameraConfigDefault.live_provider), live_provider: z.enum(LIVE_PROVIDERS).default(cameraConfigDefault.live_provider),
go2rtc: go2rtcConfigSchema.optional(), go2rtc: go2rtcConfigSchema.optional().default(go2rtcConfigDefault),
image: imageBaseConfigSchema.optional().default(imageConfigDefault), image: imageBaseConfigSchema.optional().default(imageConfigDefault),
jsmpeg: jsmpegConfigSchema.optional(), jsmpeg: jsmpegConfigSchema.optional(),
webrtc_card: webrtcCardConfigSchema.optional(), webrtc_card: webrtcCardConfigSchema.optional(),
+2
View File
@@ -20,6 +20,8 @@ export const CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE =
`${CONF_CAMERAS}.#.capabilities.disable` as const; `${CONF_CAMERAS}.#.capabilities.disable` as const;
export const CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT = export const CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT =
`${CONF_CAMERAS}.#.capabilities.disable_except` as const; `${CONF_CAMERAS}.#.capabilities.disable_except` as const;
export const CONF_CAMERAS_ARRAY_CAPABILITIES_FORCE =
`${CONF_CAMERAS}.#.capabilities.force` as const;
export const CONF_CAMERAS_ARRAY_CAST_METHOD = `${CONF_CAMERAS}.#.cast.method` as const; export const CONF_CAMERAS_ARRAY_CAST_METHOD = `${CONF_CAMERAS}.#.cast.method` as const;
export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH = export const CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH =
`${CONF_CAMERAS}.#.cast.dashboard.dashboard_path` as const; `${CONF_CAMERAS}.#.cast.dashboard.dashboard_path` as const;
+19
View File
@@ -38,6 +38,7 @@ import {
CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE, CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE,
CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT, CONF_CAMERAS_ARRAY_CAPABILITIES_DISABLE_EXCEPT,
CONF_CAMERAS_ARRAY_CAPABILITIES_FORCE,
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH, CONF_CAMERAS_ARRAY_CAST_DASHBOARD_DASHBOARD_PATH,
CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH, CONF_CAMERAS_ARRAY_CAST_DASHBOARD_VIEW_PATH,
CONF_CAMERAS_ARRAY_CAST_METHOD, CONF_CAMERAS_ARRAY_CAST_METHOD,
@@ -946,6 +947,14 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
}, },
]; ];
protected _forceableCapabilities: EditorSelectOption[] = [
{ value: '', label: '' },
{
value: '2-way-audio',
label: localize('config.cameras.capabilities.capabilities.2-way-audio'),
},
];
protected _defaultResetInteractionModes: EditorSelectOption[] = [ protected _defaultResetInteractionModes: EditorSelectOption[] = [
{ value: '', label: '' }, { value: '', label: '' },
{ {
@@ -2703,6 +2712,16 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
multiple: true, multiple: true,
}, },
)} )}
${this._renderOptionSelector(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_CAPABILITIES_FORCE,
cameraIndex,
),
this._forceableCapabilities,
{
multiple: true,
},
)}
`, `,
)} )}
${this._putInSubmenu( ${this._putInSubmenu(
+2
View File
@@ -30,6 +30,7 @@
"camera_entity": "Camera Entity", "camera_entity": "Camera Entity",
"capabilities": { "capabilities": {
"capabilities": { "capabilities": {
"2-way-audio": "2-way Audio",
"clips": "Clips", "clips": "Clips",
"favorite-events": "Favorite Events", "favorite-events": "Favorite Events",
"favorite-recordings": "Favorite Recordings", "favorite-recordings": "Favorite Recordings",
@@ -43,6 +44,7 @@
}, },
"disable": "Disable", "disable": "Disable",
"disable_except": "Disable except", "disable_except": "Disable except",
"force": "Force",
"editor_label": "Camera capabilities" "editor_label": "Camera capabilities"
}, },
"cast": { "cast": {
+7 -1
View File
@@ -25,6 +25,7 @@ export const getResolvedLiveProvider = (
export const liveProviderSupports2WayAudio = async ( export const liveProviderSupports2WayAudio = async (
hass: HomeAssistant, hass: HomeAssistant,
config: CameraConfig, config: CameraConfig,
metadataFetchTimeoutSeconds: number,
go2rtcMetadataEndpoint?: Endpoint | null, go2rtcMetadataEndpoint?: Endpoint | null,
proxyConfig?: CameraProxyConfig, proxyConfig?: CameraProxyConfig,
): Promise<boolean> => { ): Promise<boolean> => {
@@ -32,5 +33,10 @@ export const liveProviderSupports2WayAudio = async (
return false; return false;
} }
return gortcSupports2WayAudio(hass, go2rtcMetadataEndpoint, proxyConfig); return gortcSupports2WayAudio(
hass,
metadataFetchTimeoutSeconds,
go2rtcMetadataEndpoint,
proxyConfig,
);
}; };
+170
View File
@@ -76,6 +76,10 @@ describe('Camera', () => {
}); });
describe('initialize', () => { describe('initialize', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should initialize and destroy', async () => { it('should initialize and destroy', async () => {
const camera = new Camera( const camera = new Camera(
createCameraConfig({ createCameraConfig({
@@ -123,6 +127,7 @@ describe('Camera', () => {
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith( expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
expect.anything(), expect.anything(),
expect.anything(), expect.anything(),
2,
{ {
endpoint: endpoint:
'http://go2rtc/api/streams?src=stream&video=all&audio=all&microphone', 'http://go2rtc/api/streams?src=stream&video=all&audio=all&microphone',
@@ -154,6 +159,171 @@ describe('Camera', () => {
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false); expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
}); });
it('should pass camera go2rtc metadata timeout', async () => {
const camera = new Camera(
createCameraConfig({
go2rtc: {
url: 'http://go2rtc',
stream: 'stream',
metadata_fetch_timeout_seconds: 20,
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
20,
expect.anything(),
expect.anything(),
);
});
it('should force 2-way-audio capability true without metadata fetch', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
force: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(true);
});
it('should prefer disable over force rules', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
disable: ['2-way-audio'],
force: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
});
it('should prefer disable_except over force rules', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
disable_except: ['substream'],
force: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
});
it('should not fetch metadata when 2-way-audio is disabled', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
disable: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
});
it('should not fetch metadata when disable_except excludes 2-way-audio', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
disable_except: ['substream'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
});
it('should fetch metadata when disable_except includes 2-way-audio', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
disable_except: ['substream', '2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(true);
});
it('should fetch metadata when disable_except is empty', async () => {
const camera = new Camera(
createCameraConfig({
capabilities: {
disable_except: [],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalled();
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(true);
});
}); });
describe('should handle trigger state changes', () => { describe('should handle trigger state changes', () => {
@@ -18,7 +18,7 @@ describe('supports2WayAudio', () => {
}); });
it('should return false if no endpoint provided', async () => { it('should return false if no endpoint provided', async () => {
expect(await supports2WayAudio(hass, null)).toBe(false); expect(await supports2WayAudio(hass, 2, null)).toBe(false);
}); });
it('should return false if fetch fails', async () => { it('should return false if fetch fails', async () => {
@@ -26,7 +26,7 @@ describe('supports2WayAudio', () => {
vi.mocked(homeAssistantSignAndFetch).mockRejectedValue(new Error('fetch error')); vi.mocked(homeAssistantSignAndFetch).mockRejectedValue(new Error('fetch error'));
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {}); const spy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await supports2WayAudio(hass, endpoint); const result = await supports2WayAudio(hass, 2, endpoint);
expect(result).toBe(false); expect(result).toBe(false);
expect(spy).toHaveBeenCalledWith('fetch error'); expect(spy).toHaveBeenCalledWith('fetch error');
@@ -37,7 +37,35 @@ describe('supports2WayAudio', () => {
vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(endpoint); vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(endpoint);
vi.mocked(homeAssistantSignAndFetch).mockResolvedValue({ producers: undefined }); vi.mocked(homeAssistantSignAndFetch).mockResolvedValue({ producers: undefined });
expect(await supports2WayAudio(hass, endpoint)).toBe(false); expect(await supports2WayAudio(hass, 2, endpoint)).toBe(false);
});
it('should use default metadata fetch timeout', async () => {
vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(endpoint);
vi.mocked(homeAssistantSignAndFetch).mockResolvedValue({ producers: [] });
await supports2WayAudio(hass, 2, endpoint);
expect(homeAssistantSignAndFetch).toHaveBeenCalledWith(
hass,
endpoint,
expect.anything(),
{ timeoutSeconds: 2 },
);
});
it('should use custom metadata fetch timeout when provided', async () => {
vi.mocked(createProxiedEndpointIfNecessary).mockResolvedValue(endpoint);
vi.mocked(homeAssistantSignAndFetch).mockResolvedValue({ producers: [] });
await supports2WayAudio(hass, 15, endpoint);
expect(homeAssistantSignAndFetch).toHaveBeenCalledWith(
hass,
endpoint,
expect.anything(),
{ timeoutSeconds: 15 },
);
}); });
it('should return false if no producer supports audio', async () => { it('should return false if no producer supports audio', async () => {
@@ -50,7 +78,7 @@ describe('supports2WayAudio', () => {
], ],
}); });
expect(await supports2WayAudio(hass, endpoint)).toBe(false); expect(await supports2WayAudio(hass, 2, endpoint)).toBe(false);
}); });
it('should return true if producer supports audio and sendonly', async () => { it('should return true if producer supports audio and sendonly', async () => {
@@ -63,7 +91,7 @@ describe('supports2WayAudio', () => {
], ],
}); });
expect(await supports2WayAudio(hass, endpoint)).toBe(true); expect(await supports2WayAudio(hass, 2, endpoint)).toBe(true);
}); });
it('should return true if producer supports audio and sendrecv', async () => { it('should return true if producer supports audio and sendrecv', async () => {
@@ -76,7 +104,7 @@ describe('supports2WayAudio', () => {
], ],
}); });
expect(await supports2WayAudio(hass, endpoint)).toBe(true); expect(await supports2WayAudio(hass, 2, endpoint)).toBe(true);
}); });
it('should handle missing medias in producer', async () => { it('should handle missing medias in producer', async () => {
@@ -89,6 +117,6 @@ describe('supports2WayAudio', () => {
], ],
}); });
expect(await supports2WayAudio(hass, endpoint)).toBe(false); expect(await supports2WayAudio(hass, 2, endpoint)).toBe(false);
}); });
}); });
+3
View File
@@ -23,6 +23,9 @@ describe('config defaults', () => {
frigate: { frigate: {
client_id: 'frigate', client_id: 'frigate',
}, },
go2rtc: {
metadata_fetch_timeout_seconds: 2,
},
image: { image: {
mode: 'auto', mode: 'auto',
refresh_seconds: 1, refresh_seconds: 1,
+28 -2
View File
@@ -71,7 +71,11 @@ describe('live-provider utils', () => {
live_provider: 'ha', live_provider: 'ha',
}); });
const hass = createHASS(); const hass = createHASS();
const result = await liveProviderSupports2WayAudio(hass, config); const result = await liveProviderSupports2WayAudio(
hass,
config,
config.go2rtc.metadata_fetch_timeout_seconds,
);
expect(result).toBe(false); expect(result).toBe(false);
}); });
@@ -82,10 +86,32 @@ describe('live-provider utils', () => {
const hass = createHASS(); const hass = createHASS();
vi.mocked(go2rtcAudio.supports2WayAudio).mockResolvedValue(true); vi.mocked(go2rtcAudio.supports2WayAudio).mockResolvedValue(true);
const result = await liveProviderSupports2WayAudio(hass, config); const result = await liveProviderSupports2WayAudio(
hass,
config,
config.go2rtc.metadata_fetch_timeout_seconds,
);
expect(result).toBe(true); expect(result).toBe(true);
expect(go2rtcAudio.supports2WayAudio).toHaveBeenCalledWith( expect(go2rtcAudio.supports2WayAudio).toHaveBeenCalledWith(
hass, hass,
2,
undefined,
undefined,
);
});
it('should pass metadata fetch timeout through to go2rtc detection', async () => {
const config = createCameraConfig({
live_provider: 'go2rtc',
});
const hass = createHASS();
vi.mocked(go2rtcAudio.supports2WayAudio).mockResolvedValue(true);
await liveProviderSupports2WayAudio(hass, config, 30);
expect(go2rtcAudio.supports2WayAudio).toHaveBeenCalledWith(
hass,
30,
undefined, undefined,
undefined, undefined,
); );