Adds converter modules and sorters
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good

Converters are now the result of combining multiple modules, and running them in the selected sequence. I hope to be able to completely remove converters by replacing them with templates that simply run some modules in a sequence, before running FFmpeg. Then I want to be able to parse a command into a template, so users have full control of what a template does.

Sorters each sort a list of streams after a single criteria. They can be chained in order to create complex chained sorters.
This commit is contained in:
Kristian Knarvik 2024-04-12 15:31:34 +02:00
parent 376d5655f2
commit 461c7552b3
51 changed files with 1819 additions and 579 deletions

View File

@ -1,6 +1,5 @@
package net.knarcraft.ffmpegconverter; package net.knarcraft.ffmpegconverter;
import net.knarcraft.ffmpegconverter.converter.AnimeConverter;
import net.knarcraft.ffmpegconverter.converter.AudioConverter; import net.knarcraft.ffmpegconverter.converter.AudioConverter;
import net.knarcraft.ffmpegconverter.converter.Converter; import net.knarcraft.ffmpegconverter.converter.Converter;
import net.knarcraft.ffmpegconverter.converter.DownScaleConverter; import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
@ -9,7 +8,9 @@ import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter; import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
import net.knarcraft.ffmpegconverter.converter.SubtitleEmbed; import net.knarcraft.ffmpegconverter.converter.SubtitleEmbed;
import net.knarcraft.ffmpegconverter.converter.VideoConverter; import net.knarcraft.ffmpegconverter.converter.VideoConverter;
import net.knarcraft.ffmpegconverter.converter.WebAnimeConverter;
import net.knarcraft.ffmpegconverter.converter.WebVideoConverter; import net.knarcraft.ffmpegconverter.converter.WebVideoConverter;
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
import net.knarcraft.ffmpegconverter.utility.FileUtil; import net.knarcraft.ffmpegconverter.utility.FileUtil;
import net.knarcraft.ffmpegconverter.utility.ListUtil; import net.knarcraft.ffmpegconverter.utility.ListUtil;
import net.knarcraft.ffmpegconverter.utility.OutputUtil; import net.knarcraft.ffmpegconverter.utility.OutputUtil;
@ -189,9 +190,9 @@ class Main {
String[] audioLanguage = new String[]{"jpn", "0"}; String[] audioLanguage = new String[]{"jpn", "0"};
String[] subtitleLanguage = new String[]{"eng", "0"}; String[] subtitleLanguage = new String[]{"eng", "0"};
boolean toStereo = true; boolean toStereo = true;
boolean preventSigns = true; MinimalSubtitlePreference subtitlePreference = MinimalSubtitlePreference.AVOID;
int forcedAudioIndex = -1; int forcedAudioIndex = 0;
int forcedSubtitleIndex = -1; int forcedSubtitleIndex = 0;
String subtitleNameFilter = ""; String subtitleNameFilter = "";
if (!input.isEmpty()) { if (!input.isEmpty()) {
@ -204,7 +205,7 @@ class Main {
toStereo = Boolean.parseBoolean(input.get(2)); toStereo = Boolean.parseBoolean(input.get(2));
} }
if (input.size() > 3) { if (input.size() > 3) {
preventSigns = Boolean.parseBoolean(input.get(3)); subtitlePreference = MinimalSubtitlePreference.valueOf(input.get(3).toUpperCase());
} }
try { try {
if (input.size() > 4) { if (input.size() > 4) {
@ -220,8 +221,8 @@ class Main {
if (input.size() > 6) { if (input.size() > 6) {
subtitleNameFilter = input.get(6); subtitleNameFilter = input.get(6);
} }
return new AnimeConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguage, subtitleLanguage, toStereo, return new WebAnimeConverter(FFPROBE_PATH, FFMPEG_PATH, audioLanguage, subtitleLanguage, toStereo,
preventSigns, forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter); subtitlePreference, forcedAudioIndex, forcedSubtitleIndex, subtitleNameFilter);
} }

View File

@ -1,9 +1,13 @@
package net.knarcraft.ffmpegconverter.container; package net.knarcraft.ffmpegconverter.container;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -13,4 +17,56 @@ import java.util.List;
* @param parsedStreams <p>The streams that were parsed from the files</p> * @param parsedStreams <p>The streams that were parsed from the files</p>
*/ */
public record StreamProbeResult(@NotNull List<File> parsedFiles, @NotNull List<StreamObject> parsedStreams) { public record StreamProbeResult(@NotNull List<File> parsedFiles, @NotNull List<StreamObject> parsedStreams) {
/**
* Gets all probed subtitle streams
*
* @return <p>All probed subtitle streams</p>
*/
@NotNull
public List<SubtitleStream> getSubtitleStreams() {
return filterStreamsByType(this.parsedStreams, SubtitleStream.class);
}
/**
* Gets all probed audio streams
*
* @return <p>All probed audio streams</p>
*/
@NotNull
public List<AudioStream> getAudioStreams() {
return filterStreamsByType(this.parsedStreams, AudioStream.class);
}
/**
* Gets all probed video streams
*
* @return <p>All probed video streams</p>
*/
@NotNull
public List<VideoStream> getVideoStreams() {
return filterStreamsByType(this.parsedStreams, VideoStream.class);
}
/**
* Filters parsed streams into one of the stream types
*
* @param streams <p>A list of stream objects.</p>
* @param clazz <p>The class to filter</p>
* @param <G> <p>The correct object type for the streams with the selected codec type.</p>
* @return <p>A potentially shorter list of streams.</p>
*/
@NotNull
@SuppressWarnings("unchecked")
private <G extends StreamObject> List<G> filterStreamsByType(@NotNull List<StreamObject> streams,
@NotNull Class<?> clazz) {
List<G> newStreams = new ArrayList<>();
for (StreamObject stream : streams) {
if (stream.getClass() == clazz) {
newStreams.add((G) stream);
}
}
return newStreams;
}
} }

View File

@ -1,10 +1,6 @@
package net.knarcraft.ffmpegconverter.converter; package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; 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 net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import net.knarcraft.ffmpegconverter.utility.FileUtil; import net.knarcraft.ffmpegconverter.utility.FileUtil;
import net.knarcraft.ffmpegconverter.utility.OutputUtil; import net.knarcraft.ffmpegconverter.utility.OutputUtil;
@ -13,8 +9,6 @@ import org.jetbrains.annotations.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; 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. * Implements all methods which can be useful for any implementation of a converter.
@ -43,79 +37,6 @@ public abstract class AbstractConverter implements Converter {
} }
} }
/**
* Filters parsed streams into one of the stream types
*
* @param streams <p>A list of stream objects.</p>
* @param clazz <p>The class to filter</p>
* @param <G> <p>The correct object type for the streams with the selected codec type.</p>
* @return <p>A potentially shorter list of streams.</p>
*/
@SuppressWarnings("unchecked")
static <G extends StreamObject> List<G> filterStreamsByType(List<StreamObject> streams, Class<?> clazz) {
List<G> newStreams = new ArrayList<>();
for (StreamObject stream : streams) {
if (stream.getClass() == clazz) {
newStreams.add((G) stream);
}
}
return newStreams;
}
/**
* Filters and sorts audio streams according to chosen languages
*
* @param audioStreams <p>A list of audio streams.</p>
* @param audioLanguages <p>A list of languages.</p>
* @return <p>A list containing just audio tracks of chosen languages, sorted in order of languages.</p>
*/
static List<AudioStream> filterAudioStreams(List<AudioStream> audioStreams, String[] audioLanguages) {
return sortStreamsByLanguage(audioStreams, audioLanguages);
}
/**
* Filters and sorts subtitle streams according to chosen languages
*
* @param subtitleStreams <p>A list of subtitle streams.</p>
* @param subtitleLanguages <p>A list of languages.</p>
* @param preventSignsAndSongs <p>Whether partial subtitles should be avoided.</p>
* @param subtitleNameFilter <p>The filter to use for forcing streams of a given subtitle group</p>
* @return <p>A list containing just subtitles of chosen languages, sorted in order of languages.</p>
*/
static List<SubtitleStream> filterSubtitleStreams(List<SubtitleStream> subtitleStreams, String[] subtitleLanguages,
boolean preventSignsAndSongs, String subtitleNameFilter) {
List<SubtitleStream> sorted = sortStreamsByLanguage(subtitleStreams, subtitleLanguages);
sorted.removeIf((stream) -> preventSignsAndSongs && !stream.getIsFullSubtitle());
//Filter by name of subtitle group, PGS or similar
if (!subtitleNameFilter.trim().isEmpty()) {
sorted.removeIf((stream) -> !stream.getTitle().contains(subtitleNameFilter));
}
return sorted;
}
/**
* Sorts subtitle streams according to chosen languages and removes non-matching languages
*
* @param streams <p>A list of streams to sort.</p>
* @param languages <p>The languages chosen by the user.</p>
* @param <G> <p>The type of streams to sort.</p>
* @return <p>A sorted version of the list.</p>
*/
private static <G extends StreamObject> List<G> sortStreamsByLanguage(List<G> streams, String[] languages) {
List<G> sorted = new ArrayList<>();
for (String language : languages) {
for (G stream : streams) {
String streamLanguage = stream.getLanguage();
if (language.equals("*") || (streamLanguage.equals("und") && language.equals("0")) ||
streamLanguage.equals(language)) {
sorted.add(stream);
}
}
streams.removeAll(sorted);
}
return sorted;
}
/** /**
* Reads streams from a file, and converts it to a mp4 file * Reads streams from a file, and converts it to a mp4 file
* *
@ -146,69 +67,6 @@ public abstract class AbstractConverter implements Converter {
FFMpegHelper.runProcess(processBuilder, folder, "\n", true); FFMpegHelper.runProcess(processBuilder, folder, "\n", true);
} }
/**
* Gets the nth audio stream from a list of streams
*
* @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>
*/
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) {
audioStream = audioStreams.get(n);
} else if (!audioStreams.isEmpty()) {
audioStream = audioStreams.get(0);
}
return audioStream;
}
/**
* Gets the nth subtitle stream from a list of streams
*
* @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>
*/
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) {
subtitleStream = subtitleStreams.get(n);
} else if (!subtitleStreams.isEmpty()) {
subtitleStream = subtitleStreams.get(0);
}
return subtitleStream;
}
/**
* Gets the nth video stream from a list of streams
*
* @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 @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() > n) {
videoStream = videoStreams.get(n);
} else if (!videoStreams.isEmpty()) {
videoStream = videoStreams.get(0);
}
return videoStream;
}
@Override @Override
public void convert(@NotNull File file) throws IOException { public void convert(@NotNull File file) throws IOException {
processFile(file.getParentFile(), file); processFile(file.getParentFile(), file);

View File

@ -1,95 +0,0 @@
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.util.ArrayList;
import java.util.List;
/**
* A converter mainly designed for converting anime to web-playable mp4
*/
public class AnimeConverter extends AbstractConverter {
private final String[] audioLanguages;
private final String[] subtitleLanguages;
private final boolean toStereo;
private final boolean preventSignsAndSongs;
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 toStereo <p>Convert video with several audio channels to stereo.</p>
* @param preventSignsAndSongs <p>Prevent subtitles only converting signs and songs (not speech).</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(String ffprobePath, String ffmpegPath, String[] audioLanguages, String[] subtitleLanguages,
boolean toStereo, boolean preventSignsAndSongs, int forcedAudioIndex, int forcedSubtitleIndex,
String subtitleNameFilter) {
super("mp4");
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.audioLanguages = audioLanguages;
this.subtitleLanguages = subtitleLanguages;
this.toStereo = toStereo;
this.preventSignsAndSongs = preventSignsAndSongs;
this.forcedAudioIndex = forcedAudioIndex;
this.forcedSubtitleIndex = forcedSubtitleIndex;
this.subtitleNameFilter = subtitleNameFilter;
}
@Override
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams();
if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120);
}
//Get the first audio stream in accordance with chosen languages
List<AudioStream> audioStreams = filterAudioStreams(filterStreamsByType(streams, AudioStream.class),
this.audioLanguages);
AudioStream audioStream = getNthAudioSteam(new ArrayList<>(audioStreams), Math.max(this.forcedAudioIndex, 0));
//Get the first subtitle stream in accordance with chosen languages and signs and songs prevention
List<SubtitleStream> allSubtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
List<SubtitleStream> subtitleStreams = filterSubtitleStreams(allSubtitleStreams, this.subtitleLanguages,
this.preventSignsAndSongs, this.subtitleNameFilter);
SubtitleStream subtitleStream = getNthSubtitleStream(new ArrayList<>(subtitleStreams),
Math.max(this.forcedSubtitleIndex, 0));
//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);
command.setOutputFile(outFile);
return command.getResult();
}
@Override
public String[] getValidFormats() {
return this.videoFormats;
}
}

