feat(frontend): remove server-side upload process and switch to browser process
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import Playbar from "./../components/video/Playbar";
|
import Playbar from "./../components/video/Playbar";
|
||||||
import PlaybackSlider from "./../components/video/PlaybackSlider";
|
import PlaybackSlider from "./../components/video/PlaybackSlider";
|
||||||
import ClipRangeSlider from "./../components/video/ClipRangeSlider";
|
import ClipRangeSlider from "./../components/video/ClipRangeSlider";
|
||||||
import ConfigBox from "../components/video/ConfigBox.tsx";
|
import ConfigBox from "../components/video/ConfigBox.tsx";
|
||||||
import ExportWidget from "../components/video/ExportWidget.tsx";
|
import ExportWidget from "../components/video/ExportWidget.tsx";
|
||||||
import {editFile, getMetadata, processFile, getProgress} from "../utils/endpoints"
|
import { editFile, processFile, getProgress, uploadFile } from "../utils/endpoints"
|
||||||
import type { VideoMetadata } from "../utils/types.ts";
|
import type { VideoMetadata } from "../utils/types.ts";
|
||||||
import Box from "../components/Box.tsx";
|
import Box from "../components/Box.tsx";
|
||||||
import MetadataBox from "../components/video/MetadataBox.tsx";
|
import MetadataBox from "../components/video/MetadataBox.tsx";
|
||||||
@@ -13,13 +13,16 @@ import MetadataBox from "../components/video/MetadataBox.tsx";
|
|||||||
const API_URL = import.meta.env.VITE_API_URL;
|
const API_URL = import.meta.env.VITE_API_URL;
|
||||||
|
|
||||||
const ClipEdit = () => {
|
const ClipEdit = () => {
|
||||||
const { id } = useParams();
|
const location = useLocation();
|
||||||
|
const localFile: File = location.state?.file;
|
||||||
|
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
const videoUrl = API_URL + `/api/v1/download/input/${id}`
|
const [videoUrl, setVideoUrl] = useState<string>('');
|
||||||
|
const [uploadedId, setUploadedId] = useState<string | null>(null);
|
||||||
|
|
||||||
const [metadata, setMetadata] = useState<VideoMetadata | null>(null);
|
const [metadata, setMetadata] = useState<VideoMetadata | null>(null);
|
||||||
const [playbackValue, setPlaybackValue] = useState(0);
|
const [playbackValue, setPlaybackValue] = useState(0);
|
||||||
const [outputMetadata, setOutputMetadata] = useState<VideoMetadata>({
|
const [outputMetadata, setOutputMetadata] = useState<VideoMetadata>({
|
||||||
// default values
|
|
||||||
title: "",
|
title: "",
|
||||||
description: "",
|
description: "",
|
||||||
startPoint: 0,
|
startPoint: 0,
|
||||||
@@ -33,20 +36,47 @@ const ClipEdit = () => {
|
|||||||
const [downloadable, setDownloadable] = useState(false);
|
const [downloadable, setDownloadable] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const sendData = async() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!localFile) return;
|
||||||
|
const url = URL.createObjectURL(localFile);
|
||||||
|
setVideoUrl(url);
|
||||||
|
return () => URL.revokeObjectURL(url);
|
||||||
|
}, [localFile]);
|
||||||
|
|
||||||
editFile(id, outputMetadata)
|
const handleVideoMetadataLoaded = () => {
|
||||||
.then(() => {
|
const video = videoRef.current;
|
||||||
|
if (!video) return;
|
||||||
|
setMetadata({
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
startPoint: 0,
|
||||||
|
duration: video.duration,
|
||||||
|
width: video.videoWidth,
|
||||||
|
height: video.videoHeight,
|
||||||
|
fps: 30,
|
||||||
|
fileSize: localFile?.size ?? 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
processFile(id)
|
const sendData = async () => {
|
||||||
.catch((err: Error) => setError(`Failed to process file: ${err.message}`));
|
if (!localFile) return;
|
||||||
|
|
||||||
})
|
let fileId: string;
|
||||||
|
try {
|
||||||
|
fileId = await uploadFile(localFile);
|
||||||
|
setUploadedId(fileId);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
setError(`Failed to upload file: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editFile(fileId, outputMetadata)
|
||||||
|
.then(() => processFile(fileId)
|
||||||
|
.catch((err: Error) => setError(`Failed to process file: ${err.message}`)))
|
||||||
.catch((err: Error) => setError(`Failed to edit file: ${err.message}`));
|
.catch((err: Error) => setError(`Failed to edit file: ${err.message}`));
|
||||||
|
|
||||||
const interval = setInterval(async() => await pollProgress(id, interval), 500);
|
const interval = setInterval(async () => await pollProgress(fileId, interval), 500);
|
||||||
}
|
};
|
||||||
|
|
||||||
const pollProgress = async (id: string, intervalId: number) => {
|
const pollProgress = async (id: string, intervalId: number) => {
|
||||||
getProgress(id)
|
getProgress(id)
|
||||||
@@ -57,17 +87,19 @@ const ClipEdit = () => {
|
|||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
setDownloadable(true);
|
setDownloadable(true);
|
||||||
} else {
|
} else {
|
||||||
setDownloadable(false)
|
setDownloadable(false);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
setError(`Failed to fetch progress: ${err.message}`);
|
setError(`Failed to fetch progress: ${err.message}`);
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
const response = await fetch(API_URL + `/api/v1/download/output/${id}`);
|
if (!uploadedId) return;
|
||||||
|
|
||||||
|
const response = await fetch(API_URL + `/api/v1/download/output/${uploadedId}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
console.error('Download failed');
|
console.error('Download failed');
|
||||||
@@ -79,43 +111,27 @@ const ClipEdit = () => {
|
|||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.download = `${id}.mp4`;
|
a.download = `${uploadedId}.mp4`;
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
a.remove();
|
a.remove();
|
||||||
window.URL.revokeObjectURL(url);
|
window.URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!id) return;
|
|
||||||
|
|
||||||
getMetadata(id)
|
|
||||||
.then((data) => setMetadata(data ?? null))
|
|
||||||
.catch((err) => console.error("Metadata fetch failed:", err));
|
|
||||||
}, [id]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"grid grid-cols-[7fr_3fr] gap-4 p-4"}>
|
<div className={"grid grid-cols-[7fr_3fr] gap-4 p-4"}>
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className={"w-full rounded-lg shadow-sm bg-black"}>
|
src={videoUrl}
|
||||||
<source src={videoUrl} type="video/mp4" />
|
className={"w-full rounded-lg shadow-sm bg-black"}
|
||||||
<source src={videoUrl} type="video/webm" />
|
onLoadedMetadata={handleVideoMetadataLoaded}
|
||||||
<source src={videoUrl} type="video/ogg" />
|
/>
|
||||||
Your browser does not support the video tag. Bzzzz.
|
|
||||||
</video>
|
|
||||||
|
|
||||||
|
|
||||||
<Box className={"w-4/5 h-full m-auto"}>
|
<Box className={"w-4/5 h-full m-auto"}>
|
||||||
<MetadataBox
|
<MetadataBox setMetadata={setOutputMetadata} />
|
||||||
setMetadata={setOutputMetadata}
|
<ConfigBox setMetadata={setOutputMetadata} />
|
||||||
/>
|
|
||||||
<ConfigBox
|
|
||||||
setMetadata={setOutputMetadata}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
{metadata &&
|
{metadata &&
|
||||||
<Box className={"mt-4 p-5"}>
|
<Box className={"mt-4 p-5"}>
|
||||||
<Playbar
|
<Playbar
|
||||||
@@ -158,6 +174,6 @@ const ClipEdit = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ClipEdit;
|
export default ClipEdit;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
import {uploadFile, convertFile, getProgress} from "../utils/endpoints"
|
|
||||||
import BlueButton from "../components/buttons/BlueButton.tsx";
|
import BlueButton from "../components/buttons/BlueButton.tsx";
|
||||||
import Box from "../components/Box.tsx";
|
import Box from "../components/Box.tsx";
|
||||||
|
|
||||||
@@ -8,63 +7,14 @@ const ClipUpload = () => {
|
|||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [error, setError] = useState<null | string>(null);
|
const [error, setError] = useState<null | string>(null);
|
||||||
const [progress, setProgress] = useState<number>(0);
|
|
||||||
|
|
||||||
const isVideoFileSupported = (file: File): boolean => {
|
const press = () => {
|
||||||
const video = document.createElement("video");
|
|
||||||
|
|
||||||
if (file.type && video.canPlayType(file.type) !== "") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const extension = file.name.split(".").pop()?.toLowerCase();
|
|
||||||
const extensionToMime: Record<string, string> = {
|
|
||||||
mp4: "video/mp4",
|
|
||||||
webm: "video/webm",
|
|
||||||
ogg: "video/ogg",
|
|
||||||
mov: "video/quicktime",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (extension && extensionToMime[extension]) {
|
|
||||||
return video.canPlayType(extensionToMime[extension]) !== "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const press = (() => {
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setError("Please choose a file");
|
setError("Please choose a file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
navigate('/create/new', { state: { file } });
|
||||||
uploadFile(file)
|
};
|
||||||
.then(uuid => {
|
|
||||||
if (isVideoFileSupported(file)) {
|
|
||||||
navigate(`/create/${uuid}`)
|
|
||||||
} else {
|
|
||||||
convertFile(uuid);
|
|
||||||
const interval = setInterval(async() => await pollProgress(uuid, interval), 500);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e: Error) => setError(`Failed to upload file: ${e.message}`));
|
|
||||||
});
|
|
||||||
|
|
||||||
const pollProgress = async (id: string, intervalId: number) => {
|
|
||||||
getProgress(id)
|
|
||||||
.then((progress) => {
|
|
||||||
setProgress(progress.conversion.progress);
|
|
||||||
|
|
||||||
if (progress.conversion.complete) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
navigate(`/create/${id}`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: Error) => {
|
|
||||||
setError(`Failed to fetch progress: ${err.message}`);
|
|
||||||
clearInterval(intervalId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start justify-center p-8 min-h-full">
|
<div className="flex items-start justify-center p-8 min-h-full">
|
||||||
@@ -84,8 +34,6 @@ const ClipUpload = () => {
|
|||||||
<BlueButton onClick={press}>Upload</BlueButton>
|
<BlueButton onClick={press}>Upload</BlueButton>
|
||||||
|
|
||||||
{error && <p className="text-sm text-red-500 text-center">{error}</p>}
|
{error && <p className="text-sm text-red-500 text-center">{error}</p>}
|
||||||
|
|
||||||
<progress value={progress} className="w-full h-1 rounded bg-gray-200" />
|
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user