Overview
A comprehensive Video Thumbnail Extractor that can generate thumbnails from various video formats using Java libraries like FFmpeg, Xuggler, or JavaCV.
Dependencies
Maven Dependencies
<dependencies> <!-- FFmpeg wrapper --> <dependency> <groupId>net.bramp.ffmpeg</groupId> <artifactId>ffmpeg</artifactId> <version>0.7.0</version> </dependency> <!-- JavaCV (OpenCV wrapper) --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.5.7</version> </dependency> <!-- Apache Commons IO --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> <!-- Image scaling --> <dependency> <groupId>org.imgscalr</groupId> <artifactId>imgscalr-lib</artifactId> <version>4.2</version> </dependency> </dependencies>
Core Implementation
1. Thumbnail Configuration Class
import java.awt.*;
import java.io.File;
public class ThumbnailConfig {
private int width = 320;
private int height = 240;
private String format = "jpg";
private int quality = 90;
private double timeOffset = 10.0; // seconds
private boolean maintainAspectRatio = true;
private Color backgroundColor = Color.BLACK;
private ThumbnailStrategy strategy = ThumbnailStrategy.SINGLE_FRAME;
public enum ThumbnailStrategy {
SINGLE_FRAME, // Extract single frame at specific time
MULTIPLE_FRAMES, // Extract multiple frames
SCENE_DETECTION, // Use scene detection for best frame
MIDDLE_FRAME, // Extract frame from middle of video
RANDOM_FRAMES // Extract random frames
}
// Builder pattern for configuration
public static class Builder {
private final ThumbnailConfig config;
public Builder() {
config = new ThumbnailConfig();
}
public Builder width(int width) {
config.width = width;
return this;
}
public Builder height(int height) {
config.height = height;
return this;
}
public Builder size(int width, int height) {
config.width = width;
config.height = height;
return this;
}
public Builder format(String format) {
config.format = format;
return this;
}
public Builder quality(int quality) {
config.quality = Math.max(1, Math.min(100, quality));
return this;
}
public Builder timeOffset(double seconds) {
config.timeOffset = seconds;
return this;
}
public Builder maintainAspectRatio(boolean maintain) {
config.maintainAspectRatio = maintain;
return this;
}
public Builder backgroundColor(Color color) {
config.backgroundColor = color;
return this;
}
public Builder strategy(ThumbnailStrategy strategy) {
config.strategy = strategy;
return this;
}
public ThumbnailConfig build() {
return config;
}
}
// Getters
public int getWidth() { return width; }
public int getHeight() { return height; }
public String getFormat() { return format; }
public int getQuality() { return quality; }
public double getTimeOffset() { return timeOffset; }
public boolean isMaintainAspectRatio() { return maintainAspectRatio; }
public Color getBackgroundColor() { return backgroundColor; }
public ThumbnailStrategy getStrategy() { return strategy; }
public Dimension getSize() {
return new Dimension(width, height);
}
public static Builder builder() {
return new Builder();
}
}
2. Video Information Extractor
import java.io.File;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
public class VideoInfo {
private final File videoFile;
private final Duration duration;
private final double frameRate;
private final int width;
private final int height;
private final long fileSize;
private final String format;
private final String codec;
private final Map<String, String> metadata;
public VideoInfo(File videoFile, Duration duration, double frameRate,
int width, int height, long fileSize, String format,
String codec, Map<String, String> metadata) {
this.videoFile = videoFile;
this.duration = duration;
this.frameRate = frameRate;
this.width = width;
this.height = height;
this.fileSize = fileSize;
this.format = format;
this.codec = codec;
this.metadata = new HashMap<>(metadata);
}
// Getters
public File getVideoFile() { return videoFile; }
public Duration getDuration() { return duration; }
public double getFrameRate() { return frameRate; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public long getFileSize() { return fileSize; }
public String getFormat() { return format; }
public String getCodec() { return codec; }
public Map<String, String> getMetadata() { return metadata; }
public double getDurationInSeconds() {
return duration.getSeconds() + duration.getNano() / 1_000_000_000.0;
}
public String getResolution() {
return width + "x" + height;
}
public double getAspectRatio() {
return (double) width / height;
}
public long getTotalFrames() {
return (long) (getDurationInSeconds() * frameRate);
}
@Override
public String toString() {
return String.format(
"VideoInfo{file=%s, duration=%.2fs, resolution=%dx%d, fps=%.2f, size=%dMB}",
videoFile.getName(), getDurationInSeconds(), width, height,
frameRate, fileSize / (1024 * 1024)
);
}
}
3. Main Thumbnail Extractor Interface
import java.io.File;
import java.io.IOException;
import java.util.List;
public interface ThumbnailExtractor {
/**
* Extract a single thumbnail from video
*/
ThumbnailResult extractThumbnail(File videoFile, ThumbnailConfig config)
throws ThumbnailException;
/**
* Extract multiple thumbnails from video
*/
List<ThumbnailResult> extractThumbnails(File videoFile, ThumbnailConfig config, int count)
throws ThumbnailException;
/**
* Get video information
*/
VideoInfo getVideoInfo(File videoFile) throws ThumbnailException;
/**
* Check if video format is supported
*/
boolean isFormatSupported(String format);
/**
* Get supported formats
*/
List<String> getSupportedFormats();
}
4. FFmpeg-based Thumbnail Extractor
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import net.bramp.ffmpeg.probe.FFmpegStream;
import org.apache.commons.io.FileUtils;
import org.imgscalr.Scalr;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.List;
public class FFmpegThumbnailExtractor implements ThumbnailExtractor {
private final FFmpeg ffmpeg;
private final FFprobe ffprobe;
private final File tempDirectory;
private static final Set<String> SUPPORTED_FORMATS = Set.of(
"mp4", "avi", "mkv", "mov", "wmv", "flv", "webm", "m4v", "3gp", "mpg", "mpeg"
);
public FFmpegThumbnailExtractor(String ffmpegPath, String ffprobePath) throws IOException {
this.ffmpeg = new FFmpeg(ffmpegPath);
this.ffprobe = new FFprobe(ffprobePath);
this.tempDirectory = createTempDirectory();
}
public FFmpegThumbnailExtractor() throws IOException {
this("ffmpeg", "ffprobe"); // Use system PATH
}
@Override
public ThumbnailResult extractThumbnail(File videoFile, ThumbnailConfig config)
throws ThumbnailException {
validateInput(videoFile, config);
try {
VideoInfo videoInfo = getVideoInfo(videoFile);
double extractTime = calculateExtractTime(videoInfo, config);
File outputFile = createTempFile(config.getFormat());
switch (config.getStrategy()) {
case SINGLE_FRAME:
return extractSingleFrame(videoFile, outputFile, extractTime, config);
case MIDDLE_FRAME:
double middleTime = videoInfo.getDurationInSeconds() / 2;
return extractSingleFrame(videoFile, outputFile, middleTime, config);
case SCENE_DETECTION:
return extractWithSceneDetection(videoFile, outputFile, config);
default:
return extractSingleFrame(videoFile, outputFile, extractTime, config);
}
} catch (Exception e) {
throw new ThumbnailException("Failed to extract thumbnail: " + e.getMessage(), e);
}
}
@Override
public List<ThumbnailResult> extractThumbnails(File videoFile, ThumbnailConfig config, int count)
throws ThumbnailException {
validateInput(videoFile, config);
if (count <= 0) {
throw new IllegalArgumentException("Count must be positive");
}
try {
VideoInfo videoInfo = getVideoInfo(videoFile);
List<ThumbnailResult> results = new ArrayList<>();
switch (config.getStrategy()) {
case MULTIPLE_FRAMES:
results = extractMultipleFrames(videoFile, videoInfo, config, count);
break;
case RANDOM_FRAMES:
results = extractRandomFrames(videoFile, videoInfo, config, count);
break;
default:
// Fallback to multiple frames at equal intervals
results = extractMultipleFrames(videoFile, videoInfo, config, count);
}
return results;
} catch (Exception e) {
throw new ThumbnailException("Failed to extract multiple thumbnails: " + e.getMessage(), e);
}
}
@Override
public VideoInfo getVideoInfo(File videoFile) throws ThumbnailException {
try {
FFmpegProbeResult probeResult = ffprobe.probe(videoFile.getAbsolutePath());
FFmpegStream videoStream = getVideoStream(probeResult);
Map<String, String> metadata = new HashMap<>();
if (probeResult.getFormat().tags != null) {
probeResult.getFormat().tags.forEach(metadata::put);
}
return new VideoInfo(
videoFile,
Duration.ofMillis((long) (probeResult.getFormat().duration * 1000)),
videoStream.avg_frame_rate.eval(),
videoStream.width,
videoStream.height,
probeResult.getFormat().size,
probeResult.getFormat().format_name,
videoStream.codec_name,
metadata
);
} catch (Exception e) {
throw new ThumbnailException("Failed to get video info: " + e.getMessage(), e);
}
}
@Override
public boolean isFormatSupported(String format) {
return SUPPORTED_FORMATS.contains(format.toLowerCase());
}
@Override
public List<String> getSupportedFormats() {
return new ArrayList<>(SUPPORTED_FORMATS);
}
private ThumbnailResult extractSingleFrame(File videoFile, File outputFile,
double timeOffset, ThumbnailConfig config)
throws IOException {
FFmpegBuilder builder = new FFmpegBuilder()
.setInput(videoFile.getAbsolutePath())
.overrideOutputFiles(true)
.addOutput(outputFile.getAbsolutePath())
.setFormat("image2")
.addExtraArgs("-ss", String.valueOf(timeOffset))
.addExtraArgs("-vframes", "1")
.addExtraArgs("-qscale:v", "2")
.done();
FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
executor.createJob(builder).run();
// Process the extracted image
File processedFile = processImage(outputFile, config);
return new ThumbnailResult(processedFile, timeOffset, config);
}
private List<ThumbnailResult> extractMultipleFrames(File videoFile, VideoInfo videoInfo,
ThumbnailConfig config, int count)
throws IOException {
List<ThumbnailResult> results = new ArrayList<>();
double duration = videoInfo.getDurationInSeconds();
double interval = duration / (count + 1); // Avoid very beginning and end
for (int i = 1; i <= count; i++) {
double timeOffset = interval * i;
File tempOutput = createTempFile(config.getFormat());
File outputFile = new File(tempDirectory,
String.format("thumb_%d_%s", i, tempOutput.getName()));
ThumbnailResult result = extractSingleFrame(videoFile, outputFile, timeOffset, config);
results.add(result);
}
return results;
}
private List<ThumbnailResult> extractRandomFrames(File videoFile, VideoInfo videoInfo,
ThumbnailConfig config, int count)
throws IOException {
List<ThumbnailResult> results = new ArrayList<>();
double duration = videoInfo.getDurationInSeconds();
Random random = new Random();
for (int i = 0; i < count; i++) {
double timeOffset = random.nextDouble() * duration;
File tempOutput = createTempFile(config.getFormat());
File outputFile = new File(tempDirectory,
String.format("thumb_random_%d_%s", i, tempOutput.getName()));
ThumbnailResult result = extractSingleFrame(videoFile, outputFile, timeOffset, config);
results.add(result);
}
return results;
}
private ThumbnailResult extractWithSceneDetection(File videoFile, File outputFile,
ThumbnailConfig config)
throws IOException {
// Simple scene detection: extract frames at different points and choose the one with most contrast
VideoInfo videoInfo = getVideoInfo(videoFile);
double duration = videoInfo.getDurationInSeconds();
// Extract frames at 25%, 50%, 75% of video
double[] timePoints = {duration * 0.25, duration * 0.5, duration * 0.75};
List<File> tempFiles = new ArrayList<>();
List<Double> contrasts = new ArrayList<>();
for (double time : timePoints) {
File tempFile = createTempFile(config.getFormat());
extractSingleFrame(videoFile, tempFile, time,
ThumbnailConfig.builder().width(320).height(240).build());
tempFiles.add(tempFile);
double contrast = calculateImageContrast(tempFile);
contrasts.add(contrast);
}
// Find frame with highest contrast (likely most interesting)
int bestIndex = 0;
double maxContrast = contrasts.get(0);
for (int i = 1; i < contrasts.size(); i++) {
if (contrasts.get(i) > maxContrast) {
maxContrast = contrasts.get(i);
bestIndex = i;
}
}
// Process the best frame with original config
File bestFrame = tempFiles.get(bestIndex);
File processedFile = processImage(bestFrame, config);
// Cleanup temp files
for (File tempFile : tempFiles) {
if (tempFile != bestFrame) { // Keep the best frame
tempFile.delete();
}
}
return new ThumbnailResult(processedFile, timePoints[bestIndex], config);
}
private File processImage(File inputImage, ThumbnailConfig config) throws IOException {
BufferedImage original = ImageIO.read(inputImage);
if (original == null) {
throw new IOException("Failed to read extracted image");
}
BufferedImage processed;
if (config.isMaintainAspectRatio()) {
processed = resizeMaintainAspectRatio(original, config);
} else {
processed = resizeImage(original, config);
}
File outputFile = createTempFile(config.getFormat());
ImageIO.write(processed, config.getFormat(), outputFile);
// Cleanup original extracted file
if (!inputImage.equals(outputFile)) {
inputImage.delete();
}
return outputFile;
}
private BufferedImage resizeMaintainAspectRatio(BufferedImage original, ThumbnailConfig config) {
int originalWidth = original.getWidth();
int originalHeight = original.getHeight();
double aspectRatio = (double) originalWidth / originalHeight;
int newWidth, newHeight;
if ((double) config.getWidth() / config.getHeight() > aspectRatio) {
newHeight = config.getHeight();
newWidth = (int) (newHeight * aspectRatio);
} else {
newWidth = config.getWidth();
newHeight = (int) (newWidth / aspectRatio);
}
BufferedImage resized = Scalr.resize(original,
Scalr.Method.QUALITY, Scalr.Mode.FIT_EXACT, newWidth, newHeight);
// Create canvas with background color if dimensions don't match
if (newWidth != config.getWidth() || newHeight != config.getHeight()) {
BufferedImage canvas = new BufferedImage(
config.getWidth(), config.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g = canvas.createGraphics();
g.setColor(config.getBackgroundColor());
g.fillRect(0, 0, config.getWidth(), config.getHeight());
int x = (config.getWidth() - newWidth) / 2;
int y = (config.getHeight() - newHeight) / 2;
g.drawImage(resized, x, y, null);
g.dispose();
return canvas;
}
return resized;
}
private BufferedImage resizeImage(BufferedImage original, ThumbnailConfig config) {
return Scalr.resize(original, Scalr.Method.QUALITY,
Scalr.Mode.FIT_EXACT, config.getWidth(), config.getHeight());
}
private double calculateImageContrast(File imageFile) throws IOException {
BufferedImage image = ImageIO.read(imageFile);
long totalLuminance = 0;
long totalPixels = image.getWidth() * image.getHeight();
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
Color color = new Color(image.getRGB(x, y));
// Calculate relative luminance
double luminance = 0.2126 * color.getRed() +
0.7152 * color.getGreen() +
0.0722 * color.getBlue();
totalLuminance += (long) luminance;
}
}
double averageLuminance = (double) totalLuminance / totalPixels;
// Calculate variance (simplified contrast measure)
long varianceSum = 0;
for (int x = 0; x < image.getWidth(); x++) {
for (int y = 0; y < image.getHeight(); y++) {
Color color = new Color(image.getRGB(x, y));
double luminance = 0.2126 * color.getRed() +
0.7152 * color.getGreen() +
0.0722 * color.getBlue();
double diff = luminance - averageLuminance;
varianceSum += (long) (diff * diff);
}
}
return Math.sqrt((double) varianceSum / totalPixels);
}
private double calculateExtractTime(VideoInfo videoInfo, ThumbnailConfig config) {
double duration = videoInfo.getDurationInSeconds();
double requestedTime = config.getTimeOffset();
// Ensure time is within video bounds
return Math.max(0, Math.min(duration - 1, requestedTime));
}
private FFmpegStream getVideoStream(FFmpegProbeResult probeResult) throws ThumbnailException {
return probeResult.getStreams().stream()
.filter(stream -> "video".equals(stream.codec_type))
.findFirst()
.orElseThrow(() -> new ThumbnailException("No video stream found"));
}
private void validateInput(File videoFile, ThumbnailConfig config) throws ThumbnailException {
if (videoFile == null || !videoFile.exists()) {
throw new ThumbnailException("Video file does not exist: " + videoFile);
}
if (!videoFile.isFile()) {
throw new ThumbnailException("Path is not a file: " + videoFile);
}
if (!isFormatSupported(getFileExtension(videoFile))) {
throw new ThumbnailException("Unsupported video format: " + getFileExtension(videoFile));
}
if (config.getWidth() <= 0 || config.getHeight() <= 0) {
throw new ThumbnailException("Invalid thumbnail dimensions");
}
}
private String getFileExtension(File file) {
String name = file.getName();
int lastDot = name.lastIndexOf('.');
return lastDot > 0 ? name.substring(lastDot + 1).toLowerCase() : "";
}
private File createTempFile(String format) throws IOException {
return File.createTempFile("thumbnail_", "." + format, tempDirectory);
}
private File createTempDirectory() throws IOException {
Path tempDir = Files.createTempDirectory("video_thumbnails_");
return tempDir.toFile();
}
public void cleanup() {
if (tempDirectory.exists()) {
try {
FileUtils.deleteDirectory(tempDirectory);
} catch (IOException e) {
System.err.println("Warning: Failed to cleanup temp directory: " + e.getMessage());
}
}
}
@Override
protected void finalize() throws Throwable {
cleanup();
super.finalize();
}
}
5. JavaCV-based Thumbnail Extractor
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class JavaCVThumbnailExtractor implements ThumbnailExtractor {
private final Java2DFrameConverter converter;
public JavaCVThumbnailExtractor() {
this.converter = new Java2DFrameConverter();
}
@Override
public ThumbnailResult extractThumbnail(File videoFile, ThumbnailConfig config)
throws ThumbnailException {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoFile)) {
grabber.start();
double duration = grabber.getLengthInTime() / 1_000_000.0; // Convert to seconds
double extractTime = calculateExtractTime(duration, config);
// Seek to the desired time
long targetFrame = (long) (extractTime * grabber.getFrameRate());
grabber.setFrameNumber((int) targetFrame);
Frame frame = grabber.grabImage();
if (frame == null) {
throw new ThumbnailException("Could not extract frame at time: " + extractTime);
}
BufferedImage image = converter.convert(frame);
File outputFile = processAndSaveImage(image, config);
return new ThumbnailResult(outputFile, extractTime, config);
} catch (Exception e) {
throw new ThumbnailException("Failed to extract thumbnail with JavaCV: " + e.getMessage(), e);
}
}
@Override
public List<ThumbnailResult> extractThumbnails(File videoFile, ThumbnailConfig config, int count)
throws ThumbnailException {
List<ThumbnailResult> results = new ArrayList<>();
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoFile)) {
grabber.start();
double duration = grabber.getLengthInTime() / 1_000_000.0;
double interval = duration / (count + 1);
for (int i = 1; i <= count; i++) {
double timeOffset = interval * i;
long targetFrame = (long) (timeOffset * grabber.getFrameRate());
grabber.setFrameNumber((int) targetFrame);
Frame frame = grabber.grabImage();
if (frame != null) {
BufferedImage image = converter.convert(frame);
File outputFile = processAndSaveImage(image, config);
results.add(new ThumbnailResult(outputFile, timeOffset, config));
}
}
} catch (Exception e) {
throw new ThumbnailException("Failed to extract multiple thumbnails: " + e.getMessage(), e);
}
return results;
}
@Override
public VideoInfo getVideoInfo(File videoFile) throws ThumbnailException {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(videoFile)) {
grabber.start();
return new VideoInfo(
videoFile,
java.time.Duration.ofMillis((long) (grabber.getLengthInTime() / 1000)),
grabber.getFrameRate(),
grabber.getImageWidth(),
grabber.getImageHeight(),
videoFile.length(),
grabber.getFormat(),
grabber.getVideoCodecName(),
new java.util.HashMap<>()
);
} catch (Exception e) {
throw new ThumbnailException("Failed to get video info: " + e.getMessage(), e);
}
}
@Override
public boolean isFormatSupported(String format) {
// JavaCV supports most formats via FFmpeg
return true;
}
@Override
public List<String> getSupportedFormats() {
return List.of("mp4", "avi", "mkv", "mov", "wmv", "flv", "webm");
}
private File processAndSaveImage(BufferedImage image, ThumbnailConfig config) throws IOException {
BufferedImage processed = resizeImage(image, config);
File outputFile = File.createTempFile("javacv_thumb_", "." + config.getFormat());
ImageIO.write(processed, config.getFormat(), outputFile);
return outputFile;
}
private BufferedImage resizeImage(BufferedImage original, ThumbnailConfig config) {
// Simple resize implementation - in practice, use better scaling library
java.awt.Image scaled = original.getScaledInstance(
config.getWidth(), config.getHeight(), java.awt.Image.SCALE_SMOOTH);
BufferedImage result = new BufferedImage(
config.getWidth(), config.getHeight(), BufferedImage.TYPE_INT_RGB);
result.getGraphics().drawImage(scaled, 0, 0, null);
return result;
}
private double calculateExtractTime(double duration, ThumbnailConfig config) {
double requestedTime = config.getTimeOffset();
return Math.max(0, Math.min(duration - 1, requestedTime));
}
}
6. Result and Exception Classes
import java.io.File;
public class ThumbnailResult {
private final File thumbnailFile;
private final double extractTime;
private final ThumbnailConfig config;
private final long fileSize;
public ThumbnailResult(File thumbnailFile, double extractTime, ThumbnailConfig config) {
this.thumbnailFile = thumbnailFile;
this.extractTime = extractTime;
this.config = config;
this.fileSize = thumbnailFile.length();
}
// Getters
public File getThumbnailFile() { return thumbnailFile; }
public double getExtractTime() { return extractTime; }
public ThumbnailConfig getConfig() { return config; }
public long getFileSize() { return fileSize; }
public String getFormat() {
return config.getFormat();
}
public String getDimensions() {
return config.getWidth() + "x" + config.getHeight();
}
@Override
public String toString() {
return String.format(
"ThumbnailResult{file=%s, time=%.2fs, size=%s, dimensions=%s, format=%s}",
thumbnailFile.getName(), extractTime,
formatFileSize(fileSize), getDimensions(), getFormat()
);
}
private String formatFileSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
}
}
public class ThumbnailException extends Exception {
public ThumbnailException(String message) {
super(message);
}
public ThumbnailException(String message, Throwable cause) {
super(message, cause);
}
}
7. Thumbnail Generator Factory
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class ThumbnailGeneratorFactory {
public enum GeneratorType {
FFMPEG,
JAVACV,
AUTO
}
private static final Map<GeneratorType, ThumbnailExtractor> instances = new HashMap<>();
public static ThumbnailExtractor createGenerator(GeneratorType type) throws ThumbnailException {
if (instances.containsKey(type)) {
return instances.get(type);
}
try {
ThumbnailExtractor generator;
switch (type) {
case FFMPEG:
generator = new FFmpegThumbnailExtractor();
break;
case JAVACV:
generator = new JavaCVThumbnailExtractor();
break;
case AUTO:
default:
generator = createAutoGenerator();
break;
}
instances.put(type, generator);
return generator;
} catch (Exception e) {
throw new ThumbnailException("Failed to create thumbnail generator: " + e.getMessage(), e);
}
}
private static ThumbnailExtractor createAutoGenerator() {
try {
return new FFmpegThumbnailExtractor();
} catch (Exception e) {
try {
return new JavaCVThumbnailExtractor();
} catch (Exception ex) {
throw new RuntimeException("No suitable thumbnail generator found");
}
}
}
public static void cleanup() {
instances.values().forEach(generator -> {
if (generator instanceof FFmpegThumbnailExtractor) {
((FFmpegThumbnailExtractor) generator).cleanup();
}
});
instances.clear();
}
}
8. Usage Examples and Main Class
import java.io.File;
import java.util.List;
public class ThumbnailExtractorDemo {
public static void main(String[] args) {
try {
// Create thumbnail generator
ThumbnailExtractor extractor = ThumbnailGeneratorFactory.createGenerator(
ThumbnailGeneratorFactory.GeneratorType.AUTO);
// Example video file (replace with actual path)
File videoFile = new File("example.mp4");
if (!videoFile.exists()) {
System.out.println("Please provide a valid video file path");
return;
}
// Get video information
VideoInfo videoInfo = extractor.getVideoInfo(videoFile);
System.out.println("Video Info: " + videoInfo);
// Example 1: Extract single thumbnail
System.out.println("\n--- Extracting Single Thumbnail ---");
ThumbnailConfig singleConfig = ThumbnailConfig.builder()
.width(640)
.height(480)
.format("jpg")
.quality(85)
.timeOffset(30.0) // Extract at 30 seconds
.maintainAspectRatio(true)
.strategy(ThumbnailConfig.ThumbnailStrategy.SINGLE_FRAME)
.build();
ThumbnailResult singleResult = extractor.extractThumbnail(videoFile, singleConfig);
System.out.println("Single Thumbnail: " + singleResult);
// Example 2: Extract multiple thumbnails
System.out.println("\n--- Extracting Multiple Thumbnails ---");
ThumbnailConfig multiConfig = ThumbnailConfig.builder()
.width(320)
.height(240)
.format("png")
.strategy(ThumbnailConfig.ThumbnailStrategy.MULTIPLE_FRAMES)
.build();
List<ThumbnailResult> multiResults = extractor.extractThumbnails(videoFile, multiConfig, 5);
System.out.println("Extracted " + multiResults.size() + " thumbnails:");
multiResults.forEach(System.out::println);
// Example 3: Extract using scene detection
System.out.println("\n--- Extracting with Scene Detection ---");
ThumbnailConfig sceneConfig = ThumbnailConfig.builder()
.width(800)
.height(600)
.strategy(ThumbnailConfig.ThumbnailStrategy.SCENE_DETECTION)
.build();
ThumbnailResult sceneResult = extractor.extractThumbnail(videoFile, sceneConfig);
System.out.println("Scene Detection Thumbnail: " + sceneResult);
// Example 4: Batch processing
System.out.println("\n--- Batch Processing ---");
processVideoDirectory(new File("videos"), extractor);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
} finally {
ThumbnailGeneratorFactory.cleanup();
}
}
private static void processVideoDirectory(File directory, ThumbnailExtractor extractor) {
if (!directory.exists() || !directory.isDirectory()) {
System.out.println("Directory not found: " + directory);
return;
}
File[] videoFiles = directory.listFiles((dir, name) ->
name.toLowerCase().matches(".*\\.(mp4|avi|mkv|mov|wmv)$"));
if (videoFiles == null || videoFiles.length == 0) {
System.out.println("No video files found in directory");
return;
}
ThumbnailConfig batchConfig = ThumbnailConfig.builder()
.width(256)
.height(144)
.format("jpg")
.quality(75)
.build();
for (File videoFile : videoFiles) {
try {
System.out.println("Processing: " + videoFile.getName());
ThumbnailResult result = extractor.extractThumbnail(videoFile, batchConfig);
System.out.println(" → " + result.getThumbnailFile().getName());
} catch (Exception e) {
System.err.println(" Failed to process " + videoFile.getName() + ": " + e.getMessage());
}
}
}
}
Key Features
- Multiple Extraction Strategies:
- Single frame at specific time
- Multiple frames at intervals
- Scene detection for best frame
- Middle frame extraction
- Random frame extraction
- Format Support:
- MP4, AVI, MKV, MOV, WMV, FLV, WebM, and more
- Output in JPG, PNG, BMP formats
- Image Processing:
- Maintain aspect ratio
- Custom background colors
- Quality settings
- Multiple resize methods
- Error Handling:
- Comprehensive exception handling
- Input validation
- Resource cleanup
- Flexible Configuration:
- Builder pattern for easy configuration
- Multiple generator implementations
- Extensible architecture
This Video Thumbnail Extractor provides a robust solution for generating thumbnails from videos with various customization options and error handling.