View File

@ -2,11 +2,15 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -31,16 +35,17 @@ public class AudioConverter extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams(); List<ConverterModule> modules = new ArrayList<>();
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
//Gets the first audio stream from the file and adds it to the output file //Gets the first audio stream from the file and adds it to the output file
AudioStream audioStream = getNthAudioSteam(streams, 0); modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), 0));
FFMpegHelper.addAudioStream(command, audioStream, false); modules.add(new SetOutputFileModule(outFile));
command.setOutputFile(outFile);
new ModuleExecutor(command, modules).execute();
return command.getResult(); return command.getResult();
} }

View File

@ -2,11 +2,21 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
import net.knarcraft.ffmpegconverter.converter.module.output.ScaleModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetQualityModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -37,24 +47,31 @@ public class DownScaleConverter extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
List<StreamObject> streams = probeResult.parsedStreams(); List<StreamObject> streams = probeResult.parsedStreams();
VideoStream videoStream = getNthVideoStream(streams, 0); List<ConverterModule> modules = new ArrayList<>();
if (videoStream == null || (videoStream.getWidth() <= newWidth && videoStream.getHeight() <= newHeight)) { VideoStream videoStream = FFMpegHelper.getNthSteam(probeResult.getVideoStreams(), 0);
if (videoStream == null) {
throw new IllegalArgumentException("The selected file has no video stream");
}
// Simply ignore files below, or at, the target resolution
if (videoStream.getWidth() <= this.newWidth && videoStream.getHeight() <= this.newHeight) {
return new String[0]; return new String[0];
} }
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
//Add all streams without re-encoding //Add all streams without re-encoding
FFMpegHelper.mapAllStreams(command, streams); modules.add(new MapAllModule<>(streams));
command.addOutputFileOption("-c:a", "copy"); modules.add(new CopyAudioModule());
command.addOutputFileOption("-c:s", "copy"); modules.add(new CopySubtitlesModule());
command.addOutputFileOption("-vf", "scale=" + newWidth + ":" + newHeight); modules.add(new ScaleModule(this.newWidth, this.newHeight));
command.addOutputFileOption("-crf", "20"); modules.add(new SetQualityModule(20, "p7"));
command.addOutputFileOption("-preset", "slow"); modules.add(new SetOutputFileModule(outFile));
command.setOutputFile(outFile); new ModuleExecutor(command, modules).execute();
return command.getResult(); return command.getResult();
} }

View File

@ -2,10 +2,14 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthSubtitleStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthVideoStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -49,34 +53,29 @@ public class MKVToMP4Transcoder extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams(); List<ConverterModule> modules = new ArrayList<>();
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
}
//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));
if (videoStream == null) {
throw new IllegalArgumentException("The selected video stream was not found");
} }
// Copy stream info // Copy stream info
command.addOutputFileOption("-c", "copy"); modules.add(new CopyAllModule());
//Add streams to output file //Add streams to output file
FFMpegHelper.addAudioStream(command, audioStream, false); if (!probeResult.getAudioStreams().isEmpty()) {
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream); modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), this.audioStreamIndex));
}
if (!probeResult.getVideoStreams().isEmpty()) {
modules.add(new NthVideoStreamModule(probeResult.getVideoStreams(), this.videoStreamIndex));
}
if (!probeResult.getSubtitleStreams().isEmpty()) {
modules.add(new NthSubtitleStreamModule(probeResult.getSubtitleStreams(), this.subtitleStreamIndex));
}
command.setOutputFile(outFile); modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult(); return command.getResult();
} }

View File

@ -2,13 +2,21 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.streams.AudioStream; 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.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopySubtitlesModule;
import net.knarcraft.ffmpegconverter.converter.module.output.FastStartModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject; 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 net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -37,46 +45,44 @@ public class MkvH264Converter extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams(); List<ConverterModule> modules = new ArrayList<>();
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
// Map video if present // Map video if present
List<StreamObject> videoStreams = filterStreamsByType(streams, VideoStream.class); List<StreamObject> videoStreams = new ArrayList<>(probeResult.getVideoStreams());
if (!videoStreams.isEmpty()) { if (!videoStreams.isEmpty()) {
for (StreamObject streamObject : videoStreams) { for (StreamObject streamObject : videoStreams) {
if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) { if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h265")) {
continue; continue;
} }
FFMpegHelper.addH26xHardwareDecoding(command); modules.add(new H26XDecodeModule());
break; break;
} }
FFMpegHelper.mapAllStreams(command, videoStreams); modules.add(new MapAllModule<>(videoStreams));
FFMpegHelper.addH264HardwareEncoding(command, 17); modules.add(new H264HardwareEncodingModule(17));
command.addOutputFileOption("-movflags", "+faststart"); modules.add(new FastStartModule());
} }
// Map audio if present // Map audio if present
List<StreamObject> audioStreams = filterStreamsByType(streams, AudioStream.class); if (!probeResult.getAudioStreams().isEmpty()) {
if (!audioStreams.isEmpty()) { modules.add(new MapAllModule<>(probeResult.getAudioStreams()));
FFMpegHelper.mapAllStreams(command, audioStreams); modules.add(new CopyAudioModule());
command.addOutputFileOption("-c:a", "copy");
} }
// Map subtitles if present // Map subtitles if present
List<StreamObject> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class); List<StreamObject> subtitleStreams = new ArrayList<>(probeResult.getSubtitleStreams());
if (!subtitleStreams.isEmpty()) { if (!subtitleStreams.isEmpty()) {
FFMpegHelper.mapAllStreams(command, subtitleStreams); modules.add(new MapAllModule<>(probeResult.getSubtitleStreams()));
command.addOutputFileOption("-c:s", "copy"); modules.add(new CopySubtitlesModule());
} }
command.addOutputFileOption("-f", "matroska"); modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
command.setOutputFile(outFile);
return command.getResult(); return command.getResult();
} }

View File

@ -2,6 +2,14 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; 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.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.AudioStream;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
@ -9,6 +17,7 @@ import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -37,14 +46,14 @@ public class MkvH265ReducedConverter extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams(); List<ConverterModule> modules = new ArrayList<>();
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
// Map video if present // Map video if present
List<StreamObject> videoStreams = filterStreamsByType(streams, VideoStream.class); List<VideoStream> videoStreams = probeResult.getVideoStreams();
if (!videoStreams.isEmpty()) { if (!videoStreams.isEmpty()) {
for (StreamObject streamObject : videoStreams) { for (StreamObject streamObject : videoStreams) {
if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) { if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) {
@ -55,27 +64,27 @@ public class MkvH265ReducedConverter extends AbstractConverter {
break; break;
} }
FFMpegHelper.mapAllStreams(command, videoStreams); modules.add(new H265HardwareEncodingModule(19));
FFMpegHelper.addH265HardwareEncoding(command, 17); modules.add(new FastStartModule());
command.addOutputFileOption("-movflags", "+faststart"); modules.add(new MapAllModule<>(videoStreams));
} }
// Map audio if present // Map audio if present
List<StreamObject> audioStreams = filterStreamsByType(streams, AudioStream.class); List<AudioStream> audioStreams = probeResult.getAudioStreams();
if (!audioStreams.isEmpty()) { if (!audioStreams.isEmpty()) {
FFMpegHelper.mapAllStreams(command, audioStreams); modules.add(new MapAllModule<>(audioStreams));
} }
// Map subtitles if present // Map subtitles if present
List<StreamObject> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class); List<SubtitleStream> subtitleStreams = probeResult.getSubtitleStreams();
if (!subtitleStreams.isEmpty()) { if (!subtitleStreams.isEmpty()) {
FFMpegHelper.mapAllStreams(command, subtitleStreams); modules.add(new MapAllModule<>(subtitleStreams));
command.addOutputFileOption("-c:s", "copy"); modules.add(new CopySubtitlesModule());
} }
command.addOutputFileOption("-f", "matroska"); modules.add(new SetOutputFileModule(outFile));
command.setOutputFile(outFile); new ModuleExecutor(command, modules).execute();
return command.getResult(); return command.getResult();
} }

View File

@ -2,10 +2,17 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAudioModule;
import net.knarcraft.ffmpegconverter.converter.module.output.MovTextModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -34,18 +41,19 @@ public class SubtitleEmbed extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams(); List<ConverterModule> modules = new ArrayList<>();
FFMpegHelper.mapAllStreams(command, streams);
command.addOutputFileOption("-c:a", "copy");
command.addOutputFileOption("-c:v", "copy");
command.addOutputFileOption("-c:s", "mov_text");
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
command.setOutputFile(outFile); modules.add(new MapAllModule<>(probeResult.parsedStreams()));
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.getResult();
} }

View File

@ -2,10 +2,17 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.mapping.MapAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.CopyAllModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -30,16 +37,20 @@ public class VideoConverter extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
List<ConverterModule> modules = new ArrayList<>();
List<StreamObject> streams = probeResult.parsedStreams(); List<StreamObject> streams = probeResult.parsedStreams();
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
//Add all streams without re-encoding //Add all streams without re-encoding
FFMpegHelper.mapAllStreams(command, streams); modules.add(new MapAllModule<>(streams));
command.addOutputFileOption("-c", "copy"); modules.add(new CopyAllModule());
command.setOutputFile(outFile); modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult(); return command.getResult();
} }

View File

@ -0,0 +1,119 @@
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.mapping.AddStereoAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.BurnSubtitleModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.SelectSingleStreamModule;
import net.knarcraft.ffmpegconverter.converter.sorter.AudioLanguageSorter;
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.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static net.knarcraft.ffmpegconverter.utility.FFMpegHelper.getNthSteam;
/**
* A converter mainly designed for converting anime to web-playable mp4
*/
public class WebAnimeConverter extends AbstractConverter {
private final String[] audioLanguages;
private final String[] subtitleLanguages;
private final boolean toStereo;
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 toStereo <p>Convert video with several audio channels to stereo.</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 WebAnimeConverter(@NotNull String ffprobePath, @NotNull String ffmpegPath, @NotNull String[] audioLanguages,
@NotNull String[] subtitleLanguages, boolean toStereo,
@NotNull MinimalSubtitlePreference subtitlePreference, int forcedAudioIndex,
int forcedSubtitleIndex, @NotNull String subtitleNameFilter) {
super("mp4");
this.ffprobePath = ffprobePath;
this.ffmpegPath = ffmpegPath;
this.audioLanguages = audioLanguages;
this.subtitleLanguages = subtitleLanguages;
this.toStereo = toStereo;
this.subtitlePreference = subtitlePreference;
this.forcedAudioIndex = forcedAudioIndex;
this.forcedSubtitleIndex = forcedSubtitleIndex;
this.subtitleNameFilter = subtitleNameFilter;
}
@Override
public String[] 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
AudioStream audioStream = getNthSteam(new AudioLanguageSorter(this.audioLanguages).sort(
probeResult.getAudioStreams()), this.forcedAudioIndex);
if (audioStream == null) {
throw new IllegalArgumentException("The given input resulted in no audio stream being selected");
}
if (this.toStereo) {
modules.add(new AddStereoAudioStreamModule(audioStream, true));
} else {
modules.add(new SelectSingleStreamModule(audioStream));
}
//Get the first video stream
VideoStream videoStream = getNthSteam(probeResult.getVideoStreams(), 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));
SubtitleStream subtitleStream = getNthSteam(subtitleSorter.chainSort(probeResult.getSubtitleStreams()),
this.forcedSubtitleIndex);
if (subtitleStream != null && videoStream != null) {
modules.add(new BurnSubtitleModule(subtitleStream, videoStream, true));
} else if (videoStream != null) {
modules.add(new SelectSingleStreamModule(videoStream));
} else {
throw new IllegalArgumentException("The selected video stream does not exist!");
}
new ModuleExecutor(command, modules).execute();
command.setOutputFile(outFile);
return command.getResult();
}
@Override
public String[] getValidFormats() {
return this.videoFormats;
}
}

View File

@ -2,15 +2,23 @@ package net.knarcraft.ffmpegconverter.converter;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.converter.module.DebugModule;
import net.knarcraft.ffmpegconverter.converter.module.ModuleExecutor;
import net.knarcraft.ffmpegconverter.converter.module.mapping.BurnSubtitleModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.NthAudioStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.mapping.SelectSingleStreamModule;
import net.knarcraft.ffmpegconverter.converter.module.output.SetOutputFileModule;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream; import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper; import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static net.knarcraft.ffmpegconverter.utility.FFMpegHelper.getNthSteam;
/** /**
* A simple converter for web-video * A simple converter for web-video
*/ */
@ -38,27 +46,31 @@ public class WebVideoConverter extends AbstractConverter {
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult, public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
@NotNull String outFile) { @NotNull String outFile) {
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles()); FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
List<StreamObject> streams = probeResult.parsedStreams(); List<ConverterModule> modules = new ArrayList<>();
if (this.debug) { if (this.debug) {
FFMpegHelper.addDebugArguments(command, 50, 120); modules.add(new DebugModule(0, 0));
} }
//Get first streams from the file //Get first streams from the file
SubtitleStream subtitleStream = getNthSubtitleStream(streams, 0); SubtitleStream subtitleStream = getNthSteam(probeResult.getSubtitleStreams(), 0);
VideoStream videoStream = getNthVideoStream(streams, 0); VideoStream videoStream = getNthSteam(probeResult.getVideoStreams(), 0);
AudioStream audioStream = getNthAudioSteam(streams, 0);
if (videoStream == null) { if (videoStream == null) {
throw new IllegalArgumentException("The selected video stream does not exist."); throw new IllegalArgumentException("The selected video stream does not exist.");
} }
//Add streams to output if (subtitleStream != null) {
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream); modules.add(new BurnSubtitleModule(subtitleStream, videoStream, true));
if (audioStream != null) { } else {
FFMpegHelper.addAudioStream(command, audioStream, true); modules.add(new SelectSingleStreamModule(videoStream));
} }
modules.add(new NthAudioStreamModule(probeResult.getAudioStreams(), 0));
command.setOutputFile(outFile); modules.add(new SetOutputFileModule(outFile));
new ModuleExecutor(command, modules).execute();
return command.getResult(); return command.getResult();
} }
} }

View File

@ -0,0 +1,18 @@
package net.knarcraft.ffmpegconverter.converter.module;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import org.jetbrains.annotations.NotNull;
/**
* A module that adds some arguments to a ffmpeg command
*/
public interface ConverterModule {
/**
* Adds this module's arguments to the given command
*
* @param command <p>The command to add to</p>
*/
void addArguments(@NotNull FFMpegCommand command);
}

View File

