// Upload drag/drop
uploadBox.addEventListener('click', () => videoInput.click());
uploadBox.addEventListener('dragover', (e) => { e.preventDefault(); uploadBox.classList.add('drag-over'); });
uploadBox.addEventListener('dragleave', () => uploadBox.classList.remove('drag-over'));
uploadBox.addEventListener('drop', (e) => {
e.preventDefault();
uploadBox.classList.remove('drag-over');
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
});
videoInput.addEventListener('change', () => {
if (videoInput.files.length) handleFile(videoInput.files[0]);
});
async function handleFile(file) {
if (!file.type.startsWith('video/')) {
showError('Please select a video file.');
return;
}
hideError();
setStatus('Uploading video…', 10);
try {
const fd = new FormData();
fd.append('video', file);
const res = await fetch('/api/upload', { method: 'POST', body: fd });
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Upload failed');
jobId = data.jobId;
videoUrl = data.videoUrl;
// Save user ID for billing
if (data.userId) setUserId(data.userId);
setStatus('Analyzing footage…', 40);
await analyzeVideo(file);
} catch (err) {
showError(err.message);
statusBar.classList.remove('visible');
}
}
async function analyzeVideo(file) {
setStatus('Sampling frames…', 60);
const video = document.createElement('video');
video.src = URL.createObjectURL(file);
video.muted = true;
await new Promise((resolve) => {
video.onloadedmetadata = resolve;
video.load();
});
const duration = video.duration;
const frameCount = Math.min(15, Math.floor(duration / 10) + 1);
const interval = duration / frameCount;
const frames = [];
// Seek + capture frames via canvas
const canvas = document.createElement('canvas');
canvas.width = 320;
canvas.height = 180;
const ctx = canvas.getContext('2d');
for (let i = 0; i < frameCount; i++) {
const t = i * interval;
await new Promise((resolve) => {
video.currentTime = t;
video.onseeked = () => {
ctx.drawImage(video, 0, 0, 320, 180);
frames.push({
timestamp: Math.round(t),
dataUrl: canvas.toDataURL('image/jpeg', 0.6)
});
resolve();
};
});
setStatus(`Analyzing frame ${i + 1}/${frameCount}…`, 60 + Math.round((i / frameCount) * 20));
}
URL.revokeObjectURL(video.src);
setStatus('AI picking the best clips…', 85);
const res = await fetch('/api/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jobId, frames })
});
if (res.status === 402) {
// Usage limit hit — redirect to pricing
const data = await res.json();
showErrorHtml('Monthly clip limit reached. No clips generated. Try a longer video.
';
return;
}
clipsList.innerHTML = clips.map(clip => `