Improves FFMpeg command generation
Adds an object for storing FFmpeg variables. This allows any argument type to be added at any time, removing the limitation of having to add to the command in a specific order. Makes stream objects store the index of the input file they belong to. Saves the result of probes to a record that's easy to pass around. Always passes all probed files to the input files of ffmpeg. Makes it easier to enable h26x encoding/decoding when necessary. Removes the hard-coded behavior for external subtitles, and allows any stream to be an external stream.
This commit is contained in:
parent
be88845731
commit
376d5655f2
@ -0,0 +1,139 @@
|
|||||||
|
package net.knarcraft.ffmpegconverter.container;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for generating and storing a ffmpeg command
|
||||||
|
*/
|
||||||
|
public class FFMpegCommand {
|
||||||
|
|
||||||
|
private final @NotNull String executable;
|
||||||
|
private final @NotNull List<String> globalOptions;
|
||||||
|
private final @NotNull List<String> inputFileOptions;
|
||||||
|
private final @NotNull List<String> inputFiles;
|
||||||
|
private final @NotNull List<String> outputFileOptions;
|
||||||
|
private @NotNull String outputFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new FFMPEG command
|
||||||
|
*
|
||||||
|
* @param executable <p>The FFMPEG/FFPROBE executable to run</p>
|
||||||
|
*/
|
||||||
|
public FFMpegCommand(@NotNull String executable) {
|
||||||
|
this.executable = executable;
|
||||||
|
this.globalOptions = new ArrayList<>();
|
||||||
|
this.inputFileOptions = new ArrayList<>();
|
||||||
|
this.inputFiles = new ArrayList<>();
|
||||||
|
this.outputFileOptions = new ArrayList<>();
|
||||||
|
this.outputFile = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates a new FFMPEG command
|
||||||
|
*
|
||||||
|
* @param executable <p>The FFMPEG/FFPROBE executable to run</p>
|
||||||
|
* @param globalOptions <p>Options for FFMPEG itself</p>
|
||||||
|
* @param inputFileOptions <p>Options for processing of the input files</p>
|
||||||
|
* @param inputFiles <p>The input files to execute on</p>
|
||||||
|
* @param outputFileOptions <p>Options for the output files</p>
|
||||||
|
* @param outputFile <p>The output file to write to</p>
|
||||||
|
*/
|
||||||
|
public FFMpegCommand(@NotNull String executable, @NotNull List<String> globalOptions,
|
||||||
|
@NotNull List<String> inputFileOptions, @NotNull List<String> inputFiles,
|
||||||
|
@NotNull List<String> outputFileOptions, @NotNull String outputFile) {
|
||||||
|
this.executable = executable;
|
||||||
|
this.globalOptions = globalOptions;
|
||||||
|
this.inputFileOptions = inputFileOptions;
|
||||||
|
this.inputFiles = inputFiles;
|
||||||
|
this.outputFileOptions = outputFileOptions;
|
||||||
|
this.outputFile = outputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a global ffmpeg option to this command
|
||||||
|
*
|
||||||
|
* @param argument <p>The option(s) to add</p>
|
||||||
|
*/
|
||||||
|
public void addGlobalOption(@NotNull String... argument) {
|
||||||
|
this.globalOptions.addAll(List.of(argument));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an input file option to this command
|
||||||
|
*
|
||||||
|
* @param argument <p>The input file option(s) to add</p>
|
||||||
|
*/
|
||||||
|
public void addInputFileOption(@NotNull String... argument) {
|
||||||
|
this.inputFileOptions.addAll(List.of(argument));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an input file to this command
|
||||||
|
*
|
||||||
|
* <p>Note that this adds the "-i", so don't add that yourself!</p>
|
||||||
|
*
|
||||||
|
* @param argument <p>The input file(s) to add</p>
|
||||||
|
*/
|
||||||
|
public void addInputFile(@NotNull String... argument) {
|
||||||
|
for (String fileName : argument) {
|
||||||
|
if (fileName.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.inputFiles.add(fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the input files currently added to this command
|
||||||
|
*
|
||||||
|
* @return <p>The input files</p>
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public List<String> getInputFiles() {
|
||||||
|
return new ArrayList<>(this.inputFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an output file option to this command
|
||||||
|
*
|
||||||
|
* @param argument <p>The output file option(s) to add</p>
|
||||||
|
*/
|
||||||
|
public void addOutputFileOption(@NotNull String... argument) {
|
||||||
|
this.outputFileOptions.addAll(List.of(argument));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the output file for this command
|
||||||
|
*
|
||||||
|
* @param argument <p>The path to the output file</p>
|
||||||
|
*/
|
||||||
|
public void setOutputFile(@NotNull String argument) {
|
||||||
|
this.outputFile = argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the result of combining all the given input
|
||||||
|
*
|
||||||
|
* @return <p>The generated FFMPEG command</p>
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public String[] getResult() {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
result.add(executable);
|
||||||
|
result.addAll(globalOptions);
|
||||||
|
result.addAll(inputFileOptions);
|
||||||
|
for (String inputFile : inputFiles) {
|
||||||
|
result.add("-i");
|
||||||
|
result.add(inputFile);
|
||||||
|
}
|
||||||
|
result.addAll(outputFileOptions);
|
||||||
|
if (!outputFile.isEmpty()) {
|
||||||
|
result.add(outputFile);
|
||||||
|
}
|
||||||
|
return result.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package net.knarcraft.ffmpegconverter.container;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A record for storing the result of probing for streams
|
||||||
|
*
|
||||||
|
* @param parsedFiles <p>The files that were parsed to get the attached streams</p>
|
||||||
|
* @param parsedStreams <p>The streams that were parsed from the files</p>
|
||||||
|
*/
|
||||||
|
public record StreamProbeResult(@NotNull List<File> parsedFiles, @NotNull List<StreamObject> parsedStreams) {
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
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;
|
||||||
@ -122,19 +123,20 @@ public abstract class AbstractConverter implements Converter {
|
|||||||
* @param file <p>The file to process.</p>
|
* @param file <p>The file to process.</p>
|
||||||
* @throws IOException <p>If the BufferedReader fails.</p>
|
* @throws IOException <p>If the BufferedReader fails.</p>
|
||||||
*/
|
*/
|
||||||
private void processFile(File folder, File file) throws IOException {
|
private void processFile(@NotNull File folder, @NotNull File file) throws IOException {
|
||||||
List<StreamObject> streams = FFMpegHelper.probeFile(ffprobePath, file);
|
StreamProbeResult probeResult = FFMpegHelper.probeFile(ffprobePath, file);
|
||||||
if (streams.isEmpty()) {
|
if (probeResult.parsedStreams().isEmpty()) {
|
||||||
throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" +
|
throw new IllegalArgumentException("The file has no valid streams. Please make sure the file exists and" +
|
||||||
" is not corrupt.");
|
" is not corrupt.");
|
||||||
}
|
}
|
||||||
|
|
||||||
String outExtension = newExtension != null ? newExtension : FileUtil.getExtension(file.getName());
|
String outExtension = newExtension != null ? newExtension : FileUtil.getExtension(file.getName());
|
||||||
String newPath = FileUtil.getNonCollidingPath(folder, file, outExtension);
|
String newPath = FileUtil.getNonCollidingPath(folder, file, outExtension);
|
||||||
OutputUtil.println();
|
OutputUtil.println();
|
||||||
OutputUtil.println("Preparing to start process...");
|
OutputUtil.println("Preparing to start process...");
|
||||||
OutputUtil.println("Converting " + file);
|
OutputUtil.println("Converting " + file);
|
||||||
|
|
||||||
String[] command = generateConversionCommand(ffmpegPath, file, streams, newPath);
|
String[] command = generateConversionCommand(ffmpegPath, probeResult, newPath);
|
||||||
// If no commands were given, no conversion is necessary
|
// If no commands were given, no conversion is necessary
|
||||||
if (command.length == 0) {
|
if (command.length == 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
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.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;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -51,8 +53,10 @@ public class AnimeConverter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
List<String> command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName());
|
@NotNull String outFile) {
|
||||||
|
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
|
||||||
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
@ -71,13 +75,16 @@ public class AnimeConverter extends AbstractConverter {
|
|||||||
|
|
||||||
//Get the first video stream
|
//Get the first video stream
|
||||||
VideoStream videoStream = getNthVideoStream(streams, 0);
|
VideoStream videoStream = getNthVideoStream(streams, 0);
|
||||||
|
if (videoStream == null) {
|
||||||
|
throw new IllegalArgumentException("The selected video stream does not exist");
|
||||||
|
}
|
||||||
|
|
||||||
//Add streams to output file
|
//Add streams to output file
|
||||||
FFMpegHelper.addAudioStream(command, audioStream, this.toStereo);
|
FFMpegHelper.addAudioStream(command, audioStream, this.toStereo);
|
||||||
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
|
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream);
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
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.AudioStream;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,8 +28,10 @@ public class AudioConverter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
@NotNull String outFile) {
|
||||||
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
@ -35,9 +39,9 @@ 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 = getNthAudioSteam(streams, 0);
|
AudioStream audioStream = getNthAudioSteam(streams, 0);
|
||||||
FFMpegHelper.addAudioStream(command, audioStream, false);
|
FFMpegHelper.addAudioStream(command, audioStream, false);
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
|
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface describes a file converter
|
* This interface describes a file converter
|
||||||
@ -16,6 +16,7 @@ public interface Converter {
|
|||||||
*
|
*
|
||||||
* @return <p>A list of valid input formats</p>
|
* @return <p>A list of valid input formats</p>
|
||||||
*/
|
*/
|
||||||
|
@NotNull
|
||||||
String[] getValidFormats();
|
String[] getValidFormats();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,17 +25,18 @@ public interface Converter {
|
|||||||
* @param file <p>The file to convert.</p>
|
* @param file <p>The file to convert.</p>
|
||||||
* @throws IOException <p>If the file cannot be converted.</p>
|
* @throws IOException <p>If the file cannot be converted.</p>
|
||||||
*/
|
*/
|
||||||
void convert(File file) throws IOException;
|
void convert(@NotNull File file) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a command for a ProcessBuilder.
|
* Generates a command for a ProcessBuilder.
|
||||||
*
|
*
|
||||||
* @param executable <p>The executable file for ffmpeg.</p>
|
* @param executable <p>The executable file for ffmpeg</p>
|
||||||
* @param file <p>The input file.</p>
|
* @param probeResult <p>The result of probing the input file</p>
|
||||||
* @param streams <p>A list of ffprobe streams.</p>
|
* @param outFile <p>The output file</p>
|
||||||
* @param outFile <p>The output file.</p>
|
|
||||||
* @return <p>A list of commands</p>
|
* @return <p>A list of commands</p>
|
||||||
*/
|
*/
|
||||||
String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile);
|
@NotNull
|
||||||
|
String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
|
@NotNull String outFile);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||||
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,33 +34,28 @@ public class DownScaleConverter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
|
@NotNull String outFile) {
|
||||||
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
VideoStream videoStream = getNthVideoStream(streams, 0);
|
VideoStream videoStream = getNthVideoStream(streams, 0);
|
||||||
if (videoStream == null || (videoStream.getWidth() <= newWidth && videoStream.getHeight() <= newHeight)) {
|
if (videoStream == null || (videoStream.getWidth() <= newWidth && videoStream.getHeight() <= newHeight)) {
|
||||||
return new String[0];
|
return new String[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add all streams without re-encoding
|
//Add all streams without re-encoding
|
||||||
command.add("-map");
|
FFMpegHelper.mapAllStreams(command, streams);
|
||||||
command.add("0");
|
command.addOutputFileOption("-c:a", "copy");
|
||||||
command.add("-c:a");
|
command.addOutputFileOption("-c:s", "copy");
|
||||||
command.add("copy");
|
command.addOutputFileOption("-vf", "scale=" + newWidth + ":" + newHeight);
|
||||||
command.add("-c:s");
|
command.addOutputFileOption("-crf", "20");
|
||||||
command.add("copy");
|
command.addOutputFileOption("-preset", "slow");
|
||||||
command.add("-vf");
|
command.setOutputFile(outFile);
|
||||||
command.add("scale=" + newWidth + ":" + newHeight);
|
return command.getResult();
|
||||||
command.add("-crf");
|
|
||||||
command.add("20");
|
|
||||||
command.add("-preset");
|
|
||||||
command.add("slow");
|
|
||||||
|
|
||||||
command.add(outFile);
|
|
||||||
return command.toArray(new String[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
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.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;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -44,8 +46,10 @@ public class MKVToMP4Transcoder extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
@NotNull String outFile) {
|
||||||
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
@ -61,21 +65,19 @@ public class MKVToMP4Transcoder extends AbstractConverter {
|
|||||||
|
|
||||||
//Get the nth video stream
|
//Get the nth video stream
|
||||||
VideoStream videoStream = getNthVideoStream(streams, Math.max(this.videoStreamIndex, 0));
|
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.add("-map_metadata");
|
command.addOutputFileOption("-c", "copy");
|
||||||
command.add("0");
|
|
||||||
command.add("-movflags");
|
|
||||||
command.add("use_metadata_tags");
|
|
||||||
command.add("-c");
|
|
||||||
command.add("copy");
|
|
||||||
|
|
||||||
//Add streams to output file
|
//Add streams to output file
|
||||||
FFMpegHelper.addAudioStream(command, audioStream, false);
|
FFMpegHelper.addAudioStream(command, audioStream, false);
|
||||||
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
|
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream);
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
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.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;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,73 +34,50 @@ public class MkvH264Converter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams,
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
String outFile) {
|
@NotNull String outFile) {
|
||||||
List<String> command = new ArrayList<>();
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
command.add(executable);
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
command.add("-hwaccel");
|
|
||||||
command.add("cuda");
|
|
||||||
command.add("-hwaccel_output_format");
|
|
||||||
command.add("cuda");
|
|
||||||
command.add("-i");
|
|
||||||
command.add(file.getName());
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map video if present
|
// Map video if present
|
||||||
if (!filterStreamsByType(streams, VideoStream.class).isEmpty()) {
|
List<StreamObject> videoStreams = filterStreamsByType(streams, VideoStream.class);
|
||||||
command.add("-map");
|
if (!videoStreams.isEmpty()) {
|
||||||
command.add("0:v");
|
for (StreamObject streamObject : videoStreams) {
|
||||||
command.add("-crf");
|
if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) {
|
||||||
command.add("28");
|
continue;
|
||||||
command.add("-codec:v");
|
}
|
||||||
command.add("h264_nvenc");
|
|
||||||
command.add("-preset");
|
FFMpegHelper.addH26xHardwareDecoding(command);
|
||||||
command.add("slow");
|
break;
|
||||||
command.add("-movflags");
|
}
|
||||||
command.add("+faststart");
|
|
||||||
|
FFMpegHelper.mapAllStreams(command, videoStreams);
|
||||||
|
FFMpegHelper.addH264HardwareEncoding(command, 17);
|
||||||
|
command.addOutputFileOption("-movflags", "+faststart");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map audio if present
|
// Map audio if present
|
||||||
if (!filterStreamsByType(streams, AudioStream.class).isEmpty()) {
|
List<StreamObject> audioStreams = filterStreamsByType(streams, AudioStream.class);
|
||||||
command.add("-map");
|
if (!audioStreams.isEmpty()) {
|
||||||
command.add("0:a");
|
FFMpegHelper.mapAllStreams(command, audioStreams);
|
||||||
command.add("-c:a");
|
command.addOutputFileOption("-c:a", "copy");
|
||||||
command.add("copy");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map subtitles if present
|
// Map subtitles if present
|
||||||
if (hasInternalStreams(streams)) {
|
List<StreamObject> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
|
||||||
command.add("-map");
|
if (!subtitleStreams.isEmpty()) {
|
||||||
command.add("0:s");
|
FFMpegHelper.mapAllStreams(command, subtitleStreams);
|
||||||
command.add("-c:s");
|
command.addOutputFileOption("-c:s", "copy");
|
||||||
command.add("copy");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-map_metadata");
|
command.addOutputFileOption("-f", "matroska");
|
||||||
command.add("0");
|
|
||||||
command.add("-movflags");
|
|
||||||
command.add("use_metadata_tags");
|
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the processed file has any internal subtitle streams
|
|
||||||
*
|
|
||||||
* @param streams <p>All parsed streams for the video file</p>
|
|
||||||
* @return <p>True if the file has at least one internal subtitle stream</p>
|
|
||||||
*/
|
|
||||||
private boolean hasInternalStreams(List<StreamObject> streams) {
|
|
||||||
for (StreamObject subtitleStream : filterStreamsByType(streams, SubtitleStream.class)) {
|
|
||||||
if (((SubtitleStream) subtitleStream).isInternalSubtitle()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
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.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;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,76 +34,49 @@ public class MkvH265ReducedConverter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams,
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
String outFile) {
|
@NotNull String outFile) {
|
||||||
List<String> command = new ArrayList<>();
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
command.add(executable);
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
command.add("-hwaccel");
|
|
||||||
command.add("cuda");
|
|
||||||
command.add("-hwaccel_output_format");
|
|
||||||
command.add("cuda");
|
|
||||||
command.add("-i");
|
|
||||||
command.add(file.getName());
|
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map video if present
|
// Map video if present
|
||||||
if (!filterStreamsByType(streams, VideoStream.class).isEmpty()) {
|
List<StreamObject> videoStreams = filterStreamsByType(streams, VideoStream.class);
|
||||||
command.add("-map");
|
if (!videoStreams.isEmpty()) {
|
||||||
command.add("0:v");
|
for (StreamObject streamObject : videoStreams) {
|
||||||
command.add("-codec:v");
|
if (!streamObject.getCodecName().equals("h264") && !streamObject.getCodecName().equals("h266")) {
|
||||||
command.add("hevc_nvenc");
|
continue;
|
||||||
command.add("-crf");
|
}
|
||||||
command.add("28");
|
|
||||||
command.add("-preset");
|
FFMpegHelper.addH26xHardwareDecoding(command);
|
||||||
command.add("slow");
|
break;
|
||||||
command.add("-tag:v");
|
}
|
||||||
command.add("hvc1");
|
|
||||||
command.add("-movflags");
|
FFMpegHelper.mapAllStreams(command, videoStreams);
|
||||||
command.add("+faststart");
|
FFMpegHelper.addH265HardwareEncoding(command, 17);
|
||||||
|
command.addOutputFileOption("-movflags", "+faststart");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map audio if present
|
// Map audio if present
|
||||||
if (!filterStreamsByType(streams, AudioStream.class).isEmpty()) {
|
List<StreamObject> audioStreams = filterStreamsByType(streams, AudioStream.class);
|
||||||
command.add("-map");
|
if (!audioStreams.isEmpty()) {
|
||||||
command.add("0:a");
|
FFMpegHelper.mapAllStreams(command, audioStreams);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map subtitles if present
|
// Map subtitles if present
|
||||||
if (hasInternalStreams(streams)) {
|
List<StreamObject> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
|
||||||
command.add("-map");
|
if (!subtitleStreams.isEmpty()) {
|
||||||
command.add("0:s");
|
FFMpegHelper.mapAllStreams(command, subtitleStreams);
|
||||||
command.add("-c:s");
|
command.addOutputFileOption("-c:s", "copy");
|
||||||
command.add("copy");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-map_metadata");
|
command.addOutputFileOption("-f", "matroska");
|
||||||
command.add("0");
|
|
||||||
command.add("-movflags");
|
|
||||||
command.add("use_metadata_tags");
|
|
||||||
command.add("-f");
|
|
||||||
command.add("matroska");
|
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the processed file has any internal subtitle streams
|
|
||||||
*
|
|
||||||
* @param streams <p>All parsed streams for the video file</p>
|
|
||||||
* @return <p>True if the file has at least one internal subtitle stream</p>
|
|
||||||
*/
|
|
||||||
private boolean hasInternalStreams(List<StreamObject> streams) {
|
|
||||||
for (StreamObject subtitleStream : filterStreamsByType(streams, SubtitleStream.class)) {
|
|
||||||
if (((SubtitleStream) subtitleStream).isInternalSubtitle()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||||
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||||
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
|
||||||
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,44 +31,22 @@ public class SubtitleEmbed extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
@NotNull String outFile) {
|
||||||
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
List<SubtitleStream> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
List<SubtitleStream> externalSubtitles = new ArrayList<>();
|
|
||||||
for (SubtitleStream subtitleStream : subtitleStreams) {
|
FFMpegHelper.mapAllStreams(command, streams);
|
||||||
if (!subtitleStream.isInternalSubtitle()) {
|
command.addOutputFileOption("-c:a", "copy");
|
||||||
externalSubtitles.add(subtitleStream);
|
command.addOutputFileOption("-c:v", "copy");
|
||||||
}
|
command.addOutputFileOption("-c:s", "mov_text");
|
||||||
}
|
|
||||||
|
|
||||||
if (externalSubtitles.isEmpty()) {
|
|
||||||
System.err.println("No external subtitles found for " + file.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (SubtitleStream subtitleStream : externalSubtitles) {
|
|
||||||
command.add("-i");
|
|
||||||
command.add(subtitleStream.getFile());
|
|
||||||
}
|
|
||||||
|
|
||||||
command.add("-c");
|
|
||||||
command.add("copy");
|
|
||||||
command.add("-c:s");
|
|
||||||
command.add("mov_text");
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
for (SubtitleStream subtitleStream : subtitleStreams) {
|
|
||||||
command.add("-metadata:s:s:" + i);
|
|
||||||
command.add("language=" + subtitleStream.getLanguage());
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||||
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,20 +27,20 @@ public class VideoConverter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
@NotNull String outFile) {
|
||||||
|
FFMpegCommand command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, probeResult.parsedFiles());
|
||||||
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add all streams without re-encoding
|
//Add all streams without re-encoding
|
||||||
command.add("-map");
|
FFMpegHelper.mapAllStreams(command, streams);
|
||||||
command.add("0");
|
command.addOutputFileOption("-c", "copy");
|
||||||
command.add("-c");
|
|
||||||
command.add("copy");
|
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package net.knarcraft.ffmpegconverter.converter;
|
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.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;
|
||||||
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 java.io.File;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,8 +35,10 @@ public class WebVideoConverter extends AbstractConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
public String[] generateConversionCommand(@NotNull String executable, @NotNull StreamProbeResult probeResult,
|
||||||
List<String> command = FFMpegHelper.getFFMpegWebVideoCommand(executable, file.getName());
|
@NotNull String outFile) {
|
||||||
|
FFMpegCommand command = FFMpegHelper.getFFMpegWebVideoCommand(executable, probeResult.parsedFiles());
|
||||||
|
List<StreamObject> streams = probeResult.parsedStreams();
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
FFMpegHelper.addDebugArguments(command, 50, 120);
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
}
|
}
|
||||||
@ -44,13 +48,17 @@ public class WebVideoConverter extends AbstractConverter {
|
|||||||
VideoStream videoStream = getNthVideoStream(streams, 0);
|
VideoStream videoStream = getNthVideoStream(streams, 0);
|
||||||
AudioStream audioStream = getNthAudioSteam(streams, 0);
|
AudioStream audioStream = getNthAudioSteam(streams, 0);
|
||||||
|
|
||||||
|
if (videoStream == null) {
|
||||||
|
throw new IllegalArgumentException("The selected video stream does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
//Add streams to output
|
//Add streams to output
|
||||||
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream, file);
|
FFMpegHelper.addSubtitleAndVideoStream(command, subtitleStream, videoStream);
|
||||||
if (audioStream != null) {
|
if (audioStream != null) {
|
||||||
FFMpegHelper.addAudioStream(command, audioStream, true);
|
FFMpegHelper.addAudioStream(command, audioStream, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add(outFile);
|
command.setOutputFile(outFile);
|
||||||
return command.toArray(new String[0]);
|
return command.getResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,11 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public abstract class AbstractStream implements StreamObject {
|
public abstract class AbstractStream implements StreamObject {
|
||||||
|
|
||||||
|
protected final int inputIndex;
|
||||||
protected final int absoluteIndex;
|
protected final int absoluteIndex;
|
||||||
protected final int relativeIndex;
|
protected final int relativeIndex;
|
||||||
protected final String codecName;
|
protected final String codecName;
|
||||||
protected String language;
|
protected final String language;
|
||||||
protected final boolean isDefault;
|
protected final boolean isDefault;
|
||||||
protected final String title;
|
protected final String title;
|
||||||
|
|
||||||
@ -21,14 +22,16 @@ public abstract class AbstractStream implements StreamObject {
|
|||||||
* Instantiates a new abstract stream
|
* Instantiates a new abstract stream
|
||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
|
* @param inputIndex <p>The index of the input file this stream belongs to</p>
|
||||||
* @param relativeIndex <p>The relative index of this stream, only considering streams of the same type</p>
|
* @param relativeIndex <p>The relative index of this stream, only considering streams of the same type</p>
|
||||||
*/
|
*/
|
||||||
protected AbstractStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex) {
|
protected AbstractStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
this.codecName = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_NAME), "");
|
this.codecName = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_NAME), "");
|
||||||
this.absoluteIndex = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.INDEX), -1);
|
this.absoluteIndex = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.INDEX), -1);
|
||||||
this.language = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und");
|
this.language = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_LANGUAGE), "und");
|
||||||
this.isDefault = ValueParsingHelper.parseBoolean(streamInfo.get(StreamTag.DISPOSITION_DEFAULT), false);
|
this.isDefault = ValueParsingHelper.parseBoolean(streamInfo.get(StreamTag.DISPOSITION_DEFAULT), false);
|
||||||
this.title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), "");
|
this.title = ValueParsingHelper.parseString(streamInfo.get(StreamTag.TAG_TITLE), "");
|
||||||
|
this.inputIndex = inputIndex;
|
||||||
this.relativeIndex = relativeIndex;
|
this.relativeIndex = relativeIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +61,14 @@ public abstract class AbstractStream implements StreamObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@NotNull
|
||||||
public String getTitle() {
|
public String getTitle() {
|
||||||
return this.title;
|
return this.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInputIndex() {
|
||||||
|
return this.inputIndex;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -16,10 +16,11 @@ public class AudioStream extends AbstractStream implements StreamObject {
|
|||||||
* Instantiates a new audio stream
|
* Instantiates a new audio stream
|
||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
|
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||||
* @param relativeIndex <p>The index of the audio stream relative to other audio streams.</p>
|
* @param relativeIndex <p>The index of the audio stream relative to other audio streams.</p>
|
||||||
*/
|
*/
|
||||||
public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex) {
|
public AudioStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
super(streamInfo, relativeIndex);
|
super(streamInfo, inputIndex, relativeIndex);
|
||||||
this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0);
|
this.channels = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.CHANNELS), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,15 @@ public interface StreamObject {
|
|||||||
*/
|
*/
|
||||||
int getAbsoluteIndex();
|
int getAbsoluteIndex();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of the input file this stream belongs to
|
||||||
|
*
|
||||||
|
* <p>This is the first number given to a map argument in order to map this stream to the output file.</p>
|
||||||
|
*
|
||||||
|
* @return <p>The input index of this stream</p>
|
||||||
|
*/
|
||||||
|
int getInputIndex();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the relative index of a stream object (kth element of codec type)
|
* Gets the relative index of a stream object (kth element of codec type)
|
||||||
*
|
*
|
||||||
@ -57,6 +66,7 @@ public interface StreamObject {
|
|||||||
*
|
*
|
||||||
* @return <p>The title of the subtitle stream.</p>
|
* @return <p>The title of the subtitle stream.</p>
|
||||||
*/
|
*/
|
||||||
|
@NotNull
|
||||||
String getTitle();
|
String getTitle();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package net.knarcraft.ffmpegconverter.streams;
|
package net.knarcraft.ffmpegconverter.streams;
|
||||||
|
|
||||||
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -10,51 +9,21 @@ import java.util.Map;
|
|||||||
*/
|
*/
|
||||||
public class SubtitleStream extends AbstractStream implements StreamObject {
|
public class SubtitleStream extends AbstractStream implements StreamObject {
|
||||||
|
|
||||||
final private String file;
|
|
||||||
final private boolean isFullSubtitle;
|
final private boolean isFullSubtitle;
|
||||||
final private boolean isImageSubtitle;
|
final private boolean isImageSubtitle;
|
||||||
final private boolean isInternalStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new subtitle stream
|
* Instantiates a new subtitle stream
|
||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams.</p>
|
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||||
* @param file <p>The file containing the subtitle.</p>
|
* @param relativeIndex <p>The index of the subtitle stream relative to other subtitle streams.</p>
|
||||||
* @param isInternalStream <p>Whether this subtitle stream is in the video file itself</p>
|
|
||||||
*/
|
*/
|
||||||
public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex,
|
public SubtitleStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
@NotNull String file, boolean isInternalStream) {
|
super(streamInfo, inputIndex, relativeIndex);
|
||||||
super(streamInfo, relativeIndex);
|
|
||||||
this.isFullSubtitle = isFullSubtitle();
|
this.isFullSubtitle = isFullSubtitle();
|
||||||
this.isImageSubtitle = isImageSubtitle();
|
this.isImageSubtitle = codecName != null &&
|
||||||
this.isInternalStream = isInternalStream;
|
(getCodecName().equals("hdmv_pgs_subtitle") || getCodecName().equals("dvd_subtitle"));
|
||||||
this.file = file;
|
|
||||||
|
|
||||||
if (this.language == null || this.language.isEmpty()) {
|
|
||||||
String possibleLanguage = FileUtil.getLanguage(file);
|
|
||||||
if (possibleLanguage != null) {
|
|
||||||
this.language = possibleLanguage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the file name of the file containing this subtitle
|
|
||||||
*
|
|
||||||
* @return <p>The file name containing the subtitle stream.</p>
|
|
||||||
*/
|
|
||||||
public String getFile() {
|
|
||||||
return this.file;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets whether this subtitle stream is an internal stream, not an external one
|
|
||||||
*
|
|
||||||
* @return <p>True if this stream is an internal stream</p>
|
|
||||||
*/
|
|
||||||
public boolean isInternalSubtitle() {
|
|
||||||
return this.isInternalStream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -75,25 +44,12 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
|||||||
return this.isFullSubtitle;
|
return this.isFullSubtitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a subtitle is image based (as opposed to text based)
|
|
||||||
*
|
|
||||||
* @return <p>True if the subtitle is image based.</p>
|
|
||||||
*/
|
|
||||||
private boolean isImageSubtitle() {
|
|
||||||
return codecName != null && (getCodecName().equals("hdmv_pgs_subtitle")
|
|
||||||
|| getCodecName().equals("dvd_subtitle"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the subtitle translates everything (as opposed to just songs and signs)
|
* Checks whether the subtitle translates everything (as opposed to just songs and signs)
|
||||||
*
|
*
|
||||||
* @return <p>True if the subtitle translates everything.</p>
|
* @return <p>True if the subtitle translates everything.</p>
|
||||||
*/
|
*/
|
||||||
private boolean isFullSubtitle() {
|
private boolean isFullSubtitle() {
|
||||||
if (getTitle() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String titleLowercase = getTitle().toLowerCase();
|
String titleLowercase = getTitle().toLowerCase();
|
||||||
return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z]+songs?.*") &&
|
return !titleLowercase.matches(".*si(ng|gn)s?[ &/a-z]+songs?.*") &&
|
||||||
!titleLowercase.matches(".*songs?[ &/a-z]+si(gn|ng)s?.*") &&
|
!titleLowercase.matches(".*songs?[ &/a-z]+si(gn|ng)s?.*") &&
|
||||||
@ -101,7 +57,7 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
|||||||
!titleLowercase.matches(".*s&s.*") &&
|
!titleLowercase.matches(".*s&s.*") &&
|
||||||
!titleLowercase.matches("signs?") &&
|
!titleLowercase.matches("signs?") &&
|
||||||
!titleLowercase.matches("songs?") &&
|
!titleLowercase.matches("songs?") &&
|
||||||
!titleLowercase.contains("signs only");
|
!titleLowercase.matches(".*signs only.*");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -17,10 +17,11 @@ public class VideoStream extends AbstractStream implements StreamObject {
|
|||||||
* Instantiates a new video stream
|
* Instantiates a new video stream
|
||||||
*
|
*
|
||||||
* @param streamInfo <p>All info about the stream</p>
|
* @param streamInfo <p>All info about the stream</p>
|
||||||
|
* @param inputIndex <p>The index of the input file containing this stream</p>
|
||||||
* @param relativeIndex <p>The index of the video stream relative to other video streams.</p>
|
* @param relativeIndex <p>The index of the video stream relative to other video streams.</p>
|
||||||
*/
|
*/
|
||||||
public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int relativeIndex) {
|
public VideoStream(@NotNull Map<StreamTag, String> streamInfo, int inputIndex, int relativeIndex) {
|
||||||
super(streamInfo, relativeIndex);
|
super(streamInfo, inputIndex, relativeIndex);
|
||||||
this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1);
|
this.width = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.WIDTH), -1);
|
||||||
this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1);
|
this.height = ValueParsingHelper.parseInt(streamInfo.get(StreamTag.HEIGHT), -1);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package net.knarcraft.ffmpegconverter.utility;
|
package net.knarcraft.ffmpegconverter.utility;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.container.FFMpegCommand;
|
||||||
|
import net.knarcraft.ffmpegconverter.container.StreamProbeResult;
|
||||||
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.StreamTag;
|
import net.knarcraft.ffmpegconverter.streams.StreamTag;
|
||||||
@ -48,6 +50,13 @@ public final class FFMpegHelper {
|
|||||||
|
|
||||||
Plan: Sort all streams by set criteria. Map all selected streams by looping using their relative index for
|
Plan: Sort all streams by set criteria. Map all selected streams by looping using their relative index for
|
||||||
selection.
|
selection.
|
||||||
|
|
||||||
|
Streams should probably have an input index, so it's easier to treat extra subtitles more seamlessly. So, by
|
||||||
|
including any external subtitle files as input, there would be no need to fiddle more with storing input files.
|
||||||
|
|
||||||
|
Instead of storing the ffmpeg command as a list of strings, it should be stored as an object with different list
|
||||||
|
for input arguments and output arguments. That way, it would be much easier to add input-related arguments later
|
||||||
|
in the process.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -58,71 +67,73 @@ public final class FFMpegHelper {
|
|||||||
* @return <p>A list of StreamObjects.</p>
|
* @return <p>A list of StreamObjects.</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 List<StreamObject> probeFile(String ffprobePath, File file) throws IOException {
|
@NotNull
|
||||||
|
public static StreamProbeResult probeFile(@NotNull String ffprobePath, @NotNull File file) throws IOException {
|
||||||
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file);
|
return parseStreams(ffprobePath, probeForStreams(ffprobePath, file), file);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a list containing all required arguments for converting a video to a web playable video
|
* Creates a list containing all required arguments for converting a video to a web playable video
|
||||||
*
|
*
|
||||||
* @param executable <p>The executable to use (ffmpeg/ffprobe).</p>
|
* @param executable <p>The executable to use (ffmpeg/ffprobe)</p>
|
||||||
* @param fileName <p>The name of the file to execute on.</p>
|
* @param files <p>The files to execute on</p>
|
||||||
* @return <p>A base list of ffmpeg commands for converting a video for web</p>
|
* @return <p>A FFMPEG command for web-playable video</p>
|
||||||
*/
|
*/
|
||||||
public static List<String> getFFMpegWebVideoCommand(String executable, String fileName) {
|
@NotNull
|
||||||
List<String> command = getFFMpegGeneralFileCommand(executable, fileName);
|
public static FFMpegCommand getFFMpegWebVideoCommand(@NotNull String executable, @NotNull List<File> files) {
|
||||||
command.add("-vcodec");
|
FFMpegCommand command = getFFMpegGeneralFileCommand(executable, files);
|
||||||
command.add("h264");
|
command.addOutputFileOption("-vcodec", "h264");
|
||||||
command.add("-pix_fmt");
|
command.addOutputFileOption("-pix_fmt", "yuv420p");
|
||||||
command.add("yuv420p");
|
command.addOutputFileOption("-ar", "48000");
|
||||||
command.add("-ar");
|
command.addOutputFileOption("-movflags", "+faststart");
|
||||||
command.add("48000");
|
command.addOutputFileOption("-map_metadata", "0");
|
||||||
command.add("-movflags");
|
command.addOutputFileOption("-movflags", "+use_metadata_tags");
|
||||||
command.add("+faststart");
|
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a list containing command line arguments for a general file
|
* Creates a list containing command line arguments for a general file
|
||||||
*
|
*
|
||||||
* @param executable <p>The executable to use (ffmpeg/ffprobe).</p>
|
* @param executable <p>The executable to use (ffmpeg/ffprobe)</p>
|
||||||
* @param fileName <p>The name of the file to execute on.</p>
|
* @param files <p>The files to execute on</p>
|
||||||
* @return <p>A base list of ffmpeg commands for converting a file.</p>
|
* @return <p>A basic FFMPEG command</p>
|
||||||
*/
|
*/
|
||||||
public static List<String> getFFMpegGeneralFileCommand(String executable, String fileName) {
|
@NotNull
|
||||||
List<String> command = new ArrayList<>();
|
public static FFMpegCommand getFFMpegGeneralFileCommand(@NotNull String executable, @NotNull List<File> files) {
|
||||||
command.add(executable);
|
FFMpegCommand command = new FFMpegCommand(executable);
|
||||||
command.add("-nostdin");
|
command.addGlobalOption("-nostdin");
|
||||||
command.add("-i");
|
for (File file : files) {
|
||||||
command.add(fileName);
|
command.addInputFile(file.getName());
|
||||||
|
}
|
||||||
|
command.addOutputFileOption("-map_metadata", "0");
|
||||||
|
command.addOutputFileOption("-movflags", "+use_metadata_tags");
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds debugging parameters for only converting parts of a file
|
* Adds debugging parameters for only converting parts of a file
|
||||||
*
|
*
|
||||||
* @param command <p>The list containing the command to run.</p>
|
* @param command <p>The command to add to</p>
|
||||||
* @param start <p>The offset before converting.</p>
|
* @param start <p>The time to start at</p>
|
||||||
* @param length <p>The offset for stopping the conversion.</p>
|
* @param duration <p>The duration of video to output</p>
|
||||||
*/
|
*/
|
||||||
public static void addDebugArguments(List<String> command, int start, int length) {
|
public static void addDebugArguments(@NotNull FFMpegCommand command, int start, int duration) {
|
||||||
command.add("-ss");
|
command.addInputFileOption("-ss", String.valueOf(start));
|
||||||
command.add("" + start);
|
command.addOutputFileOption("-t", String.valueOf(duration));
|
||||||
command.add("-t");
|
|
||||||
command.add("" + length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts and prints output of a process
|
* Starts and prints output of a process
|
||||||
*
|
*
|
||||||
* @param processBuilder <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 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>
|
* @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 String runProcess(ProcessBuilder processBuilder, File folder, String spacer, boolean write)
|
@NotNull
|
||||||
throws IOException {
|
public static String runProcess(@NotNull ProcessBuilder processBuilder, @NotNull File folder,
|
||||||
|
@NotNull String spacer, boolean write) throws IOException {
|
||||||
//Give the user information about what's about to happen
|
//Give the user information about what's about to happen
|
||||||
OutputUtil.print("Command to be run: ");
|
OutputUtil.print("Command to be run: ");
|
||||||
OutputUtil.println(processBuilder.command().toString());
|
OutputUtil.println(processBuilder.command().toString());
|
||||||
@ -136,13 +147,15 @@ public final class FFMpegHelper {
|
|||||||
StringBuilder output = new StringBuilder();
|
StringBuilder output = new StringBuilder();
|
||||||
while (process.isAlive()) {
|
while (process.isAlive()) {
|
||||||
String read = readProcess(processReader, spacer);
|
String read = readProcess(processReader, spacer);
|
||||||
if (!read.isEmpty()) {
|
if (read.isEmpty()) {
|
||||||
if (write) {
|
continue;
|
||||||
OutputUtil.println(read);
|
}
|
||||||
} else {
|
|
||||||
OutputUtil.printDebug(read);
|
if (write) {
|
||||||
output.append(read);
|
OutputUtil.println(read);
|
||||||
}
|
} else {
|
||||||
|
OutputUtil.printDebug(read);
|
||||||
|
output.append(read);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OutputUtil.println("Process finished.");
|
OutputUtil.println("Process finished.");
|
||||||
@ -150,21 +163,16 @@ public final class FFMpegHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds audio to a command
|
* Maps an audio track to a ffmpeg command's output
|
||||||
*
|
*
|
||||||
* @param command <p>The command to add audio to.</p>
|
* @param command <p>The command to add the audio track to</p>
|
||||||
* @param audioStream <p>The audio stream to be added.</p>
|
* @param audioStream <p>The audio stream to be mapped</p>
|
||||||
* @param toStereo <p>Whether to convert the audio stream to stereo.</p>
|
* @param toStereo <p>Whether to convert the audio stream to stereo</p>
|
||||||
*/
|
*/
|
||||||
public static void addAudioStream(List<String> command, AudioStream audioStream, boolean toStereo) {
|
public static void addAudioStream(@NotNull FFMpegCommand command, @NotNull AudioStream audioStream, boolean toStereo) {
|
||||||
if (audioStream != null) {
|
mapStream(command, audioStream);
|
||||||
command.add("-map");
|
if (toStereo && audioStream.getChannels() > 2) {
|
||||||
command.add("0:" + audioStream.getAbsoluteIndex());
|
command.addOutputFileOption("-af", "pan=stereo|FL=FC+0.30*FL+0.30*BL|FR=FC+0.30*FR+0.30*BR");
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,37 +182,23 @@ public final class FFMpegHelper {
|
|||||||
* @param command <p>The list containing the rest of the command.</p>
|
* @param command <p>The list containing the rest of the command.</p>
|
||||||
* @param subtitleStream <p>The subtitle stream to be used.</p>
|
* @param subtitleStream <p>The subtitle stream to be used.</p>
|
||||||
* @param videoStream <p>The video 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 addSubtitleAndVideoStream(List<String> command, SubtitleStream subtitleStream,
|
public static void addSubtitleAndVideoStream(@NotNull FFMpegCommand command, @Nullable SubtitleStream subtitleStream,
|
||||||
VideoStream videoStream, File file) {
|
@NotNull VideoStream videoStream) {
|
||||||
//No appropriate subtitle was found. Just add the video stream.
|
//No appropriate subtitle was found. Just add the video stream.
|
||||||
if (subtitleStream == null) {
|
if (subtitleStream == null) {
|
||||||
addVideoStream(command, videoStream);
|
mapStream(command, videoStream);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add the correct command arguments depending on the subtitle type
|
//Add the correct command arguments depending on the subtitle type
|
||||||
if (!subtitleStream.getIsImageSubtitle()) {
|
if (!subtitleStream.getIsImageSubtitle()) {
|
||||||
addSubtitle(command, subtitleStream, videoStream);
|
addBurnedInSubtitle(command, subtitleStream, videoStream);
|
||||||
} else if (file.getName().equals(subtitleStream.getFile())) {
|
|
||||||
addInternalImageSubtitle(command, subtitleStream, videoStream);
|
|
||||||
} else {
|
} else {
|
||||||
addExternalImageSubtitle(command, subtitleStream, videoStream);
|
addBurnedInImageSubtitle(command, subtitleStream, videoStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds video mapping to a command
|
|
||||||
*
|
|
||||||
* @param command <p>The list containing the rest of the command.</p>
|
|
||||||
* @param videoStream <p>The video stream to be used.</p>
|
|
||||||
*/
|
|
||||||
private static void addVideoStream(List<String> command, VideoStream videoStream) {
|
|
||||||
command.add("-map");
|
|
||||||
command.add(String.format("0:%d", videoStream.getAbsoluteIndex()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds subtitle commands to a command list
|
* Adds subtitle commands to a command list
|
||||||
*
|
*
|
||||||
@ -212,14 +206,75 @@ public final class FFMpegHelper {
|
|||||||
* @param subtitleStream <p>The subtitle stream to add.</p>
|
* @param subtitleStream <p>The subtitle stream to add.</p>
|
||||||
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
||||||
*/
|
*/
|
||||||
private static void addSubtitle(List<String> command, SubtitleStream subtitleStream, VideoStream videoStream) {
|
private static void addBurnedInSubtitle(@NotNull FFMpegCommand command, @NotNull SubtitleStream subtitleStream,
|
||||||
command.add("-map");
|
@NotNull VideoStream videoStream) {
|
||||||
command.add(String.format("0:%d", videoStream.getAbsoluteIndex()));
|
mapStream(command, videoStream);
|
||||||
command.add("-vf");
|
|
||||||
String safeFileName = escapeSpecialCharactersInFileName(subtitleStream.getFile());
|
String safeFileName = escapeSpecialCharactersInFileName(
|
||||||
|
command.getInputFiles().get(subtitleStream.getInputIndex()));
|
||||||
String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName,
|
String subtitleCommand = String.format("subtitles='%s':si=%d", safeFileName,
|
||||||
subtitleStream.getRelativeIndex());
|
subtitleStream.getRelativeIndex());
|
||||||
command.add(subtitleCommand);
|
command.addOutputFileOption("-vf", subtitleCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds arguments for converting a file to h264 using hardware acceleration
|
||||||
|
*
|
||||||
|
* @param command <p>The command to add the arguments to</p>
|
||||||
|
* @param quality <p>The quality to encode. 0 = best, 51 = worst.</p>
|
||||||
|
*/
|
||||||
|
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 command <p>The command to add the arguments to</p>
|
||||||
|
* @param quality <p>The quality to encode. 0 = best, 51 = worst.</p>
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds arguments for using hardware acceleration during h264 decoding
|
||||||
|
*
|
||||||
|
* @param command <p>The command to add the arguments to</p>
|
||||||
|
*/
|
||||||
|
public static void addH26xHardwareDecoding(@NotNull FFMpegCommand command) {
|
||||||
|
command.addInputFileOption("-hwaccel", "cuda");
|
||||||
|
command.addInputFileOption("-hwaccel_output_format", "cuda");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps all streams in the given list to the output in the given command
|
||||||
|
*
|
||||||
|
* @param command <p>The command to add the mappings to</p>
|
||||||
|
* @param streams <p>The streams to map</p>
|
||||||
|
*/
|
||||||
|
public static void mapAllStreams(@NotNull FFMpegCommand command, @NotNull List<StreamObject> streams) {
|
||||||
|
for (StreamObject stream : streams) {
|
||||||
|
mapStream(command, stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the given stream to the given FFMPEG command's output
|
||||||
|
*
|
||||||
|
* @param command <p>The command to map the stream to</p>
|
||||||
|
* @param stream <p>The stream to map</p>
|
||||||
|
*/
|
||||||
|
public static void mapStream(@NotNull FFMpegCommand command, @NotNull StreamObject stream) {
|
||||||
|
command.addOutputFileOption("-map", String.format("%d:%d",
|
||||||
|
stream.getInputIndex(), stream.getAbsoluteIndex()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -237,38 +292,22 @@ public final class FFMpegHelper {
|
|||||||
.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.getRelativeIndex(),
|
|
||||||
subtitleStream.getAbsoluteIndex());
|
|
||||||
command.add(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds external image subtitle commands to a command list
|
* Adds external image subtitle commands to a command list
|
||||||
*
|
*
|
||||||
* @param command <p>The list containing the FFmpeg commands.</p>
|
* @param command <p>The FFMPEG command to modify</p>
|
||||||
* @param externalImageSubtitle <p>The external image subtitle stream to add.</p>
|
* @param subtitleStream <p>The external image subtitle stream to burn in</p>
|
||||||
* @param videoStream <p>The video stream to burn the subtitle into.</p>
|
* @param videoStream <p>The video stream to burn the subtitle into</p>
|
||||||
*/
|
*/
|
||||||
private static void addExternalImageSubtitle(List<String> command, SubtitleStream externalImageSubtitle,
|
private static void addBurnedInImageSubtitle(@NotNull FFMpegCommand command,
|
||||||
VideoStream videoStream) {
|
@NotNull SubtitleStream subtitleStream,
|
||||||
command.add("-i");
|
@NotNull VideoStream videoStream) {
|
||||||
command.add(externalImageSubtitle.getFile());
|
command.addOutputFileOption("-filter_complex",
|
||||||
command.add("-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",
|
||||||
command.add(String.format("[1:s]scale=width=%d:height=%d,crop=w=%d:h=%d:x=0:y=out_h[sub];[%d:v]" +
|
subtitleStream.getInputIndex(), subtitleStream.getAbsoluteIndex(), videoStream.getWidth(),
|
||||||
"[sub]overlay", videoStream.getWidth(), videoStream.getHeight(), videoStream.getWidth(),
|
videoStream.getHeight(), videoStream.getWidth(), videoStream.getHeight(),
|
||||||
videoStream.getHeight(), videoStream.getAbsoluteIndex()));
|
videoStream.getInputIndex(), videoStream.getAbsoluteIndex()));
|
||||||
command.add("-profile:v");
|
command.addOutputFileOption("-profile:v", "baseline");
|
||||||
command.add("baseline");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -280,13 +319,11 @@ 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 processBuilder = new ProcessBuilder(
|
FFMpegCommand probeCommand = new FFMpegCommand(ffprobePath);
|
||||||
ffprobePath,
|
probeCommand.addGlobalOption("-v", "error", "-show_streams");
|
||||||
"-v",
|
probeCommand.addInputFile(file.toString());
|
||||||
"error",
|
|
||||||
"-show_streams",
|
ProcessBuilder processBuilder = new ProcessBuilder(probeCommand.getResult());
|
||||||
file.toString()
|
|
||||||
);
|
|
||||||
String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
|
String result = runProcess(processBuilder, file.getParentFile(), PROBE_SPLIT_CHARACTER, false);
|
||||||
return StringUtil.stringBetween(result, "[STREAM]", "[/STREAM]");
|
return StringUtil.stringBetween(result, "[STREAM]", "[/STREAM]");
|
||||||
}
|
}
|
||||||
@ -298,7 +335,9 @@ public final class FFMpegHelper {
|
|||||||
* @param file <p>The file currently being converted.</p>
|
* @param file <p>The file currently being converted.</p>
|
||||||
* @return <p>A list of StreamObjects.</p>
|
* @return <p>A list of StreamObjects.</p>
|
||||||
*/
|
*/
|
||||||
private static List<StreamObject> parseStreams(String ffprobePath, String[] streams, File file) throws IOException {
|
@NotNull
|
||||||
|
private static StreamProbeResult parseStreams(@NotNull String ffprobePath, @NotNull String[] streams,
|
||||||
|
@NotNull File file) throws IOException {
|
||||||
List<StreamObject> parsedStreams = new ArrayList<>();
|
List<StreamObject> parsedStreams = new ArrayList<>();
|
||||||
int relativeAudioIndex = 0;
|
int relativeAudioIndex = 0;
|
||||||
int relativeVideoIndex = 0;
|
int relativeVideoIndex = 0;
|
||||||
@ -311,19 +350,19 @@ public final class FFMpegHelper {
|
|||||||
String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
|
String codecType = ValueParsingHelper.parseString(streamInfo.get(StreamTag.CODEC_TYPE), "");
|
||||||
switch (codecType) {
|
switch (codecType) {
|
||||||
case "video":
|
case "video":
|
||||||
parsedStreams.add(new VideoStream(streamInfo, relativeVideoIndex++));
|
parsedStreams.add(new VideoStream(streamInfo, 0, relativeVideoIndex++));
|
||||||
break;
|
break;
|
||||||
case "audio":
|
case "audio":
|
||||||
parsedStreams.add(new AudioStream(streamInfo, relativeAudioIndex++));
|
parsedStreams.add(new AudioStream(streamInfo, 0, relativeAudioIndex++));
|
||||||
break;
|
break;
|
||||||
case "subtitle":
|
case "subtitle":
|
||||||
parsedStreams.add(new SubtitleStream(streamInfo, relativeSubtitleIndex++, file.getName(), true));
|
parsedStreams.add(new SubtitleStream(streamInfo, 0, relativeSubtitleIndex++));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<StreamObject> externalSubtitles = getExternalSubtitles(ffprobePath, file.getParentFile(), file.getName());
|
StreamProbeResult probeResult = new StreamProbeResult(List.of(file), parsedStreams);
|
||||||
parsedStreams.addAll(externalSubtitles);
|
getExternalSubtitles(probeResult, ffprobePath, file.getParentFile(), file.getName());
|
||||||
return parsedStreams;
|
return probeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -349,26 +388,25 @@ public final class FFMpegHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether there exists an external image subtitle with the same filename as the file
|
* Tries to find any external subtitles adjacent to the first input file, and appends it to the given probe result
|
||||||
*
|
*
|
||||||
* @param ffprobePath <p>The path/command to ffprobe.</p>
|
* @param ffprobePath <p>The path/command to ffprobe</p>
|
||||||
* @param directory <p>The directory containing the file.</p>
|
* @param directory <p>The directory containing the file</p>
|
||||||
* @param convertingFile <p>The file to be converted.</p>
|
* @param convertingFile <p>The first/main file to be converted</p>
|
||||||
* @return <p>The extension of the subtitle or empty if no subtitle was found.</p>
|
|
||||||
*/
|
*/
|
||||||
@NotNull
|
private static void getExternalSubtitles(@NotNull StreamProbeResult streamProbeResult,
|
||||||
private static List<StreamObject> getExternalSubtitles(@NotNull String ffprobePath, @NotNull File directory,
|
@NotNull String ffprobePath, @NotNull File directory,
|
||||||
@NotNull String convertingFile) throws IOException {
|
@NotNull String convertingFile) throws IOException {
|
||||||
List<StreamObject> parsedStreams = new ArrayList<>();
|
|
||||||
//Find all files in the same directory with external subtitle formats
|
//Find all files in the same directory with external subtitle formats
|
||||||
if (subtitleFormats == null) {
|
if (subtitleFormats == null) {
|
||||||
subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
||||||
}
|
}
|
||||||
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
||||||
|
// TODO: Generalize this for external audio tracks
|
||||||
|
|
||||||
//Return early if no files were found
|
//Return early if no files were found
|
||||||
if (subtitleFiles == null) {
|
if (subtitleFiles == null) {
|
||||||
return parsedStreams;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String fileTitle = FileUtil.stripExtension(convertingFile);
|
String fileTitle = FileUtil.stripExtension(convertingFile);
|
||||||
@ -379,15 +417,17 @@ public final class FFMpegHelper {
|
|||||||
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
||||||
|
|
||||||
for (File subtitleFile : subtitleFilesList) {
|
for (File subtitleFile : subtitleFilesList) {
|
||||||
|
int inputIndex = streamProbeResult.parsedFiles().size();
|
||||||
|
streamProbeResult.parsedFiles().add(subtitleFile);
|
||||||
//Probe the files and add them to the result list
|
//Probe the files and add them to the result list
|
||||||
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
||||||
|
int relativeIndex = 0;
|
||||||
for (String stream : streams) {
|
for (String stream : streams) {
|
||||||
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
|
String[] streamParts = stream.split(PROBE_SPLIT_CHARACTER);
|
||||||
Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
|
Map<StreamTag, String> streamInfo = getStreamInfo(streamParts);
|
||||||
parsedStreams.add(new SubtitleStream(streamInfo, 0, subtitleFile.getName(), false));
|
streamProbeResult.parsedStreams().add(new SubtitleStream(streamInfo, inputIndex, relativeIndex++));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parsedStreams;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,43 +142,4 @@ public final class FileUtil {
|
|||||||
return file.substring(0, file.lastIndexOf('.'));
|
return file.substring(0, file.lastIndexOf('.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the locale specifying the language of the given file name
|
|
||||||
*
|
|
||||||
* @param fileName <p>The file name to check</p>
|
|
||||||
* @return <p>The locale, or null if no locale could be parsed</p>
|
|
||||||
*/
|
|
||||||
public static @Nullable String getLanguage(@NotNull String fileName) {
|
|
||||||
fileName = stripExtension(fileName);
|
|
||||||
|
|
||||||
String possibleLanguage = getExtension(fileName);
|
|
||||||
// NRK Nett-TV has a tendency to use nb-ttv for Norwegian for some reason
|
|
||||||
possibleLanguage = possibleLanguage.replace("nb-ttv", "nb-nor");
|
|
||||||
|
|
||||||
// TODO: Some languages are specified by using "-en" or "-English" or ".en" or ".English" at the end of file names
|
|
||||||
if (possibleLanguage.length() <= 1 || (possibleLanguage.length() >= 4 &&
|
|
||||||
(!possibleLanguage.contains("-") || possibleLanguage.length() >= 8))) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hope the text is an actual valid language
|
|
||||||
if (!possibleLanguage.contains("-")) {
|
|
||||||
return possibleLanguage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the "-" has at least two characters on each side
|
|
||||||
String[] parts = possibleLanguage.split("-");
|
|
||||||
if (parts[0].length() < 2 || parts[1].length() < 2) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts[1].length() == 3) {
|
|
||||||
// Return three-letter country code
|
|
||||||
return parts[1].toLowerCase();
|
|
||||||
} else {
|
|
||||||
// Return en-US country code
|
|
||||||
return parts[0].substring(0, 2).toLowerCase() + "-" + parts[1].substring(0, 2).toUpperCase();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user