fix: Increase reliability of media auto actions (e.g. play) (#1934)
Refactors media player handling entirely to make the code more consistent and less boilerplate. Add testing for media player actions. - Closes #1921 - Closes #1916
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { ImageMediaPlayerController } from '../components-lib/media-player/image';
|
||||
import imagePlayerStyle from '../scss/image-player.scss';
|
||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types';
|
||||
import { dispatchMediaLoadedEvent } from '../utils/media-info';
|
||||
|
||||
/**
|
||||
* A simple media player to wrap a single static image.
|
||||
*/
|
||||
@customElement('advanced-camera-card-image-player')
|
||||
export class AdvancedCameraCardImagePlayer extends LitElement implements MediaPlayer {
|
||||
@property()
|
||||
public url?: string;
|
||||
|
||||
@property()
|
||||
public filetype?: string;
|
||||
|
||||
protected _refImage: Ref<MediaPlayerElement<HTMLImageElement>> = createRef();
|
||||
protected _mediaPlayerController = new ImageMediaPlayerController(
|
||||
this,
|
||||
() => this._refImage.value ?? null,
|
||||
);
|
||||
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
return this._mediaPlayerController;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`<img
|
||||
${ref(this._refImage)}
|
||||
src="${ifDefined(this.url)}"
|
||||
@load=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev, {
|
||||
...(this._mediaPlayerController && {
|
||||
mediaPlayerController: this._mediaPlayerController,
|
||||
}),
|
||||
technology: [this.filetype ?? 'jpg'],
|
||||
});
|
||||
}}
|
||||
/>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(imagePlayerStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-image-player': AdvancedCameraCardImagePlayer;
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,15 @@ import isEqual from 'lodash-es/isEqual';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { getCameraEntityFromConfig } from '../camera-manager/utils/camera-entity-from-config.js';
|
||||
import { CachedValueController } from '../components-lib/cached-value-controller.js';
|
||||
import { UpdatingImageMediaPlayerController } from '../components-lib/media-player/updating-image.js';
|
||||
import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
|
||||
import defaultImage from '../images/iris-screensaver.jpg';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import imageStyle from '../scss/image.scss';
|
||||
import imageUpdatingPlayerStyle from '../scss/image-updating-player.scss';
|
||||
import {
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
MediaLoadedInfo,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
Message,
|
||||
} from '../types.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
@@ -60,10 +61,13 @@ export const resolveImageMode = (options?: {
|
||||
return 'screensaver';
|
||||
};
|
||||
|
||||
@customElement('advanced-camera-card-image-base')
|
||||
export class AdvancedCameraCardImageBase
|
||||
/**
|
||||
* A media player to wrap a image that updates continuously.
|
||||
*/
|
||||
@customElement('advanced-camera-card-image-updating-player')
|
||||
export class AdvancedCameraCardImageUpdatingPlayer
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
implements MediaPlayer
|
||||
{
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
@@ -93,46 +97,14 @@ export class AdvancedCameraCardImageBase
|
||||
|
||||
protected _mediaLoadedInfo: MediaLoadedInfo | null = null;
|
||||
|
||||
public async play(): Promise<void> {
|
||||
this._cachedValueController?.startTimer();
|
||||
}
|
||||
protected _mediaPlayerController = new UpdatingImageMediaPlayerController(
|
||||
this,
|
||||
() => this._refImage.value ?? null,
|
||||
() => this._cachedValueController ?? null,
|
||||
);
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
this._cachedValueController?.stopTimer();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async seek(_seconds: number): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async setControls(_controls: boolean): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return !this._cachedValueController?.hasTimer();
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
return this._cachedValueController?.value ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refImage.value ?? null;
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
return this._mediaPlayerController;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,7 +338,7 @@ export class AdvancedCameraCardImageBase
|
||||
src=${live(src)}
|
||||
@load=${(ev: Event) => {
|
||||
const mediaLoadedInfo = createMediaLoadedInfo(ev, {
|
||||
player: this,
|
||||
mediaPlayerController: this._mediaPlayerController,
|
||||
capabilities: {
|
||||
supportsPause: !!this.imageConfig?.refresh_seconds,
|
||||
},
|
||||
@@ -406,12 +378,12 @@ export class AdvancedCameraCardImageBase
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(imageStyle);
|
||||
return unsafeCSS(imageUpdatingPlayerStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-image-base': AdvancedCameraCardImageBase;
|
||||
'advanced-camera-card-image-updating-player': AdvancedCameraCardImageUpdatingPlayer;
|
||||
}
|
||||
}
|
||||
+12
-50
@@ -16,19 +16,16 @@ import { ZoomSettingsObserved } from '../components-lib/zoom/types';
|
||||
import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-view-context';
|
||||
import { CameraConfig, ImageViewConfig } from '../config/types';
|
||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../types.js';
|
||||
import imageStyle from '../scss/image.scss';
|
||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
||||
import { aspectRatioToString } from '../utils/basic';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||
import './image-base';
|
||||
import { resolveImageMode } from './image-base';
|
||||
import './image-updating-player';
|
||||
import { resolveImageMode } from './image-updating-player';
|
||||
import './zoomer.js';
|
||||
|
||||
@customElement('advanced-camera-card-image')
|
||||
export class AdvancedCameraCardImage
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@@ -44,47 +41,12 @@ export class AdvancedCameraCardImage
|
||||
@property({ attribute: false })
|
||||
public imageConfig?: ImageViewConfig;
|
||||
|
||||
protected _refImage: Ref<Element & AdvancedCameraCardMediaPlayer> = createRef();
|
||||
|
||||
public async play(): Promise<void> {
|
||||
await this._refImage.value?.play();
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
await this.updateComplete;
|
||||
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
await this._refImage.value?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
await this._refImage.value?.mute();
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
await this._refImage.value?.unmute();
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return !!this._refImage.value?.isMuted();
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
await this._refImage.value?.seek(seconds);
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
await this._refImage.value?.setControls(controls);
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._refImage.value?.isPaused() ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refImage.value?.getFullscreenElement() ?? null;
|
||||
}
|
||||
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
||||
@@ -147,19 +109,19 @@ export class AdvancedCameraCardImage
|
||||
}
|
||||
|
||||
return this._useZoomIfRequired(html`
|
||||
<advanced-camera-card-image-base
|
||||
<advanced-camera-card-image-updating-player
|
||||
${ref(this._refImage)}
|
||||
.hass=${this.hass}
|
||||
.view=${this.viewManagerEpoch?.manager.getView()}
|
||||
.imageConfig=${this.imageConfig}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
>
|
||||
</advanced-camera-card-image-base>
|
||||
</advanced-camera-card-image-updating-player>
|
||||
`);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(basicBlockStyle);
|
||||
return unsafeCSS(imageStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -429,16 +429,14 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
public updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
let initialized = false;
|
||||
if (!this._mediaActionsController.hasRoot() && this._refCarousel.value) {
|
||||
this._mediaActionsController.initialize(this._refCarousel.value);
|
||||
initialized = true;
|
||||
}
|
||||
const rootChanged = this._refCarousel.value
|
||||
? this._mediaActionsController.setRoot(this._refCarousel.value)
|
||||
: false;
|
||||
|
||||
// If the view has changed, or if the media actions controller has just been
|
||||
// initialized, then call the necessary media action.
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1626
|
||||
if (initialized || changedProperties.has('viewManagerEpoch')) {
|
||||
if (rootChanged || changedProperties.has('viewManagerEpoch')) {
|
||||
this._setMediaTarget();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,14 @@ import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||
import {
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
ExtendedHomeAssistant,
|
||||
FullscreenElement,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
} from '../../types.js';
|
||||
import { aspectRatioToString } from '../../utils/basic.js';
|
||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
import { playMediaMutingIfNecessary } from '../../utils/media.js';
|
||||
import '../icon.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import '../next-prev-control.js';
|
||||
@@ -40,10 +40,7 @@ import '../ptz.js';
|
||||
import '../surround.js';
|
||||
|
||||
@customElement('advanced-camera-card-live-provider')
|
||||
export class AdvancedCameraCardLiveProvider
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardLiveProvider extends LitElement implements MediaPlayer {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@@ -84,7 +81,7 @@ export class AdvancedCameraCardLiveProvider
|
||||
@state()
|
||||
protected _showStreamTroubleshooting = false;
|
||||
|
||||
protected _refProvider: Ref<LitElement & AdvancedCameraCardMediaPlayer> = createRef();
|
||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
// A note on dynamic imports:
|
||||
//
|
||||
@@ -98,58 +95,9 @@ export class AdvancedCameraCardLiveProvider
|
||||
// background. These calls fail without waiting for loading here.
|
||||
protected _importPromises: Promise<unknown>[] = [];
|
||||
|
||||
public async play(): Promise<void> {
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await playMediaMutingIfNecessary(this, this._refProvider.value);
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await this._refProvider.value?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await this._refProvider.value?.mute();
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await this._refProvider.value?.unmute();
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return this._refProvider.value?.isMuted() ?? true;
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await this._refProvider.value?.seek(seconds);
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
await this._refProvider.value?.setControls(controls);
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._refProvider.value?.isPaused() ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
await this.updateComplete;
|
||||
await this._refProvider.value?.updateComplete;
|
||||
return (await this._refProvider.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refProvider.value?.getFullscreenElement() ?? null;
|
||||
return (await this._refProvider.value?.getMediaPlayerController()) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,8 +213,10 @@ export class AdvancedCameraCardLiveProvider
|
||||
: undefined,
|
||||
)}
|
||||
.settings=${this.zoomSettings}
|
||||
@advanced-camera-card:zoom:zoomed=${() => this.setControls(false)}
|
||||
@advanced-camera-card:zoom:unzoomed=${() => this.setControls()}
|
||||
@advanced-camera-card:zoom:zoomed=${async () =>
|
||||
(await this.getMediaPlayerController())?.setControls(false)}
|
||||
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||
(await this.getMediaPlayerController())?.setControls()}
|
||||
>
|
||||
${template}
|
||||
</advanced-camera-card-zoomer>`
|
||||
|
||||
@@ -10,18 +10,17 @@ import { customElement, property, state } from 'lit/decorators.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, MicrophoneConfig } from '../../../../config/types.js';
|
||||
import { localize } from '../../../../localize/localize.js';
|
||||
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||
import {
|
||||
ExtendedHomeAssistant,
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
Message,
|
||||
} from '../../../../types.js';
|
||||
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
||||
import { setControlsOnVideo } from '../../../../utils/media.js';
|
||||
import { screenshotMedia } from '../../../../utils/screenshot.js';
|
||||
import { renderMessage } from '../../../message.js';
|
||||
import { VideoRTC } from './video-rtc.js';
|
||||
|
||||
@@ -35,10 +34,7 @@ customElements.define('advanced-camera-card-live-go2rtc-player', VideoRTC);
|
||||
const GO2RTC_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||
|
||||
@customElement('advanced-camera-card-live-go2rtc')
|
||||
export class AdvancedCameraCardGo2RTC
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer {
|
||||
// Not an reactive property to avoid resetting the video.
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@@ -62,52 +58,14 @@ export class AdvancedCameraCardGo2RTC
|
||||
|
||||
protected _player?: VideoRTC;
|
||||
|
||||
public async play(): Promise<void> {
|
||||
return this._player?.video?.play();
|
||||
}
|
||||
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||
this,
|
||||
() => this._player?.video ?? null,
|
||||
() => this.controls,
|
||||
);
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
this._player?.video?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return this._player?.video?.muted ?? true;
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
this._player.video.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
if (this._player?.video) {
|
||||
setControlsOnVideo(this._player.video, controls ?? this.controls);
|
||||
}
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._player?.video?.paused ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
return this._player?.video ? screenshotMedia(this._player.video) : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._player?.video ?? null;
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
return this._mediaPlayerController;
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
@@ -155,7 +113,7 @@ export class AdvancedCameraCardGo2RTC
|
||||
}
|
||||
|
||||
this._player = new VideoRTC();
|
||||
this._player.containingPlayer = this;
|
||||
this._player.mediaPlayerController = this._mediaPlayerController;
|
||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||
this._player.src = address;
|
||||
this._player.visibilityCheck = false;
|
||||
|
||||
+3
-2
@@ -1,3 +1,5 @@
|
||||
import { MediaPlayerController } from '../../../../types';
|
||||
|
||||
export class VideoRTC extends HTMLElement {
|
||||
DISCONNECT_TIMEOUT: number;
|
||||
RECONNECT_TIMEOUT: number;
|
||||
@@ -29,9 +31,8 @@ export class VideoRTC extends HTMLElement {
|
||||
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
|
||||
|
||||
// Custom methods/members.
|
||||
containingPlayer: AdvancedCameraCardMediaPlayer | null;
|
||||
mediaPlayerController: MediaPlayerController | null;
|
||||
microphoneStream: MediaStream | null;
|
||||
reconnect();
|
||||
|
||||
setControls(controls: boolean): void;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
setControlsOnVideo,
|
||||
} from '../../../../utils/media';
|
||||
} from '../../../../utils/controls.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
@@ -162,13 +162,13 @@ export class VideoRTC extends HTMLElement {
|
||||
this.microphoneStream = null;
|
||||
|
||||
/**
|
||||
* A reference to a containing AdvancedCameraCardMediaPlayer object.
|
||||
* @type {AdvancedCameraCardMediaPlayer}}
|
||||
* A reference to a MediaPlayerController for this video
|
||||
* @type {MediaPlayerController | null}
|
||||
*/
|
||||
this.containingPlayer = null;
|
||||
this.mediaPlayerController = null;
|
||||
|
||||
/**
|
||||
* Whether to show or hide video controls for videos created in future.
|
||||
* Whether to show or hide video controls for videos created *in future*.
|
||||
* @type {boolean}}
|
||||
*/
|
||||
this.controls = true;
|
||||
@@ -353,7 +353,9 @@ export class VideoRTC extends HTMLElement {
|
||||
hideMediaControlsTemporarily(this.video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||
}
|
||||
dispatchMediaLoadedEvent(this, this.video, {
|
||||
player: this.containingPlayer,
|
||||
...(this.mediaPlayerController && {
|
||||
mediaPlayerController: this.mediaPlayerController,
|
||||
}),
|
||||
capabilities: {
|
||||
// 2-way audio is only supported on WebRTC connections. The state of
|
||||
// `this.microphoneStream` is not taken into account here since
|
||||
@@ -734,7 +736,9 @@ export class VideoRTC extends HTMLElement {
|
||||
if (!receivedFirstFrame) {
|
||||
receivedFirstFrame = true;
|
||||
dispatchMediaLoadedEvent(this, this.video, {
|
||||
player: this.containingPlayer,
|
||||
...(this.mediaPlayerController && {
|
||||
mediaPlayerController: this.mediaPlayerController,
|
||||
}),
|
||||
technology: ['mjpeg'],
|
||||
});
|
||||
}
|
||||
@@ -779,7 +783,9 @@ export class VideoRTC extends HTMLElement {
|
||||
context = canvas.getContext('2d');
|
||||
|
||||
dispatchMediaLoadedEvent(this, video2, {
|
||||
player: this.containingPlayer,
|
||||
...(this.mediaPlayerController && {
|
||||
mediaPlayerController: this.mediaPlayerController,
|
||||
}),
|
||||
technology: ['mp4'],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,13 +7,14 @@ import '../../../patches/ha-camera-stream';
|
||||
import '../../../patches/ha-hls-player.js';
|
||||
import '../../../patches/ha-web-rtc-player.js';
|
||||
import liveHAStyle from '../../../scss/live-ha.scss';
|
||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
||||
import {
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
} from '../../../types.js';
|
||||
|
||||
@customElement('advanced-camera-card-live-ha')
|
||||
export class AdvancedCameraCardLiveHA
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardLiveHA extends LitElement implements MediaPlayer {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@@ -23,46 +24,11 @@ export class AdvancedCameraCardLiveHA
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public controls = false;
|
||||
|
||||
protected _playerRef: Ref<Element & AdvancedCameraCardMediaPlayer> = createRef();
|
||||
protected _playerRef: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
public async play(): Promise<void> {
|
||||
return this._playerRef.value?.play();
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
this._playerRef.value?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
this._playerRef.value?.mute();
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
this._playerRef.value?.unmute();
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return this._playerRef.value?.isMuted() ?? true;
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
this._playerRef.value?.seek(seconds);
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
this._playerRef.value?.setControls(controls ?? this.controls);
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._playerRef.value?.isPaused() ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
return (await this._playerRef.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._playerRef.value?.getFullscreenElement() ?? null;
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
await this.updateComplete;
|
||||
return (await this._playerRef.value?.getMediaPlayerController()) ?? null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
|
||||
@@ -4,60 +4,26 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { CameraConfig } from '../../../config/types';
|
||||
import basicBlockStyle from '../../../scss/basic-block.scss';
|
||||
import { AdvancedCameraCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
||||
import '../../image-base.js';
|
||||
import {
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
} from '../../../types.js';
|
||||
import '../../image-updating-player.js';
|
||||
|
||||
@customElement('advanced-camera-card-live-image')
|
||||
export class AdvancedCameraCardLiveImage
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardLiveImage extends LitElement implements MediaPlayer {
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
protected _refImage: Ref<Element & AdvancedCameraCardMediaPlayer> = createRef();
|
||||
protected _refImage: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
public async play(): Promise<void> {
|
||||
await this._refImage.value?.play();
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
await this._refImage.value?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
await this._refImage.value?.mute();
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
await this._refImage.value?.unmute();
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return !!this._refImage.value?.isMuted();
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
await this._refImage.value?.seek(seconds);
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
await this._refImage.value?.setControls(controls);
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._refImage.value?.isPaused() ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refImage.value?.getFullscreenElement() ?? null;
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
await this.updateComplete;
|
||||
return (await this._refImage.value?.getMediaPlayerController()) ?? null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
@@ -66,13 +32,13 @@ export class AdvancedCameraCardLiveImage
|
||||
}
|
||||
|
||||
return html`
|
||||
<advanced-camera-card-image-base
|
||||
<advanced-camera-card-image-updating-player
|
||||
${ref(this._refImage)}
|
||||
.hass=${this.hass}
|
||||
.imageConfig=${this.cameraConfig.image}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
>
|
||||
</advanced-camera-card-image-base>
|
||||
</advanced-camera-card-image-updating-player>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,14 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
|
||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
||||
import {
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
ExtendedHomeAssistant,
|
||||
FullscreenElement,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
Message,
|
||||
} from '../../../types.js';
|
||||
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
||||
@@ -39,10 +40,9 @@ const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||
const JSMPEG_URL_SIGN_REFRESH_THRESHOLD_SECONDS = 1 * 60 * 60;
|
||||
|
||||
@customElement('advanced-camera-card-live-jsmpeg')
|
||||
export class AdvancedCameraCardLiveJSMPEG
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPlayer {
|
||||
protected hass?: ExtendedHomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@@ -52,61 +52,21 @@ export class AdvancedCameraCardLiveJSMPEG
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected hass?: ExtendedHomeAssistant;
|
||||
@state()
|
||||
protected _message: Message | null = null;
|
||||
|
||||
protected _jsmpegCanvasElement?: HTMLCanvasElement;
|
||||
protected _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||
protected _refreshPlayerTimer = new Timer();
|
||||
|
||||
@state()
|
||||
protected _message: Message | null = null;
|
||||
protected _mediaPlayerController = new JSMPEGMediaPlayerController(
|
||||
this,
|
||||
() => this._jsmpegVideoPlayer ?? null,
|
||||
() => this._jsmpegCanvasElement ?? null,
|
||||
);
|
||||
|
||||
public async play(): Promise<void> {
|
||||
return this._jsmpegVideoPlayer?.play();
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
this._jsmpegVideoPlayer?.stop();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
const player = this._jsmpegVideoPlayer?.player;
|
||||
if (player) {
|
||||
player.volume = 1;
|
||||
}
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return this._jsmpegVideoPlayer ? this._jsmpegVideoPlayer.player.volume === 0 : true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async seek(_seconds: number): Promise<void> {
|
||||
// JSMPEG does not support seeking.
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async setControls(_controls: boolean): Promise<void> {
|
||||
// Not implemented.
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._jsmpegVideoPlayer?.player?.paused ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._jsmpegCanvasElement ?? null;
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
return this._mediaPlayerController;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
@@ -170,7 +130,7 @@ export class AdvancedCameraCardLiveJSMPEG
|
||||
// calls back to the player to check for pause status for menu buttons.
|
||||
if (this._jsmpegCanvasElement) {
|
||||
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement, {
|
||||
player: this,
|
||||
mediaPlayerController: this._mediaPlayerController,
|
||||
capabilities: {
|
||||
supportsPause: true,
|
||||
},
|
||||
|
||||
@@ -12,28 +12,28 @@ import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
||||
import { VideoMediaPlayerController } from '../../../components-lib/media-player/video.js';
|
||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
||||
import {
|
||||
AdvancedCameraCardError,
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
Message,
|
||||
} from '../../../types.js';
|
||||
import { mayHaveAudio } from '../../../utils/audio.js';
|
||||
import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
setControlsOnVideo,
|
||||
} from '../../../utils/controls.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
dispatchMediaPlayEvent,
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
} from '../../../utils/media-info.js';
|
||||
import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
setControlsOnVideo,
|
||||
} from '../../../utils/media.js';
|
||||
import { screenshotMedia } from '../../../utils/screenshot.js';
|
||||
import { renderTask } from '../../../utils/task.js';
|
||||
import '../../message.js';
|
||||
import { renderMessage } from '../../message.js';
|
||||
@@ -44,10 +44,7 @@ import { VideoRTC } from './go2rtc/video-rtc.js';
|
||||
// Create a wrapper for AlexxIT's WebRTC card
|
||||
// - https://github.com/AlexxIT/WebRTC
|
||||
@customElement('advanced-camera-card-live-webrtc-card')
|
||||
export class AdvancedCameraCardLiveWebRTCCard
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements MediaPlayer {
|
||||
@property({ attribute: false })
|
||||
public cameraConfig?: CameraConfig;
|
||||
|
||||
@@ -65,62 +62,19 @@ export class AdvancedCameraCardLiveWebRTCCard
|
||||
|
||||
protected hass?: HomeAssistant;
|
||||
|
||||
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||
this,
|
||||
() => this._getVideo(),
|
||||
() => this.controls,
|
||||
);
|
||||
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
return this._mediaPlayerController;
|
||||
}
|
||||
|
||||
// A task to await the load of the WebRTC component.
|
||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||
|
||||
public async play(): Promise<void> {
|
||||
return this._getVideo()?.play();
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
this._getVideo()?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
player.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
player.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return this._getVideo()?.muted ?? true;
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
player.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
setControlsOnVideo(player, controls ?? this.controls);
|
||||
}
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._getVideo()?.paused ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
const video = this._getVideo();
|
||||
return video ? screenshotMedia(video) : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._getVideo();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -253,7 +207,7 @@ export class AdvancedCameraCardLiveWebRTCCard
|
||||
hideMediaControlsTemporarily(video, MEDIA_LOAD_CONTROLS_HIDE_SECONDS);
|
||||
}
|
||||
dispatchMediaLoadedEvent(this, video, {
|
||||
player: this,
|
||||
mediaPlayerController: this._mediaPlayerController,
|
||||
capabilities: {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(video),
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { VideoMediaPlayerController } from '../components-lib/media-player/video';
|
||||
import videoPlayerStyle from '../scss/video-player.scss';
|
||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types';
|
||||
import { mayHaveAudio } from '../utils/audio';
|
||||
import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
} from '../utils/controls';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
dispatchMediaPlayEvent,
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
} from '../utils/media-info';
|
||||
|
||||
@customElement('advanced-camera-card-video-player')
|
||||
export class AdvancedCameraCardVideoPlayer extends LitElement implements MediaPlayer {
|
||||
@property()
|
||||
public url?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public controls = false;
|
||||
|
||||
protected _refVideo: Ref<MediaPlayerElement<HTMLVideoElement>> = createRef();
|
||||
protected _mediaPlayerController = new VideoMediaPlayerController(
|
||||
this,
|
||||
() => this._refVideo.value ?? null,
|
||||
() => this.controls,
|
||||
);
|
||||
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
return this._mediaPlayerController;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
return html`
|
||||
<video
|
||||
${ref(this._refVideo)}
|
||||
muted
|
||||
playsinline
|
||||
crossorigin="anonymous"
|
||||
?autoplay=${false}
|
||||
?controls=${this.controls}
|
||||
@loadedmetadata=${(ev: Event) => {
|
||||
if (ev.target && this.controls) {
|
||||
hideMediaControlsTemporarily(
|
||||
ev.target as HTMLVideoElement,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@loadeddata=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev, {
|
||||
...(this._mediaPlayerController && {
|
||||
mediaPlayerController: this._mediaPlayerController,
|
||||
}),
|
||||
capabilities: {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
|
||||
},
|
||||
technology: ['mp4'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
@play=${() => dispatchMediaPlayEvent(this)}
|
||||
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||
>
|
||||
<source src="${ifDefined(this.url)}" type="video/mp4" />
|
||||
</video>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(videoPlayerStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-video-player': AdvancedCameraCardVideoPlayer;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,9 @@ import { localize } from '../../localize/localize.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerCarouselStyle from '../../scss/viewer-carousel.scss';
|
||||
import {
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
ExtendedHomeAssistant,
|
||||
MediaLoadedInfo,
|
||||
MediaPlayerController,
|
||||
} from '../../types.js';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { contentsChanged, setOrRemoveAttribute } from '../../utils/basic.js';
|
||||
@@ -92,7 +92,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
|
||||
protected _media: ViewMedia[] | null = null;
|
||||
protected _mediaActionsController = new MediaActionsController();
|
||||
protected _player: AdvancedCameraCardMediaPlayer | null = null;
|
||||
protected _loadedMediaPlayerController: MediaPlayerController | null = null;
|
||||
protected _refCarousel: Ref<HTMLElement> = createRef();
|
||||
|
||||
updated(changedProperties: PropertyValues): void {
|
||||
@@ -110,8 +110,8 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._mediaActionsController.hasRoot() && this._refCarousel.value) {
|
||||
this._mediaActionsController.initialize(this._refCarousel.value);
|
||||
if (this._refCarousel.value) {
|
||||
this._mediaActionsController.setRoot(this._refCarousel.value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,11 +383,11 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
this._setViewSelectedIndex(ev.detail.index);
|
||||
}}
|
||||
@advanced-camera-card:media:loaded=${(ev: CustomEvent<MediaLoadedInfo>) => {
|
||||
this._player = ev.detail.player ?? null;
|
||||
this._loadedMediaPlayerController = ev.detail.mediaPlayerController ?? null;
|
||||
this._seekHandler();
|
||||
}}
|
||||
@advanced-camera-card:media:unloaded=${() => {
|
||||
this._player = null;
|
||||
this._loadedMediaPlayerController = null;
|
||||
}}
|
||||
>
|
||||
${this.showControls ? this._renderNextPrevious('left', neighbors) : ''}
|
||||
@@ -418,7 +418,7 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
protected async _seekHandler(): Promise<void> {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const seek = view?.context?.mediaViewer?.seek;
|
||||
if (!this.hass || !seek || !this._media || !this._player) {
|
||||
if (!this.hass || !seek || !this._media || !this._loadedMediaPlayerController) {
|
||||
return;
|
||||
}
|
||||
const selectedMedia = this._media[this._selected];
|
||||
@@ -428,17 +428,17 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
|
||||
const seekTimeInMedia = selectedMedia.includesTime(seek);
|
||||
setOrRemoveAttribute(this, !seekTimeInMedia, 'unseekable');
|
||||
if (!seekTimeInMedia && !this._player.isPaused()) {
|
||||
this._player.pause();
|
||||
} else if (seekTimeInMedia && this._player.isPaused()) {
|
||||
this._player.play();
|
||||
if (!seekTimeInMedia && !this._loadedMediaPlayerController.isPaused()) {
|
||||
this._loadedMediaPlayerController.pause();
|
||||
} else if (seekTimeInMedia && this._loadedMediaPlayerController.isPaused()) {
|
||||
this._loadedMediaPlayerController.play();
|
||||
}
|
||||
|
||||
const seekTime =
|
||||
(await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null;
|
||||
|
||||
if (seekTime !== null) {
|
||||
this._player.seek(seekTime);
|
||||
this._loadedMediaPlayerController.seek(seekTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,12 +17,12 @@ import { CardWideConfig, ViewerConfig } from '../../config/types.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||
import {
|
||||
AdvancedCameraCardMediaPlayer,
|
||||
ExtendedHomeAssistant,
|
||||
FullscreenElement,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
MediaPlayerElement,
|
||||
ResolvedMedia,
|
||||
} from '../../types.js';
|
||||
import { mayHaveAudio } from '../../utils/audio.js';
|
||||
import { aspectRatioToString, errorToConsole } from '../../utils/basic.js';
|
||||
import {
|
||||
canonicalizeHAURL,
|
||||
@@ -35,30 +35,16 @@ import {
|
||||
getWebProxiedURL,
|
||||
shouldUseWebProxy,
|
||||
} from '../../utils/ha/web-proxy.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
dispatchMediaPlayEvent,
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
} from '../../utils/media-info.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
import {
|
||||
hideMediaControlsTemporarily,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
playMediaMutingIfNecessary,
|
||||
setControlsOnVideo,
|
||||
} from '../../utils/media.js';
|
||||
import { screenshotMedia } from '../../utils/screenshot.js';
|
||||
import { ViewMediaClassifier } from '../../view/media-classifier.js';
|
||||
import { MediaQueriesClassifier } from '../../view/media-queries-classifier.js';
|
||||
import { VideoContentType, ViewMedia } from '../../view/media.js';
|
||||
import '../image-player.js';
|
||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||
import '../video-player.js';
|
||||
|
||||
@customElement('advanced-camera-card-viewer-provider')
|
||||
export class AdvancedCameraCardViewerProvider
|
||||
extends LitElement
|
||||
implements AdvancedCameraCardMediaPlayer
|
||||
{
|
||||
export class AdvancedCameraCardViewerProvider extends LitElement implements MediaPlayer {
|
||||
@property({ attribute: false })
|
||||
public hass?: ExtendedHomeAssistant;
|
||||
|
||||
@@ -86,100 +72,14 @@ export class AdvancedCameraCardViewerProvider
|
||||
@property({ attribute: false })
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _refAdvancedCameraCardMediaPlayer: Ref<
|
||||
Element & AdvancedCameraCardMediaPlayer
|
||||
> = createRef();
|
||||
protected _refVideoProvider: Ref<HTMLVideoElement> = createRef();
|
||||
protected _refImageProvider: Ref<HTMLImageElement> = createRef();
|
||||
protected _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
@state()
|
||||
protected _url: string | null = null;
|
||||
|
||||
public async play(): Promise<void> {
|
||||
await playMediaMutingIfNecessary(
|
||||
this,
|
||||
this._refAdvancedCameraCardMediaPlayer.value ?? this._refVideoProvider.value,
|
||||
);
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
(
|
||||
this._refAdvancedCameraCardMediaPlayer.value || this._refVideoProvider.value
|
||||
)?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
this._refAdvancedCameraCardMediaPlayer.value?.mute();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
this._refVideoProvider.value.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
this._refAdvancedCameraCardMediaPlayer.value?.mute();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
this._refVideoProvider.value.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
return this._refAdvancedCameraCardMediaPlayer.value?.isMuted() ?? true;
|
||||
} else if (this._refVideoProvider.value) {
|
||||
return this._refVideoProvider.value.muted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
return this._refAdvancedCameraCardMediaPlayer.value.seek(seconds);
|
||||
} else if (this._refVideoProvider.value) {
|
||||
hideMediaControlsTemporarily(this._refVideoProvider.value);
|
||||
this._refVideoProvider.value.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
return this._refAdvancedCameraCardMediaPlayer.value.setControls(controls);
|
||||
} else if (this._refVideoProvider.value) {
|
||||
setControlsOnVideo(
|
||||
this._refVideoProvider.value,
|
||||
controls ?? this.viewerConfig?.controls.builtin ?? true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
return this._refAdvancedCameraCardMediaPlayer.value.isPaused();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
return this._refVideoProvider.value.paused;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
return await this._refAdvancedCameraCardMediaPlayer.value.getScreenshotURL();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
return screenshotMedia(this._refVideoProvider.value);
|
||||
} else if (this._refImageProvider.value) {
|
||||
return this._refImageProvider.value.src;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
if (this._refAdvancedCameraCardMediaPlayer.value) {
|
||||
return this._refAdvancedCameraCardMediaPlayer.value.getFullscreenElement();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
return this._refVideoProvider.value;
|
||||
}
|
||||
return null;
|
||||
public async getMediaPlayerController(): Promise<MediaPlayerController | null> {
|
||||
await this.updateComplete;
|
||||
return (await this._refProvider.value?.getMediaPlayerController()) ?? null;
|
||||
}
|
||||
|
||||
protected async _switchToRelatedClipView(): Promise<void> {
|
||||
@@ -332,8 +232,10 @@ export class AdvancedCameraCardViewerProvider
|
||||
: undefined,
|
||||
)}
|
||||
.settings=${mediaID ? view?.context?.zoom?.[mediaID]?.requested : undefined}
|
||||
@advanced-camera-card:zoom:zoomed=${() => this.setControls(false)}
|
||||
@advanced-camera-card:zoom:unzoomed=${() => this.setControls()}
|
||||
@advanced-camera-card:zoom:zoomed=${async () =>
|
||||
(await this.getMediaPlayerController())?.setControls(false)}
|
||||
@advanced-camera-card:zoom:unzoomed=${async () =>
|
||||
(await this.getMediaPlayerController())?.setControls()}
|
||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
||||
handleZoomSettingsObservedEvent(ev, this.viewManagerEpoch?.manager, mediaID)}
|
||||
>
|
||||
@@ -359,7 +261,7 @@ export class AdvancedCameraCardViewerProvider
|
||||
${ViewMediaClassifier.isVideo(this.media)
|
||||
? this.media.getVideoContentType() === VideoContentType.HLS
|
||||
? html`<advanced-camera-card-ha-hls-player
|
||||
${ref(this._refAdvancedCameraCardMediaPlayer)}
|
||||
${ref(this._refProvider)}
|
||||
allow-exoplayer
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
?autoplay=${false}
|
||||
@@ -373,54 +275,26 @@ export class AdvancedCameraCardViewerProvider
|
||||
>
|
||||
</advanced-camera-card-ha-hls-player>`
|
||||
: html`
|
||||
<video
|
||||
${ref(this._refVideoProvider)}
|
||||
<advanced-camera-card-video-player
|
||||
${ref(this._refProvider)}
|
||||
url=${this._url}
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
muted
|
||||
playsinline
|
||||
crossorigin="anonymous"
|
||||
?autoplay=${false}
|
||||
?controls=${this.viewerConfig.controls.builtin}
|
||||
@loadedmetadata=${(ev: Event) => {
|
||||
if (ev.target && !!this.viewerConfig?.controls.builtin) {
|
||||
hideMediaControlsTemporarily(
|
||||
ev.target as HTMLVideoElement,
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@loadeddata=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev, {
|
||||
player: this,
|
||||
capabilities: {
|
||||
supportsPause: true,
|
||||
hasAudio: mayHaveAudio(ev.target as HTMLVideoElement),
|
||||
},
|
||||
technology: ['hls'],
|
||||
});
|
||||
}}
|
||||
@volumechange=${() => dispatchMediaVolumeChangeEvent(this)}
|
||||
@play=${() => dispatchMediaPlayEvent(this)}
|
||||
@pause=${() => dispatchMediaPauseEvent(this)}
|
||||
>
|
||||
<source src=${this._url} type="video/mp4" />
|
||||
</video>
|
||||
</advanced-camera-card-video-player>
|
||||
`
|
||||
: html`<img
|
||||
${ref(this._refImageProvider)}
|
||||
: html`<advanced-camera-card-image-player
|
||||
${ref(this._refProvider)}
|
||||
url="${this._url}"
|
||||
aria-label="${this.media.getTitle() ?? ''}"
|
||||
src="${this._url}"
|
||||
title="${this.media.getTitle() ?? ''}"
|
||||
@click=${() => {
|
||||
if (this.viewerConfig?.snapshot_click_plays_clip) {
|
||||
this._switchToRelatedClipView();
|
||||
}
|
||||
}}
|
||||
@load=${(ev: Event) => {
|
||||
dispatchMediaLoadedEvent(this, ev, { player: this, technology: ['jpg'] });
|
||||
}}
|
||||
/>`}
|
||||
></advanced-camera-card-image-player>`}
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user