mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-19 03:11:20 +00:00
added global player
This commit is contained in:
parent
2c3b8a6472
commit
275ddc4ac8
145
package-lock.json
generated
145
package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-idle-timer": "^5.7.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"srt-webvtt": "^2.0.0",
|
||||
"ts-key-enum": "^3.0.13",
|
||||
@ -38,6 +39,8 @@
|
||||
"@types/react": "^19.0.10",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
@ -47,7 +50,9 @@
|
||||
"@mui/icons-material": "^7.0.1",
|
||||
"@mui/material": "^7.0.1",
|
||||
"mediainfo.js": "^0.3.5",
|
||||
"react": "^19.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@ -1905,6 +1910,16 @@
|
||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
||||
@ -3194,24 +3209,58 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/re-resizable": {
|
||||
"version": "6.11.2",
|
||||
"resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz",
|
||||
"integrity": "sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
||||
"peer": true,
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"scheduler": "^0.25.0"
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.0.0"
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
|
||||
"integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^1.1.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.3.0",
|
||||
"react-dom": ">= 16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-draggable/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dropzone": {
|
||||
@ -3276,6 +3325,67 @@
|
||||
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/react-rnd": {
|
||||
"version": "10.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.5.2.tgz",
|
||||
"integrity": "sha512-0Tm4x7k7pfHf2snewJA8x7Nwgt3LV+58MVEWOVsFjk51eYruFEa6Wy7BNdxt4/lH0wIRsu7Gm3KjSXY2w7YaNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"re-resizable": "6.11.2",
|
||||
"react-draggable": "4.4.6",
|
||||
"tslib": "2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.3.0",
|
||||
"react-dom": ">=16.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-rnd/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.2.tgz",
|
||||
"integrity": "sha512-U7Nv3y+bMimgWjhlT5CRdzHPu2/KVmqPwKUCChW8en5P3znxUqwlYFlbmyj8Rgp1SF6zs5X4+77kBVknkg6a0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.2.tgz",
|
||||
"integrity": "sha512-Q8zb6VlTbdYKK5JJBLQEN06oTUa/RAbG/oQS1auK1I0TbJOXktqm+QENEVJU6QvWynlXPRBXI3fiOQcSEA78rA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@ -3427,16 +3537,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||
"peer": true
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/seedrandom": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-idle-timer": "^5.7.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"short-unique-id": "^5.2.0",
|
||||
"srt-webvtt": "^2.0.0",
|
||||
"ts-key-enum": "^3.0.13",
|
||||
@ -49,7 +50,9 @@
|
||||
"@mui/icons-material": "^7.0.1",
|
||||
"@mui/material": "^7.0.1",
|
||||
"mediainfo.js": "^0.3.5",
|
||||
"react": "^19.0.0"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
@ -60,6 +63,8 @@
|
||||
"@types/react": "^19.0.10",
|
||||
"cpy-cli": "^5.0.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"tsup": "^8.4.0",
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
|
@ -173,12 +173,21 @@ const SubtitleManagerComponent = ({
|
||||
getPublishedSubtitles,
|
||||
]);
|
||||
|
||||
const handleClose = () => {
|
||||
const ref = useRef<any>(null)
|
||||
console.log('isOpen', open)
|
||||
|
||||
useEffect(()=> {
|
||||
if(open){
|
||||
ref?.current?.focus()
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleBlur = (e: React.FocusEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish) {
|
||||
console.log('handleBlur')
|
||||
close();
|
||||
setMode(1);
|
||||
// setTitle("");
|
||||
// setDescription("");
|
||||
// setHasMetadata(false);
|
||||
setIsOpenPublish(false)
|
||||
}
|
||||
};
|
||||
|
||||
const publishHandler = async (subtitles: Subtitle[]) => {
|
||||
@ -250,23 +259,20 @@ const SubtitleManagerComponent = ({
|
||||
};
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
if(!open) return
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={subtitleBtnRef.current}
|
||||
onClose={handleClose}
|
||||
slots={{
|
||||
transition: Fade,
|
||||
}}
|
||||
slotProps={{
|
||||
transition: {
|
||||
timeout: 200,
|
||||
},
|
||||
paper: {
|
||||
sx: {
|
||||
bgcolor: alpha("#181818", 0.98),
|
||||
<Box
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
onBlur={handleBlur}
|
||||
bgcolor={alpha("#181818", 0.98)}
|
||||
|
||||
sx={
|
||||
{
|
||||
position: 'absolute',
|
||||
bottom: 60,
|
||||
right: 5,
|
||||
color: "white",
|
||||
opacity: 0.9,
|
||||
borderRadius: 2,
|
||||
@ -277,17 +283,9 @@ const SubtitleManagerComponent = ({
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
zIndex: 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
@ -421,14 +419,15 @@ const SubtitleManagerComponent = ({
|
||||
</Typography>
|
||||
))}
|
||||
</Box> */}
|
||||
</Popover>
|
||||
|
||||
</Box>
|
||||
<PublishSubtitles
|
||||
isOpen={isOpenPublish}
|
||||
setIsOpen={setIsOpenPublish}
|
||||
publishHandler={publishHandler}
|
||||
mySubtitles={mySubtitles}
|
||||
/>
|
||||
|
||||
</>
|
||||
// <Dialog
|
||||
// open={!!open}
|
||||
|
@ -340,6 +340,7 @@ export const PlaybackRate = ({
|
||||
increaseSpeed,
|
||||
isScreenSmall,
|
||||
onSelect,
|
||||
openPlaybackMenu
|
||||
}: any) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const btnRef = useRef(null);
|
||||
@ -362,130 +363,13 @@ export const PlaybackRate = ({
|
||||
fontSize: fontSizeSmall,
|
||||
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
onClick={() => openPlaybackMenu()}
|
||||
>
|
||||
<SlowMotionVideoIcon />
|
||||
</IconButton>
|
||||
</CustomFontTooltip>
|
||||
|
||||
<Popover
|
||||
open={isOpen}
|
||||
anchorEl={btnRef.current}
|
||||
onClose={() => setIsOpen(false)}
|
||||
slots={{
|
||||
transition: Fade,
|
||||
}}
|
||||
slotProps={{
|
||||
transition: {
|
||||
timeout: 200,
|
||||
},
|
||||
paper: {
|
||||
sx: {
|
||||
bgcolor: alpha("#181818", 0.98),
|
||||
color: "white",
|
||||
opacity: 0.9,
|
||||
borderRadius: 2,
|
||||
boxShadow: 5,
|
||||
p: 1,
|
||||
minWidth: 225,
|
||||
height: 300,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "5px 0px 10px 0px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={onBack}>
|
||||
<ArrowBackIosIcon
|
||||
sx={{
|
||||
fontSize: "1.15em",
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<ButtonBase>
|
||||
<Typography
|
||||
onClick={onBack}
|
||||
sx={{
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
Playback speed
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
"::-webkit-scrollbar-track": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
|
||||
"::-webkit-scrollbar": {
|
||||
width: "16px",
|
||||
height: "10px",
|
||||
},
|
||||
|
||||
"::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: "8px",
|
||||
backgroundClip: "content-box",
|
||||
border: "4px solid transparent",
|
||||
transition: "0.3s background-color",
|
||||
},
|
||||
|
||||
"::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{speeds?.map((speed) => {
|
||||
const isSelected = speed === playbackRate;
|
||||
return (
|
||||
<ButtonBase
|
||||
disabled={isSelected}
|
||||
key={speed}
|
||||
onClick={() => {
|
||||
onSelect(speed)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1,
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>{speed}</Typography>
|
||||
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />}
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -551,3 +435,138 @@ export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
|
||||
</CustomFontTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
interface PlayBackMenuProps {
|
||||
close: ()=> void
|
||||
isOpen: boolean
|
||||
onSelect: (speed: number)=> void;
|
||||
playbackRate: number
|
||||
}
|
||||
export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMenuProps)=> {
|
||||
const theme = useTheme()
|
||||
const ref = useRef<any>(null)
|
||||
console.log('isOpen', isOpen)
|
||||
|
||||
useEffect(()=> {
|
||||
if(isOpen){
|
||||
ref?.current?.focus()
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleBlur = (e: React.FocusEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
if(!isOpen) return null
|
||||
return (
|
||||
|
||||
<Box
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
onBlur={handleBlur}
|
||||
bgcolor={alpha("#181818", 0.98)}
|
||||
|
||||
sx={
|
||||
{
|
||||
position: 'absolute',
|
||||
bottom: 60,
|
||||
right: 5,
|
||||
color: "white",
|
||||
opacity: 0.9,
|
||||
borderRadius: 2,
|
||||
boxShadow: 5,
|
||||
p: 1,
|
||||
minWidth: 225,
|
||||
height: 300,
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
zIndex: 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
padding: "5px 0px 10px 0px",
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ButtonBase onClick={close}>
|
||||
<ArrowBackIosIcon
|
||||
sx={{
|
||||
fontSize: "1.15em",
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<ButtonBase>
|
||||
<Typography
|
||||
onClick={close}
|
||||
sx={{
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
Playback speed
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
"::-webkit-scrollbar-track": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
|
||||
"::-webkit-scrollbar": {
|
||||
width: "16px",
|
||||
height: "10px",
|
||||
},
|
||||
|
||||
"::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
borderRadius: "8px",
|
||||
backgroundClip: "content-box",
|
||||
border: "4px solid transparent",
|
||||
transition: "0.3s background-color",
|
||||
},
|
||||
|
||||
"::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{speeds?.map((speed) => {
|
||||
const isSelected = speed === playbackRate;
|
||||
return (
|
||||
<ButtonBase
|
||||
disabled={isSelected}
|
||||
key={speed}
|
||||
onClick={(e) => {
|
||||
onSelect(speed)
|
||||
close()
|
||||
}}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 1,
|
||||
"&:hover": {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>{speed}</Typography>
|
||||
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />}
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
@ -40,9 +40,11 @@ interface VideoControlsBarProps {
|
||||
onSelectPlaybackRate: (rate: number)=> void;
|
||||
isMuted: boolean
|
||||
toggleMute: ()=> void
|
||||
openPlaybackMenu: ()=> void
|
||||
togglePictureInPicture: ()=> void
|
||||
}
|
||||
|
||||
export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager, onSelectPlaybackRate, isMuted, toggleMute}: VideoControlsBarProps) => {
|
||||
export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager, onSelectPlaybackRate, isMuted, toggleMute, openPlaybackMenu, togglePictureInPicture}: VideoControlsBarProps) => {
|
||||
|
||||
const showMobileControls = isScreenSmall && canPlay;
|
||||
|
||||
@ -100,7 +102,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
|
||||
</Box>
|
||||
|
||||
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}>
|
||||
<PlaybackRate onSelect={onSelectPlaybackRate} playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
|
||||
<PlaybackRate openPlaybackMenu={openPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
|
||||
{/* <ObjectFitButton /> */}
|
||||
<CustomFontTooltip
|
||||
title="Subtitles"
|
||||
@ -111,7 +113,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
|
||||
<SubtitlesIcon />
|
||||
</IconButton>
|
||||
</CustomFontTooltip>
|
||||
{/* <PictureInPictureButton /> */}
|
||||
<PictureInPictureButton togglePictureInPicture={togglePictureInPicture} />
|
||||
<FullscreenButton toggleFullscreen={toggleFullscreen} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
@ -4,6 +4,7 @@ import {
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@ -28,6 +29,9 @@ import {
|
||||
import { base64ToBlobUrl } from "../../utils/base64";
|
||||
import convert from "srt-webvtt";
|
||||
import { TimelineActionsComponent } from "./TimelineActionsComponent";
|
||||
import { PlayBackMenu } from "./VideoControls";
|
||||
import { useGlobalPlayerStore } from "../../state/pip";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
export async function srtBase64ToVttBlobUrl(
|
||||
base64Srt: string
|
||||
@ -208,6 +212,9 @@ export const VideoPlayer = ({
|
||||
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
|
||||
const subtitleBtnRef = useRef(null);
|
||||
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
|
||||
const location = useLocation();
|
||||
const locationRef = useRef<string | null>(null)
|
||||
const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false)
|
||||
const {
|
||||
reloadVideo,
|
||||
togglePlay,
|
||||
@ -231,17 +238,27 @@ export const VideoPlayer = ({
|
||||
percentLoaded,
|
||||
showControlsFullScreen,
|
||||
onSelectPlaybackRate,
|
||||
seekTo
|
||||
seekTo,
|
||||
togglePictureInPicture
|
||||
} = useVideoPlayerController({
|
||||
autoPlay,
|
||||
playerRef,
|
||||
qortalVideoResource,
|
||||
retryAttempts,
|
||||
isPlayerInitialized,
|
||||
isMuted
|
||||
isMuted,
|
||||
videoRef
|
||||
});
|
||||
|
||||
useEffect(()=> {
|
||||
if(location){
|
||||
locationRef.current = location.pathname
|
||||
|
||||
}
|
||||
},[location])
|
||||
|
||||
console.log('isFullscreen', isFullscreen)
|
||||
const { getProgress } = useProgressStore();
|
||||
|
||||
const enterFullscreen = useCallback(() => {
|
||||
const ref = containerRef?.current as any;
|
||||
@ -304,6 +321,10 @@ export const VideoPlayer = ({
|
||||
if (!qortalVideoResource) return null;
|
||||
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
||||
}, [qortalVideoResource]);
|
||||
const videoLocationRef = useRef< null | string>(null)
|
||||
useEffect(()=> {
|
||||
videoLocationRef.current = videoLocation
|
||||
}, [videoLocation])
|
||||
useVideoPlayerHotKeys(hotkeyHandlers);
|
||||
|
||||
const updateProgress = useCallback(() => {
|
||||
@ -316,6 +337,15 @@ export const VideoPlayer = ({
|
||||
setLocalProgress(currentTime);
|
||||
}
|
||||
}, [videoLocation]);
|
||||
|
||||
useEffect(()=> {
|
||||
if(videoLocation){
|
||||
const vidId = useGlobalPlayerStore.getState().videoId
|
||||
if(vidId === videoLocation){
|
||||
togglePlay()
|
||||
}
|
||||
}
|
||||
}, [videoLocation])
|
||||
// useEffect(() => {
|
||||
// const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
||||
// if (!ref.current) return;
|
||||
@ -442,6 +472,13 @@ export const VideoPlayer = ({
|
||||
resetHideTimer();
|
||||
};
|
||||
|
||||
const closePlaybackMenu = useCallback(()=> {
|
||||
setIsOpenPlaybackmenu(false)
|
||||
}, [])
|
||||
const openPlaybackMenu = useCallback(()=> {
|
||||
setIsOpenPlaybackmenu(true)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
resetHideTimer(); // initial show
|
||||
return () => {
|
||||
@ -593,6 +630,24 @@ export const VideoPlayer = ({
|
||||
return JSON.stringify(qortalVideoResource);
|
||||
}, [qortalVideoResource]);
|
||||
|
||||
|
||||
const savedVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||
|
||||
useEffect(()=> {
|
||||
if(startPlay){
|
||||
useGlobalPlayerStore.getState().reset()
|
||||
|
||||
}
|
||||
}, [startPlay])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Save the video element while it's still mounted
|
||||
const video = videoRef as any
|
||||
if (video.current) {
|
||||
savedVideoRef.current = video.current;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
|
||||
return;
|
||||
@ -630,6 +685,13 @@ export const VideoPlayer = ({
|
||||
playerRef.current?.poster("");
|
||||
playerRef.current?.playbackRate(playbackRate);
|
||||
playerRef.current?.volume(volume);
|
||||
if(videoLocationRef.current){
|
||||
const savedProgress = getProgress(videoLocationRef.current);
|
||||
if (typeof savedProgress === "number") {
|
||||
playerRef.current?.currentTime(savedProgress);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
playerRef.current?.play();
|
||||
|
||||
@ -684,9 +746,31 @@ export const VideoPlayer = ({
|
||||
console.error("useEffect start player", error);
|
||||
}
|
||||
return () => {
|
||||
canceled = true;
|
||||
console.log('hello1002')
|
||||
const video = savedVideoRef as any
|
||||
const videoEl = video?.current!;
|
||||
const player = playerRef.current;
|
||||
|
||||
console.log('videohello', videoEl);
|
||||
const isPlaying = !player?.paused();
|
||||
|
||||
if (videoEl && isPlaying && videoLocationRef.current) {
|
||||
const current = player?.currentTime?.();
|
||||
const currentSource = player?.currentType();
|
||||
|
||||
useGlobalPlayerStore.getState().setVideoState({
|
||||
videoSrc: videoEl.src,
|
||||
currentTime: current ?? 0,
|
||||
isPlaying: true,
|
||||
mode: 'floating',
|
||||
videoId: videoLocationRef.current,
|
||||
location: locationRef.current || "",
|
||||
type: currentSource || 'video/mp4'
|
||||
});
|
||||
}
|
||||
|
||||
canceled = true;
|
||||
|
||||
if (player && typeof player.dispose === "function") {
|
||||
try {
|
||||
player.dispose();
|
||||
@ -753,6 +837,7 @@ export const VideoPlayer = ({
|
||||
onVolumeChange={onVolumeChangeHandler}
|
||||
controls={false}
|
||||
/>
|
||||
<PlayBackMenu close={closePlaybackMenu} isOpen={isOpenPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} />
|
||||
{/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
|
||||
|
||||
{isReady && (
|
||||
@ -781,11 +866,12 @@ export const VideoPlayer = ({
|
||||
onSelectPlaybackRate={onSelectPlaybackRate}
|
||||
isMuted={isMuted}
|
||||
toggleMute={toggleMute}
|
||||
openPlaybackMenu={openPlaybackMenu}
|
||||
togglePictureInPicture={togglePictureInPicture}
|
||||
/>
|
||||
)}
|
||||
{timelineActions && Array.isArray(timelineActions) && (
|
||||
<TimelineActionsComponent seekTo={seekTo} containerRef={containerRef} progress={localProgress} timelineActions={timelineActions}/>
|
||||
|
||||
)}
|
||||
<SubtitleManager
|
||||
subtitleBtnRef={subtitleBtnRef}
|
||||
|
@ -12,6 +12,8 @@ import { useProgressStore, useVideoStore } from "../../state/video";
|
||||
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
||||
import { useResourceStatus } from "../../hooks/useResourceStatus";
|
||||
import useIdleTimeout from "../../common/useIdleTimeout";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useGlobalPlayerStore } from "../../state/pip";
|
||||
|
||||
const controlsHeight = "42px";
|
||||
const minSpeed = 0.25;
|
||||
@ -25,10 +27,11 @@ interface UseVideoControls {
|
||||
retryAttempts?: number;
|
||||
isPlayerInitialized: boolean
|
||||
isMuted: boolean
|
||||
videoRef: any
|
||||
}
|
||||
|
||||
export const useVideoPlayerController = (props: UseVideoControls) => {
|
||||
const { autoPlay, playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, isMuted } = props;
|
||||
const { autoPlay, videoRef , playerRef, qortalVideoResource, retryAttempts, isPlayerInitialized, isMuted } = props;
|
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false)
|
||||
@ -39,7 +42,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
||||
const [startPlay, setStartPlay] = useState(false);
|
||||
const [startedFetch, setStartedFetch] = useState(false);
|
||||
const startedFetchRef = useRef(false);
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
|
||||
const { getProgress } = useProgressStore();
|
||||
|
||||
@ -61,22 +64,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
||||
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
||||
}, [qortalVideoResource]);
|
||||
|
||||
useEffect(() => {
|
||||
if (videoLocation && isPlayerInitialized) {
|
||||
try {
|
||||
const ref = playerRef as any;
|
||||
if (!ref.current) return;
|
||||
|
||||
const savedProgress = getProgress(videoLocation);
|
||||
if (typeof savedProgress === "number") {
|
||||
playerRef.current?.currentTime(savedProgress);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('line 74', error)
|
||||
}
|
||||
}
|
||||
}, [videoLocation, getProgress, isPlayerInitialized]);
|
||||
|
||||
const [playbackRate, _setLocalPlaybackRate] = useState(
|
||||
playbackSettings.playbackRate
|
||||
@ -294,6 +282,33 @@ const togglePlay = useCallback(async () => {
|
||||
}
|
||||
}, [togglePlay, isReady]);
|
||||
|
||||
// videoRef?.current?.addEventListener("enterpictureinpicture", () => {
|
||||
// setPipVideoPath(window.location.pathname);
|
||||
|
||||
// });
|
||||
|
||||
// // when PiP ends (and you're on the wrong page), go back
|
||||
// videoRef?.current?.addEventListener("leavepictureinpicture", () => {
|
||||
// const { pipVideoPath } = usePipStore.getState();
|
||||
// if (pipVideoPath && window.location.pathname !== pipVideoPath) {
|
||||
// navigate(pipVideoPath);
|
||||
// }
|
||||
// });
|
||||
|
||||
const togglePictureInPicture = async () => {
|
||||
if (!videoRef.current) return;
|
||||
const player = playerRef.current;
|
||||
if (!player || typeof player.currentTime !== 'function' || typeof player.duration !== 'function') return;
|
||||
|
||||
const current = player.currentTime();
|
||||
useGlobalPlayerStore.getState().setVideoState({
|
||||
videoSrc: videoRef.current.src,
|
||||
currentTime: current,
|
||||
isPlaying: true,
|
||||
mode: 'floating', // or 'floating'
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
reloadVideo,
|
||||
@ -314,6 +329,6 @@ const togglePlay = useCallback(async () => {
|
||||
isReady,
|
||||
resourceUrl,
|
||||
startPlay,
|
||||
status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo
|
||||
status, percentLoaded, showControlsFullScreen, onSelectPlaybackRate: updatePlaybackRate, seekTo, togglePictureInPicture
|
||||
};
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import { usePersistentStore } from "../hooks/usePersistentStore";
|
||||
import { IndexManager } from "../components/IndexManager/IndexManager";
|
||||
import { useIndexes } from "../hooks/useIndexes";
|
||||
import { useProgressStore } from "../state/video";
|
||||
import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer";
|
||||
|
||||
// ✅ Define Global Context Type
|
||||
interface GlobalContextType {
|
||||
@ -80,6 +81,7 @@ export const GlobalProvider = ({
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={contextValue}>
|
||||
<GlobalPipPlayer />
|
||||
<Toaster
|
||||
position="top-center"
|
||||
toastOptions={{
|
||||
|
323
src/hooks/useGlobalPipPlayer.tsx
Normal file
323
src/hooks/useGlobalPipPlayer.tsx
Normal file
@ -0,0 +1,323 @@
|
||||
// GlobalVideoPlayer.tsx
|
||||
import videojs from 'video.js';
|
||||
import { useGlobalPlayerStore } from '../state/pip';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Box, IconButton } from '@mui/material';
|
||||
import { VideoContainer } from '../components/VideoPlayer/VideoPlayer-styles';
|
||||
import { Rnd } from "react-rnd";
|
||||
import { useProgressStore } from '../state/video';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import PauseIcon from '@mui/icons-material/Pause';
|
||||
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
export const GlobalPipPlayer = () => {
|
||||
const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore();
|
||||
const [playing , setPlaying] = useState(false)
|
||||
const [hasStarted, setHasStarted] = useState(false)
|
||||
const playerRef = useRef<any>(null);
|
||||
const navigate = useNavigate()
|
||||
const videoNode = useRef<HTMLVideoElement>(null);
|
||||
const { setProgress } = useProgressStore();
|
||||
|
||||
const updateProgress = useCallback(() => {
|
||||
const player = playerRef?.current;
|
||||
if (!player || typeof player?.currentTime !== "function") return;
|
||||
|
||||
const currentTime = player.currentTime();
|
||||
console.log('videoId', videoId)
|
||||
if (typeof currentTime === "number" && videoId && currentTime > 0.1) {
|
||||
setProgress(videoId, currentTime);
|
||||
}
|
||||
}, [videoId]);
|
||||
|
||||
const rndRef = useRef<any>(null)
|
||||
useEffect(() => {
|
||||
if (!playerRef.current && videoNode.current) {
|
||||
playerRef.current = videojs(videoNode.current, { autoplay: true, controls: false,
|
||||
responsive: true, fluid: true });
|
||||
|
||||
// Resume playback if needed
|
||||
playerRef.current.on('ready', () => {
|
||||
if (videoSrc) {
|
||||
|
||||
playerRef.current.src(videoSrc);
|
||||
playerRef.current.currentTime(currentTime);
|
||||
if (isPlaying) playerRef.current.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
// optional: don't destroy, just hide
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(()=> {
|
||||
if(!videoSrc){
|
||||
setHasStarted(false)
|
||||
}
|
||||
}, [videoSrc])
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
|
||||
if (!player) return;
|
||||
|
||||
if (!videoSrc && player.src) {
|
||||
// Only pause the player and unload the source without re-triggering playback
|
||||
player.pause();
|
||||
|
||||
// Remove the video source safely
|
||||
const tech = player.tech({ IWillNotUseThisInPlugins: true });
|
||||
if (tech && tech.el_) {
|
||||
tech.setAttribute('src', '');
|
||||
setPlaying(false)
|
||||
setHasStarted(false)
|
||||
}
|
||||
|
||||
// Optionally clear the poster and currentTime
|
||||
player.poster('');
|
||||
player.currentTime(0);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if(videoSrc){
|
||||
// Set source and resume if needed
|
||||
player.src({ src: videoSrc, type: type });
|
||||
player.currentTime(currentTime);
|
||||
|
||||
if (isPlaying) {
|
||||
const playPromise = player.play();
|
||||
|
||||
|
||||
if (playPromise?.catch) {
|
||||
playPromise.catch((err: any) => {
|
||||
console.warn('Unable to autoplay:', err);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
}, [videoSrc, type, isPlaying, currentTime]);
|
||||
|
||||
|
||||
// const onDragStart = () => {
|
||||
// timer = Date.now();
|
||||
// isDragging.current = true;
|
||||
// };
|
||||
|
||||
// const handleStopDrag = async () => {
|
||||
// const time = Date.now();
|
||||
// if (timer && time - timer < 300) {
|
||||
// isDragging.current = false;
|
||||
// } else {
|
||||
// isDragging.current = true;
|
||||
// }
|
||||
// };
|
||||
// const onDragStop = () => {
|
||||
// handleStopDrag();
|
||||
// };
|
||||
|
||||
// const checkIfDrag = useCallback(() => {
|
||||
// return isDragging.current;
|
||||
// }, []);
|
||||
const margin = 50;
|
||||
|
||||
|
||||
const [height, setHeight] = useState(300)
|
||||
const [width, setWidth] = useState(400)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
rndRef.current.updatePosition({
|
||||
x: window.innerWidth - 400 - margin,
|
||||
y: window.innerHeight - 300 - margin,
|
||||
width: 400,
|
||||
height: 300
|
||||
});
|
||||
}, [videoSrc]);
|
||||
|
||||
const [showControls, setShowControls] = useState(false)
|
||||
|
||||
const handleMouseMove = () => {
|
||||
setShowControls(true)
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setShowControls(false);
|
||||
};
|
||||
const startPlay = useCallback(() => {
|
||||
try {
|
||||
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
|
||||
try {
|
||||
player.play();
|
||||
} catch (err) {
|
||||
console.warn('Play failed:', err);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('togglePlay', error)
|
||||
}
|
||||
}, []);
|
||||
|
||||
const stopPlay = useCallback(() => {
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
try {
|
||||
player.pause();
|
||||
} catch (err) {
|
||||
console.warn('Play failed:', err);
|
||||
}
|
||||
|
||||
|
||||
}, []);
|
||||
|
||||
const onPlayHandlerStart = useCallback(() => {
|
||||
setPlaying(true)
|
||||
setHasStarted(true)
|
||||
}, [setPlaying]);
|
||||
const onPlayHandlerStop = useCallback(() => {
|
||||
setPlaying(false)
|
||||
}, [setPlaying]);
|
||||
|
||||
return (
|
||||
<Rnd
|
||||
enableResizing={{
|
||||
top: false,
|
||||
right: false,
|
||||
bottom: false,
|
||||
left: false,
|
||||
topRight: true,
|
||||
bottomLeft: true,
|
||||
topLeft: true,
|
||||
bottomRight: true,
|
||||
}}
|
||||
|
||||
ref={rndRef}
|
||||
// onDragStart={onDragStart}
|
||||
// onDragStop={onDragStop}
|
||||
style={{
|
||||
display: hasStarted ? "block" : "none",
|
||||
position: "fixed",
|
||||
zIndex: 999999999,
|
||||
|
||||
cursor: 'default'
|
||||
}}
|
||||
size={{ width, height }}
|
||||
onResize={(e, direction, ref, delta, position) => {
|
||||
setWidth(ref.offsetWidth);
|
||||
setHeight(ref.offsetHeight);
|
||||
}}
|
||||
|
||||
// default={{
|
||||
// x: 500,
|
||||
// y: 500,
|
||||
// width: 350,
|
||||
// height: "auto",
|
||||
// }}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
onDrag={() => {}}
|
||||
>
|
||||
{/* <div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px',
|
||||
// width: '100px',
|
||||
// height: '100px',
|
||||
zIndex: 9999,
|
||||
display: videoSrc ? 'block' : 'none'
|
||||
}}
|
||||
> */}
|
||||
<Box sx={{height, width, position: 'relative' , background: 'black', overflow: 'hidden', borderRadius: '10px' }} onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}>
|
||||
{/* {backgroundColor: showControls ? 'rgba(0,0,0,.5)' : 'unset'} */}
|
||||
{showControls && (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
top: 0, bottom: 0, left: 0, right: 0,
|
||||
zIndex: 1,
|
||||
opacity: 0,
|
||||
transition: 'opacity 1s',
|
||||
"&:hover": {
|
||||
opacity: 1
|
||||
}
|
||||
}}>
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
background: 'rgba(0,0,0,.5)',
|
||||
top: 0, bottom: 0, left: 0, right: 0,
|
||||
zIndex: 1,
|
||||
opacity: 0,
|
||||
transition: 'opacity 1s',
|
||||
"&:hover": {
|
||||
opacity: 1
|
||||
}
|
||||
}} />
|
||||
<IconButton sx={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
opacity: 1,
|
||||
right: 10,
|
||||
zIndex: 2,
|
||||
|
||||
}} onClick={reset}><CloseIcon /></IconButton>
|
||||
{location && (
|
||||
<IconButton sx={{
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
zIndex: 2,
|
||||
opacity: 1,
|
||||
|
||||
}} onClick={()=> navigate(location)}><OpenInFullIcon /></IconButton>
|
||||
)}
|
||||
{playing && (
|
||||
<IconButton sx={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
opacity: 1,
|
||||
zIndex: 2,
|
||||
|
||||
}} onClick={stopPlay}><PauseIcon /></IconButton>
|
||||
)}
|
||||
{!playing && (
|
||||
<IconButton sx={{
|
||||
position: 'absolute',
|
||||
opacity: 1,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 2,
|
||||
|
||||
}} onClick={startPlay}><PlayArrowIcon /></IconButton>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
<Box/>
|
||||
</Box>
|
||||
)}
|
||||
<VideoContainer>
|
||||
<video onPlay={onPlayHandlerStart} onPause={onPlayHandlerStop} onTimeUpdate={updateProgress}
|
||||
ref={videoNode} className="video-js" style={{
|
||||
|
||||
|
||||
}}/>
|
||||
</VideoContainer>
|
||||
</Box>
|
||||
{/* </div> */}
|
||||
</Rnd>
|
||||
);
|
||||
};
|
30
src/state/pip.ts
Normal file
30
src/state/pip.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
type PlayerMode = 'embedded' | 'floating' | 'none';
|
||||
|
||||
interface GlobalPlayerState {
|
||||
videoSrc: string | null;
|
||||
videoId: string,
|
||||
isPlaying: boolean;
|
||||
currentTime: number;
|
||||
location: string;
|
||||
mode: PlayerMode;
|
||||
setVideoState: (state: Partial<GlobalPlayerState>) => void;
|
||||
type: string,
|
||||
reset: ()=> void;
|
||||
}
|
||||
const initialState = {
|
||||
videoSrc: null,
|
||||
videoId: "",
|
||||
location: "",
|
||||
isPlaying: false,
|
||||
currentTime: 0,
|
||||
type: 'video/mp4',
|
||||
mode: 'embedded' as const,
|
||||
};
|
||||
export const useGlobalPlayerStore = create<GlobalPlayerState>((set) => ({
|
||||
...initialState,
|
||||
setVideoState: (state) => set((prev) => ({ ...prev, ...state })),
|
||||
reset: () => set(initialState),
|
||||
|
||||
}));
|
Loading…
x
Reference in New Issue
Block a user