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
+24
View File
@@ -0,0 +1,24 @@
import screenfull from 'screenfull';
import { WebkitHTMLVideoElement } from '../../types';
import { CardFullscreenAPI } from '../types';
import { ScreenfullFullScreenProvider } from './screenfull';
import { FullscreenHandler, FullscreenProvider } from './types';
import { WebkitFullScreenProvider } from './webkit';
export class FullscreenProviderFactory {
public static create(
api: CardFullscreenAPI,
handler: FullscreenHandler,
): FullscreenProvider | null {
if (screenfull.isEnabled) {
return new ScreenfullFullScreenProvider(api, handler);
}
const video = document.createElement('video') as Partial<WebkitHTMLVideoElement>;
if (!!video.webkitEnterFullscreen) {
return new WebkitFullScreenProvider(api, handler);
}
return null;
}
}
@@ -0,0 +1,67 @@
import { isBeingCasted } from '../../utils/casting';
import { CardFullscreenAPI } from '../types';
import { FullscreenProviderFactory } from './factory';
import { FullscreenProvider } from './types';
export class FullscreenManager {
protected _api: CardFullscreenAPI;
protected _provider: FullscreenProvider | null;
constructor(api: CardFullscreenAPI, provider?: FullscreenProvider) {
this._api = api;
this._provider =
provider ?? FullscreenProviderFactory.create(api, this._fullscreenHandler);
}
public connect(): void {
this._provider?.connect();
}
public disconnect(): void {
this._provider?.disconnect();
}
public setFullscreen(fullscreen: boolean): void {
this._provider?.setFullscreen(fullscreen);
}
public isInFullscreen(): boolean {
return this._provider?.isInFullscreen() ?? false;
}
public isSupported(): boolean {
if (isBeingCasted()) {
return false;
}
return this._provider?.isSupported() ?? false;
}
public initialize(): void {
this._setConditionState();
}
public toggleFullscreen(): void {
if (this._provider?.isInFullscreen()) {
this._provider.setFullscreen(false);
} else {
this._provider?.setFullscreen(true);
}
}
protected _fullscreenHandler = (): void => {
this._api.getExpandManager().setExpanded(false);
this._setConditionState();
// Re-render after a change to fullscreen mode to take advantage of
// the expanded screen real-estate (vs staying in aspect-ratio locked
// modes).
this._api.getCardElementManager().update();
};
protected _setConditionState(): void {
this._api.getConditionsManager()?.setState({
fullscreen: this.isInFullscreen(),
});
}
}
@@ -0,0 +1,12 @@
import { CardFullscreenAPI } from '../types';
import { FullscreenHandler } from './types';
export class FullscreenProviderBase {
protected _api: CardFullscreenAPI;
protected _handler: FullscreenHandler;
constructor(api: CardFullscreenAPI, handler: FullscreenHandler) {
this._api = api;
this._handler = handler;
}
}
@@ -0,0 +1,40 @@
import screenfull from 'screenfull';
import { FullscreenProviderBase } from '../provider';
import { FullscreenProvider } from '../types';
export class ScreenfullFullScreenProvider
extends FullscreenProviderBase
implements FullscreenProvider
{
public connect(): void {
if (screenfull.isEnabled) {
screenfull.on('change', this._handler);
}
}
public disconnect(): void {
if (screenfull.isEnabled) {
screenfull.off('change', this._handler);
}
}
public isInFullscreen(): boolean {
return screenfull.isEnabled && screenfull.isFullscreen;
}
public isSupported(): boolean {
return screenfull.isEnabled;
}
public setFullscreen(fullscreen: boolean): void {
if (!this.isSupported()) {
return;
}
if (fullscreen) {
screenfull.request(this._api.getCardElementManager().getElement());
} else {
screenfull.exit();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
export type FullscreenHandler = () => void;
export interface FullscreenProvider {
connect(): void;
disconnect(): void;
isInFullscreen(): boolean;
isSupported(): boolean;
setFullscreen(fullscreen: boolean): void;
}
@@ -0,0 +1,92 @@
import { WebkitHTMLVideoElement } from '../../../types';
import { Timer } from '../../../utils/timer';
import { ConditionState } from '../../conditions-manager';
import { FullscreenProviderBase } from '../provider';
import { FullscreenProvider } from '../types';
const WEBKIT_PLAY_SECONDS = 0.5;
/**
* Fullscreen implementation for webkit based browsers that do not support the
* standard Fullscreen API (expected to be exclusively iOS on an iPhone).
* See: https://github.com/dermotduffy/frigate-hass-card/issues/1444
*/
export class WebkitFullScreenProvider
extends FullscreenProviderBase
implements FullscreenProvider
{
protected _playTimer = new Timer();
public connect(): void {
this._api.getConditionsManager().addListener(this._conditionChangeHandler);
}
public disconnect(): void {
this._api.getConditionsManager().removeListener(this._conditionChangeHandler);
}
protected _conditionChangeHandler = (
newState: ConditionState,
oldState?: ConditionState,
): void => {
if (
newState.mediaLoadedInfo?.player?.getFullscreenElement() !==
oldState?.mediaLoadedInfo?.player?.getFullscreenElement()
) {
const oldElement = oldState?.mediaLoadedInfo?.player?.getFullscreenElement();
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
const newElement = newState.mediaLoadedInfo?.player?.getFullscreenElement();
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
}
};
protected _getVideoElement():
| (HTMLVideoElement & Partial<WebkitHTMLVideoElement>)
| null {
const element = this._api
.getMediaLoadedInfoManager()
.get()
?.player?.getFullscreenElement();
return element instanceof HTMLVideoElement ? element : null;
}
public isInFullscreen(): boolean {
return !!this._getVideoElement()?.webkitDisplayingFullscreen;
}
public isSupported(): boolean {
return !!this._getVideoElement()?.webkitSupportsFullscreen;
}
public setFullscreen(fullscreen: boolean): void {
if (!this.isSupported()) {
return;
}
const video = this._getVideoElement();
if (fullscreen) {
video?.webkitEnterFullscreen?.();
} else {
video?.webkitExitFullscreen?.();
}
}
protected _endHandler = (): void => {
this._handler();
// Webkit on iPhone stops the video when exiting fullscreen (why!). This
// hack workaround has the disadvantage that there's no way to tell if the
// video was legitimately paused before they user exited fullscreen (as the
// common path is for exit fullscreen to not involve
// setFullscreen(...) being called at all, but rather the user simply
// clicking the 'X' which then fires this event). That's probably the rare
// case though.
this._playTimer.start(WEBKIT_PLAY_SECONDS, () => {
this._getVideoElement()?.play();
});
};
}