LLMs Resources
Video Processing Utilities
Overview
Video processing is a crucial aspect of modern web applications, enabling features like video uploads, transformations, compression, and custom playback experiences. This guide covers essential video processing utilities and techniques for Next.js applications, helping you implement efficient video handling in your projects.
Whether you're building a video-centric platform, adding video capabilities to an existing application, or optimizing video delivery, these utilities and approaches will help you create performant and user-friendly video experiences.
Techniques
Client-Side Video Processing
Browser-based video processing leverages Web APIs to manipulate videos directly in the client's browser:
- HTML5 Video API: Control playback, speed, and basic video functions
- Canvas API: Apply real-time filters, effects, and transformations
- MediaRecorder API: Capture and encode video streams
- WebRTC: Enable peer-to-peer video streaming and recording
Server-Side Video Processing
For more intensive operations, server-side processing offers greater capabilities:
- FFmpeg Integration: Transcode, compress, and transform videos
- Video Streaming: Implement HLS or DASH for adaptive bitrate streaming
- Thumbnail Generation: Create preview images from video frames
- Video Analytics: Extract metadata and analyze video content
Video Optimization
Optimize videos for web delivery and performance:
- Compression: Reduce file size while maintaining acceptable quality
- Format Selection: Choose appropriate formats (MP4, WebM, AV1)
- Lazy Loading: Defer video loading until needed
- CDN Integration: Distribute videos globally for faster delivery
Code Examples
Custom Video Player Component
"use client" import { useState, useRef, useEffect } from "react" import { Play, Pause, Volume2, VolumeX, Maximize } from 'lucide-react' export default function CustomVideoPlayer({ src, poster }) { const videoRef = useRef(null) const [isPlaying, setIsPlaying] = useState(false) const [progress, setProgress] = useState(0) const [currentTime, setCurrentTime] = useState(0) const [duration, setDuration] = useState(0) const [isMuted, setIsMuted] = useState(false) const [volume, setVolume] = useState(1) useEffect(() => { const video = videoRef.current if (!video) return const updateProgress = () => { const currentProgress = (video.currentTime / video.duration) * 100 setProgress(currentProgress) setCurrentTime(video.currentTime) } const handleLoadedMetadata = () => { setDuration(video.duration) } video.addEventListener("timeupdate", updateProgress) video.addEventListener("loadedmetadata", handleLoadedMetadata) return () => { video.removeEventListener("timeupdate", updateProgress) video.removeEventListener("loadedmetadata", handleLoadedMetadata) } }, []) const togglePlay = () => { const video = videoRef.current if (isPlaying) { video.pause() } else { video.play() } setIsPlaying(!isPlaying) } const toggleMute = () => { const video = videoRef.current video.muted = !isMuted setIsMuted(!isMuted) } const handleVolumeChange = (e) => { const newVolume = parseFloat(e.target.value) const video = videoRef.current video.volume = newVolume setVolume(newVolume) setIsMuted(newVolume === 0) } const handleProgressChange = (e) => { const newProgress = parseFloat(e.target.value) const video = videoRef.current video.currentTime = (newProgress / 100) * video.duration setProgress(newProgress) } const handleFullscreen = () => { const video = videoRef.current if (video.requestFullscreen) { video.requestFullscreen() } else if (video.webkitRequestFullscreen) { video.webkitRequestFullscreen() } else if (video.msRequestFullscreen) { video.msRequestFullscreen() } } const formatTime = (timeInSeconds) => { const minutes = Math.floor(timeInSeconds / 60) const seconds = Math.floor(timeInSeconds % 60) return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}` } return ( <div className="relative group rounded-lg overflow-hidden bg-black"> <video ref={videoRef} src={src} poster={poster} className="w-full h-auto" onClick={togglePlay} playsInline /> <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex items-center mb-2"> <input type="range" min="0" max="100" value={progress} onChange={handleProgressChange} className="w-full h-1 bg-gray-600 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500" /> </div> <div className="flex items-center justify-between"> <div className="flex items-center space-x-4"> <button onClick={togglePlay} className="text-white"> {isPlaying ? <Pause size={20} /> : <Play size={20} />} </button> <div className="flex items-center space-x-2"> <button onClick={toggleMute} className="text-white"> {isMuted ? <VolumeX size={20} /> : <Volume2 size={20} />} </button> <input type="range" min="0" max="1" step="0.1" value={volume} onChange={handleVolumeChange} className="w-20 h-1 bg-gray-600 rounded-full appearance-none cursor-pointer [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500" /> </div> <span className="text-white text-sm"> {formatTime(currentTime)} / {formatTime(duration)} </span> </div> <button onClick={handleFullscreen} className="text-white"> <Maximize size={20} /> </button> </div> </div> </div> ) }
Server-Side Video Processing with FFmpeg
// app/api/video/process/route.js import { NextResponse } from 'next/server'; import { exec } from 'child_process'; import { writeFile } from 'fs/promises'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; export async function POST(request) { try { const formData = await request.formData(); const file = formData.get('video'); if (!file) { return NextResponse.json( { error: 'No video file provided' }, { status: 400 } ); } // Create unique filenames const id = uuidv4(); const inputFilename = `input-${id}.mp4`; const outputFilename = `output-${id}.mp4`; // Get file buffer const buffer = Buffer.from(await file.arrayBuffer()); // Define paths const inputPath = path.join(process.cwd(), 'public', 'uploads', inputFilename); const outputPath = path.join(process.cwd(), 'public', 'uploads', outputFilename); // Save the uploaded file await writeFile(inputPath, buffer); // Process the video with FFmpeg await new Promise((resolve, reject) => { // Example: Compress video and resize to 720p exec( `ffmpeg -i ${inputPath} -vf scale=-1:720 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k ${outputPath}`, (error) => { if (error) { console.error('FFmpeg error:', error); reject(error); return; } resolve(); } ); }); // Return the URL to the processed video const videoUrl = `/uploads/${outputFilename}`; return NextResponse.json({ success: true, videoUrl }); } catch (error) { console.error('Error processing video:', error); return NextResponse.json( { error: 'Failed to process video' }, { status: 500 } ); } }
Video Upload Component
"use client" import { useState, useRef } from "react" import { Upload, X, Check, Loader2 } from 'lucide-react' export default function VideoUploader() { const [dragActive, setDragActive] = useState(false) const [videoFile, setVideoFile] = useState(null) const [videoPreview, setVideoPreview] = useState("") const [uploading, setUploading] = useState(false) const [uploadProgress, setUploadProgress] = useState(0) const [uploadComplete, setUploadComplete] = useState(false) const [error, setError] = useState("") const inputRef = useRef(null) const handleDrag = (e) => { e.preventDefault() e.stopPropagation() if (e.type === "dragenter" || e.type === "dragover") { setDragActive(true) } else if (e.type === "dragleave") { setDragActive(false) } } const handleDrop = (e) => { e.preventDefault() e.stopPropagation() setDragActive(false) if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleFile(e.dataTransfer.files[0]) } } const handleChange = (e) => { e.preventDefault() if (e.target.files && e.target.files[0]) { handleFile(e.target.files[0]) } } const handleFile = (file) => { // Check if file is a video if (!file.type.match('video.*')) { setError("Please upload a video file") return } // Check file size (limit to 100MB for example) if (file.size > 100 * 1024 * 1024) { setError("File size exceeds 100MB limit") return } setError("") setVideoFile(file) // Create video preview URL const previewUrl = URL.createObjectURL(file) setVideoPreview(previewUrl) } const uploadVideo = async () => { if (!videoFile) return setUploading(true) setUploadProgress(0) try { const formData = new FormData() formData.append('video', videoFile) // Simulate upload progress const progressInterval = setInterval(() => { setUploadProgress((prev) => { if (prev >= 95) { clearInterval(progressInterval) return prev } return prev + 5 }) }, 500) const response = await fetch('/api/video/upload', { method: 'POST', body: formData, }) clearInterval(progressInterval) if (!response.ok) { throw new Error('Upload failed') } setUploadProgress(100) setUploadComplete(true) // Get the response data const data = await response.json() console.log('Upload successful:', data) } catch (err) { setError('Failed to upload video: ' + err.message) setUploading(false) } } const resetUploader = () => { setVideoFile(null) setVideoPreview("") setUploading(false) setUploadProgress(0) setUploadComplete(false) setError("") // Free memory if (videoPreview) { URL.revokeObjectURL(videoPreview) } } return ( <div className="w-full max-w-md mx-auto"> {!videoFile ? ( <div className={`relative border-2 border-dashed rounded-lg p-6 ${ dragActive ? "border-blue-500 bg-blue-50/10" : "border-gray-600" } transition-colors`} onDragEnter={handleDrag} onDragLeave={handleDrag} onDragOver={handleDrag} onDrop={handleDrop} > <input ref={inputRef} type="file" accept="video/*" onChange={handleChange} className="hidden" /> <div className="flex flex-col items-center justify-center space-y-4"> <Upload className="h-10 w-10 text-gray-400" /> <p className="text-center text-gray-300"> <span className="font-medium">Click to upload</span> or drag and drop <br /> <span className="text-gray-400">MP4, WebM, or OGG (Max 100MB)</span> </p> <button onClick={() => inputRef.current?.click()} className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" > Select Video </button> </div> </div> ) : ( <div className="border border-gray-700 rounded-lg overflow-hidden"> <div className="aspect-video bg-black"> <video src={videoPreview} className="w-full h-full object-contain" controls /> </div> <div className="p-4 space-y-4"> <div className="flex items-center justify-between"> <p className="text-sm text-gray-300 truncate" title={videoFile.name}> {videoFile.name} </p> <button onClick={resetUploader} className="text-gray-400 hover:text-gray-200" disabled={uploading} > <X size={18} /> </button> </div> {error && ( <div className="text-red-500 text-sm">{error}</div> )} {uploading && ( <div className="space-y-2"> <div className="w-full bg-gray-700 rounded-full h-2.5"> <div className="bg-blue-600 h-2.5 rounded-full" style={{ width: `${uploadProgress}%` }} ></div> </div> <p className="text-sm text-gray-400 text-right"> {uploadProgress}% </p> </div> )} <button onClick={uploadVideo} disabled={uploading || uploadComplete} className={`w-full py-2 rounded-md flex items-center justify-center ${ uploadComplete ? "bg-green-600 text-white" : "bg-blue-600 text-white hover:bg-blue-700" } transition-colors disabled:opacity-50`} > {uploadComplete ? ( <> <Check size={18} className="mr-2" /> Uploaded </> ) : uploading ? ( <> <Loader2 size={18} className="mr-2 animate-spin" /> Uploading... </> ) : ( "Upload Video" )} </button> </div> </div> )} </div> ) }
Resources
Libraries & Tools
- FFmpeg - Comprehensive multimedia framework
- fluent-ffmpeg - Node.js wrapper for FFmpeg
- Video.js - HTML5 video player framework
- Next.js + Cloudinary - Example for video hosting
- react-lottie-player - For lightweight animations