diff --git a/src/components/player/atoms/Pip.tsx b/src/components/player/atoms/Pip.tsx new file mode 100644 index 00000000..cc9aa21b --- /dev/null +++ b/src/components/player/atoms/Pip.tsx @@ -0,0 +1,14 @@ +import { Icons } from "@/components/Icon"; +import { VideoPlayerButton } from "@/components/player/internals/Button"; +import { usePlayerStore } from "@/stores/player/store"; + +export function Pip() { + const display = usePlayerStore((s) => s.display); + + return ( + display?.togglePictureInPicture()} + icon={Icons.PICTURE_IN_PICTURE} + /> + ); +} diff --git a/src/components/player/atoms/index.ts b/src/components/player/atoms/index.ts index 2729acc3..111703d5 100644 --- a/src/components/player/atoms/index.ts +++ b/src/components/player/atoms/index.ts @@ -1,5 +1,6 @@ export * from "./Pause"; export * from "./Fullscreen"; +export * from "./Pip"; export * from "./ProgressBar"; export * from "./Skips"; export * from "./Time"; diff --git a/src/components/player/display/base.ts b/src/components/player/display/base.ts index bbc7a55c..6f1b1a16 100644 --- a/src/components/player/display/base.ts +++ b/src/components/player/display/base.ts @@ -42,6 +42,14 @@ function hlsLevelsToQualities(levels: Level[]): SourceQuality[] { .filter((v): v is SourceQuality => !!v); } +export function canWebkitPictureInPicture(): boolean { + return "webkitSupportsPresentationMode" in document.createElement("video"); +} + +export function canPictureInPicture(): boolean { + return "pictureInPictureEnabled" in document; +} + export function makeVideoElementDisplayInterface(): DisplayInterface { const { emit, on, off } = makeEmitter(); let source: LoadableSource | null = null; @@ -306,6 +314,24 @@ export function makeVideoElementDisplayInterface(): DisplayInterface { } } }, + togglePictureInPicture() { + if (!videoElement) return; + if (canWebkitPictureInPicture()) { + const webkitPlayer = videoElement as any; + webkitPlayer.webkitSetPresentationMode( + webkitPlayer.webkitPresentationMode === "picture-in-picture" + ? "inline" + : "picture-in-picture" + ); + } + if (canPictureInPicture()) { + if (videoElement !== document.pictureInPictureElement) { + videoElement.requestPictureInPicture(); + } else { + document.exitPictureInPicture(); + } + } + }, startAirplay() { const videoPlayer = videoElement as any; if (videoPlayer && videoPlayer.webkitShowPlaybackTargetPicker) { diff --git a/src/components/player/display/displayInterface.ts b/src/components/player/display/displayInterface.ts index a7a9d4b7..67284d73 100644 --- a/src/components/player/display/displayInterface.ts +++ b/src/components/player/display/displayInterface.ts @@ -35,6 +35,7 @@ export interface DisplayInterface extends Listener { processVideoElement(video: HTMLVideoElement): void; processContainerElement(container: HTMLElement): void; toggleFullscreen(): void; + togglePictureInPicture(): void; setSeeking(active: boolean): void; setVolume(vol: number): void; setTime(t: number): void; diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index e441a2f0..0585f502 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -79,6 +79,7 @@ export function PlayerPart(props: PlayerPartProps) {
+