@ -0,0 +1,36 @@
package net.knarcraft.ffmpegconverter.converter.module;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import org.jetbrains.annotations.NotNull;
/**
* A module for adding options useful for debugging
*/
public class DebugModule implements ConverterModule {
private double startTime = 50;
private double duration = 120;
/**
* Instantiates a new debug module
*
* @param startTime <p>The time to start at</p>
* @param duration <p>The time to stop at</p>
*/
public DebugModule(double startTime, double duration) {
if (startTime > 0) {
this.startTime = startTime;
}
if (duration > 0) {
this.duration = duration;
}
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addInputFileOption("-ss", String.valueOf(this.startTime));
command.addOutputFileOption("-t", String.valueOf(this.duration));
}
}

View File

@ -0,0 +1,36 @@
package net.knarcraft.ffmpegconverter.converter.module;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* An executor for executing a list of modules
*/
public class ModuleExecutor {
private final FFMpegCommand command;
private final List<ConverterModule> modules;
/**
* Instantiates a new module executor
*
* @param command <p>The command to alter</p>
* @param modules <p>The models to execute</p>
*/
public ModuleExecutor(@NotNull FFMpegCommand command, @NotNull List<ConverterModule> modules) {
this.command = command;
this.modules = modules;
}
/**
* Adds arguments for all the specified modules to the FFMpeg command
*/
public void execute() {
for (ConverterModule module : this.modules) {
module.addArguments(this.command);
}
}
}

View File

@ -0,0 +1,29 @@
package net.knarcraft.ffmpegconverter.converter.module.hardwarecoding;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
/**
* A module for setting output as h264, accelerated with Nvidia hardware
*/
public class H264HardwareEncodingModule implements ConverterModule {
private final int quality;
/**
* Instantiates a new h264 hardware encoding module
*
* @param quality <p>The crf quality to use</p>
*/
public H264HardwareEncodingModule(int quality) {
this.quality = quality;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
FFMpegHelper.addH264HardwareEncoding(command, this.quality);
}
}

View File

@ -0,0 +1,29 @@
package net.knarcraft.ffmpegconverter.converter.module.hardwarecoding;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
/**
* A module for setting output as h265 (hevc), accelerated with Nvidia hardware
*/
public class H265HardwareEncodingModule implements ConverterModule {
private final int quality;
/**
* Instantiates a new h265 (hevc) hardware encoding module
*
* @param quality <p>The crf quality to use</p>
*/
public H265HardwareEncodingModule(int quality) {
this.quality = quality;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
FFMpegHelper.addH265HardwareEncoding(command, this.quality);
}
}

View File

@ -0,0 +1,18 @@
package net.knarcraft.ffmpegconverter.converter.module.hardwarecoding;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import org.jetbrains.annotations.NotNull;
/**
* A module for enabling h264 and hevc decoding
*/
public class H26XDecodeModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addInputFileOption("-hwaccel", "cuda");
command.addInputFileOption("-hwaccel_output_format", "cuda");
}
}

View File

@ -0,0 +1,38 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
/**
* A module for adding an audio stream, and converting it to stereo
*/
public class AddStereoAudioStreamModule implements ConverterModule {
private final AudioStream audioStream;
private final boolean mapAudio;
/**
* Instantiates a new add stereo audio stream module
*
* @param audioStream <p>The audio stream to add and convert to stereo</p>
* @param mapAudio <p>Whether to map the given audio stream (only disable if mapped elsewhere)</p>
*/
public AddStereoAudioStreamModule(@NotNull AudioStream audioStream, boolean mapAudio) {
this.audioStream = audioStream;
this.mapAudio = mapAudio;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
if (mapAudio) {
FFMpegHelper.mapStream(command, audioStream);
}
if (audioStream.getChannels() > 2) {
command.addOutputFileOption("-af", "pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
}
}
}

View File

@ -0,0 +1,55 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
/**
* A module for burning the selected subtitle into a video
*/
public class BurnSubtitleModule implements ConverterModule {
private final SubtitleStream subtitleStream;
private final VideoStream videoStream;
private final boolean mapVideo;
/**
* Instantiates a subtitle burning converter
*
* @param subtitleStream <p>The subtitle stream to burn to a video stream</p>
* @param videoStream <p>The video stream to burn into</p>
* @param mapVideo <p>Whether to map the given video stream (only disable if mapped elsewhere)</p>
*/
public BurnSubtitleModule(@NotNull SubtitleStream subtitleStream, @NotNull VideoStream videoStream,
boolean mapVideo) {
this.subtitleStream = subtitleStream;
this.videoStream = videoStream;
this.mapVideo = mapVideo;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
if (mapVideo) {
FFMpegHelper.mapStream(command, videoStream);
}
if (subtitleStream.getIsImageSubtitle()) {
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");
} else {
String safeFileName = FFMpegHelper.escapeSpecialCharactersInFileName(
command.getInputFiles().get(subtitleStream.getInputIndex()));
String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName,
subtitleStream.getRelativeIndex());
command.addOutputFileOption("-vf", subtitleCommand);
}
}
}

View File

@ -0,0 +1,32 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A module for mapping all the given streams
*/
public class MapAllModule<K extends StreamObject> implements ConverterModule {
final List<K> streams;
/**
* Instantiates a new map all module
*
* @param streams <p>The streams to map</p>
*/
public MapAllModule(@NotNull List<K> streams) {
this.streams = streams;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
FFMpegHelper.mapAllStreams(command, this.streams);
}
}

View File

@ -0,0 +1,40 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A module for selecting and mapping the nth audio stream available
*/
public class NthAudioStreamModule implements ConverterModule {
private final int n;
private final List<AudioStream> allStreams;
/**
* Instantiates a new n-th audio stream module
*
* @param allStreams <p>All available streams</p>
* @param n <p>The index of the stream the user wants</p>
*/
public NthAudioStreamModule(@NotNull List<AudioStream> allStreams, int n) {
this.allStreams = allStreams;
this.n = n;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
AudioStream audioStream = FFMpegHelper.getNthSteam(this.allStreams, this.n);
if (audioStream != null) {
FFMpegHelper.mapStream(command, audioStream);
} else {
throw new IllegalArgumentException("Selected audio stream does not exist.");
}
}
}

View File

@ -0,0 +1,39 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A module for selecting and mapping the nth subtitle stream available
*/
public class NthSubtitleStreamModule implements ConverterModule {
private final int n;
private final List<SubtitleStream> allStreams;
/**
* Instantiates a new n-th video stream module
*
* @param allStreams <p>All available streams</p>
* @param n <p>The index of the stream the user wants</p>
*/
public NthSubtitleStreamModule(@NotNull List<SubtitleStream> allStreams, int n) {
this.allStreams = allStreams;
this.n = n;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
SubtitleStream subtitleStream = FFMpegHelper.getNthSteam(this.allStreams, this.n);
if (subtitleStream != null) {
FFMpegHelper.mapStream(command, subtitleStream);
} else {
throw new IllegalArgumentException("Selected subtitle stream does not exist.");
}
}
}

View File

@ -0,0 +1,37 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.VideoStream;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class NthVideoStreamModule implements ConverterModule {
private final int n;
private final List<VideoStream> allStreams;
/**
* Instantiates a new n-th video stream module
*
* @param allStreams <p>All available streams</p>
* @param n <p>The index of the stream the user wants</p>
*/
public NthVideoStreamModule(@NotNull List<VideoStream> allStreams, int n) {
this.allStreams = allStreams;
this.n = n;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
VideoStream videoStream = FFMpegHelper.getNthSteam(this.allStreams, this.n);
if (videoStream != null) {
FFMpegHelper.mapStream(command, videoStream);
} else {
throw new IllegalArgumentException("Selected video stream does not exist.");
}
}
}

View File

@ -0,0 +1,30 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
import org.jetbrains.annotations.NotNull;
/**
* A module for selecting and mapping a single stream
*/
public class SelectSingleStreamModule implements ConverterModule {
private final StreamObject stream;
/**
* Instantiates a new select single stream module
*
* @param stream <p>The stream to map to the output file</p>
*/
public SelectSingleStreamModule(@NotNull StreamObject stream) {
this.stream = stream;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
FFMpegHelper.mapStream(command, stream);
}
}

View File

@ -0,0 +1,44 @@
package net.knarcraft.ffmpegconverter.converter.module.mapping;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A module for selecting one or more audio streams, sorted by language
*/
public class SetDefaultStreamModule<K extends StreamObject> implements ConverterModule {
private final List<K> streams;
private final int defaultStream;
/**
* Instantiates a new language sorted audio stream module
*
* @param streams <p>All input streams</p>
* @param defaultStream <p>The index of the output stream to set as default</p>
*/
public SetDefaultStreamModule(@NotNull List<K> streams, int defaultStream) {
this.streams = streams;
this.defaultStream = defaultStream;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
for (int i = 0; i < streams.size(); i++) {
K stream = streams.get(i);
char defaultModifier;
if (i == defaultStream) {
defaultModifier = '+';
} else {
defaultModifier = '-';
}
command.addOutputFileOption(String.format("-disposition:%s:%d", stream.streamTypeCharacter(), i),
String.format("%sdefault", defaultModifier));
}
}
}

View File

@ -0,0 +1,17 @@
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 making FFMpeg copy all codecs
*/
public class CopyAllModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-c", "copy");
}
}

