Skip to content

Commit 5db3adf

Browse files
committed
Merge pull request #164 from JLLeitschuh/fix/webcamSourceSelect
Source Refactor with MultiImageFileSource
2 parents 8663b73 + 12be42b commit 5db3adf

File tree

31 files changed

+1112
-184
lines changed

31 files changed

+1112
-184
lines changed

build.gradle

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ allprojects {
5050
// Turn on test results
5151
test {
5252
testLogging {
53-
afterSuite { desc, result ->
54-
if (!desc.parent && project.hasProperty('printTestResults')) {
55-
println "Test results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)"
56-
}
57-
}
53+
events "failed"
54+
exceptionFormat "full"
5855
}
5956
}
6057
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package edu.wpi.grip.core;
2+
3+
4+
/**
5+
* An Object that can switch its value.
6+
*/
7+
public interface PreviousNext {
8+
9+
/**
10+
* Perform the next action on this object.
11+
*/
12+
void next();
13+
14+
/**
15+
* Perform the previous action on this object.
16+
*/
17+
void previous();
18+
19+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package edu.wpi.grip.core;
2+
3+
4+
import com.google.common.eventbus.EventBus;
5+
6+
import java.io.IOException;
7+
import java.util.concurrent.TimeoutException;
8+
9+
/**
10+
* An Object that can be stopped and started multiple times.
11+
*/
12+
public interface StartStoppable {
13+
14+
/**
15+
* Starts this StartStoppable
16+
*
17+
* @return Itself
18+
* @throws IOException If cleaning up some system resource fails
19+
*/
20+
default <T extends StartStoppable> T start(EventBus eventBus) throws IOException {
21+
start();
22+
eventBus.register(this);
23+
return (T) this;
24+
}
25+
26+
/**
27+
* Any method that overrides this method should post a {@link edu.wpi.grip.core.events.StartedStoppedEvent}
28+
* to the {@link EventBus} if is successfully starts.
29+
*
30+
* @throws IOException If cleaning up some system resource fails
31+
*/
32+
void start() throws IOException;
33+
34+
/**
35+
* Any method that overrides this method should post a {@link edu.wpi.grip.core.events.StartedStoppedEvent}
36+
* to the {@link EventBus} if is successfully stops.
37+
*
38+
* @throws TimeoutException If the thread fails to stop in a timely manner
39+
* @throws IOException If cleaning up some system resource fails.
40+
*/
41+
void stop() throws TimeoutException, IOException;
42+
43+
/**
44+
* Used to indicate if the source is running or stopped
45+
*
46+
* @return true if this source is running
47+
*/
48+
boolean isStarted();
49+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package edu.wpi.grip.core.events;
2+
3+
4+
import edu.wpi.grip.core.StartStoppable;
5+
6+
/**
7+
* An event that occurs when a {@link StartStoppable StartStoppable's} state changes.
8+
*/
9+
public class StartedStoppedEvent {
10+
private final StartStoppable startStoppable;
11+
12+
public StartedStoppedEvent(StartStoppable startStoppable) {
13+
this.startStoppable = startStoppable;
14+
}
15+
16+
public StartStoppable getStartStoppable() {
17+
return this.startStoppable;
18+
}
19+
}

core/src/main/java/edu/wpi/grip/core/sources/CameraSource.java

Lines changed: 96 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
import com.google.common.eventbus.EventBus;
66
import com.google.common.eventbus.Subscribe;
77
import com.thoughtworks.xstream.annotations.XStreamAlias;
8-
import edu.wpi.grip.core.OutputSocket;
9-
import edu.wpi.grip.core.SocketHint;
10-
import edu.wpi.grip.core.SocketHints;
11-
import edu.wpi.grip.core.Source;
12-
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
8+
import edu.wpi.grip.core.*;
139
import edu.wpi.grip.core.events.SourceRemovedEvent;
10+
import edu.wpi.grip.core.events.StartedStoppedEvent;
11+
import edu.wpi.grip.core.events.UnexpectedThrowableEvent;
1412
import org.bytedeco.javacpp.opencv_core.Mat;
1513
import org.bytedeco.javacv.*;
1614

@@ -27,7 +25,7 @@
2725
* Provides a way to generate a constantly updated {@link Mat} from a camera
2826
*/
2927
@XStreamAlias(value = "grip:Camera")
30-
public class CameraSource extends Source {
28+
public final class CameraSource extends Source implements StartStoppable {
3129

3230
private final static String DEVICE_NUMBER_PROPERTY = "deviceNumber";
3331
private final static String ADDRESS_PROPERTY = "address";
@@ -42,7 +40,7 @@ public class CameraSource extends Source {
4240
private OutputSocket<Mat> frameOutputSocket;
4341
private OutputSocket<Number> frameRateOutputSocket;
4442
private Optional<Thread> frameThread;
45-
private Optional<FrameGrabber> grabber;
43+
private FrameGrabber grabber;
4644

4745
/**
4846
* Creates a camera source that can be used as an input to a pipeline
@@ -72,7 +70,6 @@ public CameraSource(EventBus eventBus, String address) throws IOException {
7270
* Used for serialization
7371
*/
7472
public CameraSource() {
75-
this.grabber = Optional.empty();
7673
this.frameThread = Optional.empty();
7774
}
7875

@@ -81,9 +78,7 @@ private void initialize(EventBus eventBus, FrameGrabber frameGrabber, String nam
8178
this.name = name;
8279
this.frameOutputSocket = new OutputSocket<>(eventBus, imageOutputHint);
8380
this.frameRateOutputSocket = new OutputSocket<>(eventBus, frameRateOutputHint);
84-
85-
this.eventBus.register(this);
86-
this.startVideo(frameGrabber);
81+
this.grabber = frameGrabber;
8782
}
8883

8984
@Override
@@ -127,109 +122,119 @@ public void createFromProperties(EventBus eventBus, Properties properties) throw
127122
}
128123

129124
/**
130-
* Starts the video capture from the
131-
*
132-
* @param grabber A JavaCV {@link FrameGrabber} instance to capture from
125+
* Starts the video capture from this frame grabber.
133126
*/
134-
private synchronized void startVideo(FrameGrabber grabber) throws IOException {
127+
public void start() throws IOException, IllegalStateException {
135128
final OpenCVFrameConverter.ToMat convertToMat = new OpenCVFrameConverter.ToMat();
136-
if (this.frameThread.isPresent()) {
137-
throw new IllegalStateException("The video retrieval thread has already been started.");
138-
}
139-
if (this.grabber.isPresent()) {
140-
throw new IllegalStateException("The Frame Grabber has already been started.");
141-
}
142-
try {
143-
grabber.start();
144-
} catch (FrameGrabber.Exception e) {
145-
throw new IOException("A problem occurred trying to start the frame grabber for " + this.name, e);
146-
}
129+
synchronized (this) {
130+
if (this.frameThread.isPresent()) {
131+
throw new IllegalStateException("The video retrieval thread has already been started.");
132+
}
133+
try {
134+
grabber.start();
135+
} catch (FrameGrabber.Exception e) {
136+
throw new IOException("A problem occurred trying to start the frame grabber for " + this.name, e);
137+
}
147138

148-
// Store the grabber only once it has been started in the case that there is an exception.
149-
this.grabber = Optional.of(grabber);
139+
final Thread frameExecutor = new Thread(() -> {
140+
long lastFrame = System.currentTimeMillis();
141+
while (!Thread.interrupted()) {
142+
final Frame videoFrame;
143+
try {
144+
videoFrame = grabber.grab();
145+
} catch (FrameGrabber.Exception e) {
146+
throw new IllegalStateException("Failed to grab image", e);
147+
}
150148

151-
final Thread frameExecutor = new Thread(() -> {
152-
long lastFrame = System.currentTimeMillis();
153-
while (!Thread.interrupted()) {
154-
final Frame videoFrame;
155-
try {
156-
videoFrame = grabber.grab();
157-
} catch (FrameGrabber.Exception e) {
158-
throw new IllegalStateException("Failed to grab image", e);
159-
}
160-
final Mat frameMat = convertToMat.convert(videoFrame);
149+
final Mat frameMat = convertToMat.convert(videoFrame);
161150

162-
if (frameMat == null || frameMat.isNull()) {
163-
throw new IllegalStateException("The camera returned a null frame Mat");
151+
if (frameMat == null || frameMat.isNull()) {
152+
throw new IllegalStateException("The camera returned a null frame Mat");
153+
}
154+
155+
frameMat.copyTo(frameOutputSocket.getValue().get());
156+
frameOutputSocket.setValue(frameOutputSocket.getValue().get());
157+
long thisMoment = System.currentTimeMillis();
158+
frameRateOutputSocket.setValue(1000 / (thisMoment - lastFrame));
159+
lastFrame = thisMoment;
164160
}
161+
}, "Camera");
165162

166-
frameMat.copyTo(frameOutputSocket.getValue().get());
167-
frameOutputSocket.setValue(frameOutputSocket.getValue().get());
168-
long thisMoment = System.currentTimeMillis();
169-
frameRateOutputSocket.setValue(1000 / (thisMoment - lastFrame));
170-
lastFrame = thisMoment;
171-
}
172-
}, "Camera");
173-
frameExecutor.setUncaughtExceptionHandler(
174-
(thread, exception) -> {
175-
eventBus.post(new UnexpectedThrowableEvent(exception, "Webcam Frame Grabber Thread crashed with uncaught exception"));
176-
try {
177-
stopVideo();
178-
} catch (TimeoutException e) {
179-
eventBus.post(new UnexpectedThrowableEvent(e, "Webcam Frame Grabber could not be stopped!"));
163+
frameExecutor.setUncaughtExceptionHandler(
164+
(thread, exception) -> {
165+
// TODO: This should use the ExceptionWitness once that has a UI component added for it
166+
eventBus.post(new UnexpectedThrowableEvent(exception, "Camera Frame Grabber Thread crashed with uncaught exception"));
167+
try {
168+
stop();
169+
} catch (TimeoutException e) {
170+
// TODO: This should use the ExceptionWitness once that has a UI component added for it
171+
eventBus.post(new UnexpectedThrowableEvent(e, "Camera Frame Grabber could not be stopped!"));
172+
}
180173
}
181-
}
182-
);
183-
frameExecutor.setDaemon(true);
184-
frameExecutor.start();
185-
frameThread = Optional.of(frameExecutor);
174+
);
175+
frameExecutor.setDaemon(true);
176+
frameExecutor.start();
177+
this.frameThread = Optional.of(frameExecutor);
178+
// This should only be posted now that it is running
179+
eventBus.post(new StartedStoppedEvent(this));
180+
}
186181
}
187182

188183
/**
189-
* Stops the video feed from updating the output socket.
184+
* Stops this source.
185+
* This will stop the source publishing new socket values after this method returns.
190186
*
191-
* @throws TimeoutException If the thread running the Webcam fails to join this one after a timeout.
187+
* @return The source that was stopped
188+
* @throws TimeoutException if the thread running the source fails to stop.
189+
* @throws IOException If there is a problem stopping the Source
192190
*/
193-
private void stopVideo() throws TimeoutException {
194-
if (frameThread.isPresent()) {
195-
final Thread ex = frameThread.get();
196-
ex.interrupt();
197-
try {
198-
ex.join(TimeUnit.SECONDS.toMillis(2));
199-
if (ex.isAlive()) {
200-
throw new TimeoutException("Unable to terminate video feed from Web Camera");
201-
}
202-
} catch (InterruptedException e) {
203-
Thread.currentThread().interrupt();
204-
//TODO: Move this into a logging framework
205-
System.out.println("Caught Exception:");
206-
e.printStackTrace();
207-
} finally {
208-
frameThread = Optional.empty();
209-
// This will always run even if a timeout exception occurs
191+
public final void stop() throws TimeoutException, IllegalStateException {
192+
synchronized (this) {
193+
if (frameThread.isPresent()) {
194+
final Thread ex = frameThread.get();
195+
ex.interrupt();
210196
try {
211-
grabber.ifPresent(grabber -> {
212-
try {
213-
grabber.stop();
214-
} catch (FrameGrabber.Exception e) {
215-
throw new IllegalStateException("A problem occurred trying to stop the frame grabber", e);
216-
}
217-
});
197+
ex.join(TimeUnit.SECONDS.toMillis(500));
198+
if (ex.isAlive()) {
199+
throw new TimeoutException("Unable to terminate video feed from Web Camera");
200+
}
201+
// This should only be removed if the thread is successfully killed off
202+
frameThread = Optional.empty();
203+
} catch (InterruptedException e) {
204+
Thread.currentThread().interrupt();
205+
//TODO: Move this into a logging framework
206+
System.err.println("Caught Exception:");
207+
e.printStackTrace();
218208
} finally {
219-
// This will always run even if we fail to stop the grabber
220-
grabber = Optional.empty();
209+
// This will always run even if a timeout exception occurs
210+
try {
211+
// Calling this multiple times will have no effect
212+
grabber.stop();
213+
} catch (FrameGrabber.Exception e) {
214+
throw new IllegalStateException("A problem occurred trying to stop the frame grabber", e);
215+
}
221216
}
217+
} else {
218+
throw new IllegalStateException("Tried to stop a Webcam that is already stopped.");
222219
}
223-
} else {
224-
throw new IllegalStateException("Tried to stop a Webcam that is already stopped.");
225220
}
221+
eventBus.post(new StartedStoppedEvent(this));
222+
frameRateOutputSocket.setValue(0);
223+
}
224+
225+
@Override
226+
public synchronized boolean isStarted() {
227+
return this.frameThread.isPresent() && this.frameThread.get().isAlive();
226228
}
227229

228230
@Subscribe
229231
public void onSourceRemovedEvent(SourceRemovedEvent event) throws TimeoutException {
230232
if (event.getSource() == this) {
231-
this.stopVideo();
232-
this.eventBus.unregister(this);
233+
try {
234+
if (this.isStarted()) this.stop();
235+
} finally {
236+
this.eventBus.unregister(this);
237+
}
233238
}
234239
}
235240

0 commit comments

Comments
 (0)