Files
vod-system/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java
Dylan De Faoite 0f5fc76e55 ADD unified metadata validation to the MetadataService
Validation was happening in two places, in both EditService and in MetadataService doing different validations. This unifies them both into a singular method
2025-12-15 21:19:53 +00:00

182 lines
6.3 KiB
Java

package com.ddf.vodsystem.services.media;
import com.ddf.vodsystem.dto.CommandOutput;
import com.ddf.vodsystem.dto.ClipOptions;
import com.ddf.vodsystem.exceptions.FFMPEGException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
@Service
public class MetadataService {
private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
@Async("ffmpegTaskExecutor")
public Future<ClipOptions> getVideoMetadata(File file) {
logger.info("Getting metadata for file {}", file.getAbsolutePath());
List<String> command = List.of(
"ffprobe",
"-v", "quiet",
"-print_format", "json",
"-show_format", "-select_streams",
"v:0", "-show_entries", "stream=duration,width,height,r_frame_rate:format=size,duration",
"-i", file.getAbsolutePath()
);
ObjectMapper mapper = new ObjectMapper();
StringBuilder outputBuilder = new StringBuilder();
try {
CommandOutput output = CommandRunner.run(command);
for (String line : output.getOutput()) {
outputBuilder.append(line);
}
JsonNode node = mapper.readTree(outputBuilder.toString());
return CompletableFuture.completedFuture(parseVideoMetadata(node));
} catch (IOException | InterruptedException e) {
Thread.currentThread().interrupt();
throw new FFMPEGException("Error while getting video metadata: " + e);
}
}
public void validateMetadata(ClipOptions inputFileMetadata, ClipOptions outputFileMetadata) {
Float start = outputFileMetadata.getStartPoint();
Float duration = outputFileMetadata.getDuration();
Float fileSize = outputFileMetadata.getFileSize();
Integer width = outputFileMetadata.getWidth();
Integer height = outputFileMetadata.getHeight();
Float fps = outputFileMetadata.getFps();
if (start == null) {
outputFileMetadata.setStartPoint(0f);
}
if (duration == null) {
outputFileMetadata.setDuration(inputFileMetadata.getDuration());
}
if (start != null && start < 0) {
throw new IllegalArgumentException("Start point cannot be negative");
}
if (duration != null && duration < 0) {
throw new IllegalArgumentException("Duration cannot be negative");
}
if (fileSize != null && fileSize < 100) {
throw new IllegalArgumentException("File size cannot be less than 100kb");
}
if (width != null && width < 1) {
throw new IllegalArgumentException("Width cannot be less than 1");
}
if (height != null && height < 1) {
throw new IllegalArgumentException("Height cannot be less than 1");
}
if (fps != null && fps < 1) {
throw new IllegalArgumentException("FPS cannot be less than 1");
}
}
private ClipOptions parseVideoMetadata(JsonNode node) {
ClipOptions metadata = new ClipOptions();
metadata.setStartPoint(0f);
JsonNode streamNode = extractStreamNode(node);
metadata.setDuration(extractDuration(streamNode));
metadata.setWidth(getWidth(streamNode));
metadata.setHeight(getHeight(streamNode));
metadata.setFps(extractFps(streamNode));
JsonNode formatNode = extractFormatNode(node);
metadata.setFileSize(extractFileSize(formatNode));
extractEndPointFromFormat(metadata, formatNode);
return metadata;
}
private JsonNode extractStreamNode(JsonNode node) {
JsonNode streamNode = node.path("streams").get(0);
if (streamNode == null || streamNode.isMissingNode()) {
throw new FFMPEGException("ffprobe streams missing");
}
return streamNode;
}
private Float extractDuration(JsonNode streamNode) {
if (streamNode.has("duration")) {
return Float.valueOf(streamNode.get("duration").asText());
}
throw new FFMPEGException("ffprobe duration missing");
}
private Integer getWidth(JsonNode streamNode) {
if (streamNode.has("width")) {
return streamNode.get("width").asInt();
}
throw new FFMPEGException("ffprobe width missing");
}
private Integer getHeight(JsonNode streamNode) {
if (streamNode.has("height")) {
return streamNode.get("height").asInt();
}
throw new FFMPEGException("ffprobe height missing");
}
private JsonNode extractFormatNode(JsonNode node) {
JsonNode formatNode = node.path("format");
return (formatNode != null && !formatNode.isMissingNode()) ? formatNode : null;
}
private Float extractFileSize(JsonNode formatNode) {
if (formatNode != null && formatNode.has("size")) {
return Float.parseFloat(formatNode.get("size").asText());
}
throw new FFMPEGException("ffprobe file size missing");
}
private void extractEndPointFromFormat(ClipOptions metadata, JsonNode formatNode) {
if (formatNode != null && formatNode.has("duration") && metadata.getDuration() == null) {
metadata.setDuration(Float.parseFloat(formatNode.get("duration").asText()));
}
}
private Float extractFps(JsonNode streamNode) {
if (streamNode.has("r_frame_rate")) {
String fpsFraction = streamNode.get("r_frame_rate").asText();
if (fpsFraction.contains("/")) {
String[] parts = fpsFraction.split("/");
double numerator = Float.parseFloat(parts[0]);
double denominator = Float.parseFloat(parts[1]);
if (denominator != 0) {
return (float) (numerator / denominator);
}
} else {
return Float.valueOf(fpsFraction); // Handle cases like "25" directly
}
}
return null;
}
}