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