Features: - Red-bordered warning message on comparison page when aspect ratio changed - Red outline with glow effect around optimized video player - Visual indicators clearly show when video aspect ratio differs from original - Warning states: "Aspect Ratio Changed: This video was converted to a different aspect ratio" - State properly tracked and reset when uploading new files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
486 lines
17 KiB
JavaScript
486 lines
17 KiB
JavaScript
// Video Optimizer Frontend JavaScript
|
||
// API Configuration (imported from config.js)
|
||
const API_BASE = CONFIG ? CONFIG.API_BASE : 'http://localhost:5000/api';
|
||
|
||
// State
|
||
let currentFileId = null;
|
||
let currentPlatforms = [];
|
||
let currentVideoInfo = null;
|
||
let aspectRatioChanged = false;
|
||
|
||
// DOM Elements
|
||
const dropZone = document.getElementById('dropZone');
|
||
const fileInput = document.getElementById('fileInput');
|
||
const uploadSection = document.getElementById('uploadSection');
|
||
const configSection = document.getElementById('configSection');
|
||
const comparisonSection = document.getElementById('comparisonSection');
|
||
const videoInfo = document.getElementById('videoInfo');
|
||
const platformSelect = document.getElementById('platformSelect');
|
||
const aspectRatioSelect = document.getElementById('aspectRatioSelect');
|
||
const bitrateInput = document.getElementById('bitrateInput');
|
||
const bitrateHint = document.getElementById('bitrateHint');
|
||
const formatInfo = document.getElementById('formatInfo');
|
||
const convertBtn = document.getElementById('convertBtn');
|
||
const progressBar = document.getElementById('progressBar');
|
||
const progressFill = document.getElementById('progressFill');
|
||
const progressText = document.getElementById('progressText');
|
||
const originalVideo = document.getElementById('originalVideo');
|
||
const optimizedVideo = document.getElementById('optimizedVideo');
|
||
const originalSource = document.getElementById('originalSource');
|
||
const optimizedSource = document.getElementById('optimizedSource');
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
loadPlatforms();
|
||
setupEventListeners();
|
||
});
|
||
|
||
// Event Listeners
|
||
function setupEventListeners() {
|
||
// Drag and drop
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
dropZone.addEventListener('dragover', handleDragOver);
|
||
dropZone.addEventListener('dragleave', handleDragLeave);
|
||
dropZone.addEventListener('drop', handleDrop);
|
||
|
||
// File input
|
||
fileInput.addEventListener('change', handleFileSelect);
|
||
|
||
// Platform/aspect ratio selection
|
||
platformSelect.addEventListener('change', handlePlatformChange);
|
||
aspectRatioSelect.addEventListener('change', handleAspectRatioChange);
|
||
|
||
// Convert button
|
||
convertBtn.addEventListener('click', handleConvert);
|
||
|
||
// Comparison controls
|
||
document.getElementById('syncPlayBtn').addEventListener('click', syncPlayback);
|
||
document.getElementById('pauseAllBtn').addEventListener('click', pauseAll);
|
||
document.getElementById('muteOriginal').addEventListener('click', () => toggleMute('original'));
|
||
document.getElementById('muteOptimized').addEventListener('click', () => toggleMute('optimized'));
|
||
document.getElementById('downloadOriginal').addEventListener('click', () => downloadFile('original'));
|
||
document.getElementById('downloadOptimized').addEventListener('click', () => downloadFile('optimized'));
|
||
document.getElementById('newFileBtn').addEventListener('click', resetApp);
|
||
}
|
||
|
||
// Drag and Drop Handlers
|
||
function handleDragOver(e) {
|
||
e.preventDefault();
|
||
dropZone.classList.add('dragover');
|
||
}
|
||
|
||
function handleDragLeave(e) {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('dragover');
|
||
}
|
||
|
||
function handleDrop(e) {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('dragover');
|
||
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
handleFile(files[0]);
|
||
}
|
||
}
|
||
|
||
function handleFileSelect(e) {
|
||
const files = e.target.files;
|
||
if (files.length > 0) {
|
||
handleFile(files[0]);
|
||
}
|
||
}
|
||
|
||
// File Handling
|
||
async function handleFile(file) {
|
||
if (!file.type.startsWith('video/')) {
|
||
alert('Please select a valid video file');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
showLoading();
|
||
|
||
const response = await fetch(`${API_BASE}/upload`, {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
currentFileId = data.file_id;
|
||
currentVideoInfo = data.video_info;
|
||
|
||
displayVideoInfo(data);
|
||
|
||
// Auto-select platform and aspect ratio if detected
|
||
if (data.detected_platform) {
|
||
platformSelect.value = data.detected_platform;
|
||
handlePlatformChange();
|
||
}
|
||
|
||
if (data.detected_aspect_ratio) {
|
||
aspectRatioSelect.value = data.detected_aspect_ratio;
|
||
handleAspectRatioChange();
|
||
}
|
||
|
||
uploadSection.style.display = 'none';
|
||
configSection.style.display = 'block';
|
||
} else {
|
||
alert('Error uploading file: ' + data.error);
|
||
}
|
||
} catch (error) {
|
||
alert('Error uploading file: ' + error.message);
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
// Platform Management
|
||
async function loadPlatforms() {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/platforms`);
|
||
const data = await response.json();
|
||
currentPlatforms = data.platforms;
|
||
|
||
// Populate platform select
|
||
platformSelect.innerHTML = '<option value="">Select Platform...</option>';
|
||
data.platforms.forEach(platform => {
|
||
const option = document.createElement('option');
|
||
option.value = platform.key;
|
||
option.textContent = platform.name;
|
||
platformSelect.appendChild(option);
|
||
});
|
||
} catch (error) {
|
||
console.error('Error loading platforms:', error);
|
||
}
|
||
}
|
||
|
||
function handlePlatformChange() {
|
||
const platformKey = platformSelect.value;
|
||
|
||
if (!platformKey) {
|
||
aspectRatioSelect.innerHTML = '<option value="">Select Platform First</option>';
|
||
aspectRatioSelect.disabled = true;
|
||
convertBtn.disabled = true;
|
||
formatInfo.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
const platform = currentPlatforms.find(p => p.key === platformKey);
|
||
|
||
if (platform) {
|
||
// Populate aspect ratio select
|
||
aspectRatioSelect.innerHTML = '<option value="">Select Aspect Ratio...</option>';
|
||
platform.formats.forEach(format => {
|
||
const option = document.createElement('option');
|
||
option.value = format.ratio;
|
||
option.textContent = `${format.ratio} (${format.size})`;
|
||
aspectRatioSelect.appendChild(option);
|
||
});
|
||
aspectRatioSelect.disabled = false;
|
||
|
||
// Show codec info
|
||
formatInfo.innerHTML = `
|
||
<h4>Platform: ${platform.name}</h4>
|
||
<p><strong>Codec:</strong> ${platform.codec}</p>
|
||
`;
|
||
}
|
||
|
||
validateForm();
|
||
}
|
||
|
||
function handleAspectRatioChange() {
|
||
const platformKey = platformSelect.value;
|
||
const aspectRatio = aspectRatioSelect.value;
|
||
|
||
if (!platformKey || !aspectRatio) {
|
||
convertBtn.disabled = true;
|
||
return;
|
||
}
|
||
|
||
const platform = currentPlatforms.find(p => p.key === platformKey);
|
||
const format = platform.formats.find(f => f.ratio === aspectRatio);
|
||
|
||
if (format) {
|
||
// Update format info
|
||
formatInfo.innerHTML = `
|
||
<h4>Platform: ${platform.name}</h4>
|
||
<p><strong>Codec:</strong> ${platform.codec}</p>
|
||
<p><strong>Resolution:</strong> ${format.size}</p>
|
||
<p><strong>Recommended Bitrate:</strong> ${format.bitrate}</p>
|
||
<p><strong>Bitrate Range:</strong> ${format.bitrate_min} - ${format.bitrate_max}</p>
|
||
<p><strong>Audio Bitrate:</strong> ${format.audio}</p>
|
||
${format.note ? `<p><strong>Note:</strong> ${format.note}</p>` : ''}
|
||
`;
|
||
|
||
// Update bitrate hint
|
||
bitrateHint.textContent = `Recommended: ${format.bitrate} (Range: ${format.bitrate_min} - ${format.bitrate_max})`;
|
||
|
||
// Check for aspect ratio mismatch and show warning
|
||
const warningDiv = document.getElementById('aspectRatioWarning');
|
||
if (currentVideoInfo && currentVideoInfo.aspect_ratio !== aspectRatio) {
|
||
warningDiv.style.display = 'block';
|
||
aspectRatioChanged = true;
|
||
} else {
|
||
warningDiv.style.display = 'none';
|
||
aspectRatioChanged = false;
|
||
}
|
||
}
|
||
|
||
validateForm();
|
||
}
|
||
|
||
function validateForm() {
|
||
const platformKey = platformSelect.value;
|
||
const aspectRatio = aspectRatioSelect.value;
|
||
|
||
convertBtn.disabled = !(platformKey && aspectRatio);
|
||
}
|
||
|
||
// Video Conversion
|
||
async function handleConvert() {
|
||
const platformKey = platformSelect.value;
|
||
const aspectRatio = aspectRatioSelect.value;
|
||
const customBitrate = bitrateInput.value.trim() || null;
|
||
|
||
if (!currentFileId || !platformKey || !aspectRatio) {
|
||
alert('Please complete all required fields');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
convertBtn.disabled = true;
|
||
progressBar.style.display = 'block';
|
||
progressFill.style.width = '50%';
|
||
progressText.textContent = 'Converting video...';
|
||
|
||
const response = await fetch(`${API_BASE}/convert`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
file_id: currentFileId,
|
||
platform: platformKey,
|
||
aspect_ratio: aspectRatio,
|
||
custom_bitrate: customBitrate
|
||
})
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
progressFill.style.width = '100%';
|
||
progressText.textContent = 'Conversion complete!';
|
||
|
||
// Display comparison
|
||
displayComparison(data);
|
||
|
||
setTimeout(() => {
|
||
configSection.style.display = 'none';
|
||
comparisonSection.style.display = 'block';
|
||
}, 1000);
|
||
} else {
|
||
alert('Conversion error: ' + data.error);
|
||
convertBtn.disabled = false;
|
||
}
|
||
} catch (error) {
|
||
alert('Conversion error: ' + error.message);
|
||
convertBtn.disabled = false;
|
||
} finally {
|
||
setTimeout(() => {
|
||
progressBar.style.display = 'none';
|
||
progressFill.style.width = '0%';
|
||
}, 1500);
|
||
}
|
||
}
|
||
|
||
// Display Functions
|
||
function displayVideoInfo(data) {
|
||
const info = data.video_info;
|
||
const detected = [];
|
||
|
||
if (data.detected_platform) {
|
||
const platform = currentPlatforms.find(p => p.key === data.detected_platform);
|
||
detected.push(`Platform: ${platform ? platform.name : data.detected_platform}`);
|
||
}
|
||
|
||
if (data.detected_aspect_ratio) {
|
||
detected.push(`Aspect Ratio: ${data.detected_aspect_ratio}`);
|
||
}
|
||
|
||
videoInfo.innerHTML = `
|
||
<h3>📹 ${data.filename}</h3>
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span class="info-label">Resolution</span>
|
||
<span class="info-value">${info.width} × ${info.height}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">Duration</span>
|
||
<span class="info-value">${formatDuration(info.duration)}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">File Size</span>
|
||
<span class="info-value">${formatBytes(info.size)}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">Bitrate</span>
|
||
<span class="info-value">${info.bitrate} kbps</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">Codec</span>
|
||
<span class="info-value">${info.codec}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">Aspect Ratio</span>
|
||
<span class="info-value">${info.aspect_ratio}</span>
|
||
</div>
|
||
</div>
|
||
${detected.length > 0 ? `<p style="margin-top: 1rem; color: var(--primary-yellow);">🎯 Auto-detected: ${detected.join(', ')}</p>` : ''}
|
||
`;
|
||
}
|
||
|
||
function displayComparison(data) {
|
||
// Update stats
|
||
document.getElementById('originalSize').textContent = formatBytes(data.input_size);
|
||
document.getElementById('optimizedSize').textContent = formatBytes(data.output_size);
|
||
document.getElementById('reduction').textContent = `${data.size_reduction_percent}%`;
|
||
|
||
// Set video sources
|
||
originalSource.src = `${API_BASE}/stream/original/${currentFileId}`;
|
||
optimizedSource.src = `${API_BASE}/stream/optimized/${currentFileId}`;
|
||
|
||
// Display video specifications
|
||
displayVideoSpecs('originalSpecs', {
|
||
platform: 'Unknown',
|
||
resolution: `${currentVideoInfo.width}×${currentVideoInfo.height}`,
|
||
codec: currentVideoInfo.codec.toUpperCase(),
|
||
bitrate: `${currentVideoInfo.bitrate} kbps`,
|
||
duration: formatDuration(currentVideoInfo.duration),
|
||
fileSize: formatBytes(data.input_size),
|
||
aspectRatio: currentVideoInfo.aspect_ratio
|
||
});
|
||
|
||
const conversionDetails = data.conversion_details;
|
||
displayVideoSpecs('optimizedSpecs', {
|
||
resolution: conversionDetails.resolution,
|
||
codec: conversionDetails.codec.replace('lib', '').toUpperCase(),
|
||
bitrate: conversionDetails.bitrate,
|
||
duration: formatDuration(conversionDetails.duration),
|
||
fileSize: formatBytes(data.output_size),
|
||
platform: conversionDetails.platform.charAt(0).toUpperCase() + conversionDetails.platform.slice(1),
|
||
aspectRatio: conversionDetails.aspect_ratio
|
||
});
|
||
|
||
// Show warning and red outline if aspect ratio was changed
|
||
const comparisonWarning = document.getElementById('aspectRatioChangedWarning');
|
||
if (aspectRatioChanged) {
|
||
comparisonWarning.style.display = 'block';
|
||
optimizedVideo.classList.add('aspect-changed');
|
||
} else {
|
||
comparisonWarning.style.display = 'none';
|
||
optimizedVideo.classList.remove('aspect-changed');
|
||
}
|
||
|
||
// Reload videos
|
||
originalVideo.load();
|
||
optimizedVideo.load();
|
||
}
|
||
|
||
function displayVideoSpecs(elementId, specs) {
|
||
const specsElement = document.getElementById(elementId);
|
||
specsElement.innerHTML = `
|
||
${specs.platform ? `<div class="spec-row"><span class="spec-label">Platform:</span><span class="spec-value">${specs.platform}</span></div>` : ''}
|
||
<div class="spec-row"><span class="spec-label">Resolution:</span><span class="spec-value">${specs.resolution}</span></div>
|
||
<div class="spec-row"><span class="spec-label">Aspect Ratio:</span><span class="spec-value">${specs.aspectRatio}</span></div>
|
||
<div class="spec-row"><span class="spec-label">Codec:</span><span class="spec-value">${specs.codec}</span></div>
|
||
<div class="spec-row"><span class="spec-label">Bitrate:</span><span class="spec-value">${specs.bitrate}</span></div>
|
||
<div class="spec-row"><span class="spec-label">Duration:</span><span class="spec-value">${specs.duration}</span></div>
|
||
<div class="spec-row"><span class="spec-label">File Size:</span><span class="spec-value">${specs.fileSize}</span></div>
|
||
`;
|
||
}
|
||
|
||
// Video Playback Controls
|
||
function syncPlayback() {
|
||
originalVideo.currentTime = 0;
|
||
optimizedVideo.currentTime = 0;
|
||
originalVideo.play();
|
||
optimizedVideo.play();
|
||
}
|
||
|
||
function pauseAll() {
|
||
originalVideo.pause();
|
||
optimizedVideo.pause();
|
||
}
|
||
|
||
// Mute Controls
|
||
function toggleMute(type) {
|
||
const video = type === 'original' ? originalVideo : optimizedVideo;
|
||
const button = document.getElementById(type === 'original' ? 'muteOriginal' : 'muteOptimized');
|
||
|
||
video.muted = !video.muted;
|
||
|
||
if (video.muted) {
|
||
button.textContent = '🔇 Unmute';
|
||
button.classList.add('muted');
|
||
} else {
|
||
button.textContent = '🔊 Mute';
|
||
button.classList.remove('muted');
|
||
}
|
||
}
|
||
|
||
// Download
|
||
function downloadFile(type) {
|
||
window.open(`${API_BASE}/download/${type}/${currentFileId}`, '_blank');
|
||
}
|
||
|
||
// Utility Functions
|
||
function formatBytes(bytes) {
|
||
if (bytes === 0) return '0 Bytes';
|
||
const k = 1024;
|
||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
||
}
|
||
|
||
function formatDuration(seconds) {
|
||
const mins = Math.floor(seconds / 60);
|
||
const secs = Math.floor(seconds % 60);
|
||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
function showLoading() {
|
||
uploadSection.classList.add('loading');
|
||
}
|
||
|
||
function hideLoading() {
|
||
uploadSection.classList.remove('loading');
|
||
}
|
||
|
||
function resetApp() {
|
||
// Clean up files
|
||
if (currentFileId) {
|
||
fetch(`${API_BASE}/cleanup/${currentFileId}`, { method: 'DELETE' });
|
||
}
|
||
|
||
// Reset state
|
||
currentFileId = null;
|
||
currentVideoInfo = null;
|
||
aspectRatioChanged = false;
|
||
fileInput.value = '';
|
||
platformSelect.value = '';
|
||
aspectRatioSelect.value = '';
|
||
bitrateInput.value = '';
|
||
formatInfo.innerHTML = '';
|
||
|
||
// Reset display
|
||
uploadSection.style.display = 'block';
|
||
configSection.style.display = 'none';
|
||
comparisonSection.style.display = 'none';
|
||
}
|