React Integration
Learn how to integrate VideoIntel.js into your React applications with practical examples and best practices.
Basic Integration
Here's a simple React component that uses VideoIntel to analyze videos:
VideoAnalyzer.tsxtypescript
import { useState } from 'react';
import videoIntel, { type AnalysisResult } from 'videointel';
export default function VideoAnalyzer() {
const [file, setFile] = useState<File | null>(null);
const [results, setResults] = useState<AnalysisResult | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
setFile(selectedFile);
setError(null);
}
};
const analyzeVideo = async () => {
if (!file) return;
setLoading(true);
setError(null);
try {
const result = await videoIntel.analyze(file, {
thumbnails: { count: 5, quality: 0.8 },
scenes: { threshold: 30 },
colors: { count: 5 },
metadata: true,
});
setResults(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to analyze video');
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Video Analyzer</h1>
<div className="mb-6">
<input
type="file"
accept="video/*"
onChange={handleFileChange}
className="block w-full text-sm"
/>
</div>
<button
onClick={analyzeVideo}
disabled={!file || loading}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{loading ? 'Analyzing...' : 'Analyze Video'}
</button>
{error && (
<div className="mt-4 p-4 bg-red-50 text-red-700 rounded">
{error}
</div>
)}
{results && (
<div className="mt-6 space-y-4">
{results.thumbnails && (
<div>
<h2 className="text-xl font-semibold mb-2">Thumbnails</h2>
<div className="grid grid-cols-3 gap-4">
{results.thumbnails.map((thumb, i) => (
<img
key={i}
src={thumb.dataUrl}
alt={`Thumbnail ${i + 1}`}
className="rounded shadow"
/>
))}
</div>
</div>
)}
{results.colors && (
<div>
<h2 className="text-xl font-semibold mb-2">Colors</h2>
<div className="flex gap-2">
{results.colors.map((color, i) => (
<div
key={i}
className="w-16 h-16 rounded"
style={{ backgroundColor: color.hex }}
title={`${color.hex} - ${color.percentage}%`}
/>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}Video Uploader Component
A reusable video uploader component with drag-and-drop support:
VideoUploader.tsxtypescript
import { useCallback, useState } from 'react';
interface VideoUploaderProps {
onVideoSelect: (file: File) => void;
accept?: string;
maxSizeMB?: number;
}
export default function VideoUploader({
onVideoSelect,
accept = 'video/*',
maxSizeMB = 100,
}: VideoUploaderProps) {
const [dragActive, setDragActive] = useState(false);
const [error, setError] = useState<string | null>(null);
const validateFile = (file: File): boolean => {
// Check file type
if (!file.type.startsWith('video/')) {
setError('Please select a valid video file');
return false;
}
// Check file size
const maxSize = maxSizeMB * 1024 * 1024;
if (file.size > maxSize) {
setError(`File size exceeds ${maxSizeMB}MB limit`);
return false;
}
setError(null);
return true;
};
const handleFile = (file: File) => {
if (validateFile(file)) {
onVideoSelect(file);
}
};
const handleDrag = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true);
} else if (e.type === 'dragleave') {
setDragActive(false);
}
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
const file = e.dataTransfer.files?.[0];
if (file) handleFile(file);
}, []);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) handleFile(file);
};
return (
<div>
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
dragActive
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 hover:border-gray-400'
}`}
>
<input
type="file"
accept={accept}
onChange={handleChange}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
/>
<div className="pointer-events-none">
<p className="text-lg font-medium text-gray-700">
Drag and drop your video here
</p>
<p className="mt-1 text-sm text-gray-500">
or click to browse (max {maxSizeMB}MB)
</p>
</div>
</div>
{error && (
<p className="mt-2 text-sm text-red-600">{error}</p>
)}
</div>
);
}Thumbnail Gallery
Display thumbnails in a responsive gallery with download functionality:
ThumbnailGallery.tsxtypescript
import type { Thumbnail } from 'videointel';
interface ThumbnailGalleryProps {
thumbnails: Thumbnail[];
onThumbnailClick?: (thumbnail: Thumbnail, index: number) => void;
}
export default function ThumbnailGallery({
thumbnails,
onThumbnailClick,
}: ThumbnailGalleryProps) {
const downloadThumbnail = (thumb: Thumbnail, index: number) => {
const link = document.createElement('a');
link.href = thumb.dataUrl;
link.download = `thumbnail-${index + 1}.jpg`;
link.click();
};
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{thumbnails.map((thumb, index) => (
<div
key={index}
className="group relative rounded-lg overflow-hidden shadow-lg hover:shadow-xl transition-shadow"
>
<img
src={thumb.dataUrl}
alt={`Thumbnail at ${formatTime(thumb.timestamp)}`}
className="w-full h-auto cursor-pointer"
onClick={() => onThumbnailClick?.(thumb, index)}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
<div className="absolute bottom-0 left-0 right-0 p-4">
<div className="flex items-center justify-between text-white">
<div>
<div className="text-sm font-medium">
{formatTime(thumb.timestamp)}
</div>
<div className="text-xs opacity-80">
Quality: {(thumb.quality * 100).toFixed(0)}%
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
downloadThumbnail(thumb, index);
}}
className="px-3 py-1 bg-white/20 hover:bg-white/30 rounded text-sm backdrop-blur-sm transition-colors"
>
Download
</button>
</div>
</div>
</div>
</div>
))}
</div>
);
}Progress Tracking
Track analysis progress with a custom hook:
useVideoAnalysis.tstypescript
import { useState, useCallback } from 'react';
import videoIntel, { type AnalysisOptions, type AnalysisResult } from 'videointel';
interface AnalysisState {
loading: boolean;
progress: number;
status: string;
results: AnalysisResult | null;
error: Error | null;
}
export function useVideoAnalysis() {
const [state, setState] = useState<AnalysisState>({
loading: false,
progress: 0,
status: '',
results: null,
error: null,
});
const analyze = useCallback(async (file: File, options?: AnalysisOptions) => {
setState({
loading: true,
progress: 0,
status: 'Initializing...',
results: null,
error: null,
});
try {
// Initialize
await videoIntel.init();
setState(prev => ({ ...prev, progress: 10, status: 'Loading video...' }));
// Start analysis
setState(prev => ({ ...prev, progress: 20, status: 'Analyzing video...' }));
const results = await videoIntel.analyze(file, options);
setState({
loading: false,
progress: 100,
status: 'Complete',
results,
error: null,
});
return results;
} catch (error) {
setState({
loading: false,
progress: 0,
status: 'Failed',
results: null,
error: error as Error,
});
throw error;
}
}, []);
const reset = useCallback(() => {
setState({
loading: false,
progress: 0,
status: '',
results: null,
error: null,
});
}, []);
return { ...state, analyze, reset };
}
// Usage example
function VideoAnalyzer() {
const { loading, progress, status, results, error, analyze } = useVideoAnalysis();
const handleAnalyze = async (file: File) => {
try {
await analyze(file, {
thumbnails: { count: 5 },
scenes: { threshold: 30 },
colors: { count: 5 },
metadata: true,
});
} catch (err) {
console.error('Analysis failed:', err);
}
};
return (
<div>
{loading && (
<div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${progress}%` }}
/>
</div>
<p className="mt-2 text-sm text-gray-600">{status}</p>
</div>
)}
{/* ... rest of component ... */}
</div>
);
}Error Handling
Implement robust error handling for video analysis:
ErrorBoundary.tsxtypescript
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class VideoAnalysisErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Video analysis error:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
this.props.fallback || (
<div className="p-6 bg-red-50 border border-red-200 rounded-lg">
<h2 className="text-lg font-semibold text-red-900 mb-2">
Something went wrong
</h2>
<p className="text-sm text-red-700">
{this.state.error?.message || 'Failed to analyze video'}
</p>
<button
onClick={() => this.setState({ hasError: false, error: null })}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Try Again
</button>
</div>
)
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<VideoAnalysisErrorBoundary>
<VideoAnalyzer />
</VideoAnalysisErrorBoundary>
);
}💡 Pro Tips
- • Use React.memo() to prevent unnecessary re-renders of thumbnail components
- • Implement cleanup in useEffect when component unmounts
- • Consider using React Query for caching analysis results
- • Use Web Workers via VideoIntel's built-in support for better performance
📚 More Examples