View File

@ -0,0 +1,17 @@
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 making FFMpeg copy the audio codec
*/
public class CopyAudioModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-c:a", "copy");
}
}

View File

@ -0,0 +1,17 @@
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 making FFMpeg copy the subtitle codec
*/
public class CopySubtitlesModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-c:s", "copy");
}
}

View File

@ -0,0 +1,17 @@
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 adding the fast start flag (immediate playback)
*/
public class FastStartModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-movflags", "+faststart");
}
}

View File

@ -0,0 +1,17 @@
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 subtitle codec to mov_text (necessary for embedding in .mp4 files)
*/
public class MovTextModule implements ConverterModule {
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-c:s", "mov_text");
}
}

View File

@ -0,0 +1,31 @@
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 scaling the resolution of video streams
*/
public class ScaleModule implements ConverterModule {
final int newWidth;
final int newHeight;
/**
* Instantiates a new scale module
*
* @param newWidth <p>The new width of the video stream</p>
* @param newHeight <p>The new height of the video stream</p>
*/
public ScaleModule(int newWidth, int newHeight) {
this.newWidth = newWidth;
this.newHeight = newHeight;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-vf", "scale=" + this.newWidth + ":" + this.newHeight);
}
}

View File

@ -0,0 +1,32 @@
package net.knarcraft.ffmpegconverter.converter.module.output;
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
import net.knarcraft.ffmpegconverter.converter.module.ConverterModule;
import net.knarcraft.ffmpegconverter.utility.FileUtil;
import org.jetbrains.annotations.NotNull;
/**
* A module for setting the output file
*/
public class SetOutputFileModule implements ConverterModule {
private final String outputFile;
/**
* Instantiates a new set output file module
*
* @param outputFile <p>The output file to set</p>
*/
public SetOutputFileModule(@NotNull String outputFile) {
this.outputFile = outputFile;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.setOutputFile(this.outputFile);
if (FileUtil.getExtension(this.outputFile).equals("mkv")) {
command.addOutputFileOption("-f", "matroska");
}
}
}

View File

@ -0,0 +1,32 @@
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 output quality
*/
public class SetQualityModule implements ConverterModule {
private final int crf;
private final String preset;
/**
* Instantiates a new quality module
*
* @param crf <p>The CRF to set. 0 = lossless, 51 = terrible, 17 is visually lossless</p>
* @param preset <p>The preset to use (p1-p7, p7 is slowest and best)</p>
*/
public SetQualityModule(int crf, @NotNull String preset) {
this.crf = crf;
this.preset = preset;
}
@Override
public void addArguments(@NotNull FFMpegCommand command) {
command.addOutputFileOption("-crf", String.valueOf(crf));
command.addOutputFileOption("-preset", this.preset);
}
}

View File

@ -0,0 +1,89 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* An abstract stream sorter, making implementation easier
*
* @param <L> <p>The type of streams this sorter sorts</p>
*/
public abstract class AbstractSorter<L extends StreamObject> implements StreamSorter<L> {
protected StreamSorter<L> nextSorter = null;
@Override
public @NotNull StreamSorter<L> prepend(@NotNull StreamSorter<L> other) {
this.nextSorter = other;
return this;
}
@Override
public @NotNull StreamSorter<L> append(@NotNull StreamSorter<L> other) {
StreamSorter<L> end = other;
while (end != null && end.hasChainElement()) {
end = end.getNextInChain();
}
if (end == null) {
throw new IllegalStateException("Other cannot be null. Something is wrong!");
}
end.setNextInChain(this);
return other;
}
@Override
public @NotNull List<L> chainSort(@NotNull List<L> input) {
List<L> sorted = this.sort(input);
if (nextSorter != null) {
return nextSorter.chainSort(sorted);
} else {
return sorted;
}
}
@Override
public boolean hasChainElement() {
return this.nextSorter != null;
}
@Override
@Nullable
public StreamSorter<L> getNextInChain() {
return this.nextSorter;
}
@Override
public void setNextInChain(@NotNull StreamSorter<L> next) {
this.nextSorter = next;
}
/**
* Sorts subtitle streams according to chosen languages and removes non-matching languages
*
* @param streams <p>A list of streams to sort.</p>
* @param languages <p>The languages chosen by the user.</p>
* @param <G> <p>The type of streams to sort.</p>
* @return <p>A sorted version of the list.</p>
*/
@NotNull
protected <G extends StreamObject> List<G> sortStreamsByLanguage(@NotNull List<G> streams,
@NotNull String[] languages) {
List<G> sorted = new ArrayList<>();
for (String language : languages) {
for (G stream : streams) {
String streamLanguage = stream.getLanguage();
if (language.equals("*") || (streamLanguage.equals("und") && language.equals("0")) ||
streamLanguage.equals(language)) {
sorted.add(stream);
}
}
streams.removeAll(sorted);
}
return sorted;
}
}

View File

@ -0,0 +1,29 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.streams.AudioStream;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A sorter for sorting audio streams by language
*/
public class AudioLanguageSorter extends AbstractSorter<AudioStream> {
private final String[] languageOrder;
/**
* Instantiates a new audio language sorter
*
* @param languageOrder <p>The order of preference for audio languages</p>
*/
public AudioLanguageSorter(@NotNull String[] languageOrder) {
this.languageOrder = languageOrder;
}
@Override
public @NotNull List<AudioStream> sort(@NotNull List<AudioStream> input) {
return sortStreamsByLanguage(input, this.languageOrder);
}
}

View File

@ -0,0 +1,67 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.property.MinimalSubtitlePreference;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A sorter for sorting/filtering subtitles by a minimal subtitle preference
*/
public class MinimalSubtitleSorter extends AbstractSorter<SubtitleStream> {
private final MinimalSubtitlePreference minimalSubtitlePreference;
/**
* Instantiates a new minimal subtitle preference sorter
*
* @param minimalSubtitlePreference <p>The minimal subtitle preference sort/filter by</p>
*/
public MinimalSubtitleSorter(@NotNull MinimalSubtitlePreference minimalSubtitlePreference) {
this.minimalSubtitlePreference = minimalSubtitlePreference;
}
@Override
public @NotNull List<SubtitleStream> sort(@NotNull List<SubtitleStream> input) {
// Split all subtitles into full and minimal
List<SubtitleStream> fullSubtitles = new ArrayList<>();
List<SubtitleStream> minimalSubtitles = new ArrayList<>();
for (SubtitleStream subtitleStream : input) {
if (subtitleStream.getIsFullSubtitle()) {
fullSubtitles.add(subtitleStream);
} else {
minimalSubtitles.add(subtitleStream);
}
}
// Sort/filter subtitles based on full and minimal
switch (this.minimalSubtitlePreference) {
case REJECT -> {
// Only return full subtitles
return fullSubtitles;
}
case REQUIRE -> {
// Only return minimal subtitles
return minimalSubtitles;
}
case NO_PREFERENCE -> {
// Don't change order
return input;
}
case PREFER -> {
// Sort minimal subtitles first, and full subtitles last
minimalSubtitles.addAll(fullSubtitles);
return minimalSubtitles;
}
case AVOID -> {
// Sort full subtitles first, and minimal subtitles last
fullSubtitles.addAll(minimalSubtitles);
return fullSubtitles;
}
default -> throw new IllegalStateException("Unknown enum value encountered");
}
}
}

View File

@ -0,0 +1,79 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.streams.StreamObject;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
/**
* An interface describing a chaining-capable stream sorter
*
* @param <K> <p>The type of stream this sorter sorts</p>
*/
@SuppressWarnings("unused")
public interface StreamSorter<K extends StreamObject> {
/**
* Prepends this stream sorter to another
*
* <p>This stream sorter's next in chain will be set to other</p>
*
* @param other <p>The stream sorter to prepend</p>
* @return <p>A reference to the first stream sorter in the current chain</p>
*/
@NotNull
StreamSorter<K> prepend(@NotNull StreamSorter<K> other);
/**
* Appends this stream sorter to another
*
* <p>This stream sorter is added to the end of other's chain</p>
*
* @param other <p>The stream sorter to append to this one</p>
* @return <p>A reference to the first stream sorter in the current chain</p>
*/
@NotNull
StreamSorter<K> append(@NotNull StreamSorter<K> other);
/**
* Sorts the given input streams using this sorter only
*
* @param input <p>The input to sort</p>
* @return <p>The sorted input</p>
*/
@NotNull
List<K> sort(@NotNull List<K> input);
/**
* Sorts the given input streams using all sorters in the chain
*
* @param input <p>The input to sort</p>
* @return <p>The sorted input</p>
*/
@NotNull
List<K> chainSort(@NotNull List<K> input);
/**
* Gets whether this stream sorter has a sorter set as the next in its chain
*
* @return <p>True if a next chain item exists</p>
*/
boolean hasChainElement();
/**
* Gets the next item in this stream sorter's chain
*
* @return <p>The next item in the chain</p>
*/
@Nullable
StreamSorter<K> getNextInChain();
/**
* Sets the given stream sorter as the next in the sorter chain
*
* @param next <p>The next item in the sorter chain</p>
*/
void setNextInChain(@NotNull StreamSorter<K> next);
}

View File

@ -0,0 +1,30 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A sorter for sorting subtitles by language
*/
public class SubtitleLanguageSorter extends AbstractSorter<SubtitleStream> {
private final String[] languageOrder;
/**
* Instantiates a new subtitle language sorter
*
* @param languageOrder <p>The order of preference for subtitle languages</p>
*/
public SubtitleLanguageSorter(@NotNull String[] languageOrder) {
this.languageOrder = languageOrder;
}
@Override
public @NotNull List<SubtitleStream> sort(@NotNull List<SubtitleStream> input) {
return sortStreamsByLanguage(new ArrayList<>(input), this.languageOrder);
}
}

View File

@ -0,0 +1,69 @@
package net.knarcraft.ffmpegconverter.converter.sorter;
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
/**
* A sorter for filtering subtitle streams by title
*/
public class SubtitleTitleSorter extends AbstractSorter<SubtitleStream> {
private final String titleFilter;
/**
* Instantiates a new subtitle title sorter
*
* <p>If a simple string, or invalid RegEx is given, any stream containing the titleFilter in its title will be
* retained. If a valid RegEx is given, any stream matching the titleFilter is retained.</p>
*
* @param titleFilter <p>The filter to use. RegEx match, or a string the title must contain.</p>
*/
public SubtitleTitleSorter(@NotNull String titleFilter) {
this.titleFilter = titleFilter;
}
@Override
public @NotNull List<SubtitleStream> sort(@NotNull List<SubtitleStream> input) {
List<SubtitleStream> output = new ArrayList<>(input);
if (!this.titleFilter.trim().isEmpty()) {
if (isValidRegularExpression(this.titleFilter) && hasSpecialRegexCharacters(this.titleFilter)) {
output.removeIf((stream) -> !stream.getTitle().matches(this.titleFilter));
} else {
output.removeIf((stream) -> !stream.getTitle().contains(this.titleFilter));
}
}
return output;
}
/**
* Checks whether the given string is a valid regular expression
*
* @param input <p>The string to check</p>
* @return <p>True if the given string has no invalid expressions</p>
*/
private static boolean isValidRegularExpression(@NotNull String input) {
try {
Pattern.compile(input);
return true;
} catch (PatternSyntaxException e) {
return false;
}
}
/**
* Checks whether the input string has any RegEx special characters
*
* @param input <p>The input to check</p>
* @return <p>True if RegEx characters exist in the string</p>
*/
private static boolean hasSpecialRegexCharacters(String input) {
Pattern regexSpecialCharacters = Pattern.compile("[\\\\.\\[\\]{}()<>*+\\-=!?^$|]");
return regexSpecialCharacters.matcher(input).find();
}
}

View File

@ -0,0 +1,38 @@
package net.knarcraft.ffmpegconverter.property;
/**
* A representation for different preferences related to minimal subtitles
*
* <p>Minimal subtitles are also referred to as partial subtitles or signs and songs. For Japanese media, they are aimed
* at users that understand the spoken language, but struggles with reading, or when things are said too fast or in an
* odd rhythm (singing). In american movies, some partial subtitles only translate non-english spoken language. Some
* dubbed movies have subtitles that only cover signs, text or logos in the original language.</p>
*/
public enum MinimalSubtitlePreference {
/**
* Only map minimal subtitles
*/
REQUIRE,
/**
* Prefer minimal subtitles when available
*/
PREFER,
/**
* Don't do any changes in sorting based on minimal subtitles
*/
NO_PREFERENCE,
/**
* Avoid minimal subtitles, unless it's the only available choice
*/
AVOID,
/**
* Don't include minimal subtitles, no matter what
*/
REJECT,
}

View File

@ -34,8 +34,8 @@ public class AudioStream extends AbstractStream implements StreamObject {
} }
@Override @Override
public @NotNull StreamType getStreamType() { public char streamTypeCharacter() {
return StreamType.AUDIO; return 'a';
} }
} }

View File

@ -53,14 +53,6 @@ public interface StreamObject {
*/ */
boolean isDefault(); boolean isDefault();
/**
* Gets the stream type of this stream object
*
* @return <p>The stream type for this stream</p>
*/
@NotNull
StreamType getStreamType();
/** /**
* Gets the title of the subtitle stream * Gets the title of the subtitle stream
* *
@ -69,4 +61,11 @@ public interface StreamObject {
@NotNull @NotNull
String getTitle(); String getTitle();
/**
* Gets the character ffmpeg uses for this type of stream
*
* @return <p>The character used to specify this type of stream</p>
*/
char streamTypeCharacter();
} }

View File

