Backend MP4 conversion (#23)
* ADD conversion queue * ADD RemuxService for MP4 conversion * REMOVE unused conversion queue * REORGANISE Job-related classes * ADD Job stages * REVERT to old commit, using Spring Async instead * ADD asynchronous processing for video tasks * PATCH and streamline progress tracking * ADD asynchronous video processing and job restructuring * REFACTOR job service method * ADD job remux functionality * ADD remuxing endpoint * PATCH complete flag not updating in API response * ADD progress type in frontend * ADD reset functionality for job status * PATCH missing progress bar for subsequent exports * REDESIGN settings box * ADD tracking video file conversion in frontend * PATCH extension bug * REMOVE autowired decorator
This commit is contained in:
@@ -9,7 +9,7 @@ type prop = {
|
||||
|
||||
export default function ConfigBox({setMetadata, className}: prop) {
|
||||
const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
var vals = e.target.value.split(",");
|
||||
const vals = e.target.value.split(",");
|
||||
setMetadata((prevState: VideoMetadata) => ({
|
||||
...prevState,
|
||||
width: parseInt(vals[0]),
|
||||
@@ -33,14 +33,14 @@ export default function ConfigBox({setMetadata, className}: prop) {
|
||||
|
||||
return (
|
||||
<div className={clsx("flex flex-col gap-2 p-10", className)}>
|
||||
<h2 className={"text-3xl font-bold mb-4"}>Export Settings</h2>
|
||||
<h2 className={"text-xl font-bold"}>Export</h2>
|
||||
|
||||
<Selector label={"Resolution"}>
|
||||
<select id="resolution"
|
||||
name="resolution"
|
||||
defaultValue="1280,720"
|
||||
onChange={updateRes}
|
||||
className={"border-black bg-gray-200 rounded-md w-full"}>
|
||||
className={"border-black bg-gray-200 rounded-md w-full p-2"}>
|
||||
<option value="3840,2160">2160p</option>
|
||||
<option value="2560,1440">1440p</option>
|
||||
<option value="1920,1080">1080p</option>
|
||||
@@ -55,7 +55,7 @@ export default function ConfigBox({setMetadata, className}: prop) {
|
||||
name="fps"
|
||||
defaultValue="30"
|
||||
onChange={updateFps}
|
||||
className={"border-black bg-gray-200 rounded-md w-full"}>
|
||||
className={"border-black bg-gray-200 rounded-md w-full p-2"}>
|
||||
<option value="60">60</option>
|
||||
<option value="30">30</option>
|
||||
<option value="15">15</option>
|
||||
@@ -67,7 +67,7 @@ export default function ConfigBox({setMetadata, className}: prop) {
|
||||
min="1"
|
||||
defaultValue="10"
|
||||
onChange={updateFileSize}
|
||||
className={"border-black bg-gray-200 rounded-md w-full"}
|
||||
className={"border-black bg-gray-200 rounded-md w-full p-2"}
|
||||
/>
|
||||
</Selector>
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import clsx from "clsx";
|
||||
import type {VideoMetadata} from "../../utils/types.ts";
|
||||
import Selector from "../Selector.tsx";
|
||||
|
||||
type MetadataBoxProps = {
|
||||
setMetadata: Function
|
||||
@@ -10,30 +9,18 @@ type MetadataBoxProps = {
|
||||
const MetadataBox = ({setMetadata, className}: MetadataBoxProps) => {
|
||||
return (
|
||||
<div className={clsx("flex flex-col content-between p-10 gap-2", className)}>
|
||||
<h2 className={"text-3xl font-bold mb-4 col-span-2"}>Metadata Settings</h2>
|
||||
{/*<h2 className={"text-2xl font-bold col-span-2"}>Metadata</h2>*/}
|
||||
|
||||
<Selector label={"Title"}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter title"
|
||||
onChange={(e) => setMetadata((prevState: VideoMetadata) => ({
|
||||
...prevState,
|
||||
title: e.target.value
|
||||
}))}
|
||||
className={"border-black bg-gray-200 rounded-md w-full p-2"}
|
||||
/>
|
||||
</Selector>
|
||||
|
||||
<Selector label={"Description"}>
|
||||
<textarea
|
||||
placeholder="Enter description"
|
||||
onChange={(e) => setMetadata((prevState: VideoMetadata) => ({
|
||||
...prevState,
|
||||
description: e.target.value
|
||||
}))}
|
||||
className={"border-black bg-gray-200 rounded-md w-full p-2 pb-10 resize-none max-h"}
|
||||
/>
|
||||
</Selector>
|
||||
<p className={"w-full font-bold text-xl "}>Title</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter title"
|
||||
onChange={(e) => setMetadata((prevState: VideoMetadata) => ({
|
||||
...prevState,
|
||||
title: e.target.value
|
||||
}))}
|
||||
className={"border-black bg-gray-200 rounded-md w-full p-2"}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,11 +49,13 @@ const ClipEdit = () => {
|
||||
const pollProgress = async (id: string, intervalId: number) => {
|
||||
getProgress(id)
|
||||
.then((progress) => {
|
||||
setProgress(progress);
|
||||
setProgress(progress.process.progress);
|
||||
|
||||
if (progress >= 1) {
|
||||
if (progress.process.complete) {
|
||||
clearInterval(intervalId);
|
||||
setDownloadable(true);
|
||||
} else {
|
||||
setDownloadable(false)
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
|
||||
@@ -1,25 +1,75 @@
|
||||
import {useState} from "react";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
import { uploadFile } from "../utils/endpoints"
|
||||
import {uploadFile, convertFile, getProgress} from "../utils/endpoints"
|
||||
import BlueButton from "../components/buttons/BlueButton.tsx";
|
||||
import Box from "../components/Box.tsx";
|
||||
|
||||
const clipUpload = () => {
|
||||
const ClipUpload = () => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [error, setError] = useState<null | string>(null);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
const isVideoFileSupported = (file: File): boolean => {
|
||||
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) {
|
||||
setError("Please choose a file");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
uploadFile(file)
|
||||
.then(uuid => navigate(`/create/${uuid}`))
|
||||
.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 (
|
||||
<Box className={"flex flex-col justify-between gap-3 p-5"}>
|
||||
<input
|
||||
@@ -37,8 +87,12 @@ const clipUpload = () => {
|
||||
</BlueButton>
|
||||
|
||||
<label className={"text-center text-red-500"}>{error}</label>
|
||||
<progress
|
||||
value={progress}
|
||||
className={"bg-gray-300 rounded-lg h-1"}>
|
||||
</progress>
|
||||
</Box>
|
||||
)
|
||||
};
|
||||
|
||||
export default clipUpload;
|
||||
export default ClipUpload;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {VideoMetadata, APIResponse, User, Clip} from "./types.ts";
|
||||
import type {VideoMetadata, APIResponse, User, Clip, ProgressResult } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Uploads a file to the backend.
|
||||
@@ -74,11 +74,25 @@ const processFile = async (uuid: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const convertFile = async (uuid: string) => {
|
||||
const response = await fetch(`/api/v1/convert/${uuid}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to convert file: ${response.status}`);
|
||||
}
|
||||
|
||||
const result: APIResponse = await response.json();
|
||||
|
||||
if (result.status === "error") {
|
||||
throw new Error("Failed to convert file: " + result.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the processing progress percentage.
|
||||
* @param uuid - The UUID of the video file.
|
||||
*/
|
||||
const getProgress = async (uuid: string): Promise<number> => {
|
||||
const getProgress = async (uuid: string): Promise<ProgressResult> => {
|
||||
const response = await fetch(`/api/v1/progress/${uuid}`);
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -91,11 +105,11 @@ const getProgress = async (uuid: string): Promise<number> => {
|
||||
throw new Error(`Failed to fetch progress: ${result.message}`);
|
||||
}
|
||||
|
||||
if (!result.data || typeof result.data.progress !== 'number') {
|
||||
if (!result.data) {
|
||||
throw new Error('Invalid progress data received');
|
||||
}
|
||||
|
||||
return result.data.progress;
|
||||
return result.data;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -189,6 +203,7 @@ export {
|
||||
uploadFile,
|
||||
editFile,
|
||||
processFile,
|
||||
convertFile,
|
||||
getProgress,
|
||||
getMetadata,
|
||||
getUser,
|
||||
|
||||
@@ -30,9 +30,21 @@ type Clip = {
|
||||
createdAt: string,
|
||||
}
|
||||
|
||||
type ProgressResult = {
|
||||
process: {
|
||||
progress: number,
|
||||
complete: boolean
|
||||
};
|
||||
conversion: {
|
||||
progress: number,
|
||||
complete: boolean
|
||||
};
|
||||
};
|
||||
|
||||
export type {
|
||||
APIResponse,
|
||||
VideoMetadata,
|
||||
User,
|
||||
Clip
|
||||
Clip,
|
||||
ProgressResult
|
||||
}
|
||||
Reference in New Issue
Block a user