fix mobile options

This commit is contained in:
PhilReact 2025-06-23 15:02:47 +03:00
parent 378ec78c25
commit 8afeb4af7b
5 changed files with 323 additions and 280 deletions

View File

@ -21,6 +21,7 @@ interface MobileControlsProps {
openPlaybackMenu: () => void; openPlaybackMenu: () => void;
toggleFullscreen: () => void; toggleFullscreen: () => void;
setProgressRelative: (val: number) => void; setProgressRelative: (val: number) => void;
setLocalProgress: (val: number)=> void;
} }
export const MobileControls = ({ export const MobileControls = ({
showControlsMobile, showControlsMobile,
@ -34,6 +35,7 @@ export const MobileControls = ({
openPlaybackMenu, openPlaybackMenu,
toggleFullscreen, toggleFullscreen,
setProgressRelative, setProgressRelative,
setLocalProgress
}: MobileControlsProps) => { }: MobileControlsProps) => {
return ( return (
<Box <Box
@ -195,6 +197,7 @@ export const MobileControls = ({
playerRef={playerRef} playerRef={playerRef}
progress={progress} progress={progress}
duration={duration} duration={duration}
setLocalProgress={setLocalProgress}
/> />
</Box> </Box>
</Box> </Box>

View File

@ -65,17 +65,26 @@ export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => {
); );
}; };
export const ProgressSlider = ({ progress, duration, playerRef }: any) => { export const ProgressSlider = ({ progress, setLocalProgress, duration, playerRef }: any) => {
const sliderRef = useRef(null); const sliderRef = useRef(null);
const [isDragging, setIsDragging] = useState(false);
const [sliderValue, setSliderValue] = useState(0); // local slider value
const [hoverX, setHoverX] = useState<number | null>(null); const [hoverX, setHoverX] = useState<number | null>(null);
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null); const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const [showDuration, setShowDuration] = useState(0); const [showDuration, setShowDuration] = useState(0);
const onProgressChange = (e: any, value: number | number[]) => { const onProgressChange = (e: any, value: number | number[]) => {
if (!playerRef.current) return; setIsDragging(true);
setSliderValue(value as number);
playerRef.current?.currentTime(value as number);
}; };
const onChangeCommitted = (e: any, value: number | number[]) => {
if (!playerRef.current) return;
setSliderValue(value as number);
playerRef.current?.currentTime(value as number);
setIsDragging(false);
setLocalProgress(value)
};
const THUMBNAIL_DEBOUNCE = 500; const THUMBNAIL_DEBOUNCE = 500;
const THUMBNAIL_MIN_DIFF = 10; const THUMBNAIL_MIN_DIFF = 10;
@ -158,8 +167,10 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => {
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
onClickCapture={handleClickCapture} onClickCapture={handleClickCapture}
value={progress} value={isDragging ? sliderValue : progress} // use local state if dragging
onChange={onProgressChange} onChange={onProgressChange}
onChangeCommitted={onChangeCommitted}
min={0} min={0}
max={duration || 100} max={duration || 100}
step={0.1} step={0.1}

View File

@ -43,9 +43,10 @@ interface VideoControlsBarProps {
openPlaybackMenu: ()=> void openPlaybackMenu: ()=> void
togglePictureInPicture: ()=> void togglePictureInPicture: ()=> void
isVideoPlayerSmall: boolean isVideoPlayerSmall: boolean
setLocalProgress: (val: number)=> 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, openPlaybackMenu, togglePictureInPicture, isVideoPlayerSmall}: VideoControlsBarProps) => { export const VideoControlsBar = ({subtitleBtnRef, setLocalProgress, 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; const showMobileControls = isScreenSmall && canPlay;
@ -87,7 +88,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
width: '100%' width: '100%'
}}> }}>
<ProgressSlider playerRef={playerRef} progress={progress} duration={duration} /> <ProgressSlider setLocalProgress={setLocalProgress} playerRef={playerRef} progress={progress} duration={duration} />
{!isVideoPlayerSmall && ( {!isVideoPlayerSmall && (
<Box sx={{ <Box sx={{
width: '100%', width: '100%',

View File

@ -33,7 +33,14 @@ import { TimelineActionsComponent } from "./TimelineActionsComponent";
import { PlayBackMenu } from "./VideoControls"; import { PlayBackMenu } from "./VideoControls";
import { useGlobalPlayerStore } from "../../state/pip"; import { useGlobalPlayerStore } from "../../state/pip";
import { LocationContext } from "../../context/GlobalProvider"; import { LocationContext } from "../../context/GlobalProvider";
import { alpha, Box, Drawer, List, ListItem } from "@mui/material"; import {
alpha,
Box,
ClickAwayListener,
Drawer,
List,
ListItem,
} from "@mui/material";
import { MobileControls } from "./MobileControls"; import { MobileControls } from "./MobileControls";
export async function srtBase64ToVttBlobUrl( export async function srtBase64ToVttBlobUrl(
@ -61,21 +68,21 @@ type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
export type TimelineAction = export type TimelineAction =
| { | {
type: 'SEEK'; type: "SEEK";
time: number; time: number;
duration: number; duration: number;
label: string; label: string;
onClick?: () => void; onClick?: () => void;
seekToTime: number; // ✅ Required for SEEK seekToTime: number; // ✅ Required for SEEK
placement?: 'TOP-RIGHT' | 'TOP-LEFT' | 'BOTTOM-LEFT' | 'BOTTOM-RIGHT'; placement?: "TOP-RIGHT" | "TOP-LEFT" | "BOTTOM-LEFT" | "BOTTOM-RIGHT";
} }
| { | {
type: 'CUSTOM'; type: "CUSTOM";
time: number; time: number;
duration: number; duration: number;
label: string; label: string;
onClick: () => void; // ✅ Required for CUSTOM onClick: () => void; // ✅ Required for CUSTOM
placement?: 'TOP-RIGHT' | 'TOP-LEFT' | 'BOTTOM-LEFT' | 'BOTTOM-RIGHT'; placement?: "TOP-RIGHT" | "TOP-LEFT" | "BOTTOM-LEFT" | "BOTTOM-RIGHT";
}; };
interface VideoPlayerProps { interface VideoPlayerProps {
qortalVideoResource: QortalGetMetadata; qortalVideoResource: QortalGetMetadata;
@ -84,18 +91,14 @@ interface VideoPlayerProps {
poster?: string; poster?: string;
autoPlay?: boolean; autoPlay?: boolean;
onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void; onEnded?: (e: React.SyntheticEvent<HTMLVideoElement, Event>) => void;
timelineActions?: TimelineAction[] timelineActions?: TimelineAction[];
} }
const videoStyles = { const videoStyles = {
videoContainer: {}, videoContainer: {},
video: {}, video: {},
}; };
async function getVideoMimeTypeFromUrl( async function getVideoMimeTypeFromUrl(
qortalVideoResource: any qortalVideoResource: any
): Promise<string | null> { ): Promise<string | null> {
@ -109,8 +112,8 @@ async function getVideoMimeTypeFromUrl(
return null; return null;
} }
} }
export const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; export const isTouchDevice =
"ontouchstart" in window || navigator.maxTouchPoints > 0;
export const VideoPlayer = ({ export const VideoPlayer = ({
videoRef, videoRef,
@ -119,21 +122,21 @@ export const VideoPlayer = ({
poster, poster,
autoPlay, autoPlay,
onEnded, onEnded,
timelineActions timelineActions,
}: VideoPlayerProps) => { }: VideoPlayerProps) => {
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const [videoObjectFit] = useState<StretchVideoType>("contain"); const [videoObjectFit] = useState<StretchVideoType>("contain");
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [width, setWidth] = useState(0); const [width, setWidth] = useState(0);
console.log('width',width) console.log("width", width);
useEffect(() => { useEffect(() => {
const observer = new ResizeObserver(([entry]) => { const observer = new ResizeObserver(([entry]) => {
setWidth(entry.contentRect.width); setWidth(entry.contentRect.width);
}); });
if (containerRef.current) observer.observe(containerRef.current); if (containerRef.current) observer.observe(containerRef.current);
return () => observer.disconnect(); return () => observer.disconnect();
}, []); }, []);
const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore( const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore(
(state) => ({ (state) => ({
volume: state.playbackSettings.volume, volume: state.playbackSettings.volume,
@ -143,9 +146,9 @@ useEffect(() => {
}) })
); );
const playerRef = useRef<Player | null>(null); const playerRef = useRef<Player | null>(null);
const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false) const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false);
const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false) const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false);
const [showControlsMobile, setShowControlsMobile] = useState(false) const [showControlsMobile, setShowControlsMobile] = useState(false);
const [isPlayerInitialized, setIsPlayerInitialized] = useState(false); const [isPlayerInitialized, setIsPlayerInitialized] = useState(false);
const [videoCodec, setVideoCodec] = useState<null | false | string>(null); const [videoCodec, setVideoCodec] = useState<null | false | string>(null);
const [isMuted, setIsMuted] = useState(false); const [isMuted, setIsMuted] = useState(false);
@ -156,12 +159,12 @@ useEffect(() => {
const [showControls, setShowControls] = useState(false); const [showControls, setShowControls] = useState(false);
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 = useContext(LocationContext) const location = useContext(LocationContext);
const locationRef = useRef<string | null>(null) const locationRef = useRef<string | null>(null);
const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false) const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false);
const isVideoPlayerSmall = width < 600 const isVideoPlayerSmall = width < 600 || isTouchDevice;
const { const {
reloadVideo, reloadVideo,
togglePlay, togglePlay,
@ -186,7 +189,7 @@ useEffect(() => {
showControlsFullScreen, showControlsFullScreen,
onSelectPlaybackRate, onSelectPlaybackRate,
seekTo, seekTo,
togglePictureInPicture togglePictureInPicture,
} = useVideoPlayerController({ } = useVideoPlayerController({
autoPlay, autoPlay,
playerRef, playerRef,
@ -194,89 +197,84 @@ useEffect(() => {
retryAttempts, retryAttempts,
isPlayerInitialized, isPlayerInitialized,
isMuted, isMuted,
videoRef videoRef,
}); });
useEffect(()=> { useEffect(() => {
if(location){ if (location) {
locationRef.current = location.pathname locationRef.current = location.pathname;
} }
},[location]) }, [location]);
const { getProgress } = useProgressStore(); const { getProgress } = useProgressStore();
const enterFullscreen = useCallback(async () => { const enterFullscreen = useCallback(async () => {
const ref = containerRef?.current as HTMLElement | null; const ref = containerRef?.current as HTMLElement | null;
if (!ref || document.fullscreenElement) return; if (!ref || document.fullscreenElement) return;
try { try {
// Wait for fullscreen to activate // Wait for fullscreen to activate
if (ref.requestFullscreen) { if (ref.requestFullscreen) {
await ref.requestFullscreen(); await ref.requestFullscreen();
} else if ((ref as any).webkitRequestFullscreen) { } else if ((ref as any).webkitRequestFullscreen) {
await (ref as any).webkitRequestFullscreen(); // Safari fallback await (ref as any).webkitRequestFullscreen(); // Safari fallback
}
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);
}
}, []);
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(() => { // const exitFullscreen = useCallback(() => {
// document?.exitFullscreen(); // document?.exitFullscreen();
// }, [isFullscreen]); // }, [isFullscreen]);
const exitFullscreen = useCallback(async () => { const exitFullscreen = useCallback(async () => {
try { try {
if (document.fullscreenElement) { if (document.fullscreenElement) {
await document.exitFullscreen(); 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]);
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(() => { const toggleFullscreen = useCallback(() => {
isFullscreen ? exitFullscreen() : enterFullscreen(); isFullscreen ? exitFullscreen() : enterFullscreen();
}, [isFullscreen]); }, [isFullscreen]);
const hotkeyHandlers = useMemo( const hotkeyHandlers = useMemo(
() => ({ () => ({
reloadVideo, reloadVideo,
@ -290,7 +288,7 @@ const enterFullscreen = useCallback(async () => {
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls, setAlwaysShowControls,
toggleFullscreen toggleFullscreen,
}), }),
[ [
reloadVideo, reloadVideo,
@ -304,17 +302,17 @@ const enterFullscreen = useCallback(async () => {
toggleMute, toggleMute,
setProgressAbsolute, setProgressAbsolute,
setAlwaysShowControls, setAlwaysShowControls,
toggleFullscreen toggleFullscreen,
] ]
); );
const closeSubtitleManager = useCallback(() => { const closeSubtitleManager = useCallback(() => {
setIsOpenSubtitleManage(false); setIsOpenSubtitleManage(false);
setDrawerOpenSubtitles(false) setDrawerOpenSubtitles(false);
}, []); }, []);
const openSubtitleManager = useCallback(() => { const openSubtitleManager = useCallback(() => {
if(isVideoPlayerSmall){ if (isVideoPlayerSmall) {
setDrawerOpenSubtitles(true) setDrawerOpenSubtitles(true);
} }
setIsOpenSubtitleManage(true); setIsOpenSubtitleManage(true);
}, [isVideoPlayerSmall]); }, [isVideoPlayerSmall]);
@ -323,10 +321,10 @@ const enterFullscreen = useCallback(async () => {
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) const videoLocationRef = useRef<null | string>(null);
useEffect(()=> { useEffect(() => {
videoLocationRef.current = videoLocation videoLocationRef.current = videoLocation;
}, [videoLocation]) }, [videoLocation]);
useVideoPlayerHotKeys(hotkeyHandlers); useVideoPlayerHotKeys(hotkeyHandlers);
const updateProgress = useCallback(() => { const updateProgress = useCallback(() => {
@ -340,14 +338,14 @@ const videoLocationRef = useRef< null | string>(null)
} }
}, [videoLocation]); }, [videoLocation]);
useEffect(()=> { useEffect(() => {
if(videoLocation){ if (videoLocation) {
const vidId = useGlobalPlayerStore.getState().videoId const vidId = useGlobalPlayerStore.getState().videoId;
if(vidId === videoLocation){ if (vidId === videoLocation) {
togglePlay() togglePlay();
} }
} }
}, [videoLocation]) }, [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;
@ -428,7 +426,6 @@ const videoLocationRef = useRef< null | string>(null)
}; };
}, [isPlayerInitialized]); }, [isPlayerInitialized]);
const canvasRef = useRef(null); const canvasRef = useRef(null);
const videoRefForCanvas = useRef<any>(null); const videoRefForCanvas = useRef<any>(null);
const extractFrames = useCallback((time: number): void => { const extractFrames = useCallback((time: number): void => {
@ -463,7 +460,7 @@ const videoLocationRef = useRef< null | string>(null)
const hideTimeout = useRef<any>(null); const hideTimeout = useRef<any>(null);
const resetHideTimer = () => { const resetHideTimer = () => {
if(isTouchDevice) return if (isTouchDevice) return;
setShowControls(true); setShowControls(true);
if (hideTimeout.current) clearTimeout(hideTimeout.current); if (hideTimeout.current) clearTimeout(hideTimeout.current);
hideTimeout.current = setTimeout(() => { hideTimeout.current = setTimeout(() => {
@ -472,24 +469,24 @@ const videoLocationRef = useRef< null | string>(null)
}; };
const handleMouseMove = () => { const handleMouseMove = () => {
if(isTouchDevice) return if (isTouchDevice) return;
resetHideTimer(); resetHideTimer();
}; };
const closePlaybackMenu = useCallback(()=> { const closePlaybackMenu = useCallback(() => {
setIsOpenPlaybackmenu(false) setIsOpenPlaybackmenu(false);
setDrawerOpenPlayback(false) setDrawerOpenPlayback(false);
}, []) }, []);
const openPlaybackMenu = useCallback(()=> { const openPlaybackMenu = useCallback(() => {
if(isVideoPlayerSmall){ if (isVideoPlayerSmall) {
setDrawerOpenPlayback(true) setDrawerOpenPlayback(true);
return return;
} }
setIsOpenPlaybackmenu(true) setIsOpenPlaybackmenu(true);
}, [isVideoPlayerSmall]) }, [isVideoPlayerSmall]);
useEffect(() => { useEffect(() => {
if(isTouchDevice) return if (isTouchDevice) return;
resetHideTimer(); // initial show resetHideTimer(); // initial show
return () => { return () => {
if (hideTimeout.current) clearTimeout(hideTimeout.current); if (hideTimeout.current) clearTimeout(hideTimeout.current);
@ -510,34 +507,34 @@ setDrawerOpenPlayback(false)
const onSelectSubtitle = useCallback( const onSelectSubtitle = useCallback(
async (subtitle: SubtitlePublishedData) => { async (subtitle: SubtitlePublishedData) => {
if(subtitle === null){ if (subtitle === null) {
setCurrentSubTrack(null) setCurrentSubTrack(null);
if (previousSubtitleUrlRef.current) { if (previousSubtitleUrlRef.current) {
URL.revokeObjectURL(previousSubtitleUrlRef.current); URL.revokeObjectURL(previousSubtitleUrlRef.current);
previousSubtitleUrlRef.current = null; previousSubtitleUrlRef.current = null;
} }
const remoteTracksList = playerRef.current?.remoteTextTracks(); const remoteTracksList = playerRef.current?.remoteTextTracks();
if (remoteTracksList) { if (remoteTracksList) {
const toRemove: TextTrack[] = []; const toRemove: TextTrack[] = [];
// Bypass TS restrictions safely // Bypass TS restrictions safely
const list = remoteTracksList as unknown as { const list = remoteTracksList as unknown as {
length: number; length: number;
[index: number]: TextTrack; [index: number]: TextTrack;
}; };
for (let i = 0; i < list.length; i++) { for (let i = 0; i < list.length; i++) {
const track = list[i]; const track = list[i];
if (track) toRemove.push(track); if (track) toRemove.push(track);
}
toRemove.forEach((track) => {
playerRef.current?.removeRemoteTextTrack(track);
});
} }
toRemove.forEach((track) => { return;
playerRef.current?.removeRemoteTextTrack(track);
});
}
return
} }
const player = playerRef.current; const player = playerRef.current;
if (!player || !subtitle.subtitleData || !subtitle.type) return; if (!player || !subtitle.subtitleData || !subtitle.type) return;
@ -618,7 +615,6 @@ setDrawerOpenPlayback(false)
(_, i) => (tracksInfo as any)[i] (_, i) => (tracksInfo as any)[i]
); );
for (const track of tracks) { for (const track of tracks) {
if (track.kind === "subtitles") { if (track.kind === "subtitles") {
track.mode = "showing"; // force display track.mode = "showing"; // force display
} }
@ -636,23 +632,21 @@ setDrawerOpenPlayback(false)
return JSON.stringify(qortalVideoResource); return JSON.stringify(qortalVideoResource);
}, [qortalVideoResource]); }, [qortalVideoResource]);
const savedVideoRef = useRef<HTMLVideoElement | null>(null); const savedVideoRef = useRef<HTMLVideoElement | null>(null);
useEffect(()=> { useEffect(() => {
if(startPlay){ if (startPlay) {
useGlobalPlayerStore.getState().reset() useGlobalPlayerStore.getState().reset();
} }
}, [startPlay]) }, [startPlay]);
useLayoutEffect(() => { useLayoutEffect(() => {
// Save the video element while it's still mounted // Save the video element while it's still mounted
const video = videoRef as any const video = videoRef as any;
if (video.current) { if (video.current) {
savedVideoRef.current = video.current; savedVideoRef.current = video.current;
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay)
@ -687,16 +681,15 @@ savedVideoRef.current = video.current;
playerRef.current = videojs(ref.current, options, () => { playerRef.current = videojs(ref.current, options, () => {
setIsPlayerInitialized(true); setIsPlayerInitialized(true);
ref.current.tabIndex = -1; // Prevents focus entirely ref.current.tabIndex = -1; // Prevents focus entirely
ref.current.style.outline = 'none'; // Backup ref.current.style.outline = "none"; // Backup
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){ if (videoLocationRef.current) {
const savedProgress = getProgress(videoLocationRef.current); const savedProgress = getProgress(videoLocationRef.current);
if (typeof savedProgress === "number") { if (typeof savedProgress === "number") {
playerRef.current?.currentTime(savedProgress); playerRef.current?.currentTime(savedProgress);
}
}
} }
playerRef.current?.play(); playerRef.current?.play();
@ -711,20 +704,18 @@ savedVideoRef.current = video.current;
(_, i) => (tracksInfo as any)[i] (_, i) => (tracksInfo as any)[i]
); );
for (const track of tracks) { for (const track of tracks) {
if (track.kind === "subtitles" || track.kind === "captions") {
if (track.kind === 'subtitles' || track.kind === 'captions') { if (track.mode === "showing") {
if (track.mode === 'showing') { activeTrack = track;
activeTrack = track; break;
break; }
} }
}
} }
if (activeTrack) { if (activeTrack) {
setCurrentSubTrack(activeTrack.language || activeTrack.srclang);
setCurrentSubTrack(activeTrack.language || activeTrack.srclang)
} else { } else {
setCurrentSubTrack(null) setCurrentSubTrack(null);
console.log("No subtitle is currently showing"); console.log("No subtitle is currently showing");
} }
}; };
@ -748,26 +739,26 @@ savedVideoRef.current = video.current;
console.error("useEffect start player", error); console.error("useEffect start player", error);
} }
return () => { return () => {
const video = savedVideoRef as any const video = savedVideoRef as any;
const videoEl = video?.current!; const videoEl = video?.current!;
const player = playerRef.current; const player = playerRef.current;
const isPlaying = !player?.paused(); const isPlaying = !player?.paused();
if (videoEl && isPlaying && videoLocationRef.current) { if (videoEl && isPlaying && videoLocationRef.current) {
const current = player?.currentTime?.(); const current = player?.currentTime?.();
const currentSource = player?.currentType(); const currentSource = player?.currentType();
useGlobalPlayerStore.getState().setVideoState({ useGlobalPlayerStore.getState().setVideoState({
videoSrc: videoEl.src, videoSrc: videoEl.src,
currentTime: current ?? 0, currentTime: current ?? 0,
isPlaying: true, isPlaying: true,
mode: 'floating', mode: "floating",
videoId: videoLocationRef.current, videoId: videoLocationRef.current,
location: locationRef.current || "", location: locationRef.current || "",
type: currentSource || 'video/mp4' type: currentSource || "video/mp4",
}); });
} }
canceled = true; canceled = true;
@ -800,10 +791,9 @@ savedVideoRef.current = video.current;
player.off("ratechange", handleRateChange); player.off("ratechange", handleRateChange);
}; };
}, [isPlayerInitialized]); }, [isPlayerInitialized]);
const hideTimeoutRef = useRef<number | null>(null); const hideTimeoutRef = useRef<number | null>(null);
const resetHideTimeout = () => {
const resetHideTimeout = () => {
setShowControlsMobile(true); setShowControlsMobile(true);
if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current);
hideTimeoutRef.current = setTimeout(() => { hideTimeoutRef.current = setTimeout(() => {
@ -817,16 +807,16 @@ savedVideoRef.current = video.current;
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
container.addEventListener('touchstart', handleInteraction); container.addEventListener("touchstart", handleInteraction);
// container.addEventListener('mousemove', handleInteraction); // container.addEventListener('mousemove', handleInteraction);
return () => { return () => {
container.removeEventListener('touchstart', handleInteraction); container.removeEventListener("touchstart", handleInteraction);
// container.removeEventListener('mousemove', handleInteraction); // container.removeEventListener('mousemove', handleInteraction);
}; };
}, []); }, []);
console.log('showControlsMobile', showControlsMobile) console.log("showControlsMobile", showControlsMobile);
return ( return (
<> <>
@ -865,11 +855,17 @@ savedVideoRef.current = video.current;
onVolumeChange={onVolumeChangeHandler} onVolumeChange={onVolumeChangeHandler}
controls={false} controls={false}
/> />
<PlayBackMenu isFromDrawer={false} close={closePlaybackMenu} isOpen={isOpenPlaybackMenu} onSelect={onSelectPlaybackRate} playbackRate={playbackRate} /> <PlayBackMenu
isFromDrawer={false}
close={closePlaybackMenu}
isOpen={isOpenPlaybackMenu}
onSelect={onSelectPlaybackRate}
playbackRate={playbackRate}
/>
{isReady && !showControlsMobile && ( {isReady && !showControlsMobile && (
<VideoControlsBar <VideoControlsBar
isVideoPlayerSmall={isVideoPlayerSmall} isVideoPlayerSmall={isVideoPlayerSmall}
subtitleBtnRef={subtitleBtnRef} subtitleBtnRef={subtitleBtnRef}
playbackRate={playbackRate} playbackRate={playbackRate}
increaseSpeed={hotkeyHandlers.increaseSpeed} increaseSpeed={hotkeyHandlers.increaseSpeed}
@ -896,78 +892,110 @@ savedVideoRef.current = video.current;
toggleMute={toggleMute} toggleMute={toggleMute}
openPlaybackMenu={openPlaybackMenu} openPlaybackMenu={openPlaybackMenu}
togglePictureInPicture={togglePictureInPicture} togglePictureInPicture={togglePictureInPicture}
setLocalProgress={setLocalProgress}
/>
)}
{timelineActions && Array.isArray(timelineActions) && (
<TimelineActionsComponent
seekTo={seekTo}
containerRef={containerRef}
progress={localProgress}
timelineActions={timelineActions}
/>
)}
{showControlsMobile && (
<MobileControls
setLocalProgress={setLocalProgress}
setProgressRelative={setProgressRelative}
toggleFullscreen={toggleFullscreen}
openPlaybackMenu={openPlaybackMenu}
openSubtitleManager={openSubtitleManager}
togglePlay={togglePlay}
isPlaying={isPlaying}
setShowControlsMobile={setShowControlsMobile}
duration={duration}
progress={localProgress}
playerRef={playerRef}
showControlsMobile={showControlsMobile}
/> />
)} )}
{timelineActions && Array.isArray(timelineActions) && (
<TimelineActionsComponent seekTo={seekTo} containerRef={containerRef} progress={localProgress} timelineActions={timelineActions}/>
)}
{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}
/>
)}
{!isVideoPlayerSmall && (
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={isOpenSubtitleManage}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
setDrawerOpenSubtitles={setDrawerOpenSubtitles}
isFromDrawer={false}
/>
)}
<ClickAwayListener onClickAway={() => setDrawerOpenSubtitles(false)}>
<Drawer
variant="persistent"
anchor="bottom"
open={drawerOpenSubtitles}
sx={{}}
slotProps={{
paper: {
sx: {
backgroundColor: alpha("#181818", 0.98),
borderRadius: 2,
width: "90%",
margin: "0 auto",
p: 1,
backgroundImage: "none",
mb: 1,
position: "absolute",
},
},
}}
>
<SubtitleManager
subtitleBtnRef={subtitleBtnRef}
close={closeSubtitleManager}
open={true}
qortalMetadata={qortalVideoResource}
onSelect={onSelectSubtitle}
currentSubTrack={currentSubTrack}
setDrawerOpenSubtitles={setDrawerOpenSubtitles}
isFromDrawer={true}
/>
</Drawer>
</ClickAwayListener>
<ClickAwayListener onClickAway={() => setDrawerOpenPlayback(false)}>
<Drawer
variant="persistent"
anchor="bottom"
open={drawerOpenPlayback}
sx={{}}
slotProps={{
paper: {
sx: {
backgroundColor: alpha("#181818", 0.98),
borderRadius: 2,
width: "90%",
margin: "0 auto",
p: 1,
backgroundImage: "none",
mb: 1,
position: "absolute",
},
},
}}
>
<PlayBackMenu
isFromDrawer
close={closePlaybackMenu}
isOpen={true}
onSelect={onSelectPlaybackRate}
playbackRate={playbackRate}
/>
</Drawer>
</ClickAwayListener>
</VideoContainer> </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

@ -155,7 +155,7 @@ useEffect(() => {
const aspectRatio = 0.75; // 300 / 400 = 3:4 const aspectRatio = 0.75; // 300 / 400 = 3:4
const maxWidthByScreen = screenWidth * 0.75; const maxWidthByScreen = screenWidth * 0.75;
const maxWidthByHeight = (screenHeight * 0.2) / aspectRatio; const maxWidthByHeight = (screenHeight * 0.3) / aspectRatio;
const maxWidth = savedWidthRef.current || Math.min(maxWidthByScreen, maxWidthByHeight); const maxWidth = savedWidthRef.current || Math.min(maxWidthByScreen, maxWidthByHeight);
const maxHeight = savedHeightRef.current || maxWidth * aspectRatio; const maxHeight = savedHeightRef.current || maxWidth * aspectRatio;