first commit
Some checks failed
Build / run (push) Has been cancelled

This commit is contained in:
maher
2025-10-29 11:42:25 +01:00
commit 703f50a09d
4595 changed files with 385164 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
import {PipAdapter} from '@common/player/state/pip/pip-adapter';
import {IS_CLIENT} from '@common/utils/platform';
export const createChromePipAdapter = (
host: HTMLVideoElement,
onChange: () => void
): PipAdapter => {
return {
isSupported: () => canUsePiPInChrome(),
isPip: () => {
return host === document.pictureInPictureElement;
},
enter: () => {
if (canUsePiPInChrome()) {
return host.requestPictureInPicture();
}
},
exit: () => {
if (canUsePiPInChrome()) {
return document.exitPictureInPicture();
}
},
bindEvents: () => {
if (canUsePiPInChrome()) {
host.addEventListener('enterpictureinpicture', onChange);
host.addEventListener('leavepictureinpicture', onChange);
}
},
unbindEvents: () => {
if (canUsePiPInChrome()) {
host.removeEventListener('enterpictureinpicture', onChange);
host.removeEventListener('leavepictureinpicture', onChange);
}
},
};
};
/**
* Checks if the native HTML5 video player can enter picture-in-picture (PIP) mode when using
* the Chrome browser.
*
* @see https://developers.google.com/web/updates/2018/10/watch-video-using-picture-in-picture
*/
let _canUsePiPInChrome: boolean | undefined;
const canUsePiPInChrome = (): boolean => {
if (!IS_CLIENT) return false;
if (_canUsePiPInChrome == null) {
const video = document.createElement('video');
_canUsePiPInChrome =
!!document.pictureInPictureEnabled && !video.disablePictureInPicture;
}
return _canUsePiPInChrome;
};

View File

@@ -0,0 +1,8 @@
export interface PipAdapter {
isSupported: () => boolean;
isPip: () => boolean;
enter: () => Promise<unknown> | undefined;
exit: () => Promise<unknown> | undefined;
bindEvents: () => void;
unbindEvents: () => void;
}

View File

@@ -0,0 +1,91 @@
import {StateCreator} from 'zustand';
import {
PlayerState,
ProviderListeners,
} from '@common/player/state/player-state';
import {PipAdapter} from '@common/player/state/pip/pip-adapter';
import {createChromePipAdapter} from '@common/player/state/pip/chrome-pip-adapter';
import {createSafariPipAdapter} from '@common/player/state/pip/safari-pip-adapter';
export interface PipSlice {
isPip: boolean;
canPip: boolean;
enterPip: () => void;
exitPip: () => void;
togglePip: () => void;
initPip: () => void;
destroyPip: () => void;
}
type BaseSliceCreator = StateCreator<
PipSlice & PlayerState,
[['zustand/immer', unknown]],
[],
PipSlice
>;
type StoreLice = BaseSliceCreator extends (...a: infer U) => infer R
? (...a: [...U, Set<Partial<ProviderListeners>>]) => R
: never;
const adapterFactories = [createChromePipAdapter, createSafariPipAdapter];
export const createPipSlice: StoreLice = (set, get) => {
let subscription: () => void | undefined;
let adapters: PipAdapter[] = [];
const onPipChange = () => {
set({isPip: adapters.some(a => a.isPip())});
};
const isSupported = (): boolean => {
if (get().providerName !== 'htmlVideo') {
return false;
}
return adapters.some(adapter => adapter.isSupported());
};
return {
isPip: false,
canPip: false,
enterPip: async () => {
if (get().isPip || !isSupported()) return;
await adapters.find(a => a.isSupported())?.enter();
},
exitPip: async () => {
if (!get().isPip) return;
await adapters.find(a => a.isSupported())?.exit();
},
togglePip: () => {
if (get().isPip) {
get().exitPip();
} else {
get().enterPip();
}
},
initPip: () => {
subscription = get().subscribe({
providerReady: ({el}) => {
// when changing adapters, remove previous adapter events and exit pip
adapters.every(a => a.unbindEvents());
if (get().isPip) {
adapters.every(a => a.exit());
}
// create new adapters, and if pip is supported on at least one, bind events
adapters = adapterFactories.map(factory =>
factory(el as HTMLVideoElement, onPipChange)
);
const canPip = isSupported();
if (canPip) {
adapters.every(a => a.bindEvents());
}
set({canPip});
},
});
},
destroyPip: () => {
get().exitPip();
subscription?.();
},
};
};

View File

@@ -0,0 +1,56 @@
import {PipAdapter} from '@common/player/state/pip/pip-adapter';
import {IS_CLIENT, IS_IPHONE} from '@common/utils/platform';
export const createSafariPipAdapter = (
host: HTMLVideoElement,
onChange: () => void
): PipAdapter => {
return {
isSupported: () => canUsePiPInSafari(),
isPip: () => {
return host.webkitPresentationMode === 'picture-in-picture';
},
enter: () => {
if (canUsePiPInSafari()) {
return host.webkitSetPresentationMode?.('picture-in-picture');
}
},
exit: () => {
if (canUsePiPInSafari()) {
return host.webkitSetPresentationMode?.('inline');
}
},
bindEvents: () => {
if (canUsePiPInSafari()) {
host.addEventListener('webkitpresentationmodechanged', onChange);
}
},
unbindEvents: () => {
if (canUsePiPInSafari()) {
host.removeEventListener('webkitpresentationmodechanged', onChange);
}
},
};
};
/**
* Checks if the native HTML5 video player can enter picture-in-picture (PIP) mode when using
* the desktop Safari browser, iOS Safari appears to "support" PiP through the check, however PiP
* does not function.
*
* @see https://developer.apple.com/documentation/webkitjs/adding_picture_in_picture_to_your_safari_media_controls
*/
let _canUsePiPInSafari: boolean | undefined;
const canUsePiPInSafari = (): boolean => {
if (!IS_CLIENT) return false;
const video = document.createElement('video');
if (_canUsePiPInSafari == null) {
_canUsePiPInSafari =
// @ts-ignore
!!video.webkitSupportsPresentationMode &&
// @ts-ignore
!!video.webkitSetPresentationMode &&
!IS_IPHONE;
}
return _canUsePiPInSafari;
};