Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save masterkain/641e43c623e5e30081733a5fb56a563b to your computer and use it in GitHub Desktop.
Save masterkain/641e43c623e5e30081733a5fb56a563b to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced Video File Interaction App</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
padding: 20px;
background-color: #f4f7f9; /* Softer background */
color: #333;
line-height: 1.6;
}
.app-container {
width: 100%;
max-width: 900px; /* Max width for the main content */
display: flex;
flex-direction: column;
gap: 25px;
}
header {
width: 100%;
text-align: center;
margin-bottom: 10px;
}
h1 {
color: #2c3e50; /* Darker, more professional blue/grey */
font-weight: 600;
font-size: 2.2em;
}
h2 {
color: #34495e; /* Slightly lighter than h1 */
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
margin-top: 0;
margin-bottom: 20px;
font-size: 1.6em;
}
.card {
background-color: #fff;
padding: 25px; /* Increased padding */
border-radius: 12px; /* More rounded */
box-shadow: 0 5px 15px rgba(0,0,0,0.08); /* Softer, more diffused shadow */
}
.video-setup video {
width: 100%; /* Make video responsive within its container */
max-width: 700px; /* Max width for the video itself */
height: auto;
border: 1px solid #ddd;
background-color: #000;
border-radius: 8px;
display: block; /* For centering if a max-width is set */
margin: 0 auto 20px auto; /* Center video if it's smaller than container */
}
.file-input-area {
display: flex;
flex-direction: column; /* Stack label and input/filename */
gap: 10px;
align-items: flex-start; /* Align items to the start */
margin-bottom: 20px;
}
.file-input-label {
background-color: #3498db; /* A pleasant blue */
color: white;
padding: 10px 18px;
border-radius: 6px;
cursor: pointer;
display: inline-block;
font-size: 0.95em;
transition: background-color 0.2s ease;
}
.file-input-label:hover {
background-color: #2980b9;
}
#fileName {
margin-top: 5px; /* Space between button and filename */
font-style: italic;
color: #555;
font-size: 0.9em;
}
.io-areas {
display: flex;
flex-direction: column;
gap: 20px; /* Increased gap for clarity */
}
.io-areas div {
display: flex;
flex-direction: column; /* Stack label above input */
}
textarea, input[type="text"], select {
width: 100%;
padding: 12px; /* More padding */
border: 1px solid #dde1e4;
border-radius: 6px;
font-size: 0.95em;
box-sizing: border-box;
margin-top: 5px; /* Space between label and input */
background-color: #fdfdfd;
}
textarea:focus, input[type="text"]:focus, select:focus {
border-color: #3498db; /* Accent color */
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15);
outline: none;
}
label {
font-weight: 600;
color: #4a4a4a; /* Dark grey for labels */
font-size: 1em;
}
.controls {
display: flex;
flex-wrap: wrap; /* Allow wrapping on smaller screens */
gap: 20px;
align-items: flex-end; /* Align items to the bottom if they have different heights */
margin-top: 25px; /* Space above the controls */
}
.controls > div { /* Target the div wrapping label and select */
flex-grow: 1; /* Allow interval select to take space */
}
#startButton {
padding: 12px 25px;
font-size: 1.05em;
font-weight: bold;
cursor: pointer;
border: none;
border-radius: 6px;
color: white;
transition: background-color 0.2s ease-in-out, transform 0.1s ease;
min-width: 180px; /* Ensure button has a decent width */
}
#startButton.start { background-color: #2ecc71; } /* Fresher green */
#startButton.start:hover { background-color: #27ae60; }
#startButton.stop { background-color: #e74c3c; } /* Fresher red */
#startButton.stop:hover { background-color: #c0392b; }
#startButton:disabled {
background-color: #bdc3c7; /* Neutral grey for disabled */
cursor: not-allowed;
opacity: 0.8;
}
#startButton:not(:disabled):active {
transform: translateY(1px);
}
#responseText {
min-height: 3em; /* Ensure it's tall enough for a few lines */
background-color: #f9f9f9; /* Slightly different background for readonly */
}
.hidden {
display: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.app-container {
gap: 20px;
}
.card {
padding: 20px;
}
h1 {
font-size: 1.8em;
}
h2 {
font-size: 1.4em;
}
.controls {
flex-direction: column;
align-items: stretch; /* Make controls stack vertically */
}
#startButton {
width: 100%; /* Full width button on small screens */
}
}
</style>
</head>
<body>
<header>
<h1>Video Analysis Dashboard</h1>
</header>
<main class="app-container">
<section class="card video-setup">
<h2>Video Source & Player</h2>
<div class="file-input-area">
<label for="videoFile" class="file-input-label">Choose Video File</label>
<input type="file" id="videoFile" accept="video/*" style="display: none;">
<span id="fileName">No file chosen</span>
</div>
<video id="videoFeed" controls playsinline></video>
<canvas id="canvas" class="hidden"></canvas>
</section>
<section class="card analysis-config">
<h2>Analysis Configuration</h2>
<div class="io-areas">
<div>
<label for="baseURL">Base API Endpoint:</label>
<input type="text" id="baseURL" name="BaseAPI" value="http://localhost:8080">
</div>
<div>
<label for="instructionText">Instruction for AI:</label>
<textarea id="instructionText" style="min-height: 3em;" name="Instruction"></textarea>
</div>
<div>
<label for="responseText">Live Response:</label>
<textarea id="responseText" name="Response" readonly placeholder="Server response will appear here..."></textarea>
</div>
</div>
<div class="controls">
<div>
<label for="intervalSelect">Processing Interval:</label>
<select id="intervalSelect" name="Interval between 2 requests">
<option value="100">100ms</option>
<option value="250">250ms</option>
<option value="500" selected>500ms</option>
<option value="1000">1s</option>
<option value="2000">2s</option>
<option value="5000">5s</option>
</select>
</div>
<button id="startButton" class="start" disabled>Start Processing</button>
</div>
</section>
</main>
<script>
const video = document.getElementById('videoFeed');
const canvas = document.getElementById('canvas');
const videoFile = document.getElementById('videoFile');
const fileNameDisplay = document.getElementById('fileName'); // For custom file input
const baseURLInput = document.getElementById('baseURL');
const instructionText = document.getElementById('instructionText');
const responseText = document.getElementById('responseText');
const intervalSelect = document.getElementById('intervalSelect');
const startButton = document.getElementById('startButton');
instructionText.value = "What do you see in this frame?"; // default instruction
let intervalId;
let isProcessing = false;
let currentObjectURL = null;
videoFile.addEventListener('change', function(event) {
if (isProcessing) {
handleStop();
}
if (currentObjectURL) {
URL.revokeObjectURL(currentObjectURL);
}
const file = event.target.files[0];
if (file) {
fileNameDisplay.textContent = file.name; // Update displayed filename
currentObjectURL = URL.createObjectURL(file);
video.src = currentObjectURL;
video.load();
responseText.value = "Video loaded. Ready to start processing.";
startButton.disabled = true; // Keep disabled until 'loadeddata'
} else {
fileNameDisplay.textContent = "No file chosen";
responseText.value = "No video file selected.";
startButton.disabled = true;
video.src = "";
}
});
async function sendChatCompletionRequest(instruction, imageBase64URL) {
const apiURL = `${baseURLInput.value}/v1/chat/completions`;
try {
const response = await fetch(apiURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
max_tokens: 150, // Slightly increased for potentially more descriptive answers
messages: [
{ role: 'user', content: [
{ type: 'text', text: instruction },
{ type: 'image_url', image_url: {
url: imageBase64URL,
} }
] },
]
})
});
if (!response.ok) {
const errorData = await response.text();
console.error(`Server error: ${response.status} - ${errorData} at ${apiURL}`);
return `Server error: ${response.status} - ${errorData.substring(0,100)}...`; // Show snippet of error
}
const data = await response.json();
if (data.choices && data.choices.length > 0 && data.choices[0].message) {
return data.choices[0].message.content;
} else {
console.error("Unexpected response structure:", data);
return "Received an unexpected response structure from the server.";
}
} catch (error) {
console.error(`Network error: ${error.message} while trying to reach ${apiURL}`);
return `Network error: ${error.message}`;
}
}
function captureImage() {
if (!video.videoWidth || video.ended) {
if (video.ended && isProcessing) {
responseText.value = "Video has ended. Stopping processing.";
handleStop();
}
return null;
}
// Only capture if video is playing or explicitly paused but processing is active
if (video.paused && !isProcessing && video.currentTime === 0) {
// Don't capture if paused at the very beginning before start is pressed
// This condition might need tweaking based on desired behavior for paused videos
// return null;
}
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
return canvas.toDataURL('image/jpeg', 0.75); // Adjusted quality slightly
}
async function sendData() {
if (!isProcessing) return;
if (video.ended) {
console.log("Video ended, stopping data send.");
handleStop();
responseText.value = "Video finished. Processing automatically stopped.";
return;
}
const instruction = instructionText.value;
const imageBase64URL = captureImage();
if (!imageBase64URL) {
if (!video.ended && !video.paused) { // Only show if not ended and not just paused
responseText.value = "Failed to capture image. Video might not be playing or fully loaded.";
} else if (video.paused && isProcessing){
// If deliberately processing while paused, we might still want to send.
// For now, let it proceed if image capture was successful.
}
return;
}
try {
const response = await sendChatCompletionRequest(instruction, imageBase64URL);
responseText.value = response;
} catch (error) {
console.error('Error sending data:', error);
responseText.value = `Error: ${error.message}`;
}
}
function handleStart() {
if (!video.src || video.readyState < video.HAVE_FUTURE_DATA) { // HAVE_FUTURE_DATA (3) or HAVE_ENOUGH_DATA (4)
responseText.value = "Video not loaded or not ready. Cannot start.";
alert("Please select a video file and wait for it to load fully.");
return;
}
isProcessing = true;
startButton.textContent = "Stop Processing";
startButton.classList.remove('start');
startButton.classList.add('stop');
instructionText.disabled = true;
intervalSelect.disabled = true;
videoFile.disabled = true;
baseURLInput.disabled = true;
document.querySelector('.file-input-label').classList.add('disabled-label');
if (video.paused) {
video.play().catch(e => console.warn("Autoplay on start failed:", e.message));
}
responseText.value = "Processing started...";
const intervalMs = parseInt(intervalSelect.value, 10);
// Initial immediate call if video is ready
if (!video.paused || video.currentTime > 0) {
sendData(); // Send first frame immediately
}
intervalId = setInterval(sendData, intervalMs);
}
function handleStop() {
isProcessing = false;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
startButton.textContent = "Start Processing";
startButton.classList.remove('stop');
startButton.classList.add('start');
if (video.src && video.readyState >= video.HAVE_METADATA) {
startButton.disabled = false;
} else {
startButton.disabled = true;
}
instructionText.disabled = false;
intervalSelect.disabled = false;
videoFile.disabled = false;
baseURLInput.disabled = false;
const fileLabel = document.querySelector('.file-input-label');
if (fileLabel) fileLabel.classList.remove('disabled-label');
if (responseText.value.startsWith("Processing started...") || responseText.value.startsWith("Sending frame...")) {
if (video.ended) {
responseText.value = "Video finished. Processing stopped.";
} else {
responseText.value = "Processing stopped by user.";
}
}
}
startButton.addEventListener('click', () => {
if (isProcessing) {
handleStop();
} else {
handleStart();
}
});
video.addEventListener('ended', () => {
// responseText.value = "Video finished."; // This message is now handled in sendData or handleStop
if (isProcessing) {
// handleStop will be called by sendData, or if it wasn't, call it here.
// This ensures UI is reset correctly if sendData's interval already cleared.
if (isProcessing) { // check again as sendData might have set it to false
responseText.value = "Video finished. Processing automatically stopped.";
handleStop();
}
} else {
responseText.value = "Video finished.";
// If a file is loaded, the start button should be enabled to re-process
if (videoFile.files.length > 0 && video.readyState >= video.HAVE_METADATA) {
startButton.disabled = false;
}
}
});
video.addEventListener('error', (e) => {
console.error("Video Error:", e);
let errorMsg = 'Unknown video error.';
if (e.target.error) {
switch (e.target.error.code) {
case e.target.error.MEDIA_ERR_ABORTED:
errorMsg = 'Video playback aborted.';
break;
case e.target.error.MEDIA_ERR_NETWORK:
errorMsg = 'A network error caused video download to fail.';
break;
case e.target.error.MEDIA_ERR_DECODE:
errorMsg = 'Video playback aborted due to a decoding error.';
break;
case e.target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
errorMsg = 'The video format is not supported.';
break;
default:
errorMsg = 'An unknown error occurred with the video.';
}
}
responseText.value = `Video error: ${errorMsg} Try a different video file.`;
fileNameDisplay.textContent = "Error with video file.";
startButton.disabled = true;
if (isProcessing) {
handleStop();
}
});
video.addEventListener('loadeddata', () => {
// console.log("Video data loaded, readyState:", video.readyState);
if (!isProcessing && videoFile.files.length > 0) {
startButton.disabled = false;
responseText.value = "Video ready. Click 'Start Processing'.";
}
});
video.addEventListener('canplay', () => {
// console.log("Video can play, readyState:", video.readyState);
if (!isProcessing && videoFile.files.length > 0) {
startButton.disabled = false;
if (!responseText.value.includes("Video loaded") && !responseText.value.includes("Video ready")) {
responseText.value = "Video can play. Click 'Start Processing'.";
}
}
});
window.addEventListener('beforeunload', () => {
if (currentObjectURL) {
URL.revokeObjectURL(currentObjectURL);
}
if (intervalId) {
clearInterval(intervalId);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment