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-hot-toast": "^2.5.2",
|
||||||
"react-idle-timer": "^5.7.2",
|
"react-idle-timer": "^5.7.2",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
|
"react-rnd": "^10.5.2",
|
||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"srt-webvtt": "^2.0.0",
|
"srt-webvtt": "^2.0.0",
|
||||||
"ts-key-enum": "^3.0.13",
|
"ts-key-enum": "^3.0.13",
|
||||||
@ -38,6 +39,8 @@
|
|||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"cpy-cli": "^5.0.0",
|
"cpy-cli": "^5.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.2",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"typescript": "^5.2.0"
|
"typescript": "^5.2.0"
|
||||||
},
|
},
|
||||||
@ -47,7 +50,9 @@
|
|||||||
"@mui/icons-material": "^7.0.1",
|
"@mui/icons-material": "^7.0.1",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.0.1",
|
||||||
"mediainfo.js": "^0.3.5",
|
"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": {
|
"node_modules/@babel/code-frame": {
|
||||||
@ -1905,6 +1910,16 @@
|
|||||||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/cosmiconfig": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
|
"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": {
|
"node_modules/react": {
|
||||||
"version": "19.0.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "19.0.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"peer": true,
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.25.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"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": {
|
"node_modules/react-dropzone": {
|
||||||
@ -3276,6 +3325,67 @@
|
|||||||
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
|
"integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
@ -3427,16 +3537,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.25.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||||
"peer": true
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/seedrandom": {
|
"node_modules/seedrandom": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||||
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-idle-timer": "^5.7.2",
|
"react-idle-timer": "^5.7.2",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
|
"react-rnd": "^10.5.2",
|
||||||
"short-unique-id": "^5.2.0",
|
"short-unique-id": "^5.2.0",
|
||||||
"srt-webvtt": "^2.0.0",
|
"srt-webvtt": "^2.0.0",
|
||||||
"ts-key-enum": "^3.0.13",
|
"ts-key-enum": "^3.0.13",
|
||||||
@ -49,7 +50,9 @@
|
|||||||
"@mui/icons-material": "^7.0.1",
|
"@mui/icons-material": "^7.0.1",
|
||||||
"@mui/material": "^7.0.1",
|
"@mui/material": "^7.0.1",
|
||||||
"mediainfo.js": "^0.3.5",
|
"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": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
@ -60,6 +63,8 @@
|
|||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"cpy-cli": "^5.0.0",
|
"cpy-cli": "^5.0.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.2",
|
||||||
"tsup": "^8.4.0",
|
"tsup": "^8.4.0",
|
||||||
"typescript": "^5.2.0"
|
"typescript": "^5.2.0"
|
||||||
},
|
},
|
||||||
|
@ -173,13 +173,22 @@ const SubtitleManagerComponent = ({
|
|||||||
getPublishedSubtitles,
|
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();
|
close();
|
||||||
setMode(1);
|
setIsOpenPublish(false)
|
||||||
// setTitle("");
|
}
|
||||||
// setDescription("");
|
};
|
||||||
// setHasMetadata(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const publishHandler = async (subtitles: Subtitle[]) => {
|
const publishHandler = async (subtitles: Subtitle[]) => {
|
||||||
try {
|
try {
|
||||||
@ -250,23 +259,20 @@ const SubtitleManagerComponent = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
if(!open) return
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Popover
|
<Box
|
||||||
open={!!open}
|
ref={ref}
|
||||||
anchorEl={subtitleBtnRef.current}
|
tabIndex={-1}
|
||||||
onClose={handleClose}
|
onBlur={handleBlur}
|
||||||
slots={{
|
bgcolor={alpha("#181818", 0.98)}
|
||||||
transition: Fade,
|
|
||||||
}}
|
sx={
|
||||||
slotProps={{
|
{
|
||||||
transition: {
|
position: 'absolute',
|
||||||
timeout: 200,
|
bottom: 60,
|
||||||
},
|
right: 5,
|
||||||
paper: {
|
|
||||||
sx: {
|
|
||||||
bgcolor: alpha("#181818", 0.98),
|
|
||||||
color: "white",
|
color: "white",
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
borderRadius: 2,
|
borderRadius: 2,
|
||||||
@ -277,17 +283,9 @@ const SubtitleManagerComponent = ({
|
|||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
},
|
zIndex: 10,
|
||||||
},
|
}
|
||||||
}}
|
}
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "center",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "center",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -421,14 +419,15 @@ const SubtitleManagerComponent = ({
|
|||||||
</Typography>
|
</Typography>
|
||||||
))}
|
))}
|
||||||
</Box> */}
|
</Box> */}
|
||||||
</Popover>
|
|
||||||
|
|
||||||
|
</Box>
|
||||||
<PublishSubtitles
|
<PublishSubtitles
|
||||||
isOpen={isOpenPublish}
|
isOpen={isOpenPublish}
|
||||||
setIsOpen={setIsOpenPublish}
|
setIsOpen={setIsOpenPublish}
|
||||||
publishHandler={publishHandler}
|
publishHandler={publishHandler}
|
||||||
mySubtitles={mySubtitles}
|
mySubtitles={mySubtitles}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
// <Dialog
|
// <Dialog
|
||||||
// open={!!open}
|
// open={!!open}
|
||||||
|
@ -340,6 +340,7 @@ export const PlaybackRate = ({
|
|||||||
increaseSpeed,
|
increaseSpeed,
|
||||||
isScreenSmall,
|
isScreenSmall,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
openPlaybackMenu
|
||||||
}: any) => {
|
}: any) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const btnRef = useRef(null);
|
const btnRef = useRef(null);
|
||||||
@ -362,130 +363,13 @@ export const PlaybackRate = ({
|
|||||||
fontSize: fontSizeSmall,
|
fontSize: fontSizeSmall,
|
||||||
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||||
}}
|
}}
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => openPlaybackMenu()}
|
||||||
>
|
>
|
||||||
<SlowMotionVideoIcon />
|
<SlowMotionVideoIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</CustomFontTooltip>
|
</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>
|
</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;
|
onSelectPlaybackRate: (rate: number)=> void;
|
||||||
isMuted: boolean
|
isMuted: boolean
|
||||||
toggleMute: ()=> void
|
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;
|
const showMobileControls = isScreenSmall && canPlay;
|
||||||
|
|
||||||
@ -100,7 +102,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box sx={{...controlGroupSX, marginLeft: 'auto'}}>
|
<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 /> */}
|
{/* <ObjectFitButton /> */}
|
||||||
<CustomFontTooltip
|
<CustomFontTooltip
|
||||||
title="Subtitles"
|
title="Subtitles"
|
||||||
@ -111,7 +113,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
|
|||||||
<SubtitlesIcon />
|
<SubtitlesIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</CustomFontTooltip>
|
</CustomFontTooltip>
|
||||||
{/* <PictureInPictureButton /> */}
|
<PictureInPictureButton togglePictureInPicture={togglePictureInPicture} />
|
||||||
<FullscreenButton toggleFullscreen={toggleFullscreen} />
|
<FullscreenButton toggleFullscreen={toggleFullscreen} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
RefObject,
|
RefObject,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -28,6 +29,9 @@ import {
|
|||||||
import { base64ToBlobUrl } from "../../utils/base64";
|
import { base64ToBlobUrl } from "../../utils/base64";
|
||||||
import convert from "srt-webvtt";
|
import convert from "srt-webvtt";
|
||||||
import { TimelineActionsComponent } from "./TimelineActionsComponent";
|
import { TimelineActionsComponent } from "./TimelineActionsComponent";
|
||||||
|
import { PlayBackMenu } from "./VideoControls";
|
||||||
|
import { useGlobalPlayerStore } from "../../state/pip";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
export async function srtBase64ToVttBlobUrl(
|
export async function srtBase64ToVttBlobUrl(
|
||||||
base64Srt: string
|
base64Srt: string
|
||||||
@ -208,6 +212,9 @@ export const VideoPlayer = ({
|
|||||||
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
|
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
|
||||||
const subtitleBtnRef = useRef(null);
|
const subtitleBtnRef = useRef(null);
|
||||||
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
|
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
|
||||||
|
const location = useLocation();
|
||||||
|
const locationRef = useRef<string | null>(null)
|
||||||
|
const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false)
|
||||||
const {
|
const {
|
||||||
reloadVideo,
|
reloadVideo,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
@ -231,17 +238,27 @@ export const VideoPlayer = ({
|
|||||||
percentLoaded,
|
percentLoaded,
|
||||||
showControlsFullScreen,
|
showControlsFullScreen,
|
||||||
onSelectPlaybackRate,
|
onSelectPlaybackRate,
|
||||||
seekTo
|
seekTo,
|
||||||
|
togglePictureInPicture
|
||||||
} = useVideoPlayerController({
|
} = useVideoPlayerController({
|
||||||
autoPlay,
|
autoPlay,
|
||||||
playerRef,
|
playerRef,
|
||||||
qortalVideoResource,
|
qortalVideoResource,
|
||||||
retryAttempts,
|
retryAttempts,
|
||||||
isPlayerInitialized,
|
isPlayerInitialized,
|
||||||
isMuted
|
isMuted,
|
||||||
|
videoRef
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(location){
|
||||||
|
locationRef.current = location.pathname
|
||||||
|
|
||||||
|
}
|
||||||
|
},[location])
|
||||||
|
|
||||||
console.log('isFullscreen', isFullscreen)
|
console.log('isFullscreen', isFullscreen)
|
||||||
|
const { getProgress } = useProgressStore();
|
||||||
|
|
||||||
const enterFullscreen = useCallback(() => {
|
const enterFullscreen = useCallback(() => {
|
||||||
const ref = containerRef?.current as any;
|
const ref = containerRef?.current as any;
|
||||||
@ -304,6 +321,10 @@ export const VideoPlayer = ({
|
|||||||
if (!qortalVideoResource) return null;
|
if (!qortalVideoResource) return null;
|
||||||
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
||||||
}, [qortalVideoResource]);
|
}, [qortalVideoResource]);
|
||||||
|
const videoLocationRef = useRef< null | string>(null)
|
||||||
|
useEffect(()=> {
|
||||||
|
videoLocationRef.current = videoLocation
|
||||||
|
}, [videoLocation])
|
||||||
useVideoPlayerHotKeys(hotkeyHandlers);
|
useVideoPlayerHotKeys(hotkeyHandlers);
|
||||||
|
|
||||||
const updateProgress = useCallback(() => {
|
const updateProgress = useCallback(() => {
|
||||||
@ -316,6 +337,15 @@ export const VideoPlayer = ({
|
|||||||
setLocalProgress(currentTime);
|
setLocalProgress(currentTime);
|
||||||
}
|
}
|
||||||
}, [videoLocation]);
|
}, [videoLocation]);
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(videoLocation){
|
||||||
|
const vidId = useGlobalPlayerStore.getState().videoId
|
||||||
|
if(vidId === videoLocation){
|
||||||
|
togglePlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [videoLocation])
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
// const ref = videoRef as React.RefObject<HTMLVideoElement>;
|
||||||
// if (!ref.current) return;
|
// if (!ref.current) return;
|
||||||
@ -442,6 +472,13 @@ export const VideoPlayer = ({
|
|||||||
resetHideTimer();
|
resetHideTimer();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closePlaybackMenu = useCallback(()=> {
|
||||||
|
setIsOpenPlaybackmenu(false)
|
||||||
|
}, [])
|
||||||
|
const openPlaybackMenu = useCallback(()=> {
|
||||||
|
setIsOpenPlaybackmenu(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetHideTimer(); // initial show
|
resetHideTimer(); // initial show
|
||||||
return () => {
|
return () => {
|
||||||
@ -593,6 +630,24 @@ export const VideoPlayer = ({
|
|||||||
return JSON.stringify(qortalVideoResource);
|
return JSON.stringify(qortalVideoResource);
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
|
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
|
||||||
return;
|
return;
|
||||||
@ -630,6 +685,13 @@ export const VideoPlayer = ({
|
|||||||
playerRef.current?.poster("");
|
playerRef.current?.poster("");
|
||||||
playerRef.current?.playbackRate(playbackRate);
|
playerRef.current?.playbackRate(playbackRate);
|
||||||
playerRef.current?.volume(volume);
|
playerRef.current?.volume(volume);
|
||||||
|
if(videoLocationRef.current){
|
||||||
|
const savedProgress = getProgress(videoLocationRef.current);
|
||||||
|
if (typeof savedProgress === "number") {
|
||||||
|
playerRef.current?.currentTime(savedProgress);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
playerRef.current?.play();
|
playerRef.current?.play();
|
||||||
|
|
||||||
@ -684,8 +746,30 @@ export const VideoPlayer = ({
|
|||||||
console.error("useEffect start player", error);
|
console.error("useEffect start player", error);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
|
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;
|
canceled = true;
|
||||||
const player = playerRef.current;
|
|
||||||
|
|
||||||
if (player && typeof player.dispose === "function") {
|
if (player && typeof player.dispose === "function") {
|
||||||
try {
|
try {
|
||||||
@ -753,6 +837,7 @@ export const VideoPlayer = ({
|
|||||||
onVolumeChange={onVolumeChangeHandler}
|
onVolumeChange={onVolumeChangeHandler}
|
||||||
controls={false}
|
controls={false}
|
||||||
/>
|
/>
|
||||||
|
<PlayBackMenu close={closePlaybackMenu} isOpen={isOpenPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} />
|
||||||
{/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
|
{/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
|
||||||
|
|
||||||
{isReady && (
|
{isReady && (
|
||||||
@ -781,11 +866,12 @@ export const VideoPlayer = ({
|
|||||||
onSelectPlaybackRate={onSelectPlaybackRate}
|
onSelectPlaybackRate={onSelectPlaybackRate}
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
toggleMute={toggleMute}
|
toggleMute={toggleMute}
|
||||||
|
openPlaybackMenu={openPlaybackMenu}
|
||||||
|
togglePictureInPicture={togglePictureInPicture}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{timelineActions && Array.isArray(timelineActions) && (
|
{timelineActions && Array.isArray(timelineActions) && (
|
||||||
<TimelineActionsComponent seekTo={seekTo} containerRef={containerRef} progress={localProgress} timelineActions={timelineActions}/>
|
<TimelineActionsComponent seekTo={seekTo} containerRef={containerRef} progress={localProgress} timelineActions={timelineActions}/>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
<SubtitleManager
|
<SubtitleManager
|
||||||
subtitleBtnRef={subtitleBtnRef}
|
subtitleBtnRef={subtitleBtnRef}
|
||||||
|
@ -12,6 +12,8 @@ import { useProgressStore, useVideoStore } from "../../state/video";
|
|||||||
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
||||||
import { useResourceStatus } from "../../hooks/useResourceStatus";
|
import { useResourceStatus } from "../../hooks/useResourceStatus";
|
||||||
import useIdleTimeout from "../../common/useIdleTimeout";
|
import useIdleTimeout from "../../common/useIdleTimeout";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { useGlobalPlayerStore } from "../../state/pip";
|
||||||
|
|
||||||
const controlsHeight = "42px";
|
const controlsHeight = "42px";
|
||||||
const minSpeed = 0.25;
|
const minSpeed = 0.25;
|
||||||
@ -25,10 +27,11 @@ interface UseVideoControls {
|
|||||||
retryAttempts?: number;
|
retryAttempts?: number;
|
||||||
isPlayerInitialized: boolean
|
isPlayerInitialized: boolean
|
||||||
isMuted: boolean
|
isMuted: boolean
|
||||||
|
videoRef: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVideoPlayerController = (props: UseVideoControls) => {
|
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 [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false)
|
const [showControlsFullScreen, setShowControlsFullScreen] = useState(false)
|
||||||
@ -39,7 +42,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
|||||||
const [startPlay, setStartPlay] = useState(false);
|
const [startPlay, setStartPlay] = useState(false);
|
||||||
const [startedFetch, setStartedFetch] = useState(false);
|
const [startedFetch, setStartedFetch] = useState(false);
|
||||||
const startedFetchRef = useRef(false);
|
const startedFetchRef = useRef(false);
|
||||||
|
const navigate = useNavigate()
|
||||||
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
|
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
|
||||||
const { getProgress } = useProgressStore();
|
const { getProgress } = useProgressStore();
|
||||||
|
|
||||||
@ -61,22 +64,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
|||||||
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
return `${qortalVideoResource.service}-${qortalVideoResource.name}-${qortalVideoResource.identifier}`;
|
||||||
}, [qortalVideoResource]);
|
}, [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(
|
const [playbackRate, _setLocalPlaybackRate] = useState(
|
||||||
playbackSettings.playbackRate
|
playbackSettings.playbackRate
|
||||||
@ -294,6 +282,33 @@ const togglePlay = useCallback(async () => {
|
|||||||
}
|
}
|
||||||
}, [togglePlay, isReady]);
|
}, [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 {
|
return {
|
||||||
reloadVideo,
|
reloadVideo,
|
||||||
@ -314,6 +329,6 @@ const togglePlay = useCallback(async () => {
|
|||||||
isReady,
|
isReady,
|
||||||
resourceUrl,
|
resourceUrl,
|
||||||
startPlay,
|
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 { IndexManager } from "../components/IndexManager/IndexManager";
|
||||||
import { useIndexes } from "../hooks/useIndexes";
|
import { useIndexes } from "../hooks/useIndexes";
|
||||||
import { useProgressStore } from "../state/video";
|
import { useProgressStore } from "../state/video";
|
||||||
|
import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer";
|
||||||
|
|
||||||
// ✅ Define Global Context Type
|
// ✅ Define Global Context Type
|
||||||
interface GlobalContextType {
|
interface GlobalContextType {
|
||||||
@ -80,6 +81,7 @@ export const GlobalProvider = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GlobalContext.Provider value={contextValue}>
|
<GlobalContext.Provider value={contextValue}>
|
||||||
|
<GlobalPipPlayer />
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-center"
|
position="top-center"
|
||||||
toastOptions={{
|
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