fix: Lazy unloading should not leave dangling connections (#2004)
- Closes #1992
This commit is contained in:
@@ -97,7 +97,18 @@ export class AdvancedCameraCardCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (!this._carousel && this._refRoot.value && this._refParent.value) {
|
||||
if (
|
||||
!this._carousel &&
|
||||
this._refRoot.value &&
|
||||
this._refParent.value &&
|
||||
// Never construct a carousel if the node is not connected. There can be a
|
||||
// race condition between the Lit update lifecycle, and the
|
||||
// disconnect/connect callbacks, causing a carousel to potentially be
|
||||
// created after the node is disconnected. This could cause a dangling
|
||||
// carousel and hold open connections that should have been closed.
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1992
|
||||
this.isConnected
|
||||
) {
|
||||
this._carousel = new CarouselController(
|
||||
this._refRoot.value,
|
||||
this._refParent.value,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { HomeAssistant } from '../../ha/types.js';
|
||||
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||
import { getStreamCameraID } from '../../utils/substream.js';
|
||||
@@ -36,7 +35,6 @@ import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import { AdvancedCameraCardPTZ } from '../ptz.js';
|
||||
import './provider.js';
|
||||
import { AdvancedCameraCardLiveProvider } from './provider.js';
|
||||
|
||||
const ADVANCED_CAMERA_CARD_LIVE_PROVIDER = 'advanced-camera-card-live-provider';
|
||||
|
||||
@@ -140,19 +138,7 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _getPlugins(): EmblaCarouselPlugins {
|
||||
return [
|
||||
AutoLazyLoad({
|
||||
...(this.liveConfig?.lazy_load && {
|
||||
lazyLoadCallback: (index, slide) =>
|
||||
this._lazyloadOrUnloadSlide('load', index, slide),
|
||||
}),
|
||||
lazyUnloadConditions: this.liveConfig?.lazy_unload,
|
||||
lazyUnloadCallback: (index, slide) =>
|
||||
this._lazyloadOrUnloadSlide('unload', index, slide),
|
||||
}),
|
||||
AutoMediaLoadedInfo(),
|
||||
AutoSize(),
|
||||
];
|
||||
return [AutoMediaLoadedInfo(), AutoSize()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,23 +203,6 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected _lazyloadOrUnloadSlide(
|
||||
action: 'load' | 'unload',
|
||||
_index: number,
|
||||
slide: Element,
|
||||
): void {
|
||||
if (slide instanceof HTMLSlotElement) {
|
||||
slide = slide.assignedElements({ flatten: true })[0];
|
||||
}
|
||||
|
||||
const liveProvider = slide?.querySelector(
|
||||
ADVANCED_CAMERA_CARD_LIVE_PROVIDER,
|
||||
) as AdvancedCameraCardLiveProvider | null;
|
||||
if (liveProvider) {
|
||||
liveProvider.load = action === 'load';
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderLive(
|
||||
cameraID: string,
|
||||
cameraConfig: CameraConfig,
|
||||
@@ -248,7 +217,6 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
<advanced-camera-card-live-provider
|
||||
?load=${!this.liveConfig.lazy_load}
|
||||
.microphoneState=${view?.camera === cameraID
|
||||
? this.microphoneState
|
||||
: undefined}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { guard } from 'lit/directives/guard.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.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';
|
||||
@@ -45,12 +46,6 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
@property({ attribute: false })
|
||||
public liveConfig?: LiveConfig;
|
||||
|
||||
// Whether or not to load the video for this camera. If `false`, no contents
|
||||
// are rendered until this attribute is set to `true` (this is useful for lazy
|
||||
// loading).
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public load = false;
|
||||
|
||||
// Label that is used for ARIA support and as tooltip.
|
||||
@property({ attribute: false })
|
||||
public label = '';
|
||||
@@ -74,6 +69,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
protected _showStreamTroubleshooting = false;
|
||||
|
||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||
protected _lazyLoadController: LazyLoadController | null = null;
|
||||
|
||||
// A note on dynamic imports:
|
||||
//
|
||||
@@ -144,11 +140,23 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('load')) {
|
||||
if (!this.load) {
|
||||
this._isVideoMediaLoaded = false;
|
||||
dispatchMediaUnloadedEvent(this);
|
||||
}
|
||||
if (
|
||||
changedProps.has('liveConfig') ||
|
||||
(!this._lazyLoadController && this.liveConfig)
|
||||
) {
|
||||
this._lazyLoadController?.destroy();
|
||||
this._lazyLoadController?.removeController();
|
||||
this._lazyLoadController = new LazyLoadController(
|
||||
this,
|
||||
this.liveConfig?.lazy_load,
|
||||
this.liveConfig?.lazy_unload,
|
||||
);
|
||||
this._lazyLoadController.addListener((loaded: boolean) => {
|
||||
if (!loaded) {
|
||||
this._isVideoMediaLoaded = false;
|
||||
dispatchMediaUnloadedEvent(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changedProps.has('liveConfig')) {
|
||||
@@ -216,7 +224,12 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.load || !this.hass || !this.liveConfig || !this.cameraConfig) {
|
||||
if (
|
||||
!this._lazyLoadController?.isLoaded() ||
|
||||
!this.hass ||
|
||||
!this.liveConfig ||
|
||||
!this.cameraConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import { MediaLoadedInfo, MediaPlayerController } from '../../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { contentsChanged, setOrRemoveAttribute } from '../../utils/basic.js';
|
||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
||||
import AutoMediaLoadedInfo from '../../utils/embla/plugins/auto-media-loaded-info/auto-media-loaded-info.js';
|
||||
import AutoSize from '../../utils/embla/plugins/auto-size/auto-size.js';
|
||||
import { ResolvedMediaCache } from '../../utils/ha/resolved-media.js';
|
||||
@@ -36,7 +35,6 @@ import { renderMessage } from '../message.js';
|
||||
import '../next-prev-control.js';
|
||||
import '../ptz.js';
|
||||
import './provider.js';
|
||||
import { AdvancedCameraCardViewerProvider } from './provider.js';
|
||||
|
||||
interface MediaNeighbor {
|
||||
index: number;
|
||||
@@ -137,15 +135,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
* @returns A list of EmblaOptionsTypes.
|
||||
*/
|
||||
protected _getPlugins(): EmblaCarouselPlugins {
|
||||
return [
|
||||
AutoLazyLoad({
|
||||
...(this.viewerConfig?.lazy_load && {
|
||||
lazyLoadCallback: (_index, slide) => this._lazyloadSlide(slide),
|
||||
}),
|
||||
}),
|
||||
AutoMediaLoadedInfo(),
|
||||
AutoSize(),
|
||||
];
|
||||
return [AutoMediaLoadedInfo(), AutoSize()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,23 +201,6 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy load a slide.
|
||||
* @param slide The slide to lazy load.
|
||||
*/
|
||||
protected _lazyloadSlide(slide: Element): void {
|
||||
if (slide instanceof HTMLSlotElement) {
|
||||
slide = slide.assignedElements({ flatten: true })[0];
|
||||
}
|
||||
|
||||
const viewerProvider = slide?.querySelector(
|
||||
'advanced-camera-card-viewer-provider',
|
||||
) as AdvancedCameraCardViewerProvider | null;
|
||||
if (viewerProvider) {
|
||||
viewerProvider.load = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slides to include in the render.
|
||||
* @returns The slides to include in the render.
|
||||
@@ -450,7 +423,6 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.load=${!this.viewerConfig.lazy_load}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
></advanced-camera-card-viewer-provider>
|
||||
</div>`;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { guard } from 'lit/directives/guard.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
|
||||
import { LazyLoadController } from '../../components-lib/lazy-load-controller.js';
|
||||
import { ZoomSettingsObserved } from '../../components-lib/zoom/types.js';
|
||||
import { handleZoomSettingsObservedEvent } from '../../components-lib/zoom/zoom-view-context.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
@@ -61,12 +62,6 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
@property({ attribute: false })
|
||||
public resolvedMediaCache?: ResolvedMediaCache;
|
||||
|
||||
// Whether or not to load the viewer media. If `false`, no contents are
|
||||
// rendered until this attribute is set to `true` (this is useful for lazy
|
||||
// loading).
|
||||
@property({ attribute: false })
|
||||
public load = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
|
||||
@@ -74,6 +69,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||
protected _lazyLoadController: LazyLoadController | null = null;
|
||||
|
||||
@state()
|
||||
protected _url: string | null = null;
|
||||
@@ -127,7 +123,7 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
!this.media ||
|
||||
!mediaContentID ||
|
||||
!this.hass ||
|
||||
(this.viewerConfig?.lazy_load && !this.load)
|
||||
!this._lazyLoadController?.isLoaded()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -185,15 +181,25 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (
|
||||
changedProps.has('load') ||
|
||||
changedProps.has('viewerConfig') ||
|
||||
(!this._lazyLoadController && this.viewerConfig)
|
||||
) {
|
||||
this._lazyLoadController?.destroy();
|
||||
this._lazyLoadController?.removeController();
|
||||
this._lazyLoadController = new LazyLoadController(
|
||||
this,
|
||||
this.viewerConfig?.lazy_load,
|
||||
);
|
||||
this._lazyLoadController.addListener((loaded) => loaded && this._setURL());
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has('media') ||
|
||||
changedProps.has('viewerConfig') ||
|
||||
changedProps.has('resolvedMediaCache') ||
|
||||
changedProps.has('hass')
|
||||
) {
|
||||
this._setURL().then(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
this._setURL();
|
||||
}
|
||||
|
||||
if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) {
|
||||
@@ -246,7 +252,12 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.load || !this.media || !this.hass || !this.viewerConfig) {
|
||||
if (
|
||||
!this._lazyLoadController?.isLoaded() ||
|
||||
!this.media ||
|
||||
!this.hass ||
|
||||
!this.viewerConfig
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user