@@ -17,7 +17,6 @@ import { MediaActionsController } from '../../components-lib/media-actions-contr
|
||||
import { MediaHeightController } from '../../components-lib/media-height-controller.js';
|
||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||
import { CameraConfig } from '../../config/schema/cameras.js';
|
||||
import { TransitionEffect } from '../../config/schema/common/transition-effect.js';
|
||||
import { LiveConfig } from '../../config/schema/live.js';
|
||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||
@@ -170,18 +169,8 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
const slides: TemplateResult[] = [];
|
||||
const cameraToSlide: Record<string, number> = {};
|
||||
|
||||
for (const [cameraID, cameraConfig] of this.cameraManager
|
||||
.getStore()
|
||||
.getCameraConfigEntries(cameraIDs)) {
|
||||
const liveCameraID = this._getSubstreamCameraID(cameraID, view);
|
||||
const liveCameraConfig =
|
||||
cameraID === liveCameraID
|
||||
? cameraConfig
|
||||
: this.cameraManager?.getStore().getCameraConfig(liveCameraID);
|
||||
|
||||
const slide = liveCameraConfig
|
||||
? this._renderLive(liveCameraID, liveCameraConfig)
|
||||
: null;
|
||||
for (const cameraID of cameraIDs ?? []) {
|
||||
const slide = this._renderLive(this._getSubstreamCameraID(cameraID, view));
|
||||
if (slide) {
|
||||
cameraToSlide[cameraID] = slides.length;
|
||||
slides.push(slide);
|
||||
@@ -207,11 +196,9 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderLive(
|
||||
cameraID: string,
|
||||
cameraConfig: CameraConfig,
|
||||
): TemplateResult | void {
|
||||
if (!this.liveConfig || !this.hass || !this.cameraManager) {
|
||||
protected _renderLive(cameraID: string): TemplateResult | void {
|
||||
const camera = this.cameraManager?.getStore().getCamera(cameraID);
|
||||
if (!this.liveConfig || !this.hass || !this.cameraManager || !camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,7 +211,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
.microphoneState=${view?.camera === cameraID
|
||||
? this.microphoneState
|
||||
: undefined}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.camera=${camera}
|
||||
.cameraEndpoints=${guard(
|
||||
[this.cameraManager, cameraID],
|
||||
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
||||
|
||||
@@ -10,12 +10,13 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { Camera } from '../../camera-manager/camera.js';
|
||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||
import { MicrophoneState } from '../../card-controller/types.js';
|
||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||
import { dispatchLiveErrorEvent } from '../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { PartialZoomSettings } from '../../components-lib/zoom/types.js';
|
||||
import { CameraConfig, LiveProvider } from '../../config/schema/cameras.js';
|
||||
import { LiveProvider } from '../../config/schema/cameras.js';
|
||||
import { LiveConfig } from '../../config/schema/live.js';
|
||||
import { CardWideConfig, configDefaults } from '../../config/schema/types.js';
|
||||
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
||||
@@ -39,7 +40,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
public camera?: Camera;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraEndpoints?: CameraEndpoints;
|
||||
@@ -105,20 +106,18 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
* @returns A live provider (that is not 'auto').
|
||||
*/
|
||||
protected _getResolvedProvider(): Omit<LiveProvider, 'auto'> {
|
||||
if (this.cameraConfig?.live_provider === 'auto') {
|
||||
if (
|
||||
this.cameraConfig?.webrtc_card?.entity ||
|
||||
this.cameraConfig?.webrtc_card?.url
|
||||
) {
|
||||
const config = this.camera?.getConfig();
|
||||
if (config?.live_provider === 'auto') {
|
||||
if (config?.webrtc_card?.entity || config?.webrtc_card?.url) {
|
||||
return 'webrtc-card';
|
||||
} else if (this.cameraConfig?.camera_entity) {
|
||||
} else if (config?.camera_entity) {
|
||||
return 'ha';
|
||||
} else if (this.cameraConfig?.frigate.camera_name) {
|
||||
} else if (config?.frigate.camera_name) {
|
||||
return 'jsmpeg';
|
||||
}
|
||||
return configDefaults.cameras.live_provider;
|
||||
}
|
||||
return this.cameraConfig?.live_provider || 'image';
|
||||
return config?.live_provider || 'image';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +128,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
protected _shouldShowImageDuringLoading(): boolean {
|
||||
return (
|
||||
!this._isVideoMediaLoaded &&
|
||||
!!this.cameraConfig?.camera_entity &&
|
||||
!!this.camera?.getConfig()?.camera_entity &&
|
||||
!!this.hass &&
|
||||
!!this.liveConfig?.show_image_during_load &&
|
||||
!this._showStreamTroubleshooting &&
|
||||
@@ -172,7 +171,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has('cameraConfig')) {
|
||||
if (changedProps.has('camera')) {
|
||||
const provider = this._getResolvedProvider();
|
||||
if (provider === 'jsmpeg') {
|
||||
this._importPromises.push(import('./providers/jsmpeg.js'));
|
||||
@@ -198,8 +197,9 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
}
|
||||
|
||||
protected _renderContainer(template: TemplateResult): TemplateResult {
|
||||
const config = this.camera?.getConfig();
|
||||
const intermediateTemplate = html` <advanced-camera-card-media-dimensions-container
|
||||
.dimensionsConfig=${this.cameraConfig?.dimensions}
|
||||
.dimensionsConfig=${config?.dimensions}
|
||||
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||
if (ev.detail.placeholder) {
|
||||
ev.stopPropagation();
|
||||
@@ -213,11 +213,11 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
|
||||
return html` ${this.liveConfig?.zoomable
|
||||
? html` <advanced-camera-card-zoomer
|
||||
.defaultSettings=${guard([this.cameraConfig?.dimensions?.layout], () =>
|
||||
this.cameraConfig?.dimensions?.layout
|
||||
.defaultSettings=${guard([config?.dimensions?.layout], () =>
|
||||
config?.dimensions?.layout
|
||||
? {
|
||||
pan: this.cameraConfig.dimensions.layout.pan,
|
||||
zoom: this.cameraConfig.dimensions.layout.zoom,
|
||||
pan: config.dimensions.layout.pan,
|
||||
zoom: config.dimensions.layout.zoom,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
@@ -233,11 +233,13 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const cameraConfig = this.camera?.getConfig();
|
||||
if (
|
||||
!this._lazyLoadController?.isLoaded() ||
|
||||
!this.hass ||
|
||||
!this.liveConfig ||
|
||||
!this.cameraConfig
|
||||
!this.camera ||
|
||||
!cameraConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -251,27 +253,26 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
if (
|
||||
provider === 'ha' ||
|
||||
provider === 'image' ||
|
||||
(this.cameraConfig?.camera_entity &&
|
||||
this.cameraConfig.always_error_if_entity_unavailable)
|
||||
(cameraConfig?.camera_entity && cameraConfig.always_error_if_entity_unavailable)
|
||||
) {
|
||||
if (!this.cameraConfig?.camera_entity) {
|
||||
if (!cameraConfig?.camera_entity) {
|
||||
dispatchLiveErrorEvent(this);
|
||||
return renderMessage({
|
||||
message: localize('error.no_live_camera'),
|
||||
type: 'error',
|
||||
icon: 'mdi:camera',
|
||||
context: this.cameraConfig,
|
||||
context: cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this.cameraConfig.camera_entity];
|
||||
const stateObj = this.hass.states[cameraConfig.camera_entity];
|
||||
if (!stateObj) {
|
||||
dispatchLiveErrorEvent(this);
|
||||
return renderMessage({
|
||||
message: localize('error.live_camera_not_found'),
|
||||
type: 'error',
|
||||
icon: 'mdi:camera',
|
||||
context: this.cameraConfig,
|
||||
context: cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -301,7 +302,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
? html` <advanced-camera-card-live-image
|
||||
${ref(this._refProvider)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraConfig=${cameraConfig}
|
||||
class=${classMap({
|
||||
...classes,
|
||||
// The image provider is providing the temporary loading image,
|
||||
@@ -320,7 +321,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(classes)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraConfig=${cameraConfig}
|
||||
?controls=${this.liveConfig.controls.builtin}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
>
|
||||
@@ -330,7 +331,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(classes)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.camera=${this.camera}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.microphoneState=${this.microphoneState}
|
||||
.microphoneConfig=${this.liveConfig.microphone}
|
||||
@@ -343,7 +344,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(classes)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
?controls=${this.liveConfig.controls.builtin}
|
||||
@@ -355,7 +356,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
${ref(this._refProvider)}
|
||||
class=${classMap(classes)}
|
||||
.hass=${this.hass}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameraEndpoints=${this.cameraEndpoints}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
@advanced-camera-card:live:error=${() => this._providerErrorHandler()}
|
||||
|
||||
@@ -7,17 +7,23 @@ import {
|
||||
unsafeCSS,
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Camera } from '../../../../camera-manager/camera.js';
|
||||
import { CameraEndpoints } from '../../../../camera-manager/types.js';
|
||||
import { MicrophoneState } from '../../../../card-controller/types.js';
|
||||
import { dispatchLiveErrorEvent } from '../../../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { VideoMediaPlayerController } from '../../../../components-lib/media-player/video.js';
|
||||
import { CameraConfig } from '../../../../config/schema/cameras.js';
|
||||
import { MicrophoneConfig } from '../../../../config/schema/live.js';
|
||||
import { homeAssistantSignPath } from '../../../../ha/sign-path.js';
|
||||
import { HomeAssistant } from '../../../../ha/types.js';
|
||||
import {
|
||||
addDynamicProxyURL,
|
||||
getWebProxiedURL,
|
||||
shouldUseWebProxy,
|
||||
} from '../../../../ha/web-proxy.js';
|
||||
import { localize } from '../../../../localize/localize.js';
|
||||
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||
import { MediaPlayer, MediaPlayerController, Message } from '../../../../types.js';
|
||||
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
||||
import { errorToConsole } from '../../../../utils/basic.js';
|
||||
import { renderMessage } from '../../../message.js';
|
||||
import { VideoRTC } from './video-rtc.js';
|
||||
|
||||
@@ -36,7 +42,7 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
public camera?: Camera;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraEndpoints?: CameraEndpoints;
|
||||
@@ -79,46 +85,102 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected async _createPlayer(): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
protected _handleError(message: Message, e?: Error): void {
|
||||
if (e) {
|
||||
errorToConsole(e as Error);
|
||||
}
|
||||
|
||||
this._message = {
|
||||
type: 'error',
|
||||
...message,
|
||||
};
|
||||
dispatchLiveErrorEvent(this);
|
||||
return;
|
||||
}
|
||||
|
||||
protected async _getPlayerSource(): Promise<string | null> {
|
||||
const cameraConfig = this.camera?.getConfig();
|
||||
if (!this.hass || !cameraConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const endpoint = this.cameraEndpoints?.go2rtc;
|
||||
if (!endpoint) {
|
||||
this._message = {
|
||||
type: 'error',
|
||||
this._handleError({
|
||||
message: localize('error.live_camera_no_endpoint'),
|
||||
context: this.cameraConfig,
|
||||
};
|
||||
dispatchLiveErrorEvent(this);
|
||||
return;
|
||||
context: cameraConfig,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const address = await convertEndpointAddressToSignedWebsocket(
|
||||
this.hass,
|
||||
endpoint,
|
||||
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
||||
);
|
||||
if (!address) {
|
||||
this._message = {
|
||||
type: 'error',
|
||||
message: localize('error.failed_sign'),
|
||||
context: this.cameraConfig,
|
||||
};
|
||||
dispatchLiveErrorEvent(this);
|
||||
const proxyConfig = this.camera?.getProxyConfig();
|
||||
let src: string | null = endpoint.endpoint;
|
||||
let sign: boolean = endpoint.sign ?? false;
|
||||
|
||||
if (proxyConfig && shouldUseWebProxy(this.hass, proxyConfig, 'live')) {
|
||||
if (proxyConfig.dynamic) {
|
||||
try {
|
||||
await addDynamicProxyURL(this.hass, endpoint.endpoint, {
|
||||
proxyConfig,
|
||||
ttl: GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
||||
|
||||
// The link may need to be opened multiple times.
|
||||
openLimit: 0,
|
||||
});
|
||||
} catch (e) {
|
||||
this._handleError(
|
||||
{
|
||||
message: localize('error.failed_proxy'),
|
||||
context: cameraConfig,
|
||||
},
|
||||
e as Error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
src = getWebProxiedURL(endpoint.endpoint, { websocket: true });
|
||||
sign = true;
|
||||
}
|
||||
|
||||
if (src && sign) {
|
||||
try {
|
||||
src = await homeAssistantSignPath(
|
||||
this.hass,
|
||||
src,
|
||||
GO2RTC_URL_SIGN_EXPIRY_SECONDS,
|
||||
);
|
||||
} catch (e) {
|
||||
this._handleError(
|
||||
{
|
||||
message: localize('error.failed_sign'),
|
||||
context: cameraConfig,
|
||||
},
|
||||
e as Error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return src;
|
||||
}
|
||||
|
||||
protected async _createPlayer(): Promise<void> {
|
||||
const src = await this._getPlayerSource();
|
||||
if (!src) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._player = new VideoRTC();
|
||||
this._player.mediaPlayerController = this._mediaPlayerController;
|
||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||
this._player.src = address;
|
||||
this._player.src = src;
|
||||
this._player.visibilityCheck = false;
|
||||
this._player.setControls(this.controls);
|
||||
|
||||
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
|
||||
this._player.mode = this.cameraConfig.go2rtc.modes.join(',');
|
||||
const cameraConfig = this.camera?.getConfig();
|
||||
if (cameraConfig?.go2rtc?.modes && cameraConfig.go2rtc.modes.length) {
|
||||
this._player.mode = cameraConfig.go2rtc.modes.join(',');
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
|
||||
@@ -14,11 +14,12 @@ import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispa
|
||||
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
|
||||
import { CameraConfig } from '../../../config/schema/cameras.js';
|
||||
import { CardWideConfig } from '../../../config/schema/types.js';
|
||||
import { homeAssistantSignPath } from '../../../ha/sign-path.js';
|
||||
import { HomeAssistant } from '../../../ha/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
||||
import { MediaPlayer, MediaPlayerController, Message } from '../../../types.js';
|
||||
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
||||
import { convertHTTPAdressToWebsocket, errorToConsole } from '../../../utils/basic.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
@@ -183,11 +184,18 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
return;
|
||||
}
|
||||
|
||||
const address = await convertEndpointAddressToSignedWebsocket(
|
||||
this.hass,
|
||||
endpoint,
|
||||
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
|
||||
);
|
||||
let response: string | null | undefined;
|
||||
try {
|
||||
response = await homeAssistantSignPath(
|
||||
this.hass,
|
||||
endpoint.endpoint,
|
||||
JSMPEG_URL_SIGN_EXPIRY_SECONDS,
|
||||
);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
}
|
||||
const address = response ? convertHTTPAdressToWebsocket(response) : null;
|
||||
|
||||
if (!address) {
|
||||
this._message = {
|
||||
type: 'error',
|
||||
|
||||
@@ -158,13 +158,17 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
// Don't use URL() parsing, since that will strip the port number if
|
||||
// it's the default, just need to strip any hash part of the URL.
|
||||
const urlWithoutQSorHash = unsignedURL.split(/#/)[0];
|
||||
await addDynamicProxyURL(this.hass, urlWithoutQSorHash, {
|
||||
sslVerification: proxyConfig.ssl_verification,
|
||||
sslCiphers: proxyConfig.ssl_ciphers,
|
||||
|
||||
// The link may need to be opened multiple times.
|
||||
openLimit: 0,
|
||||
});
|
||||
try {
|
||||
await addDynamicProxyURL(this.hass, urlWithoutQSorHash, {
|
||||
proxyConfig,
|
||||
|
||||
// The link may need to be opened multiple times.
|
||||
openLimit: 0,
|
||||
});
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user