Skip to content

Commit 5308536

Browse files
committed
Updates from review. Add lots of documentation, fix hostname on windows
Set a flag when cscore can't be loaded to make the operation perform() fail, instead of crashing to desktop Still need to do mac hostname resolution
1 parent 1691326 commit 5308536

File tree

1 file changed

+109
-33
lines changed

1 file changed

+109
-33
lines changed

core/src/main/java/edu/wpi/grip/core/operations/composite/PublishVideoOperation.java

Lines changed: 109 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@
1717
import edu.wpi.first.wpilibj.networktables.NetworkTable;
1818
import edu.wpi.first.wpilibj.tables.ITable;
1919

20+
import org.apache.commons.lang.SystemUtils;
2021
import org.bytedeco.javacpp.opencv_core;
2122
import org.opencv.core.Mat;
2223

24+
import java.io.BufferedReader;
25+
import java.io.IOException;
26+
import java.io.InputStreamReader;
2327
import java.lang.reflect.Field;
28+
import java.util.Arrays;
2429
import java.util.Deque;
2530
import java.util.LinkedList;
2631
import java.util.List;
@@ -29,9 +34,6 @@
2934
import java.util.stream.Collectors;
3035
import java.util.stream.Stream;
3136

32-
import static org.bytedeco.javacpp.opencv_core.CV_8S;
33-
import static org.bytedeco.javacpp.opencv_core.CV_8U;
34-
3537
/**
3638
* Publish an M-JPEG stream with the protocol used by SmartDashboard and the FRC Dashboard. This
3739
* allows FRC teams to view video streams on their dashboard during competition even when GRIP has
@@ -41,14 +43,23 @@ public class PublishVideoOperation implements Operation {
4143

4244
private static final Logger logger = Logger.getLogger(PublishVideoOperation.class.getName());
4345

46+
/**
47+
* Flags whether or not cscore was loaded. If it could not be loaded, the MJPEG streaming server
48+
* can't be started, preventing this operation from running.
49+
*/
50+
private static final boolean cscoreLoaded;
51+
4452
static {
53+
boolean loaded;
4554
try {
4655
// Loading the CameraServerJNI class will load the appropriate platform-specific OpenCV JNI
4756
CameraServerJNI.getHostname();
57+
loaded = true;
4858
} catch (Throwable e) {
49-
logger.log(Level.SEVERE, "CameraServerJNI load failed! Exiting", e);
50-
System.exit(31);
59+
logger.log(Level.SEVERE, "CameraServerJNI load failed!", e);
60+
loaded = false;
5161
}
62+
cscoreLoaded = loaded;
5263
}
5364

