Adds a new type of anime converter and stuff
Some checks failed
KnarCraft/FFmpegConvert/pipeline/head There was a failure building this commit

Adds a new type of anime converter that retains all streams, but converts the video to hevc, and re-orders the streams
Adds support for several types of encoding hardware acceleration that are automatically disabled if not available on the system.
Adds automatic hardware decoding acceleration.
Automatically removes empty files if ffmpeg fails.
This commit is contained in:
Kristian Knarvik 2024-04-17 04:39:42 +02:00
parent 461c7552b3
commit f0e75eb440
24 changed files with 647 additions and 193 deletions

46
pom.xml
View File

@ -6,7 +6,6 @@
<groupId>net.knarcraft.ffmpegconvert</groupId>
<artifactId>ffmpegconvert</artifactId>
<version>0.1-alpha</version>
<packaging>jar</packaging>
<name>FFMpeg Convert</name>
@ -73,19 +72,35 @@
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>net.knarcraft.ffmpegconverter.Main</mainClass>
</manifest>
</archive>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>net.knarcraft.ffmpegconverter.Main</Main-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
<dependencies>
@ -101,5 +116,16 @@
<version>20.1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-configuration2</artifactId>
<version>2.10.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
</project>

View File

@ -1,5 +1,6 @@
package net.knarcraft.ffmpegconverter;
import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
import net.knarcraft.ffmpegconverter.converter.AudioConverter;
import net.knarcraft.ffmpegconverter.converter.Converter;
import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
@ -63,10 +64,8 @@ class Main {
/**
* Asks the user which converter they want, and assigns a converter instance to the converter variable
*
* @throws IOException <p>If there's a problem getting user input.</p>
*/
private static Converter loadConverter() throws IOException {
private static Converter loadConverter() {
int choice = getChoice("""
Which converter do you want do use?
1. Anime to web mp4
@ -77,10 +76,11 @@ class Main {
6. MKV to h265 reduced converter
7. MKV to MP4 transcoder
8. DownScaleConverter
9. mp4 Subtitle Embed""", 1, 9);
9. mp4 Subtitle Embed
10. Anime to h265 all streams""", 1, 10);
return switch (choice) {
case 1 -> generateAnimeConverter();
case 1 -> generateWebAnimeConverter();
case 2 -> new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
case 3 -> new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
case 4 -> new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
@ -89,6 +89,7 @@ class Main {
case 7 -> generateMKVToMP4Transcoder();
case 8 -> generateDownScaleConverter();
case 9 -> new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH);
case 10 -> generateAnimeConverter();
default -> null;
};
}
@ -128,9 +129,8 @@ class Main {
* Initializes and returns the downscale converter
*
* @return <p>The initialized downscale converter</p>
* @throws IOException <p>If unable to print to output</p>
*/
private static Converter generateDownScaleConverter() throws IOException {
private static Converter generateDownScaleConverter() {
OutputUtil.println("(New width e.x. 1920) (New height e.x. 1080)\nYour input: ");
List<String> input = readInput(3);
int newWidth;
@ -150,9 +150,8 @@ class Main {
* Initializes and returns the MKV to MP4 transcoder
*
* @return <p>The initialized transcoder</p>
* @throws IOException <p>If unable to print to output</p>
*/
private static Converter generateMKVToMP4Transcoder() throws IOException {
private static Converter generateMKVToMP4Transcoder() {
OutputUtil.println("[Audio stream index 0-n] [Subtitle stream index 0-n] [Video stream index 0-n]\nYour input: ");
List<String> input = readInput(3);
int audioStreamIndex = -1;
@ -180,9 +179,52 @@ class Main {
* Initializes and returns the anime converter
*
* @return <p>The initialized anime converter</p>
* @throws IOException <p>If reading or writing fails.</p>
*/
private static Converter generateAnimeConverter() throws IOException {
private static Converter generateAnimeConverter() {
OutputUtil.println("[Audio languages jpn,eng,ger,fre] [Subtitle languages eng,ger,fre] [Minimal subtitle " +
"preference REQUIRE/PREFER/NO_PREFERENCE/AVOID/REJECT] [Forced audio index 0-n] " +
"[Forced subtitle index 0-n] [Subtitle name filter]\nYour input: ");
List<String> input = readInput(7);
String[] audioLanguage = new String[]{"jpn", "eng", "0"};
String[] subtitleLanguage = new String[]{"nob", "nor", "eng", "jpn", "0"};
MinimalSubtitlePreference subtitlePreference = MinimalSubtitlePreference.AVOID;
int forcedAudioIndex = 0;
int forcedSubtitleIndex = 0;
String subtitleNameFilter = "";
if (!input.isEmpty()) {
audioLanguage = ListUtil.getListFromCommaSeparatedString(input.get(0));
}
if (input.size() > 1) {
subtitleLanguage = ListUtil.getListFromCommaSeparatedString(input.get(1));
}
if (input.size() > 3) {
subtitlePreference = MinimalSubtitlePreference.valueOf(input.get(3).toUpperCase());
}
try {
if (input.size() > 4) {
forcedAudioIndex = Integer.parseInt(input.get(4));
}
if (input.size() > 5) {
forcedSubtitleIndex = Integer.parseInt(input.get(5));
}
} catch (NumberFormatException exception) {
OutputUtil.println("Forced audio or subtitle index is not a number");
return null;
}
if (input.size() > 6) {
subtitleNameFilter = input.get(6);
}
return new AnimeConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguage, subtitleLanguage, subtitlePreference,
forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter);
}
/**
* Initializes and returns the web anime converter
*
* @return <p>The initialized anime converter</p>
*/
private static Converter generateWebAnimeConverter() {
OutputUtil.println("[Audio languages jpn,eng,ger,fre] [Subtitle languages eng,ger,fre] [Convert to stereo if " +
"necessary true/false] [Prevent signs&songs subtitles true/false] [Forced audio index 0-n] " +
"[Forced subtitle index 0-n] [Subtitle name filter]\nYour input: ");
@ -246,9 +288,8 @@ class Main {
*
* @param prompt <p>The prompt shown to the user.</p>
* @return <p>The non-empty choice given by the user.</p>
* @throws IOException <p>If reading or writing fails.</p>
*/
private static String getChoice(String prompt) throws IOException {
private static String getChoice(String prompt) {
OutputUtil.println(prompt);
String choice = "";
while (choice.isEmpty()) {
@ -266,7 +307,7 @@ class Main {
* @param max The maximum allowed value
* @return The value given by the user
*/
private static int getChoice(String prompt, int min, int max) throws IOException {
private static int getChoice(String prompt, int min, int max) {
OutputUtil.println(prompt);
int choice = Integer.MIN_VALUE;
do {

View File

@ -1,6 +1,7 @@
package net.knarcraft.ffmpegconverter.container;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
@ -15,6 +16,7 @@ public class FFMpegCommand {
private final @NotNull List<String> inputFileOptions;
private final @NotNull List<String> inputFiles;
private final @NotNull List<String> outputFileOptions;
private @Nullable String outputVideoCodec;
private @NotNull String outputFile;
/**
@ -31,27 +33,6 @@ public class FFMpegCommand {
this.outputFile = "";
}
/**
* Instantiates a new FFMPEG command
*
* @param executable <p>The FFMPEG/FFPROBE executable to run</p>
* @param globalOptions <p>Options for FFMPEG itself</p>
* @param inputFileOptions <p>Options for processing of the input files</p>
* @param inputFiles <p>The input files to execute on</p>
* @param outputFileOptions <p>Options for the output files</p>
* @param outputFile <p>The output file to write to</p>
*/
public FFMpegCommand(@NotNull String executable, @NotNull List<String> globalOptions,
@NotNull List<String> inputFileOptions, @NotNull List<String> inputFiles,
@NotNull List<String> 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
*
@ -103,6 +84,14 @@ public class FFMpegCommand {
*/
public void addOutputFileOption(@NotNull String... argument) {
this.outputFileOptions.addAll(List.of(argument));
// Detect when the output video codec is set
for (int i = 0; i < argument.length; i++) {
String item = argument[i].trim().toLowerCase();
if (i < argument.length - 1 && item.equals("-vcodec") || item.equals("-codec:v") || item.equals("-c:v")) {
this.outputVideoCodec = argument[i + 1];
}
}
}
/**
@ -114,6 +103,16 @@ public class FFMpegCommand {
this.outputFile = argument;
}
/**
* Gets the output video codec set in this command
*
* @return <p>The output video codec, or null if not set</p>
*/
@Nullable
public String getOutputVideoCodec() {
return this.outputVideoCodec;
}
/**
* Gets the result of combining all the given input
*

View File

@ -0,0 +1,12 @@
package net.knarcraft.ffmpegconverter.container;
import org.jetbrains.annotations.NotNull;
/**
* The result of running a process
*
* @param exitCode <p>The exit code the process exited with</p>
* @param output <p>The output the process produced</p>
*/
public record ProcessResult(int exitCode, @NotNull String output) {
}

View File

@ -1,14 +1,19 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.handler.AvailableHardwareEncoderHandler;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.apache.commons.configuration2.ex.ConfigurationException;
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.List;
/**
* Implements all methods which can be useful for any implementation of a converter.
@ -17,10 +22,12 @@ public abstract class AbstractConverter implements Converter {
final boolean debug = false;
private final String newExtension;
String ffprobePath;
String ffmpegPath;
String[] audioFormats;
String[] videoFormats;
protected String ffprobePath;
protected String ffmpegPath;
protected String[] audioFormats;
protected String[] videoFormats;
protected AvailableHardwareEncoderHandler encoderHandler = null;
/**
* Initializes variables used by the abstract converter
@ -37,14 +44,8 @@ public abstract class AbstractConverter implements Converter {
}
}
/**
* Reads streams from a file, and converts it to a mp4 file
*
* @param folder <p>The folder of the file to process.</p>
* @param file <p>The file to process.</p>
* @throws IOException <p>If the BufferedReader fails.</p>
*/
private void processFile(@NotNull File folder, @NotNull File file) throws IOException {
@Override
public void convert(@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" +
@ -52,24 +53,88 @@ public abstract class AbstractConverter implements Converter {
}
String outExtension = newExtension != null ? newExtension : FileUtil.getExtension(file.getName());
String newPath = FileUtil.getNonCollidingPath(folder, file, outExtension);
String newPath = FileUtil.getNonCollidingPath(file.getParentFile(), file, outExtension);
OutputUtil.println();
OutputUtil.println("Preparing to start process...");
OutputUtil.println("Converting " + file);
String[] command = generateConversionCommand(ffmpegPath, probeResult, newPath);
FFMpegCommand ffMpegCommand = generateConversionCommand(ffmpegPath, probeResult, newPath);
// If the command is null, that means the file does not need conversion
if (ffMpegCommand == null) {
return;
}
String[] command = ffMpegCommand.getResult();
// If no commands were given, no conversion is necessary
if (command.length == 0) {
return;
}
ProcessBuilder processBuilder = new ProcessBuilder(command);
FFMpegHelper.runProcess(processBuilder, folder, "\n", true);
int exitCode = FFMpegHelper.runProcess(processBuilder, file.getParentFile(), "\n", true).exitCode();
if (exitCode != 0) {
try {
handleError(ffMpegCommand, file, newPath);
} catch (ConfigurationException e) {
throw new RuntimeException(e);
}
}
}
@Override
public void convert(@NotNull File file) throws IOException {
processFile(file.getParentFile(), file);
/**
* Handles an ffmpeg conversion error
*
* @param ffMpegCommand <p>The failed ffmpeg command</p>
* @param file <p>The file that was to be converted</p>
* @param newPath <p>The path of the output file</p>
* @throws IOException <p>If unable to produce output</p>
* @throws ConfigurationException <p>If unable </p>
*/
private void handleError(@NotNull FFMpegCommand ffMpegCommand, @NotNull File file,
@NotNull String newPath) throws IOException, ConfigurationException {
File outputFile = new File(newPath);
if (outputFile.exists() && !outputFile.delete()) {
OutputUtil.println("Failed to remove failed output file. Please remove it manually");
}
String outputCodec = ffMpegCommand.getOutputVideoCodec();
if (outputCodec != null && outputCodec.contains("_")) {
String[] parts = outputCodec.split("_");
String hardwareAcceleration = parts[parts.length - 1];
List<String> encodingMethods = getAvailableHardwareEncodingMethods();
if (encodingMethods.contains(hardwareAcceleration)) {
OutputUtil.println("Disabling " + hardwareAcceleration + " hardware acceleration");
encoderHandler.removeHardwareEncoding(hardwareAcceleration);
encoderHandler.save();
convert(file);
return;
}
}
throw new IllegalArgumentException("Conversion failed. Please check the preceding output.");
}
/**
* Gets the available methods for hardware encoding
*
* @return <p>Available hardware encoding methods</p>
*/
protected List<String> getAvailableHardwareEncodingMethods() {
try {
if (encoderHandler == null) {
encoderHandler = AvailableHardwareEncoderHandler.load();
if (encoderHandler.availableHardwareEncodings().isEmpty()) {
List<String> hardwareEncoding = new ArrayList<>(FFMpegHelper.getHWAcceleration(ffmpegPath));
hardwareEncoding.remove(0);
encoderHandler = new AvailableHardwareEncoderHandler(hardwareEncoding);
encoderHandler.save();
}
}
return encoderHandler.availableHardwareEncodings();
} catch (ConfigurationException | IOException exception) {
OutputUtil.println("Unable to get available hardware encoders: " + exception.getMessage());
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,125 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
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.hardwarecoding.H265HardwareEncodingModule;
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.SetDefaultStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetQualityModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetVideoCodecModule;
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.ForcedFirstSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.MinimalSubtitleSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.StreamSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleLanguageSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.SubtitleTitleSorter;
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A converter for converting anime, keeping all streams
*/
public class AnimeConverter extends AbstractConverter {
private final String[] audioLanguages;
private final String[] subtitleLanguages;
private final MinimalSubtitlePreference subtitlePreference;
private final int forcedAudioIndex;
private final int forcedSubtitleIndex;
private final String subtitleNameFilter;
/**
* Instantiates a new anime converter
*
* @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param audioLanguages <p>List of wanted audio languages in descending order.</p>
* @param subtitleLanguages <p>List of wanted subtitle languages in descending order.</p>
* @param subtitlePreference <p>How minimal subtitles should be prioritized</p>
* @param forcedAudioIndex <p>A specific audio stream to force. 0-indexed from the first audio stream found</p>
* @param forcedSubtitleIndex <p>A specific subtitle stream to force. 0-indexed for the first subtitle stream found</p>
*/
public AnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String[] audioLanguages,
@NotNull String[] subtitleLanguages,
@NotNull MinimalSubtitlePreference subtitlePreference, int forcedAudioIndex,
int forcedSubtitleIndex, @NotNull String subtitleNameFilter) {
super("mkv");
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.audioLanguages = audioLanguages;
this.subtitleLanguages = subtitleLanguages;
this.subtitlePreference = subtitlePreference;
this.forcedAudioIndex = forcedAudioIndex;
this.forcedSubtitleIndex = forcedSubtitleIndex;
this.subtitleNameFilter = subtitleNameFilter;
}
@Override
public FFMpegCommand generateConversionCommand(@NotNull String executable,
@NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
if (this.debug) {
modules.add(new DebugModule(90, 120));
}
modules.add(new FastStartModule());
//Get the first video stream
modules.add(new MapAllModule<>(probeResult.getVideoStreams()));
//Get the first audio stream in accordance with chosen languages
StreamSorter<AudioStream> audioSorter = new AudioLanguageSorter(this.audioLanguages).append(
new ForcedFirstSorter<>(this.forcedAudioIndex));
List<AudioStream> sortedAudio = audioSorter.chainSort(probeResult.getAudioStreams());
modules.add(new MapAllModule<>(sortedAudio));
modules.add(new SetDefaultStreamModule<>(sortedAudio, 0));
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
StreamSorter<SubtitleStream> subtitleSorter = new SubtitleTitleSorter(this.subtitleNameFilter)
.append(new MinimalSubtitleSorter(this.subtitlePreference))
.append(new SubtitleLanguageSorter(this.subtitleLanguages))
.append(new ForcedFirstSorter<>(this.forcedSubtitleIndex));
List<SubtitleStream> sorted = subtitleSorter.chainSort(probeResult.getSubtitleStreams());
modules.add(new MapAllModule<>(sorted));
modules.add(new SetDefaultStreamModule<>(sorted, 0));
modules.add(new HardwareDecodeModule());
modules.add(new CopyAudioModule());
modules.add(new CopySubtitlesModule());
List<String> availableHWAcceleration = getAvailableHardwareEncodingMethods();
if (availableHWAcceleration.contains("qsv")) {
modules.add(new SetVideoCodecModule("hevc_qsv"));
modules.add(new SetQualityModule(17, "veryslow"));
} else if (availableHWAcceleration.contains("cuda")) {
modules.add(new H265HardwareEncodingModule(20));
} else {
modules.add(new SetVideoCodecModule("hevc"));
}
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command;
}
@Override
public @NotNull String[] getValidFormats() {
return this.videoFormats;
}
}

View File

@ -32,7 +32,7 @@ public class AudioConverter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -46,7 +46,7 @@ public class AudioConverter extends AbstractConverter {
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
@Override

View File

@ -1,5 +1,6 @@
package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import org.jetbrains.annotations.NotNull;
@ -35,8 +36,7 @@ public interface Converter {
* @param outFile <p>The output file</p>
* @return <p>A list of commands</p>
*/
@NotNull
String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile);
}

View File

@ -5,6 +5,7 @@ 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.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
@ -44,7 +45,7 @@ public class DownScaleConverter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
List<StreamObject> streams = probeResult.parsedStreams();
List<ConverterModule> modules = new ArrayList<>();
@ -56,7 +57,7 @@ public class DownScaleConverter extends AbstractConverter {
// Simply ignore files below, or at, the target resolution
if (videoStream.getWidth() <= this.newWidth && videoStream.getHeight() <= this.newHeight) {
return new String[0];
return null;
}
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
@ -71,8 +72,9 @@ public class DownScaleConverter extends AbstractConverter {
modules.add(new ScaleModule(this.newWidth, this.newHeight));
modules.add(new SetQualityModule(20, "p7"));
modules.add(new SetOutputFileModule(outFile));
modules.add(new HardwareDecodeModule());
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
@Override

View File

@ -5,6 +5,7 @@ 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.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthSubtitleStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthVideoStreamModule;
@ -50,7 +51,7 @@ public class MKVToMP4Transcoder extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -68,6 +69,7 @@ public class MKVToMP4Transcoder extends AbstractConverter {
}
if (!probeResult.getVideoStreams().isEmpty()) {
modules.add(new NthVideoStreamModule(probeResult.getVideoStreams(), this.videoStreamIndex));
modules.add(new HardwareDecodeModule());
}
if (!probeResult.getSubtitleStreams().isEmpty()) {
modules.add(new NthSubtitleStreamModule(probeResult.getSubtitleStreams(), this.subtitleStreamIndex));
@ -76,7 +78,7 @@ public class MKVToMP4Transcoder extends AbstractConverter {
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
}

View File

@ -6,7 +6,7 @@ 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.hardwarecoding.H264HardwareEncodingModule;
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.H26XDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
@ -42,7 +42,7 @@ public class MkvH264Converter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -54,15 +54,7 @@ public class MkvH264Converter extends AbstractConverter {
// Map video if present
List<StreamObject> videoStreams = new ArrayList<>(probeResult.getVideoStreams());
if (!videoStreams.isEmpty()) {
for (StreamObject streamObject : videoStreams) {
if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h265")) {
continue;
}
modules.add(new H26XDecodeModule());
break;
}
modules.add(new HardwareDecodeModule());
modules.add(new MapAllModule<>(videoStreams));
modules.add(new H264HardwareEncodingModule(17));
modules.add(new FastStartModule());
@ -83,7 +75,7 @@ public class MkvH264Converter extends AbstractConverter {
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
}

View File

@ -6,12 +6,12 @@ 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.hardwarecoding.H265HardwareEncodingModule;
import net.knarcraft.ffmpegconverter.converter.module.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
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;
@ -43,7 +43,7 @@ public class MkvH265ReducedConverter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -55,15 +55,7 @@ public class MkvH265ReducedConverter extends AbstractConverter {
// Map video if present
List<VideoStream> videoStreams = probeResult.getVideoStreams();
if (!videoStreams.isEmpty()) {
for (StreamObject streamObject : videoStreams) {
if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) {
continue;
}
FFMpegHelper.addH26xHardwareDecoding(command);
break;
}
modules.add(new HardwareDecodeModule());
modules.add(new H265HardwareEncodingModule(19));
modules.add(new FastStartModule());
modules.add(new MapAllModule<>(videoStreams));
@ -85,7 +77,7 @@ public class MkvH265ReducedConverter extends AbstractConverter {
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
}

View File

@ -5,6 +5,7 @@ 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.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.MovTextModule;
@ -38,7 +39,7 @@ public class SubtitleEmbed extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -48,13 +49,14 @@ public class SubtitleEmbed extends AbstractConverter {
}
modules.add(new MapAllModule<>(probeResult.parsedStreams()));
modules.add(new HardwareDecodeModule());
modules.add(new CopyAudioModule());
modules.add(new CopyAudioModule());
modules.add(new MovTextModule());
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
}

View File

@ -5,6 +5,7 @@ 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.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
@ -34,7 +35,7 @@ public class VideoConverter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -46,12 +47,13 @@ public class VideoConverter extends AbstractConverter {
//Add all streams without re-encoding
modules.add(new MapAllModule<>(streams));
modules.add(new HardwareDecodeModule());
modules.add(new CopyAllModule());
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
@Override

View File

@ -5,9 +5,11 @@ 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.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.AddStereoAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.BurnSubtitleModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.SelectSingleStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.MinimalSubtitleSorter;
import net.knarcraft.ffmpegconverter.converter.sorter.StreamSorter;
@ -67,13 +69,12 @@ public class WebAnimeConverter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
if (this.debug) {
modules.add(new DebugModule(0, 0));
FFMpegHelper.addDebugArguments(command, 50, 120);
}
//Get the first audio stream in accordance with chosen languages
@ -98,6 +99,7 @@ public class WebAnimeConverter extends AbstractConverter {
SubtitleStream subtitleStream = getNthSteam(subtitleSorter.chainSort(probeResult.getSubtitleStreams()),
this.forcedSubtitleIndex);
modules.add(new HardwareDecodeModule());
if (subtitleStream != null && videoStream != null) {
modules.add(new BurnSubtitleModule(subtitleStream, videoStream, true));
} else if (videoStream != null) {
@ -105,10 +107,10 @@ public class WebAnimeConverter extends AbstractConverter {
} else {
throw new IllegalArgumentException("The selected video stream does not exist!");
}
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
command.setOutputFile(outFile);
return command.getResult();
return command;
}
@Override

View File

@ -5,6 +5,7 @@ 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.hardwarecoding.HardwareDecodeModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.BurnSubtitleModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.SelectSingleStreamModule;
@ -43,7 +44,7 @@ public class WebVideoConverter extends AbstractConverter {
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
public FFMpegCommand generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
@ -66,11 +67,11 @@ public class WebVideoConverter extends AbstractConverter {
modules.add(new SelectSingleStreamModule(videoStream));
}
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), 0));
modules.add(new HardwareDecodeModule());
modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult();
return command;
}
}

View File

@ -5,14 +5,13 @@ import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import org.jetbrains.annotations.NotNull;
/**
* A module for enabling h264 and hevc decoding
* A module for enabling hardware decoding
*/
public class H26XDecodeModule implements ConverterModule {
public class HardwareDecodeModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addInputFileOption("-hwaccel", "cuda");
command.addInputFileOption("-hwaccel_output_format", "cuda");
command.addInputFileOption("-hwaccel", "auto");
}
}

View File

@ -0,0 +1,28 @@
package net.knarcraft.ffmpegconverter.converter.module.output;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import org.jetbrains.annotations.NotNull;
/**
* A module for setting the video codec
*/
public class SetVideoCodecModule implements ConverterModule {
private final String codec;
/**
* Instantiates a new set video codec module
*
* @param codec <p>The codec to set</p>
*/
public SetVideoCodecModule(@NotNull String codec) {
this.codec = codec;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-vcodec", codec);
}
}

View File

@ -0,0 +1,42 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A sorter that puts the stream with the given relative index first, without changing order of other items
*
* @param <K> <p>The type of stream to sort</p>
*/
public class ForcedFirstSorter<K extends StreamObject> extends AbstractSorter<K> {
private final int forcedIndex;
/**
* Instantiates a new forced first sorter
*
* @param forcedIndex <p>The relative index of the stream to set as the first item</p>
*/
public ForcedFirstSorter(int forcedIndex) {
this.forcedIndex = forcedIndex;
}
@Override
public @NotNull List<K> sort(@NotNull List<K> input) {
int listIndex = 0;
for (int i = 0; i < input.size(); i++) {
if (input.get(i).getRelativeIndex() == forcedIndex) {
listIndex = i;
break;
}
}
List<K> output = new ArrayList<>(input);
K first = output.remove(listIndex);
output.add(0, first);
return output;
}
}

View File

@ -0,0 +1,88 @@
package net.knarcraft.ffmpegconverter.handler;
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.apache.commons.configuration2.Configuration;
import org.apache.commons.configuration2.PropertiesConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Configurations;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* A handler for keeping track of which hardware acceleration methods are available on the current system
*/
public record AvailableHardwareEncoderHandler(@NotNull List<String> availableHardwareEncodings) {
private static final File configFolder = new File("conf").getAbsoluteFile();
private static final Configurations configurations = new Configurations();
/**
* Gets all hardware encodings
*
* @return <p>All hardware encodings</p>
*/
@Override
@NotNull
public List<String> availableHardwareEncodings() {
return new ArrayList<>(this.availableHardwareEncodings);
}
/**
* Removes the specified hardware encoding
*
* @param encoding <p>The hardware encoding to remove</p>
*/
public void removeHardwareEncoding(@NotNull String encoding) {
this.availableHardwareEncodings.remove(encoding);
}
/**
* Saves settings fro this available hardware encoder handler
*
* @throws ConfigurationException <p>If the configuration file cannot be saved</p>
*/
public void save() throws ConfigurationException {
OutputUtil.printDebug("Preparing to save config");
if (!configFolder.exists() && !configFolder.mkdir()) {
throw new RuntimeException("Unable to create config folder. Make sure to run this .jar file from a " +
"writable directory!");
}
File settingsFile = new File(configFolder, "config.properties");
try {
if (!settingsFile.exists() && !settingsFile.createNewFile()) {
OutputUtil.println("Failed to create configuration file.");
}
} catch (IOException e) {
OutputUtil.println("Failed to create configuration file.");
}
FileBasedConfigurationBuilder<PropertiesConfiguration> builder =
configurations.propertiesBuilder(settingsFile);
PropertiesConfiguration config = builder.getConfiguration();
config.setProperty("encoder.hardware", this.availableHardwareEncodings);
builder.save();
OutputUtil.printDebug("Saved available hardware encoder handler");
}
/**
* Loads saved settings for an available hardware encoder handler
*
* @return <p>The loaded available hardware encoder handler, or a new one if no data has been saved</p>
* @throws ConfigurationException <p>If the configuration file cannot be loaded</p>
*/
public static AvailableHardwareEncoderHandler load() throws ConfigurationException {
File settingsFile = new File(configFolder, "config.properties");
if (!settingsFile.exists()) {
return new AvailableHardwareEncoderHandler(new ArrayList<>());
}
Configuration configuration = configurations.properties(settingsFile);
List<String> getEncodings = configuration.getList(String.class, "encoder.hardware");
return new AvailableHardwareEncoderHandler(getEncodings);
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.ffmpegconverter.utility;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.ProcessResult;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
@ -57,6 +58,13 @@ public final class FFMpegHelper {
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
*/
/**
@ -110,18 +118,6 @@ public final class FFMpegHelper {
return command;
}
/**
* Adds debugging parameters for only converting parts of a file
*
* @param command <p>The command to add to</p>
* @param start <p>The time to start at</p>
* @param duration <p>The duration of video to output</p>
*/
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
*
@ -129,17 +125,20 @@ public final class FFMpegHelper {
* @param folder <p>The folder the process should run in</p>
* @param spacer <p>The character(s) to use between each new line read</p>
* @param write <p>Whether to write the output directly instead of storing it</p>
* @return <p>The result of running the process</p>
* @throws IOException <p>If the process can't be readProcess</p>
*/
@NotNull
public static String runProcess(@NotNull ProcessBuilder processBuilder, @NotNull File folder,
public static ProcessResult runProcess(@NotNull ProcessBuilder processBuilder, @Nullable 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());
//Set directory and error stream
if (folder != null) {
processBuilder.directory(folder);
}
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
@ -158,8 +157,13 @@ public final class FFMpegHelper {
output.append(read);
}
}
try {
int exitCode = process.waitFor();
OutputUtil.println("Process finished.");
return output.toString();
return new ProcessResult(exitCode, output.toString());
} catch (InterruptedException e) {
return new ProcessResult(1, output.toString());
}
}
/**
@ -189,16 +193,6 @@ public final class FFMpegHelper {
command.addOutputFileOption("-crf", String.valueOf(quality));
}
/**
* Adds arguments for using hardware acceleration during h264 decoding
*
* @param command <p>The command to add the arguments to</p>
*/
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
*
@ -266,14 +260,17 @@ public final class FFMpegHelper {
* @return <p>A list of streams.</p>
* @throws IOException <p>If something goes wrong while probing.</p>
*/
private static String[] probeForStreams(String ffprobePath, File file) throws IOException {
private static List<String> probeForStreams(String ffprobePath, File file) throws IOException {
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]");
ProcessResult result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
if (result.exitCode() != 0) {
throw new IllegalArgumentException("File probe failed with code " + result.exitCode());
}
return StringUtil.stringBetween(result.output(), "[STREAM]", "[/STREAM]");
}
/**
@ -284,7 +281,7 @@ public final class FFMpegHelper {
* @return <p>A list of StreamObjects.</p>
*/
@NotNull
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull String[] streams,
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull List<String> streams,
@NotNull File file) throws IOException {
List<StreamObject> parsedStreams = new ArrayList<>();
int relativeAudioIndex = 0;
@ -368,7 +365,7 @@ public final class FFMpegHelper {
int inputIndex = streamProbeResult.parsedFiles().size();
streamProbeResult.parsedFiles().add(subtitleFile);
//Probe the files and add them to the result list
String[] streams = probeForStreams(ffprobePath, subtitleFile);
List<String> streams = probeForStreams(ffprobePath, subtitleFile);
int relativeIndex = 0;
for (String stream : streams) {
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
@ -394,4 +391,20 @@ public final class FFMpegHelper {
return text.toString().trim();
}
/**
* Gets available hardware acceleration types
*
* @param ffmpegPath <p>The path to ffmpeg's executable</p>
* @return <p>The available hardware acceleration methods</p>
* @throws IOException <p>If the process fails</p>
*/
public static List<String> getHWAcceleration(@NotNull String ffmpegPath) throws IOException {
FFMpegCommand probeCommand = new FFMpegCommand(ffmpegPath);
probeCommand.addGlobalOption("-v", "error", "-hwaccels");
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
ProcessResult result = runProcess(processBuilder, null, PROBE_SPLIT_CHARACTER, false);
return List.of(result.output().split(PROBE_SPLIT_CHARACTER));
}
}

View File

@ -29,11 +29,14 @@ public final class OutputUtil {
* Prints something and a newline to the commandline efficiently
*
* @param input <p>The text to print.</p>
* @throws IOException <p>If a write is not possible.</p>
*/
public static void println(String input) throws IOException {
public static void println(String input) {
if (!input.isEmpty()) {
try {
writer.write(input);
} catch (IOException e) {
System.out.print(input);
}
}
println();
}
@ -42,22 +45,27 @@ public final class OutputUtil {
* Prints a string
*
* @param input <p>The string to print.</p>
* @throws IOException <p>If the writer fails to write.</p>
*/
public static void print(String input) throws IOException {
public static void print(String input) {
try {
writer.write(input);
writer.flush();
} catch (IOException e) {
System.out.print(input);
}
}
/**
* Prints a newline
*
* @throws IOException <p>If a write is not possible.</p>
*/
public static void println() throws IOException {
public static void println() {
try {
writer.newLine();
writer.flush();
} catch (IOException e) {
System.out.println();
}
}
/**
@ -73,11 +81,10 @@ public final class OutputUtil {
* Prints a message if debug messages should be shown
*
* @param message <p>The debug message to show.</p>
* @throws IOException <p>If a write is not possible.</p>
*/
public static void printDebug(String message) throws IOException {
public static void printDebug(String message) {
if (debug) {
print(message);
println(message);
}
}

View File

@ -1,5 +1,10 @@
package net.knarcraft.ffmpegconverter.utility;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A class which helps with operations on strings
*/
@ -12,23 +17,26 @@ final class StringUtil {
/**
* Finds all substrings between two substrings in a string
*
* @param string <p>The string containing the substrings.</p>
* @param input <p>The string containing the substrings.</p>
* @param start <p>The substring before the wanted substring.</p>
* @param end <p>The substring after the wanted substring.</p>
* @return <p>A list of all occurrences of the substring.</p>
*/
static String[] stringBetween(String string, String start, String end) {
int startPosition = string.indexOf(start) + start.length();
public static @NotNull List<String> stringBetween(@NotNull String input, @NotNull String start,
@NotNull String end) {
List<String> output = new ArrayList<>();
String inputString = input;
while (true) {
int startPosition = inputString.indexOf(start) + start.length();
//Return if the string is not found
if (!string.contains(start) || string.indexOf(end, startPosition) < startPosition) {
return new String[]{};
if (!inputString.contains(start) || inputString.indexOf(end, startPosition) < startPosition) {
return output;
}
int endPosition = string.indexOf(end, startPosition);
int endPosition = inputString.indexOf(end, startPosition);
//Get the string between the start and end string
String outString = string.substring(startPosition, endPosition).trim();
String nextString = string.substring(endPosition + end.length());
//Add other occurrences recursively
return ListUtil.concatenate(new String[]{outString}, stringBetween(nextString, start, end));
output.add(inputString.substring(startPosition, endPosition).trim());
inputString = inputString.substring(endPosition + end.length());
}
}
}

View File

@ -2,27 +2,33 @@ package net.knarcraft.ffmpegconverter.utility;
import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import java.util.ArrayList;
import java.util.List;
import static org.junit.Assert.assertEquals;
public class StringUtilTest {
@Test
public void stringBetweenNoMatches() {
String[] result = StringUtil.stringBetween("a test string", "[", "]");
assertArrayEquals(new String[]{}, result);
List<String> result = StringUtil.stringBetween("a test string", "[", "]");
List<String> empty = new ArrayList<>();
assertEquals(empty, result);
}
@Test
public void stringBetweenOneMatch() {
String[] result = StringUtil.stringBetween("a [test] string", "[", "]");
assertArrayEquals(new String[]{"test"}, result);
List<String> result = StringUtil.stringBetween("a [test] string", "[", "]");
List<String> expected = List.of("test");
assertEquals(expected, result);
}
@Test
public void stringBetweenSeveralMatches() {
String[] result = StringUtil.stringBetween("a long string containing a lot of potential matches for " +
List<String> result = StringUtil.stringBetween("a long string containing a lot of potential matches for " +
"the string between method defined in the StringUtil class", " ", "a");
assertArrayEquals(new String[]{"long string cont", "", "lot of potenti", "m",
"for the string between method defined in the StringUtil cl"}, result);
List<String> expected = List.of("long string cont", "", "lot of potenti", "m",
"for the string between method defined in the StringUtil cl");
assertEquals(expected, result);
}
}