feat: Add basic fullscreen support for iPhone (#1857)

- Closes #1444
This commit is contained in:
Dermot Duffy
2025-01-27 07:23:07 -08:00
committed by GitHub
parent d1afbc95b6
commit 3e8e688b36
44 changed files with 1150 additions and 308 deletions
+10 -1
View File
@@ -19,7 +19,12 @@ import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
import defaultImage from '../images/frigate-bird-in-sky.jpg';
import { localize } from '../localize/localize.js';
import imageStyle from '../scss/image.scss';
import { FrigateCardMediaPlayer, MediaLoadedInfo, Message } from '../types.js';
import {
FrigateCardMediaPlayer,
FullscreenElement,
MediaLoadedInfo,
Message,
} from '../types.js';
import { contentsChanged } from '../utils/basic.js';
import { isHassDifferent } from '../utils/ha/index.js';
import {
@@ -123,6 +128,10 @@ export class FrigateCardImageBase extends LitElement implements FrigateCardMedia
return this._cachedValueController?.value ?? null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._refImage.value ?? null;
}
/**
* Determine whether the element should be updated.
* @param changedProps The changed properties if any.
+5 -1
View File
@@ -17,7 +17,7 @@ import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-vie
import { CameraConfig, ImageViewConfig } from '../config/types';
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
import basicBlockStyle from '../scss/basic-block.scss';
import { FrigateCardMediaPlayer } from '../types.js';
import { FrigateCardMediaPlayer, FullscreenElement } from '../types.js';
import { aspectRatioToString } from '../utils/basic';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import './image-base';
@@ -79,6 +79,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
return (await this._refImage.value?.getScreenshotURL()) ?? null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._refImage.value?.getFullscreenElement() ?? null;
}
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
if (
+9 -1
View File
@@ -24,7 +24,11 @@ import {
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
import { localize } from '../../localize/localize.js';
import liveProviderStyle from '../../scss/live-provider.scss';
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
import {
ExtendedHomeAssistant,
FrigateCardMediaPlayer,
FullscreenElement,
} from '../../types.js';
import { aspectRatioToString } from '../../utils/basic.js';
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
@@ -144,6 +148,10 @@ export class FrigateCardLiveProvider
return (await this._refProvider.value?.getScreenshotURL()) ?? null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._refProvider.value?.getFullscreenElement() ?? null;
}
/**
* Get the fully resolved live provider.
* @returns A live provider (that is not 'auto').
@@ -16,6 +16,7 @@ import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
import {
ExtendedHomeAssistant,
FrigateCardMediaPlayer,
FullscreenElement,
Message,
} from '../../../../types.js';
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
@@ -102,6 +103,10 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
return this._player?.video ? screenshotMedia(this._player.video) : null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._player?.video ?? null;
}
disconnectedCallback(): void {
this._player = undefined;
this._message = null;
@@ -151,7 +156,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
this._player.microphoneStream = this.microphoneState?.stream ?? null;
this._player.src = address;
this._player.visibilityCheck = false;
this._player.controls = this.controls;
this._player.setControls(this.controls);
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
this._player.mode = this.cameraConfig.go2rtc.modes.join(',');
@@ -170,7 +175,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
}
if (changedProps.has('controls') && this._player) {
this._player.controls = this.controls;
this._player.setControls(this.controls);
}
if (
+2 -1
View File
@@ -29,8 +29,9 @@ export class VideoRTC extends HTMLElement {
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
// Custom methods/members.
controls: boolean;
containingPlayer: FrigateCardMediaPlayer | null;
microphoneStream: MediaStream | null;
reconnect();
setControls(controls: boolean): void;
}
@@ -168,7 +168,7 @@ export class VideoRTC extends HTMLElement {
this.containingPlayer = null;
/**
* Whether to show or hide video controls.
* Whether to show or hide video controls for videos created in future.
* @type {boolean}}
*/
this.controls = true;
@@ -191,6 +191,15 @@ export class VideoRTC extends HTMLElement {
}
}
setControls(controls) {
this.controls = controls;
// If the video already exists the controls won't be changed by default.
if (this.video) {
setControlsOnVideo(this.video, controls);
}
}
/**
* Set video source (WebSocket URL). Support relative path.
* @param {string|URL} value
+5 -1
View File
@@ -7,7 +7,7 @@ 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 { FrigateCardMediaPlayer } from '../../../types.js';
import { FrigateCardMediaPlayer, FullscreenElement } from '../../../types.js';
@customElement('frigate-card-live-ha')
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
@@ -58,6 +58,10 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
return (await this._playerRef.value?.getScreenshotURL()) ?? null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._playerRef.value?.getFullscreenElement() ?? null;
}
protected render(): TemplateResult | void {
if (!this.hass) {
return;
+5 -1
View File
@@ -4,7 +4,7 @@ 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 { FrigateCardMediaPlayer } from '../../../types.js';
import { FrigateCardMediaPlayer, FullscreenElement } from '../../../types.js';
import '../../image-base.js';
@customElement('frigate-card-live-image')
@@ -53,6 +53,10 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
return (await this._refImage.value?.getScreenshotURL()) ?? null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._refImage.value?.getFullscreenElement() ?? null;
}
protected render(): TemplateResult | void {
if (!this.hass || !this.cameraConfig) {
return;
+5
View File
@@ -17,6 +17,7 @@ import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
import {
ExtendedHomeAssistant,
FrigateCardMediaPlayer,
FullscreenElement,
Message,
} from '../../../types.js';
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
@@ -101,6 +102,10 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._jsmpegCanvasElement ?? null;
}
protected willUpdate(changedProperties: PropertyValues): void {
if (
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
+21 -12
View File
@@ -15,7 +15,12 @@ import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
import { localize } from '../../../localize/localize.js';
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
import { FrigateCardError, FrigateCardMediaPlayer, Message } from '../../../types.js';
import {
FrigateCardError,
FrigateCardMediaPlayer,
FullscreenElement,
Message,
} from '../../../types.js';
import { mayHaveAudio } from '../../../utils/audio.js';
import {
dispatchMediaLoadedEvent,
@@ -64,54 +69,58 @@ export class FrigateCardLiveWebRTCCard
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
public async play(): Promise<void> {
return this._getPlayer()?.play();
return this._getVideo()?.play();
}
public async pause(): Promise<void> {
this._getPlayer()?.pause();
this._getVideo()?.pause();
}
public async mute(): Promise<void> {
const player = this._getPlayer();
const player = this._getVideo();
if (player) {
player.muted = true;
}
}
public async unmute(): Promise<void> {
const player = this._getPlayer();
const player = this._getVideo();
if (player) {
player.muted = false;
}
}
public isMuted(): boolean {
return this._getPlayer()?.muted ?? true;
return this._getVideo()?.muted ?? true;
}
public async seek(seconds: number): Promise<void> {
const player = this._getPlayer();
const player = this._getVideo();
if (player) {
player.currentTime = seconds;
}
}
public async setControls(controls?: boolean): Promise<void> {
const player = this._getPlayer();
const player = this._getVideo();
if (player) {
setControlsOnVideo(player, controls ?? this.controls);
}
}
public isPaused(): boolean {
return this._getPlayer()?.paused ?? true;
return this._getVideo()?.paused ?? true;
}
public async getScreenshotURL(): Promise<string | null> {
const video = this._getPlayer();
const video = this._getVideo();
return video ? screenshotMedia(video) : null;
}
public getFullscreenElement(): FullscreenElement | null {
return this._getVideo();
}
connectedCallback(): void {
super.connectedCallback();
@@ -141,7 +150,7 @@ export class FrigateCardLiveWebRTCCard
* Get the underlying video player.
* @returns The player or `null` if not found.
*/
protected _getPlayer(): HTMLVideoElement | null {
protected _getVideo(): HTMLVideoElement | null {
return this._getVideoRTC()?.video ?? null;
}
@@ -236,7 +245,7 @@ export class FrigateCardLiveWebRTCCard
// media load event.
this.updateComplete.then(() => {
const videoRTC = this._getVideoRTC();
const video = this._getPlayer();
const video = this._getVideo();
if (video) {
setControlsOnVideo(video, this.controls);
video.onloadeddata = () => {
+10
View File
@@ -19,6 +19,7 @@ import viewerProviderStyle from '../../scss/viewer-provider.scss';
import {
ExtendedHomeAssistant,
FrigateCardMediaPlayer,
FullscreenElement,
ResolvedMedia,
} from '../../types.js';
import { mayHaveAudio } from '../../utils/audio.js';
@@ -169,6 +170,15 @@ export class FrigateCardViewerProvider
return null;
}
public getFullscreenElement(): FullscreenElement | null {
if (this._refFrigateCardMediaPlayer.value) {
return this._refFrigateCardMediaPlayer.value.getFullscreenElement();
} else if (this._refVideoProvider.value) {
return this._refVideoProvider.value;
}
return null;
}
protected async _switchToRelatedClipView(): Promise<void> {
const view = this.viewManagerEpoch?.manager.getView();
if (