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) {
|
export default function ConfigBox({setMetadata, className}: prop) {
|
||||||
const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const updateRes = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
var vals = e.target.value.split(",");
|
const vals = e.target.value.split(",");
|
||||||
setMetadata((prevState: VideoMetadata) => ({
|
setMetadata((prevState: VideoMetadata) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
width: parseInt(vals[0]),
|
width: parseInt(vals[0]),
|
||||||
@@ -33,14 +33,14 @@ export default function ConfigBox({setMetadata, className}: prop) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx("flex flex-col gap-2 p-10", className)}>
|
<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"}>
|
<Selector label={"Resolution"}>
|
||||||
<select id="resolution"
|
<select id="resolution"
|
||||||
name="resolution"
|
name="resolution"
|
||||||
defaultValue="1280,720"
|
defaultValue="1280,720"
|
||||||
onChange={updateRes}
|
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="3840,2160">2160p</option>
|
||||||
<option value="2560,1440">1440p</option>
|
<option value="2560,1440">1440p</option>
|
||||||
<option value="1920,1080">1080p</option>
|
<option value="1920,1080">1080p</option>
|
||||||
@@ -55,7 +55,7 @@ export default function ConfigBox({setMetadata, className}: prop) {
|
|||||||
name="fps"
|
name="fps"
|
||||||
defaultValue="30"
|
defaultValue="30"
|
||||||
onChange={updateFps}
|
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="60">60</option>
|
||||||
<option value="30">30</option>
|
<option value="30">30</option>
|
||||||
<option value="15">15</option>
|
<option value="15">15</option>
|
||||||
@@ -67,7 +67,7 @@ export default function ConfigBox({setMetadata, className}: prop) {
|
|||||||
min="1"
|
min="1"
|
||||||
defaultValue="10"
|
defaultValue="10"
|
||||||
onChange={updateFileSize}
|
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>
|
</Selector>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import type {VideoMetadata} from "../../utils/types.ts";
|
import type {VideoMetadata} from "../../utils/types.ts";
|
||||||
import Selector from "../Selector.tsx";
|
|
||||||
|
|
||||||
type MetadataBoxProps = {
|
type MetadataBoxProps = {
|
||||||
setMetadata: Function
|
setMetadata: Function
|
||||||
@@ -10,9 +9,9 @@ type MetadataBoxProps = {
|
|||||||
const MetadataBox = ({setMetadata, className}: MetadataBoxProps) => {
|
const MetadataBox = ({setMetadata, className}: MetadataBoxProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={clsx("flex flex-col content-between p-10 gap-2", className)}>
|
<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"}>
|
<p className={"w-full font-bold text-xl "}>Title</p>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter title"
|
placeholder="Enter title"
|
||||||
@@ -22,18 +21,6 @@ const MetadataBox = ({setMetadata, className}: MetadataBoxProps) => {
|
|||||||
}))}
|
}))}
|
||||||
className={"border-black bg-gray-200 rounded-md w-full p-2"}
|
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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,11 +49,13 @@ const ClipEdit = () => {
|
|||||||
const pollProgress = async (id: string, intervalId: number) => {
|
const pollProgress = async (id: string, intervalId: number) => {
|
||||||
getProgress(id)
|
getProgress(id)
|
||||||
.then((progress) => {
|
.then((progress) => {
|
||||||
setProgress(progress);
|
setProgress(progress.process.progress);
|
||||||
|
|
||||||
if (progress >= 1) {
|
if (progress.process.complete) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
setDownloadable(true);
|
setDownloadable(true);
|
||||||
|
} else {
|
||||||
|
setDownloadable(false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
|
|||||||
@@ -1,25 +1,75 @@
|
|||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {useNavigate} from "react-router-dom";
|
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 BlueButton from "../components/buttons/BlueButton.tsx";
|
||||||
import Box from "../components/Box.tsx";
|
import Box from "../components/Box.tsx";
|
||||||
|
|
||||||
const clipUpload = () => {
|
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 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 = (() => {
|
const press = (() => {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setError("Please choose a file");
|
setError("Please choose a file");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
uploadFile(file)
|
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}`));
|
.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 (
|
||||||
<Box className={"flex flex-col justify-between gap-3 p-5"}>
|
<Box className={"flex flex-col justify-between gap-3 p-5"}>
|
||||||
<input
|
<input
|
||||||
@@ -37,8 +87,12 @@ const clipUpload = () => {
|
|||||||
</BlueButton>
|
</BlueButton>
|
||||||
|
|
||||||
<label className={"text-center text-red-500"}>{error}</label>
|
<label className={"text-center text-red-500"}>{error}</label>
|
||||||
|
<progress
|
||||||
|
value={progress}
|
||||||
|
className={"bg-gray-300 rounded-lg h-1"}>
|
||||||
|
</progress>
|
||||||
</Box>
|
</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.
|
* 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.
|
* Fetches the processing progress percentage.
|
||||||
* @param uuid - The UUID of the video file.
|
* @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}`);
|
const response = await fetch(`/api/v1/progress/${uuid}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -91,11 +105,11 @@ const getProgress = async (uuid: string): Promise<number> => {
|
|||||||
throw new Error(`Failed to fetch progress: ${result.message}`);
|
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');
|
throw new Error('Invalid progress data received');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.data.progress;
|
return result.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,6 +203,7 @@ export {
|
|||||||
uploadFile,
|
uploadFile,
|
||||||
editFile,
|
editFile,
|
||||||
processFile,
|
processFile,
|
||||||
|
convertFile,
|
||||||
getProgress,
|
getProgress,
|
||||||
getMetadata,
|
getMetadata,
|
||||||
getUser,
|
getUser,
|
||||||
|
|||||||
@@ -30,9 +30,21 @@ type Clip = {
|
|||||||
createdAt: string,
|
createdAt: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ProgressResult = {
|
||||||
|
process: {
|
||||||
|
progress: number,
|
||||||
|
complete: boolean
|
||||||
|
};
|
||||||
|
conversion: {
|
||||||
|
progress: number,
|
||||||
|
complete: boolean
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
APIResponse,
|
APIResponse,
|
||||||
VideoMetadata,
|
VideoMetadata,
|
||||||
User,
|
User,
|
||||||
Clip
|
Clip,
|
||||||
|
ProgressResult
|
||||||
}
|
}
|
||||||
@@ -2,10 +2,12 @@ package com.ddf.vodsystem;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@EnableAsync
|
||||||
public class VodSystemApplication {
|
public class VodSystemApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ddf.vodsystem.configuration;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
@Bean(name = "ffmpegTaskExecutor")
|
||||||
|
public Executor taskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
executor.setCorePoolSize(4);
|
||||||
|
executor.setMaxPoolSize(8);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("ffmpegExecutor-");
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ public class SecurityConfig {
|
|||||||
.requestMatchers("/api/v1/download/clip/**").authenticated()
|
.requestMatchers("/api/v1/download/clip/**").authenticated()
|
||||||
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/user").permitAll()
|
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/user").permitAll()
|
||||||
.requestMatchers("/api/v1/upload", "/api/v1/download/**").permitAll()
|
.requestMatchers("/api/v1/upload", "/api/v1/download/**").permitAll()
|
||||||
.requestMatchers("/api/v1/edit/**", "/api/v1/process/**", "/api/v1/progress/**").permitAll()
|
.requestMatchers("/api/v1/edit/**", "/api/v1/process/**", "/api/v1/progress/**", "/api/v1/convert/**").permitAll()
|
||||||
.requestMatchers("/api/v1/metadata/**").permitAll()
|
.requestMatchers("/api/v1/metadata/**").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.ddf.vodsystem.controllers;
|
package com.ddf.vodsystem.controllers;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.JobStatus;
|
||||||
import com.ddf.vodsystem.dto.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.services.EditService;
|
import com.ddf.vodsystem.services.EditService;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import com.ddf.vodsystem.dto.APIResponse;
|
import com.ddf.vodsystem.dto.APIResponse;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -26,22 +25,20 @@ public class EditController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/process/{uuid}")
|
@GetMapping("/process/{uuid}")
|
||||||
public ResponseEntity<APIResponse<Void>> convert(@PathVariable("uuid") String uuid) {
|
public ResponseEntity<APIResponse<Void>> process(@PathVariable("uuid") String uuid) {
|
||||||
editService.process(uuid);
|
editService.process(uuid);
|
||||||
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Processing started for UUID: " + uuid, null));
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Processing started for UUID: " + uuid, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/convert/{uuid}")
|
||||||
|
public ResponseEntity<APIResponse<Void>> convert(@PathVariable("uuid") String uuid) {
|
||||||
|
editService.convert(uuid);
|
||||||
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Conversion started for UUID: " + uuid, null));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/progress/{uuid}")
|
@GetMapping("/progress/{uuid}")
|
||||||
public ResponseEntity<APIResponse<ProgressResponse>> getProgress(@PathVariable("uuid") String uuid) {
|
public ResponseEntity<APIResponse<JobStatus>> getProgress(@PathVariable("uuid") String uuid) {
|
||||||
float progress = editService.getProgress(uuid);
|
JobStatus status = editService.getProgress(uuid);
|
||||||
|
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Progress for UUID: " + uuid, status));
|
||||||
ProgressResponse progressResponse = new ProgressResponse(progress);
|
|
||||||
return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Progress for UUID: " + uuid, progressResponse));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@AllArgsConstructor
|
|
||||||
public static class ProgressResponse {
|
|
||||||
private float progress;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.ddf.vodsystem.entities;
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
import com.ddf.vodsystem.dto.ProgressTracker;
|
|
||||||
import com.ddf.vodsystem.dto.VideoMetadata;
|
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -22,8 +20,7 @@ public class Job {
|
|||||||
private SecurityContext securityContext;
|
private SecurityContext securityContext;
|
||||||
|
|
||||||
// job status
|
// job status
|
||||||
private JobStatus status = JobStatus.NOT_READY;
|
private JobStatus status = new JobStatus();
|
||||||
private ProgressTracker progress = new ProgressTracker(0.0f);
|
|
||||||
|
|
||||||
public Job(String uuid, File inputFile, File outputFile, VideoMetadata inputVideoMetadata) {
|
public Job(String uuid, File inputFile, File outputFile, VideoMetadata inputVideoMetadata) {
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
11
src/main/java/com/ddf/vodsystem/dto/JobStatus.java
Normal file
11
src/main/java/com/ddf/vodsystem/dto/JobStatus.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class JobStatus {
|
||||||
|
private ProgressTracker process = new ProgressTracker();
|
||||||
|
private ProgressTracker conversion = new ProgressTracker();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
package com.ddf.vodsystem.dto;
|
package com.ddf.vodsystem.dto;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class ProgressTracker {
|
public class ProgressTracker {
|
||||||
private float progress;
|
private float progress = 0.0f;
|
||||||
|
private boolean isComplete = false;
|
||||||
|
|
||||||
public ProgressTracker(float initialProgress) {
|
public synchronized float getProgress() {
|
||||||
this.progress = initialProgress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProgress(float newProgress) {
|
public synchronized boolean isComplete() {
|
||||||
|
return isComplete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setProgress(float newProgress) {
|
||||||
if (newProgress < 0 || newProgress > 1) {
|
if (newProgress < 0 || newProgress > 1) {
|
||||||
throw new IllegalArgumentException("Progress must be between 0 and 1");
|
throw new IllegalArgumentException("Progress must be between 0 and 1");
|
||||||
}
|
}
|
||||||
this.progress = newProgress;
|
this.progress = newProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void markComplete() {
|
||||||
|
this.isComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void reset() {
|
||||||
|
this.progress = 0.0f;
|
||||||
|
this.isComplete = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.ddf.vodsystem.entities;
|
|
||||||
|
|
||||||
public enum JobStatus {
|
|
||||||
NOT_READY,
|
|
||||||
PENDING,
|
|
||||||
RUNNING,
|
|
||||||
FINISHED,
|
|
||||||
FAILED
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
import com.ddf.vodsystem.repositories.ClipRepository;
|
import com.ddf.vodsystem.repositories.ClipRepository;
|
||||||
@@ -57,26 +56,22 @@ public class ClipService {
|
|||||||
* @param progress A tracker to monitor the progress of the video processing.
|
* @param progress A tracker to monitor the progress of the video processing.
|
||||||
* @throws IOException if an I/O error occurs during file processing.
|
* @throws IOException if an I/O error occurs during file processing.
|
||||||
* @throws InterruptedException if the thread is interrupted during processing.
|
* @throws InterruptedException if the thread is interrupted during processing.
|
||||||
* @return An Optional containing the created Clip if the user is authenticated, otherwise an empty Optional.
|
|
||||||
*/
|
*/
|
||||||
public Optional<Clip> create(VideoMetadata inputMetadata,
|
public void create(VideoMetadata inputMetadata,
|
||||||
VideoMetadata outputMetadata,
|
VideoMetadata outputMetadata,
|
||||||
File inputFile,
|
File inputFile,
|
||||||
File outputFile,
|
File outputFile,
|
||||||
ProgressTracker progress) throws IOException, InterruptedException {
|
ProgressTracker progress)
|
||||||
metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata);
|
throws IOException, InterruptedException {
|
||||||
compressionService.compress(inputFile, outputFile, outputMetadata, progress);
|
|
||||||
|
|
||||||
Float fileSize = metadataService.getVideoMetadata(outputFile).getFileSize();
|
|
||||||
outputMetadata.setFileSize(fileSize);
|
|
||||||
|
|
||||||
User user = userService.getUser();
|
User user = userService.getUser();
|
||||||
|
metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata);
|
||||||
if (user == null) {
|
compressionService.compress(inputFile, outputFile, outputMetadata, progress)
|
||||||
return Optional.empty();
|
.thenRun(() -> {
|
||||||
|
if (user != null) {
|
||||||
|
persistClip(outputMetadata, user, outputFile, inputFile.getName());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return Optional.of(persistClip(outputMetadata, user, outputFile, inputFile.getName()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Clip> getClipsByUser() {
|
public List<Clip> getClipsByUser() {
|
||||||
@@ -136,7 +131,7 @@ public class ClipService {
|
|||||||
return user.getId().equals(clip.getUser().getId());
|
return user.getId().equals(clip.getUser().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Clip persistClip(VideoMetadata videoMetadata,
|
private void persistClip(VideoMetadata videoMetadata,
|
||||||
User user,
|
User user,
|
||||||
File tempFile,
|
File tempFile,
|
||||||
String fileName) {
|
String fileName) {
|
||||||
@@ -166,6 +161,6 @@ public class ClipService {
|
|||||||
clip.setFileSize(videoMetadata.getFileSize());
|
clip.setFileSize(videoMetadata.getFileSize());
|
||||||
clip.setVideoPath(clipFile.getPath());
|
clip.setVideoPath(clipFile.getPath());
|
||||||
clip.setThumbnailPath(thumbnailFile.getPath());
|
clip.setThumbnailPath(thumbnailFile.getPath());
|
||||||
return clipRepository.save(clip);
|
clipRepository.save(clip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.Clip;
|
import com.ddf.vodsystem.entities.Clip;
|
||||||
import com.ddf.vodsystem.entities.JobStatus;
|
|
||||||
import com.ddf.vodsystem.exceptions.JobNotFinished;
|
import com.ddf.vodsystem.exceptions.JobNotFinished;
|
||||||
import com.ddf.vodsystem.exceptions.JobNotFound;
|
import com.ddf.vodsystem.exceptions.JobNotFound;
|
||||||
import com.ddf.vodsystem.entities.Job;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
import com.ddf.vodsystem.repositories.ClipRepository;
|
import com.ddf.vodsystem.repositories.ClipRepository;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -21,7 +19,6 @@ public class DownloadService {
|
|||||||
private final ClipRepository clipRepository;
|
private final ClipRepository clipRepository;
|
||||||
private final ClipService clipService;
|
private final ClipService clipService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public DownloadService(JobService jobService,
|
public DownloadService(JobService jobService,
|
||||||
ClipRepository clipRepository,
|
ClipRepository clipRepository,
|
||||||
ClipService clipService) {
|
ClipService clipService) {
|
||||||
@@ -48,7 +45,7 @@ public class DownloadService {
|
|||||||
throw new JobNotFound("Job doesn't exist");
|
throw new JobNotFound("Job doesn't exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (job.getStatus() != JobStatus.FINISHED) {
|
if (!job.getStatus().getProcess().isComplete()) {
|
||||||
throw new JobNotFinished("Job is not finished");
|
throw new JobNotFinished("Job is not finished");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.JobStatus;
|
||||||
import com.ddf.vodsystem.dto.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import com.ddf.vodsystem.entities.*;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -20,17 +21,18 @@ public class EditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void process(String uuid) {
|
public void process(String uuid) {
|
||||||
jobService.jobReady(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getProgress(String uuid) {
|
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
|
jobService.processJob(job);
|
||||||
if (job.getStatus() == JobStatus.FINISHED) {
|
|
||||||
return 1f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return job.getProgress().getProgress();
|
public void convert(String uuid) {
|
||||||
|
Job job = jobService.getJob(uuid);
|
||||||
|
jobService.convertJob(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JobStatus getProgress(String uuid) {
|
||||||
|
Job job = jobService.getJob(uuid);
|
||||||
|
return job.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateClipConfig(VideoMetadata videoMetadata) {
|
private void validateClipConfig(VideoMetadata videoMetadata) {
|
||||||
|
|||||||
@@ -1,22 +1,17 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.Job;
|
||||||
|
import com.ddf.vodsystem.services.media.RemuxService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.Job;
|
|
||||||
import com.ddf.vodsystem.entities.JobStatus;
|
|
||||||
import com.ddf.vodsystem.exceptions.JobNotFound;
|
import com.ddf.vodsystem.exceptions.JobNotFound;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing and processing jobs in a background thread.
|
* Service for managing and processing jobs in a background thread.
|
||||||
* Uses a blocking queue to avoid busy waiting and ensures jobs are processed sequentially.
|
* Uses a blocking queue to avoid busy waiting and ensures jobs are processed sequentially.
|
||||||
@@ -25,15 +20,19 @@ import jakarta.annotation.PostConstruct;
|
|||||||
public class JobService {
|
public class JobService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
|
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
|
||||||
private final ConcurrentHashMap<String, Job> jobs = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, Job> jobs = new ConcurrentHashMap<>();
|
||||||
private final BlockingQueue<Job> jobQueue = new LinkedBlockingQueue<>();
|
|
||||||
private final ClipService clipService;
|
private final ClipService clipService;
|
||||||
|
private final RemuxService remuxService;
|
||||||
|
private final DirectoryService directoryService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a JobService with the given CompressionService.
|
* Constructs a JobService with the given CompressionService.
|
||||||
* @param clipService the compression service to use for processing jobs
|
* @param clipService the compression service to use for processing jobs
|
||||||
*/
|
*/
|
||||||
public JobService(ClipService clipService) {
|
public JobService(ClipService clipService,
|
||||||
|
RemuxService remuxService, DirectoryService directoryService) {
|
||||||
this.clipService = clipService;
|
this.clipService = clipService;
|
||||||
|
this.remuxService = remuxService;
|
||||||
|
this.directoryService = directoryService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,76 +60,49 @@ public class JobService {
|
|||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void convertJob(Job job) {
|
||||||
|
logger.info("Converting job: {}", job.getUuid());
|
||||||
|
File tempFile = new File(job.getInputFile().getAbsolutePath() + ".temp");
|
||||||
|
directoryService.copyFile(job.getInputFile(), tempFile);
|
||||||
|
|
||||||
|
job.getStatus().getConversion().reset();
|
||||||
|
|
||||||
|
try {
|
||||||
|
remuxService.remux(
|
||||||
|
tempFile,
|
||||||
|
job.getInputFile(),
|
||||||
|
job.getStatus().getConversion(),
|
||||||
|
job.getInputVideoMetadata().getEndPoint())
|
||||||
|
.thenRun(() -> {
|
||||||
|
job.getStatus().getConversion().markComplete();
|
||||||
|
directoryService.deleteFile(tempFile);
|
||||||
|
});
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
logger.error("Error converting job {}: {}", job.getUuid(), e.getMessage());
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks a job as ready and adds it to the processing queue.
|
* Marks a job as ready and adds it to the processing queue.
|
||||||
* @param uuid the UUID of the job to mark as ready
|
* @param job The job to process
|
||||||
*/
|
*/
|
||||||
public void jobReady(String uuid) {
|
public void processJob(Job job) {
|
||||||
Job job = getJob(uuid);
|
|
||||||
|
|
||||||
SecurityContext context = SecurityContextHolder.getContext();
|
|
||||||
job.setSecurityContext(context);
|
|
||||||
|
|
||||||
logger.info("Job ready: {}", job.getUuid());
|
logger.info("Job ready: {}", job.getUuid());
|
||||||
job.setStatus(JobStatus.PENDING);
|
job.getStatus().getProcess().reset();
|
||||||
jobQueue.add(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes a job by running the compression service.
|
|
||||||
* @param job the job to process
|
|
||||||
*/
|
|
||||||
private void processJob(Job job) {
|
|
||||||
SecurityContext previousContext = SecurityContextHolder.getContext(); // optional, for restoring later
|
|
||||||
try {
|
try {
|
||||||
if (job.getSecurityContext() != null) {
|
|
||||||
SecurityContextHolder.setContext(job.getSecurityContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
clipService.create(
|
clipService.create(
|
||||||
job.getInputVideoMetadata(),
|
job.getInputVideoMetadata(),
|
||||||
job.getOutputVideoMetadata(),
|
job.getOutputVideoMetadata(),
|
||||||
job.getInputFile(),
|
job.getInputFile(),
|
||||||
job.getOutputFile(),
|
job.getOutputFile(),
|
||||||
job.getProgress()
|
job.getStatus().getProcess()
|
||||||
);
|
);
|
||||||
|
|
||||||
job.setStatus(JobStatus.FINISHED);
|
|
||||||
|
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
|
logger.error("Error processing job {}: {}", job.getUuid(), e.getMessage());
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
logger.error("Error while running job {}", job.getUuid(), e);
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
// 🔄 Restore previous context to avoid leaking across jobs
|
|
||||||
SecurityContextHolder.setContext(previousContext);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the background processing loop in a daemon thread.
|
|
||||||
* The loop blocks until a job is available and then processes it.
|
|
||||||
*/
|
|
||||||
@PostConstruct
|
|
||||||
private void startProcessingLoop() {
|
|
||||||
Thread thread = new Thread(() -> {
|
|
||||||
logger.info("Starting processing loop");
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
Job job = jobQueue.take(); // Blocks until a job is available
|
|
||||||
|
|
||||||
logger.info("Starting job {}", job.getUuid());
|
|
||||||
job.setStatus(JobStatus.RUNNING);
|
|
||||||
processJob(job);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
logger.error("Processing loop interrupted", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
thread.setDaemon(true);
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.entities.Job;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
import com.ddf.vodsystem.dto.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
|
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
||||||
import com.ddf.vodsystem.services.media.MetadataService;
|
import com.ddf.vodsystem.services.media.MetadataService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
@@ -11,6 +11,9 @@ import java.io.File;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -23,7 +26,6 @@ public class UploadService {
|
|||||||
private final MetadataService metadataService;
|
private final MetadataService metadataService;
|
||||||
private final DirectoryService directoryService;
|
private final DirectoryService directoryService;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public UploadService(JobService jobService,
|
public UploadService(JobService jobService,
|
||||||
MetadataService metadataService,
|
MetadataService metadataService,
|
||||||
DirectoryService directoryService) {
|
DirectoryService directoryService) {
|
||||||
@@ -38,12 +40,19 @@ public class UploadService {
|
|||||||
String extension = directoryService.getFileExtension(file.getOriginalFilename());
|
String extension = directoryService.getFileExtension(file.getOriginalFilename());
|
||||||
|
|
||||||
File inputFile = directoryService.getTempInputFile(uuid + "." + extension);
|
File inputFile = directoryService.getTempInputFile(uuid + "." + extension);
|
||||||
File outputFile = directoryService.getTempOutputFile(uuid + "." + extension);
|
File outputFile = directoryService.getTempOutputFile(uuid + ".mp4");
|
||||||
directoryService.saveAtDir(inputFile, file);
|
directoryService.saveAtDir(inputFile, file);
|
||||||
|
|
||||||
// add job
|
// add job
|
||||||
logger.info("Uploaded file and creating job with UUID: {}", uuid);
|
logger.info("Uploaded file and creating job with UUID: {}", uuid);
|
||||||
VideoMetadata videoMetadata = metadataService.getVideoMetadata(inputFile);
|
|
||||||
|
VideoMetadata videoMetadata;
|
||||||
|
try {
|
||||||
|
videoMetadata = metadataService.getVideoMetadata(inputFile).get(5, TimeUnit.SECONDS);
|
||||||
|
} catch (ExecutionException | TimeoutException | InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new FFMPEGException(e.getMessage());
|
||||||
|
}
|
||||||
Job job = new Job(uuid, inputFile, outputFile, videoMetadata);
|
Job job = new Job(uuid, inputFile, outputFile, videoMetadata);
|
||||||
jobService.add(job);
|
jobService.add(job);
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package com.ddf.vodsystem.services.media;
|
package com.ddf.vodsystem.services.media;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.CommandOutput;
|
||||||
import com.ddf.vodsystem.dto.ProgressTracker;
|
import com.ddf.vodsystem.dto.ProgressTracker;
|
||||||
import com.ddf.vodsystem.dto.VideoMetadata;
|
import com.ddf.vodsystem.dto.VideoMetadata;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -22,12 +25,21 @@ public class CompressionService {
|
|||||||
private static final float BITRATE_MULTIPLIER = 0.9f;
|
private static final float BITRATE_MULTIPLIER = 0.9f;
|
||||||
private final Pattern timePattern = Pattern.compile("out_time_ms=(\\d+)");
|
private final Pattern timePattern = Pattern.compile("out_time_ms=(\\d+)");
|
||||||
|
|
||||||
public void compress(File inputFile, File outputFile, VideoMetadata videoMetadata, ProgressTracker progress) throws IOException, InterruptedException {
|
@Async("ffmpegTaskExecutor")
|
||||||
|
public CompletableFuture<CommandOutput> compress(File inputFile,
|
||||||
|
File outputFile,
|
||||||
|
VideoMetadata videoMetadata,
|
||||||
|
ProgressTracker progress
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
logger.info("Compressing video from {} to {}", inputFile.getAbsolutePath(), outputFile.getAbsolutePath());
|
logger.info("Compressing video from {} to {}", inputFile.getAbsolutePath(), outputFile.getAbsolutePath());
|
||||||
|
|
||||||
float length = videoMetadata.getEndPoint() - videoMetadata.getStartPoint();
|
float length = videoMetadata.getEndPoint() - videoMetadata.getStartPoint();
|
||||||
List<String> command = buildCommand(inputFile, outputFile, videoMetadata);
|
List<String> command = buildCommand(inputFile, outputFile, videoMetadata);
|
||||||
CommandRunner.run(command, line -> setProgress(line, progress, length));
|
|
||||||
|
CommandOutput result = CommandRunner.run(command, line -> setProgress(line, progress, length));
|
||||||
|
progress.markComplete();
|
||||||
|
|
||||||
|
return CompletableFuture.completedFuture(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setProgress(String line, ProgressTracker progress, float length) {
|
private void setProgress(String line, ProgressTracker progress, float length) {
|
||||||
|
|||||||
@@ -7,17 +7,21 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class MetadataService {
|
public class MetadataService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
|
private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
|
||||||
|
|
||||||
public VideoMetadata getVideoMetadata(File file) {
|
@Async("ffmpegTaskExecutor")
|
||||||
|
public Future<VideoMetadata> getVideoMetadata(File file) {
|
||||||
logger.info("Getting metadata for file {}", file.getAbsolutePath());
|
logger.info("Getting metadata for file {}", file.getAbsolutePath());
|
||||||
|
|
||||||
List<String> command = List.of(
|
List<String> command = List.of(
|
||||||
@@ -40,7 +44,7 @@ public class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
JsonNode node = mapper.readTree(outputBuilder.toString());
|
JsonNode node = mapper.readTree(outputBuilder.toString());
|
||||||
return parseVideoMetadata(node);
|
return CompletableFuture.completedFuture(parseVideoMetadata(node));
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new FFMPEGException("Error while getting video metadata: " + e);
|
throw new FFMPEGException("Error while getting video metadata: " + e);
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ddf.vodsystem.services.media;
|
||||||
|
|
||||||
|
import com.ddf.vodsystem.dto.CommandOutput;
|
||||||
|
import com.ddf.vodsystem.dto.ProgressTracker;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class RemuxService {
|
||||||
|
private final Pattern timePattern = Pattern.compile("out_time_ms=(\\d+)");
|
||||||
|
|
||||||
|
@Async("ffmpegTaskExecutor")
|
||||||
|
public CompletableFuture<CommandOutput> remux(File inputFile,
|
||||||
|
File outputFile,
|
||||||
|
ProgressTracker remuxProgress,
|
||||||
|
float length
|
||||||
|
) throws IOException, InterruptedException {
|
||||||
|
List<String> command = List.of(
|
||||||
|
"ffmpeg",
|
||||||
|
"-progress", "pipe:1",
|
||||||
|
"-y",
|
||||||
|
"-i", inputFile.getAbsolutePath(),
|
||||||
|
"-c:v", "h264",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-f", "mp4",
|
||||||
|
outputFile.getAbsolutePath()
|
||||||
|
);
|
||||||
|
|
||||||
|
return CompletableFuture.completedFuture(CommandRunner.run(command, line -> setProgress(line, remuxProgress, length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setProgress(String line, ProgressTracker progress, float length) {
|
||||||
|
Matcher matcher = timePattern.matcher(line);
|
||||||
|
if (matcher.find()) {
|
||||||
|
float timeInMs = Float.parseFloat(matcher.group(1)) / 1000000f;
|
||||||
|
progress.setProgress(timeInMs / length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.ddf.vodsystem.services.media;
|
package com.ddf.vodsystem.services.media;
|
||||||
|
|
||||||
import com.ddf.vodsystem.dto.CommandOutput;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -13,7 +13,8 @@ import java.util.List;
|
|||||||
public class ThumbnailService {
|
public class ThumbnailService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ThumbnailService.class);
|
private static final Logger logger = LoggerFactory.getLogger(ThumbnailService.class);
|
||||||
|
|
||||||
public CommandOutput createThumbnail(File inputFile, File outputFile, Float timeInVideo) throws IOException, InterruptedException {
|
@Async("ffmpegTaskExecutor")
|
||||||
|
public void createThumbnail(File inputFile, File outputFile, Float timeInVideo) throws IOException, InterruptedException {
|
||||||
logger.info("Creating thumbnail at {} seconds", timeInVideo);
|
logger.info("Creating thumbnail at {} seconds", timeInVideo);
|
||||||
|
|
||||||
List<String> command = List.of(
|
List<String> command = List.of(
|
||||||
@@ -24,6 +25,6 @@ public class ThumbnailService {
|
|||||||
outputFile.getAbsolutePath()
|
outputFile.getAbsolutePath()
|
||||||
);
|
);
|
||||||
|
|
||||||
return CommandRunner.run(command);
|
CommandRunner.run(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- DROP TABLE IF EXISTS clips;
|
DROP TABLE IF EXISTS clips;
|
||||||
-- DROP TABLE IF EXISTS users;
|
DROP TABLE IF EXISTS users;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|||||||
Reference in New Issue
Block a user