diff --git a/src/main/java/net/knarcraft/ffmpegconverter/container/FFMpegCommand.java b/src/main/java/net/knarcraft/ffmpegconverter/container/FFMpegCommand.java new file mode 100644 index 0000000..9756682 --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/container/FFMpegCommand.java @@ -0,0 +1,139 @@ +package net.knarcraft.ffmpegconverter.container; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * A class for generating and storing a ffmpeg command + */ +public class FFMpegCommand { + + private final @NotNull String executable; + private final @NotNull List globalOptions; + private final @NotNull List inputFileOptions; + private final @NotNull List inputFiles; + private final @NotNull List outputFileOptions; + private @NotNull String outputFile; + + /** + * Instantiates a new FFMPEG command + * + * @param executable

The FFMPEG/FFPROBE executable to run

+ */ + public FFMpegCommand(@NotNull String executable) { + this.executable = executable; + this.globalOptions = new ArrayList<>(); + this.inputFileOptions = new ArrayList<>(); + this.inputFiles = new ArrayList<>(); + this.outputFileOptions = new ArrayList<>(); + this.outputFile = ""; + } + + /** + * Instantiates a new FFMPEG command + * + * @param executable

The FFMPEG/FFPROBE executable to run

+ * @param globalOptions

Options for FFMPEG itself

+ * @param inputFileOptions

Options for processing of the input files

+ * @param inputFiles

The input files to execute on

+ * @param outputFileOptions

Options for the output files

+ * @param outputFile

The output file to write to

+ */ + public FFMpegCommand(@NotNull String executable, @NotNull List globalOptions, + @NotNull List inputFileOptions, @NotNull List inputFiles, + @NotNull List outputFileOptions, @NotNull String outputFile) { + this.executable = executable; + this.globalOptions = globalOptions; + this.inputFileOptions = inputFileOptions; + this.inputFiles = inputFiles; + this.outputFileOptions = outputFileOptions; + this.outputFile = outputFile; + } + + /** + * Adds a global ffmpeg option to this command + * + * @param argument

The option(s) to add

+ */ + public void addGlobalOption(@NotNull String... argument) { + this.globalOptions.addAll(List.of(argument)); + } + + /** + * Adds an input file option to this command + * + * @param argument

The input file option(s) to add

+ */ + public void addInputFileOption(@NotNull String... argument) { + this.inputFileOptions.addAll(List.of(argument)); + } + + /** + * Adds an input file to this command + * + *

Note that this adds the "-i", so don't add that yourself!

+ * + * @param argument

The input file(s) to add

+ */ + public void addInputFile(@NotNull String... argument) { + for (String fileName : argument) { + if (fileName.isEmpty()) { + continue; + } + this.inputFiles.add(fileName); + } + } + + /** + * Gets the input files currently added to this command + * + * @return

The input files

+ */ + @NotNull + public List getInputFiles() { + return new ArrayList<>(this.inputFiles); + } + + /** + * Adds an output file option to this command + * + * @param argument

The output file option(s) to add

+ */ + public void addOutputFileOption(@NotNull String... argument) { + this.outputFileOptions.addAll(List.of(argument)); + } + + /** + * Sets the output file for this command + * + * @param argument

The path to the output file

+ */ + public void setOutputFile(@NotNull String argument) { + this.outputFile = argument; + } + + /** + * Gets the result of combining all the given input + * + * @return

The generated FFMPEG command

+ */ + @NotNull + public String[] getResult() { + List result = new ArrayList<>(); + result.add(executable); + result.addAll(globalOptions); + result.addAll(inputFileOptions); + for (String inputFile : inputFiles) { + result.add("-i"); + result.add(inputFile); + } + result.addAll(outputFileOptions); + if (!outputFile.isEmpty()) { + result.add(outputFile); + } + return result.toArray(new String[0]); + } + +} diff --git a/src/main/java/net/knarcraft/ffmpegconverter/container/StreamProbeResult.java b/src/main/java/net/knarcraft/ffmpegconverter/container/StreamProbeResult.java new file mode 100644 index 0000000..1b346b5 --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/container/StreamProbeResult.java @@ -0,0 +1,16 @@ +package net.knarcraft.ffmpegconverter.container; + +import net.knarcraft.ffmpegconverter.streams.StreamObject; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.List; + +/** + * A record for storing the result of probing for streams + * + * @param parsedFiles

The files that were parsed to get the attached streams

+ * @param parsedStreams

The streams that were parsed from the files

+ */ +public record StreamProbeResult(@NotNull List parsedFiles, @NotNull List parsedStreams) { +} diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java index 0e3dd76..bdbb95d 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java @@ -1,5 +1,6 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; @@ -122,19 +123,20 @@ public abstract class AbstractConverter implements Converter { * @param file

The file to process.

* @throws IOException

If the BufferedReader fails.

*/ - private void processFile(File folder, File file) throws IOException { - List streams = FFMpegHelper.probeFile(ffprobePath, file); - if (streams.isEmpty()) { + private void processFile(@NotNull File folder, @NotNull File file) throws IOException { + StreamProbeResult probeResult = FFMpegHelper.probeFile(ffprobePath, file); + if (probeResult.parsedStreams().isEmpty()) { throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" + " is not corrupt."); } + String outExtension = newExtension != null ? newExtension : FileUtil.getExtension(file.getName()); String newPath = FileUtil.getNonCollidingPath(folder, file, outExtension); OutputUtil.println(); OutputUtil.println("Preparing to start process..."); OutputUtil.println("Converting " + file); - String[] command = generateConversionCommand(ffmpegPath, file, streams, newPath); + String[] command = generateConversionCommand(ffmpegPath, probeResult, newPath); // If no commands were given, no conversion is necessary if (command.length == 0) { return; diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/AnimeConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/AnimeConverter.java index ef16a0f..e166ead 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/AnimeConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/AnimeConverter.java @@ -1,12 +1,14 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.ArrayList; import java.util.List; @@ -51,8 +53,10 @@ public class AnimeConverter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { - List command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } @@ -71,13 +75,16 @@ public class AnimeConverter extends AbstractConverter { //Get the first video stream VideoStream videoStream = getNthVideoStream(streams, 0); + if (videoStream == null) { + throw new IllegalArgumentException("The selected video stream does not exist"); + } //Add streams to output file FFMpegHelper.addAudioStream(command, audioStream, this.toStereo); - FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file); + FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream); - command.add(outFile); - return command.toArray(new String[0]); + command.setOutputFile(outFile); + return command.getResult(); } @Override diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/AudioConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/AudioConverter.java index 4ccab47..ed441c1 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/AudioConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/AudioConverter.java @@ -1,10 +1,12 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.List; /** @@ -26,8 +28,10 @@ public class AudioConverter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { - List command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } @@ -35,9 +39,9 @@ public class AudioConverter extends AbstractConverter { //Gets the first audio stream from the file and adds it to the output file AudioStream audioStream = getNthAudioSteam(streams, 0); FFMpegHelper.addAudioStream(command, audioStream, false); - command.add(outFile); + command.setOutputFile(outFile); - return command.toArray(new String[0]); + return command.getResult(); } @Override diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java index 0607790..ec35467 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/Converter.java @@ -1,10 +1,10 @@ package net.knarcraft.ffmpegconverter.converter; -import net.knarcraft.ffmpegconverter.streams.StreamObject; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; +import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; -import java.util.List; /** * This interface describes a file converter @@ -16,6 +16,7 @@ public interface Converter { * * @return

A list of valid input formats

*/ + @NotNull String[] getValidFormats(); /** @@ -24,17 +25,18 @@ public interface Converter { * @param file

The file to convert.

* @throws IOException

If the file cannot be converted.

*/ - void convert(File file) throws IOException; + void convert(@NotNull File file) throws IOException; /** * Generates a command for a ProcessBuilder. * - * @param executable

The executable file for ffmpeg.

- * @param file

The input file.

- * @param streams

A list of ffprobe streams.

- * @param outFile

The output file.

+ * @param executable

The executable file for ffmpeg

+ * @param probeResult

The result of probing the input file

+ * @param outFile

The output file

* @return

A list of commands

*/ - String[] generateConversionCommand(String executable, File file, List streams, String outFile); + @NotNull + String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile); } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/DownScaleConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/DownScaleConverter.java index cea1490..950366c 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/DownScaleConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/DownScaleConverter.java @@ -1,10 +1,12 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.List; /** @@ -32,33 +34,28 @@ public class DownScaleConverter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + List streams = probeResult.parsedStreams(); VideoStream videoStream = getNthVideoStream(streams, 0); if (videoStream == null || (videoStream.getWidth() <= newWidth && videoStream.getHeight() <= newHeight)) { return new String[0]; } - List command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } //Add all streams without re-encoding - command.add("-map"); - command.add("0"); - command.add("-c:a"); - command.add("copy"); - command.add("-c:s"); - command.add("copy"); - command.add("-vf"); - command.add("scale=" + newWidth + ":" + newHeight); - command.add("-crf"); - command.add("20"); - command.add("-preset"); - command.add("slow"); - - command.add(outFile); - return command.toArray(new String[0]); + FFMpegHelper.mapAllStreams(command, streams); + command.addOutputFileOption("-c:a", "copy"); + command.addOutputFileOption("-c:s", "copy"); + command.addOutputFileOption("-vf", "scale=" + newWidth + ":" + newHeight); + command.addOutputFileOption("-crf", "20"); + command.addOutputFileOption("-preset", "slow"); + command.setOutputFile(outFile); + return command.getResult(); } @Override diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/MKVToMP4Transcoder.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/MKVToMP4Transcoder.java index 7241e68..762770f 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/MKVToMP4Transcoder.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/MKVToMP4Transcoder.java @@ -1,12 +1,14 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.ArrayList; import java.util.List; @@ -44,8 +46,10 @@ public class MKVToMP4Transcoder extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { - List command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } @@ -61,21 +65,19 @@ public class MKVToMP4Transcoder extends AbstractConverter { //Get the nth video stream VideoStream videoStream = getNthVideoStream(streams, Math.max(this.videoStreamIndex, 0)); + if (videoStream == null) { + throw new IllegalArgumentException("The selected video stream was not found"); + } // Copy stream info - command.add("-map_metadata"); - command.add("0"); - command.add("-movflags"); - command.add("use_metadata_tags"); - command.add("-c"); - command.add("copy"); + command.addOutputFileOption("-c", "copy"); //Add streams to output file FFMpegHelper.addAudioStream(command, audioStream, false); - FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file); + FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream); - command.add(outFile); - return command.toArray(new String[0]); + command.setOutputFile(outFile); + return command.getResult(); } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH264Converter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH264Converter.java index 0c30ed9..ebb183e 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH264Converter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH264Converter.java @@ -1,13 +1,14 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; -import java.util.ArrayList; import java.util.List; /** @@ -33,73 +34,50 @@ public class MkvH264Converter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, - String outFile) { - List command = new ArrayList<>(); - command.add(executable); - command.add("-hwaccel"); - command.add("cuda"); - command.add("-hwaccel_output_format"); - command.add("cuda"); - command.add("-i"); - command.add(file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); + if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } // Map video if present - if (!filterStreamsByType(streams, VideoStream.class).isEmpty()) { - command.add("-map"); - command.add("0:v"); - command.add("-crf"); - command.add("28"); - command.add("-codec:v"); - command.add("h264_nvenc"); - command.add("-preset"); - command.add("slow"); - command.add("-movflags"); - command.add("+faststart"); + List videoStreams = filterStreamsByType(streams, VideoStream.class); + if (!videoStreams.isEmpty()) { + for (StreamObject streamObject : videoStreams) { + if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) { + continue; + } + + FFMpegHelper.addH26xHardwareDecoding(command); + break; + } + + FFMpegHelper.mapAllStreams(command, videoStreams); + FFMpegHelper.addH264HardwareEncoding(command, 17); + command.addOutputFileOption("-movflags", "+faststart"); } // Map audio if present - if (!filterStreamsByType(streams, AudioStream.class).isEmpty()) { - command.add("-map"); - command.add("0:a"); - command.add("-c:a"); - command.add("copy"); + List audioStreams = filterStreamsByType(streams, AudioStream.class); + if (!audioStreams.isEmpty()) { + FFMpegHelper.mapAllStreams(command, audioStreams); + command.addOutputFileOption("-c:a", "copy"); } // Map subtitles if present - if (hasInternalStreams(streams)) { - command.add("-map"); - command.add("0:s"); - command.add("-c:s"); - command.add("copy"); + List subtitleStreams = filterStreamsByType(streams, SubtitleStream.class); + if (!subtitleStreams.isEmpty()) { + FFMpegHelper.mapAllStreams(command, subtitleStreams); + command.addOutputFileOption("-c:s", "copy"); } - command.add("-map_metadata"); - command.add("0"); - command.add("-movflags"); - command.add("use_metadata_tags"); + command.addOutputFileOption("-f", "matroska"); - command.add(outFile); - return command.toArray(new String[0]); - } - - /** - * Checks whether the processed file has any internal subtitle streams - * - * @param streams

All parsed streams for the video file

- * @return

True if the file has at least one internal subtitle stream

- */ - private boolean hasInternalStreams(List streams) { - for (StreamObject subtitleStream : filterStreamsByType(streams, SubtitleStream.class)) { - if (((SubtitleStream) subtitleStream).isInternalSubtitle()) { - return true; - } - } - - return false; + command.setOutputFile(outFile); + return command.getResult(); } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH265ReducedConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH265ReducedConverter.java index ac470c2..a8edbc2 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH265ReducedConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/MkvH265ReducedConverter.java @@ -1,13 +1,14 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; -import java.util.ArrayList; import java.util.List; /** @@ -33,76 +34,49 @@ public class MkvH265ReducedConverter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, - String outFile) { - List command = new ArrayList<>(); - command.add(executable); - command.add("-hwaccel"); - command.add("cuda"); - command.add("-hwaccel_output_format"); - command.add("cuda"); - command.add("-i"); - command.add(file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } // Map video if present - if (!filterStreamsByType(streams, VideoStream.class).isEmpty()) { - command.add("-map"); - command.add("0:v"); - command.add("-codec:v"); - command.add("hevc_nvenc"); - command.add("-crf"); - command.add("28"); - command.add("-preset"); - command.add("slow"); - command.add("-tag:v"); - command.add("hvc1"); - command.add("-movflags"); - command.add("+faststart"); + List videoStreams = filterStreamsByType(streams, VideoStream.class); + if (!videoStreams.isEmpty()) { + for (StreamObject streamObject : videoStreams) { + if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) { + continue; + } + + FFMpegHelper.addH26xHardwareDecoding(command); + break; + } + + FFMpegHelper.mapAllStreams(command, videoStreams); + FFMpegHelper.addH265HardwareEncoding(command, 17); + command.addOutputFileOption("-movflags", "+faststart"); } // Map audio if present - if (!filterStreamsByType(streams, AudioStream.class).isEmpty()) { - command.add("-map"); - command.add("0:a"); + List audioStreams = filterStreamsByType(streams, AudioStream.class); + if (!audioStreams.isEmpty()) { + FFMpegHelper.mapAllStreams(command, audioStreams); } // Map subtitles if present - if (hasInternalStreams(streams)) { - command.add("-map"); - command.add("0:s"); - command.add("-c:s"); - command.add("copy"); + List subtitleStreams = filterStreamsByType(streams, SubtitleStream.class); + if (!subtitleStreams.isEmpty()) { + FFMpegHelper.mapAllStreams(command, subtitleStreams); + command.addOutputFileOption("-c:s", "copy"); } - command.add("-map_metadata"); - command.add("0"); - command.add("-movflags"); - command.add("use_metadata_tags"); - command.add("-f"); - command.add("matroska"); + command.addOutputFileOption("-f", "matroska"); - command.add(outFile); - return command.toArray(new String[0]); - } - - /** - * Checks whether the processed file has any internal subtitle streams - * - * @param streams

All parsed streams for the video file

- * @return

True if the file has at least one internal subtitle stream

- */ - private boolean hasInternalStreams(List streams) { - for (StreamObject subtitleStream : filterStreamsByType(streams, SubtitleStream.class)) { - if (((SubtitleStream) subtitleStream).isInternalSubtitle()) { - return true; - } - } - - return false; + command.setOutputFile(outFile); + return command.getResult(); } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java index 79281ed..79c38a7 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/SubtitleEmbed.java @@ -1,11 +1,11 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.StreamObject; -import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; -import java.util.ArrayList; import java.util.List; /** @@ -31,44 +31,22 @@ public class SubtitleEmbed extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { - List command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); - - List subtitleStreams = filterStreamsByType(streams, SubtitleStream.class); - List externalSubtitles = new ArrayList<>(); - for (SubtitleStream subtitleStream : subtitleStreams) { - if (!subtitleStream.isInternalSubtitle()) { - externalSubtitles.add(subtitleStream); - } - } - - if (externalSubtitles.isEmpty()) { - System.err.println("No external subtitles found for " + file.getName()); - } - - for (SubtitleStream subtitleStream : externalSubtitles) { - command.add("-i"); - command.add(subtitleStream.getFile()); - } - - command.add("-c"); - command.add("copy"); - command.add("-c:s"); - command.add("mov_text"); - - int i = 0; - for (SubtitleStream subtitleStream : subtitleStreams) { - command.add("-metadata:s:s:" + i); - command.add("language=" + subtitleStream.getLanguage()); - i++; - } + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); + + FFMpegHelper.mapAllStreams(command, streams); + command.addOutputFileOption("-c:a", "copy"); + command.addOutputFileOption("-c:v", "copy"); + command.addOutputFileOption("-c:s", "mov_text"); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } - command.add(outFile); - return command.toArray(new String[0]); + command.setOutputFile(outFile); + return command.getResult(); } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/VideoConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/VideoConverter.java index 64aca97..c898ba8 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/VideoConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/VideoConverter.java @@ -1,9 +1,11 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.List; /** @@ -25,20 +27,20 @@ public class VideoConverter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { - List command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } //Add all streams without re-encoding - command.add("-map"); - command.add("0"); - command.add("-c"); - command.add("copy"); + FFMpegHelper.mapAllStreams(command, streams); + command.addOutputFileOption("-c", "copy"); - command.add(outFile); - return command.toArray(new String[0]); + command.setOutputFile(outFile); + return command.getResult(); } @Override diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/WebVideoConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/WebVideoConverter.java index e9905f0..d36a969 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/converter/WebVideoConverter.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/WebVideoConverter.java @@ -1,12 +1,14 @@ package net.knarcraft.ffmpegconverter.converter; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; +import org.jetbrains.annotations.NotNull; -import java.io.File; import java.util.List; /** @@ -33,8 +35,10 @@ public class WebVideoConverter extends AbstractConverter { } @Override - public String[] generateConversionCommand(String executable, File file, List streams, String outFile) { - List command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName()); + public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, + @NotNull String outFile) { + FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles()); + List streams = probeResult.parsedStreams(); if (this.debug) { FFMpegHelper.addDebugArguments(command, 50, 120); } @@ -44,13 +48,17 @@ public class WebVideoConverter extends AbstractConverter { VideoStream videoStream = getNthVideoStream(streams, 0); AudioStream audioStream = getNthAudioSteam(streams, 0); + if (videoStream == null) { + throw new IllegalArgumentException("The selected video stream does not exist."); + } + //Add streams to output - FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file); + FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream); if (audioStream != null) { FFMpegHelper.addAudioStream(command, audioStream, true); } - command.add(outFile); - return command.toArray(new String[0]); + command.setOutputFile(outFile); + return command.getResult(); } } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java index 335f30c..abf94f5 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/AbstractStream.java @@ -10,10 +10,11 @@ import java.util.Map; */ public abstract class AbstractStream implements StreamObject { + protected final int inputIndex; protected final int absoluteIndex; protected final int relativeIndex; protected final String codecName; - protected String language; + protected final String language; protected final boolean isDefault; protected final String title; @@ -21,14 +22,16 @@ public abstract class AbstractStream implements StreamObject { * Instantiates a new abstract stream * * @param streamInfo

All info about the stream

+ * @param inputIndex

The index of the input file this stream belongs to

* @param relativeIndex

The relative index of this stream, only considering streams of the same type

*/ - protected AbstractStream(@NotNull Map streamInfo, int relativeIndex) { + protected AbstractStream(@NotNull Map streamInfo, int inputIndex, int relativeIndex) { this.codecName = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_NAME), ""); this.absoluteIndex = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.INDEX), -1); this.language = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und"); this.isDefault = ValueParsingHelper.parseBoolean(streamInfo.get(StreamTag.DISPOSITION_DEFAULT), false); this.title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), ""); + this.inputIndex = inputIndex; this.relativeIndex = relativeIndex; } @@ -58,8 +61,14 @@ public abstract class AbstractStream implements StreamObject { } @Override + @NotNull public String getTitle() { return this.title; } + @Override + public int getInputIndex() { + return this.inputIndex; + } + } \ No newline at end of file diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java index 3580533..6f857ae 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/AudioStream.java @@ -16,10 +16,11 @@ public class AudioStream extends AbstractStream implements StreamObject { * Instantiates a new audio stream * * @param streamInfo

All info about the stream

+ * @param inputIndex

The index of the input file containing this stream

* @param relativeIndex

The index of the audio stream relative to other audio streams.

*/ - public AudioStream(@NotNull Map streamInfo, int relativeIndex) { - super(streamInfo, relativeIndex); + public AudioStream(@NotNull Map streamInfo, int inputIndex, int relativeIndex) { + super(streamInfo, inputIndex, relativeIndex); this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0); } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java index 5e79cd0..0c7d9c9 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/StreamObject.java @@ -22,6 +22,15 @@ public interface StreamObject { */ int getAbsoluteIndex(); + /** + * Gets the index of the input file this stream belongs to + * + *

This is the first number given to a map argument in order to map this stream to the output file.

+ * + * @return

The input index of this stream

+ */ + int getInputIndex(); + /** * Gets the relative index of a stream object (kth element of codec type) * @@ -57,6 +66,7 @@ public interface StreamObject { * * @return

The title of the subtitle stream.

*/ + @NotNull String getTitle(); } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java index 705f4c4..d33d654 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/SubtitleStream.java @@ -1,6 +1,5 @@ package net.knarcraft.ffmpegconverter.streams; -import net.knarcraft.ffmpegconverter.utility.FileUtil; import org.jetbrains.annotations.NotNull; import java.util.Map; @@ -10,51 +9,21 @@ import java.util.Map; */ public class SubtitleStream extends AbstractStream implements StreamObject { - final private String file; final private boolean isFullSubtitle; final private boolean isImageSubtitle; - final private boolean isInternalStream; /** * Instantiates a new subtitle stream * - * @param streamInfo

All info about the stream

- * @param relativeIndex

The index of the subtitle stream relative to other subtitle streams.

- * @param file

The file containing the subtitle.

- * @param isInternalStream

Whether this subtitle stream is in the video file itself

+ * @param streamInfo

All info about the stream

+ * @param inputIndex

The index of the input file containing this stream

+ * @param relativeIndex

The index of the subtitle stream relative to other subtitle streams.

*/ - public SubtitleStream(@NotNull Map streamInfo, int relativeIndex, - @NotNull String file, boolean isInternalStream) { - super(streamInfo, relativeIndex); + public SubtitleStream(@NotNull Map streamInfo, int inputIndex, int relativeIndex) { + super(streamInfo, inputIndex, relativeIndex); this.isFullSubtitle = isFullSubtitle(); - this.isImageSubtitle = isImageSubtitle(); - this.isInternalStream = isInternalStream; - this.file = file; - - if (this.language == null || this.language.isEmpty()) { - String possibleLanguage = FileUtil.getLanguage(file); - if (possibleLanguage != null) { - this.language = possibleLanguage; - } - } - } - - /** - * Gets the file name of the file containing this subtitle - * - * @return

The file name containing the subtitle stream.

- */ - public String getFile() { - return this.file; - } - - /** - * Gets whether this subtitle stream is an internal stream, not an external one - * - * @return

True if this stream is an internal stream

- */ - public boolean isInternalSubtitle() { - return this.isInternalStream; + this.isImageSubtitle = codecName != null && + (getCodecName().equals("hdmv_pgs_subtitle") || getCodecName().equals("dvd_subtitle")); } /** @@ -75,25 +44,12 @@ public class SubtitleStream extends AbstractStream implements StreamObject { return this.isFullSubtitle; } - /** - * Checks whether a subtitle is image based (as opposed to text based) - * - * @return

True if the subtitle is image based.

- */ - private boolean isImageSubtitle() { - return codecName != null && (getCodecName().equals("hdmv_pgs_subtitle") - || getCodecName().equals("dvd_subtitle")); - } - /** * Checks whether the subtitle translates everything (as opposed to just songs and signs) * * @return

True if the subtitle translates everything.

*/ private boolean isFullSubtitle() { - if (getTitle() == null) { - return false; - } String titleLowercase = getTitle().toLowerCase(); return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z]+songs?.*") && !titleLowercase.matches(".*songs?[ &/a-z]+si(gn|ng)s?.*") && @@ -101,7 +57,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject { !titleLowercase.matches(".*s&s.*") && !titleLowercase.matches("signs?") && !titleLowercase.matches("songs?") && - !titleLowercase.contains("signs only"); + !titleLowercase.matches(".*signs only.*"); } @Override diff --git a/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java b/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java index 24740a7..625a198 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/streams/VideoStream.java @@ -17,10 +17,11 @@ public class VideoStream extends AbstractStream implements StreamObject { * Instantiates a new video stream * * @param streamInfo

All info about the stream

+ * @param inputIndex

The index of the input file containing this stream

* @param relativeIndex

The index of the video stream relative to other video streams.

*/ - public VideoStream(@NotNull Map streamInfo, int relativeIndex) { - super(streamInfo, relativeIndex); + public VideoStream(@NotNull Map streamInfo, int inputIndex, int relativeIndex) { + super(streamInfo, inputIndex, relativeIndex); this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1); this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1); } diff --git a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java index b8afd89..ed373c2 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java @@ -1,5 +1,7 @@ package net.knarcraft.ffmpegconverter.utility; +import net.knarcraft.ffmpegconverter.container.FFMpegCommand; +import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamTag; @@ -48,6 +50,13 @@ public final class FFMpegHelper { 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. */ /** @@ -58,71 +67,73 @@ public final class FFMpegHelper { * @return

A list of StreamObjects.

* @throws IOException

If the process can't be readProcess.

*/ - public static List probeFile(String ffprobePath, File file) throws IOException { + @NotNull + public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file) throws IOException { return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file); } /** * Creates a list containing all required arguments for converting a video to a web playable video * - * @param executable

The executable to use (ffmpeg/ffprobe).

- * @param fileName

The name of the file to execute on.

- * @return

A base list of ffmpeg commands for converting a video for web

+ * @param executable

The executable to use (ffmpeg/ffprobe)

+ * @param files

The files to execute on

+ * @return

A FFMPEG command for web-playable video

*/ - public static List getFFMpegWebVideoCommand(String executable, String fileName) { - List command = getFFMpegGeneralFileCommand(executable, fileName); - command.add("-vcodec"); - command.add("h264"); - command.add("-pix_fmt"); - command.add("yuv420p"); - command.add("-ar"); - command.add("48000"); - command.add("-movflags"); - command.add("+faststart"); + @NotNull + public static FFMpegCommand getFFMpegWebVideoCommand(@NotNull String executable, @NotNull List files) { + FFMpegCommand command = getFFMpegGeneralFileCommand(executable, files); + command.addOutputFileOption("-vcodec", "h264"); + command.addOutputFileOption("-pix_fmt", "yuv420p"); + command.addOutputFileOption("-ar", "48000"); + command.addOutputFileOption("-movflags", "+faststart"); + command.addOutputFileOption("-map_metadata", "0"); + command.addOutputFileOption("-movflags", "+use_metadata_tags"); return command; } /** * Creates a list containing command line arguments for a general file * - * @param executable

The executable to use (ffmpeg/ffprobe).

- * @param fileName

The name of the file to execute on.

- * @return

A base list of ffmpeg commands for converting a file.

+ * @param executable

The executable to use (ffmpeg/ffprobe)

+ * @param files

The files to execute on

+ * @return

A basic FFMPEG command

*/ - public static List getFFMpegGeneralFileCommand(String executable, String fileName) { - List command = new ArrayList<>(); - command.add(executable); - command.add("-nostdin"); - command.add("-i"); - command.add(fileName); + @NotNull + public static FFMpegCommand getFFMpegGeneralFileCommand(@NotNull String executable, @NotNull List files) { + FFMpegCommand command = new FFMpegCommand(executable); + command.addGlobalOption("-nostdin"); + for (File file : files) { + command.addInputFile(file.getName()); + } + command.addOutputFileOption("-map_metadata", "0"); + command.addOutputFileOption("-movflags", "+use_metadata_tags"); return command; } /** * Adds debugging parameters for only converting parts of a file * - * @param command

The list containing the command to run.

- * @param start

The offset before converting.

- * @param length

The offset for stopping the conversion.

+ * @param command

The command to add to

+ * @param start

The time to start at

+ * @param duration

The duration of video to output

*/ - public static void addDebugArguments(List command, int start, int length) { - command.add("-ss"); - command.add("" + start); - command.add("-t"); - command.add("" + length); + public static void addDebugArguments(@NotNull FFMpegCommand command, int start, int duration) { + command.addInputFileOption("-ss", String.valueOf(start)); + command.addOutputFileOption("-t", String.valueOf(duration)); } /** * Starts and prints output of a process * - * @param processBuilder

The process to run.

- * @param folder

The folder the process should run in.

- * @param spacer

The character(s) to use between each new line read.

- * @param write

Whether to write the output directly instead of storing it.

- * @throws IOException

If the process can't be readProcess.

+ * @param processBuilder

The process to run

+ * @param folder

The folder the process should run in

+ * @param spacer

The character(s) to use between each new line read

+ * @param write

Whether to write the output directly instead of storing it

+ * @throws IOException

If the process can't be readProcess

*/ - public static String runProcess(ProcessBuilder processBuilder, File folder, String spacer, boolean write) - throws IOException { + @NotNull + public static String runProcess(@NotNull ProcessBuilder processBuilder, @NotNull File folder, + @NotNull String spacer, boolean write) throws IOException { //Give the user information about what's about to happen OutputUtil.print("Command to be run: "); OutputUtil.println(processBuilder.command().toString()); @@ -136,13 +147,15 @@ public final class FFMpegHelper { StringBuilder output = new StringBuilder(); while (process.isAlive()) { String read = readProcess(processReader, spacer); - if (!read.isEmpty()) { - if (write) { - OutputUtil.println(read); - } else { - OutputUtil.printDebug(read); - output.append(read); - } + if (read.isEmpty()) { + continue; + } + + if (write) { + OutputUtil.println(read); + } else { + OutputUtil.printDebug(read); + output.append(read); } } OutputUtil.println("Process finished."); @@ -150,21 +163,16 @@ public final class FFMpegHelper { } /** - * Adds audio to a command + * Maps an audio track to a ffmpeg command's output * - * @param command

The command to add audio to.

- * @param audioStream

The audio stream to be added.

- * @param toStereo

Whether to convert the audio stream to stereo.

+ * @param command

The command to add the audio track to

+ * @param audioStream

The audio stream to be mapped

+ * @param toStereo

Whether to convert the audio stream to stereo

*/ - public static void addAudioStream(List command, AudioStream audioStream, boolean toStereo) { - if (audioStream != null) { - command.add("-map"); - command.add("0:" + audioStream.getAbsoluteIndex()); - if (toStereo && audioStream.getChannels() > 2) { - command.add("-af"); - command.add("pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR"); - //command.add("pan=stereo|FL < 1.0*FL + 0.707*FC + 0.707*BL|FR < 1.0*FR + 0.707*FC + 0.707*BR"); - } + public static void addAudioStream(@NotNull FFMpegCommand command, @NotNull AudioStream audioStream, boolean toStereo) { + mapStream(command, audioStream); + if (toStereo && audioStream.getChannels() > 2) { + command.addOutputFileOption("-af", "pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR"); } } @@ -174,37 +182,23 @@ public final class FFMpegHelper { * @param command

The list containing the rest of the command.

* @param subtitleStream

The subtitle stream to be used.

* @param videoStream

The video stream to be used.

- * @param file

The file to convert.

*/ - public static void addSubtitleAndVideoStream(List command, SubtitleStream subtitleStream, - VideoStream videoStream, File file) { + public static void addSubtitleAndVideoStream(@NotNull FFMpegCommand command, @Nullable SubtitleStream subtitleStream, + @NotNull VideoStream videoStream) { //No appropriate subtitle was found. Just add the video stream. if (subtitleStream == null) { - addVideoStream(command, videoStream); + mapStream(command, videoStream); return; } //Add the correct command arguments depending on the subtitle type if (!subtitleStream.getIsImageSubtitle()) { - addSubtitle(command, subtitleStream, videoStream); - } else if (file.getName().equals(subtitleStream.getFile())) { - addInternalImageSubtitle(command, subtitleStream, videoStream); + addBurnedInSubtitle(command, subtitleStream, videoStream); } else { - addExternalImageSubtitle(command, subtitleStream, videoStream); + addBurnedInImageSubtitle(command, subtitleStream, videoStream); } } - /** - * Adds video mapping to a command - * - * @param command

The list containing the rest of the command.

- * @param videoStream

The video stream to be used.

- */ - private static void addVideoStream(List command, VideoStream videoStream) { - command.add("-map"); - command.add(String.format("0:%d", videoStream.getAbsoluteIndex())); - } - /** * Adds subtitle commands to a command list * @@ -212,14 +206,75 @@ public final class FFMpegHelper { * @param subtitleStream

The subtitle stream to add.

* @param videoStream

The video stream to burn the subtitle into.

*/ - private static void addSubtitle(List command, SubtitleStream subtitleStream, VideoStream videoStream) { - command.add("-map"); - command.add(String.format("0:%d", videoStream.getAbsoluteIndex())); - command.add("-vf"); - String safeFileName = escapeSpecialCharactersInFileName(subtitleStream.getFile()); + private static void addBurnedInSubtitle(@NotNull FFMpegCommand command, @NotNull SubtitleStream subtitleStream, + @NotNull VideoStream videoStream) { + mapStream(command, videoStream); + + String safeFileName = escapeSpecialCharactersInFileName( + command.getInputFiles().get(subtitleStream.getInputIndex())); String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName, subtitleStream.getRelativeIndex()); - command.add(subtitleCommand); + command.addOutputFileOption("-vf", subtitleCommand); + } + + /** + * Adds arguments for converting a file to h264 using hardware acceleration + * + * @param command

The command to add the arguments to

+ * @param quality

The quality to encode. 0 = best, 51 = worst.

+ */ + public static void addH264HardwareEncoding(@NotNull FFMpegCommand command, int quality) { + command.addOutputFileOption("-codec:v", "h264_nvenc"); + command.addOutputFileOption("-profile", "high"); + command.addOutputFileOption("-preset", "p7"); + command.addOutputFileOption("-crf", String.valueOf(quality)); + } + + /** + * Adds arguments for converting a file to h265 using hardware acceleration + * + * @param command

The command to add the arguments to

+ * @param quality

The quality to encode. 0 = best, 51 = worst.

+ */ + public static void addH265HardwareEncoding(@NotNull FFMpegCommand command, int quality) { + command.addOutputFileOption("-codec:v", "hevc_nvenc"); + command.addOutputFileOption("-profile", "main10"); + command.addOutputFileOption("-preset", "p7"); + command.addOutputFileOption("-tag:v", "hvc1"); + command.addOutputFileOption("-crf", String.valueOf(quality)); + } + + /** + * Adds arguments for using hardware acceleration during h264 decoding + * + * @param command

The command to add the arguments to

+ */ + public static void addH26xHardwareDecoding(@NotNull FFMpegCommand command) { + command.addInputFileOption("-hwaccel", "cuda"); + command.addInputFileOption("-hwaccel_output_format", "cuda"); + } + + /** + * Maps all streams in the given list to the output in the given command + * + * @param command

The command to add the mappings to

+ * @param streams

The streams to map

+ */ + public static void mapAllStreams(@NotNull FFMpegCommand command, @NotNull List streams) { + for (StreamObject stream : streams) { + mapStream(command, stream); + } + } + + /** + * Maps the given stream to the given FFMPEG command's output + * + * @param command

The command to map the stream to

+ * @param stream

The stream to map

+ */ + public static void mapStream(@NotNull FFMpegCommand command, @NotNull StreamObject stream) { + command.addOutputFileOption("-map", String.format("%d:%d", + stream.getInputIndex(), stream.getAbsoluteIndex())); } /** @@ -237,38 +292,22 @@ public final class FFMpegHelper { .replace("[", "\\["); } - /** - * Adds image subtitle commands to a command list - * - * @param command

The list containing the FFmpeg commands.

- * @param subtitleStream

The subtitle stream to add.

- * @param videoStream

The video stream to burn the subtitle into.

- */ - private static void addInternalImageSubtitle(List command, SubtitleStream subtitleStream, - VideoStream videoStream) { - command.add("-filter_complex"); - String filter = String.format("[0:v:%d][0:%d]overlay", videoStream.getRelativeIndex(), - subtitleStream.getAbsoluteIndex()); - command.add(filter); - } - /** * Adds external image subtitle commands to a command list * - * @param command

The list containing the FFmpeg commands.

- * @param externalImageSubtitle

The external image subtitle stream to add.

- * @param videoStream

The video stream to burn the subtitle into.

+ * @param command

The FFMPEG command to modify

+ * @param subtitleStream

The external image subtitle stream to burn in

+ * @param videoStream

The video stream to burn the subtitle into

*/ - private static void addExternalImageSubtitle(List command, SubtitleStream externalImageSubtitle, - VideoStream videoStream) { - command.add("-i"); - command.add(externalImageSubtitle.getFile()); - command.add("-filter_complex"); - command.add(String.format("[1:s]scale=width=%d:height=%d,crop=w=%d:h=%d:x=0:y=out_h[sub];[%d:v]" + - "[sub]overlay", videoStream.getWidth(), videoStream.getHeight(), videoStream.getWidth(), - videoStream.getHeight(), videoStream.getAbsoluteIndex())); - command.add("-profile:v"); - command.add("baseline"); + private static void addBurnedInImageSubtitle(@NotNull FFMpegCommand command, + @NotNull SubtitleStream subtitleStream, + @NotNull VideoStream videoStream) { + command.addOutputFileOption("-filter_complex", + String.format("[%d:%d]scale=width=%d:height=%d,crop=w=%d:h=%d:x=0:y=out_h[sub];[%d:%d][sub]overlay", + subtitleStream.getInputIndex(), subtitleStream.getAbsoluteIndex(), videoStream.getWidth(), + videoStream.getHeight(), videoStream.getWidth(), videoStream.getHeight(), + videoStream.getInputIndex(), videoStream.getAbsoluteIndex())); + command.addOutputFileOption("-profile:v", "baseline"); } /** @@ -280,13 +319,11 @@ public final class FFMpegHelper { * @throws IOException

If something goes wrong while probing.

*/ private static String[] probeForStreams(String ffprobePath, File file) throws IOException { - ProcessBuilder processBuilder = new ProcessBuilder( - ffprobePath, - "-v", - "error", - "-show_streams", - file.toString() - ); + FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath); + probeCommand.addGlobalOption("-v", "error", "-show_streams"); + probeCommand.addInputFile(file.toString()); + + ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult()); String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false); return StringUtil.stringBetween(result, "[STREAM]", "[/STREAM]"); } @@ -298,7 +335,9 @@ public final class FFMpegHelper { * @param file

The file currently being converted.

* @return

A list of StreamObjects.

*/ - private static List parseStreams(String ffprobePath, String[] streams, File file) throws IOException { + @NotNull + private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull String[] streams, + @NotNull File file) throws IOException { List parsedStreams = new ArrayList<>(); int relativeAudioIndex = 0; int relativeVideoIndex = 0; @@ -311,19 +350,19 @@ public final class FFMpegHelper { String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), ""); switch (codecType) { case "video": - parsedStreams.add(new VideoStream(streamInfo, relativeVideoIndex++)); + parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++)); break; case "audio": - parsedStreams.add(new AudioStream(streamInfo, relativeAudioIndex++)); + parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++)); break; case "subtitle": - parsedStreams.add(new SubtitleStream(streamInfo, relativeSubtitleIndex++, file.getName(), true)); + parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++)); break; } } - List externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName()); - parsedStreams.addAll(externalSubtitles); - return parsedStreams; + StreamProbeResult probeResult = new StreamProbeResult(List.of(file), parsedStreams); + getExternalSubtitles(probeResult, ffprobePath, file.getParentFile(), file.getName()); + return probeResult; } /** @@ -349,26 +388,25 @@ public final class FFMpegHelper { } /** - * Checks whether there exists an external image subtitle with the same filename as the file + * Tries to find any external subtitles adjacent to the first input file, and appends it to the given probe result * - * @param ffprobePath

The path/command to ffprobe.

- * @param directory

The directory containing the file.

- * @param convertingFile

The file to be converted.

- * @return

The extension of the subtitle or empty if no subtitle was found.

+ * @param ffprobePath

The path/command to ffprobe

+ * @param directory

The directory containing the file

+ * @param convertingFile

The first/main file to be converted

*/ - @NotNull - private static List getExternalSubtitles(@NotNull String ffprobePath, @NotNull File directory, - @NotNull String convertingFile) throws IOException { - List parsedStreams = new ArrayList<>(); + private static void getExternalSubtitles(@NotNull StreamProbeResult streamProbeResult, + @NotNull String ffprobePath, @NotNull File directory, + @NotNull String convertingFile) throws IOException { //Find all files in the same directory with external subtitle formats if (subtitleFormats == null) { subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt"); } File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1); + // TODO: Generalize this for external audio tracks //Return early if no files were found if (subtitleFiles == null) { - return parsedStreams; + return; } String fileTitle = FileUtil.stripExtension(convertingFile); @@ -379,15 +417,17 @@ public final class FFMpegHelper { (subtitleFile) -> subtitleFile.getName().contains(fileTitle)); for (File subtitleFile : subtitleFilesList) { + int inputIndex = streamProbeResult.parsedFiles().size(); + streamProbeResult.parsedFiles().add(subtitleFile); //Probe the files and add them to the result list String[] streams = probeForStreams(ffprobePath, subtitleFile); + int relativeIndex = 0; for (String stream : streams) { String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); Map streamInfo = getStreamInfo(streamParts); - parsedStreams.add(new SubtitleStream(streamInfo, 0, subtitleFile.getName(), false)); + streamProbeResult.parsedStreams().add(new SubtitleStream(streamInfo, inputIndex, relativeIndex++)); } } - return parsedStreams; } /** diff --git a/src/main/java/net/knarcraft/ffmpegconverter/utility/FileUtil.java b/src/main/java/net/knarcraft/ffmpegconverter/utility/FileUtil.java index 94386ba..1ce8e5d 100644 --- a/src/main/java/net/knarcraft/ffmpegconverter/utility/FileUtil.java +++ b/src/main/java/net/knarcraft/ffmpegconverter/utility/FileUtil.java @@ -142,43 +142,4 @@ public final class FileUtil { return file.substring(0, file.lastIndexOf('.')); } - /** - * Gets the locale specifying the language of the given file name - * - * @param fileName

The file name to check

- * @return

The locale, or null if no locale could be parsed

- */ - public static @Nullable String getLanguage(@NotNull String fileName) { - fileName = stripExtension(fileName); - - String possibleLanguage = getExtension(fileName); - // NRK Nett-TV has a tendency to use nb-ttv for Norwegian for some reason - possibleLanguage = possibleLanguage.replace("nb-ttv", "nb-nor"); - - // TODO: Some languages are specified by using "-en" or "-English" or ".en" or ".English" at the end of file names - if (possibleLanguage.length() <= 1 || (possibleLanguage.length() >= 4 && - (!possibleLanguage.contains("-") || possibleLanguage.length() >= 8))) { - return null; - } - - // Hope the text is an actual valid language - if (!possibleLanguage.contains("-")) { - return possibleLanguage; - } - - // Make sure the "-" has at least two characters on each side - String[] parts = possibleLanguage.split("-"); - if (parts[0].length() < 2 || parts[1].length() < 2) { - return null; - } - - if (parts[1].length() == 3) { - // Return three-letter country code - return parts[1].toLowerCase(); - } else { - // Return en-US country code - return parts[0].substring(0, 2).toLowerCase() + "-" + parts[1].substring(0, 2).toUpperCase(); - } - } - }