mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
videoplayer tranported
This commit is contained in:
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";
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user