Initial commit
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user