From dbb7bc5fd3862c158db9482841b455e470874ba2 Mon Sep 17 00:00:00 2001 From: a2-imeri <Alfret2.imeri@live.uwe.ac.uk> Date: Wed, 3 Jul 2024 13:09:13 +0100 Subject: [PATCH] FrontEnd Video Settings --- frontend/package-lock.json | 10 ++ frontend/package.json | 1 + .../Common/input/AmazingDropdown.js | 35 +++++ .../{UploadForm.js => VideoUploadForm.js} | 6 +- .../detection/video/VideoUploadForm.js | 125 ++++++++++++++++++ .../components/detection/video/videoUtils.js | 70 ++++++++++ .../src/pages/Video/VideoDetectionPage.js | 43 ++++-- frontend/src/styles/input/AmazingDropdown.css | 17 +++ 8 files changed, 295 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/Common/input/AmazingDropdown.js rename frontend/src/components/detection/{UploadForm.js => VideoUploadForm.js} (82%) create mode 100644 frontend/src/components/detection/video/VideoUploadForm.js create mode 100644 frontend/src/components/detection/video/videoUtils.js create mode 100644 frontend/src/styles/input/AmazingDropdown.css diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7e36288..f61fa33 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@tensorflow/tfjs": "^3.9.0", "axios": "^1.7.2", "bootstrap": "^5.3.3", + "font-awesome": "^4.7.0", "react": "^18.0.0", "react-bootstrap": "^2.10.2", "react-dom": "^18.0.0", @@ -9081,6 +9082,15 @@ } } }, + "node_modules/font-awesome": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz", + "integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==", + "license": "(OFL-1.1 AND MIT)", + "engines": { + "node": ">=0.10.3" + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index fe6e6b2..38d2da0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "@tensorflow/tfjs": "^3.9.0", "axios": "^1.7.2", "bootstrap": "^5.3.3", + "font-awesome": "^4.7.0", "react": "^18.0.0", "react-bootstrap": "^2.10.2", "react-dom": "^18.0.0", diff --git a/frontend/src/components/Common/input/AmazingDropdown.js b/frontend/src/components/Common/input/AmazingDropdown.js new file mode 100644 index 0000000..2841e15 --- /dev/null +++ b/frontend/src/components/Common/input/AmazingDropdown.js @@ -0,0 +1,35 @@ +// src/components/Common/input/AmazingDropdown.js +import React from 'react'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'font-awesome/css/font-awesome.min.css'; +import '../../../styles/input/AmazingDropdown.css'; + +const AmazingDropdown = ({ options, selectedOption, onSelect }) => { + return ( + <div className="dropdown"> + <button + className="btn btn-primary dropdown-toggle" + type="button" + id="dropdownMenuButton" + data-toggle="dropdown" + aria-haspopup="true" + aria-expanded="false" + > + {selectedOption ? selectedOption : 'Select an option'} + </button> + <div className="dropdown-menu" aria-labelledby="dropdownMenuButton"> + {options.map(option => ( + <button + key={option} + className="dropdown-item" + onClick={() => onSelect(option)} + > + {option} + </button> + ))} + </div> + </div> + ); +}; + +export default AmazingDropdown; diff --git a/frontend/src/components/detection/UploadForm.js b/frontend/src/components/detection/VideoUploadForm.js similarity index 82% rename from frontend/src/components/detection/UploadForm.js rename to frontend/src/components/detection/VideoUploadForm.js index 625ddb5..9e00d88 100644 --- a/frontend/src/components/detection/UploadForm.js +++ b/frontend/src/components/detection/VideoUploadForm.js @@ -1,8 +1,8 @@ -/* src/components/detection/UploadForm.js */ +/* src/components/detection/VideoUploadForm.js */ import React from 'react'; -const UploadForm = ({ handleFileChange, handleSubmit, handleFrameIntervalChange, handleCameraClick, handleGalleryClick, isLoading }) => { +const VideoUploadForm = ({ handleFileChange, handleSubmit, handleFrameIntervalChange, handleCameraClick, handleGalleryClick, isLoading }) => { return ( <form id="uploadForm" className="text-center" onSubmit={handleSubmit} encType="multipart/form-data"> <div className="form-group"> @@ -30,4 +30,4 @@ const UploadForm = ({ handleFileChange, handleSubmit, handleFrameIntervalChange, ); }; -export default UploadForm; +export default VideoUploadForm; diff --git a/frontend/src/components/detection/video/VideoUploadForm.js b/frontend/src/components/detection/video/VideoUploadForm.js new file mode 100644 index 0000000..daccf2d --- /dev/null +++ b/frontend/src/components/detection/video/VideoUploadForm.js @@ -0,0 +1,125 @@ +import React, { useState, useEffect } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { getVideoDuration, calculateMinimumDelay, calculateMaximumDelay, calculateExpectedLength, isValidVideoLength, calculateOptimalValues, calculateMaxFrameJump, isDelayPerFrameValid, calculateValidFramesJumpOptions, calculateMinimumValidFrameDelay } from './videoUtils'; + +const VideoUploadForm = ({ handleFileChange, handleSubmit, handleFramesJumpChange, handleFrameDelayChange, framesJump, frameDelay, isLoading }) => { + const [videoFile, setVideoFile] = useState(null); + const [videoDuration, setVideoDuration] = useState(0); + const [expectedLength, setExpectedLength] = useState(0); + const [maxFrameDelay, setMaxFrameDelay] = useState(60); + const [minFrameDelay, setMinFrameDelay] = useState(1); + const [isVideoUploaded, setIsVideoUploaded] = useState(false); + const [validFramesJumpOptions, setValidFramesJumpOptions] = useState([]); + + useEffect(() => { + if (videoFile) { + console.log('Video file uploaded:', videoFile); + getVideoDuration(videoFile).then(duration => { + console.log('Video duration:', duration); + setVideoDuration(duration); + const { framesJump, frameDelay } = calculateOptimalValues(duration); + updateConstraintsAndExpectedLength(duration, framesJump, frameDelay); + handleFramesJumpChange({ target: { value: framesJump } }); + handleFrameDelayChange({ target: { value: frameDelay } }); + setValidFramesJumpOptions(calculateValidFramesJumpOptions(duration)); + setIsVideoUploaded(true); + }).catch(error => { + console.error('Error getting video duration:', error); + }); + } + }, [videoFile]); + + const calculateValidFramesJumpOptions = (duration) => { + const options = []; + for (let i = 1; i <= duration; i++) { + if (duration % i === 0 && duration / i >= 2) { + options.push(i); + } + } + return options; + }; + + const updateConstraintsAndExpectedLength = (duration, framesJump, frameDelay) => { + const minDelay = calculateMinimumValidFrameDelay(duration, framesJump); + const maxDelay = calculateMaximumDelay(duration, framesJump); + + setMinFrameDelay(minDelay); + setMaxFrameDelay(maxDelay); + + const expectedLen = calculateExpectedLength(duration, framesJump, frameDelay); + setExpectedLength(expectedLen); + + // Adjust frame delay to valid range if necessary + if (!isDelayPerFrameValid(expectedLen)) { + const adjustedDelay = expectedLen < 30 ? minDelay : maxDelay; + handleFrameDelayChange({ target: { value: adjustedDelay } }); + setExpectedLength(calculateExpectedLength(duration, framesJump, adjustedDelay)); + } + }; + + const handleFileInputChange = (event) => { + const file = event.target.files[0]; + setVideoFile(file); + handleFileChange(event); + }; + + const handleFramesJumpInputChange = (event) => { + const value = parseInt(event.target.value); + handleFramesJumpChange({ target: { value } }); + updateConstraintsAndExpectedLength(videoDuration, value, frameDelay); + }; + + const handleFrameDelayInputChange = (event) => { + const value = Math.min(Math.max(parseInt(event.target.value), minFrameDelay), maxFrameDelay); + handleFrameDelayChange({ target: { value } }); + setExpectedLength(calculateExpectedLength(videoDuration, framesJump, value)); + }; + + return ( + <Form onSubmit={handleSubmit}> + <Form.Group controlId="videoFile"> + <Form.Label>Upload Video</Form.Label> + <Form.Control type="file" accept="video/*" onChange={handleFileInputChange} disabled={isLoading} /> + </Form.Group> + {isVideoUploaded && ( + <> + <Form.Group controlId="framesJump"> + <Form.Label>Frames Jump Seconds</Form.Label> + <Form.Control + as="select" + value={framesJump} + onChange={handleFramesJumpInputChange} + disabled={isLoading} + > + {validFramesJumpOptions.map(option => ( + <option key={option} value={option}> + {option} + </option> + ))} + </Form.Control> + </Form.Group> + <Form.Group controlId="frameDelay"> + <Form.Label>Delay per Frame (seconds)</Form.Label> + <Form.Control + type="number" + value={frameDelay} + onChange={handleFrameDelayInputChange} + min={minFrameDelay} + max={maxFrameDelay} + step="1" + disabled={isLoading} + /> + </Form.Group> + </> + )} + {isVideoUploaded && <p>Expected output video length: {expectedLength} seconds</p>} + <Button variant="primary" type="submit" disabled={isLoading || expectedLength > 600 || expectedLength < 30 || !isVideoUploaded}> + {isLoading ? 'Processing...' : 'Upload'} + </Button> + {isVideoUploaded && expectedLength > 600 && <p style={{ color: 'red' }}>The expected video length exceeds the maximum allowed duration of 10 minutes.</p>} + {isVideoUploaded && expectedLength < 30 && <p style={{ color: 'red' }}>The expected video length must be at least 30 seconds.</p>} + </Form> + ); +}; + +export default VideoUploadForm; \ No newline at end of file diff --git a/frontend/src/components/detection/video/videoUtils.js b/frontend/src/components/detection/video/videoUtils.js new file mode 100644 index 0000000..1efb7d6 --- /dev/null +++ b/frontend/src/components/detection/video/videoUtils.js @@ -0,0 +1,70 @@ +// src/components/detection/video/videoUtils.js + +export const getVideoDuration = (file) => { + return new Promise((resolve) => { + const video = document.createElement('video'); + video.preload = 'metadata'; + video.onloadedmetadata = function () { + window.URL.revokeObjectURL(video.src); + resolve(Math.floor(video.duration)); // Ensure duration is an integer + }; + video.src = URL.createObjectURL(file); + }); +}; + +export const calculateMinimumDelay = (duration, framesJump) => { + const frames = Math.floor(duration / framesJump); + return Math.max(30 / frames, 1); // Ensure at least 1 second delay per frame +}; + +export const calculateMaximumDelay = (duration, framesJump) => { + const frames = Math.floor(duration / framesJump); + return Math.min(600 / frames, 60); // Ensure the video length does not exceed 10 minutes +}; + +export const calculateExpectedLength = (duration, framesJump, frameDelay) => { + const frames = Math.floor(duration / framesJump); + return frames * frameDelay; +}; + +export const isValidVideoLength = (duration) => { + return duration >= 3 && duration <= 600; // Between 3 seconds and 10 minutes +}; + +export const calculateOptimalValues = (duration) => { + let framesJump = 1; // Default frame jump value + let frameDelay = Math.floor(30 / (duration / framesJump)); + + if (frameDelay < 1) { + frameDelay = 1; + framesJump = Math.floor(30 / (duration * frameDelay)); + } + + return { + framesJump: Math.max(Math.floor(framesJump), 1), // Minimum 1 second jump + frameDelay: Math.max(Math.ceil(frameDelay), 1) + }; +}; + +export const calculateMaxFrameJump = (duration) => { + return Math.floor(duration / 2); // Ensuring at least 2 frames +}; + +export const calculateValidFramesJumpOptions = (duration) => { + const options = []; + for (let i = 1; i <= duration; i++) { + if (duration % i === 0 && duration / i >= 2) { + options.push(i); + } + } + return options; +}; + +export const calculateMinimumValidFrameDelay = (duration, framesJump) => { + const frames = Math.floor(duration / framesJump); + return Math.max(Math.ceil(30 / frames), 1); // Ensure the video length is at least 30 seconds +}; + +export const isDelayPerFrameValid = (expectedLength) => { + return expectedLength >= 30 && expectedLength <= 600; +}; diff --git a/frontend/src/pages/Video/VideoDetectionPage.js b/frontend/src/pages/Video/VideoDetectionPage.js index f12b292..fb43528 100644 --- a/frontend/src/pages/Video/VideoDetectionPage.js +++ b/frontend/src/pages/Video/VideoDetectionPage.js @@ -7,13 +7,15 @@ import { checkDailyLimit, incrementDetection } from '../../services/dailyLimitAp import '../../styles/main.css'; import LoadingIndicator from '../../components/detection/LoadingIndicator'; import DetectionResults from '../../components/detection/DetectionResults'; -import UploadForm from '../../components/detection/UploadForm'; +import VideoUploadForm from '../../components/detection/video/VideoUploadForm'; import DetectionContainer from '../../components/detection/DetectionContainer'; import { Button } from 'react-bootstrap'; +import { getVideoDuration, calculateMinimumDelay, calculateMaximumDelay, calculateExpectedLength, isValidVideoLength } from '../../components/detection/video/videoUtils'; const VideoDetectionPage = () => { const [videoFile, setVideoFile] = useState(null); - const [frameInterval, setFrameInterval] = useState(1); + const [framesJump, setFramesJump] = useState(1); + const [frameDelay, setFrameDelay] = useState(1); const [result, setResult] = useState(null); const [isLoading, setIsLoading] = useState(false); const [videoName, setVideoName] = useState(null); @@ -79,8 +81,12 @@ const VideoDetectionPage = () => { setVideoFile(event.target.files[0]); }; - const handleFrameIntervalChange = (event) => { - setFrameInterval(event.target.value); + const handleFramesJumpChange = (event) => { + setFramesJump(parseInt(event.target.value)); + }; + + const handleFrameDelayChange = (event) => { + setFrameDelay(parseInt(event.target.value)); }; const handleSubmit = async (event) => { @@ -91,10 +97,28 @@ const VideoDetectionPage = () => { } if (videoFile) { + // Validate video duration here before proceeding + const videoDuration = await getVideoDuration(videoFile); + if (!isValidVideoLength(videoDuration)) { + alert('The video length must be between 3 seconds and 10 minutes.'); + return; + } + + // Calculate the minimum and maximum delay to ensure the output video is within the required length + const minDelay = calculateMinimumDelay(videoDuration, framesJump); + const maxDelay = calculateMaximumDelay(videoDuration, framesJump); + const expectedLength = calculateExpectedLength(videoDuration, framesJump, frameDelay); + + if (frameDelay < minDelay || frameDelay > maxDelay || expectedLength < 30 || expectedLength > 600) { + alert(`The delay per frame must be between ${minDelay} and ${maxDelay} seconds, and the expected output video length must be between 30 seconds and 10 minutes.`); + return; + } + setIsLoading(true); const formData = new FormData(); formData.append('video', videoFile); - formData.append('frame_interval', frameInterval); + formData.append('frames_jump', framesJump); + formData.append('frame_delay', frameDelay); try { const uploadResponse = await uploadVideo(formData); @@ -145,12 +169,13 @@ const VideoDetectionPage = () => { {!isLoading && ( <> <p>You have {limitInfo.remaining} out of {limitInfo.limit} video detections remaining today.</p> - <UploadForm + <VideoUploadForm handleFileChange={handleFileChange} handleSubmit={handleSubmit} - handleFrameIntervalChange={handleFrameIntervalChange} - handleCameraClick={() => { }} - handleGalleryClick={() => { }} + handleFramesJumpChange={handleFramesJumpChange} + handleFrameDelayChange={handleFrameDelayChange} + framesJump={framesJump} + frameDelay={frameDelay} isLoading={isLoading} /> </> diff --git a/frontend/src/styles/input/AmazingDropdown.css b/frontend/src/styles/input/AmazingDropdown.css new file mode 100644 index 0000000..01449b4 --- /dev/null +++ b/frontend/src/styles/input/AmazingDropdown.css @@ -0,0 +1,17 @@ +.dropdown-menu { + max-height: 200px; + overflow-y: auto; + background-color: #f8f9fa; + border: 1px solid #ced4da; + border-radius: 0.25rem; +} + +.dropdown-item { + padding: 10px 20px; + transition: background-color 0.3s ease, color 0.3s ease; +} + +.dropdown-item:hover { + background-color: #007bff; + color: #fff; +} \ No newline at end of file -- GitLab