fixed global player for mobile

This commit is contained in:
PhilReact 2025-06-22 17:51:40 +03:00
parent 4847b4a002
commit b01232b39f
10 changed files with 810 additions and 384 deletions

View File

@ -0,0 +1,202 @@
import { alpha, Box, IconButton } from "@mui/material";
import React from "react";
import { ProgressSlider } from "./VideoControls";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import PauseIcon from "@mui/icons-material/Pause";
import SubtitlesIcon from "@mui/icons-material/Subtitles";
import SlowMotionVideoIcon from "@mui/icons-material/SlowMotionVideo";
import Fullscreen from "@mui/icons-material/Fullscreen";
import Forward10Icon from "@mui/icons-material/Forward10";
import Replay10Icon from "@mui/icons-material/Replay10";
interface MobileControlsProps {
showControlsMobile: boolean;
progress: number;
duration: number;
playerRef: any;
setShowControlsMobile: (val: boolean) => void;
isPlaying: boolean;
togglePlay: () => void;
openSubtitleManager: () => void;
openPlaybackMenu: () => void;
toggleFullscreen: () => void;
setProgressRelative: (val: number) => void;
}
export const MobileControls = ({
showControlsMobile,
togglePlay,
isPlaying,
setShowControlsMobile,
playerRef,
progress,
duration,
openSubtitleManager,
openPlaybackMenu,
toggleFullscreen,
setProgressRelative,
}: MobileControlsProps) => {
return (
<Box
onClick={() => setShowControlsMobile(false)}
sx={{
position: "absolute",
display: showControlsMobile ? "block" : "none",
top: 0,
bottom: 0,
right: 0,
left: 0,
zIndex: 1,
background: "rgba(0,0,0,.5)",
opacity: 1,
}}
>
<Box
sx={{
position: "absolute",
top: "10px",
right: "10px",
display: "flex",
gap: "10px",
alignItems: "center",
}}
>
<IconButton
onClick={(e) => {
e.stopPropagation();
openSubtitleManager();
}}
>
<SubtitlesIcon
sx={{
fontSize: "24px",
}}
/>
</IconButton>
<IconButton
sx={{
fontSize: "24px",
}}
onClick={(e) => {
e.stopPropagation();
openPlaybackMenu();
}}
>
<SlowMotionVideoIcon />
</IconButton>
</Box>
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
gap: "50px",
display: 'flex',
alignItems: 'center'
}}
>
<IconButton
sx={{
opacity: 1,
zIndex: 2,
}}
onClick={(e) => {
e.stopPropagation();
setProgressRelative(-10);
}}
>
<Replay10Icon
sx={{
fontSize: "36px",
}}
/>
</IconButton>
{isPlaying && (
<IconButton
sx={{
opacity: 1,
zIndex: 2,
}}
onClick={(e) => {
e.stopPropagation();
togglePlay();
}}
>
<PauseIcon
sx={{
fontSize: "36px",
}}
/>
</IconButton>
)}
{!isPlaying && (
<IconButton
sx={{
opacity: 1,
zIndex: 2,
}}
onClick={(e) => {
e.stopPropagation();
togglePlay();
}}
>
<PlayArrowIcon
sx={{
fontSize: "36px",
}}
/>
</IconButton>
)}
<IconButton
sx={{
opacity: 1,
zIndex: 2,
}}
onClick={(e) => {
e.stopPropagation();
setProgressRelative(10);
}}
>
<Forward10Icon
sx={{
fontSize: "36px",
}}
/>
</IconButton>
</Box>
<Box
sx={{
position: "absolute",
bottom: "20px",
right: "10px",
}}
>
<IconButton
sx={{
fontSize: "24px",
}}
onClick={(e) => {
e.stopPropagation();
toggleFullscreen();
}}
>
<Fullscreen />
</IconButton>
</Box>
<Box
sx={{
width: "100%",
position: "absolute",
bottom: 0,
}}
>
<ProgressSlider
playerRef={playerRef}
progress={progress}
duration={duration}
/>
</Box>
</Box>
);
};

View File

