Compare commits
10 Commits
decf2703bd
...
fe097c8ee7
| Author | SHA1 | Date | |
|---|---|---|---|
| fe097c8ee7 | |||
| ab943be44e | |||
| 0c5b93571b | |||
| f2596e75f0 | |||
| 7c95f87b40 | |||
|
|
83aa6e230d | ||
| eed46cf266 | |||
| 65ec8cb29a | |||
| 0f5fc76e55 | |||
| 2d0d168784 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
### Security ###
|
### Security ###
|
||||||
.env.local
|
.env.local
|
||||||
.env.prod
|
.env.prod
|
||||||
|
.env
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
HELP.md
|
HELP.md
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ This VoD system began as a small project back in my 5th year of secondary school
|
|||||||
6. Endpoints should be available at 8080 (backend) and 5173 (frontend)
|
6. Endpoints should be available at 8080 (backend) and 5173 (frontend)
|
||||||
|
|
||||||
# Future Plans
|
# Future Plans
|
||||||
|
|
||||||
- **User Management**
|
|
||||||
- Database integration with authentication and login
|
|
||||||
- Clip saving and management for authenticated users
|
|
||||||
- **Format Handling**
|
- **Format Handling**
|
||||||
- Backend conversion of non-MP4 files via FFMPEG for broader format support
|
- Backend conversion of non-MP4 files via FFMPEG for broader format support
|
||||||
- **Input Sources**
|
- **Input Sources**
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
container_name: my_postgres
|
container_name: vod_postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: myuser
|
POSTGRES_USER: myuser
|
||||||
POSTGRES_PASSWORD: mypassword
|
POSTGRES_PASSWORD: mypassword
|
||||||
@@ -13,6 +12,31 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: src/main/Dockerfile
|
||||||
|
container_name: vod_backend
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: vod_frontend
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
ports:
|
||||||
|
- "5173:5173"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres_data:
|
postgres_data:
|
||||||
9
frontend/Dockerfile
Normal file
9
frontend/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
EXPOSE 5173
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {useParams} from "react-router-dom";
|
import {useParams} from "react-router-dom";
|
||||||
import type {Clip} from "../utils/types";
|
import type {Clip} from "../utils/types";
|
||||||
import {getClipById} from "../utils/endpoints.ts";
|
import {getClipById, getVideoBlob } from "../utils/endpoints.ts";
|
||||||
import Box from "../components/Box.tsx"
|
import Box from "../components/Box.tsx"
|
||||||
import {dateToTimeAgo, stringToDate} from "../utils/utils.ts";
|
import {dateToTimeAgo, stringToDate} from "../utils/utils.ts";
|
||||||
|
|
||||||
@@ -13,28 +13,18 @@ const VideoPlayer = () => {
|
|||||||
const [timeAgo, setTimeAgo] = useState<String>("");
|
const [timeAgo, setTimeAgo] = useState<String>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Fetch the video URL from the server
|
|
||||||
fetch(`/api/v1/download/clip/${id}`)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error("Failed to load video");
|
|
||||||
}
|
|
||||||
return response.blob();
|
|
||||||
})
|
|
||||||
.then(blob => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
setVideoUrl(url);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error fetching video:", err);
|
|
||||||
setError("Failed to load video. Please try again later.");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
setError("Clip ID is required.");
|
setError("Clip ID is required.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVideoBlob(id)
|
||||||
|
.then((blob) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
setVideoUrl(url);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
getClipById(id)
|
getClipById(id)
|
||||||
.then((fetchedClip) => {setClip(fetchedClip)})
|
.then((fetchedClip) => {setClip(fetchedClip)})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
|||||||
@@ -239,6 +239,20 @@ const getClipById = async (id: string): Promise<Clip | null> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVideoBlob = async(id: string): Promise<Blob> => {
|
||||||
|
const response = await fetch(API_URL + `/api/v1/download/clip/${id}`, {credentials: "include",});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch video: ${id}: ${response.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return response.blob();
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Failed to convert Clip Return Object to blob`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
const isThumbnailAvailable = async (id: number): Promise<boolean> => {
|
||||||
const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"});
|
const response = await fetch(API_URL + `/api/v1/download/thumbnail/${id}`, {credentials: "include"});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -260,5 +274,6 @@ export {
|
|||||||
getUser,
|
getUser,
|
||||||
getClips,
|
getClips,
|
||||||
getClipById,
|
getClipById,
|
||||||
|
getVideoBlob,
|
||||||
isThumbnailAvailable
|
isThumbnailAvailable
|
||||||
};
|
};
|
||||||
@@ -2,12 +2,11 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import flowbiteReact from "flowbite-react/plugin/vite";
|
import flowbiteReact from "flowbite-react/plugin/vite";
|
||||||
import mkcert from 'vite-plugin-mkcert'
|
|
||||||
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss(), flowbiteReact(), mkcert()],
|
plugins: [react(), tailwindcss(), flowbiteReact()],
|
||||||
preview: {
|
preview: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
strictPort: true,
|
strictPort: true,
|
||||||
|
|||||||
21
src/main/Dockerfile
Normal file
21
src/main/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM maven:3.9.6-eclipse-temurin-21 AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pom.xml .
|
||||||
|
COPY src ./src
|
||||||
|
RUN mvn clean package -DskipTests
|
||||||
|
|
||||||
|
|
||||||
|
# Run stage
|
||||||
|
FROM eclipse-temurin:21-jdk
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y ffmpeg && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/target/*.jar app.jar
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.ddf.vodsystem.services;
|
package com.ddf.vodsystem.services;
|
||||||
|
|
||||||
import com.ddf.vodsystem.dto.ProgressTracker;
|
|
||||||
import com.ddf.vodsystem.dto.ClipOptions;
|
import com.ddf.vodsystem.dto.ClipOptions;
|
||||||
import com.ddf.vodsystem.entities.*;
|
import com.ddf.vodsystem.entities.*;
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ import java.util.concurrent.ExecutionException;
|
|||||||
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
||||||
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 com.ddf.vodsystem.services.media.CompressionService;
|
|
||||||
import com.ddf.vodsystem.services.media.MetadataService;
|
import com.ddf.vodsystem.services.media.MetadataService;
|
||||||
import com.ddf.vodsystem.services.media.ThumbnailService;
|
import com.ddf.vodsystem.services.media.ThumbnailService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -28,60 +26,22 @@ public class ClipService {
|
|||||||
|
|
||||||
private final ClipRepository clipRepository;
|
private final ClipRepository clipRepository;
|
||||||
private final DirectoryService directoryService;
|
private final DirectoryService directoryService;
|
||||||
private final CompressionService compressionService;
|
|
||||||
private final MetadataService metadataService;
|
private final MetadataService metadataService;
|
||||||
private final ThumbnailService thumbnailService;
|
private final ThumbnailService thumbnailService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
public ClipService(ClipRepository clipRepository,
|
public ClipService(ClipRepository clipRepository,
|
||||||
DirectoryService directoryService,
|
DirectoryService directoryService,
|
||||||
CompressionService compressionService,
|
|
||||||
MetadataService metadataService,
|
MetadataService metadataService,
|
||||||
ThumbnailService thumbnailService,
|
ThumbnailService thumbnailService,
|
||||||
UserService userService) {
|
UserService userService) {
|
||||||
this.clipRepository = clipRepository;
|
this.clipRepository = clipRepository;
|
||||||
this.directoryService = directoryService;
|
this.directoryService = directoryService;
|
||||||
this.compressionService = compressionService;
|
|
||||||
this.metadataService = metadataService;
|
this.metadataService = metadataService;
|
||||||
this.thumbnailService = thumbnailService;
|
this.thumbnailService = thumbnailService;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the clip creation process.
|
|
||||||
* This method normalizes the input metadata, compresses the video file,
|
|
||||||
* updates the output metadata with the file size, and saves the clip
|
|
||||||
* to the database if the user is authenticated.
|
|
||||||
*
|
|
||||||
* @param inputMetadata The metadata of the input video file.
|
|
||||||
* @param outputMetadata The metadata for the output video file.
|
|
||||||
* @param inputFile The input video file to be processed.
|
|
||||||
* @param outputFile The output file where the processed video will be saved.
|
|
||||||
* @param progress A tracker to monitor the progress of the video processing.
|
|
||||||
* @throws IOException if an I/O error occurs during file processing.
|
|
||||||
* @throws InterruptedException if the thread is interrupted during processing.
|
|
||||||
*/
|
|
||||||
public void create(ClipOptions inputMetadata,
|
|
||||||
ClipOptions outputMetadata,
|
|
||||||
File inputFile,
|
|
||||||
File outputFile,
|
|
||||||
ProgressTracker progress)
|
|
||||||
throws IOException, InterruptedException {
|
|
||||||
|
|
||||||
Optional<User> user = userService.getLoggedInUser();
|
|
||||||
metadataService.normalizeVideoMetadata(inputMetadata, outputMetadata);
|
|
||||||
compressionService.compress(inputFile, outputFile, outputMetadata, progress)
|
|
||||||
.thenRun(() -> user.ifPresent(value ->
|
|
||||||
persistClip(
|
|
||||||
outputMetadata,
|
|
||||||
value,
|
|
||||||
outputFile,
|
|
||||||
inputFile.getName()
|
|
||||||
))).exceptionally(ex -> {
|
|
||||||
throw new FFMPEGException("FFMPEG Compression failed: " + ex.getMessage());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all clips associated with the currently logged-in user.
|
* Retrieves all clips associated with the currently logged-in user.
|
||||||
*
|
*
|
||||||
@@ -161,7 +121,34 @@ public class ClipService {
|
|||||||
return user.get().getId().equals(clip.getUser().getId());
|
return user.get().getId().equals(clip.getUser().getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void persistClip(ClipOptions clipOptions,
|
/**
|
||||||
|
* Persists a clip to the database
|
||||||
|
* @param options ClipOptions object of the clip metadata to save to the database. All fields required except for title, description
|
||||||
|
* @param user User to save the clip to
|
||||||
|
* @param videoPath Path of the clip
|
||||||
|
* @param thumbnailPath Path of the thumbnail
|
||||||
|
* @return Clip object saved to the database
|
||||||
|
*/
|
||||||
|
public Clip saveClip(ClipOptions options,
|
||||||
|
User user,
|
||||||
|
String videoPath,
|
||||||
|
String thumbnailPath) {
|
||||||
|
Clip clip = new Clip();
|
||||||
|
clip.setUser(user);
|
||||||
|
clip.setTitle(options.getTitle() != null ? options.getTitle() : "Untitled Clip");
|
||||||
|
clip.setDescription(options.getDescription() != null ? options.getDescription() : "");
|
||||||
|
clip.setCreatedAt(LocalDateTime.now());
|
||||||
|
clip.setWidth(options.getWidth());
|
||||||
|
clip.setHeight(options.getHeight());
|
||||||
|
clip.setFps(options.getFps());
|
||||||
|
clip.setDuration(options.getDuration() - options.getStartPoint());
|
||||||
|
clip.setFileSize(options.getFileSize());
|
||||||
|
clip.setVideoPath(videoPath);
|
||||||
|
clip.setThumbnailPath(thumbnailPath);
|
||||||
|
return clipRepository.save(clip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void persistClip(ClipOptions clipOptions,
|
||||||
User user,
|
User user,
|
||||||
File tempFile,
|
File tempFile,
|
||||||
String fileName) {
|
String fileName) {
|
||||||
@@ -181,25 +168,14 @@ public class ClipService {
|
|||||||
try {
|
try {
|
||||||
thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f);
|
thumbnailService.createThumbnail(clipFile, thumbnailFile, 0.0f);
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
logger.error("Error generating thumbnail for clip: {}", e.getMessage());
|
logger.error("Error generating thumbnail for user: {}, {}", user.getId(), e.getMessage());
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save clip to database
|
clipMetadata.setTitle(clipOptions.getTitle());
|
||||||
Clip clip = new Clip();
|
clipMetadata.setDescription(clipOptions.getDescription());
|
||||||
clip.setUser(user);
|
|
||||||
clip.setTitle(clipOptions.getTitle() != null ? clipOptions.getTitle() : "Untitled Clip");
|
|
||||||
clip.setDescription(clipOptions.getDescription() != null ? clipOptions.getDescription() : "");
|
|
||||||
clip.setCreatedAt(LocalDateTime.now());
|
|
||||||
clip.setWidth(clipMetadata.getWidth());
|
|
||||||
clip.setHeight(clipMetadata.getHeight());
|
|
||||||
clip.setFps(clipMetadata.getFps());
|
|
||||||
clip.setDuration(clipMetadata.getDuration() - clipMetadata.getStartPoint());
|
|
||||||
clip.setFileSize(clipMetadata.getFileSize());
|
|
||||||
clip.setVideoPath(clipFile.getPath());
|
|
||||||
clip.setThumbnailPath(thumbnailFile.getPath());
|
|
||||||
clipRepository.save(clip);
|
|
||||||
|
|
||||||
|
Clip clip = saveClip(clipMetadata, user, clipFile.getAbsolutePath(), thumbnailFile.getAbsolutePath());
|
||||||
logger.info("Clip created successfully with ID: {}", clip.getId());
|
logger.info("Clip created successfully with ID: {}", clip.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +187,7 @@ public class ClipService {
|
|||||||
Files.deleteIfExists(clipFile.toPath());
|
Files.deleteIfExists(clipFile.toPath());
|
||||||
Files.deleteIfExists(thumbnailFile.toPath());
|
Files.deleteIfExists(thumbnailFile.toPath());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
logger.error("Could not delete clip files for clip ID: {}", clip.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,69 +3,44 @@ package com.ddf.vodsystem.services;
|
|||||||
import com.ddf.vodsystem.dto.JobStatus;
|
import com.ddf.vodsystem.dto.JobStatus;
|
||||||
import com.ddf.vodsystem.dto.ClipOptions;
|
import com.ddf.vodsystem.dto.ClipOptions;
|
||||||
import com.ddf.vodsystem.dto.Job;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
|
import com.ddf.vodsystem.services.media.MetadataService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class EditService {
|
public class EditService {
|
||||||
private final JobService jobService;
|
private final JobService jobService;
|
||||||
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(EditService.class);
|
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(EditService.class);
|
||||||
|
private final MetadataService metadataService;
|
||||||
|
|
||||||
public EditService(JobService jobService) {
|
public EditService(JobService jobService, MetadataService metadataService) {
|
||||||
this.jobService = jobService;
|
this.jobService = jobService;
|
||||||
|
this.metadataService = metadataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void edit(String uuid, ClipOptions clipOptions) {
|
public void edit(String uuid, ClipOptions clipOptions) {
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
validateClipConfig(clipOptions);
|
metadataService.validateMetadata(job.getInputClipOptions(), clipOptions);
|
||||||
job.setOutputClipOptions(clipOptions);
|
job.setOutputClipOptions(clipOptions);
|
||||||
|
|
||||||
|
logger.info("Job {} - Updated clip config to {}", job.getUuid(), clipOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void process(String uuid) {
|
public void process(String uuid) {
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
jobService.processJob(job);
|
jobService.processJob(job);
|
||||||
|
|
||||||
|
logger.info("Job {} - Started processing", uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void convert(String uuid) {
|
public void convert(String uuid) {
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
jobService.convertJob(job);
|
jobService.convertJob(job);
|
||||||
|
|
||||||
|
logger.info("Job {} - Started converting", uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public JobStatus getStatus(String uuid) {
|
public JobStatus getStatus(String uuid) {
|
||||||
Job job = jobService.getJob(uuid);
|
Job job = jobService.getJob(uuid);
|
||||||
return job.getStatus();
|
return job.getStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateClipConfig(ClipOptions clipOptions) {
|
|
||||||
Float start = clipOptions.getStartPoint();
|
|
||||||
Float duration = clipOptions.getDuration();
|
|
||||||
Float fileSize = clipOptions.getFileSize();
|
|
||||||
Integer width = clipOptions.getWidth();
|
|
||||||
Integer height = clipOptions.getHeight();
|
|
||||||
Float fps = clipOptions.getFps();
|
|
||||||
|
|
||||||
if (start != null && start < 0) {
|
|
||||||
throw new IllegalArgumentException("Start point cannot be negative");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (duration != null && duration < 0) {
|
|
||||||
throw new IllegalArgumentException("Duration cannot be negative");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileSize != null && fileSize < 100) {
|
|
||||||
throw new IllegalArgumentException("File size cannot be less than 100kb");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width != null && width < 1) {
|
|
||||||
throw new IllegalArgumentException("Width cannot be less than 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (height != null && height < 1) {
|
|
||||||
throw new IllegalArgumentException("Height cannot be less than 1");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fps != null && fps < 1) {
|
|
||||||
throw new IllegalArgumentException("FPS cannot be less than 1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import com.ddf.vodsystem.dto.Job;
|
import com.ddf.vodsystem.dto.Job;
|
||||||
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
import com.ddf.vodsystem.entities.User;
|
||||||
|
import com.ddf.vodsystem.services.media.CompressionService;
|
||||||
import com.ddf.vodsystem.services.media.RemuxService;
|
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.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -26,16 +29,23 @@ public class JobService {
|
|||||||
private final ClipService clipService;
|
private final ClipService clipService;
|
||||||
private final RemuxService remuxService;
|
private final RemuxService remuxService;
|
||||||
private final DirectoryService directoryService;
|
private final DirectoryService directoryService;
|
||||||
|
private final CompressionService compressionService;
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
RemuxService remuxService,
|
||||||
|
CompressionService compressionService,
|
||||||
|
DirectoryService directoryService,
|
||||||
|
UserService userService) {
|
||||||
this.clipService = clipService;
|
this.clipService = clipService;
|
||||||
this.remuxService = remuxService;
|
this.remuxService = remuxService;
|
||||||
this.directoryService = directoryService;
|
this.directoryService = directoryService;
|
||||||
|
this.compressionService = compressionService;
|
||||||
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +94,14 @@ public class JobService {
|
|||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
}).whenComplete((ignored, throwable) -> {
|
||||||
|
if (throwable != null) {
|
||||||
|
logger.error("Remux failed for jobId={}", job.getUuid(), throwable);
|
||||||
|
} else {
|
||||||
|
logger.info("Remux completed for jobId={}", job.getUuid());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
logger.error("Error converting job {}: {}", job.getUuid(), e.getMessage());
|
logger.error("Error converting job {}: {}", job.getUuid(), e.getMessage());
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
@@ -100,18 +117,25 @@ public class JobService {
|
|||||||
job.getStatus().getProcess().reset();
|
job.getStatus().getProcess().reset();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
clipService.create(
|
Optional<User> user = userService.getLoggedInUser();
|
||||||
job.getInputClipOptions(),
|
compressionService.compress(job.getInputFile(), job.getOutputFile(), job.getOutputClipOptions(), job.getStatus().getProcess())
|
||||||
|
.thenRun(() -> user.ifPresent(userVal ->
|
||||||
|
clipService.persistClip(
|
||||||
job.getOutputClipOptions(),
|
job.getOutputClipOptions(),
|
||||||
job.getInputFile(),
|
userVal,
|
||||||
job.getOutputFile(),
|
job.getOutputFile(),
|
||||||
job.getStatus().getProcess()
|
job.getInputFile().getName()
|
||||||
|
)
|
||||||
|
)).exceptionally(
|
||||||
|
ex -> {
|
||||||
|
job.getStatus().setFailed(true);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
|
job.getStatus().setFailed(true);
|
||||||
logger.error("Error processing job {}: {}", job.getUuid(), e.getMessage());
|
logger.error("Error processing job {}: {}", job.getUuid(), e.getMessage());
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
} catch (FFMPEGException e) {
|
|
||||||
job.getStatus().setFailed(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,14 +51,45 @@ public class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void normalizeVideoMetadata(ClipOptions inputFileMetadata, ClipOptions outputFileMetadata) {
|
public void validateMetadata(ClipOptions inputFileMetadata, ClipOptions outputFileMetadata) {
|
||||||
if (outputFileMetadata.getStartPoint() == null) {
|
Float start = outputFileMetadata.getStartPoint();
|
||||||
|
Float duration = outputFileMetadata.getDuration();
|
||||||
|
Float fileSize = outputFileMetadata.getFileSize();
|
||||||
|
Integer width = outputFileMetadata.getWidth();
|
||||||
|
Integer height = outputFileMetadata.getHeight();
|
||||||
|
Float fps = outputFileMetadata.getFps();
|
||||||
|
|
||||||
|
if (start == null) {
|
||||||
outputFileMetadata.setStartPoint(0f);
|
outputFileMetadata.setStartPoint(0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputFileMetadata.getDuration() == null) {
|
if (duration == null) {
|
||||||
outputFileMetadata.setDuration(inputFileMetadata.getDuration());
|
outputFileMetadata.setDuration(inputFileMetadata.getDuration());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (start != null && start < 0) {
|
||||||
|
throw new IllegalArgumentException("Start point cannot be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (duration != null && duration < 0) {
|
||||||
|
throw new IllegalArgumentException("Duration cannot be negative");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileSize != null && fileSize < 100) {
|
||||||
|
throw new IllegalArgumentException("File size cannot be less than 100kb");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width != null && width < 1) {
|
||||||
|
throw new IllegalArgumentException("Width cannot be less than 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (height != null && height < 1) {
|
||||||
|
throw new IllegalArgumentException("Height cannot be less than 1");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fps != null && fps < 1) {
|
||||||
|
throw new IllegalArgumentException("FPS cannot be less than 1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,4 +10,4 @@ spring.sql.init.schema-locations=classpath:db/schema.sql
|
|||||||
#spring.sql.init.data-locations=classpath:db/data.sql
|
#spring.sql.init.data-locations=classpath:db/data.sql
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
frontend.url=https://localhost:5173
|
frontend.url=http://localhost:5173
|
||||||
|
|||||||
Reference in New Issue
Block a user