193 lines
6.2 KiB
TypeScript
193 lines
6.2 KiB
TypeScript
|
import {
|
||
|
config,
|
||
|
CONTROL_BAR_HEIGHT,
|
||
|
VideoResizeKey,
|
||
|
YT_TYPE,
|
||
|
} from "components/apps/VideoPlayer/config";
|
||
|
import type {
|
||
|
SourceObjectWithUrl,
|
||
|
VideoPlayer,
|
||
|
} from "components/apps/VideoPlayer/types";
|
||
|
import { getMimeType } from "components/system/Files/FileEntry/functions";
|
||
|
import useTitle from "components/system/Window/useTitle";
|
||
|
import useWindowSize from "components/system/Window/useWindowSize";
|
||
|
import { useFileSystem } from "contexts/fileSystem";
|
||
|
import { useProcesses } from "contexts/process";
|
||
|
import { basename } from "path";
|
||
|
import { useCallback, useEffect, useState } from "react";
|
||
|
import { VIDEO_FALLBACK_MIME_TYPE } from "utils/constants";
|
||
|
import {
|
||
|
bufferToUrl,
|
||
|
cleanUpBufferUrl,
|
||
|
isSafari,
|
||
|
isYouTubeUrl,
|
||
|
loadFiles,
|
||
|
viewHeight,
|
||
|
viewWidth,
|
||
|
} from "utils/functions";
|
||
|
|
||
|
const useVideoPlayer = (
|
||
|
id: string,
|
||
|
url: string,
|
||
|
containerRef: React.MutableRefObject<HTMLDivElement | null>,
|
||
|
setLoading: React.Dispatch<React.SetStateAction<boolean>>,
|
||
|
loading: boolean
|
||
|
): void => {
|
||
|
const { readFile } = useFileSystem();
|
||
|
const {
|
||
|
linkElement,
|
||
|
processes: { [id]: { closing = false, libs = [] } = {} },
|
||
|
} = useProcesses();
|
||
|
const { updateWindowSize } = useWindowSize(id);
|
||
|
const [player, setPlayer] = useState<VideoPlayer>();
|
||
|
const { prependFileToTitle } = useTitle(id);
|
||
|
const cleanUpSource = useCallback((): void => {
|
||
|
const { src: sources = [] } = player?.getMedia() || {};
|
||
|
|
||
|
if (Array.isArray(sources) && sources.length > 0) {
|
||
|
const [{ src, url: sourceUrl }] = sources as SourceObjectWithUrl[];
|
||
|
|
||
|
if (src.startsWith("blob:") && (sourceUrl !== url || closing)) {
|
||
|
cleanUpBufferUrl(src);
|
||
|
}
|
||
|
}
|
||
|
}, [closing, player, url]);
|
||
|
const getSource = useCallback(async () => {
|
||
|
cleanUpSource();
|
||
|
|
||
|
const isYT = isYouTubeUrl(url);
|
||
|
const type = isYT ? YT_TYPE : getMimeType(url) || VIDEO_FALLBACK_MIME_TYPE;
|
||
|
const src = isYT
|
||
|
? url
|
||
|
: bufferToUrl(await readFile(url), isSafari() ? type : undefined);
|
||
|
|
||
|
return { src, type, url };
|
||
|
}, [cleanUpSource, readFile, url]);
|
||
|
const loadPlayer = useCallback(() => {
|
||
|
const [videoElement] =
|
||
|
(containerRef.current?.childNodes as NodeListOf<HTMLVideoElement>) ?? [];
|
||
|
const videoPlayer = window.videojs(videoElement, config, () => {
|
||
|
videoPlayer.on("firstplay", () => {
|
||
|
const [height, width] = [
|
||
|
videoPlayer.videoHeight(),
|
||
|
videoPlayer.videoWidth(),
|
||
|
];
|
||
|
const [vh, vw] = [viewHeight(), viewWidth()];
|
||
|
|
||
|
if (height && width) {
|
||
|
const heightWithControlBar = CONTROL_BAR_HEIGHT + height;
|
||
|
|
||
|
if (heightWithControlBar > vh || width > vw) {
|
||
|
updateWindowSize(vw * (heightWithControlBar / width), vw);
|
||
|
} else {
|
||
|
updateWindowSize(heightWithControlBar, width);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
const toggleFullscreen = (): void => {
|
||
|
try {
|
||
|
if (videoPlayer.isFullscreen()) videoPlayer.exitFullscreen();
|
||
|
else videoPlayer.requestFullscreen();
|
||
|
} catch {
|
||
|
// Ignore fullscreen errors
|
||
|
}
|
||
|
};
|
||
|
|
||
|
videoElement.addEventListener("dblclick", toggleFullscreen);
|
||
|
videoElement.addEventListener("mousewheel", (event) => {
|
||
|
videoPlayer.volume(
|
||
|
videoPlayer.volume() + ((event as WheelEvent).deltaY > 0 ? -0.1 : 0.1)
|
||
|
);
|
||
|
});
|
||
|
containerRef.current
|
||
|
?.closest("section")
|
||
|
?.addEventListener("keydown", ({ key, altKey, ctrlKey }) => {
|
||
|
if (altKey) {
|
||
|
if (VideoResizeKey[key]) {
|
||
|
updateWindowSize(
|
||
|
videoPlayer.videoHeight() / VideoResizeKey[key],
|
||
|
videoPlayer.videoWidth() / VideoResizeKey[key]
|
||
|
);
|
||
|
} else if (key === "Enter") {
|
||
|
toggleFullscreen();
|
||
|
}
|
||
|
} else if (!ctrlKey) {
|
||
|
// eslint-disable-next-line default-case
|
||
|
switch (key) {
|
||
|
case " ":
|
||
|
if (videoPlayer.paused()) videoPlayer.play();
|
||
|
else videoPlayer.pause();
|
||
|
break;
|
||
|
case "ArrowUp":
|
||
|
videoPlayer.volume(videoPlayer.volume() + 0.1);
|
||
|
break;
|
||
|
case "ArrowDown":
|
||
|
videoPlayer.volume(videoPlayer.volume() - 0.1);
|
||
|
break;
|
||
|
case "ArrowLeft":
|
||
|
videoPlayer.currentTime(videoPlayer.currentTime() - 10);
|
||
|
break;
|
||
|
case "ArrowRight":
|
||
|
videoPlayer.currentTime(videoPlayer.currentTime() + 10);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
setPlayer(videoPlayer);
|
||
|
setLoading(false);
|
||
|
if (!isYouTubeUrl(url)) linkElement(id, "peekElement", videoElement);
|
||
|
});
|
||
|
}, [containerRef, id, linkElement, setLoading, updateWindowSize, url]);
|
||
|
const maybeHideControlbar = useCallback(
|
||
|
(type?: string): void => {
|
||
|
const controlBar =
|
||
|
containerRef.current?.querySelector(".vjs-control-bar");
|
||
|
|
||
|
if (controlBar instanceof HTMLElement) {
|
||
|
if (type === YT_TYPE) {
|
||
|
controlBar.classList.add("no-interaction");
|
||
|
} else {
|
||
|
controlBar.classList.remove("no-interaction");
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
[containerRef]
|
||
|
);
|
||
|
const loadVideo = useCallback(async () => {
|
||
|
if (player && url) {
|
||
|
try {
|
||
|
const source = await getSource();
|
||
|
|
||
|
player.src(source);
|
||
|
maybeHideControlbar(source.type);
|
||
|
prependFileToTitle(isYouTubeUrl(url) ? "YouTube" : basename(url));
|
||
|
} catch {
|
||
|
// Ignore player errors
|
||
|
}
|
||
|
}
|
||
|
}, [getSource, maybeHideControlbar, player, prependFileToTitle, url]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (loading && !player) {
|
||
|
loadFiles(libs).then(() => {
|
||
|
if (window.videojs !== undefined) {
|
||
|
loadPlayer();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return () => {
|
||
|
if (closing) {
|
||
|
cleanUpSource();
|
||
|
player?.dispose();
|
||
|
}
|
||
|
};
|
||
|
}, [cleanUpSource, closing, libs, loadPlayer, loading, player]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (!loading && !closing && player && url) loadVideo();
|
||
|
}, [closing, loadVideo, loading, player, url]);
|
||
|
};
|
||
|
|
||
|
export default useVideoPlayer;
|