diff --git a/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java new file mode 100644 index 0000000..ffee661 --- /dev/null +++ b/src/main/java/net/knarcraft/ffmpegconverter/utility/FFMpegHelper.java @@ -0,0 +1,294 @@ +package net.knarcraft.ffmpegconverter.utility; + +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 java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * A class which helps with ffmpeg probing and converting + */ +public final class FFMpegHelper { + + private FFMpegHelper() { + } + + private static final String PROBE_SPLIT_CHARACTER = "øæåÆØå"; + + /** + * Gets streams from a file + * @param ffprobePath

The path/command to ffprobe.

+ * @param file

The file to probe.

+ * @return

A list of StreamObjects.

+ * @throws IOException

If the process can't be readProcess.

+ */ + public static List probeFile(String ffprobePath, File file) throws IOException { + return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file); + } + + /** + * Gets a list of all streams in a file + * @param ffprobePath

The path/command to ffprobe.

+ * @param file

The file to probe.

+ * @return

A list of streams.

+ * @throws IOException

If something goes wrong while probing.

+ */ + private static String[] probeForStreams(String ffprobePath, File file) throws IOException { + ProcessBuilder builderProbe = new ProcessBuilder( + ffprobePath, + "-v", + "error", + "-show_entries", + "stream_tags=language,title:stream=index,codec_name,codec_type,channels,codec_type,width,height", + file.toString() + ); + OutputUtil.println(); + OutputUtil.print("Probe command: "); + 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]"); + } + + /** + * Takes a list of all streams and parses each stream into one of three objects + * @param streams

A list of all streams for the current file.

+ * @param file

The file currently being converted.

+ * @return

A list of StreamObjects.

