ADD VideoPlayer component and implement video fetching functionality

This commit is contained in:
2025-07-12 14:27:51 +02:00
parent e6d3b48855
commit 9f8894798d
8 changed files with 102 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ import ClipEdit from './pages/ClipEdit';
import Home from './pages/Home'; import Home from './pages/Home';
import {useEffect} from "react"; import {useEffect} from "react";
import MyClips from './pages/MyClips'; import MyClips from './pages/MyClips';
import VideoPlayer from "./pages/VideoPlayer.tsx";
function App() { function App() {
@@ -21,6 +22,7 @@ function App() {
<Route path="/create" element={<ClipUpload />} /> <Route path="/create" element={<ClipUpload />} />
<Route path="/create/:id" element={<ClipEdit />} /> <Route path="/create/:id" element={<ClipEdit />} />
<Route path="/my-clips" element={<MyClips />} /> <Route path="/my-clips" element={<MyClips />} />
<Route path="/video/:id" element={<VideoPlayer />} />
</Route> </Route>
</Routes> </Routes>
</Router> </Router>

View File

@@ -4,6 +4,7 @@ import { Link } from "react-router-dom";
import { useState } from "react"; import { useState } from "react";
type VideoCardProps = { type VideoCardProps = {
id: number,
title: string, title: string,
duration: number, duration: number,
thumbnailPath: string | null, thumbnailPath: string | null,
@@ -15,19 +16,24 @@ type VideoCardProps = {
const fallbackThumbnail = "../../../public/default_thumbnail.png"; const fallbackThumbnail = "../../../public/default_thumbnail.png";
const VideoCard = ({ const VideoCard = ({
id,
title, title,
duration, duration,
thumbnailPath, thumbnailPath,
videoPath,
createdAt, createdAt,
className className
}: VideoCardProps) => { }: VideoCardProps) => {
const initialSrc = thumbnailPath && thumbnailPath.trim() !== "" ? thumbnailPath : fallbackThumbnail; const initialSrc = thumbnailPath && thumbnailPath.trim() !== "" ? thumbnailPath : fallbackThumbnail;
const [imgSrc, setImgSrc] = useState(initialSrc); const [imgSrc, setImgSrc] = useState(initialSrc);
const [timeAgo, setTimeAgo] = useState(dateToTimeAgo(stringToDate(createdAt)));
setTimeout(() => {
setTimeAgo(dateToTimeAgo(stringToDate(createdAt)))
}, 1000);
return ( return (
<Link to={videoPath}> <Link to={"/video/" + id}>
<div className={clsx("flex flex-col", className)}> <div className={clsx("flex flex-col", className)}>
<div className={"relative inline-block"}> <div className={"relative inline-block"}>
<img <img
@@ -55,15 +61,16 @@ const VideoCard = ({
"> ">
{formatTime(duration)} {formatTime(duration)}
</p> </p>
<p>
{dateToTimeAgo(stringToDate(createdAt))}
</p>
</div> </div>
<div className={"flex flex-row justify-between items-center p-2"}> <div className={"flex flex-col justify-between p-2"}>
<p>{title == "" ? "(No Title)" : title}</p> <p>{title == "" ? "(No Title)" : title}</p>
<p
className={"text-gray-600 text-sm"}
>
{timeAgo}
</p>
</div> </div>
</div> </div>
</Link> </Link>

View File

@@ -15,6 +15,7 @@ const MyClips = () => {
<div className={"flex flex-row"}> <div className={"flex flex-row"}>
{clips.map((clip) => ( {clips.map((clip) => (
<VideoCard <VideoCard
id={clip.id}
key={clip.videoPath} key={clip.videoPath}
title={clip.title} title={clip.title}
duration={clip.duration} duration={clip.duration}

View File

@@ -0,0 +1,48 @@
import {useEffect, useState} from "react";
import {useParams} from "react-router-dom";
const VideoPlayer = () => {
const { id } = useParams();
const [videoUrl, setVideoUrl] = useState<string>("");
const [error, setError] = useState<string | null>(null);
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.");
});
}, [id]);
return (
<div className="video-player">
<video
className="w-full h-full"
controls
autoPlay
src={videoUrl}
onError={(e) => {
setError(e.currentTarget.error?.message || "An error occurred while playing the video.");
}}
>
Your browser does not support the video tag.
</video>
{error && <div className="text-red-500 mt-2">{error}</div>}
{!videoUrl && !error && <div className="text-gray-500 mt-2">Loading video...</div>}
</div>
);
};
export default VideoPlayer;

View File

@@ -22,6 +22,7 @@ type User = {
} }
type Clip = { type Clip = {
id: number,
title: string, title: string,
description: string, description: string,
duration: number, duration: number,

View File

@@ -49,4 +49,18 @@ public class DownloadController {
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM)) .contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.body(resource); .body(resource);
} }
@GetMapping("/clip/{id}")
public ResponseEntity<Resource> downloadClip(@PathVariable Long id) {
Resource resource = downloadService.downloadClip(id);
if (resource == null || !resource.exists()) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resource.getFilename() + "\"")
.contentType(MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM))
.body(resource);
}
} }

View File

@@ -13,3 +13,5 @@ public interface ClipRepository extends JpaRepository<Clip, Long> {
@Query("SELECT c FROM Clip c WHERE c.user = ?1") @Query("SELECT c FROM Clip c WHERE c.user = ?1")
List<Clip> findByUser(User user); List<Clip> findByUser(User user);
} }

View File

@@ -1,9 +1,11 @@
package com.ddf.vodsystem.services; package com.ddf.vodsystem.services;
import com.ddf.vodsystem.entities.Clip;
import com.ddf.vodsystem.entities.JobStatus; 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.entities.Job;
import com.ddf.vodsystem.repositories.ClipRepository;
import org.springframework.beans.factory.annotation.Autowired; 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;
@@ -15,10 +17,12 @@ import java.io.File;
public class DownloadService { public class DownloadService {
private final JobService jobService; private final JobService jobService;
private final ClipRepository clipRepository;
@Autowired @Autowired
public DownloadService(JobService jobService) { public DownloadService(JobService jobService, ClipRepository clipRepository) {
this.jobService = jobService; this.jobService = jobService;
this.clipRepository = clipRepository;
} }
public Resource downloadInput(String uuid) { public Resource downloadInput(String uuid) {
@@ -46,4 +50,19 @@ public class DownloadService {
File file = job.getOutputFile(); File file = job.getOutputFile();
return new FileSystemResource(file); return new FileSystemResource(file);
} }
public Resource downloadClip(Clip clip) {
String path = clip.getVideoPath();
File file = new File(path);
if (!file.exists()) {
throw new JobNotFound("Clip file not found");
}
return new FileSystemResource(file);
}
public Resource downloadClip(Long id) {
Clip clip = clipRepository.findById(id).orElseThrow(() -> new JobNotFound("Clip not found with id: " + id));
return downloadClip(clip);
}
} }