Skip to content

Commit

Permalink
Full impl of the Process Listener Feature.
Browse files Browse the repository at this point in the history
Now with a test and a default solution included for easy use.
  • Loading branch information
Speiger committed Nov 2, 2024
1 parent d6a9051 commit ace6712
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 2 deletions.
17 changes: 17 additions & 0 deletions src/main/java/com/github/kokorin/jaffree/ffmpeg/FFmpeg.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -49,6 +50,7 @@ public class FFmpeg {
private final List<String> additionalArguments = new ArrayList<>();
private boolean overwriteOutput;
private ProgressListener progressListener;
private ProcessListener processListener;
private OutputListener outputListener;
private String progress;
//-filter_threads nb_threads (global)
Expand Down Expand Up @@ -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
* <p>
* 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.
Expand Down Expand Up @@ -506,6 +522,7 @@ protected ProcessHandler<FFmpegResult> createProcessHandler() {
.setStdErrReader(createStdErrReader(outputListener))
.setStdOutReader(createStdOutReader())
.setHelpers(helpers)
.setProcessListener(processListener)
.setArguments(buildArguments());
if (executorTimeoutMillis != null) {
processHandler.setExecutorTimeoutMillis(executorTimeoutMillis);
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/github/kokorin/jaffree/ffprobe/FFprobe.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,7 @@ public class FFprobe {
private Input input;

private FormatParser formatParser = new JsonFormatParser();
private ProcessListener processListener;

private final Path executable;

Expand Down Expand Up @@ -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
* <p>
* 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.
Expand Down Expand Up @@ -545,6 +561,7 @@ public FFprobeResult execute() {
.setStdOutReader(createStdOutReader(formatParser))
.setStdErrReader(createStdErrReader())
.setHelpers(helpers)
.setProcessListener(processListener)
.setArguments(buildArguments())
.execute();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class ProcessHandler<T> {
private StdReader<T> stdOutReader = new GobblingStdReader<>();
private StdReader<T> stdErrReader = new GobblingStdReader<>();
private List<ProcessHelper> helpers = null;
private ProcessListener listener;
private Stopper stopper = null;
private List<String> arguments = Collections.emptyList();
private int executorTimeoutMillis = DEFAULT_EXECUTOR_TIMEOUT_MILLIS;
Expand Down Expand Up @@ -98,7 +99,18 @@ public synchronized ProcessHandler<T> setHelpers(final List<ProcessHelper> 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<T> setProcessListener(ProcessListener listener) {
this.listener = listener;
return this;
}

/**
* Sets {@link Stopper} which can be used to interrupt program execution.
*
Expand All @@ -109,7 +121,7 @@ public synchronized ProcessHandler<T> setStopper(final Stopper stopper) {
this.stopper = stopper;
return this;
}

/**
* Sets arguments list to pass to a program.
*
Expand Down Expand Up @@ -158,13 +170,20 @@ public synchronized T execute() {
if (stopper != null) {
stopper.setProcess(process);
}
if(listener != null) {
listener.onStart(process);
}

return interactWithProcess(process);
} catch (IOException e) {
collectDebugInformation();
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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Process> instances) {
return new Impl(instances);
}

static class Impl implements ProcessListener {
Set<Process> instances;

public Impl(Set<Process> instances) {
this.instances = instances;
}

@Override
public void onStart(Process process) {
instances.add(process);
}

@Override
public void onStop(Process process) {
instances.remove(process);
}

}
}
43 changes: 43 additions & 0 deletions src/test/java/com/github/kokorin/jaffree/ffmpeg/FFmpegTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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");
Expand Down
33 changes: 33 additions & 0 deletions src/test/java/com/github/kokorin/jaffree/ffprobe/FFprobeTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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)
Expand Down

0 comments on commit ace6712

Please sign in to comment.