Simplifies and improves code. Removes some duplication
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
This commit is contained in:
parent
9673266c09
commit
e1256f61c7
@ -16,7 +16,7 @@ import java.util.Scanner;
|
|||||||
import static net.knarcraft.ffmpegconverter.utility.Parser.tokenize;
|
import static net.knarcraft.ffmpegconverter.utility.Parser.tokenize;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a files or files in a folder to a web playable mp4.
|
* The main class for starting the software
|
||||||
*/
|
*/
|
||||||
class Main {
|
class Main {
|
||||||
private static final String FFPROBE_PATH = "ffprobe"; //Can be just ffprobe if it's in the path
|
private static final String FFPROBE_PATH = "ffprobe"; //Can be just ffprobe if it's in the path
|
||||||
@ -25,27 +25,7 @@ class Main {
|
|||||||
private static AbstractConverter converter = null;
|
private static AbstractConverter converter = null;
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
public static void main(String[] args) throws IOException {
|
||||||
//System.out.println(tokenizer("AnimeConverter -audiolang jap,eng -sublang eng,nor,* \"C:\\Users\\Kristian\\Downloads\\Anime\\[Kametsu] ERASED (BD 1080p Hi10 FLAC)\""));
|
loadConverter();
|
||||||
//parser(tokenizer("AnimeConverter \"C:\\Users\\Kristian\\Downloads\\Anime\\[Kametsu] ERASED (BD 1080p Hi10 FLAC)\""));
|
|
||||||
//System.exit(1);
|
|
||||||
|
|
||||||
int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" +
|
|
||||||
"3. VideoStream converter", 1, 3);
|
|
||||||
|
|
||||||
OutputUtil.println("Input for this converter:");
|
|
||||||
switch (choice) {
|
|
||||||
case 1:
|
|
||||||
animeConverter();
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
converter = new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
converter = new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
int recursionSteps = 1;
|
int recursionSteps = 1;
|
||||||
|
|
||||||
@ -64,8 +44,46 @@ class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folder.isDirectory()) {
|
convertAllFiles(folder, recursionSteps);
|
||||||
File[] files = FileUtil.listFilesRecursive(folder, converter.getValidFormats(), recursionSteps);
|
|
||||||
|
OutputUtil.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asks the user which converter they want, and assigns a converter instance to the converter variable
|
||||||
|
*
|
||||||
|
* @throws IOException <p>If there's a problem getting user input.</p>
|
||||||
|
*/
|
||||||
|
private static void loadConverter() throws IOException {
|
||||||
|
int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" +
|
||||||
|
"3. VideoStream converter", 1, 3);
|
||||||
|
|
||||||
|
OutputUtil.println("Input for this converter:");
|
||||||
|
switch (choice) {
|
||||||
|
case 1:
|
||||||
|
animeConverter();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
converter = new AudioConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
converter = new VideoConverter(FFPROBE_PATH, FFMPEG_PATH, getChoice("<output extension>"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the file(s) as specified
|
||||||
|
*
|
||||||
|
* @param fileOrFolder <p>A file or a folder.</p>
|
||||||
|
* @param recursionSteps <p>The depth to recurse if a folder is given.</p>
|
||||||
|
* @throws IOException <p>If conversion or writing fails.</p>
|
||||||
|
*/
|
||||||
|
private static void convertAllFiles(File fileOrFolder, int recursionSteps) throws IOException {
|
||||||
|
if (fileOrFolder.isDirectory()) {
|
||||||
|
File[] files = FileUtil.listFilesRecursive(fileOrFolder, converter.getValidFormats(), recursionSteps);
|
||||||
if (files != null && files.length > 0) {
|
if (files != null && files.length > 0) {
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
converter.convert(file);
|
converter.convert(file);
|
||||||
@ -73,12 +91,11 @@ class Main {
|
|||||||
} else {
|
} else {
|
||||||
OutputUtil.println("No valid files found in folder.");
|
OutputUtil.println("No valid files found in folder.");
|
||||||
}
|
}
|
||||||
} else if (folder.exists()) {
|
} else if (fileOrFolder.exists()) {
|
||||||
converter.convert(folder);
|
converter.convert(fileOrFolder);
|
||||||
} else {
|
} else {
|
||||||
OutputUtil.println("Path " + folder.getAbsolutePath() + " does not point to any file or folder.");
|
OutputUtil.println("Path " + fileOrFolder.getAbsolutePath() + " does not point to any file or folder.");
|
||||||
}
|
}
|
||||||
OutputUtil.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -152,8 +169,8 @@ class Main {
|
|||||||
*/
|
*/
|
||||||
private static int getChoice(String prompt, int min, int max) throws IOException {
|
private static int getChoice(String prompt, int min, int max) throws IOException {
|
||||||
OutputUtil.println(prompt);
|
OutputUtil.println(prompt);
|
||||||
int choice = 0;
|
int choice = Integer.MIN_VALUE;
|
||||||
while (choice < min || choice > max) {
|
do {
|
||||||
OutputUtil.println("Your input: ");
|
OutputUtil.println("Your input: ");
|
||||||
try {
|
try {
|
||||||
choice = Integer.parseInt(READER.next());
|
choice = Integer.parseInt(READER.next());
|
||||||
@ -162,7 +179,7 @@ class Main {
|
|||||||
} finally {
|
} finally {
|
||||||
READER.nextLine();
|
READER.nextLine();
|
||||||
}
|
}
|
||||||
}
|
} while (choice < min || choice > max);
|
||||||
return choice;
|
return choice;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ import net.knarcraft.ffmpegconverter.utility.OutputUtil;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -49,12 +48,10 @@ public abstract class AbstractConverter implements Converter {
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
static <G extends StreamObject> List<G> filterStreamsByType(List<StreamObject> streams, Class<?> clazz) {
|
static <G extends StreamObject> List<G> filterStreamsByType(List<StreamObject> streams, Class<?> clazz) {
|
||||||
Iterator<StreamObject> i = streams.iterator();
|
|
||||||
List<G> newStreams = new ArrayList<>();
|
List<G> newStreams = new ArrayList<>();
|
||||||
while (i.hasNext()) {
|
for (StreamObject stream : streams) {
|
||||||
StreamObject next = i.next();
|
if (stream.getClass() == clazz) {
|
||||||
if (next.getClass() == clazz) {
|
newStreams.add((G) stream);
|
||||||
newStreams.add((G) next);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newStreams;
|
return newStreams;
|
||||||
@ -68,17 +65,7 @@ public abstract class AbstractConverter implements Converter {
|
|||||||
* @return <p>A list containing just audio tracks of chosen languages, sorted in order 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) {
|
static List<AudioStream> filterAudioStreams(List<AudioStream> audioStreams, String[] audioLanguages) {
|
||||||
List<AudioStream> filtered = new ArrayList<>();
|
return sortStreamsByLanguage(audioStreams, audioLanguages);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,35 +78,32 @@ public abstract class AbstractConverter implements Converter {
|
|||||||
*/
|
*/
|
||||||
static List<SubtitleStream> filterSubtitleStreams(List<SubtitleStream> subtitleStreams, String[] subtitleLanguages,
|
static List<SubtitleStream> filterSubtitleStreams(List<SubtitleStream> subtitleStreams, String[] subtitleLanguages,
|
||||||
boolean preventSignsAndSongs) {
|
boolean preventSignsAndSongs) {
|
||||||
List<SubtitleStream> filtered = new ArrayList<>();
|
List<SubtitleStream> sorted = sortStreamsByLanguage(subtitleStreams, subtitleLanguages);
|
||||||
//Go through languages. Select all subtitles of the language
|
sorted.removeIf((stream) -> preventSignsAndSongs && !stream.getIsFullSubtitle());
|
||||||
for (String language : subtitleLanguages) {
|
return sorted;
|
||||||
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
|
* Sorts subtitle streams according to chosen languages and removes non matching languages
|
||||||
*
|
*
|
||||||
* @param fileName <p>The filename to escape.</p>
|
* @param streams <p>A list of streams to sort.</p>
|
||||||
* @return <p>A filename with known special characters escaped.</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 String escapeSpecialCharactersInFileName(String fileName) {
|
private static <G extends StreamObject> List<G> sortStreamsByLanguage(List<G> streams, String[] languages) {
|
||||||
return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\")
|
List<G> sorted = new ArrayList<>();
|
||||||
.replaceAll("'", "'\\\\\\\\\\\\\''")
|
for (String language : languages) {
|
||||||
.replaceAll("%", "\\\\\\\\\\\\%")
|
for (G stream : streams) {
|
||||||
.replaceAll(":", "\\\\\\\\\\\\:")
|
String streamLanguage = stream.getLanguage();
|
||||||
.replace("]", "\\]")
|
if (language.equals("*") || (streamLanguage == null && language.equals("0")) ||
|
||||||
.replace("[", "\\[");
|
(streamLanguage != null && streamLanguage.equals(language))) {
|
||||||
|
sorted.add(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
streams.removeAll(sorted);
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,100 +113,6 @@ public abstract class AbstractConverter implements Converter {
|
|||||||
*/
|
*/
|
||||||
public abstract String[] getValidFormats();
|
public abstract String[] getValidFormats();
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds audio to a command
|
|
||||||
*
|
|
||||||
* @param command <p>The command to add audio to.</p>
|
|
||||||
* @param audioStream <p>The audio stream to be added.</p>
|
|
||||||
* @param toStereo <p>Whether to convert the audio stream to stereo.</p>
|
|
||||||
*/
|
|
||||||
void addAudioStreams(List<String> command, AudioStream audioStream, boolean toStereo) {
|
|
||||||
if (audioStream != null) {
|
|
||||||
command.add("-map");
|
|
||||||
command.add("0:" + audioStream.getAbsoluteIndex());
|
|
||||||
if (toStereo && audioStream.getChannels() > 2) {
|
|
||||||
command.add("-af");
|
|
||||||
command.add("pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
|
|
||||||
//command.add("pan=stereo|FL < 1.0*FL + 0.707*FC + 0.707*BL|FR < 1.0*FR + 0.707*FC + 0.707*BR");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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>
|
|
||||||
* @param file <p>The file to convert.</p>
|
|
||||||
*/
|
|
||||||
void addSubtitlesAndVideo(List<String> 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 <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 void addSubtitle(List<String> 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 <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 void addInternalImageSubtitle(List<String> 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 <p>The list containing the FFmpeg commands.</p>
|
|
||||||
* @param externalImageSubtitle <p>The external image subtitle stream to add.</p>
|
|
||||||
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
|
||||||
*/
|
|
||||||
private void addExternalImageSubtitle(List<String> 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");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads streams from a file, and converts it to an mp4
|
* Reads streams from a file, and converts it to an mp4
|
||||||
*
|
*
|
||||||
@ -241,7 +131,7 @@ public abstract class AbstractConverter implements Converter {
|
|||||||
OutputUtil.println("Preparing to start process...");
|
OutputUtil.println("Preparing to start process...");
|
||||||
OutputUtil.println("Converting " + file);
|
OutputUtil.println("Converting " + file);
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(builderCommand(ffmpegPath, file, streams, newPath));
|
ProcessBuilder processBuilder = new ProcessBuilder(builderCommand(ffmpegPath, file, streams, newPath));
|
||||||
FFMpegHelper.convertProcess(processBuilder, folder);
|
FFMpegHelper.runProcess(processBuilder, folder, "\n", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,8 +66,8 @@ public class AnimeConverter extends AbstractConverter {
|
|||||||
VideoStream videoStream = getFirstVideoStream(streams);
|
VideoStream videoStream = getFirstVideoStream(streams);
|
||||||
|
|
||||||
//Add streams to output file
|
//Add streams to output file
|
||||||
addAudioStreams(command, audioStream, toStereo);
|
FFMpegHelper.addAudioStreams(command, audioStream, toStereo);
|
||||||
addSubtitlesAndVideo(command, subtitleStream, videoStream, file);
|
FFMpegHelper.addSubtitlesAndVideo(command, subtitleStream, videoStream, file);
|
||||||
|
|
||||||
command.add(outFile);
|
command.add(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.toArray(new String[0]);
|
||||||
|
@ -35,7 +35,7 @@ public class AudioConverter extends AbstractConverter {
|
|||||||
|
|
||||||
//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 = getFirstAudioSteam(streams);
|
AudioStream audioStream = getFirstAudioSteam(streams);
|
||||||
addAudioStreams(command, audioStream, false);
|
FFMpegHelper.addAudioStreams(command, audioStream, false);
|
||||||
|
|
||||||
return command.toArray(new String[0]);
|
return command.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,9 @@ public class VideoConverter extends AbstractConverter {
|
|||||||
AudioStream audioStream = getFirstAudioSteam(streams);
|
AudioStream audioStream = getFirstAudioSteam(streams);
|
||||||
|
|
||||||
//Add streams to output
|
//Add streams to output
|
||||||
addSubtitlesAndVideo(command, subtitleStream, videoStream, file);
|
FFMpegHelper.addSubtitlesAndVideo(command, subtitleStream, videoStream, file);
|
||||||
if (audioStream != null) {
|
if (audioStream != null) {
|
||||||
addAudioStreams(command, audioStream, true);
|
FFMpegHelper.addAudioStreams(command, audioStream, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add(outFile);
|
command.add(outFile);
|
||||||
|
@ -7,6 +7,7 @@ public abstract class AbstractStream implements StreamObject {
|
|||||||
int absoluteIndex;
|
int absoluteIndex;
|
||||||
int relativeIndex;
|
int relativeIndex;
|
||||||
String codecName;
|
String codecName;
|
||||||
|
String language;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getCodecName() {
|
public String getCodecName() {
|
||||||
@ -22,4 +23,10 @@ public abstract class AbstractStream implements StreamObject {
|
|||||||
public int getRelativeIndex() {
|
public int getRelativeIndex() {
|
||||||
return this.relativeIndex;
|
return this.relativeIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getLanguage() {
|
||||||
|
return this.language;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -4,7 +4,6 @@ package net.knarcraft.ffmpegconverter.streams;
|
|||||||
* This class represents an ffmpeg audio stream
|
* This class represents an ffmpeg audio stream
|
||||||
*/
|
*/
|
||||||
public class AudioStream extends AbstractStream implements StreamObject {
|
public class AudioStream extends AbstractStream implements StreamObject {
|
||||||
private final String language;
|
|
||||||
private final int channels;
|
private final int channels;
|
||||||
private final String title;
|
private final String title;
|
||||||
|
|
||||||
@ -27,15 +26,6 @@ public class AudioStream extends AbstractStream implements StreamObject {
|
|||||||
this.channels = channels;
|
this.channels = channels;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the language of the audio stream
|
|
||||||
*
|
|
||||||
* @return <p>The language of the audio stream.</p>
|
|
||||||
*/
|
|
||||||
public String getLanguage() {
|
|
||||||
return this.language;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the number of channels for the audio stream
|
* Gets the number of channels for the audio stream
|
||||||
*
|
*
|
||||||
|
@ -23,4 +23,11 @@ public interface StreamObject {
|
|||||||
*/
|
*/
|
||||||
int getRelativeIndex();
|
int getRelativeIndex();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the language of the audio stream
|
||||||
|
*
|
||||||
|
* @return <p>The language of the audio stream.</p>
|
||||||
|
*/
|
||||||
|
String getLanguage();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ package net.knarcraft.ffmpegconverter.streams;
|
|||||||
* An object representation of a subtitle stream in a media file
|
* An object representation of a subtitle stream in a media file
|
||||||
*/
|
*/
|
||||||
public class SubtitleStream extends AbstractStream implements StreamObject {
|
public class SubtitleStream extends AbstractStream implements StreamObject {
|
||||||
final private String language;
|
|
||||||
final private String title;
|
final private String title;
|
||||||
final private String file;
|
final private String file;
|
||||||
final private boolean isFullSubtitle;
|
final private boolean isFullSubtitle;
|
||||||
@ -32,15 +31,6 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
|||||||
this.file = file;
|
this.file = file;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the language of the subtitle stream
|
|
||||||
*
|
|
||||||
* @return <p>The language of the subtitle stream.</p>
|
|
||||||
*/
|
|
||||||
public String getLanguage() {
|
|
||||||
return this.language;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the title of the subtitle stream
|
* Gets the title of the subtitle stream
|
||||||
*
|
*
|
||||||
|
@ -88,24 +88,149 @@ public final class FFMpegHelper {
|
|||||||
/**
|
/**
|
||||||
* Starts and prints output of a process
|
* Starts and prints output of a process
|
||||||
*
|
*
|
||||||
* @param process <p>The process to run.</p>
|
* @param processBuilder <p>The process to run.</p>
|
||||||
* @param folder <p>The folder the process should run in.</p>
|
* @param folder <p>The folder the process should run in.</p>
|
||||||
|
* @param spacer <p>The character(s) to use between each new line read.</p>
|
||||||
|
* @param write <p>Whether to write the output directly instead of storing it.</p>
|
||||||
* @throws IOException <p>If the process can't be readProcess.</p>
|
* @throws IOException <p>If the process can't be readProcess.</p>
|
||||||
*/
|
*/
|
||||||
public static void convertProcess(ProcessBuilder process, File folder) throws IOException {
|
public static String runProcess(ProcessBuilder processBuilder, File folder, String spacer, boolean write)
|
||||||
|
throws IOException {
|
||||||
|
//Give the user information about what's about to happen
|
||||||
OutputUtil.print("Command to be run: ");
|
OutputUtil.print("Command to be run: ");
|
||||||
OutputUtil.println(process.command().toString());
|
OutputUtil.println(processBuilder.command().toString());
|
||||||
process.directory(folder);
|
|
||||||
process.redirectErrorStream(true);
|
//Set directory and error stream
|
||||||
Process processConvert = process.start();
|
processBuilder.directory(folder);
|
||||||
BufferedReader readerConvert = new BufferedReader(new InputStreamReader(processConvert.getInputStream()));
|
processBuilder.redirectErrorStream(true);
|
||||||
while (processConvert.isAlive()) {
|
|
||||||
String read = readProcess(readerConvert, "\n");
|
Process process = processBuilder.start();
|
||||||
|
BufferedReader processReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
while (process.isAlive()) {
|
||||||
|
String read = readProcess(processReader, spacer);
|
||||||
if (!read.equals("")) {
|
if (!read.equals("")) {
|
||||||
OutputUtil.println(read);
|
if (write) {
|
||||||
|
OutputUtil.print(read);
|
||||||
|
} else {
|
||||||
|
output.append(read);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutputUtil.println("Process finished.");
|
OutputUtil.println("Process finished.");
|
||||||
|
return output.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds audio to a command
|
||||||
|
*
|
||||||
|
* @param command <p>The command to add audio to.</p>
|
||||||
|
* @param audioStream <p>The audio stream to be added.</p>
|
||||||
|
* @param toStereo <p>Whether to convert the audio stream to stereo.</p>
|
||||||
|
*/
|
||||||
|
public static void addAudioStreams(List<String> command, AudioStream audioStream, boolean toStereo) {
|
||||||
|
if (audioStream != null) {
|
||||||
|
command.add("-map");
|
||||||
|
command.add("0:" + audioStream.getAbsoluteIndex());
|
||||||
|
if (toStereo && audioStream.getChannels() > 2) {
|
||||||
|
command.add("-af");
|
||||||
|
command.add("pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
|
||||||
|
//command.add("pan=stereo|FL < 1.0*FL + 0.707*FC + 0.707*BL|FR < 1.0*FR + 0.707*FC + 0.707*BR");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
* @param file <p>The file to convert.</p>
|
||||||
|
*/
|
||||||
|
public static void addSubtitlesAndVideo(List<String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Add the correct command arguments depending on the subtitle type
|
||||||
|
if (!subtitleStream.getIsImageSubtitle()) {
|
||||||
|
addSubtitle(command, subtitleStream, videoStream);
|
||||||
|
} else if (file.getName().equals(subtitleStream.getFile())) {
|
||||||
|
addInternalImageSubtitle(command, subtitleStream, videoStream);
|
||||||
|
} else {
|
||||||
|
addExternalImageSubtitle(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 addSubtitle(List<String> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes special characters which can cause trouble for ffmpeg
|
||||||
|
*
|
||||||
|
* @param fileName <p>The filename to escape.</p>
|
||||||
|
* @return <p>A filename with known special characters escaped.</p>
|
||||||
|
*/
|
||||||
|
private static String escapeSpecialCharactersInFileName(String fileName) {
|
||||||
|
return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\")
|
||||||
|
.replaceAll("'", "'\\\\\\\\\\\\\''")
|
||||||
|
.replaceAll("%", "\\\\\\\\\\\\%")
|
||||||
|
.replaceAll(":", "\\\\\\\\\\\\:")
|
||||||
|
.replace("]", "\\]")
|
||||||
|
.replace("[", "\\[");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds image 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 addInternalImageSubtitle(List<String> 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 <p>The list containing the FFmpeg commands.</p>
|
||||||
|
* @param externalImageSubtitle <p>The external image subtitle stream to add.</p>
|
||||||
|
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
||||||
|
*/
|
||||||
|
private static void addExternalImageSubtitle(List<String> 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,7 +242,7 @@ public final class FFMpegHelper {
|
|||||||
* @throws IOException <p>If something goes wrong while probing.</p>
|
* @throws IOException <p>If something goes wrong while probing.</p>
|
||||||
*/
|
*/
|
||||||
private static String[] probeForStreams(String ffprobePath, File file) throws IOException {
|
private static String[] probeForStreams(String ffprobePath, File file) throws IOException {
|
||||||
ProcessBuilder builderProbe = new ProcessBuilder(
|
ProcessBuilder processBuilder = new ProcessBuilder(
|
||||||
ffprobePath,
|
ffprobePath,
|
||||||
"-v",
|
"-v",
|
||||||
"error",
|
"error",
|
||||||
@ -125,21 +250,8 @@ public final class FFMpegHelper {
|
|||||||
"stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height",
|
"stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height",
|
||||||
file.toString()
|
file.toString()
|
||||||
);
|
);
|
||||||
OutputUtil.println();
|
String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
|
||||||
OutputUtil.print("Probe command: ");
|
return StringUtil.stringBetween(result, "[STREAM]", "[/STREAM]");
|
||||||
OutputUtil.println(builderProbe.command().toString());
|
|
||||||
builderProbe.redirectErrorStream(true);
|
|
||||||
Process processProbe = builderProbe.start();
|
|
||||||
BufferedReader readerProbe = new BufferedReader(new InputStreamReader(processProbe.getInputStream()));
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
while (processProbe.isAlive()) {
|
|
||||||
String read = readProcess(readerProbe, PROBE_SPLIT_CHARACTER);
|
|
||||||
if (!read.equals("")) {
|
|
||||||
OutputUtil.printDebug(read);
|
|
||||||
output.append(read);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return StringUtil.stringBetween(output.toString(), "[STREAM]", "[/STREAM]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,19 +292,22 @@ public final class FFMpegHelper {
|
|||||||
private static List<StreamObject> getExternalSubtitles(String ffprobePath, File directory, String convertingFile)
|
private static List<StreamObject> getExternalSubtitles(String ffprobePath, File directory, String convertingFile)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
List<StreamObject> parsedStreams = new ArrayList<>();
|
List<StreamObject> parsedStreams = new ArrayList<>();
|
||||||
|
//Find all files in the same directory with external subtitle formats
|
||||||
String[] subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
String[] subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
||||||
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
||||||
|
|
||||||
|
//Return early if no files were found
|
||||||
if (subtitleFiles == null) {
|
if (subtitleFiles == null) {
|
||||||
return parsedStreams;
|
return parsedStreams;
|
||||||
}
|
}
|
||||||
|
|
||||||
String fileTitle = FileUtil.stripExtension(convertingFile);
|
String fileTitle = FileUtil.stripExtension(convertingFile);
|
||||||
List<File> subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles));
|
List<File> subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles));
|
||||||
//Finds the files which are subtitles belonging to the file
|
//Finds the files which are subtitles probably belonging to the file
|
||||||
subtitleFilesList = ListUtil.getMatching(subtitleFilesList,
|
subtitleFilesList = ListUtil.getMatching(subtitleFilesList,
|
||||||
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
||||||
for (File subtitleFile : subtitleFilesList) {
|
for (File subtitleFile : subtitleFilesList) {
|
||||||
|
//Probe the files and add them to the result list
|
||||||
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
||||||
for (String stream : streams) {
|
for (String stream : streams) {
|
||||||
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
|
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
|
||||||
@ -291,7 +406,7 @@ public final class FFMpegHelper {
|
|||||||
String language = null;
|
String language = null;
|
||||||
String title = "";
|
String title = "";
|
||||||
for (String streamPart : streamParts) {
|
for (String streamPart : streamParts) {
|
||||||
if (streamPart.contains("codec_name=")) {
|
if (streamPart.startsWith("codec_name=")) {
|
||||||
codecName = streamPart.replace("codec_name=", "");
|
codecName = streamPart.replace("codec_name=", "");
|
||||||
} else if (streamPart.startsWith("index=")) {
|
} else if (streamPart.startsWith("index=")) {
|
||||||
absoluteIndex = Integer.parseInt(streamPart.replace("index=", ""));
|
absoluteIndex = Integer.parseInt(streamPart.replace("index=", ""));
|
||||||
|
Loading…
Reference in New Issue
Block a user