Adds new converters

Adds a MKV to HEVC converter which aims to reduce file-sizes
Adds a MKV to MP4 transcoder which allows selection of each type of stream
This commit is contained in:
Kristian Knarvik 2023-11-14 17:10:45 +01:00
parent f81a21b9e9
commit 2346e651ef
9 changed files with 294 additions and 26 deletions

View File

@ -104,5 +104,11 @@
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>20.1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@ -3,7 +3,9 @@ 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.MKVToMP4Transcoder;
import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
import net.knarcraft.ffmpegconverter.converter.VideoConverter;
import net.knarcraft.ffmpegconverter.converter.WebVideoConverter;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
@ -12,6 +14,7 @@ import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
@ -61,11 +64,12 @@ class Main {
*/
private static Converter loadConverter() throws IOException {
int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" +
"3. Video converter\n4. Web video converter\n5. MKV to h264 converter", 1, 5);
"3. Video converter\n4. Web video converter\n5. MKV to h264 converter\n6. MKV to h265 reduced " +
"converter\n7. MKV to MP4 transcoder", 1, 7);
switch (choice) {
case 1:
return animeConverter();
return generateAnimeConverter();
case 2:
return new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
case 3:
@ -74,6 +78,10 @@ class Main {
return new WebVideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
case 5:
return new MkvH264Converter(FFPROBE_PATH, FFMPEG_PATH);
case 6:
return new MkvH265ReducedConverter(FFPROBE_PATH, FFMPEG_PATH);
case 7:
return generateMKVToMP4Transcoder();
}
return null;
}
@ -96,18 +104,56 @@ class Main {
OutputUtil.println("No valid files found in folder.");
}
} else if (fileOrFolder.exists()) {
converter.convert(fileOrFolder);
String path = fileOrFolder.getPath();
if (Arrays.stream(converter.getValidFormats()).anyMatch((format) -> format.equalsIgnoreCase(
path.substring(path.lastIndexOf('.') + 1)))) {
converter.convert(fileOrFolder);
} else {
OutputUtil.println("The specified file " + fileOrFolder.getAbsolutePath() + " is not supported for " +
"the selected converter.");
}
} else {
OutputUtil.println("Path " + fileOrFolder.getAbsolutePath() + " does not point to any file or folder.");
}
}
/**
* Initializes the anime converter
* 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 {
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;
int subtitleStreamIndex = -1;
int videoStreamIndex = -1;
try {
if (input.size() > 0) {
audioStreamIndex = Integer.parseInt(input.get(0));
}
if (input.size() > 1) {
subtitleStreamIndex = Integer.parseInt(input.get(1));
}
if (input.size() > 2) {
videoStreamIndex = Integer.parseInt(input.get(2));
}
return new MKVToMP4Transcoder(FFPROBE_PATH, FFMPEG_PATH, audioStreamIndex, subtitleStreamIndex, videoStreamIndex);
} catch (NumberFormatException exception) {
OutputUtil.println("Audio, Subtitle or Video stream index is not a number");
return null;
}
}
/**
* 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 animeConverter() throws IOException {
private static Converter generateAnimeConverter() throws IOException {
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: ");

View File

@ -7,6 +7,8 @@ import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
import net.knarcraft.ffmpegconverter.utility.OutputUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
@ -135,13 +137,16 @@ public abstract class AbstractConverter implements Converter {
}
/**
* Gets the n-th audio stream from a list of streams
* Gets the nth audio stream from a list of streams
*
* @param streams <p>A list of all streams.</p>
* @param streams <p>A list of all streams</p>
* @param n <p>The index of the audio stream to get</p>
* @return <p>The first audio stream found or null if no audio streams were found.</p>
* @return <p>The first audio stream found or null if no audio streams were found</p>
*/
protected AudioStream getNthAudioSteam(List<StreamObject> streams, int n) {
if (n < 0) {
throw new IllegalArgumentException("N cannot be negative!");
}
List<AudioStream> audioStreams = filterStreamsByType(streams, AudioStream.class);
AudioStream audioStream = null;
if (audioStreams.size() > n) {
@ -153,13 +158,16 @@ public abstract class AbstractConverter implements Converter {
}
/**
* Gets the first subtitle stream from a list of streams
* Gets the nth subtitle stream from a list of streams
*
* @param streams <p>A list of all streams.</p>
* @param streams <p>A list of all streams</p>
* @param n <p>The index of the subtitle stream to get</p>
* @return <p>The first subtitle stream found or null if no subtitle streams were found.</p>
* @return <p>The first subtitle stream found or null if no subtitle streams were found</p>
*/
protected SubtitleStream getNthSubtitleStream(List<StreamObject> streams, int n) {
if (n < 0) {
throw new IllegalArgumentException("N cannot be negative!");
}
List<SubtitleStream> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
SubtitleStream subtitleStream = null;
if (subtitleStreams.size() > n) {
@ -171,25 +179,28 @@ public abstract class AbstractConverter implements Converter {
}
/**
* Gets the first video stream from a list of streams
* Gets the nth video stream from a list of streams
*
* @param streams <p>A list of all streams.</p>
* @return <p>The first video stream found or null if no video streams were found.</p>
* @param streams <p>A list of all streams</p>
* @param n <p>The index of the video stream to get</p>
* @return <p>The nth video stream, or null if no video streams were found</p>
*/
protected VideoStream getFirstVideoStream(List<StreamObject> streams) {
protected @Nullable VideoStream getNthVideoStream(@NotNull List<StreamObject> streams, int n) {
if (n < 0) {
throw new IllegalArgumentException("N cannot be negative!");
}
List<VideoStream> videoStreams = filterStreamsByType(streams, VideoStream.class);
VideoStream videoStream = null;
if (videoStreams.size() > 0) {
if (videoStreams.size() > n) {
videoStream = videoStreams.get(n);
} else if (videoStreams.size() > 0) {
videoStream = videoStreams.get(0);
}
if (videoStream == null) {
throw new IllegalArgumentException("The file does not have any valid video streams.");
}
return videoStream;
}
@Override
public void convert(File file) throws IOException {
public void convert(@NotNull File file) throws IOException {
processFile(file.getParentFile(), file);
}

View File

@ -70,7 +70,7 @@ public class AnimeConverter extends AbstractConverter {
Math.max(this.forcedSubtitleIndex, 0));
//Get the first video stream
VideoStream videoStream = getFirstVideoStream(streams);
VideoStream videoStream = getNthVideoStream(streams, 0);
//Add streams to output file
FFMpegHelper.addAudioStream(command, audioStream, this.toStereo);

View File

@ -0,0 +1,81 @@
package net.knarcraft.ffmpegconverter.converter;
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 java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* A transcoder which takes one of each stream from an MKV file and produces an MP4 file
*/
public class MKVToMP4Transcoder extends AbstractConverter {
private final int videoStreamIndex;
private final int audioStreamIndex;
private final int subtitleStreamIndex;
/**
* Instantiates a new mkv to mp4 transcoder
*
* @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
* @param audioStreamIndex <p>The relative index of the audio stream to use (0 or below selects the first)</p>
* @param subtitleStreamIndex <p>The relative index of the subtitle stream to use (0 or below selects the first)</p>
* @param videoStreamIndex <p>The relative index of the video stream to use (0 or below selects the first)</p>
*/
public MKVToMP4Transcoder(String ffprobePath, String ffmpegPath, int audioStreamIndex, int subtitleStreamIndex,
int videoStreamIndex) {
super("mp4");
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.videoStreamIndex = videoStreamIndex;
this.audioStreamIndex = audioStreamIndex;
this.subtitleStreamIndex = subtitleStreamIndex;
}
@Override
public String[] getValidFormats() {
return new String[]{"mkv"};
}
@Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120);
}
//Get the nth audio stream
List<AudioStream> audioStreams = filterStreamsByType(streams, AudioStream.class);
AudioStream audioStream = getNthAudioSteam(new ArrayList<>(audioStreams), Math.max(this.audioStreamIndex, 0));
//Get the nth subtitle stream
List<SubtitleStream> allSubtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
SubtitleStream subtitleStream = getNthSubtitleStream(new ArrayList<>(allSubtitleStreams),
Math.max(this.subtitleStreamIndex, 0));
//Get the nth video stream
VideoStream videoStream = getNthVideoStream(streams, Math.max(this.videoStreamIndex, 0));
// Copy stream info
command.add("-map_metadata");
command.add("0");
command.add("-movflags");
command.add("use_metadata_tags");
command.add("-c");
command.add("copy");
//Add streams to output file
FFMpegHelper.addAudioStream(command, audioStream, false);
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
command.add(outFile);
return command.toArray(new String[0]);
}
}

View File

@ -7,6 +7,7 @@ import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
@ -34,7 +35,14 @@ public class MkvH264Converter extends AbstractConverter {
@Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams,
String outFile) {
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
List<String> 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());
if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120);
}
@ -43,8 +51,14 @@ public class MkvH264Converter extends AbstractConverter {
if (!filterStreamsByType(streams, VideoStream.class).isEmpty()) {
command.add("-map");
command.add("0:v");
command.add("-vcodec");
command.add("h264");
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");
}
// Map audio if present

View File

@ -0,0 +1,108 @@
package net.knarcraft.ffmpegconverter.converter;
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 java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* A converter solely for the purpose of converting video streams of MKV files into h264
*/
public class MkvH265ReducedConverter extends AbstractConverter {
/**
* Initializes variables used by the abstract converter
*
* @param ffprobePath <p>Path/command to ffprobe.</p>
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
*/
public MkvH265ReducedConverter(String ffprobePath, String ffmpegPath) {
super("mkv");
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
}
@Override
public String[] getValidFormats() {
return new String[]{"mkv"};
}
@Override
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams,
String outFile) {
List<String> 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());
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");
}
// Map audio if present
if (!filterStreamsByType(streams, AudioStream.class).isEmpty()) {
command.add("-map");
command.add("0:a");
}
// Map subtitles if present
if (hasInternalStreams(streams)) {
command.add("-map");
command.add("0:s");
command.add("-c:s");
command.add("copy");
}
command.add("-map_metadata");
command.add("0");
command.add("-movflags");
command.add("use_metadata_tags");
command.add("-f");
command.add("matroska");
command.add(outFile);
return command.toArray(new String[0]);
}
/**
* Checks whether the processed file has any internal subtitle streams
*
* @param streams <p>All parsed streams for the video file</p>
* @return <p>True if the file has at least one internal subtitle stream</p>
*/
private boolean hasInternalStreams(List<StreamObject> streams) {
for (StreamObject subtitleStream : filterStreamsByType(streams, SubtitleStream.class)) {
if (((SubtitleStream) subtitleStream).isInternalSubtitle()) {
return true;
}
}
return false;
}
}

View File

@ -41,7 +41,7 @@ public class WebVideoConverter extends AbstractConverter {
//Get first streams from the file
SubtitleStream subtitleStream = getNthSubtitleStream(streams, 0);
VideoStream videoStream = getFirstVideoStream(streams);
VideoStream videoStream = getNthVideoStream(streams, 0);
AudioStream audioStream = getNthAudioSteam(streams, 0);
//Add streams to output

View File

@ -103,7 +103,9 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z]+songs?.*") &&
!titleLowercase.matches(".*songs?[ &/a-z]+si(gn|ng)s?.*") &&
!titleLowercase.matches(".*forced.*") &&
!titleLowercase.matches(".*s&s.*");
!titleLowercase.matches(".*s&s.*") &&
!titleLowercase.matches("signs?") &&
!titleLowercase.matches("songs?");
}
}