Merge remote-tracking branch 'stacktoshi/pip'
This commit is contained in:
@ -33,6 +33,7 @@
|
|||||||
"react-intl": "^6.4.4",
|
"react-intl": "^6.4.4",
|
||||||
"react-router-dom": "^6.13.0",
|
"react-router-dom": "^6.13.0",
|
||||||
"react-tag-input-component": "^2.0.2",
|
"react-tag-input-component": "^2.0.2",
|
||||||
|
"react-use-pip": "^1.5.0",
|
||||||
"recharts": "^2.9.3",
|
"recharts": "^2.9.3",
|
||||||
"semantic-sdp": "^3.26.3",
|
"semantic-sdp": "^3.26.3",
|
||||||
"usehooks-ts": "^2.9.1",
|
"usehooks-ts": "^2.9.1",
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { HTMLProps, useEffect, useMemo, useRef, useState } from "react";
|
import { HTMLProps, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { ProgressBar } from "./progress-bar";
|
import { ProgressBar } from "./progress-bar";
|
||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import usePictureInPicture, { VideoRefType } from "react-use-pip";
|
||||||
|
|
||||||
export enum VideoStatus {
|
export enum VideoStatus {
|
||||||
Online = "online",
|
Online = "online",
|
||||||
@ -138,6 +139,17 @@ export default function LiveVideoPlayer({
|
|||||||
}
|
}
|
||||||
}, [video, volume, muted]);
|
}, [video, volume, muted]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isPictureInPictureActive,
|
||||||
|
isPictureInPictureAvailable,
|
||||||
|
togglePictureInPicture
|
||||||
|
} = usePictureInPicture(video as VideoRefType);
|
||||||
|
|
||||||
|
const handlePIPClick = useCallback(async () => {
|
||||||
|
togglePictureInPicture(!isPictureInPictureActive);
|
||||||
|
}, [
|
||||||
|
isPictureInPictureActive, togglePictureInPicture]);
|
||||||
|
|
||||||
function playStateToIcon() {
|
function playStateToIcon() {
|
||||||
switch (playState) {
|
switch (playState) {
|
||||||
case "playing":
|
case "playing":
|
||||||
@ -184,15 +196,17 @@ export default function LiveVideoPlayer({
|
|||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
</div>
|
</div>
|
||||||
{/* CENTER PLAY ICON */}
|
{/* CENTER PLAY ICON */}
|
||||||
<div className="absolute w-full h-full flex items-center justify-center pointer">
|
<div className="absolute w-full h-full flex items-center justify-center cursor-pointer">
|
||||||
<Icon name={playStateToIcon()} size={80} className={playState === "loading" ? "animate-spin" : ""} />
|
{!isPictureInPictureActive && (
|
||||||
|
<Icon name={playStateToIcon()} size={80} className={playState === "loading" ? "animate-spin" : ""} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* PLAYER CONTROLS OVERLAY */}
|
{/* PLAYER CONTROLS OVERLAY */}
|
||||||
<div
|
<div
|
||||||
className="absolute flex items-center gap-1 bottom-0 w-full bg-primary h-[40px]"
|
className="absolute flex items-center gap-1 bottom-0 w-full bg-primary h-[40px]"
|
||||||
onClick={e => e.stopPropagation()}>
|
onClick={e => e.stopPropagation()}>
|
||||||
<div className="flex grow gap-1 items-center">
|
<div className="flex grow gap-1 items-center">
|
||||||
<div className="px-5 py-2 pointer" onClick={() => togglePlay()}>
|
<div className="px-5 py-2 cursor-pointer" onClick={() => togglePlay()}>
|
||||||
<Icon name={playStateToIcon()} className={playState === "loading" ? "animate-spin" : ""} />
|
<Icon name={playStateToIcon()} className={playState === "loading" ? "animate-spin" : ""} />
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-2 uppercase font-bold tracking-wide hover:bg-primary-hover">{pStatus}</div>
|
<div className="px-3 py-2 uppercase font-bold tracking-wide hover:bg-primary-hover">{pStatus}</div>
|
||||||
@ -219,7 +233,7 @@ export default function LiveVideoPlayer({
|
|||||||
<Menu
|
<Menu
|
||||||
direction="top"
|
direction="top"
|
||||||
align="center"
|
align="center"
|
||||||
menuButton={<div className="px-3 py-2 tracking-wide pointer">{levelName(level)}</div>}
|
menuButton={<div className="px-3 py-2 tracking-wide cursor-pointer">{levelName(level)}</div>}
|
||||||
menuClassName="bg-primary w-fit">
|
menuClassName="bg-primary w-fit">
|
||||||
{levels?.map(v => (
|
{levels?.map(v => (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -232,8 +246,12 @@ export default function LiveVideoPlayer({
|
|||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
|
{isPictureInPictureAvailable && (
|
||||||
|
<div className="pl-3 py-2 cursor-pointer tracking-wide font-bold text-sm"
|
||||||
|
onClick={handlePIPClick}>PIP</div>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className="px-3 py-2 pointer"
|
className="px-2 py-2 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (video.current) {
|
if (video.current) {
|
||||||
video.current.requestFullscreen();
|
video.current.requestFullscreen();
|
||||||
@ -244,6 +262,13 @@ export default function LiveVideoPlayer({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{isPictureInPictureActive && (
|
||||||
|
<div
|
||||||
|
className="absolute z-20 bg-[#00000055] select-none w-full h-full flex items-center justify-center cursor-pointer">
|
||||||
|
<h2 onClick={async () => togglePictureInPicture(!isPictureInPictureActive)}>Video is playing in PIP
|
||||||
|
window</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{status === VideoStatus.Offline && (
|
{status === VideoStatus.Offline && (
|
||||||
<div className="absolute w-full h-full z-20 bg-[#000000aa] flex items-center justify-center text-3xl font-bold uppercase">
|
<div className="absolute w-full h-full z-20 bg-[#000000aa] flex items-center justify-center text-3xl font-bold uppercase">
|
||||||
<FormattedMessage defaultMessage="Offline" id="7UOvbT" />
|
<FormattedMessage defaultMessage="Offline" id="7UOvbT" />
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -6838,6 +6838,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-use-pip@npm:^1.5.0":
|
||||||
|
version: 1.5.0
|
||||||
|
resolution: "react-use-pip@npm:1.5.0"
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.9.0 || ^17 || ^18
|
||||||
|
checksum: 10c0/448a6d176eada90cafdcc2465dd9c974ba46e80c033168b3ecef87a064dd5f2976a8758f039e8b5c3f4abf15f844ad4096a2db6b1b0465f8a76988bee28aefdc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react@npm:^18.2.0":
|
"react@npm:^18.2.0":
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
resolution: "react@npm:18.2.0"
|
resolution: "react@npm:18.2.0"
|
||||||
@ -7516,6 +7525,7 @@ __metadata:
|
|||||||
react-intl: "npm:^6.4.4"
|
react-intl: "npm:^6.4.4"
|
||||||
react-router-dom: "npm:^6.13.0"
|
react-router-dom: "npm:^6.13.0"
|
||||||
react-tag-input-component: "npm:^2.0.2"
|
react-tag-input-component: "npm:^2.0.2"
|
||||||
|
react-use-pip: "npm:^1.5.0"
|
||||||
recharts: "npm:^2.9.3"
|
recharts: "npm:^2.9.3"
|
||||||
rollup-plugin-visualizer: "npm:^5.10.0"
|
rollup-plugin-visualizer: "npm:^5.10.0"
|
||||||
semantic-sdp: "npm:^3.26.3"
|
semantic-sdp: "npm:^3.26.3"
|
||||||
|
Reference in New Issue
Block a user