Video Processing Utilities

FFmpegVideo ProcessingNext.js

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