Adds a Letterbox cropper, and fixes cover images for the anime converter
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good

Adds a new letterbox cropper, which will use 10 samples at 30 points in a video in order to find the correct crop to remove letter-boxing.
Makes sure to always copy codec of cover images, as ffmpeg treats them as video streams.
This commit is contained in:
2024-05-25 00:15:41 +02:00
parent c3c89fcb75
commit c0c8c9c054
13 changed files with 306 additions and 64 deletions

View File

@@ -33,40 +33,6 @@ public final class FFMpegHelper {
private FFMpegHelper() {
}
/*
Developer notes:
Setting the default track:
-disposition:s:0 +default
Where s,a,v is the type specifier, and the number is the relative index
To unset default:
-disposition:s:0 -default
-map command for reordering:
First number is the index of the input file, which is 0, unless more files are given as input
Optionally, a,v,s can be used to select the type of stream to map
Lastly, the number is either the global index of the stream, or the relative, if a type has been specified
If only the first number is given, all streams from that file are selected
The output file will contain all mapped streams in the order they are mapped
Plan: Sort all streams by set criteria. Map all selected streams by looping using their relative index for
selection.
Streams should probably have an input index, so it's easier to treat extra subtitles more seamlessly. So, by
including any external subtitle files as input, there would be no need to fiddle more with storing input files.
Instead of storing the ffmpeg command as a list of strings, it should be stored as an object with different list
for input arguments and output arguments. That way, it would be much easier to add input-related arguments later
in the process.
ffmpeg -codecs:
Used for checking which codecs are available.
ffmpeg -h encoder=h264_nvenc:
Used to see available encoder presets/profiles/info
*/
/**
* Gets streams from a file
@@ -280,6 +246,29 @@ public final class FFMpegHelper {
return StringUtil.stringBetween(result.output(), "[STREAM]", "[/STREAM]");
}
/**
* Gets the duration, in seconds, of the given file
*
* @param ffprobePath <p>The path to the ffprobe executable</p>
* @param file <p>The file to get the duration of</p>
* @return <p>The duration</p>
* @throws IOException <p>If unable to probe the file</p>
* @throws NumberFormatException <p>If ffmpeg returns a non-number</p>
*/
public static double getDuration(@NotNull String ffprobePath, @NotNull File file) throws IOException, NumberFormatException {
FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath);
probeCommand.addGlobalOption("-v", "error", "-show_entries", "format=duration", "-of",
"default=noprint_wrappers=1:nokey=1");
probeCommand.addInputFile(file.toString());
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
ProcessResult result = runProcess(processBuilder, file.getParentFile(), "", false);
if (result.exitCode() != 0) {
throw new IllegalArgumentException("File probe failed with code " + result.exitCode());
}
return Double.parseDouble(result.output().trim());
}
/**
* Takes a list of all streams and parses each stream into one of three objects
*
@@ -294,7 +283,8 @@ public final class FFMpegHelper {
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams,
@NotNull File file, @NotNull List<String> subtitleFormats,
@NotNull List<String> audioFormats) throws IOException {
StreamProbeResult probeResult = new StreamProbeResult(new ArrayList<>(List.of(file)), parseStreamObjects(streams));
StreamProbeResult probeResult = new StreamProbeResult(new ArrayList<>(List.of(file)),
parseStreamObjects(streams));
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), subtitleFormats);
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), audioFormats);
return probeResult;
@@ -319,18 +309,11 @@ public final class FFMpegHelper {
StreamType streamType = getStreamType(streamInfo);
switch (streamType) {
case VIDEO:
parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
break;
case AUDIO:
parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++));
break;
case SUBTITLE:
parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++));
break;
case OTHER:
parsedStreams.add(new OtherStream(streamInfo, 0));
break;
case VIDEO -> parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
case AUDIO -> parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++));
case SUBTITLE -> parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++));
case OTHER -> parsedStreams.add(new OtherStream(streamInfo, 0, false));
case COVER_IMAGE -> parsedStreams.add(new OtherStream(streamInfo, 0, true));
}
}
return parsedStreams;
@@ -347,11 +330,13 @@ public final class FFMpegHelper {
String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
switch (codecType) {
case "video":
String mime = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_MIME_TYPE), "");
// Some attached covers are marked as video streams
if (ValueParsingHelper.parseInt(streamInfo.get(StreamTag.DISPOSITION_ATTACHED_PIC), 0) != 1) {
if (ValueParsingHelper.parseInt(streamInfo.get(StreamTag.DISPOSITION_ATTACHED_PIC), 0) != 1 &&
!mime.startsWith("image/") && !mime.endsWith("-font")) {
return StreamType.VIDEO;
} else {
return StreamType.OTHER;
return StreamType.COVER_IMAGE;
}
case "audio":
return StreamType.AUDIO;

View File

@@ -8,7 +8,7 @@ import java.util.List;
/**
* A class which helps with operations on strings
*/
final class StringUtil {
public final class StringUtil {
private StringUtil() {