feat(frontend): remove server-side upload process and switch to browser process

This commit is contained in:
2026-06-20 22:04:42 +01:00
parent b05ae62230
commit 9185a41147
2 changed files with 63 additions and 99 deletions

View File

@@ -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;

View File

@@ -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>
) )