diff --git a/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java
new file mode 100644
index 0000000..92b0889
--- /dev/null
+++ b/src/main/java/net/knarcraft/ffmpegconverter/converter/AbstractConverter.java
@@ -0,0 +1,212 @@
+package net.knarcraft.ffmpegconverter.converter;
+
+import net.knarcraft.ffmpegconverter.streams.AudioStream;
+import net.knarcraft.ffmpegconverter.streams.StreamObject;
+import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
+import net.knarcraft.ffmpegconverter.streams.VideoStream;
+import net.knarcraft.ffmpegconverter.utility.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Implements all methods which can be useful for any implementation of a converter.
+ */
+public abstract class AbstractConverter implements Converter {
+ String ffprobePath;
+ String ffmpegPath;
+ final boolean DEBUG = false;
+ String[] audioFormats;
+ String[] videoFormats;
+
+ /**
+ * Initializes variables used by the abstract converter
+ */
+ AbstractConverter() {
+ try {
+ audioFormats = FileUtil.readFileLines("audio_formats.txt");
+ videoFormats = FileUtil.readFileLines("video_formats.txt");
+ } catch (IOException e) {
+ e.printStackTrace();
+ System.exit(1);
+ }
+ }
+
+ /**
+ * Gets all valid input formats for the converter
+ * @return
A list of valid input formats
+ */
+ public abstract String[] getValidFormats();
+
+ /**
+ * Filters parsed streams into one of the stream types
+ * @param streams A list of stream objects.
+ * @param codecType The codec type of the streams to select.
+ * @param The correct object type for the streams with the selected codec type.
+ * @return A potentially shorter list of streams.
+ */
+ @SuppressWarnings("unchecked")
+ static List filterStreamsByType(List streams, String codecType) {
+ Iterator i = streams.iterator();
+ List newStreams = new ArrayList<>();
+ while (i.hasNext()) {
+ StreamObject next = i.next();
+ if (next.getCodecType().equals(codecType)) {
+ newStreams.add((G) next);
+ }
+ }
+ return newStreams;
+ }
+
+ /**
+ * Filters and sorts audio streams according to chosen languages
+ * @param audioStreams A list of audio streams.
+ * @param audioLanguages A list of languages.
+ * @return A list containing just audio tracks of chosen languages, sorted in order of languages.
+ */
+ static List filterAudioStreams(List audioStreams, String[] audioLanguages) {
+ List filtered = new ArrayList<>();
+ for (String language : audioLanguages) {
+ for (AudioStream stream : audioStreams) {
+ if ((stream.getLanguage() != null && stream.getLanguage().equals(language)) || language.equals("*")) {
+ filtered.add(stream);
+ }
+ }
+ //Tries to reduce execution time from n^2
+ audioStreams.removeAll(filtered);
+ }
+ return filtered;
+ }
+
+ /**
+ * Filters and sorts subtitle streams according to chosen languages
+ * @param subtitleStreams A list of subtitle streams.
+ * @param subtitleLanguages A list of languages.
+ * @param preventSignsAndSongs Whether partial subtitles should be avoided.
+ * @return A list containing just subtitles of chosen languages, sorted in order of languages.
+ */
+ static List filterSubtitleStreams(List subtitleStreams, String[] subtitleLanguages,
+ boolean preventSignsAndSongs) {
+ List filtered = new ArrayList<>();
+ //Go through languages. Select all subtitles of the language
+ for (String language : subtitleLanguages) {
+ for (SubtitleStream stream : subtitleStreams) {
+ String streamLanguage = stream.getLanguage();
+ if (((streamLanguage != null && streamLanguage.equals(language)) || language.equals("*")) &&
+ (!preventSignsAndSongs || stream.getIsFullSubtitle())) {
+ filtered.add(stream);
+ }
+ }
+ //Tries to reduce execution time from n^2
+ subtitleStreams.removeAll(filtered);
+ }
+ return filtered;
+ }
+
+ /**
+ * Escapes special characters which can cause trouble for ffmpeg
+ * @param fileName The filename to escape.
+ * @return A filename with known special characters escaped.
+ */
+ private static String escapeSpecialCharactersInFileName(String fileName) {
+ return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\")
+ .replaceAll("'", "'\\\\\\\\\\\\\''")
+ .replaceAll("%", "\\\\\\\\\\\\%")
+ .replaceAll(":", "\\\\\\\\\\\\:")
+ .replace("]", "\\]")
+ .replace("[", "\\[");
+ }
+
+
+
+ /**
+ * Adds audio to a command
+ * @param command The command to add audio to.
+ * @param audioStream The audio stream to be added.
+ * @param toStereo Whether to convert the audio stream to stereo.
+ */
+ void addAudioStreams(List command, AudioStream audioStream, boolean toStereo) {
+ if (audioStream != null) {
+ command.add("-map");
+ command.add("0:" + audioStream.getAbsoluteIndex());
+ if (toStereo && audioStream.getChannels() > 2) {
+ command.add("-af");
+ command.add("pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
+ }
+ }
+ }
+
+ /**
+ * Adds subtitles and video mapping to a command
+ * @param command The list containing the rest of the command.
+ * @param subtitleStream The subtitle stream to be used.
+ * @param videoStream The video stream to be used.
+ * @param file The file to convert.
+ */
+ void addSubtitles(List command, SubtitleStream subtitleStream, VideoStream videoStream, File file) {
+ //No appropriate subtitle was found. Just add the video stream.
+ if (subtitleStream == null) {
+ command.add("-map");
+ command.add(String.format("0:%d", videoStream.getAbsoluteIndex()));
+ return;
+ }
+
+ if (!subtitleStream.getIsImageSubtitle()) {
+ addSubtitle(command, subtitleStream, videoStream);
+ } else if (file.getName().equals(subtitleStream.getFile())) {
+ addInternalImageSubtitle(command, subtitleStream, videoStream);
+ } else {
+ addExternalImageSubtitle(command, subtitleStream, videoStream);
+ }
+ }
+
+ /**
+ * Adds subtitle commands to a command list
+ * @param command The list containing the FFmpeg commands.
+ * @param subtitleStream The subtitle stream to add.
+ * @param videoStream The video stream to burn the subtitle into.
+ */
+ private void addSubtitle(List command, SubtitleStream subtitleStream, VideoStream videoStream) {
+ command.add("-map");
+ command.add(String.format("0:%d", videoStream.getAbsoluteIndex()));
+ command.add("-vf");
+ String safeFileName = escapeSpecialCharactersInFileName(subtitleStream.getFile());
+ String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName,
+ subtitleStream.getRelativeIndex());
+ command.add(subtitleCommand);
+ }
+
+ /**
+ * Adds image subtitle commands to a command list
+ * @param command The list containing the FFmpeg commands.
+ * @param subtitleStream The subtitle stream to add.
+ * @param videoStream The video stream to burn the subtitle into.
+ */
+ private void addInternalImageSubtitle(List command, SubtitleStream subtitleStream, VideoStream videoStream) {
+ command.add("-filter_complex");
+ String filter = String.format("[0:v:%d][0:%d]overlay", videoStream.getAbsoluteIndex(),
+ subtitleStream.getAbsoluteIndex());
+ command.add(filter);
+ }
+
+ /**
+ * Adds external image subtitle commands to a command list
+ * @param command The list containing the FFmpeg commands.
+ * @param externalImageSubtitle The external image subtitle stream to add.
+ * @param videoStream The video stream to burn the subtitle into.
+ */
+ private void addExternalImageSubtitle(List command, SubtitleStream externalImageSubtitle,
+ VideoStream videoStream) {
+ command.add("-i");
+ command.add(externalImageSubtitle.getFile());
+ command.add("-filter_complex");
+ command.add(String.format("[1:s]scale=width=%d:height=%d,crop=w=%d:h=%d:x=0:y=out_h[sub];[%d:v]" +
+ "[sub]overlay", videoStream.getWidth(), videoStream.getHeight(), videoStream.getWidth(),
+ videoStream.getHeight(), videoStream.getAbsoluteIndex()));
+ command.add("-profile:v");
+ command.add("baseline");
+ }
+}