Initial commit

This commit is contained in:
Guillaume Tâche
2024-07-27 17:45:46 +02:00
commit 75829244b9
66 changed files with 2906 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
package com.github.gtache.autosubtitle.ffmpeg;
import com.github.gtache.autosubtitle.Audio;
import com.github.gtache.autosubtitle.File;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.impl.AudioInfoImpl;
import com.github.gtache.autosubtitle.impl.FileAudioImpl;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleConverter;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.SequencedMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.Objects.requireNonNull;
/**
* FFmpeg implementation of {@link VideoConverter}
*/
public class FFmpegVideoConverter implements VideoConverter {
private static final String TEMP_FILE_PREFIX = "autosubtitle";
private final Path ffmpegPath;
private final SubtitleConverter subtitleConverter;
@Inject
FFmpegVideoConverter(final Path ffmpegPath, final SubtitleConverter subtitleConverter) {
this.ffmpegPath = requireNonNull(ffmpegPath);
this.subtitleConverter = requireNonNull(subtitleConverter);
}
@Override
public Video addSoftSubtitles(final Video video, final Collection<SubtitleCollection> subtitles) throws IOException {
final var videoPath = getPath(video);
final var collectionMap = dumpCollections(subtitles);
final var out = getTempFile("mkv"); //Soft subtitles are only supported by mkv apparently
final var args = new ArrayList<String>();
args.add(ffmpegPath.toString());
args.add("-i");
args.add(videoPath.toString());
collectionMap.forEach((c, p) -> {
args.add("-i");
args.add(p.toString());
});
args.add("-c");
args.add("copy");
args.add("-map");
args.add("0:v");
args.add("-map");
args.add("0:a");
final var i = new AtomicInteger(1);
collectionMap.forEach((c, p) -> {
final var n = i.getAndIncrement();
args.add("-map");
args.add(String.valueOf(n));
args.add("-metadata:s:s:" + n);
args.add("language=" + c.language());
});
args.add(out.toString());
run(args);
return new FileVideoImpl(out, new VideoInfoImpl("mkv"));
}
@Override
public Video addHardSubtitles(final Video video, final SubtitleCollection subtitles) throws IOException {
final var videoPath = getPath(video);
final var subtitlesPath = dumpSubtitles(subtitles);
final var out = getTempFile(video.info().videoFormat());
final var subtitleArg = subtitleConverter.formatName().equalsIgnoreCase("ass") ? "ass=" + subtitlesPath : "subtitles=" + subtitlesPath;
final var args = List.of(
ffmpegPath.toString(),
"-i",
videoPath.toString(),
"-vf",
subtitleArg,
out.toString()
);
run(args);
return new FileVideoImpl(out, video.info());
}
@Override
public Audio getAudio(final Video video) throws IOException {
final var videoPath = getPath(video);
final var audioPath = getTempFile(".wav");
final var dumpVideoPath = getTempFile("." + video.info().videoFormat());
final var args = List.of(
ffmpegPath.toString(),
"-i",
videoPath.toString(),
"-map",
"0:a",
audioPath.toString(),
"-map",
"0:v",
dumpVideoPath.toString()
);
run(args);
Files.deleteIfExists(dumpVideoPath);
return new FileAudioImpl(audioPath, new AudioInfoImpl("wav"));
}
private static Path getPath(final Video video) throws IOException {
if (video instanceof final File f) {
return f.path();
} else {
return dumpVideo(video);
}
}
private static Path dumpVideo(final Video video) throws IOException {
final var path = getTempFile(video.info().videoFormat());
try (final var out = Files.newOutputStream(path)) {
video.getInputStream().transferTo(out);
}
return path;
}
private SequencedMap<SubtitleCollection, Path> dumpCollections(final Collection<SubtitleCollection> collections) throws IOException {
final var ret = new LinkedHashMap<SubtitleCollection, Path>(collections.size());
for (final var subtitles : collections) {
ret.put(subtitles, dumpSubtitles(subtitles));
}
return ret;
}
private Path dumpSubtitles(final SubtitleCollection subtitles) throws IOException {
final var path = getTempFile("ass");
Files.writeString(path, subtitleConverter.convert(subtitles));
return path;
}
private static Path getTempFile(final String extension) throws IOException {
final var path = Files.createTempFile(TEMP_FILE_PREFIX, "." + extension);
path.toFile().deleteOnExit();
return path;
}
private void run(final String... args) throws IOException {
run(Arrays.asList(args));
}
private void run(final List<String> args) throws IOException {
final var builder = new ProcessBuilder(args);
builder.inheritIO();
builder.redirectErrorStream(true);
final var process = builder.start();
try {
process.waitFor(1, TimeUnit.HOURS);
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
process.destroy();
}
if (process.exitValue() != 0) {
throw new IOException("FFmpeg exited with code " + process.exitValue());
}
}
}

View File

@@ -0,0 +1,42 @@
package com.github.gtache.autosubtitle.setup.ffmpeg;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
/**
* Manager managing the FFmpeg installation
*/
public class FFmpegSetupManager implements SetupManager {
@Override
public boolean isInstalled() throws SetupException {
return checkSystemFFmpeg() || checkBundledFFmpeg();
}
@Override
public void install() throws SetupException {
}
@Override
public void uninstall() throws SetupException {
}
@Override
public boolean isUpdateAvailable() throws SetupException {
return false;
}
@Override
public void update() throws SetupException {
}
private boolean checkSystemFFmpeg() {
return false;
}
private boolean checkBundledFFmpeg() {
return false;
}
}