videoplayer tranported

This commit is contained in:
wil committed 2024-09-11 00:09:37 -04:00
1 parent bc4ca2f605
commit fccc6616de
10 files changed
+270 -68

No files matched your search

@@ -31,6 +31,7 @@ export const KonvaRootComponent = ({ className }: { className: string }) => {
return (
<>
<Button
className="absolute bottom-0 left-0"
onClick={() => {
uiAccess.errorDialogue.show({
title: "Test Error",
@@ -24,23 +24,23 @@ export const DragAndDropZone = ({ file, fileTypes }: Props) => {
const fileName = file && getFileName(file).toUpperCase();
const wrapperClassName = twMerge(
"group cursor-pointer p-3 bg-ui-panel",
"flex items-center gap-3.5",
"rounded-lg border-2 border-dashed border-ui-border",
"group cursor-pointer p-3 bg-gray-100",
!file && "aspect-video flex flex-col items-center justify-center gap-6",
file && "flex items-center gap-3.5",
// "rounded-lg border-2 border-dashed border-ui-border",
);
if (!file) {
return (
<div className={wrapperClassName}>
<FontAwesomeIcon icon={faFileArrowUp} className="text-4xl" />
<div className="flex flex-col gap-0">
<p className="font-medium">
<u>Upload a file</u> or drop it here
</p>
<p className="flex items-center gap-2 text-sm font-normal opacity-50">
{fileTypes.join(", ").toString().toUpperCase()} supported
</p>
</div>
<FontAwesomeIcon icon={faFileArrowUp} className="text-7xl" />
<p className="text-2xl font-medium">
<u>Upload a file</u> or drop it here
</p>
<p className="flex items-center gap-2 text-lg font-normal opacity-50">
Supported file types:{" "}
<b>{fileTypes.join(", ").toString().toUpperCase()}</b>
</p>
</div>
);
} else {
@@ -0,0 +1,95 @@
import { useCallback, useMemo, useRef, useState } from "react";
import { VIDEO_STATE_STATUSES } from "./VideoPlaybar/enum";
import { VideoPlaybar } from "./VideoPlaybar";
const MAX_TRIM_DURATION = 6000;
export const QuickTrimVideoPlayer = ({ file }: { file: File }) => {
const [trimData, setTrimData] = useState<{
videoDuration: number | undefined;
trimStartMs: number | undefined;
trimEndMs: number | undefined;
}>({
videoDuration: undefined,
trimStartMs: undefined,
trimEndMs: undefined,
});
const videoState = useMemo(() => {
if (trimData.videoDuration) {
return VIDEO_STATE_STATUSES.METADATA_LOADED;
}
return VIDEO_STATE_STATUSES.INIT;
}, [trimData.videoDuration]);
// const { trimStartMs, trimEndMs } = trimData;
const setTrimStartMs = useCallback((value: number) => {
setTrimData((prev) => {
if (!prev.trimEndMs || value > prev.trimEndMs) {
return prev;
}
if (value > prev.trimEndMs) {
return { ...prev, trimStartMs: prev.trimEndMs - 1 };
}
if (prev.trimEndMs - value >= MAX_TRIM_DURATION) {
return { ...prev, trimStartMs: prev.trimEndMs - MAX_TRIM_DURATION };
}
return { ...prev, trimStartMs: value };
});
}, []);
const setTrimEndMs = useCallback((value: number) => {
setTrimData((prev) => {
if (!prev.trimStartMs) {
return prev;
}
if (value < prev.trimStartMs) {
return { ...prev, trimEndMs: prev.trimStartMs + 1 };
}
if (value - prev.trimStartMs >= MAX_TRIM_DURATION) {
return { ...prev, trimEndMs: prev.trimStartMs + MAX_TRIM_DURATION };
}
return { ...prev, trimEndMs: value };
});
}, []);
const videoRef = useRef<HTMLVideoElement | undefined>(undefined);
const videoRefCallback = useCallback(
(node: HTMLVideoElement) => {
const handleLoadedmetadata = () => {
node.removeEventListener("loadedmetadata", handleLoadedmetadata);
setTrimData({
trimStartMs: 0,
trimEndMs: (node.duration ?? 0 >= 6000) ? 6000 : node.duration,
videoDuration: node.duration ?? 0,
});
};
if (node !== null) {
// DOM node referenced by ref has changed and exists
videoRef.current = node;
node.addEventListener("loadedmetadata", handleLoadedmetadata);
}
},
[
// No Dependency !
],
); //END videoRefCallback\
return (
<>
<div
className="relative w-full bg-black"
style={{ height: "calc(100vh - 500px)" }}
>
<video
className="max-w-8/12 mx-auto max-h-full w-full"
ref={videoRefCallback}
src={URL.createObjectURL(file)}
// className="border border-dashed border-white"
/>
</div>
<VideoPlaybar status={videoState} vidEl={videoRef.current} />
</>
);
};
@@ -0,0 +1,36 @@
import { useEffect, useState } from "react";
import { faVolumeSlash, faVolume } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "~/components/ui";
export function ButtonMute({ vidEl }: { vidEl: HTMLVideoElement }) {
const [isMuted, setIsMuted] = useState<boolean>(vidEl.muted || false);
const toggleMute = () => {
if (vidEl.muted) {
vidEl.muted = false;
} else {
vidEl.muted = true;
}
};
useEffect(() => {
vidEl.addEventListener("volumechange", () => {
if (vidEl.muted) {
setIsMuted(true);
} else {
setIsMuted(false);
}
});
}, [vidEl]);
return (
<Button
className="w-10"
icon={isMuted ? faVolumeSlash : faVolume}
variant="secondary"
onClick={toggleMute}
/>
);
}
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { faPlay, faPause } from "@fortawesome/pro-solid-svg-icons";
import { Button } from "~/components/ui";
export function ButtonPlaypause({ vidEl }: { vidEl: HTMLVideoElement }) {
const [playpause, setPlayPause] = useState<"playing" | "paused">("paused");
const togglePlaypause = () => {
if (playpause === "playing") {
vidEl.pause();
} else {
vidEl.play();
}
};
useEffect(() => {
const setPlaying = () => setPlayPause("playing");
const setPaused = () => setPlayPause("paused");
vidEl.addEventListener("play", setPlaying);
vidEl.addEventListener("pause", setPaused);
return () => {
vidEl.removeEventListener("play", setPlaying);
vidEl.removeEventListener("pause", setPaused);
};
}, [vidEl]);
return (
<Button
className="w-10"
icon={playpause === "playing" ? faPause : faPlay}
variant={playpause === "playing" ? "secondary" : "primary"}
onClick={togglePlaypause}
/>
);
}
@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
export function LabelTimeDuration({ vidEl }: { vidEl: HTMLVideoElement }) {
const [currentTime, setCurrentTime] = useState<number>(0);
useEffect(() => {
const handleTimeStamp = () => setCurrentTime(vidEl.currentTime || 0);
vidEl.addEventListener("timeupdate", handleTimeStamp);
return () => {
vidEl.removeEventListener("timeupdate", handleTimeStamp);
};
}, [vidEl]);
return (
<div className="flex items-center gap-1">
<p className="w-12">{`${formatSecondsToHHMMSSCS(currentTime)}`}</p>
<span>/</span>
<p className="w-12">{`${formatSecondsToHHMMSSCS(vidEl.duration)}`}</p>
</div>
);
}
function formatSecondsToHHMMSSCS(seconds: number) {
//example of the ISO String: 1970-01-01T00:01:40.774Z
const isoString = new Date(seconds * 1000).toISOString();
if (seconds > 3600)
return isoString.substring(11, 19) + "." + isoString.substring(20, 22);
else return isoString.substring(14, 19) + "." + isoString.substring(20, 22);
}
@@ -0,0 +1,37 @@
import { twMerge } from "tailwind-merge";
import { VIDEO_STATE_STATUSES } from "./enum";
import { ButtonPlaypause } from "./ButtonPlayPause";
// import { ButtonRepeat } from "./ButtonRepeat";
import { ButtonMute } from "./ButtonMute";
import { LabelTimeDuration } from "./LabelTimeDuration";
// import { SelectTrim } from "./SelectTrims";
import { Spinner } from "~/components/ui";
export const VideoPlaybar = ({
status,
vidEl,
}: {
status: VIDEO_STATE_STATUSES;
vidEl?: HTMLVideoElement;
}) => {
const wrapperClass =
"flex w-full h-10 justify-center items-center gap-2 bg-gray-100";
if (status === VIDEO_STATE_STATUSES.METADATA_LOADED && vidEl) {
return (
<div className={wrapperClass}>
<ButtonPlaypause vidEl={vidEl} />
<ButtonMute vidEl={vidEl} />
<LabelTimeDuration vidEl={vidEl} />
<div className="grow"></div>
</div>
);
}
return (
<div className={wrapperClass}>
<Spinner className="size-5" />
<span>Loading...</span>
</div>
);
};
@@ -0,0 +1,7 @@
export enum VIDEO_STATE_STATUSES {
INIT = "init",
METADATA_LOADED = "video_metadata_loaded",
ERROR_DURATION_TOO_SHORT = "error_video_too_short",
ERROR_LOAD_ORDER = "error_load_order",
ERROR_ACTION_TYPE = "error_no_such_action",
}
@@ -0,0 +1 @@
export * from "./VideoPlaybar";
+14 -56
View File
@@ -1,21 +1,14 @@
import { useState } from "react";
import { twMerge } from "tailwind-merge";
import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
import Cropper, { Point, Area } from "react-easy-crop";
import { FileUploader, VIDEO_FILE_TYPE } from "../FileUploader";
import { Button } from "~/components/ui";
import { paperWrapperStyles } from "~/components/styles";
import { dispatchUiEvents } from "~/signals/uiEvents";
import { QuickTrimVideoPlayer } from "./QuickTrimVideoPlayer";
function readFile(file: File) {
return new Promise<string | ArrayBuffer | null>((resolve) => {
const reader = new FileReader();
reader.addEventListener("load", () => resolve(reader.result), false);
reader.readAsDataURL(file);
});
}
export const UploadVideo = ({
isOpen,
closeCallback,
@@ -24,15 +17,6 @@ export const UploadVideo = ({
closeCallback: () => void;
}) => {
const [assetFile, setAssetFile] = useState<File | null>(null);
const [videoFile, setVideoFile] = useState<string | null>(null);
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const onCropComplete = (croppedArea: Area, croppedAreaPixels: Point) => {
console.log(croppedArea, croppedAreaPixels);
};
function handleClose() {
setAssetFile(null);
closeCallback();
@@ -51,48 +35,22 @@ export const UploadVideo = ({
<DialogPanel
className={twMerge(
paperWrapperStyles,
"w-full max-w-xl space-y-4 p-8",
"flex w-full max-w-5xl flex-col gap-4 px-6 py-4",
)}
>
<DialogTitle className="font-bold">Upload Video</DialogTitle>
<FileUploader
title=""
fileTypes={Object.values(VIDEO_FILE_TYPE)}
file={assetFile}
setFile={async (file: File | null) => {
setAssetFile(file);
if (file) {
let videoDataUrl = await readFile(file);
setVideoFile(videoDataUrl as string);
}
}}
/>
{assetFile && videoFile && (
<div className="relative flex aspect-video items-center justify-center rounded-xl bg-ui-border">
<label className="absolute left-0 top-0 rounded-br-xl rounded-tl-xl border border-ui-border bg-white p-2 shadow-md">
Preview
</label>
{/* <video
src={URL.createObjectURL(assetFile)}
controls
className="border border-dashed border-white"
/> */}
<div className="crop-container">
<Cropper
video={videoFile}
crop={crop}
zoom={zoom}
aspect={4 / 3}
onCropChange={setCrop}
onZoomChange={setZoom}
// onImageLoaded={(res:any) => {
// console.log(res);
// }}
/>
</div>
</div>
)}
<div className="flex w-full justify-end gap-4 pt-4">
<div className="flex flex-col rounded-lg border-2 border-dashed border-ui-border">
<FileUploader
title=""
fileTypes={Object.values(VIDEO_FILE_TYPE)}
file={assetFile}
setFile={async (file: File | null) => {
setAssetFile(file);
}}
/>
{assetFile && <QuickTrimVideoPlayer file={assetFile} />}
</div>
<div className="flex w-full justify-end gap-4">
<Button onClick={handleClose} variant="secondary">
Cancel
</Button>