import React, { useState, useRef } from 'react';
import { Image, Download, AlertCircle, CheckCircle, Loader2, Play } from 'lucide-react';
const DEFAULT_URLS = `https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1d7d5539722178e677a92a/1780317528116/OTS_shots_with_chatbot_mascots_202606011426.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1d7eda39722178e6786899/1780317937978/Hero+video_v4.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1eb2262df35c08ea748e05/1780396584936/2_jap+problem.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1ef470b971ee3cd8d5ba21/1780413555189/2_solution_v2.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38ad3aa0431ee4e4d005/1780234419680/Card_1_0_Hero.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1cbbfd9c41d54b8f779225/1780268030454/Card+1_1_Fully+Animated+3D+Characters.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c383712c80708b0d2d04b/1780234301939/Card+1_2_Tone+of+voice.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c383733cd2d796d5cef59/1780234302285/Card+1_3+Quik+reskin.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38ad780b2d06b8c373e6/1780234420843/Card_2_0_hero.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38376af2ef7ddd6492ea/1780234303901/Card+2_1_Map+Every+Journey.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c3837bddf62635f2de1e9/1780234301476/Card+2_2_Powered+by+Your+Raw+Data.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38374a7eaf7926d66b77/1780234304980/Card+2_3_Omnichannel+Deployment.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c3837a2b5ad6d4183e5d3/1780234304368/Card+3_0_hero.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38ad3ce62272d156b791/1780234419716/Card_3_1+Real-Time+Insights.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c398a7e38353bd63a596b/1780234637751/Card+3_2+Emotional+Intelligence.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38ad12c80708b0d2f520/1780234418972/Card_3_3_Track+Your+ROI.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c398a0bd25a1db0a69660/1780234638209/Card+4_0_Hero.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c398a3aa0431ee4e4f888/1780234638077/Card+4_1.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38ad1eaa0b5d789061a5/1780234420750/Card+4_2.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/6a1c38adac56b001da72d9a0/1780234420005/Card+4_3.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/69d47b4aab856a27e86bd40b/1775532907808/Willy.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/69d47b4a5ec75b0f31bbec33/1775532907808/Biek_1.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/69d47b4a4a1bc73ee1ec1cd9/1775532895446/Liza.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/69d47b4a960144044bf3569e/1775532902828/Fin.mp4
https://static1.squarespace.com/static/67fe6263177965472e25bc12/t/69d47b4a0465de355013688f/1775532907904/Woof.mp4`;
export default function App() {
const [urlsInput, setUrlsInput] = useState(DEFAULT_URLS);
const [results, setResults] = useState([]);
const [isProcessing, setIsProcessing] = useState(false);
const [progress, setProgress] = useState({ current: 0, total: 0 });
const getFilenameFromUrl = (url) => {
try {
const parts = url.split('?')[0].split('/');
let filename = parts[parts.length - 1] || 'frame';
// Decode URI components (e.g., %20 to space, + to space)
filename = decodeURIComponent(filename.replace(/\+/g, ' '));
// Replace .mp4 or .webm with .png
if (filename.toLowerCase().endsWith('.mp4')) {
filename = filename.slice(0, -4) + '.png';
} else if (filename.toLowerCase().endsWith('.webm')) {
filename = filename.slice(0, -5) + '.png';
} else {
filename += '.png';
}
return filename;
} catch (e) {
return `frame_${Date.now()}.png`;
}
};
const extractFrame = (url) => {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.crossOrigin = 'anonymous'; // Important to avoid Canvas Tainting
video.muted = true;
video.playsInline = true;
video.src = url;
let timeoutId = setTimeout(() => {
reject(new Error("Timeout loading video"));
}, 15000); // 15 second timeout
const handleLoadedData = () => {
// Seek to 0.1 seconds to guarantee a frame is drawn (frame 0 is sometimes blank on the web)
video.currentTime = 0.1;
};
const handleSeeked = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || 1920; // Fallbacks just in case
canvas.height = video.videoHeight || 1080;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL('image/png');
clearTimeout(timeoutId);
resolve(dataUrl);
} catch (err) {
clearTimeout(timeoutId);
reject(new Error("Canvas tainted by CORS or draw error."));
}
};
const handleError = () => {
clearTimeout(timeoutId);
reject(new Error("Failed to load video or CORS error."));
};
video.addEventListener('loadeddata', handleLoadedData);
video.addEventListener('seeked', handleSeeked);
video.addEventListener('error', handleError);
// Trigger load
video.load();
});
};
const processVideos = async () => {
const urls = urlsInput
.split('\n')
.map(u => u.trim())
.filter(u => u.length > 0);
if (urls.length === 0) return;
setIsProcessing(true);
setResults([]);
setProgress({ current: 0, total: urls.length });
const newResults = [];
// Process sequentially to prevent browser overload/crashes
for (let i = 0; i < urls.length; i++) {
const url = urls[i];
const filename = getFilenameFromUrl(url);
setProgress({ current: i + 1, total: urls.length });
try {
const dataUrl = await extractFrame(url);
newResults.push({
id: i,
url,
filename,
dataUrl,
status: 'success'
});
} catch (error) {
newResults.push({
id: i,
url,
filename,
error: error.message,
status: 'error'
});
}
// Update state incrementally so user sees progress
setResults([...newResults]);
}
setIsProcessing(false);
};
const downloadImage = (dataUrl, filename) => {
const a = document.createElement('a');
a.href = dataUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const downloadAll = () => {
// Triggers downloads with a slight delay to bypass popup blockers
const successful = results.filter(r => r.status === 'success');
successful.forEach((result, idx) => {
setTimeout(() => {
downloadImage(result.dataUrl, result.filename);
}, idx * 300);
});
};
return (
);
}
{/* Header */}
Video Frame Extractor
Automatically capture the first frame of a list of videos and download them as PNGs.
{/* Controls Column */}
{/* Results Column */}
{results.length === 0 ? (
) : (
Extracted Frames {results.length > 0 && `(${results.filter(r => r.status === 'success').length})`}
{results.filter(r => r.status === 'success').length > 0 && ( )}No frames extracted yet. Click "Extract Frames" to begin.
{results.map((result) => (
)}
{/* Image Preview / Error State */}
{result.status === 'success' && (
)}
))}
{result.status === 'success' ? (
) : (
Extraction Failed
{result.error}
)}
{/* Status Badge */}
{/* Info & Actions */}
{result.status === 'success' ? (
Success
) : (
Failed
)}
{result.filename}