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:
@@ -0,0 +1,89 @@
|
||||
import { ZodSchema } from 'zod';
|
||||
import { localize } from '../localize/localize';
|
||||
import { AdvancedCameraCardError, Endpoint } from '../types';
|
||||
import { homeAssistantSignPath } from './sign-path';
|
||||
import { HomeAssistant } from './types';
|
||||
|
||||
/**
|
||||
* Fetch a JSON response from a signed or unsigned endpoint and validate it
|
||||
* against a Zod schema.
|
||||
* May throw.
|
||||
*
|
||||
* @param hass Home Assistant instance.
|
||||
* @param endpoint The endpoint to fetch from (string or Endpoint object).
|
||||
* @param schema The Zod schema to validate the response against.
|
||||
* @returns The parsed data or throws if fetch/validation fails.
|
||||
*/
|
||||
export const homeAssistantSignAndFetch = async <T>(
|
||||
hass: HomeAssistant,
|
||||
endpoint: Endpoint,
|
||||
schema: ZodSchema<T>,
|
||||
options?: {
|
||||
timeoutSeconds?: number;
|
||||
},
|
||||
): Promise<T> => {
|
||||
let url: string | null = endpoint.endpoint;
|
||||
const sign = endpoint.sign;
|
||||
|
||||
// Sign the path if needed
|
||||
if (sign) {
|
||||
try {
|
||||
url = await homeAssistantSignPath(hass, url);
|
||||
} catch (error) {
|
||||
throw new AdvancedCameraCardError(localize('error.failed_sign'), {
|
||||
endpoint,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
if (!url) {
|
||||
throw new AdvancedCameraCardError(localize('error.failed_sign'), {
|
||||
endpoint,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
...(options?.timeoutSeconds && {
|
||||
signal: AbortSignal.timeout(options.timeoutSeconds * 1000),
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new AdvancedCameraCardError(`${localize('error.failed_fetch')}: ${url}`, {
|
||||
endpoint,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new AdvancedCameraCardError(localize('error.failed_response'), {
|
||||
endpoint,
|
||||
response,
|
||||
});
|
||||
}
|
||||
|
||||
let data: unknown;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (error) {
|
||||
throw new AdvancedCameraCardError(localize('error.invalid_response'), {
|
||||
endpoint,
|
||||
response,
|
||||
error,
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = schema.safeParse(data);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new AdvancedCameraCardError(localize('error.invalid_response'), {
|
||||
endpoint,
|
||||
data,
|
||||
error: parsed.error,
|
||||
});
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LRUCache } from '../cache/lru';
|
||||
import { errorToConsole } from '../utils/basic';
|
||||
import { HomeAssistant, ResolvedMedia, resolvedMediaSchema } from './types';
|
||||
import { homeAssistantWSRequest } from './ws-request';
|
||||
import { HomeAssistant, ResolvedMedia, resolvedMediaSchema } from './types';
|
||||
|
||||
// It's important the cache size be at least as large as the largest likely
|
||||
// media query or media items will from a given query will be evicted for other
|
||||
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
import { SignedPath, signedPathSchema } from '../types';
|
||||
import { HomeAssistant } from './types';
|
||||
import { homeAssistantWSRequest } from './ws-request';
|
||||
import { HomeAssistant } from './types';
|
||||
|
||||
/**
|
||||
* Request that HA sign a path. May throw.
|
||||
@@ -10,7 +10,6 @@ import { homeAssistantWSRequest } from './ws-request';
|
||||
* HA will sign for 30 seconds).
|
||||
* @returns The signed URL, or null if the response was malformed.
|
||||
*/
|
||||
|
||||
export async function homeAssistantSignPath(
|
||||
hass: HomeAssistant,
|
||||
path: string,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CameraProxyConfig } from '../camera-manager/types';
|
||||
import { Endpoint } from '../types';
|
||||
import { HomeAssistant } from './types';
|
||||
|
||||
export const HASS_WEB_PROXY_DOMAIN = 'hass_web_proxy';
|
||||
@@ -59,3 +60,45 @@ export async function addDynamicProxyURL(
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
interface CreateProxiedEndpointOptions {
|
||||
context?: 'live' | 'media';
|
||||
ttl?: number;
|
||||
websocket?: boolean;
|
||||
openLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a proxied endpoint if the proxy configuration requires it.
|
||||
* Handles dynamic proxy registration and returns a proxied Endpoint.
|
||||
* @param hass Home Assistant instance.
|
||||
* @param endpoint The endpoint to potentially proxy.
|
||||
* @param proxyConfig The camera proxy configuration. If undefined, returns original endpoint.
|
||||
* @param options Additional options for proxy registration.
|
||||
* @returns Proxied Endpoint if proxying needed, original endpoint otherwise.
|
||||
*/
|
||||
export const createProxiedEndpointIfNecessary = async (
|
||||
hass: HomeAssistant,
|
||||
endpoint: Endpoint,
|
||||
proxyConfig?: CameraProxyConfig,
|
||||
options?: CreateProxiedEndpointOptions,
|
||||
): Promise<Endpoint> => {
|
||||
const context = options?.context ?? 'media';
|
||||
if (!proxyConfig || !shouldUseWebProxy(hass, proxyConfig, context)) {
|
||||
return endpoint;
|
||||
}
|
||||
if (proxyConfig.dynamic) {
|
||||
// Strip hash fragment for registration - it's client-side only and
|
||||
// not relevant for proxy pattern matching.
|
||||
const registrationUrl = endpoint.endpoint.split(/#/)[0];
|
||||
await addDynamicProxyURL(hass, registrationUrl, {
|
||||
proxyConfig,
|
||||
ttl: options?.ttl,
|
||||
openLimit: options?.openLimit ?? 0,
|
||||
});
|
||||
}
|
||||
return {
|
||||
endpoint: getWebProxiedURL(endpoint.endpoint, { websocket: options?.websocket }),
|
||||
sign: true,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@ import { HomeAssistant } from './types';
|
||||
* @param request The request to make.
|
||||
* @returns The parsed valid response or null on malformed.
|
||||
*/
|
||||
|
||||
export async function homeAssistantWSRequest<T>(
|
||||
hass: HomeAssistant,
|
||||
schema: ZodSchema<T>,
|
||||
|
||||
Reference in New Issue
Block a user