Can playback video with controls, need to fix performance and reducing window size

This commit is contained in:
Guillaume Tâche
2024-08-01 19:42:25 +02:00
parent 75829244b9
commit a94eaff9ad
102 changed files with 2921 additions and 423 deletions

View File

@@ -8,20 +8,22 @@ 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.modules.ffmpeg.FFmpegBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.SubtitleConverter;
import javax.inject.Inject;
import javax.inject.Singleton;
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;
@@ -29,15 +31,18 @@ import static java.util.Objects.requireNonNull;
/**
* FFmpeg implementation of {@link VideoConverter}
*/
public class FFmpegVideoConverter implements VideoConverter {
@Singleton
public class FFmpegVideoConverter extends AbstractProcessRunner implements VideoConverter {
private static final String TEMP_FILE_PREFIX = "autosubtitle";
private final Path ffmpegPath;
private final Path bundledPath;
private final Path systemPath;
private final SubtitleConverter subtitleConverter;
@Inject
FFmpegVideoConverter(final Path ffmpegPath, final SubtitleConverter subtitleConverter) {
this.ffmpegPath = requireNonNull(ffmpegPath);
FFmpegVideoConverter(@FFmpegBundledPath final Path bundledPath, @FFmpegSystemPath final Path systemPath, final SubtitleConverter subtitleConverter) {
this.bundledPath = requireNonNull(bundledPath);
this.systemPath = requireNonNull(systemPath);
this.subtitleConverter = requireNonNull(subtitleConverter);
}
@@ -47,7 +52,7 @@ public class FFmpegVideoConverter implements VideoConverter {
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(getFFmpegPath());
args.add("-i");
args.add(videoPath.toString());
collectionMap.forEach((c, p) -> {
@@ -66,11 +71,11 @@ public class FFmpegVideoConverter implements VideoConverter {
args.add("-map");
args.add(String.valueOf(n));
args.add("-metadata:s:s:" + n);
args.add("language=" + c.language());
args.add("language=" + c.locale().getISO3Language());
});
args.add(out.toString());
run(args);
return new FileVideoImpl(out, new VideoInfoImpl("mkv"));
return new FileVideoImpl(out, new VideoInfoImpl("mkv", video.info().width(), video.info().height(), video.info().duration()));
}
@Override
@@ -80,7 +85,7 @@ public class FFmpegVideoConverter implements VideoConverter {
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(),
getFFmpegPath(),
"-i",
videoPath.toString(),
"-vf",
@@ -97,7 +102,7 @@ public class FFmpegVideoConverter implements VideoConverter {
final var audioPath = getTempFile(".wav");
final var dumpVideoPath = getTempFile("." + video.info().videoFormat());
final var args = List.of(
ffmpegPath.toString(),
getFFmpegPath(),
"-i",
videoPath.toString(),
"-map",
@@ -129,7 +134,7 @@ public class FFmpegVideoConverter implements VideoConverter {
}
private SequencedMap<SubtitleCollection, Path> dumpCollections(final Collection<SubtitleCollection> collections) throws IOException {
final var ret = new LinkedHashMap<SubtitleCollection, Path>(collections.size());
final var ret = LinkedHashMap.<SubtitleCollection, Path>newLinkedHashMap(collections.size());
for (final var subtitles : collections) {
ret.put(subtitles, dumpSubtitles(subtitles));
}
@@ -148,23 +153,7 @@ public class FFmpegVideoConverter implements VideoConverter {
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());
}
private String getFFmpegPath() {
return Files.isRegularFile(bundledPath) ? bundledPath.toString() : systemPath.toString();
}
}

View File

@@ -0,0 +1,51 @@
package com.github.gtache.autosubtitle.ffmpeg;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.util.Objects.requireNonNull;
/**
* FFprobe implementation of {@link VideoLoader}
*/
@Singleton
public class FFprobeVideoLoader extends AbstractProcessRunner implements VideoLoader {
private final Path bundledPath;
private final Path systemPath;
@Inject
FFprobeVideoLoader(@FFprobeBundledPath final Path bundledPath, @FFprobeSystemPath final Path systemPath) {
this.bundledPath = requireNonNull(bundledPath);
this.systemPath = requireNonNull(systemPath);
}
@Override
public Video loadVideo(final Path path) throws IOException {
final var result = run(getFFprobePath(), "-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height,duration", "-of", "csv=p=0", path.toString());
final var resolution = result.output().getLast();
final var split = resolution.split(",");
final var width = Integer.parseInt(split[0]);
final var height = Integer.parseInt(split[1]);
final var filename = path.getFileName().toString();
final var extension = filename.substring(filename.lastIndexOf('.') + 1);
final var duration = (long) (Double.parseDouble(split[2]) * 1000L);
final var info = new VideoInfoImpl(extension, width, height, duration);
return new FileVideoImpl(path, info);
}
private String getFFprobePath() {
return Files.isRegularFile(bundledPath) ? bundledPath.toString() : systemPath.toString();
}
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FFBundledRoot {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FFmpegBundledPath {
}

View File

@@ -0,0 +1,23 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import com.github.gtache.autosubtitle.VideoConverter;
import com.github.gtache.autosubtitle.VideoLoader;
import com.github.gtache.autosubtitle.ffmpeg.FFmpegVideoConverter;
import com.github.gtache.autosubtitle.ffmpeg.FFprobeVideoLoader;
import dagger.Binds;
import dagger.Module;
import javax.inject.Singleton;
@Module
public interface FFmpegModule {
@Binds
@Singleton
VideoConverter bindsVideoConverter(final FFmpegVideoConverter converter);
@Binds
@Singleton
VideoLoader bindsVideoLoader(final FFprobeVideoLoader loader);
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FFmpegSystemPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FFmpegVersion {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FFprobeBundledPath {
}

View File

@@ -0,0 +1,16 @@
package com.github.gtache.autosubtitle.modules.ffmpeg;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
public @interface FFprobeSystemPath {
}

View File

@@ -1,15 +1,62 @@
package com.github.gtache.autosubtitle.setup.ffmpeg;
import com.github.gtache.autosubtitle.impl.Architecture;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
/**
* Manager managing the FFmpeg installation
*/
public class FFmpegSetupManager implements SetupManager {
public class FFmpegSetupManager extends AbstractProcessRunner implements SetupManager {
private static final Logger logger = LogManager.getLogger(FFmpegSetupManager.class);
private final Path bundledPath;
private final Path systemPath;
private final String version;
private final OS os;
private final Architecture architecture;
private final String executableExtension;
@Inject
FFmpegSetupManager(@FFmpegBundledPath final Path bundledPath, @FFmpegSystemPath final Path systemPath, @FFmpegVersion final String version, final OS os, final Architecture architecture) {
this.bundledPath = Objects.requireNonNull(bundledPath);
this.systemPath = Objects.requireNonNull(systemPath);
this.version = Objects.requireNonNull(version);
this.os = Objects.requireNonNull(os);
this.architecture = Objects.requireNonNull(architecture);
this.executableExtension = os == OS.WINDOWS ? ".exe" : "";
}
@Override
public boolean isInstalled() throws SetupException {
return checkSystemFFmpeg() || checkBundledFFmpeg();
public String name() {
return "FFmpeg";
}
@Override
public SetupStatus status() {
try {
if (checkSystemFFmpeg() || checkBundledFFmpeg()) {
return SetupStatus.INSTALLED;
} else {
return SetupStatus.NOT_INSTALLED;
}
} catch (final IOException e) {
logger.error("Error checking status of {}", name(), e);
return SetupStatus.ERRORED;
}
}
@Override
@@ -22,21 +69,17 @@ public class FFmpegSetupManager implements SetupManager {
}
@Override
public boolean isUpdateAvailable() throws SetupException {
return false;
}
@Override
public void update() throws SetupException {
}
private boolean checkSystemFFmpeg() {
return false;
private boolean checkSystemFFmpeg() throws IOException {
final var result = run(systemPath.toString(), "-h");
return result.exitCode() == 0;
}
private boolean checkBundledFFmpeg() {
return false;
private boolean checkBundledFFmpeg() throws IOException {
return Files.isRegularFile(bundledPath);
}
}

View File

@@ -0,0 +1,75 @@
package com.github.gtache.autosubtitle.setup.modules.ffmpeg;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFBundledRoot;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegSystemPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFmpegVersion;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeBundledPath;
import com.github.gtache.autosubtitle.modules.ffmpeg.FFprobeSystemPath;
import com.github.gtache.autosubtitle.modules.impl.ExecutableExtension;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.ffmpeg.FFmpegSetupManager;
import com.github.gtache.autosubtitle.setup.modules.impl.VideoConverterSetup;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Dagger module for FFmpeg setup
*/
@Module
public abstract class FFmpegSetupModule {
private static final String FFMPEG = "ffmpeg";
private static final String FFPROBE = "ffprobe";
@Binds
@VideoConverterSetup
abstract SetupManager bindsFFmpegSetupManager(final FFmpegSetupManager manager);
@Provides
@Singleton
@FFBundledRoot
static Path providesFFBundledRoot() {
return Paths.get("tools", FFMPEG);
}
@Provides
@Singleton
@FFprobeBundledPath
static Path providesFFProbeBundledPath(@FFBundledRoot final Path root, @ExecutableExtension final String extension) {
return root.resolve(FFPROBE + extension);
}
@Provides
@Singleton
@FFprobeSystemPath
static Path providesFFProbeSystemPath(@ExecutableExtension final String extension) {
return Paths.get(FFPROBE + extension);
}
@Provides
@Singleton
@FFmpegBundledPath
static Path providesFFmpegBundledPath(@FFBundledRoot final Path root, @ExecutableExtension final String extension) {
return root.resolve(FFMPEG + extension);
}
@Provides
@Singleton
@FFmpegSystemPath
static Path providesFFmpegSystemPath(@ExecutableExtension final String extension) {
return Paths.get(FFMPEG + extension);
}
@Provides
@Singleton
@FFmpegVersion
static String providesFFmpegVersion() {
return "7.0.1";
}
}

View File

@@ -0,0 +1,14 @@
/**
* FFmpeg module for auto-subtitle
*/
module com.github.gtache.autosubtitle.ffmpeg {
requires transitive com.github.gtache.autosubtitle.core;
requires transitive dagger;
requires transitive javax.inject;
requires org.apache.logging.log4j;
exports com.github.gtache.autosubtitle.ffmpeg;
exports com.github.gtache.autosubtitle.modules.ffmpeg;
exports com.github.gtache.autosubtitle.setup.ffmpeg;
exports com.github.gtache.autosubtitle.setup.modules.ffmpeg;
}