Show still images during live camera load.

This commit is contained in:
Dermot Duffy
2022-08-07 16:04:25 -07:00
parent d7bc6d009a
commit 88b9c7427a
11 changed files with 120 additions and 26 deletions
+14
View File
@@ -15,3 +15,17 @@ reviews:
source: en
text: Câmera indisponível
time: '2022-07-24T05:34:22.088Z'
config.live.show_image_during_load:
locales:
it:
translation_candidate:
source: en
text: Mostra l'immagine fissa durante il caricamento del live streaming
time: '2022-08-07T23:00:31.001Z'
pt-BR:
translation_candidate:
source: en
text: >-
Mostrar imagem estática enquanto a transmissão ao vivo está
carregando
time: '2022-08-07T23:00:35.117Z'
+5
View File
@@ -327,6 +327,7 @@ See the [fully expanded live configuration example](#config-expanded-live) for h
| `lazy_unload` | `never` | :heavy_multiplication_x: | When to lazily **un**load lazyily-loaded cameras. `never` will never lazily-unload, `unselected` will lazy-unload a camera when it is unselected in the carousel, `hidden` will lazy-unload all cameras when the browser/tab becomes hidden or `all` on any opportunity to lazily unload (i.e. either case). This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. This option has no effect if `lazy_load` is false. Some live providers (e.g. `webrtc-card`) implement their own lazy unloading independently which may occur regardless of the value of this setting.|
| `draggable` | `true` | :heavy_multiplication_x: | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. |
| `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between live cameras. Accepted values: `slide` or `none`. |
| `show_image_during_load` | `true` | :white_check_mark: | If `true`, during the initial stream load, a still image will be shown instead of the loading video stream. This still image will auto-refresh every 1 second and will be replaced with the live stream once loaded. |
| `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.|
| `controls` | | :white_check_mark: | Configuration for the `live` view controls. See below. |
| `jsmpeg` | | :white_check_mark: | Configuration for the `frigate-jsmpeg` live provider. See below.|
@@ -2704,6 +2705,10 @@ You must be using a version of the [Frigate integration](https://github.com/blak
Even if `live.auto_play` or `media_viewer.auto_play` is set to `never`, Chrome itself will still auto play a video that was previously playing prior to the tab being hidden, once that tab is visible again. This behavior cannot be influenced by the card. Other browsers (e.g. Firefox, Safari) do not exhibit this behavior.
### The `live` view just shows a blank white image during loading
For some slowly loading cameras, for which [Home Assistant stream preloading](https://www.home-assistant.io/integrations/camera/) is not enabled, Home Assistant may return a blank white image when asked for a still. These stills are used during initial Frigate card load of the `live` view if the `live.show_image_during_load` option is enabled. Disabling this option should show the default media loading controls (e.g. a spinner or empty video player) instead of the blank white image.
<a name="jsmpeg-troubleshooting"></a>
### JSMPEG Live Camera Only Shows A 'spinner'
+2 -1
View File
@@ -1010,6 +1010,7 @@ export class FrigateCard extends LitElement {
view: this._view?.clone().mergeInContext(ev.detail),
});
}
/**
* Called before each update.
*/
@@ -1795,7 +1796,7 @@ export class FrigateCard extends LitElement {
@frigate-card:message=${this._messageHandler.bind(this)}
@frigate-card:view:change=${this._changeViewHandler.bind(this)}
@frigate-card:view:change-context=${this._addViewContextHandler.bind(this)}
@frigate-card:media-show=${this._mediaShowHandler}
@frigate-card:media-show=${this._mediaShowHandler.bind(this)}
@frigate-card:render=${() => this.requestUpdate()}
>
${renderMenuAbove ? this._renderMenu() : ''}
+79 -19
View File
@@ -17,17 +17,14 @@ import { guard } from 'lit/directives/guard.js';
import { keyed } from 'lit/directives/keyed.js';
import { until } from 'lit/directives/until.js';
import { ConditionState, getOverriddenConfig } from '../card-condition.js';
import {
dispatchFrigateCardErrorEvent,
dispatchMessageEvent,
renderProgressIndicator,
} from '../components/message.js';
import { dispatchMessageEvent, renderProgressIndicator } from '../components/message.js';
import { localize } from '../localize/localize.js';
import liveFrigateStyle from '../scss/live-frigate.scss';
import liveJSMPEGStyle from '../scss/live-jsmpeg.scss';
import liveWebRTCStyle from '../scss/live-webrtc.scss';
import liveStyle from '../scss/live.scss';
import liveCarouselStyle from '../scss/live-carousel.scss';
import liveProviderStyle from '../scss/live-provider.scss';
import {
CameraConfig,
ExtendedHomeAssistant,
@@ -44,10 +41,7 @@ import {
WebRTCCardConfig,
} from '../types.js';
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
import {
contentsChanged,
errorToConsole,
} from '../utils/basic.js';
import { contentsChanged, errorToConsole } from '../utils/basic.js';
import { getCameraIcon, getCameraTitle } from '../utils/camera.js';
import { homeAssistantSignPath } from '../utils/ha';
import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js';
@@ -69,6 +63,8 @@ import './surround-thumbnails';
import '../patches/ha-camera-stream';
import { EmblaCarouselPlugins } from './carousel.js';
import { renderTask } from '../utils/task.js';
import { classMap } from 'lit/directives/class-map.js';
import './image';
// Number of seconds a signed URL is valid for.
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
@@ -591,7 +587,7 @@ export class FrigateCardLiveCarousel extends LitElement {
@frigate-card:media-carousel:select=${this._setViewHandler.bind(this)}
@frigate-card:carousel:settle=${() => {
// Fetch the thumbnails after the carousel has settled.
dispatchViewContextChangeEvent(this, { thumbnails: { fetch: true }});
dispatchViewContextChangeEvent(this, { thumbnails: { fetch: true } });
}}
>
<frigate-card-next-previous-control
@@ -657,6 +653,9 @@ export class FrigateCardLiveProvider extends LitElement {
@property({ attribute: false })
public label = '';
@state()
protected _isVideoMediaLoaded = false;
protected _providerRef: Ref<Element & FrigateCardMediaPlayer> = createRef();
/**
@@ -694,7 +693,11 @@ export class FrigateCardLiveProvider extends LitElement {
this._providerRef.value?.seek(seconds);
}
protected _getResolvedProvider(): LiveProvider {
/**
* Get the fully resolved live provider.
* @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 ||
@@ -713,6 +716,35 @@ export class FrigateCardLiveProvider extends LitElement {
);
}
/**
* Determine if a camera image should be shown in lieu of the real stream
* whilst loading.
* @returns`true` if an image should be shown.
*/
protected _shouldShowImageDuringLoading(): boolean {
return (
!!this.cameraConfig?.camera_entity &&
!!this.hass &&
!!this.liveConfig?.show_image_during_load
);
}
/**
* Record that video media is being shown.
*/
protected _videoMediaShowHandler(): void {
this._isVideoMediaLoaded = true;
}
/**
* Called before each update.
*/
protected willUpdate(): void {
if (this.disabled) {
this._isVideoMediaLoaded = false;
}
}
/**
* Master render method.
* @returns A rendered template.
@@ -727,32 +759,60 @@ export class FrigateCardLiveProvider extends LitElement {
this.ariaLabel = this.label;
const provider = this._getResolvedProvider();
const showImage = !this._isVideoMediaLoaded && this._shouldShowImageDuringLoading();
const providerClasses = {
hidden: showImage,
};
return html`
${provider == 'ha'
? html` <frigate-card-live-ha
${ref(this._providerRef)}
${showImage
? html`<frigate-card-image
.imageConfig=${{
mode: 'camera' as const,
refresh_seconds: 1,
}}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
>
</frigate-card-image>`
: html``}
${provider === 'ha'
? html` <frigate-card-live-ha
${ref(this._providerRef)}
class=${classMap(providerClasses)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
@frigate-card:media-show=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-ha>`
: provider == 'webrtc-card'
: provider === 'webrtc-card'
? html`<frigate-card-live-webrtc-card
${ref(this._providerRef)}
class=${classMap(providerClasses)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.webRTCConfig=${this.liveConfig.webrtc_card}
@frigate-card:media-show=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-webrtc-card>`
: html` <frigate-card-live-jsmpeg
${ref(this._providerRef)}
class=${classMap(providerClasses)}
.hass=${this.hass}
.cameraConfig=${this.cameraConfig}
.jsmpegConfig=${this.liveConfig.jsmpeg}
@frigate-card:media-show=${this._videoMediaShowHandler.bind(this)}
>
</frigate-card-live-jsmpeg>`}
`;
}
/**
* Get styles.
*/
static get styles(): CSSResultGroup {
return unsafeCSS(liveProviderStyle);
}
}
@customElement('frigate-card-live-ha')
@@ -1006,11 +1066,11 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
this.updateComplete.then(() => {
const video = this._getPlayer();
if (video) {
const onloadedmetadata = video.onloadedmetadata;
const onloadeddata = video.onloadeddata;
video.onloadedmetadata = (e) => {
if (onloadedmetadata) {
onloadedmetadata.call(video, e);
video.onloadeddata = (e) => {
if (onloadeddata) {
onloadeddata.call(video, e);
}
dispatchMediaShowEvent(this, video);
};
+1
View File
@@ -119,6 +119,7 @@ export const CONF_LIVE_LAZY_LOAD = `${CONF_LIVE}.lazy_load` as const;
export const CONF_LIVE_LAZY_UNLOAD = `${CONF_LIVE}.lazy_unload` as const;
export const CONF_LIVE_PRELOAD = `${CONF_LIVE}.preload` as const;
export const CONF_LIVE_TRANSITION_EFFECT = `${CONF_LIVE}.transition_effect` as const;
export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD = `${CONF_LIVE}.show_image_during_load` as const;
export const CONF_LIVE_WEBRTC_CARD = `${CONF_LIVE}.webrtc_card` as const;
export const CONF_IMAGE = 'image' as const;
+11 -6
View File
@@ -9,7 +9,7 @@ import {
getConfigValue,
isConfigUpgradeable,
setConfigValue,
upgradeConfig
upgradeConfig,
} from './config-mgmt.js';
import {
CONF_CAMERAS,
@@ -57,6 +57,7 @@ import {
CONF_LIVE_LAZY_LOAD,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_PRELOAD,
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
CONF_LIVE_TRANSITION_EFFECT,
CONF_MEDIA_VIEWER_AUTO_MUTE,
CONF_MEDIA_VIEWER_AUTO_PAUSE,
@@ -99,7 +100,7 @@ import {
CONF_VIEW_TIMEOUT_SECONDS,
CONF_VIEW_UPDATE_CYCLE_CAMERA,
CONF_VIEW_UPDATE_FORCE,
CONF_VIEW_UPDATE_SECONDS
CONF_VIEW_UPDATE_SECONDS,
} from './const.js';
import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
@@ -110,7 +111,7 @@ import {
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
THUMBNAIL_WIDTH_MIN
THUMBNAIL_WIDTH_MIN,
} from './types.js';
import { arrayMove } from './utils/basic.js';
import { getCameraID, getCameraTitle } from './utils/camera.js';
@@ -1203,6 +1204,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
CONF_LIVE_TRANSITION_EFFECT,
this._transitionEffects,
)}
${this._renderSwitch(
CONF_LIVE_SHOW_IMAGE_DURING_LOAD,
defaults.live.show_image_during_load,
)}
${this._putInSubmenu(
MENU_LIVE_CONTROLS,
true,
@@ -1511,7 +1516,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
}
declare global {
interface HTMLElementTagNameMap {
"frigate-card-editor": FrigateCardEditor
}
interface HTMLElementTagNameMap {
'frigate-card-editor': FrigateCardEditor;
}
}
+1
View File
@@ -125,6 +125,7 @@
"lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded",
"preload": "Preload live view in the background",
"show_image_during_load": "Show still image while the live stream is loading",
"transition_effect": "Live camera transition effect"
},
"media_viewer": {
+1
View File
@@ -125,6 +125,7 @@
"lazy_load": "Le telecamere dal vivo sono pigramente cariche",
"lazy_unload": "Le telecamere dal vivo sono pigramente non caricate",
"preload": "Precarica Live View in background",
"show_image_during_load": "",
"transition_effect": "Effetto di transizione della telecamera dal vivo"
},
"media_viewer": {
+1
View File
@@ -125,6 +125,7 @@
"lazy_load": "As câmeras ao vivo são carregadas lentamente",
"lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente",
"preload": "Pré-carregar a visualização ao vivo em segundo plano",
"show_image_during_load": "",
"transition_effect": "Efeito de transição de câmera ao vivo"
},
"media_viewer": {
+3
View File
@@ -0,0 +1,3 @@
.hidden {
display: none;
}
+2
View File
@@ -642,6 +642,7 @@ const liveConfigDefault = {
lazy_unload: 'never' as const,
draggable: true,
transition_effect: 'slide' as const,
show_image_during_load: true,
controls: {
next_previous: {
size: 48,
@@ -739,6 +740,7 @@ const liveOverridableConfigSchema = z
.default(liveConfigDefault.controls.title),
})
.default(liveConfigDefault.controls),
show_image_during_load: z.boolean().default(liveConfigDefault.show_image_during_load),
})
.merge(actionsSchema);