From ace67125c8a56d2c66e8b53bd26533e1a4e6f442 Mon Sep 17 00:00:00 2001 From: Speiger Date: Sat, 2 Nov 2024 17:12:41 +0100 Subject: [PATCH 1/3] Full impl of the Process Listener Feature. Now with a test and a default solution included for easy use. --- .../github/kokorin/jaffree/ffmpeg/FFmpeg.java | 17 ++++++++ .../kokorin/jaffree/ffprobe/FFprobe.java | 17 ++++++++ .../jaffree/process/ProcessHandler.java | 23 +++++++++- .../jaffree/process/ProcessListener.java | 42 ++++++++++++++++++ .../kokorin/jaffree/ffmpeg/FFmpegTest.java | 43 +++++++++++++++++++ .../kokorin/jaffree/ffprobe/FFprobeTest.java | 33 ++++++++++++++ 6 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java diff --git a/src/main/java/com/github/kokorin/jaffree/ffmpeg/FFmpeg.java b/src/main/java/com/github/kokorin/jaffree/ffmpeg/FFmpeg.java index 0edf7932..1352524e 100644 --- a/src/main/java/com/github/kokorin/jaffree/ffmpeg/FFmpeg.java +++ b/src/main/java/com/github/kokorin/jaffree/ffmpeg/FFmpeg.java @@ -23,6 +23,7 @@ import com.github.kokorin.jaffree.process.LoggingStdReader; import com.github.kokorin.jaffree.process.ProcessHandler; import com.github.kokorin.jaffree.process.ProcessHelper; +import com.github.kokorin.jaffree.process.ProcessListener; import com.github.kokorin.jaffree.process.StdReader; import com.github.kokorin.jaffree.process.Stopper; import org.slf4j.Logger; @@ -49,6 +50,7 @@ public class FFmpeg { private final List additionalArguments = new ArrayList<>(); private boolean overwriteOutput; private ProgressListener progressListener; + private ProcessListener processListener; private OutputListener outputListener; private String progress; //-filter_threads nb_threads (global) @@ -343,6 +345,20 @@ public FFmpeg setOutputListener(final OutputListener outputListener) { this.outputListener = outputListener; return this; } + + /** + * Send a Process Listener to receive the Process Instance when FFMpeg is executed + *

+ * This can really help when more than basic controls are required and/or you want to keep track of all the ffpmeg instances going around. + * Note: Use with Responsibility! + * + * @param processListener process listener + * @return this + */ + public FFmpeg setProcessListener(final ProcessListener processListener) { + this.processListener = processListener; + return this; + } /** * Send program-friendly progress information to url. @@ -506,6 +522,7 @@ protected ProcessHandler createProcessHandler() { .setStdErrReader(createStdErrReader(outputListener)) .setStdOutReader(createStdOutReader()) .setHelpers(helpers) + .setProcessListener(processListener) .setArguments(buildArguments()); if (executorTimeoutMillis != null) { processHandler.setExecutorTimeoutMillis(executorTimeoutMillis); diff --git a/src/main/java/com/github/kokorin/jaffree/ffprobe/FFprobe.java b/src/main/java/com/github/kokorin/jaffree/ffprobe/FFprobe.java index 9e58bb4c..70b13d0f 100644 --- a/src/main/java/com/github/kokorin/jaffree/ffprobe/FFprobe.java +++ b/src/main/java/com/github/kokorin/jaffree/ffprobe/FFprobe.java @@ -23,6 +23,7 @@ import com.github.kokorin.jaffree.ffprobe.data.JsonFormatParser; import com.github.kokorin.jaffree.process.ProcessHandler; import com.github.kokorin.jaffree.process.ProcessHelper; +import com.github.kokorin.jaffree.process.ProcessListener; import com.github.kokorin.jaffree.process.StdReader; import java.io.InputStream; @@ -69,6 +70,7 @@ public class FFprobe { private Input input; private FormatParser formatParser = new JsonFormatParser(); + private ProcessListener processListener; private final Path executable; @@ -488,6 +490,20 @@ public FFprobe setFormatParser(final FormatParser formatParser) { this.formatParser = formatParser; return this; } + + /** + * Send a Process Listener to receive the Process Instance when FFMpeg is executed + *

+ * This can really help when more than basic controls are required and/or you want to keep track of all the ffpmeg instances going around. + * Note: Use with Responsibility! + * + * @param processListener process listener + * @return this + */ + public FFprobe setProcessListener(final ProcessListener processListener) { + this.processListener = processListener; + return this; + } /** * Sets ffprobe logging level. @@ -545,6 +561,7 @@ public FFprobeResult execute() { .setStdOutReader(createStdOutReader(formatParser)) .setStdErrReader(createStdErrReader()) .setHelpers(helpers) + .setProcessListener(processListener) .setArguments(buildArguments()) .execute(); } diff --git a/src/main/java/com/github/kokorin/jaffree/process/ProcessHandler.java b/src/main/java/com/github/kokorin/jaffree/process/ProcessHandler.java index 4ed9d280..ba24f640 100644 --- a/src/main/java/com/github/kokorin/jaffree/process/ProcessHandler.java +++ b/src/main/java/com/github/kokorin/jaffree/process/ProcessHandler.java @@ -47,6 +47,7 @@ public class ProcessHandler { private StdReader stdOutReader = new GobblingStdReader<>(); private StdReader stdErrReader = new GobblingStdReader<>(); private List helpers = null; + private ProcessListener listener; private Stopper stopper = null; private List arguments = Collections.emptyList(); private int executorTimeoutMillis = DEFAULT_EXECUTOR_TIMEOUT_MILLIS; @@ -98,7 +99,18 @@ public synchronized ProcessHandler setHelpers(final List helpe this.helpers = helpers; return this; } - + + /** + * Sets {@link ProcessListener} which can be used to track program execution. + * + * @param listener listener + * @return this + */ + public synchronized ProcessHandler setProcessListener(ProcessListener listener) { + this.listener = listener; + return this; + } + /** * Sets {@link Stopper} which can be used to interrupt program execution. * @@ -109,7 +121,7 @@ public synchronized ProcessHandler setStopper(final Stopper stopper) { this.stopper = stopper; return this; } - + /** * Sets arguments list to pass to a program. * @@ -158,6 +170,9 @@ public synchronized T execute() { if (stopper != null) { stopper.setProcess(process); } + if(listener != null) { + listener.onStart(process); + } return interactWithProcess(process); } catch (IOException e) { @@ -165,6 +180,10 @@ public synchronized T execute() { throw new JaffreeException("Failed to start process.", e); } finally { if (process != null) { + if(listener != null) { + //Done before the Process is destroyed just in case. + listener.onStop(process); + } process.destroy(); // Process must be destroyed before closing streams, can't use // try-with-resources, as resources are closing when leaving try block, diff --git a/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java b/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java new file mode 100644 index 00000000..1a219271 --- /dev/null +++ b/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java @@ -0,0 +1,42 @@ +package com.github.kokorin.jaffree.process; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Speiger + */ +public interface ProcessListener { + public void onStart(Process process); + public void onStop(Process process); + + /** + * Simple tracker wrapper that allows to track all instances being loaded. + * + * @param instances Set. Highly Suggest {@link Collections#newSetFromMap} using a {@link ConcurrentHashMap} for multithreading support + * @return ProcessListener wrapper + */ + public static ProcessListener of(Set instances) { + return new Impl(instances); + } + + static class Impl implements ProcessListener { + Set instances; + + public Impl(Set instances) { + this.instances = instances; + } + + @Override + public void onStart(Process process) { + instances.add(process); + } + + @Override + public void onStop(Process process) { + instances.remove(process); + } + + } +} diff --git a/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java b/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java index c1c99d44..7d78ffa9 100644 --- a/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java +++ b/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java @@ -9,6 +9,7 @@ import com.github.kokorin.jaffree.ffprobe.FFprobeResult; import com.github.kokorin.jaffree.ffprobe.Stream; import com.github.kokorin.jaffree.process.ProcessHelper; +import com.github.kokorin.jaffree.process.ProcessListener; import com.github.kokorin.jaffree.process.JaffreeAbnormalExitException; import org.hamcrest.core.AllOf; import org.hamcrest.core.StringContains; @@ -29,10 +30,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -55,7 +59,46 @@ public class FFmpegTest { @Rule public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void testProcessTracking() throws Exception { + AtomicLong started = new AtomicLong(); + AtomicLong stopped = new AtomicLong(); + ProcessListener listener = new ProcessListener() { + @Override + public void onStart(Process process) { started.getAndAdd(1); } + @Override + public void onStop(Process process) { stopped.getAndAdd(1); } + }; + + Path tempDir = Files.createTempDirectory("jaffree"); + Path outputPath = tempDir.resolve("test.mkv"); + + FFmpegResult result = FFmpeg.atPath(Config.FFMPEG_BIN) + .addInput(UrlInput.fromPath(Artifacts.VIDEO_FLV)) + .addOutput(UrlOutput.toPath(outputPath)) + .setProcessListener(listener) + .execute(); + + Assert.assertNotNull(result); + assertTrue("Process was never started", started.get() > 0); + assertTrue("Process was never stopped", stopped.get() > 0); + outputPath = tempDir.resolve("test.flv"); + started.set(0L); + stopped.set(0L); + + result = FFmpeg.atPath(Config.FFMPEG_BIN) + .addInput(UrlInput.fromPath(Artifacts.SMALL_MP4)) + .addOutput(UrlOutput.toPath(outputPath)) + .setProcessListener(listener) + .execute(); + + Assert.assertNotNull(result); + assertTrue("Process was never started", started.get() > 0); + assertTrue("Process was never stopped", stopped.get() > 0); + } + @Test public void testSimpleCopy() throws Exception { Path tempDir = Files.createTempDirectory("jaffree"); diff --git a/src/test/java/com/github/kokorin/jaffree/ffprobe/FFprobeTest.java b/src/test/java/com/github/kokorin/jaffree/ffprobe/FFprobeTest.java index 8cb961eb..bf2a7a84 100644 --- a/src/test/java/com/github/kokorin/jaffree/ffprobe/FFprobeTest.java +++ b/src/test/java/com/github/kokorin/jaffree/ffprobe/FFprobeTest.java @@ -10,6 +10,8 @@ import com.github.kokorin.jaffree.ffprobe.data.FormatParser; import com.github.kokorin.jaffree.ffprobe.data.JsonFormatParser; import com.github.kokorin.jaffree.process.JaffreeAbnormalExitException; +import com.github.kokorin.jaffree.process.ProcessListener; + import org.junit.Assert; import org.junit.Ignore; import org.junit.Rule; @@ -28,6 +30,7 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.IsCollectionContaining.hasItems; @@ -54,7 +57,37 @@ public FFprobeTest(FormatParser formatParser) { } //private boolean showData; + @Test + public void testProcessListener() throws Exception { + AtomicLong started = new AtomicLong(); + AtomicLong stopped = new AtomicLong(); + ProcessListener listener = new ProcessListener() { + @Override + public void onStart(Process process) { started.getAndAdd(1); } + @Override + public void onStop(Process process) { stopped.getAndAdd(1); } + }; + + FFprobeResult result = FFprobe.atPath(Config.FFMPEG_BIN) + .setInput(Artifacts.VIDEO_MP4) + .setShowData(true) + .setShowStreams(true) + .setFormatParser(formatParser) + .setProcessListener(listener) + .execute(); + assertNotNull(result); + assertNotNull(result.getStreams()); + assertFalse(result.getStreams().isEmpty()); + + Stream stream = result.getStreams().get(0); + assertNotNull(stream.getExtradata()); + assertEquals(Rational.valueOf(30L), stream.getAvgFrameRate()); + assertTrue("Process was never started", started.get() > 0); + assertTrue("Process was never stopped", stopped.get() > 0); + } + + @Test public void testShowDataWithShowStreams() throws Exception { FFprobeResult result = FFprobe.atPath(Config.FFMPEG_BIN) From 1d6c523d2639189b7a5f3f9a2a39da39ed855f52 Mon Sep 17 00:00:00 2001 From: Speiger Date: Sat, 2 Nov 2024 17:20:18 +0100 Subject: [PATCH 2/3] Removing unused Imports --- .../java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java b/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java index 7d78ffa9..91d845e4 100644 --- a/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java +++ b/src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java @@ -30,13 +30,10 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; From d3ae1cc497ab89caf16c1061c44cf5d2e51f990d Mon Sep 17 00:00:00 2001 From: Speiger Date: Tue, 5 Nov 2024 14:03:52 +0100 Subject: [PATCH 3/3] Updated Process Listener for things that made sense to do. --- .../jaffree/process/ProcessListener.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java b/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java index 1a219271..279009cb 100644 --- a/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java +++ b/src/main/java/com/github/kokorin/jaffree/process/ProcessListener.java @@ -8,8 +8,17 @@ * @author Speiger */ public interface ProcessListener { - public void onStart(Process process); - public void onStop(Process process); + /** + * Provides the Process instance that was started + * @param process the process + */ + void onStart(Process process); + + /** + * Provides the Process instance that was just completed + * @param process the process + */ + void onStop(Process process); /** * Simple tracker wrapper that allows to track all instances being loaded. @@ -17,7 +26,7 @@ public interface ProcessListener { * @param instances Set. Highly Suggest {@link Collections#newSetFromMap} using a {@link ConcurrentHashMap} for multithreading support * @return ProcessListener wrapper */ - public static ProcessListener of(Set instances) { + static ProcessListener of(Set instances) { return new Impl(instances); } @@ -27,12 +36,12 @@ static class Impl implements ProcessListener { public Impl(Set instances) { this.instances = instances; } - + @Override public void onStart(Process process) { instances.add(process); } - + @Override public void onStop(Process process) { instances.remove(process);