@ -4,9 +4,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* A class for representing stream tags that might be found for streams in video files * A class for representing stream tags that might be found for streams in video files
@ -15,181 +13,461 @@ public enum StreamTag {
/** /**
* The absolute index of this stream in the file * The absolute index of this stream in the file
*
* <p>Applicable for all 3 stream types</p>
*/ */
INDEX("index", StreamType.ALL), INDEX("index"),
/** /**
* The name of the codec, useful for identification * The name of the codec, useful for identification
*
* <p>Applicable for all 3 stream types</p>
*/ */
CODEC_NAME("codec_name=", StreamType.ALL), CODEC_NAME("codec_name="),
/** /**
* The long name of the codec, useful for displaying information * The long name of the codec, useful for displaying information
*
* <p>Applicable for all 3 stream types</p>
*/ */
CODEC_LONG_NAME("codec_long_name", StreamType.ALL), CODEC_LONG_NAME("codec_long_name"),
/** /**
* The profile the encoder is set to * The profile the encoder is set to
*
* <p>Applicable for all 3 stream types</p>
*/ */
PROFILE("profile", StreamType.ALL), PROFILE("profile"),
/** /**
* Whether the type of codec for the stream is audio, video or subtitle * Whether the type of codec for the stream is audio, video or subtitle
*
* <p>Applicable for all 3 stream types</p>
*/ */
CODEC_TYPE("codec_type", StreamType.ALL), CODEC_TYPE("codec_type"),
CODEC_TAG_STRING("codec_tag_string", StreamType.ALL),
CODEC_TAG("codec_tag", StreamType.ALL), /**
SAMPLE_FMT("sample_fmt", Set.of(StreamType.AUDIO)), * <p>Applicable for all 3 stream types</p>
SAMPLE_RATE("sample_rate", Set.of(StreamType.AUDIO)), */
CODEC_TAG_STRING("codec_tag_string"),
/**
* <p>Applicable for all 3 stream types</p>
*/
CODEC_TAG("codec_tag"),
/**
* <p>Applicable for audio streams</p>
*/
SAMPLE_FMT("sample_fmt"),
/**
* <p>Applicable for audio streams</p>
*/
SAMPLE_RATE("sample_rate"),
/** /**
* The number of channels in an audio stream * The number of channels in an audio stream
*
* <p>Applicable for audio streams</p>
*/ */
CHANNELS("channels", Set.of(StreamType.AUDIO)), CHANNELS("channels"),
/** /**
* Human-recognizable term for the number of audio channels, such as stereo, mono or surround * Human-recognizable term for the number of audio channels, such as stereo, mono or surround
*
* <p>Applicable for audio streams</p>
*/ */
CHANNEL_LAYOUT("channel_layout", Set.of(StreamType.AUDIO)), CHANNEL_LAYOUT("channel_layout"),
BITS_PER_SAMPLE("bits_per_sample", Set.of(StreamType.AUDIO)),
INITIAL_PADDING("initial_padding", Set.of(StreamType.AUDIO)), /**
* <p>Applicable for audio streams</p>
*/
BITS_PER_SAMPLE("bits_per_sample"),
/**
* <p>Applicable for audio streams</p>
*/
INITIAL_PADDING("initial_padding"),
/** /**
* The viewable video width * The viewable video width
*
* <p>Applicable for video and subtitle streams</p>
*/ */
WIDTH("width", StreamType.VIDEO_SUBTITLE), WIDTH("width"),
/** /**
* The viewable video height * The viewable video height
*
* <p>Applicable for video and subtitle streams</p>
*/ */
HEIGHT("height", StreamType.VIDEO_SUBTITLE), HEIGHT("height"),
/** /**
* The original video width, before any padding was applied to account for resolution multiples * The original video width, before any padding was applied to account for resolution multiples
*
* <p>Applicable for video streams</p>
*/ */
CODED_WIDTH("coded_width", Set.of(StreamType.VIDEO)), CODED_WIDTH("coded_width"),
/** /**
* The original video height, before any padding was applied to account for resolution multiples * The original video height, before any padding was applied to account for resolution multiples
*
* <p>Applicable for video streams</p>
*/ */
CODED_HEIGHT("coded_height", Set.of(StreamType.VIDEO)), CODED_HEIGHT("coded_height"),
CLOSED_CAPTIONS("closed_captions", Set.of(StreamType.VIDEO)),
FILM_GRAIN("film_grain", Set.of(StreamType.VIDEO)), /**
HAS_B_FRAMES("has_b_frames", Set.of(StreamType.VIDEO)), * <p>Applicable for video streams</p>
*/
CLOSED_CAPTIONS("closed_captions"),
/**
* <p>Applicable for video streams</p>
*/
FILM_GRAIN("film_grain"),
/**
* <p>Applicable for video streams</p>
*/
HAS_B_FRAMES("has_b_frames"),
/** /**
* The aspect ratio used to stretch the video for playback * The aspect ratio used to stretch the video for playback
*
* <p>Applicable for video streams</p>
*/ */
SAMPLE_ASPECT_RATIO("sample_aspect_ratio", Set.of(StreamType.VIDEO)), SAMPLE_ASPECT_RATIO("sample_aspect_ratio"),
/** /**
* The aspect ratio of the video stream * The aspect ratio of the video stream
*
* <p>Applicable for video streams</p>
*/ */
DISPLAY_ASPECT_RATIO("display_aspect_ratio", Set.of(StreamType.VIDEO)), DISPLAY_ASPECT_RATIO("display_aspect_ratio"),
/** /**
* The pixel format used for the video stream * The pixel format used for the video stream
*
* <p>Applicable for video streams</p>
*/ */
PIX_FMT("pix_fmt", Set.of(StreamType.VIDEO)), PIX_FMT("pix_fmt"),
/** /**
* The quality level of the video stream * The quality level of the video stream
*
* <p>Applicable for video streams</p>
*/ */
LEVEL("level", Set.of(StreamType.VIDEO)), LEVEL("level"),
COLOR_RANGE("color_range", Set.of(StreamType.VIDEO)),
/**
* <p>Applicable for video streams</p>
*/
COLOR_RANGE("color_range"),
/** /**
* How colors are stored in the video stream's file * How colors are stored in the video stream's file
*
* <p>Applicable for video streams</p>
*/ */
COLOR_SPACE("color_space", Set.of(StreamType.VIDEO)), COLOR_SPACE("color_space"),
COLOR_TRANSFER("color_transfer", Set.of(StreamType.VIDEO)),
COLOR_PRIMARIES("color_primaries", Set.of(StreamType.VIDEO)), /**
CHROMA_LOCATION("chroma_location", Set.of(StreamType.VIDEO)), * <p>Applicable for video streams</p>
FIELD_ORDER("field_order", Set.of(StreamType.VIDEO)), */
REFS("refs", Set.of(StreamType.VIDEO)), COLOR_TRANSFER("color_transfer"),
IS_AVC("is_avc", Set.of(StreamType.VIDEO)),
NAL_LENGTH_SIZE("nal_length_size", Set.of(StreamType.VIDEO)), /**
ID("id", StreamType.ALL), * <p>Applicable for video streams</p>
R_FRAME_RATE("r_frame_rate", StreamType.ALL), */
AVERAGE_FRAME_RATE("avg_frame_rate", StreamType.ALL), COLOR_PRIMARIES("color_primaries"),
TIME_BASE("time_base", StreamType.ALL),
START_PTS("start_pts", StreamType.ALL), /**
START_TIME("start_time", StreamType.ALL), * <p>Applicable for video streams</p>
DURATION_TS("duration_ts", StreamType.ALL), */
DURATION("duration", StreamType.ALL), CHROMA_LOCATION("chroma_location"),
BIT_RATE("bit_rate", StreamType.ALL),
MAX_BIT_RATE("max_bit_rate", StreamType.ALL), /**
BITS_PER_RAW_SAMPLE("bits_per_raw_sample", StreamType.ALL), * <p>Applicable for video streams</p>
NB_FRAMES("nb_frames", StreamType.ALL), */
NB_READ_FRAMES("nb_read_frames", StreamType.ALL), FIELD_ORDER("field_order"),
NB_READ_PACKETS("nb_read_packets", StreamType.ALL),
EXTRA_DATA_SIZE("extradata_size", StreamType.VIDEO_SUBTITLE), /**
DISPOSITION_DEFAULT("DISPOSITION:default", StreamType.ALL), * <p>Applicable for video streams</p>
DISPOSITION_DUB("DISPOSITION:dub", StreamType.ALL), */
DISPOSITION_ORIGINAL("DISPOSITION:original", StreamType.ALL), REFS("refs"),
DISPOSITION_COMMENT("DISPOSITION:comment", StreamType.ALL),
DISPOSITION_LYRICS("DISPOSITION:lyrics", StreamType.ALL), /**
DISPOSITION_KARAOKE("DISPOSITION:karaoke", StreamType.ALL), * <p>Applicable for video streams</p>
DISPOSITION_FORCED("DISPOSITION:forced", StreamType.ALL), */
DISPOSITION_HEARING_IMPAIRED("DISPOSITION:hearing_impaired", StreamType.ALL), IS_AVC("is_avc"),
DISPOSITION_VISUAL_IMPAIRED("DISPOSITION:visual_impaired", StreamType.ALL),
DISPOSITION_CLEAN_EFFECTS("DISPOSITION:clean_effects", StreamType.ALL), /**
DISPOSITION_ATTACHED_PIC("DISPOSITION:attached_pic", StreamType.ALL), * <p>Applicable for video streams</p>
DISPOSITION_TIMED_THUMBNAILS("DISPOSITION:timed_thumbnails", StreamType.ALL), */
DISPOSITION_NON_DIEGETIC("DISPOSITION:non_diegetic", StreamType.ALL), NAL_LENGTH_SIZE("nal_length_size"),
DISPOSITION_CAPTIONS("DISPOSITION:captions", StreamType.ALL),
DISPOSITION_DESCRIPTIONS("DISPOSITION:descriptions", StreamType.ALL), /**
DISPOSITION_METADATA("DISPOSITION:metadata", StreamType.ALL), * <p>Applicable for all 3 stream types</p>
DISPOSITION_DEPENDENT("DISPOSITION:dependent", StreamType.ALL), */
DISPOSITION_STILL_IMAGE("DISPOSITION:still_image", StreamType.ALL), ID("id"),
/**
* <p>Applicable for all 3 stream types</p>
*/
R_FRAME_RATE("r_frame_rate"),
/**
* <p>Applicable for all 3 stream types</p>
*/
AVERAGE_FRAME_RATE("avg_frame_rate"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TIME_BASE("time_base"),
/**
* <p>Applicable for all 3 stream types</p>
*/
START_PTS("start_pts"),
/**
* <p>Applicable for all 3 stream types</p>
*/
START_TIME("start_time"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DURATION_TS("duration_ts"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DURATION("duration"),
/**
* <p>Applicable for all 3 stream types</p>
*/
BIT_RATE("bit_rate"),
/**
* <p>Applicable for all 3 stream types</p>
*/
MAX_BIT_RATE("max_bit_rate"),
/**
* <p>Applicable for all 3 stream types</p>
*/
BITS_PER_RAW_SAMPLE("bits_per_raw_sample"),
/**
* <p>Applicable for all 3 stream types</p>
*/
NB_FRAMES("nb_frames"),
/**
* <p>Applicable for all 3 stream types</p>
*/
NB_READ_FRAMES("nb_read_frames"),
/**
* <p>Applicable for all 3 stream types</p>
*/
NB_READ_PACKETS("nb_read_packets"),
/**
* <p>Applicable for video and subtitle streams</p>
*/
EXTRA_DATA_SIZE("extradata_size"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_DEFAULT("DISPOSITION:default"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_DUB("DISPOSITION:dub"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_ORIGINAL("DISPOSITION:original"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_COMMENT("DISPOSITION:comment"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_LYRICS("DISPOSITION:lyrics"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_KARAOKE("DISPOSITION:karaoke"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_FORCED("DISPOSITION:forced"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_HEARING_IMPAIRED("DISPOSITION:hearing_impaired"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_VISUAL_IMPAIRED("DISPOSITION:visual_impaired"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_CLEAN_EFFECTS("DISPOSITION:clean_effects"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_ATTACHED_PIC("DISPOSITION:attached_pic"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_TIMED_THUMBNAILS("DISPOSITION:timed_thumbnails"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_NON_DIEGETIC("DISPOSITION:non_diegetic"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_CAPTIONS("DISPOSITION:captions"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_DESCRIPTIONS("DISPOSITION:descriptions"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_METADATA("DISPOSITION:metadata"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_DEPENDENT("DISPOSITION:dependent"),
/**
* <p>Applicable for all 3 stream types</p>
*/
DISPOSITION_STILL_IMAGE("DISPOSITION:still_image"),
/** /**
* The language of the stream * The language of the stream
*
* <p>Applicable for all 3 stream types</p>
*/ */
TAG_LANGUAGE("TAG:language", StreamType.ALL), TAG_LANGUAGE("TAG:language"),
/** /**
* The title of an audio stream * The title of an audio stream
*
* <p>Applicable for all 3 stream types</p>
*/ */
TAG_TITLE("TAG:title", StreamType.ALL), TAG_TITLE("TAG:title"),
TAG_BPS_ENG("TAG:BPS-eng", StreamType.ALL),
TAG_DURATION_ENG("TAG:DURATION-eng", StreamType.ALL), /**
TAG_NUMBER_OF_FRAMES_ENG("TAG:NUMBER_OF_FRAMES-eng", StreamType.ALL), * <p>Applicable for all 3 stream types</p>
TAG_NUMBER_OF_BYTES_ENG("TAG:NUMBER_OF_BYTES-eng", StreamType.ALL), */
TAG_SOURCE_ID_ENG("TAG:SOURCE_ID-eng", StreamType.ALL), TAG_BPS_ENG("TAG:BPS-eng"),
TAG_SOURCE_ID("TAG:SOURCE_ID", StreamType.ALL),
TAG_STATISTICS_WRITING_APP_ENG("TAG:_STATISTICS_WRITING_APP-eng", StreamType.ALL), /**
TAG_STATISTICS_WRITING_APP("TAG:_STATISTICS_WRITING_APP", StreamType.ALL), * <p>Applicable for all 3 stream types</p>
TAG_STATISTICS_WRITING_DATE_UTC_ENG("TAG:_STATISTICS_WRITING_DATE_UTC-eng", StreamType.ALL), */
TAG_STATISTICS_WRITING_DATE_UTC("TAG:_STATISTICS_WRITING_DATE_UTC", StreamType.ALL), TAG_DURATION_ENG("TAG:DURATION-eng"),
TAG_STATISTICS_TAGS_ENG("TAG:_STATISTICS_TAGS-eng", StreamType.ALL),
TAG_STATISTICS_TAGS("TAG:_STATISTICS_TAGS", StreamType.ALL), /**
TAG_ENCODER("TAG:ENCODER", Set.of(StreamType.VIDEO)), * <p>Applicable for all 3 stream types</p>
TAG_DURATION("TAG:DURATION", StreamType.ALL), */
TAG_NUMBER_OF_FRAMES_ENG("TAG:NUMBER_OF_FRAMES-eng"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_NUMBER_OF_BYTES_ENG("TAG:NUMBER_OF_BYTES-eng"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_SOURCE_ID_ENG("TAG:SOURCE_ID-eng"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_SOURCE_ID("TAG:SOURCE_ID"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_STATISTICS_WRITING_APP_ENG("TAG:_STATISTICS_WRITING_APP-eng"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_STATISTICS_WRITING_APP("TAG:_STATISTICS_WRITING_APP"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_STATISTICS_WRITING_DATE_UTC_ENG("TAG:_STATISTICS_WRITING_DATE_UTC-eng"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_STATISTICS_WRITING_DATE_UTC("TAG:_STATISTICS_WRITING_DATE_UTC"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_STATISTICS_TAGS_ENG("TAG:_STATISTICS_TAGS-eng"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_STATISTICS_TAGS("TAG:_STATISTICS_TAGS"),
/**
* <p>Applicable for video streams</p>
*/
TAG_ENCODER("TAG:ENCODER"),
/**
* <p>Applicable for all 3 stream types</p>
*/
TAG_DURATION("TAG:DURATION"),
; ;
private static final Map<String, StreamTag> tagLookup = new HashMap<>(); private static final Map<String, StreamTag> tagLookup = new HashMap<>();
private final @NotNull String tagString; private final @NotNull String tagString;
private final Set<StreamType> applicableFor;
/** /**
* Instantiates a new stream tag * Instantiates a new stream tag
* *
* @param tagString <p>The tag string ffmpeg prints to specify this tag</p> * @param tagString <p>The tag string ffmpeg prints to specify this tag</p>
*/ */
StreamTag(@NotNull String tagString, @NotNull Set<StreamType> applicableFor) { StreamTag(@NotNull String tagString) {
this.tagString = tagString; this.tagString = tagString;
this.applicableFor = applicableFor;
}
/**
* Gets the types of streams this tag is applicable for
*
* @return <p>The types of streams this tag is applicable for</p>
*/
@NotNull
public Set<StreamType> getApplicableFor() {
return new HashSet<>(this.applicableFor);
} }
/** /**

View File

@ -1,36 +0,0 @@
package net.knarcraft.ffmpegconverter.streams;
import java.util.Set;
/**
* A specifier for a type of stream
*/
public enum StreamType {
/**
* A video stream
*/
VIDEO,
/**
* An audio stream
*/
AUDIO,
/**
* A subtitle stream
*/
SUBTITLE,
;
/**
* A set of all stream types
*/
public static final Set<StreamType> ALL = Set.of(VIDEO, AUDIO, SUBTITLE);
/**
* A set of the video and subtitle stream types
*/
public static final Set<StreamType> VIDEO_SUBTITLE = Set.of(VIDEO, SUBTITLE);
}

View File

@ -61,8 +61,8 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
} }
@Override @Override
public @NotNull StreamType getStreamType() { public char streamTypeCharacter() {
return StreamType.SUBTITLE; return 's';
} }
} }

View File

@ -45,8 +45,8 @@ public class VideoStream extends AbstractStream implements StreamObject {
} }
@Override @Override
public @NotNull StreamType getStreamType() { public char streamTypeCharacter() {
return StreamType.VIDEO; return 'v';
} }
} }

View File

@ -162,61 +162,6 @@ public final class FFMpegHelper {
return output.toString(); return output.toString();
} }
/**
* Maps an audio track to a ffmpeg command's output
*
* @param command <p>The command to add the audio track to</p>
* @param audioStream <p>The audio stream to be mapped</p>
* @param toStereo <p>Whether to convert the audio stream to stereo</p>
*/
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");
}
}
/**
* Adds subtitles and video mapping to a command
*
* @param command <p>The list containing the rest of the command.</p>
* @param subtitleStream <p>The subtitle stream to be used.</p>
* @param videoStream <p>The video stream to be used.</p>
*/
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) {
mapStream(command, videoStream);
return;
}
//Add the correct command arguments depending on the subtitle type
if (!subtitleStream.getIsImageSubtitle()) {
addBurnedInSubtitle(command, subtitleStream, videoStream);
} else {
addBurnedInImageSubtitle(command, subtitleStream, videoStream);
}
}
/**
* Adds subtitle commands to a command list
*
* @param command <p>The list containing the FFmpeg commands.</p>
* @param subtitleStream <p>The subtitle stream to add.</p>
* @param videoStream <p>The video stream to burn the subtitle into.</p>
*/
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.addOutputFileOption("-vf", subtitleCommand);
}
/** /**
* Adds arguments for converting a file to h264 using hardware acceleration * Adds arguments for converting a file to h264 using hardware acceleration
* *
@ -259,8 +204,9 @@ public final class FFMpegHelper {
* *
* @param command <p>The command to add the mappings to</p> * @param command <p>The command to add the mappings to</p>
* @param streams <p>The streams to map</p> * @param streams <p>The streams to map</p>
* @param <K> <p>The type of stream object to map</p>
*/ */
public static void mapAllStreams(@NotNull FFMpegCommand command, @NotNull List<StreamObject> streams) { public static <K extends StreamObject> void mapAllStreams(@NotNull FFMpegCommand command, @NotNull List<K> streams) {
for (StreamObject stream : streams) { for (StreamObject stream : streams) {
mapStream(command, stream); mapStream(command, stream);
} }
@ -283,7 +229,7 @@ public final class FFMpegHelper {
* @param fileName <p>The filename to escape.</p> * @param fileName <p>The filename to escape.</p>
* @return <p>A filename with known special characters escaped.</p> * @return <p>A filename with known special characters escaped.</p>
*/ */
private static String escapeSpecialCharactersInFileName(String fileName) { public static String escapeSpecialCharactersInFileName(String fileName) {
return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\") return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\")
.replaceAll("'", "'\\\\\\\\\\\\\''") .replaceAll("'", "'\\\\\\\\\\\\\''")
.replaceAll("%", "\\\\\\\\\\\\%") .replaceAll("%", "\\\\\\\\\\\\%")
@ -293,21 +239,23 @@ public final class FFMpegHelper {
} }
/** /**
* Adds external image subtitle commands to a command list * Gets the nth stream from a list of streams
* *
* @param command <p>The FFMPEG command to modify</p> * @param streams <p>A list of streams</p>
* @param subtitleStream <p>The external image subtitle stream to burn in</p> * @param n <p>The index of the audio stream to get</p>
* @param videoStream <p>The video stream to burn the subtitle into</p> * @return <p>The first audio stream found, or null if no audio streams were found</p>
*/ */
private static void addBurnedInImageSubtitle(@NotNull FFMpegCommand command, public static <G extends StreamObject> G getNthSteam(@NotNull List<G> streams, int n) {
@NotNull SubtitleStream subtitleStream, if (n < 0) {
@NotNull VideoStream videoStream) { throw new IllegalArgumentException("N cannot be negative!");
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", G stream = null;
subtitleStream.getInputIndex(), subtitleStream.getAbsoluteIndex(), videoStream.getWidth(), if (streams.size() > n) {
videoStream.getHeight(), videoStream.getWidth(), videoStream.getHeight(), stream = streams.get(n);
videoStream.getInputIndex(), videoStream.getAbsoluteIndex())); } else if (!streams.isEmpty()) {
command.addOutputFileOption("-profile:v", "baseline"); stream = streams.get(0);
}
return stream;
} }
/** /**

View File

@ -1,8 +1,5 @@
package net.knarcraft.ffmpegconverter.utility; package net.knarcraft.ffmpegconverter.utility;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;

View File

@ -1,14 +0,0 @@
package net.knarcraft.ffmpegconverter.converter;
import org.junit.Before;
public class AnimeConverterTest {
@Before
public void setUp() {
new AnimeConverter("ffprobe", "ffmpeg",
new String[]{"jpn", "eng", "nor", "swe"}, new String[]{"nor", "eng", "swe", "fin"}, false,
false, -1, -1, "");
}
}