@ -72,6 +72,8 @@ export interface SubtitleManagerProps {
onSelect: (subtitle: SubtitlePublishedData) => void;
subtitleBtnRef: any;
currentSubTrack: null | string
setDrawerOpenSubtitles: (val: boolean)=> void
isFromDrawer: boolean
}
export interface Subtitle {
language: string | null;
@ -108,6 +110,8 @@ const SubtitleManagerComponent = ({
onSelect,
subtitleBtnRef,
currentSubTrack,
setDrawerOpenSubtitles,
isFromDrawer = false
}: SubtitleManagerProps) => {
const [mode, setMode] = useState(1);
const [isOpenPublish, setIsOpenPublish] = useState(false);
@ -177,8 +181,11 @@ const SubtitleManagerComponent = ({
}
}, [open])
console.log('isFromDrawer', )
const handleBlur = (e: React.FocusEvent) => {
if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish) {
if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish && !isFromDrawer && open) {
console.log('hello close')
close();
setIsOpenPublish(false)
}
@ -262,13 +269,13 @@ const SubtitleManagerComponent = ({
sx={
{
position: 'absolute',
bottom: 60,
right: 5,
position: isFromDrawer ? 'relative' : 'absolute',
bottom: isFromDrawer ? 'unset' : 60,
right: isFromDrawer ? 'unset' : 5,
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: 5,
boxShadow: isFromDrawer ? 'unset' : 5,
p: 1,
minWidth: 225,
height: 300,
@ -387,30 +394,7 @@ const SubtitleManagerComponent = ({
Load community subs
</Button>
</Box>
{/* <Box>
{[
'Ambient mode',
'Annotations',
'Subtitles/CC',
'Sleep timer',
'Playback speed',
'Quality',
].map((label) => (
<Typography
key={label}
sx={{
px: 2,
py: 1,
'&:hover': {
backgroundColor: 'rgba(255, 255, 255, 0.1)',
cursor: 'pointer',
},
}}
>
{label}
</Typography>
))}
</Box> */}
</Box>
<PublishSubtitles
@ -421,70 +405,10 @@ const SubtitleManagerComponent = ({
/>
</>
// <Dialog
// open={!!open}
// fullWidth={true}
// maxWidth={"md"}
// sx={{
// zIndex: 999990,
// }}
// slotProps={{
// paper: {
// elevation: 0,
// },
// }}
// >
// <DialogTitle>Subtitles</DialogTitle>
// <IconButton
// aria-label="close"
// onClick={handleClose}
// sx={(theme) => ({
// position: "absolute",
// right: 8,
// top: 8,
// })}
// >
// <CloseIcon />
// </IconButton>
// <Button onClick={() => setMode(5)}>New subtitles</Button>
// {mode === 1 && (
// <PublisherSubtitles
// subtitles={subtitles}
// publisherName={qortalMetadata.name}
// setMode={setMode}
// onSelect={onSelect}
// />
// )}
// {mode === 5 && <PublishSubtitles publishHandler={publishHandler} />}
// {/* {mode === 2 && (
// <CommunitySubtitles
// link={open?.link}
// name={open?.name}
// mode={mode}
// setMode={setMode}
// username={username}
// category={open?.category}
// rootName={open?.rootName}
// />
// )}
// {mode === 4 && (
// <MySubtitles
// link={open?.link}
// name={open?.name}
// mode={mode}
// setMode={setMode}
// username={username}
// title={title}
// description={description}
// setDescription={setDescription}
// setTitle={setTitle}
// />
// )} */}
// </Dialog>
);
};
interface PublisherSubtitlesProps {
publisherName: string;
subtitles: any[];

View File

@ -71,7 +71,7 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => {
const [hoverX, setHoverX] = useState<number | null>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [showDuration, setShowDuration] = useState(0);
const onProgressChange = (_: any, value: number | number[]) => {
const onProgressChange = (e: any, value: number | number[]) => {
if (!playerRef.current) return;
playerRef.current?.currentTime(value as number);
@ -128,6 +128,11 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => {
console.log("thumbnailUrl", thumbnailUrl, hoverX);
}
const handleClickCapture = (e: React.MouseEvent) => {
e.stopPropagation();
};
return (
<Box
@ -152,6 +157,7 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => {
ref={sliderRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClickCapture={handleClickCapture}
value={progress}
onChange={onProgressChange}
min={0}
@ -439,8 +445,9 @@ interface PlayBackMenuProps {
isOpen: boolean
onSelect: (speed: number)=> void;
playbackRate: number
isFromDrawer: boolean
}
export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMenuProps)=> {
export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawer}: PlayBackMenuProps)=> {
const theme = useTheme()
const ref = useRef<any>(null)
@ -451,7 +458,7 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMe
}, [isOpen])
const handleBlur = (e: React.FocusEvent) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
if (!e.currentTarget.contains(e.relatedTarget) && !isFromDrawer) {
close();
}
};
@ -466,13 +473,13 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMe
sx={
{
position: 'absolute',
bottom: 60,
right: 5,
position: isFromDrawer ? 'relative' : 'absolute',
bottom: isFromDrawer ? 'relative' : 60,
right:isFromDrawer ? 'relative' : 5,
color: "white",
opacity: 0.9,
borderRadius: 2,
boxShadow: 5,
boxShadow: isFromDrawer ? 'relative' : 5,
p: 1,
minWidth: 225,
height: 300,

View File

@ -42,9 +42,10 @@ interface VideoControlsBarProps {
toggleMute: ()=> void
openPlaybackMenu: ()=> void
togglePictureInPicture: ()=> void
isVideoPlayerSmall: boolean
}
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) => {
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, isVideoPlayerSmall}: VideoControlsBarProps) => {
const showMobileControls = isScreenSmall && canPlay;
@ -87,7 +88,8 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
}}>
<ProgressSlider playerRef={playerRef} progress={progress} duration={duration} />
<Box sx={{
{!isVideoPlayerSmall && (
<Box sx={{
width: '100%',
display: 'flex'
}}>
@ -117,6 +119,8 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
<FullscreenButton toggleFullscreen={toggleFullscreen} />
</Box>
</Box>
)}
</Box>
) : null}
</ControlsContainer>

View File

@ -1,7 +1,9 @@
import { styled } from "@mui/system";
import { styled, Theme } from "@mui/system";
import { Box } from "@mui/material";
export const VideoContainer = styled(Box)(({ theme }) => ({
export const VideoContainer = styled(Box, {
shouldForwardProp: (prop) => prop !== 'isVideoPlayerSmall',
})<{ isVideoPlayerSmall?: boolean }>(({ theme, isVideoPlayerSmall }) => ({
position: "relative",
display: "flex",
flexDirection: "column",
@ -11,7 +13,7 @@ export const VideoContainer = styled(Box)(({ theme }) => ({
height: "100%",
margin: 0,
padding: 0,
borderRadius: '12px',
borderRadius: isVideoPlayerSmall ? '0px' : '12px',
overflow: 'hidden',
"&:focus": { outline: "none" },
}));

View File

@ -33,6 +33,8 @@ import { TimelineActionsComponent } from "./TimelineActionsComponent";
import { PlayBackMenu } from "./VideoControls";
import { useGlobalPlayerStore } from "../../state/pip";
import { LocationContext } from "../../context/GlobalProvider";
import { alpha, Box, Drawer, List, ListItem } from "@mui/material";
import { MobileControls } from "./MobileControls";
export async function srtBase64ToVttBlobUrl(
base64Srt: string
@ -107,6 +109,8 @@ async function getVideoMimeTypeFromUrl(
return null;
}
}
export const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
export const VideoPlayer = ({
videoRef,
@ -117,9 +121,19 @@ export const VideoPlayer = ({
onEnded,
timelineActions
}: VideoPlayerProps) => {
const containerRef = useRef<RefObject<HTMLDivElement> | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("contain");
const [isPlaying, setIsPlaying] = useState(false);
const [width, setWidth] = useState(0);
console.log('width',width)
useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
setWidth(entry.contentRect.width);
});
if (containerRef.current) observer.observe(containerRef.current);
return () => observer.disconnect();
}, []);
const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore(
(state) => ({
volume: state.playbackSettings.volume,
@ -129,6 +143,9 @@ export const VideoPlayer = ({
})
);
const playerRef = useRef<Player | null>(null);
const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false)
const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false)
const [showControlsMobile, setShowControlsMobile] = useState(false)
const [isPlayerInitialized, setIsPlayerInitialized] = useState(false);
const [videoCodec, setVideoCodec] = useState<null | false | string>(null);
const [isMuted, setIsMuted] = useState(false);
@ -144,6 +161,7 @@ export const VideoPlayer = ({
const locationRef = useRef<string | null>(null)
const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false)
const isVideoPlayerSmall = width < 600
const {
reloadVideo,
togglePlay,
@ -188,18 +206,71 @@ export const VideoPlayer = ({
const { getProgress } = useProgressStore();
const enterFullscreen = useCallback(() => {
const ref = containerRef?.current as any;
if (!ref) return;
const enterFullscreen = useCallback(async () => {
const ref = containerRef?.current as HTMLElement | null;
if (!ref || document.fullscreenElement) return;
if (ref.requestFullscreen && !isFullscreen) {
ref.requestFullscreen();
try {
// Wait for fullscreen to activate
if (ref.requestFullscreen) {
await ref.requestFullscreen();
} else if ((ref as any).webkitRequestFullscreen) {
await (ref as any).webkitRequestFullscreen(); // Safari fallback
}
}, []);
const exitFullscreen = useCallback(() => {
document?.exitFullscreen();
}, [isFullscreen]);
if (
typeof screen.orientation !== 'undefined' &&
'lock' in screen.orientation &&
typeof screen.orientation.lock === 'function'
) {
try {
await (screen.orientation as any).lock('landscape');
} catch (err) {
console.warn('Orientation lock failed:', err);
}
}
await qortalRequest({
action: 'SCREEN_ORIENTATION',
mode: 'landscape'
})
} catch (err) {
console.error('Failed to enter fullscreen or lock orientation:', err);
}
}, []);
// const exitFullscreen = useCallback(() => {
// document?.exitFullscreen();
// }, [isFullscreen]);
const exitFullscreen = useCallback(async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
}
if (
typeof screen.orientation !== 'undefined' &&
'lock' in screen.orientation &&
typeof screen.orientation.lock === 'function'
) {
try {
// Attempt to reset by locking to 'portrait' or 'any' (if supported)
await screen.orientation.lock('portrait'); // or 'any' if supported
} catch (err) {
console.warn('Orientation lock failed:', err);
}
}
await qortalRequest({
action: 'SCREEN_ORIENTATION',
mode: 'portrait'
})
} catch (err) {
console.warn('Error exiting fullscreen or unlocking orientation:', err);
}
}, [isFullscreen]);
const toggleFullscreen = useCallback(() => {
isFullscreen ? exitFullscreen() : enterFullscreen();
@ -239,10 +310,14 @@ export const VideoPlayer = ({
const closeSubtitleManager = useCallback(() => {
setIsOpenSubtitleManage(false);
setDrawerOpenSubtitles(false)
}, []);
const openSubtitleManager = useCallback(() => {
if(isVideoPlayerSmall){
setDrawerOpenSubtitles(true)
}
setIsOpenSubtitleManage(true);
}, []);
}, [isVideoPlayerSmall]);
const videoLocation = useMemo(() => {
if (!qortalVideoResource) return null;
@ -388,6 +463,7 @@ const videoLocationRef = useRef< null | string>(null)
const hideTimeout = useRef<any>(null);
const resetHideTimer = () => {
if(isTouchDevice) return
setShowControls(true);
if (hideTimeout.current) clearTimeout(hideTimeout.current);
hideTimeout.current = setTimeout(() => {
@ -396,17 +472,24 @@ const videoLocationRef = useRef< null | string>(null)
};
const handleMouseMove = () => {
if(isTouchDevice) return
resetHideTimer();
};
const closePlaybackMenu = useCallback(()=> {
setIsOpenPlaybackmenu(false)
setDrawerOpenPlayback(false)
}, [])
const openPlaybackMenu = useCallback(()=> {
if(isVideoPlayerSmall){
setDrawerOpenPlayback(true)
return
}
setIsOpenPlaybackmenu(true)
}, [])
}, [isVideoPlayerSmall])
useEffect(() => {
if(isTouchDevice) return
resetHideTimer(); // initial show
return () => {
if (hideTimeout.current) clearTimeout(hideTimeout.current);
@ -717,6 +800,33 @@ savedVideoRef.current = video.current;
player.off("ratechange", handleRateChange);
};
}, [isPlayerInitialized]);
const hideTimeoutRef = useRef<number | null>(null);
const resetHideTimeout = () => {
setShowControlsMobile(true);
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => {
setShowControlsMobile(false);
}, 3000);
};
useEffect(() => {
const handleInteraction = () => resetHideTimeout();
const container = containerRef.current;
if (!container) return;
container.addEventListener('touchstart', handleInteraction);
// container.addEventListener('mousemove', handleInteraction);
return () => {
container.removeEventListener('touchstart', handleInteraction);
// container.removeEventListener('mousemove', handleInteraction);
};
}, []);
console.log('showControlsMobile', showControlsMobile)
return (
<>
@ -728,6 +838,7 @@ savedVideoRef.current = video.current;
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
ref={containerRef}
isVideoPlayerSmall={isVideoPlayerSmall}
>
<LoadingVideo
togglePlay={togglePlay}
@ -754,11 +865,11 @@ savedVideoRef.current = video.current;
onVolumeChange={onVolumeChangeHandler}
controls={false}
/>
<PlayBackMenu close={closePlaybackMenu} isOpen={isOpenPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} />
{/* <canvas ref={canvasRef} style={{ display: "none" }}></canvas> */}
<PlayBackMenu isFromDrawer={false} close={closePlaybackMenu} isOpen={isOpenPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} />
{isReady && (
{isReady && !showControlsMobile && (
<VideoControlsBar
isVideoPlayerSmall={isVideoPlayerSmall}
subtitleBtnRef={subtitleBtnRef}
playbackRate={playbackRate}
increaseSpeed={hotkeyHandlers.increaseSpeed}
@ -790,15 +901,73 @@ savedVideoRef.current = video.current;
{timelineActions && Array.isArray(timelineActions) && (
<TimelineActionsComponent seekTo={seekTo} containerRef={containerRef} progress={localProgress} timelineActions={timelineActions}/>
)}
<SubtitleManager
{showControlsMobile && (
<MobileControls setProgressRelative={setProgressRelative} toggleFullscreen={toggleFullscreen} openPlaybackMenu={openPlaybackMenu} openSubtitleManager={openSubtitleManager} togglePlay={togglePlay} isPlaying={isPlaying} setShowControlsMobile={setShowControlsMobile} duration={duration}
progress={localProgress} playerRef={playerRef} showControlsMobile={showControlsMobile} />
)}
{!isVideoPlayerSmall && (
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={isOpenSubtitleManage}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
setDrawerOpenSubtitles={setDrawerOpenSubtitles}
isFromDrawer={false}
/>
)}
</VideoContainer>
<Drawer anchor="bottom" open={drawerOpenSubtitles} onClose={() => setDrawerOpenSubtitles(false)} sx={{
}} slotProps={{
paper: {
sx: {
backgroundColor: alpha("#181818", 0.98),
borderRadius: 2,
width: '90%',
margin: '0 auto',
p: 1,
backgroundImage: 'none',
mb: 1
},
}
}}>
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={true}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
setDrawerOpenSubtitles={setDrawerOpenSubtitles}
isFromDrawer={true}
/>
</Drawer>
<Drawer anchor="bottom" open={drawerOpenPlayback} onClose={() => setDrawerOpenPlayback(false)} sx={{
}} slotProps={{
paper: {
sx: {
backgroundColor: alpha("#181818", 0.98),
borderRadius: 2,
width: '90%',
margin: '0 auto',
p: 1,
backgroundImage: 'none',
mb: 1
},
}
}}>
<PlayBackMenu isFromDrawer close={closePlaybackMenu} isOpen={true} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} />
</Drawer>
</>
);
};

View File

@ -18,6 +18,7 @@ import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer";
import { Location, NavigateFunction } from "react-router-dom";
import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog";
import { useMultiplePublishStore } from "../state/multiplePublish";
import { useGlobalPlayerStore } from "../state/pip";
// ✅ Define Global Context Type
interface GlobalContextType {
@ -62,7 +63,7 @@ export const GlobalProvider = ({
// ✅ Call hooks and pass in options dynamically
const auth = useAuth(config?.auth || {});
const isPublishing = useMultiplePublishStore((s)=> s.isPublishing);
const videoSrc = useGlobalPlayerStore((s)=> s.videoSrc);
const appInfo = useAppInfo(config.appName, config?.publicSalt);
const lists = useResources();
const identifierOperations = useIdentifiers(
@ -97,7 +98,8 @@ export const GlobalProvider = ({
<LocationContext.Provider value={location}>
<GlobalContext.Provider value={contextValue}>
<GlobalPipPlayer />
<GlobalPipPlayer />
{isPublishing && (
<MultiPublishDialog />
)}

View File

@ -1,4 +1,4 @@
import { AddForeignServerQortalRequest, AddListItemsQortalRequest, BuyNameQortalRequest, CancelSellNameQortalRequest, CancelTradeSellOrderQortalRequest, CreatePollQortalRequest, CreateTradeBuyOrderQortalRequest, CreateTradeSellOrderQortalRequest, DecryptDataQortalRequest, DecryptDataWithSharingKeyQortalRequest, DecryptQortalGroupDataQortalRequest, DeleteHostedDataQortalRequest, DeleteListItemQortalRequest, EncryptDataQortalRequest, EncryptDataWithSharingKeyQortalRequest, EncryptQortalGroupDataQortalRequest, FetchQdnResourceQortalRequest, GetAccountDataQortalRequest, GetAccountNamesQortalRequest, GetBalanceQortalRequest, GetCrosschainServerInfoQortalRequest, GetDaySummaryQortalRequest, GetForeignFeeQortalRequest, GetHostedDataQortalRequest, GetListItemsQortalRequest, GetNameDataQortalRequest, GetPriceQortalRequest, GetQdnResourceMetadataQortalRequest, GetQdnResourcePropertiesQortalRequest, GetQdnResourceStatusQortalRequest, GetQdnResourceUrlQortalRequest, GetServerConnectionHistoryQortalRequest, GetTxActivitySummaryQortalRequest, GetUserAccountQortalRequest, GetUserWalletInfoQortalRequest, GetUserWalletQortalRequest, GetWalletBalanceQortalRequest, LinkToQdnResourceQortalRequest, ListQdnResourcesQortalRequest, PublishMultipleQdnResourcesQortalRequest, PublishQdnResourceQortalRequest, RegisterNameQortalRequest, RemoveForeignServerQortalRequest, SearchNamesQortalRequest, SearchQdnResourcesQortalRequest, SellNameQortalRequest, SendCoinQortalRequest, SetCurrentForeignServerQortalRequest, UpdateForeignFeeQortalRequest, UpdateNameQortalRequest, VoteOnPollQortalRequest, SendChatMessageQortalRequest, SearchChatMessagesQortalRequest, JoinGroupQortalRequest, AddGroupAdminQortalRequest, UpdateGroupQortalRequest, ListGroupsQortalRequest, CreateGroupQortalRequest, RemoveGroupAdminQortalRequest, BanFromGroupQortalRequest, CancelGroupBanQortalRequest, KickFromGroupQortalRequest, InviteToGroupQortalRequest, CancelGroupInviteQortalRequest, LeaveGroupQortalRequest, DeployAtQortalRequest, GetAtQortalRequest, GetAtDataQortalRequest, ListAtsQortalRequest, FetchBlockQortalRequest, FetchBlockRangeQortalRequest, SearchTransactionsQortalRequest, IsUsingPublicNodeQortalRequest, AdminActionQortalRequest, OpenNewTabQortalRequest, ShowActionsQortalRequest, SignTransactionQortalRequest, CreateAndCopyEmbedLinkQortalRequest, TransferAssetQortalRequest, ShowPdfReaderQortalRequest, SaveFileQortalRequest, GetPrimaryNameQortalRequest, } from "./types/qortalRequests/interfaces"
import { AddForeignServerQortalRequest, AddListItemsQortalRequest, BuyNameQortalRequest, CancelSellNameQortalRequest, CancelTradeSellOrderQortalRequest, CreatePollQortalRequest, CreateTradeBuyOrderQortalRequest, CreateTradeSellOrderQortalRequest, DecryptDataQortalRequest, DecryptDataWithSharingKeyQortalRequest, DecryptQortalGroupDataQortalRequest, DeleteHostedDataQortalRequest, DeleteListItemQortalRequest, EncryptDataQortalRequest, EncryptDataWithSharingKeyQortalRequest, EncryptQortalGroupDataQortalRequest, FetchQdnResourceQortalRequest, GetAccountDataQortalRequest, GetAccountNamesQortalRequest, GetBalanceQortalRequest, GetCrosschainServerInfoQortalRequest, GetDaySummaryQortalRequest, GetForeignFeeQortalRequest, GetHostedDataQortalRequest, GetListItemsQortalRequest, GetNameDataQortalRequest, GetPriceQortalRequest, GetQdnResourceMetadataQortalRequest, GetQdnResourcePropertiesQortalRequest, GetQdnResourceStatusQortalRequest, GetQdnResourceUrlQortalRequest, GetServerConnectionHistoryQortalRequest, GetTxActivitySummaryQortalRequest, GetUserAccountQortalRequest, GetUserWalletInfoQortalRequest, GetUserWalletQortalRequest, GetWalletBalanceQortalRequest, LinkToQdnResourceQortalRequest, ListQdnResourcesQortalRequest, PublishMultipleQdnResourcesQortalRequest, PublishQdnResourceQortalRequest, RegisterNameQortalRequest, RemoveForeignServerQortalRequest, SearchNamesQortalRequest, SearchQdnResourcesQortalRequest, SellNameQortalRequest, SendCoinQortalRequest, SetCurrentForeignServerQortalRequest, UpdateForeignFeeQortalRequest, UpdateNameQortalRequest, VoteOnPollQortalRequest, SendChatMessageQortalRequest, SearchChatMessagesQortalRequest, JoinGroupQortalRequest, AddGroupAdminQortalRequest, UpdateGroupQortalRequest, ListGroupsQortalRequest, CreateGroupQortalRequest, RemoveGroupAdminQortalRequest, BanFromGroupQortalRequest, CancelGroupBanQortalRequest, KickFromGroupQortalRequest, InviteToGroupQortalRequest, CancelGroupInviteQortalRequest, LeaveGroupQortalRequest, DeployAtQortalRequest, GetAtQortalRequest, GetAtDataQortalRequest, ListAtsQortalRequest, FetchBlockQortalRequest, FetchBlockRangeQortalRequest, SearchTransactionsQortalRequest, IsUsingPublicNodeQortalRequest, AdminActionQortalRequest, OpenNewTabQortalRequest, ShowActionsQortalRequest, SignTransactionQortalRequest, CreateAndCopyEmbedLinkQortalRequest, TransferAssetQortalRequest, ShowPdfReaderQortalRequest, SaveFileQortalRequest, GetPrimaryNameQortalRequest, ScreenOrientation, GetNodeStatusQortalRequest, GetNodeInfoQortalRequest, } from "./types/qortalRequests/interfaces"
declare global {
@ -84,7 +84,7 @@ declare global {
CreateAndCopyEmbedLinkQortalRequest |
TransferAssetQortalRequest |
ShowPdfReaderQortalRequest |
SaveFileQortalRequest | GetPrimaryNameQortalRequest
SaveFileQortalRequest | GetPrimaryNameQortalRequest | ScreenOrientation | GetNodeStatusQortalRequest | GetNodeInfoQortalRequest;
function qortalRequest(options: QortalRequestOptions): Promise<any>

View File

@ -1,46 +1,65 @@
// GlobalVideoPlayer.tsx
import videojs from 'video.js';
import { useGlobalPlayerStore } from '../state/pip';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Box, IconButton } from '@mui/material';
import { VideoContainer } from '../components/VideoPlayer/VideoPlayer-styles';
import videojs from "video.js";
import { useGlobalPlayerStore } from "../state/pip";
import { useCallback, useContext, 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 { GlobalContext } from '../context/GlobalProvider';
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 { GlobalContext } from "../context/GlobalProvider";
import { isTouchDevice } from "../components/VideoPlayer/VideoPlayer";
export const GlobalPipPlayer = () => {
const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore();
const [playing , setPlaying] = useState(false)
const [hasStarted, setHasStarted] = useState(false)
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 context = useContext(GlobalContext)
const navigate = context?.navigate
const containerRef = useRef<HTMLDivElement | null>(null);
const context = useContext(GlobalContext);
const navigate = context?.navigate;
const videoNode = useRef<HTMLVideoElement>(null);
const { setProgress } = useProgressStore();
const hideTimeoutRef = useRef<number | null>(null);
const { setProgress } = useProgressStore();
const updateProgress = useCallback(() => {
const player = playerRef?.current;
if (!player || typeof player?.currentTime !== "function") return;
const updateProgress = useCallback(() => {
const player = playerRef?.current;
if (!player || typeof player?.currentTime !== "function") return;
const currentTime = player.currentTime();
if (typeof currentTime === "number" && videoId && currentTime > 0.1) {
setProgress(videoId, currentTime);
}
}, [videoId]);
const currentTime = player.currentTime();
if (typeof currentTime === "number" && videoId && currentTime > 0.1) {
setProgress(videoId, currentTime);
}
}, [videoId]);
const rndRef = useRef<any>(null)
const rndRef = useRef<any>(null);
useEffect(() => {
if (!playerRef.current && videoNode.current) {
playerRef.current = videojs(videoNode.current, { autoplay: true, controls: false,
responsive: true, fluid: true });
playerRef.current = videojs(videoNode.current, {
autoplay: true,
controls: false,
responsive: true,
fluid: true,
});
playerRef.current?.on("error", () => {
// Optional: display user-friendly message
});
// Resume playback if needed
playerRef.current.on('ready', () => {
playerRef.current.on("ready", () => {
if (videoSrc) {
playerRef.current.src(videoSrc);
playerRef.current.currentTime(currentTime);
if (isPlaying) playerRef.current.play();
@ -53,181 +72,216 @@ export const GlobalPipPlayer = () => {
};
}, []);
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 - (width || 400) - margin,
y: window.innerHeight - (height || 300) - margin,
width: width || 400,
height: height || 300
});
if (!videoSrc) {
setHasStarted(false);
}
}, [videoSrc]);
const [showControls, setShowControls] = useState(false)
useEffect(() => {
const player = playerRef.current;
const handleMouseMove = () => {
setShowControls(true)
if (!player) return;
if (!videoSrc && player.src) {
// Only pause the player and unload the source without re-triggering playback
player.pause();
// player.src({ src: '', type: '' }); // ⬅️ this is the safe way to clear it
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(() => {
if (!videoSrc) return;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const aspectRatio = 0.75; // 300 / 400 = 3:4
const maxWidthByScreen = screenWidth * 0.75;
const maxWidthByHeight = (screenHeight * 0.2) / aspectRatio;
const maxWidth = Math.min(maxWidthByScreen, maxWidthByHeight);
const maxHeight = maxWidth * aspectRatio;
setWidth(maxWidth);
setHeight(maxHeight);
rndRef.current.updatePosition({
x: screenWidth - maxWidth - margin,
y: screenHeight - maxHeight - margin,
width: maxWidth,
height: maxHeight,
});
}, [videoSrc]);
const [showControls, setShowControls] = useState(false);
const handleMouseMove = () => {
if (isTouchDevice) return;
setShowControls(true);
};
const handleMouseLeave = () => {
if (isTouchDevice) return;
setShowControls(false);
};
const startPlay = useCallback(() => {
try {
const player = playerRef.current;
if (!player) return;
try {
const player = playerRef.current;
if (!player) return;
try {
player.play();
player.play();
} catch (err) {
console.warn('Play failed:', err);
console.warn("Play failed:", err);
}
} catch (error) {
console.error('togglePlay', error)
}
} 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);
}
if (!player) return;
try {
player.pause();
} catch (err) {
console.warn("Play failed:", err);
}
}, []);
const onPlayHandlerStart = useCallback(() => {
setPlaying(true)
setHasStarted(true)
setPlaying(true);
setHasStarted(true);
}, [setPlaying]);
const onPlayHandlerStop = useCallback(() => {
setPlaying(false)
const onPlayHandlerStop = useCallback(() => {
setPlaying(false);
}, [setPlaying]);
const resetHideTimeout = () => {
setShowControls(true);
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => {
setShowControls(false);
}, 3000);
};
useEffect(() => {
const container = containerRef.current;
if (!videoSrc || !container) return;
const handleInteraction = () => {
console.log("Touchstart detected!");
resetHideTimeout();
};
container.addEventListener("touchstart", handleInteraction, {
passive: true,
capture: true,
});
return () => {
container.removeEventListener("touchstart", handleInteraction, {
capture: true,
});
};
}, [videoSrc]);
console.log("showControls", showControls);
return (
<Rnd
enableResizing={{
top: false,
right: false,
bottom: false,
left: false,
topRight: true,
bottomLeft: true,
topLeft: true,
bottomRight: true,
}}
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,
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
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',
@ -238,90 +292,135 @@ const margin = 50;
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={()=> {
if(navigate){
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>
<Box
ref={containerRef}
sx={{
height,
pointerEvents: "auto",
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: showControls ? 1 : 0,
pointerEvents: showControls ? "auto" : "none",
transition: "opacity 0.5s ease-in-out",
}}
>
<Box
sx={{
position: "absolute",
background: "rgba(0,0,0,.5)",
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 1,
opacity: showControls ? 1 : 0,
pointerEvents: showControls ? "auto" : "none",
transition: "opacity 0.5s ease-in-out",
}}
/>
<IconButton
sx={{
position: "absolute",
top: 10,
opacity: 1,
right: 10,
zIndex: 2,
}}
onClick={reset}
onTouchStart={reset}
>
<CloseIcon />
</IconButton>
{location && (
<IconButton
sx={{
position: "absolute",
top: 10,
left: 10,
zIndex: 2,
opacity: 1,
}}
onClick={(e) => {
e.stopPropagation()
if (navigate) {
navigate(location);
}
}}
onTouchStart={(e) => {
e.stopPropagation()
if (navigate) {
navigate(location);
}
}}
>
<OpenInFullIcon />
</IconButton>
)}
{playing && (
<IconButton
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
opacity: 1,
zIndex: 2,
}}
onClick={stopPlay}
onTouchStart={stopPlay}
>
<PauseIcon />
</IconButton>
)}
{!playing && (
<IconButton
sx={{
position: "absolute",
opacity: 1,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 2,
}}
onClick={startPlay}
onTouchStart={startPlay}
>
<PlayArrowIcon />
</IconButton>
)}
<Box />
</Box>
)}
<VideoContainer>
<video onPlay={onPlayHandlerStart} onPause={onPlayHandlerStop} onTimeUpdate={updateProgress}
ref={videoNode} className="video-js" style={{
}}/>
</VideoContainer>
<video
onPlay={onPlayHandlerStart}
onPause={onPlayHandlerStop}
onTimeUpdate={updateProgress}
ref={videoNode}
className="video-js"
style={{}}
/>
</VideoContainer>
</Box>
{/* </div> */}
{/* </div> */}
</Rnd>
);
};

View File

@ -615,12 +615,29 @@ export interface ShowActionsQortalRequest extends BaseRequest {
action: 'SHOW_ACTIONS'
}
export interface ScreenOrientation extends BaseRequest {
action: 'SCREEN_ORIENTATION',
mode: | "portrait"
| "landscape"
| "portrait-primary"
| "portrait-secondary"
| "landscape-primary"
| "landscape-secondary" | "unlock";
}
export interface SignTransactionQortalRequest extends BaseRequest {
action: 'SIGN_TRANSACTION'
unsignedBytes: string
process?: boolean
}
export interface GetNodeStatusQortalRequest extends BaseRequest {
action: 'GET_NODE_STATUS'
}
export interface GetNodeInfoQortalRequest extends BaseRequest {
action: 'GET_NODE_INFO'
}
export interface CreateAndCopyEmbedLinkQortalRequest extends BaseRequest {
action: 'CREATE_AND_COPY_EMBED_LINK'
type: string