+ */ + private static List parseStreams(String ffprobePath, String[] streams, File file) throws IOException { + List parsedStreams = new ArrayList<>(); + int relativeAudioIndex = 0; + int relativeVideoIndex = 0; + int relativeSubtitleIndex = 0; + for (String stream : streams) { + String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); + if (stream.contains("codec_type=video")) { + parsedStreams.add(parseVideoStream(streamParts, relativeVideoIndex++)); + } else if (stream.contains("codec_type=audio")) { + parsedStreams.add(parseAudioStream(streamParts, relativeAudioIndex++)); + } else if (stream.contains("codec_type=subtitle")) { + parsedStreams.add(parseSubtitleStream(streamParts, relativeSubtitleIndex++, file.getName())); + } + } + List externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName()); + parsedStreams.addAll(externalSubtitles); + return parsedStreams; + } + + /** + * Checks whether there exists an external image subtitle with the same filename as the file + * @param ffprobePath

The path/command to ffprobe.

+ * @param directory

The directory containing the file.

+ * @param convertingFile

The file to be converted.

+ * @return

The extension of the subtitle or empty if no subtitle was found.

+ */ + private static List getExternalSubtitles(String ffprobePath, File directory, String convertingFile) + throws IOException { + List parsedStreams = new ArrayList<>(); + String[] subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt"); + File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1); + + if (subtitleFiles == null) { + return parsedStreams; + } + + String fileTitle = FileUtil.stripExtension(convertingFile); + List subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles)); + //Finds the files which are subtitles belonging to the file + subtitleFilesList = ListUtil.getMatching(subtitleFilesList, + (subtitleFile) -> subtitleFile.getName().contains(fileTitle)); + for (File subtitleFile : subtitleFilesList) { + String[] streams = probeForStreams(ffprobePath, subtitleFile); + for (String stream : streams) { + String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER); + parsedStreams.add(parseSubtitleStream(streamParts, 0, subtitleFile.getName())); + } + } + return parsedStreams; + } + + /** + * Starts and prints output of a process + * @param process

The process to run.

+ * @param folder

The folder the process should run in.

+ * @throws IOException

If the process can't be readProcess.

+ */ + public static void convertProcess(ProcessBuilder process, File folder) throws IOException { + OutputUtil.print("Command to be run: "); + OutputUtil.println(process.command().toString()); + process.directory(folder); + process.redirectErrorStream(true); + Process processConvert = process.start(); + BufferedReader readerConvert = new BufferedReader(new InputStreamReader(processConvert.getInputStream())); + while (processConvert.isAlive()) { + String read = readProcess(readerConvert, "\n"); + if (!read.equals("")) { + OutputUtil.println(read); + } + } + OutputUtil.println("Process is finished."); + } + + /** + * Reads from a process reader + * @param reader

The reader of a process.

+ * @return

The output from the readProcess.

+ * @throws IOException

On reader failure.

+ */ + private static String readProcess(BufferedReader reader, String spacer) throws IOException { + String line; + StringBuilder text = new StringBuilder(); + while (reader.ready() && (line = reader.readLine()) != null && !line.equals("") && !line.equals("\n")) { + text.append(line).append(spacer); + } + return text.toString().trim(); + } + + /** + * Creates a list containing all required arguments for converting a video to a web playable video + * @param executable

The executable to use (ffmpeg/ffprobe).

+ * @param fileName

The name of the file to execute on.

+ * @return

A base list of ffmpeg commands for converting a video for web

+ */ + public static List getFFMpegWebVideoCommand(String executable, String fileName) { + List command = getFFMpegGeneralFileCommand(executable, fileName); + command.add("-vcodec"); + command.add("h264"); + command.add("-pix_fmt"); + command.add("yuv420p"); + command.add("-ar"); + command.add("48000"); + command.add("-movflags"); + command.add("+faststart"); + return command; + } + + /** + * Creates a list containing command line arguments for a general file + * @param executable

The executable to use (ffmpeg/ffprobe).

+ * @param fileName

The name of the file to execute on.

+ * @return

A base list of ffmpeg commands for converting a file.

+ */ + public static List getFFMpegGeneralFileCommand(String executable, String fileName) { + List command = new ArrayList<>(); + command.add(executable); + command.add("-nostdin"); + command.add("-i"); + command.add(fileName); + return command; + } + + /** + * Adds debugging parameters for only converting parts of a file + * @param command

The list containing the command to run.

+ * @param start

The offset before converting.

+ * @param length

The offset for stopping the conversion.

+ */ + public static void addDebugArguments(List command, int start, int length) { + command.add("-ss"); + command.add("" + start); + command.add("-t"); + command.add("" + length); + } + + /** + * Parses a list of video stream parameters to a video stream object + * @param streamParts

A list of parameters belonging to an video stream.

+ * @param relativeIndex

The relative index of the video stream.

+ * @return

A SubtitleStream object.

+ * @throws NumberFormatException

If codec index contains a non-numeric value.

+ */ + private static VideoStream parseVideoStream(String[] streamParts, int relativeIndex) throws NumberFormatException { + String codec = null; + int absoluteIndex = -1; + int width = -1; + int height = -1; + for (String streamPart : streamParts) { + if (streamPart.startsWith("codec_name=")) { + codec = streamPart.replace("codec_name=", ""); + } else if (streamPart.startsWith("index=")) { + absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); + } else if (streamPart.startsWith("width=")) { + width = Integer.parseInt(streamPart.replace("width=", "")); + } else if (streamPart.startsWith("height=")) { + height = Integer.parseInt(streamPart.replace("height=", "")); + } + } + return new VideoStream(codec, absoluteIndex, relativeIndex, width, height); + } + + /** + * Parses a list of audio stream parameters to an audio stream object + * @param streamParts

A list of parameters belonging to an audio stream.

+ * @param relativeIndex

The relative index of the audio stream.

+ * @return

A SubtitleStream object.

+ * @throws NumberFormatException

If codec index contains a non-numeric value.

+ */ + private static AudioStream parseAudioStream(String[] streamParts, int relativeIndex) throws NumberFormatException { + String codec = null; + int absoluteIndex = -1; + String language = null; + int channels = 0; + String title = ""; + for (String streamPart : streamParts) { + if (streamPart.startsWith("codec_name=")) { + codec = streamPart.replace("codec_name=", ""); + } else if (streamPart.startsWith("index=")) { + absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); + } else if (streamPart.startsWith("TAG:language=")) { + language = streamPart.replace("TAG:language=", ""); + } else if (streamPart.startsWith("channels=")) { + channels = Integer.parseInt(streamPart.replace("channels=", "")); + } else if (streamPart.startsWith("TAG:title=")) { + title = streamPart.replace("TAG:title=", ""); + } + } + return new AudioStream(codec, absoluteIndex, relativeIndex, language, title, channels); + } + + /** + * Parses a list of subtitle stream parameters to a subtitle stream object + * @param streamParts

A list of parameters belonging to a subtitle stream.

+ * @param relativeIndex

The relative index of the subtitle.

+ * @param file

The file currently being converted.

+ * @return

A SubtitleStream object.

+ * @throws NumberFormatException

If codec index contains a non-numeric value.

+ */ + private static SubtitleStream parseSubtitleStream(String[] streamParts, int relativeIndex, String file) + throws NumberFormatException { + String codecName = null; + int absoluteIndex = -1; + String language = null; + String title = ""; + for (String streamPart : streamParts) { + if (streamPart.contains("codec_name=")) { + codecName = streamPart.replace("codec_name=", ""); + } else if (streamPart.startsWith("index=")) { + absoluteIndex = Integer.parseInt(streamPart.replace("index=", "")); + } else if (streamPart.startsWith("TAG:language=")) { + language = streamPart.replace("TAG:language=", ""); + } else if (streamPart.startsWith("TAG:title=")) { + title = streamPart.replace("TAG:title=", ""); + } + } + return new SubtitleStream(codecName, absoluteIndex, relativeIndex, language, title, file); + } +}