Adds a Letterbox cropper, and fixes cover images for the anime converter
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
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:
parent
c3c89fcb75
commit
c0c8c9c054
@ -5,6 +5,7 @@ import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
|
|||||||
import net.knarcraft.ffmpegconverter.converter.AudioConverter;
|
import net.knarcraft.ffmpegconverter.converter.AudioConverter;
|
||||||
import net.knarcraft.ffmpegconverter.converter.Converter;
|
import net.knarcraft.ffmpegconverter.converter.Converter;
|
||||||
import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
|
import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.LetterboxCropper;
|
||||||
import net.knarcraft.ffmpegconverter.converter.MKVToMP4Transcoder;
|
import net.knarcraft.ffmpegconverter.converter.MKVToMP4Transcoder;
|
||||||
import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
|
import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
|
||||||
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
|
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
|
||||||
@ -96,7 +97,8 @@ public class FFMpegConvert {
|
|||||||
8. DownScaleConverter
|
8. DownScaleConverter
|
||||||
9. mp4 Subtitle Embed
|
9. mp4 Subtitle Embed
|
||||||
10. Anime to h265 all streams
|
10. Anime to h265 all streams
|
||||||
11. Stream reorder""", 1, 11);
|
11. Stream reorder
|
||||||
|
12. Letterbox cropper""", 1, 12);
|
||||||
|
|
||||||
return switch (choice) {
|
return switch (choice) {
|
||||||
case 1 -> generateWebAnimeConverter();
|
case 1 -> generateWebAnimeConverter();
|
||||||
@ -110,6 +112,7 @@ public class FFMpegConvert {
|
|||||||
case 9 -> new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH);
|
case 9 -> new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH);
|
||||||
case 10 -> generateAnimeConverter();
|
case 10 -> generateAnimeConverter();
|
||||||
case 11 -> generateStreamOrderConverter();
|
case 11 -> generateStreamOrderConverter();
|
||||||
|
case 12 -> new LetterboxCropper(FFPROBE_PATH, FFMPEG_PATH);
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,13 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* A class for generating and storing a ffmpeg command
|
* A class for generating and storing a ffmpeg command
|
||||||
*/
|
*/
|
||||||
public class FFMpegCommand {
|
public class FFMpegCommand implements Cloneable {
|
||||||
|
|
||||||
private final @NotNull String executable;
|
private @NotNull String executable;
|
||||||
private final @NotNull List<String> globalOptions;
|
private @NotNull List<String> globalOptions;
|
||||||
private final @NotNull List<String> inputFileOptions;
|
private @NotNull List<String> inputFileOptions;
|
||||||
private final @NotNull List<String> inputFiles;
|
private @NotNull List<String> inputFiles;
|
||||||
private final @NotNull List<String> outputFileOptions;
|
private @NotNull List<String> outputFileOptions;
|
||||||
private @Nullable String outputVideoCodec;
|
private @Nullable String outputVideoCodec;
|
||||||
private @NotNull String outputFile;
|
private @NotNull String outputFile;
|
||||||
|
|
||||||
@ -135,4 +135,21 @@ public class FFMpegCommand {
|
|||||||
return result.toArray(new String[0]);
|
return result.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FFMpegCommand clone() {
|
||||||
|
try {
|
||||||
|
FFMpegCommand clone = (FFMpegCommand) super.clone();
|
||||||
|
clone.outputVideoCodec = this.outputVideoCodec;
|
||||||
|
clone.outputFile = this.outputFile;
|
||||||
|
clone.executable = this.executable;
|
||||||
|
clone.globalOptions = new ArrayList<>(this.globalOptions);
|
||||||
|
clone.inputFileOptions = new ArrayList<>(this.inputFileOptions);
|
||||||
|
clone.inputFiles = new ArrayList<>(this.inputFiles);
|
||||||
|
clone.outputFileOptions = new ArrayList<>(this.outputFileOptions);
|
||||||
|
return clone;
|
||||||
|
} catch (CloneNotSupportedException e) {
|
||||||
|
throw new AssertionError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,6 +28,7 @@ import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleLanguageSorter;
|
|||||||
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleTitleSorter;
|
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleTitleSorter;
|
||||||
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
|
||||||
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||||
|
import net.knarcraft.ffmpegconverter.streams.OtherStream;
|
||||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||||
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
import net.knarcraft.ffmpegconverter.streams.VideoStream;
|
||||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||||
@ -164,6 +165,14 @@ public class AnimeConverter extends AbstractConverter {
|
|||||||
modules.add(new SetOutputFileModule(outFile));
|
modules.add(new SetOutputFileModule(outFile));
|
||||||
|
|
||||||
new ModuleExecutor(command, modules).execute();
|
new ModuleExecutor(command, modules).execute();
|
||||||
|
|
||||||
|
int index = videoStreams.size();
|
||||||
|
for (OtherStream stream : probeResult.getOtherStreams()) {
|
||||||
|
if (stream.isCoverImage()) {
|
||||||
|
command.addOutputFileOption("-c:v:" + index++, "copy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,197 @@
|
|||||||
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||||
|
import net.knarcraft.ffmpegconverter.container.ProcessResult;
|
||||||
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
|
||||||
|
import net.knarcraft.ffmpegconverter.streams.AudioStream;
|
||||||
|
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||||
|
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||||
|
import net.knarcraft.ffmpegconverter.utility.StringUtil;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A converter that crops letter-boxing (black padding) from video files
|
||||||
|
*/
|
||||||
|
public class LetterboxCropper extends AbstractConverter {
|
||||||
|
|
||||||
|
private final static String SPACER = "|,-,|";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new letter box cropper
|
||||||
|
*
|
||||||
|
* @param ffprobePath <p>Path/command to ffprobe</p>
|
||||||
|
* @param ffmpegPath <p>Path/command to ffmpeg</p>
|
||||||
|
*/
|
||||||
|
public LetterboxCropper(@NotNull String ffprobePath, @NotNull String ffmpegPath) {
|
||||||
|
super("mkv");
|
||||||
|
this.ffprobePath = ffprobePath;
|
||||||
|
this.ffmpegPath = ffmpegPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull List<String> getValidFormats() {
|
||||||
|
return List.of("mkv", "mp4");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
|
@NotNull String outFile) {
|
||||||
|
File inputFile = probeResult.parsedFiles().get(0);
|
||||||
|
|
||||||
|
List<ConverterModule> modules = new ArrayList<>();
|
||||||
|
|
||||||
|
Map<String, Integer> cropValues = getLetterboxCropValues(modules, inputFile);
|
||||||
|
|
||||||
|
FFMpegCommand convertCommand = getConversionCommand(cropValues, inputFile);
|
||||||
|
|
||||||
|
modules.add(new MapAllModule<>(probeResult.getVideoStreams()));
|
||||||
|
|
||||||
|
// Map audio if present
|
||||||
|
List<AudioStream> audioStreams = probeResult.getAudioStreams();
|
||||||
|
if (!audioStreams.isEmpty()) {
|
||||||
|
modules.add(new MapAllModule<>(audioStreams));
|
||||||
|
setOutputIndexes(audioStreams);
|
||||||
|
modules.add(new CopyAudioModule(audioStreams));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map subtitles if present
|
||||||
|
List<StreamObject> subtitleStreams = new ArrayList<>(probeResult.getSubtitleStreams());
|
||||||
|
if (!subtitleStreams.isEmpty()) {
|
||||||
|
modules.add(new MapAllModule<>(probeResult.getSubtitleStreams()));
|
||||||
|
setOutputIndexes(subtitleStreams);
|
||||||
|
modules.add(new CopySubtitlesModule());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map all attached streams, such as fonts and covers
|
||||||
|
modules.add(new MapAllModule<>(probeResult.getOtherStreams()));
|
||||||
|
|
||||||
|
modules.add(new SetOutputFileModule(outFile));
|
||||||
|
|
||||||
|
new ModuleExecutor(convertCommand, modules).execute();
|
||||||
|
return convertCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the initial command used for removing the letterbox from a video file
|
||||||
|
*
|
||||||
|
* @param cropValues <p>The possible crop values to be used</p>
|
||||||
|
* @param inputFile <p>The file to be converted</p>
|
||||||
|
* @return <p>The initial command to use for converting the file</p>
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private FFMpegCommand getConversionCommand(@NotNull Map<String, Integer> cropValues, @NotNull File inputFile) {
|
||||||
|
String crop = null;
|
||||||
|
int counts = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<String, Integer> entry : cropValues.entrySet()) {
|
||||||
|
if (crop == null || entry.getValue() > counts) {
|
||||||
|
crop = entry.getKey();
|
||||||
|
counts = entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crop == null) {
|
||||||
|
throw new IllegalArgumentException("Unable to detect letterbox for video");
|
||||||
|
}
|
||||||
|
|
||||||
|
FFMpegCommand convertCommand = new FFMpegCommand(ffmpegPath);
|
||||||
|
|
||||||
|
convertCommand.addInputFileOption(inputFile.getName());
|
||||||
|
convertCommand.addOutputFileOption("-vf", "crop=" + crop);
|
||||||
|
convertCommand.addOutputFileOption("-c:v", "libx265");
|
||||||
|
convertCommand.addOutputFileOption("-crf", "22");
|
||||||
|
convertCommand.addOutputFileOption("-preset", "medium");
|
||||||
|
convertCommand.addOutputFileOption("-tune", "ssim");
|
||||||
|
convertCommand.addOutputFileOption("-profile:v", "main10");
|
||||||
|
convertCommand.addOutputFileOption("-level:v", "4");
|
||||||
|
convertCommand.addOutputFileOption("-x265-params", "bframes=8:scenecut-bias=0.05:me=star:subme=5:" +
|
||||||
|
"refine-mv=1:limit-refs=1:rskip=0:max-merge=5:rc-lookahead=80:lookahead-slices=5:min-keyint=23:" +
|
||||||
|
"max-luma=1023:psy-rd=2:strong-intra-smoothing=0:b-intra=1:hist-threshold=0.03");
|
||||||
|
return convertCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a map counting each time a given letterbox crop suggestion has been encountered
|
||||||
|
*
|
||||||
|
* <p>As some parts of a video might be entirely black, or some parts of a video might be dark enough that parts of
|
||||||
|
* a scene might be detected as part of the letterbox, this takes 30 crop-detect samples evenly spread among the
|
||||||
|
* file. It can be assumed that the crop-detect value that was encountered the most times is the correct one.</p>
|
||||||
|
*
|
||||||
|
* @param modules <p>The converter modules to append to</p>
|
||||||
|
* @param inputFile <p>The input file to find letterbox of</p>
|
||||||
|
* @return <p>A map counting all returned crop-detect values</p>
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
private Map<String, Integer> getLetterboxCropValues(@NotNull List<ConverterModule> modules, @NotNull File inputFile) {
|
||||||
|
Map<String, Integer> cropValues = new HashMap<>();
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
modules.add(new DebugModule(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
FFMpegCommand probeCommand = new FFMpegCommand(ffmpegPath);
|
||||||
|
probeCommand.addGlobalOption("-nostats", "-hide_banner");
|
||||||
|
probeCommand.addInputFile(inputFile.toString());
|
||||||
|
probeCommand.addOutputFileOption("-vframes", "10");
|
||||||
|
probeCommand.addOutputFileOption("-vf", "cropdetect");
|
||||||
|
probeCommand.addOutputFileOption("-f", "null");
|
||||||
|
probeCommand.addOutputFileOption("-");
|
||||||
|
|
||||||
|
double duration;
|
||||||
|
try {
|
||||||
|
duration = FFMpegHelper.getDuration(ffprobePath, inputFile);
|
||||||
|
} catch (IOException | NumberFormatException exception) {
|
||||||
|
throw new RuntimeException("Unable to get duration from video file");
|
||||||
|
}
|
||||||
|
|
||||||
|
int increments = (int) (duration / 30d);
|
||||||
|
|
||||||
|
for (int i = 0; i < duration; i += increments) {
|
||||||
|
FFMpegCommand clone = probeCommand.clone();
|
||||||
|
clone.addInputFileOption("-ss", String.valueOf(i));
|
||||||
|
|
||||||
|
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
|
||||||
|
ProcessResult result = null;
|
||||||
|
try {
|
||||||
|
result = FFMpegHelper.runProcess(processBuilder, inputFile.getParentFile(), SPACER, false);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
if (result == null || result.exitCode() != 0) {
|
||||||
|
throw new IllegalArgumentException("File probe failed with code " + (result == null ? "null" :
|
||||||
|
result.exitCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> parsed = StringUtil.stringBetween(result.output(), "crop=", SPACER);
|
||||||
|
for (String string : parsed) {
|
||||||
|
if (string.contains("-")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cropValues.containsKey(string)) {
|
||||||
|
cropValues.put(string, cropValues.get(string) + 1);
|
||||||
|
} else {
|
||||||
|
cropValues.put(string, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cropValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -32,7 +32,7 @@ public class MkvH264Converter extends AbstractConverter {
|
|||||||
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||||
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||||
*/
|
*/
|
||||||
public MkvH264Converter(String ffprobePath, String ffmpegPath) {
|
public MkvH264Converter(@NotNull String ffprobePath, @NotNull String ffmpegPath) {
|
||||||
super("mkv");
|
super("mkv");
|
||||||
this.ffprobePath = ffprobePath;
|
this.ffprobePath = ffprobePath;
|
||||||
this.ffmpegPath = ffmpegPath;
|
this.ffmpegPath = ffmpegPath;
|
||||||
@ -76,6 +76,7 @@ public class MkvH264Converter extends AbstractConverter {
|
|||||||
List<StreamObject> subtitleStreams = new ArrayList<>(probeResult.getSubtitleStreams());
|
List<StreamObject> subtitleStreams = new ArrayList<>(probeResult.getSubtitleStreams());
|
||||||
if (!subtitleStreams.isEmpty()) {
|
if (!subtitleStreams.isEmpty()) {
|
||||||
modules.add(new MapAllModule<>(probeResult.getSubtitleStreams()));
|
modules.add(new MapAllModule<>(probeResult.getSubtitleStreams()));
|
||||||
|
setOutputIndexes(subtitleStreams);
|
||||||
modules.add(new CopySubtitlesModule());
|
modules.add(new CopySubtitlesModule());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,13 @@ public enum StreamType {
|
|||||||
*/
|
*/
|
||||||
SUBTITLE,
|
SUBTITLE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cover image
|
||||||
|
*
|
||||||
|
* <p>Cover images are treated as video streams by ffmpeg, so they need special treatment</p>
|
||||||
|
*/
|
||||||
|
COVER_IMAGE,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* None of the above
|
* None of the above
|
||||||
*/
|
*/
|
||||||
|
@ -18,7 +18,7 @@ public class AudioStream extends AbstractStream implements StreamObject {
|
|||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
* @param inputIndex <p>The index of the input file containing this stream</p>
|
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||||
* @param relativeIndex <p>The index of the audio stream relative to other audio streams.</p>
|
* @param relativeIndex <p>The index of the audio stream relative to other audio streams</p>
|
||||||
*/
|
*/
|
||||||
public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
super(streamInfo, inputIndex, relativeIndex);
|
super(streamInfo, inputIndex, relativeIndex);
|
||||||
|
@ -9,14 +9,27 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class OtherStream extends AbstractStream {
|
public class OtherStream extends AbstractStream {
|
||||||
|
|
||||||
|
private final boolean isCoverImage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new other stream
|
* Instantiates a new other stream
|
||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
* @param inputIndex <p>The index of the input file this stream belongs to</p>
|
* @param inputIndex <p>The index of the input file this stream belongs to</p>
|
||||||
|
* @param isCoverImage <p>Whether this stream is a cover image stream</p>
|
||||||
*/
|
*/
|
||||||
public OtherStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex) {
|
public OtherStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, boolean isCoverImage) {
|
||||||
super(streamInfo, inputIndex, 0);
|
super(streamInfo, inputIndex, 0);
|
||||||
|
this.isCoverImage = isCoverImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets whether this stream is a cover image stream
|
||||||
|
*
|
||||||
|
* @return <p>True if this stream is a cover image stream</p>
|
||||||
|
*/
|
||||||
|
public boolean isCoverImage() {
|
||||||
|
return isCoverImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -455,6 +455,16 @@ public enum StreamTag {
|
|||||||
* <p>Applicable for all 3 stream types</p>
|
* <p>Applicable for all 3 stream types</p>
|
||||||
*/
|
*/
|
||||||
TAG_DURATION("TAG:DURATION"),
|
TAG_DURATION("TAG:DURATION"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The file name of the attached file (image/font)
|
||||||
|
*/
|
||||||
|
TAG_FILE_NAME("TAG:filename"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mime type of the attached file (image/font)
|
||||||
|
*/
|
||||||
|
TAG_MIME_TYPE("TAG:mimetype"),
|
||||||
;
|
;
|
||||||
|
|
||||||
private static final Map<String, StreamTag> tagLookup = new HashMap<>();
|
private static final Map<String, StreamTag> tagLookup = new HashMap<>();
|
||||||
|
@ -17,7 +17,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
|||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
* @param inputIndex <p>The index of the input file containing this stream</p>
|
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||||
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams.</p>
|
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams</p>
|
||||||
*/
|
*/
|
||||||
public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
super(streamInfo, inputIndex, relativeIndex);
|
super(streamInfo, inputIndex, relativeIndex);
|
||||||
|
@ -18,7 +18,7 @@ public class VideoStream extends AbstractStream implements StreamObject {
|
|||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
* @param inputIndex <p>The index of the input file containing this stream</p>
|
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||||
* @param relativeIndex <p>The index of the video stream relative to other video streams.</p>
|
* @param relativeIndex <p>The index of the video stream relative to other video streams</p>
|
||||||
*/
|
*/
|
||||||
public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
super(streamInfo, inputIndex, relativeIndex);
|
super(streamInfo, inputIndex, relativeIndex);
|
||||||
|
@ -34,40 +34,6 @@ public final class 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
|
* Gets streams from a file
|
||||||
*
|
*
|
||||||
@ -280,6 +246,29 @@ public final class FFMpegHelper {
|
|||||||
return StringUtil.stringBetween(result.output(), "[STREAM]", "[/STREAM]");
|
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
|
* 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,
|
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams,
|
||||||
@NotNull File file, @NotNull List<String> subtitleFormats,
|
@NotNull File file, @NotNull List<String> subtitleFormats,
|
||||||
@NotNull List<String> audioFormats) throws IOException {
|
@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(), subtitleFormats);
|
||||||
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), audioFormats);
|
getExternalStreams(probeResult, ffprobePath, file.getParentFile(), file.getName(), audioFormats);
|
||||||
return probeResult;
|
return probeResult;
|
||||||
@ -319,18 +309,11 @@ public final class FFMpegHelper {
|
|||||||
StreamType streamType = getStreamType(streamInfo);
|
StreamType streamType = getStreamType(streamInfo);
|
||||||
|
|
||||||
switch (streamType) {
|
switch (streamType) {
|
||||||
case VIDEO:
|
case VIDEO -> parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
|
||||||
parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
|
case AUDIO -> parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++));
|
||||||
break;
|
case SUBTITLE -> parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++));
|
||||||
case AUDIO:
|
case OTHER -> parsedStreams.add(new OtherStream(streamInfo, 0, false));
|
||||||
parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++));
|
case COVER_IMAGE -> parsedStreams.add(new OtherStream(streamInfo, 0, true));
|
||||||
break;
|
|
||||||
case SUBTITLE:
|
|
||||||
parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++));
|
|
||||||
break;
|
|
||||||
case OTHER:
|
|
||||||
parsedStreams.add(new OtherStream(streamInfo, 0));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parsedStreams;
|
return parsedStreams;
|
||||||
@ -347,11 +330,13 @@ public final class FFMpegHelper {
|
|||||||
String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
|
String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
|
||||||
switch (codecType) {
|
switch (codecType) {
|
||||||
case "video":
|
case "video":
|
||||||
|
String mime = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_MIME_TYPE), "");
|
||||||
// Some attached covers are marked as video streams
|
// 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;
|
return StreamType.VIDEO;
|
||||||
} else {
|
} else {
|
||||||
return StreamType.OTHER;
|
return StreamType.COVER_IMAGE;
|
||||||
}
|
}
|
||||||
case "audio":
|
case "audio":
|
||||||
return StreamType.AUDIO;
|
return StreamType.AUDIO;
|
||||||
|
@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
/**
|
/**
|
||||||
* A class which helps with operations on strings
|
* A class which helps with operations on strings
|
||||||
*/
|
*/
|
||||||
final class StringUtil {
|
public final class StringUtil {
|
||||||
|
|
||||||
private StringUtil() {
|
private StringUtil() {
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user