package net.knarcraft.ffmpegconverter.utility; import net.knarcraft.ffmpegconverter.FFMpegConvert; import net.knarcraft.ffmpegconverter.container.FFMpegCommand; import net.knarcraft.ffmpegconverter.container.ProcessResult; import net.knarcraft.ffmpegconverter.container.StreamProbeResult; import net.knarcraft.ffmpegconverter.property.StreamType; import net.knarcraft.ffmpegconverter.streams.AudioStream; import net.knarcraft.ffmpegconverter.streams.OtherStream; import net.knarcraft.ffmpegconverter.streams.StreamObject; import net.knarcraft.ffmpegconverter.streams.StreamTag; import net.knarcraft.ffmpegconverter.streams.SubtitleStream; import net.knarcraft.ffmpegconverter.streams.VideoStream; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; 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.HashMap; import java.util.List; import java.util.Map; /** * A class which helps with ffmpeg probing and converting */ public final class FFMpegHelper { private static final String PROBE_SPLIT_CHARACTER = "øæåÆØå"; private FFMpegHelper() { } /** * Gets streams from a file * * @param ffprobePath
The path/command to ffprobe
* @param fileThe file to probe
* @param subtitleFormatsThe extensions to accept for external subtitles
* @param audioFormatsThe extensions to accept for external audio files
* @returnA list of StreamObjects
* @throws IOExceptionIf the process can't be readProcess
*/ @NotNull public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file, @NotNull ListThe executable to use (ffmpeg/ffprobe)
* @param filesThe files to execute on
* @returnA FFMPEG command for web-playable video
*/ @NotNull public static FFMpegCommand getFFMpegWebVideoCommand(@NotNull String executable, @NotNull ListThe executable to use (ffmpeg/ffprobe)
* @param filesThe files to execute on
* @returnA basic FFMPEG command
*/ @NotNull public static FFMpegCommand getFFMpegGeneralFileCommand(@NotNull String executable, @NotNull ListThe process to run
* @param folderThe folder the process should run in
* @param spacerThe character(s) to use between each new line read
* @param writeWhether to write the output directly instead of storing it
* @returnThe result of running the process
* @throws IOExceptionIf the process can't be readProcess
*/ @NotNull public static ProcessResult runProcess(@NotNull ProcessBuilder processBuilder, @Nullable File folder, @NotNull String spacer, boolean write) throws IOException { //Give the user information about what's about to happen OutputUtil.print("Command to be run: "); OutputUtil.println(processBuilder.command().toString()); //Set directory and error stream if (folder != null) { processBuilder.directory(folder); } processBuilder.redirectErrorStream(true); 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.isEmpty()) { continue; } if (write) { OutputUtil.println(read); } else { OutputUtil.printDebug(read); output.append(read); } } try { int exitCode = process.waitFor(); OutputUtil.println("Process finished with exit code: " + exitCode); return new ProcessResult(exitCode, output.toString()); } catch (InterruptedException e) { return new ProcessResult(1, output.toString()); } } /** * Adds arguments for converting a file to h264 using hardware acceleration * * @param commandThe command to add the arguments to
* @param qualityThe quality to encode. 0 = best, 51 = worst.
*/ public static void addH264HardwareEncoding(@NotNull FFMpegCommand command, int quality) { command.addOutputFileOption("-codec:v", "h264_nvenc"); command.addOutputFileOption("-profile", "high"); command.addOutputFileOption("-preset", "p7"); command.addOutputFileOption("-crf", String.valueOf(quality)); } /** * Adds arguments for converting a file to h265 using hardware acceleration * * @param commandThe command to add the arguments to
* @param qualityThe quality to encode. 0 = best, 51 = worst.
*/ public static void addH265HardwareEncoding(@NotNull FFMpegCommand command, int quality) { command.addOutputFileOption("-codec:v", "hevc_nvenc"); command.addOutputFileOption("-profile", "main10"); command.addOutputFileOption("-preset", "p7"); command.addOutputFileOption("-tag:v", "hvc1"); command.addOutputFileOption("-crf", String.valueOf(quality)); } /** * Maps all streams in the given list to the output in the given command * * @param commandThe command to add the mappings to
* @param streamsThe streams to map
* @paramThe type of stream object to map
*/ public staticThe command to map the stream to
* @param streamThe stream to map
*/ public static void mapStream(@NotNull FFMpegCommand command, @NotNull StreamObject stream) { command.addOutputFileOption("-map", String.format("%d:%d", stream.getInputIndex(), stream.getAbsoluteIndex())); } /** * Escapes special characters which can cause trouble for ffmpeg * * @param fileNameThe filename to escape.
* @returnA filename with known special characters escaped.
*/ @NotNull public static String escapeSpecialCharactersInFileName(@NotNull String fileName) { return fileName.replaceAll("\\\\", "\\\\\\\\\\\\\\\\") .replaceAll("'", "'\\\\\\\\\\\\\''") .replaceAll("%", "\\\\\\\\\\\\%") .replaceAll(":", "\\\\\\\\\\\\:") .replace("]", "\\]") .replace("[", "\\["); } /** * Gets the nth stream from a list of streams * * @param streamsA list of streams
* @param nThe index of the audio stream to get
* @returnThe first audio stream found, or null if no audio streams were found
*/ public staticThe path/command to ffprobe.
* @param fileThe file to probe.
* @returnA list of streams.
* @throws IOExceptionIf something goes wrong while probing.
*/ @NotNull private static ListThe path to the ffprobe executable
* @param fileThe file to get the duration of
* @returnThe duration
* @throws IOExceptionIf unable to probe the file
* @throws NumberFormatExceptionIf ffmpeg returns a non-number
*/ public static double getDuration(@NotNull String ffprobePath, @NotNull File file) throws IOException, NumberFormatException { FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath); probeCommand.addGlobalOption("-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1"); probeCommand.addInputFile(file.toString()); ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult()); ProcessResult result = runProcess(processBuilder, file.getParentFile(), "", false); if (result.exitCode() != 0) { throw new IllegalArgumentException("File probe failed with code " + result.exitCode()); } return Double.parseDouble(result.output().trim()); } /** * Takes a list of all streams and parses each stream into one of three objects * * @param ffprobePathThe path to the ffprobe executable
* @param streamsA list of all streams for the current file.
* @param fileThe file currently being converted.
* @param subtitleFormatsThe extensions to accept for external subtitles
* @param audioFormatsThe extensions to accept for external audio tracks
* @returnA list of StreamObjects.
*/ @NotNull private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull ListThe stream data to parse
* @returnThe parsed stream objects
*/ @NotNull private static ListThe information describing the stream
* @returnThe type of the stream
*/ @NotNull private static StreamType getStreamType(@NotNull MapThe stream info lines to parse
* @returnThe stream tag map parsed
*/ @NotNull private static MapThe stream probe result to append to
* @param ffprobePathThe path/command to ffprobe
* @param directoryThe directory containing the file
* @param convertingFileThe first/main file to be converted
* @param formatsThe extensions to accept for external tracks
*/ private static void getExternalStreams(@NotNull StreamProbeResult streamProbeResult, @NotNull String ffprobePath, @NotNull File directory, @NotNull String convertingFile, @NotNull ListThe reader of a process.
* @returnThe output from the readProcess.
* @throws IOExceptionOn reader failure.
*/ @NotNull private static String readProcess(@NotNull BufferedReader reader, @NotNull String spacer) throws IOException { String line; StringBuilder text = new StringBuilder(); while (reader.ready() && (line = reader.readLine()) != null && !line.isEmpty() && !line.equals("\n")) { text.append(line).append(spacer); } return text.toString().trim(); } /** * Gets available hardware acceleration types * * @param ffmpegPathThe path to ffmpeg's executable
* @returnThe available hardware acceleration methods
* @throws IOExceptionIf the process fails
*/ @NotNull public static List