5465
public static final OperationDescription DESCRIPTION =
@@ -58,15 +69,15 @@ public class PublishVideoOperation implements Operation {
5869
.category(OperationDescription.Category.NETWORK)
5970
.icon(Icon.iconStream("publish-video"))
6071
.build();
61-
private static final int PORT = 1180;
72+
private static final int INITIAL_PORT = 1180;
73+
private static final int MAX_STEP_COUNT = 10; // limit ports to 1180-1189
6274

6375
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
6476
private static int totalStepCount;
6577
@SuppressWarnings("PMD.AssignmentToNonFinalStatic")
6678
private static int numSteps;
67-
private static final int MAX_STEP_COUNT = 10; // limit ports to 1180-1189
6879
private static final Deque<Integer> availablePorts =
69-
Stream.iterate(PORT, i -> i + 1)
80+
Stream.iterate(INITIAL_PORT, i -> i + 1)
7081
.limit(MAX_STEP_COUNT)
7182
.collect(Collectors.toCollection(LinkedList::new));
7283

@@ -77,7 +88,7 @@ public class PublishVideoOperation implements Operation {
7788

7889
// Write to the /CameraPublisher table so the MJPEG streams are discoverable by other
7990
// applications connected to the same NetworkTable server (eg Shuffleboard)
80-
private static final ITable cameraPublisherTable = NetworkTable.getTable("/CameraPublisher");
91+
private final ITable cameraPublisherTable = NetworkTable.getTable("/CameraPublisher"); // NOPMD
8192
private final ITable ourTable;
8293
private final Mat publishMat = new Mat();
8394
private long lastFrame = -1;
@@ -95,16 +106,23 @@ public PublishVideoOperation(InputSocket.Factory inputSocketFactory) {
95106
this.qualitySocket = inputSocketFactory.create(SocketHints.Inputs
96107
.createNumberSliderSocketHint("Quality", 80, 0, 100));
97108

98-
int ourPort = availablePorts.removeFirst();
99-
100-
server = new MjpegServer("GRIP video publishing server " + totalStepCount, ourPort);
101-
serverSource = new CvSource("GRIP CvSource " + totalStepCount,
102-
VideoMode.PixelFormat.kMJPEG, 0, 0, 0);
103-
server.setSource(serverSource);
104-
105-
ourTable = cameraPublisherTable.getSubTable("GRIP-" + totalStepCount);
106-
ourTable.putStringArray("streams",
107-
new String[]{CameraServerJNI.getHostname() + ":" + ourPort + "/?action=stream"});
109+
if (cscoreLoaded) {
110+
int ourPort = availablePorts.removeFirst();
111+
112+
server = new MjpegServer("GRIP video publishing server " + totalStepCount, ourPort);
113+
serverSource = new CvSource("GRIP CvSource " + totalStepCount,
114+
VideoMode.PixelFormat.kMJPEG, 0, 0, 0);
115+
server.setSource(serverSource);
116+
117+
ourTable = cameraPublisherTable.getSubTable("GRIP-" + totalStepCount);
118+
ourTable.putStringArray("streams",
119+
new String[]{"mjpeg:http://" + getHostName() + ":" + ourPort + "/?action=stream"});
120+
System.out.println(" Stream URLs: " + Arrays.toString(ourTable.getStringArray("streams")));
121+
} else {
122+
server = null;
123+
serverSource = null;
124+
ourTable = null;
125+
}
108126

109127
numSteps++;
110128
totalStepCount++;
@@ -126,39 +144,60 @@ public List<OutputSocket> getOutputSockets() {
126144
@Override
127145
public void perform() {
128146
final long now = System.nanoTime(); // NOPMD
147+
148+
if (!cscoreLoaded) {
149+
throw new IllegalStateException(
150+
"cscore could not be loaded. The image streaming server cannot be started.");
151+
}
152+
129153
opencv_core.Mat input = inputSocket.getValue().get();
130154
if (input.empty() || input.isNull()) {
131155
throw new IllegalArgumentException("Input image must not be empty");
132156
}
133157

134158
copyJavaCvToOpenCvMat(input, publishMat);
159+
// Make sure the output resolution is up to date. Might not be needed, depends on cscore updates
160+
serverSource.setResolution(input.size().width(), input.size().height());
135161
serverSource.putFrame(publishMat);
136162
if (lastFrame != -1) {
137163
long dt = now - lastFrame;
138164
serverSource.setFPS((int) (1e9 / dt));
139165
}
140166
lastFrame = now;
141-
server.setSource(serverSource);
142167
}
143168

144169
@Override
145170
public synchronized void cleanUp() {
146-
// Stop the video server if there are no Publish Video steps left
147171
numSteps--;
148-
availablePorts.addFirst(server.getPort());
149-
ourTable.getKeys().forEach(ourTable::delete);
150-
}
151-
152-
private void copyJavaCvToOpenCvMat(opencv_core.Mat javaCvMat, Mat openCvMat) {
153-
if (javaCvMat.depth() != CV_8U && javaCvMat.depth() != CV_8S) {
154-
throw new IllegalArgumentException("Only 8-bit depth images are supported");
172+
if (cscoreLoaded) {
173+
availablePorts.addFirst(server.getPort());
174+
ourTable.getKeys().forEach(ourTable::delete);
175+
serverSource.setConnected(false);
176+
serverSource.free();
177+
server.free();
155178
}
179+
}
156180

157-
final opencv_core.Size size = javaCvMat.size();
158-
159-
// Make sure the output resolution is up to date
160-
serverSource.setResolution(size.width(), size.height());
161-
181+
/**
182+
* Copies the data from a JavaCV Mat wrapper object into an OpenCV Mat wrapper object so it's
183+
* usable by the {@link CvSource} for this operation.
184+
*
185+
* <p>Since the JavaCV and OpenCV bindings both target the same native version of OpenCV, this is
186+
* implemented by simply changing the OpenCV Mat's native pointer to be the same as the one for
187+
* the JavaCV Mat. This prevents memory copies and resizing/reallocating memory for the OpenCV
188+
* wrapper to fit the source image. Updating the pointer is a simple field write (albeit via
189+
* reflection), which is much faster and easier than allocating and copying byte buffers.</p>
190+
*
191+
* <p>A caveat to this approach is that the memory layout used by the OpenCV binaries bundled with
192+
* both wrapper libraries <i>must</i> be identical. Using the same OpenCV version for both
193+
* libraries should be enough.</p>
194+
*
195+
* @param javaCvMat the JavaCV Mat wrapper object to copy from
196+
* @param openCvMat the OpenCV Mat wrapper object to copy into
197+
* @throws RuntimeException if the OpenCV native pointer could not be set
198+
*/
199+
private static void copyJavaCvToOpenCvMat(opencv_core.Mat javaCvMat, Mat openCvMat)
200+
throws RuntimeException {
162201
// Make the OpenCV Mat object point to the same block of memory as the JavaCV object.
163202
// This requires no data transfers or copies and is O(1) instead of O(n)
164203
if (javaCvMat.address() != openCvMat.nativeObj) {
@@ -168,7 +207,44 @@ private void copyJavaCvToOpenCvMat(opencv_core.Mat javaCvMat, Mat openCvMat) {
168207
nativeObjField.setLong(openCvMat, javaCvMat.address());
169208
} catch (ReflectiveOperationException e) {
170209
logger.log(Level.WARNING, "Could not set native object pointer", e);
210+
throw new RuntimeException("Could not copy the image", e);
211+
}
212+
}
213+
}
214+
215+
/**
216+
* Multi platform method for getting the hostname of the local computer. cscore's
217+
* {@link CameraServerJNI#getHostname() getHostName() function} only works on Linux, so we need to
218+
* implement the method for Windows and Mac ourselves.
219+
*/
220+
private static String getHostName() {
221+
if (SystemUtils.IS_OS_WINDOWS) {
222+
// Use the Windows `hostname` command-line utility
223+
// This will return a single line of text containing the hostname, no parsing required
224+
ProcessBuilder builder = new ProcessBuilder("hostname");
225+
Process hostname;
226+
try {
227+
hostname = builder.start();
228+
} catch (IOException e) {
229+
logger.log(Level.WARNING, "Could not start hostname process", e);
230+
return "";
231+
}
232+
try (BufferedReader in =
233+
new BufferedReader(new InputStreamReader(hostname.getInputStream()))) {
234+
return in.readLine() + ".local";
235+
} catch (IOException e) {
236+
logger.log(Level.WARNING, "Could not read the hostname process output", e);
237+
return "";
171238
}
239+
} else if (SystemUtils.IS_OS_LINUX) {
240+
// cscore already defines it for linux
241+
return CameraServerJNI.getHostname();
242+
} else if (SystemUtils.IS_OS_MAC) {
243+
// todo
244+
return "TODO-MAC";
245+
} else {
246+
throw new UnsupportedOperationException(
247+
"Unsupported operating system " + System.getProperty("os.name"));
172248
}
173249
}
174250

0 commit comments

Comments
 (0)