Adds a converter to embed subtitles
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
All checks were successful
KnarCraft/FFmpegConvert/pipeline/head This commit looks good
This commit is contained in:
parent
2962114601
commit
2c75d91cce
14
pom.xml
14
pom.xml
@ -71,6 +71,20 @@
|
|||||||
<target>${java.version}</target>
|
<target>${java.version}</target>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
<configuration>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<addClasspath>true</addClasspath>
|
||||||
|
<classpathPrefix>lib/</classpathPrefix>
|
||||||
|
<mainClass>net.knarcraft.ffmpegconverter.Main</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import net.knarcraft.ffmpegconverter.converter.DownScaleConverter;
|
|||||||
import net.knarcraft.ffmpegconverter.converter.MKVToMP4Transcoder;
|
import net.knarcraft.ffmpegconverter.converter.MKVToMP4Transcoder;
|
||||||
import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
|
import net.knarcraft.ffmpegconverter.converter.MkvH264Converter;
|
||||||
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
|
import net.knarcraft.ffmpegconverter.converter.MkvH265ReducedConverter;
|
||||||
|
import net.knarcraft.ffmpegconverter.converter.SubtitleEmbed;
|
||||||
import net.knarcraft.ffmpegconverter.converter.VideoConverter;
|
import net.knarcraft.ffmpegconverter.converter.VideoConverter;
|
||||||
import net.knarcraft.ffmpegconverter.converter.WebVideoConverter;
|
import net.knarcraft.ffmpegconverter.converter.WebVideoConverter;
|
||||||
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
import net.knarcraft.ffmpegconverter.utility.FileUtil;
|
||||||
@ -66,7 +67,7 @@ class Main {
|
|||||||
private static Converter loadConverter() throws IOException {
|
private static Converter loadConverter() throws IOException {
|
||||||
int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" +
|
int choice = getChoice("Which converter do you want do use?\n1. Anime to web mp4\n2. Audio converter\n" +
|
||||||
"3. Video converter\n4. Web video converter\n5. MKV to h264 converter\n6. MKV to h265 reduced " +
|
"3. Video converter\n4. Web video converter\n5. MKV to h264 converter\n6. MKV to h265 reduced " +
|
||||||
"converter\n7. MKV to MP4 transcoder\n8. DownScaleConverter", 1, 8);
|
"converter\n7. MKV to MP4 transcoder\n8. DownScaleConverter\n9. mp4 Subtitle Embed", 1, 9);
|
||||||
|
|
||||||
switch (choice) {
|
switch (choice) {
|
||||||
case 1:
|
case 1:
|
||||||
@ -85,6 +86,8 @@ class Main {
|
|||||||
return generateMKVToMP4Transcoder();
|
return generateMKVToMP4Transcoder();
|
||||||
case 8:
|
case 8:
|
||||||
return generateDownScaleConverter();
|
return generateDownScaleConverter();
|
||||||
|
case 9:
|
||||||
|
return new SubtitleEmbed(FFPROBE_PATH, FFMPEG_PATH);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
package net.knarcraft.ffmpegconverter.converter;
|
||||||
|
|
||||||
|
import net.knarcraft.ffmpegconverter.streams.StreamObject;
|
||||||
|
import net.knarcraft.ffmpegconverter.streams.SubtitleStream;
|
||||||
|
import net.knarcraft.ffmpegconverter.utility.FFMpegHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A converter to embed adjacent subtitles into mp4 files
|
||||||
|
*/
|
||||||
|
public class SubtitleEmbed extends AbstractConverter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes variables used by the abstract converter
|
||||||
|
*
|
||||||
|
* @param ffprobePath <p>Path/command to ffprobe.</p>
|
||||||
|
* @param ffmpegPath <p>Path/command to ffmpeg.</p>
|
||||||
|
*/
|
||||||
|
public SubtitleEmbed(String ffprobePath, String ffmpegPath) {
|
||||||
|
super(null);
|
||||||
|
this.ffprobePath = ffprobePath;
|
||||||
|
this.ffmpegPath = ffmpegPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getValidFormats() {
|
||||||
|
return new String[]{"mp4"};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] generateConversionCommand(String executable, File file, List<StreamObject> streams, String outFile) {
|
||||||
|
List<String> command = FFMpegHelper.getFFMpegGeneralFileCommand(executable, file.getName());
|
||||||
|
|
||||||
|
List<SubtitleStream> subtitleStreams = filterStreamsByType(streams, SubtitleStream.class);
|
||||||
|
List<SubtitleStream> externalSubtitles = new ArrayList<>();
|
||||||
|
for (SubtitleStream subtitleStream : subtitleStreams) {
|
||||||
|
if (!subtitleStream.isInternalSubtitle()) {
|
||||||
|
externalSubtitles.add(subtitleStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if (subtitleStream.getLanguage() != null) {
|
||||||
|
command.add("-metadata:s:s:" + i);
|
||||||
|
command.add("language=" + subtitleStream.getLanguage());
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.debug) {
|
||||||
|
FFMpegHelper.addDebugArguments(command, 50, 120);
|
||||||
|
}
|
||||||
|
|
||||||
|
command.add(outFile);
|
||||||
|
return command.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,9 @@
|
|||||||
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.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An object representation of a subtitle stream in a media file
|
* An object representation of a subtitle stream in a media file
|
||||||
*/
|
*/
|
||||||
@ -22,8 +26,8 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
|||||||
* @param file <p>The file containing the subtitle.</p>
|
* @param file <p>The file containing the subtitle.</p>
|
||||||
* @param isInternalStream <p>Whether this subtitle stream is in the video file itself</p>
|
* @param isInternalStream <p>Whether this subtitle stream is in the video file itself</p>
|
||||||
*/
|
*/
|
||||||
public SubtitleStream(String codecName, int absoluteIndex, int relativeIndex, String language, String title,
|
public SubtitleStream(@Nullable String codecName, int absoluteIndex, int relativeIndex, @Nullable String language,
|
||||||
String file, boolean isInternalStream) {
|
@Nullable String title, @NotNull String file, boolean isInternalStream) {
|
||||||
this.codecName = codecName;
|
this.codecName = codecName;
|
||||||
this.absoluteIndex = absoluteIndex;
|
this.absoluteIndex = absoluteIndex;
|
||||||
this.language = language;
|
this.language = language;
|
||||||
@ -33,6 +37,13 @@ public class SubtitleStream extends AbstractStream implements StreamObject {
|
|||||||
this.isImageSubtitle = isImageSubtitle();
|
this.isImageSubtitle = isImageSubtitle();
|
||||||
this.isInternalStream = isInternalStream;
|
this.isInternalStream = isInternalStream;
|
||||||
this.file = file;
|
this.file = file;
|
||||||
|
|
||||||
|
if (this.language == null || this.language.isEmpty()) {
|
||||||
|
String possibleLanguage = FileUtil.getLanguage(file);
|
||||||
|
if (possibleLanguage != null) {
|
||||||
|
this.language = possibleLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,6 +19,7 @@ import java.util.List;
|
|||||||
public final class FFMpegHelper {
|
public final class FFMpegHelper {
|
||||||
|
|
||||||
private static final String PROBE_SPLIT_CHARACTER = "øæåÆØå";
|
private static final String PROBE_SPLIT_CHARACTER = "øæåÆØå";
|
||||||
|
private static String[] subtitleFormats = null;
|
||||||
|
|
||||||
private FFMpegHelper() {
|
private FFMpegHelper() {
|
||||||
|
|
||||||
@ -305,7 +306,9 @@ public final class FFMpegHelper {
|
|||||||
throws IOException {
|
throws IOException {
|
||||||
List<StreamObject> parsedStreams = new ArrayList<>();
|
List<StreamObject> parsedStreams = new ArrayList<>();
|
||||||
//Find all files in the same directory with external subtitle formats
|
//Find all files in the same directory with external subtitle formats
|
||||||
String[] subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
if (subtitleFormats == null) {
|
||||||
|
subtitleFormats = FileUtil.readFileLines("subtitle_formats.txt");
|
||||||
|
}
|
||||||
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
File[] subtitleFiles = FileUtil.listFilesRecursive(directory, subtitleFormats, 1);
|
||||||
|
|
||||||
//Return early if no files were found
|
//Return early if no files were found
|
||||||
@ -315,9 +318,11 @@ public final class FFMpegHelper {
|
|||||||
|
|
||||||
String fileTitle = FileUtil.stripExtension(convertingFile);
|
String fileTitle = FileUtil.stripExtension(convertingFile);
|
||||||
List<File> subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles));
|
List<File> subtitleFilesList = new ArrayList<>(Arrays.asList(subtitleFiles));
|
||||||
|
|
||||||
//Finds the files which are subtitles probably belonging to the file
|
//Finds the files which are subtitles probably belonging to the file
|
||||||
subtitleFilesList = ListUtil.getMatching(subtitleFilesList,
|
subtitleFilesList = ListUtil.getMatching(subtitleFilesList,
|
||||||
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
(subtitleFile) -> subtitleFile.getName().contains(fileTitle));
|
||||||
|
|
||||||
for (File subtitleFile : subtitleFilesList) {
|
for (File subtitleFile : subtitleFilesList) {
|
||||||
//Probe the files and add them to the result list
|
//Probe the files and add them to the result list
|
||||||
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
String[] streams = probeForStreams(ffprobePath, subtitleFile);
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package net.knarcraft.ffmpegconverter.utility;
|
package net.knarcraft.ffmpegconverter.utility;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -139,4 +142,43 @@ 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
4
|
5
|
||||||
idx
|
idx
|
||||||
sub
|
sub
|
||||||
srt
|
srt
|
||||||
ass
|
ass
|
||||||
|
vtt
|
Loading…
Reference in New Issue
Block a user