Generalize camera endpoints.

This commit is contained in:
Dermot Duffy
2023-02-18 11:03:12 -08:00
parent 5767e98501
commit f5ce02fe4a
13 changed files with 219 additions and 132 deletions
+5 -4
View File
@@ -18,9 +18,10 @@ import {
CameraManagerCameraCapabilities, CameraManagerCameraCapabilities,
CameraManagerMediaCapabilities, CameraManagerMediaCapabilities,
CameraManagerCameraMetadata, CameraManagerCameraMetadata,
CameraURLContext, CameraEndpointsContext,
CameraConfigs, CameraConfigs,
Engine, Engine,
CameraEndpoints,
} from './types'; } from './types';
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000; export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
@@ -118,8 +119,8 @@ export interface CameraManagerEngine {
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null; getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
getCameraURL( getCameraEndpoints(
cameraConfig: CameraConfig, cameraConfig: CameraConfig,
context?: CameraURLContext, context?: CameraEndpointsContext,
): string | null; ): CameraEndpoints | null;
} }
+56 -10
View File
@@ -36,8 +36,10 @@ import {
RecordingSegment, RecordingSegment,
RecordingSegmentsQuery, RecordingSegmentsQuery,
RecordingSegmentsQueryResultsMap, RecordingSegmentsQueryResultsMap,
CameraURLContext, CameraEndpointsContext,
CameraConfigs, CameraConfigs,
CameraEndpoints,
CameraEndpoint,
} from '../types'; } from '../types';
import { FrigateRecording } from './types'; import { FrigateRecording } from './types';
import { import {
@@ -1006,15 +1008,23 @@ export class FrigateCameraManagerEngine
}; };
} }
public getCameraURL( public getCameraEndpoints(
cameraConfig: CameraConfig, cameraConfig: CameraConfig,
context?: CameraURLContext, context?: CameraEndpointsContext,
): string | null { ): CameraEndpoints | null {
const getUIEndpoint = (): CameraEndpoint | null => {
if (!cameraConfig.frigate.url) { if (!cameraConfig.frigate.url) {
return null; return null;
} }
if (!cameraConfig.frigate.camera_name) { if (!cameraConfig.frigate.camera_name) {
return cameraConfig.frigate.url; return { endpoint: cameraConfig.frigate.url };
}
const cameraURL =
`${cameraConfig.frigate.url}/cameras/` + cameraConfig.frigate.camera_name;
if (context?.view === 'live') {
return { endpoint: cameraURL };
} }
const eventsURL = const eventsURL =
@@ -1027,11 +1037,11 @@ export class FrigateCameraManagerEngine
switch (context?.media?.getMediaType()) { switch (context?.media?.getMediaType()) {
case 'clip': case 'clip':
case 'snapshot': case 'snapshot':
return eventsURL; return { endpoint: eventsURL };
case 'recording': case 'recording':
const startTime = context.media.getStartTime(); const startTime = context.media.getStartTime();
if (startTime) { if (startTime) {
return recordingsURL + format(startTime, 'yyyy-MM-dd/HH'); return { endpoint: recordingsURL + format(startTime, 'yyyy-MM-dd/HH') };
} }
} }
@@ -1041,12 +1051,48 @@ export class FrigateCameraManagerEngine
case 'clips': case 'clips':
case 'snapshots': case 'snapshots':
case 'snapshot': case 'snapshot':
return eventsURL; return { endpoint: eventsURL };
case 'recording': case 'recording':
case 'recordings': case 'recordings':
return recordingsURL; return { endpoint: recordingsURL };
} }
return `${cameraConfig.frigate.url}/cameras/${cameraConfig.frigate.camera_name}`; return {
endpoint: cameraURL,
};
};
const getGo2RTC = (): CameraEndpoint | null => {
return {
endpoint:
`/api/frigate/${cameraConfig.frigate.client_id}` +
// go2rtc is exposed by the integration under the (slightly
// misleading) 'mse' path, even though that path can serve all go2rtc
// modes.
`/mse/api/ws?src=${cameraConfig.frigate.camera_name}`,
sign: true,
};
};
const getJSMPEG = (): CameraEndpoint | null => {
return {
endpoint:
`/api/frigate/${cameraConfig.frigate.client_id}` +
`/jsmpeg/${cameraConfig.frigate.camera_name}`,
sign: true,
};
};
const ui = getUIEndpoint();
const go2rtc = getGo2RTC();
const jsmpeg = getJSMPEG();
return ui || go2rtc || jsmpeg
? {
...(ui && { ui: ui }),
...(go2rtc && { go2rtc: go2rtc }),
...(jsmpeg && { jsmpeg: jsmpeg }),
}
: null;
} }
} }
+5 -4
View File
@@ -16,12 +16,13 @@ import {
RecordingQueryResultsMap, RecordingQueryResultsMap,
RecordingSegmentsQuery, RecordingSegmentsQuery,
RecordingSegmentsQueryResultsMap, RecordingSegmentsQueryResultsMap,
CameraURLContext, CameraEndpointsContext,
CameraConfigs, CameraConfigs,
RecordingQuery, RecordingQuery,
QueryReturnType, QueryReturnType,
CameraManagerCameraCapabilities, CameraManagerCameraCapabilities,
Engine, Engine,
CameraEndpoints,
} from '../types'; } from '../types';
import { getEntityIcon, getEntityTitle } from '../../utils/ha'; import { getEntityIcon, getEntityTitle } from '../../utils/ha';
import { EntityRegistryManager } from '../../utils/ha/entity-registry'; import { EntityRegistryManager } from '../../utils/ha/entity-registry';
@@ -177,10 +178,10 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
return null; return null;
} }
public getCameraURL( public getCameraEndpoints(
_cameraConfig: CameraConfig, _cameraConfig: CameraConfig,
_context?: CameraURLContext, _context?: CameraEndpointsContext,
): string | null { ): CameraEndpoints | null {
return null; return null;
} }
} }
+7 -3
View File
@@ -6,7 +6,7 @@ import {
CameraManagerCameraMetadata, CameraManagerCameraMetadata,
CameraManagerCapabilities, CameraManagerCapabilities,
CameraManagerMediaCapabilities, CameraManagerMediaCapabilities,
CameraURLContext, CameraEndpointsContext,
DataQuery, DataQuery,
EventQuery, EventQuery,
EventQueryResults, EventQueryResults,
@@ -29,6 +29,7 @@ import {
RecordingSegmentsQueryResults, RecordingSegmentsQueryResults,
RecordingSegmentsQueryResultsMap, RecordingSegmentsQueryResultsMap,
ResultsMap, ResultsMap,
CameraEndpoints,
} from './types.js'; } from './types.js';
import orderBy from 'lodash-es/orderBy'; import orderBy from 'lodash-es/orderBy';
import { CameraManagerEngineFactory } from './engine-factory.js'; import { CameraManagerEngineFactory } from './engine-factory.js';
@@ -643,13 +644,16 @@ export class CameraManager {
); );
} }
public getCameraURL(cameraID: string, context?: CameraURLContext): string | null { public getCameraEndpoints(
cameraID: string,
context?: CameraEndpointsContext,
): CameraEndpoints | null {
const cameraConfig = this._store.getCameraConfig(cameraID); const cameraConfig = this._store.getCameraConfig(cameraID);
const engine = this._store.getEngineForCameraID(cameraID); const engine = this._store.getEngineForCameraID(cameraID);
if (!cameraConfig || !engine) { if (!cameraConfig || !engine) {
return null; return null;
} }
return engine.getCameraURL(cameraConfig, context); return engine.getCameraEndpoints(cameraConfig, context);
} }
public getCameraMetadata( public getCameraMetadata(
+12 -1
View File
@@ -106,11 +106,22 @@ export interface CameraManagerCameraMetadata {
icon: string; icon: string;
} }
export interface CameraURLContext { export interface CameraEndpointsContext {
media?: ViewMedia; media?: ViewMedia;
view?: FrigateCardView; view?: FrigateCardView;
} }
export interface CameraEndpoint {
endpoint: string;
sign?: boolean;
}
export interface CameraEndpoints {
ui?: CameraEndpoint;
go2rtc?: CameraEndpoint;
jsmpeg?: CameraEndpoint;
}
export type CameraConfigs = Map<string, CameraConfig>; export type CameraConfigs = Map<string, CameraConfig>;
// =========== // ===========
+11 -8
View File
@@ -1461,15 +1461,18 @@ class FrigateCard extends LitElement {
* @returns The URL or null if unavailable. * @returns The URL or null if unavailable.
*/ */
protected _getCameraURLFromContext(): string | null { protected _getCameraURLFromContext(): string | null {
const view = this._view; if (!this._view) {
const selectedCameraID = view?.camera; return null;
const media = view?.queryResults?.getSelectedResult() ?? null; }
return this._hass && view && selectedCameraID
? this._cameraManager?.getCameraURL(selectedCameraID, { const selectedCameraID = this._view.camera;
const media = this._view.queryResults?.getSelectedResult() ?? null;
const endpoints =
this._cameraManager?.getCameraEndpoints(selectedCameraID, {
view: this._view.view,
...(media && { media: media }), ...(media && { media: media }),
...(view && { view: view.view }), }) ?? null;
}) ?? null return endpoints?.ui?.endpoint ?? null;
: null;
} }
/** /**
+20 -24
View File
@@ -7,7 +7,7 @@ import {
unsafeCSS, unsafeCSS,
} from 'lit'; } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import liveMSEStyle from '../../scss/live-mse-webrtc.scss'; import liveMSEStyle from '../../scss/live-go2rtc.scss';
import { CameraConfig, ExtendedHomeAssistant } from '../../types.js'; import { CameraConfig, ExtendedHomeAssistant } from '../../types.js';
import '../image.js'; import '../image.js';
import { import {
@@ -18,8 +18,8 @@ import { dispatchMediaLoadedEvent } from '../../utils/media-info';
import { localize } from '../../localize/localize'; import { localize } from '../../localize/localize';
import { dispatchErrorMessageEvent } from '../message'; import { dispatchErrorMessageEvent } from '../message';
import { VideoRTC } from '../../external/go2rtc/video-rtc'; import { VideoRTC } from '../../external/go2rtc/video-rtc';
import { homeAssistantSignPath } from '../../utils/ha'; import { CameraEndpoints } from '../../camera-manager/types.js';
import { errorToConsole } from '../../utils/basic'; import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
// Note (2023-02-18): Depending on the behavior of the player / browser is // Note (2023-02-18): Depending on the behavior of the player / browser is
// possible this URL will need to be re-signed in order to avoid HA spamming // possible this URL will need to be re-signed in order to avoid HA spamming
@@ -58,6 +58,9 @@ export class FrigateCardGo2RTC extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public cameraConfig?: CameraConfig; public cameraConfig?: CameraConfig;
@property({ attribute: false })
public cameraEndpoints?: CameraEndpoints;
protected _player?: FrigateCardGo2RTCPlayer; protected _player?: FrigateCardGo2RTCPlayer;
public play(): void { public play(): void {
@@ -87,36 +90,29 @@ export class FrigateCardGo2RTC extends LitElement {
} }
protected async _createPlayer(): Promise<void> { protected async _createPlayer(): Promise<void> {
if ( if (!this.hass) {
!this.hass ||
!this.cameraConfig?.frigate.client_id ||
!this.cameraConfig.frigate.camera_name
) {
return; return;
} }
let response: string | null | undefined; const endpoint = this.cameraEndpoints?.go2rtc;
try { if (!endpoint) {
response = await homeAssistantSignPath( return dispatchErrorMessageEvent(this, localize('error.live_camera_no_endpoint'), {
context: this.cameraConfig,
});
}
const address = await getEndpointAddressOrDispatchError(
this,
this.hass, this.hass,
`/api/frigate/${this.cameraConfig.frigate.client_id}` + endpoint,
`/mse/api/ws?src=${this.cameraConfig.frigate.camera_name}`,
GO2RTC_URL_SIGN_EXPIRY_SECONDS, GO2RTC_URL_SIGN_EXPIRY_SECONDS,
); );
} catch (e) { if (!address) {
errorToConsole(e as Error);
return; return;
} }
if (!response) {
return;
}
const url = response.replace(/^http/i, 'ws');
if (!url) {
return dispatchErrorMessageEvent(this, localize('error.failed_sign'));
}
this._player = new FrigateCardGo2RTCPlayer(); this._player = new FrigateCardGo2RTCPlayer();
this._player.src = url; this._player.src = address;
this._player.visibilityCheck = false; this._player.visibilityCheck = false;
this._player.background = true; this._player.background = true;
this._player.mode = 'webrtc'; this._player.mode = 'webrtc';
@@ -125,7 +121,7 @@ export class FrigateCardGo2RTC extends LitElement {
} }
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('cameraConfig')) { if (changedProps.has('cameraEndpoints')) {
this._createPlayer(); this._createPlayer();
} }
} }
+23 -41
View File
@@ -11,10 +11,11 @@ import {
ExtendedHomeAssistant, ExtendedHomeAssistant,
JSMPEGConfig, JSMPEGConfig,
} from '../../types.js'; } from '../../types.js';
import { homeAssistantSignPath } from '../../utils/ha';
import { dispatchMediaLoadedEvent } from '../../utils/media-info.js'; import { dispatchMediaLoadedEvent } from '../../utils/media-info.js';
import { dispatchErrorMessageEvent } from '../message.js'; import { dispatchErrorMessageEvent } from '../message.js';
import { contentsChanged, errorToConsole } from '../../utils/basic.js'; import { contentsChanged } from '../../utils/basic.js';
import { CameraEndpoints } from '../../camera-manager/types.js';
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint.js';
// Number of seconds a signed URL is valid for. // Number of seconds a signed URL is valid for.
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
@@ -27,6 +28,9 @@ export class FrigateCardLiveJSMPEG extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public cameraConfig?: CameraConfig; public cameraConfig?: CameraConfig;
@property({ attribute: false })
public cameraEndpoints?: CameraEndpoints;
@property({ attribute: false, hasChanged: contentsChanged }) @property({ attribute: false, hasChanged: contentsChanged })
public jsmpegConfig?: JSMPEGConfig; public jsmpegConfig?: JSMPEGConfig;
@@ -83,37 +87,6 @@ export class FrigateCardLiveJSMPEG extends LitElement {
// JSMPEG does not support seeking. // JSMPEG does not support seeking.
} }
/**
* Get a signed player URL.
* @returns A URL or null.
*/
protected async _getURL(): Promise<string | null> {
if (
!this.hass ||
!this.cameraConfig?.frigate.client_id ||
!this.cameraConfig?.frigate.camera_name
) {
return null;
}
let response: string | null | undefined;
try {
response = await homeAssistantSignPath(
this.hass,
`/api/frigate/${this.cameraConfig.frigate.client_id}` +
`/jsmpeg/${this.cameraConfig.frigate.camera_name}`,
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
);
} catch (e) {
errorToConsole(e as Error);
return null;
}
if (!response) {
return null;
}
return response.replace(/^http/i, 'ws');
}
/** /**
* Create a JSMPEG player. * Create a JSMPEG player.
* @param url The URL for the player to connect to. * @param url The URL for the player to connect to.
@@ -205,26 +178,35 @@ export class FrigateCardLiveJSMPEG extends LitElement {
* Refresh the JSMPEG player. * Refresh the JSMPEG player.
*/ */
protected async _refreshPlayer(): Promise<void> { protected async _refreshPlayer(): Promise<void> {
if (!this.hass) {
return;
}
this._resetPlayer(); this._resetPlayer();
this._jsmpegCanvasElement = document.createElement('canvas'); this._jsmpegCanvasElement = document.createElement('canvas');
this._jsmpegCanvasElement.className = 'media'; this._jsmpegCanvasElement.className = 'media';
if (!this.cameraConfig?.frigate.camera_name) { const endpoint = this.cameraEndpoints?.jsmpeg;
return dispatchErrorMessageEvent(this, localize('error.no_camera_name'), { if (!endpoint) {
return dispatchErrorMessageEvent(this, localize('error.live_camera_no_endpoint'), {
context: this.cameraConfig, context: this.cameraConfig,
}); });
} }
const url = await this._getURL(); const address = await getEndpointAddressOrDispatchError(
if (url) { this,
this._jsmpegVideoPlayer = await this._createJSMPEGPlayer(url); this.hass,
endpoint,
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
);
if (!address) {
return;
}
this._jsmpegVideoPlayer = await this._createJSMPEGPlayer(address);
this._refreshPlayerTimerID = window.setTimeout(() => { this._refreshPlayerTimerID = window.setTimeout(() => {
this.requestUpdate(); this.requestUpdate();
}, (JSMPEG_URL_SIGN_EXPIRY_SECONDS - JSMPEG_URL_SIGN_REFRESH_THRESHOLD_SECONDS) * 1000); }, (JSMPEG_URL_SIGN_EXPIRY_SECONDS - JSMPEG_URL_SIGN_REFRESH_THRESHOLD_SECONDS) * 1000);
} else {
dispatchErrorMessageEvent(this, localize('error.jsmpeg_no_sign'));
}
} }
/** /**
+10
View File
@@ -54,6 +54,7 @@ import { CameraManager } from '../../camera-manager/manager.js';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js'; import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
import { HassEntity } from 'home-assistant-js-websocket'; import { HassEntity } from 'home-assistant-js-websocket';
import { CameraEndpoints } from '../../camera-manager/types.js';
/** /**
* Get the state object or dispatch an error. Used in `ha` and `image` live * Get the state object or dispatch an error. Used in `ha` and `image` live
@@ -514,6 +515,10 @@ export class FrigateCardLiveCarousel extends LitElement {
<frigate-card-live-provider <frigate-card-live-provider
?disabled=${this.liveConfig.lazy_load} ?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig} .cameraConfig=${cameraConfig}
.cameraEndpoints=${guard(
[this.cameraManager],
() => this.cameraManager?.getCameraEndpoints(cameraID),
)}
.label=${cameraMetadata?.title ?? ''} .label=${cameraMetadata?.title ?? ''}
.liveConfig=${config} .liveConfig=${config}
.hass=${this.hass} .hass=${this.hass}
@@ -666,6 +671,9 @@ export class FrigateCardLiveProvider extends LitElement {
@property({ attribute: false }) @property({ attribute: false })
public cameraConfig?: CameraConfig; public cameraConfig?: CameraConfig;
@property({ attribute: false })
public cameraEndpoints?: CameraEndpoints;
@property({ attribute: false }) @property({ attribute: false })
public liveConfig?: LiveConfig; public liveConfig?: LiveConfig;
@@ -858,6 +866,7 @@ export class FrigateCardLiveProvider extends LitElement {
class=${classMap(providerClasses)} class=${classMap(providerClasses)}
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.cameraEndpoints=${this.cameraEndpoints}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)} @frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
> >
</frigate-card-live-webrtc-card>` </frigate-card-live-webrtc-card>`
@@ -878,6 +887,7 @@ export class FrigateCardLiveProvider extends LitElement {
class=${classMap(providerClasses)} class=${classMap(providerClasses)}
.hass=${this.hass} .hass=${this.hass}
.cameraConfig=${this.cameraConfig} .cameraConfig=${this.cameraConfig}
.cameraEndpoints=${this.cameraEndpoints}
.jsmpegConfig=${this.liveConfig.jsmpeg} .jsmpegConfig=${this.liveConfig.jsmpeg}
.cardWideConfig=${this.cardWideConfig} .cardWideConfig=${this.cardWideConfig}
@frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)} @frigate-card:media:loaded=${this._videoMediaShowHandler.bind(this)}
+1 -1
View File
@@ -353,7 +353,7 @@
"invalid_elements_config": "Invalid picture elements configuration", "invalid_elements_config": "Invalid picture elements configuration",
"invalid_response": "Received invalid response from Home Assistant for request", "invalid_response": "Received invalid response from Home Assistant for request",
"jsmpeg_no_player": "Could not start JSMPEG player", "jsmpeg_no_player": "Could not start JSMPEG player",
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path", "live_camera_no_endpoint": "Could not get camera endpoint for this live provider (incomplete configuration?)",
"live_camera_not_found": "The configured camera_entity was not found", "live_camera_not_found": "The configured camera_entity was not found",
"live_camera_unavailable": "Camera unavailable", "live_camera_unavailable": "Camera unavailable",
"no_camera_engine": "Could not determine suitable engine for camera", "no_camera_engine": "Could not determine suitable engine for camera",
+1 -1
View File
@@ -342,7 +342,7 @@
"invalid_elements_config": "Configurazione degli elementi di immagine non valida", "invalid_elements_config": "Configurazione degli elementi di immagine non valida",
"invalid_response": "Ricevuta una risposta non valida da Home Assistant per la richiesta", "invalid_response": "Ricevuta una risposta non valida da Home Assistant per la richiesta",
"jsmpeg_no_player": "Impossibile avviare JSMPEG Player", "jsmpeg_no_player": "Impossibile avviare JSMPEG Player",
"jsmpeg_no_sign": "Impossibile recuperare o firmare il percorso WebSocket JSMPEG", "live_camera_no_endpoint": "",
"live_camera_not_found": "La telecamera configurata non è stata trovata", "live_camera_not_found": "La telecamera configurata non è stata trovata",
"live_camera_unavailable": "Telecamera non disponibile", "live_camera_unavailable": "Telecamera non disponibile",
"no_camera_engine": "", "no_camera_engine": "",
+1 -1
View File
@@ -342,7 +342,7 @@
"invalid_elements_config": "Configuração de elementos de imagem inválida", "invalid_elements_config": "Configuração de elementos de imagem inválida",
"invalid_response": "Resposta inválida recebida do Home Assistant para a solicitação", "invalid_response": "Resposta inválida recebida do Home Assistant para a solicitação",
"jsmpeg_no_player": "Não foi possível iniciar o player JSMPEG", "jsmpeg_no_player": "Não foi possível iniciar o player JSMPEG",
"jsmpeg_no_sign": "Não foi possível recuperar ou assinar o caminho do websocket JSMPEG", "live_camera_no_endpoint": "",
"live_camera_not_found": "", "live_camera_not_found": "",
"live_camera_unavailable": "", "live_camera_unavailable": "",
"no_camera_engine": "", "no_camera_engine": "",
+33
View File
@@ -0,0 +1,33 @@
import { CameraEndpoint } from '../camera-manager/types';
import { dispatchErrorMessageEvent } from '../components/message';
import { localize } from '../localize/localize';
import { ExtendedHomeAssistant } from '../types';
import { errorToConsole } from './basic';
import { homeAssistantSignPath } from './ha';
export const getEndpointAddressOrDispatchError = async (
element: HTMLElement,
hass: ExtendedHomeAssistant,
endpoint: CameraEndpoint,
expires?: number,
): Promise<string | null> => {
let address: string | null;
if (!endpoint.sign) {
address = endpoint.endpoint;
} else {
let response: string | null | undefined;
try {
response = await homeAssistantSignPath(hass, endpoint.endpoint, expires);
} catch (e) {
errorToConsole(e as Error);
return null;
}
address = response ? response.replace(/^http/i, 'ws') : null;
}
if (!address) {
dispatchErrorMessageEvent(element, localize('error.failed_sign'));
return null;
}
return address;
};