17
17
import edu .wpi .first .wpilibj .networktables .NetworkTable ;
18
18
import edu .wpi .first .wpilibj .tables .ITable ;
19
19
20
+ import org .apache .commons .lang .SystemUtils ;
20
21
import org .bytedeco .javacpp .opencv_core ;
21
22
import org .opencv .core .Mat ;
22
23
24
+ import java .io .BufferedReader ;
25
+ import java .io .IOException ;
26
+ import java .io .InputStreamReader ;
23
27
import java .lang .reflect .Field ;
28
+ import java .util .Arrays ;
24
29
import java .util .Deque ;
25
30
import java .util .LinkedList ;
26
31
import java .util .List ;
29
34
import java .util .stream .Collectors ;
30
35
import java .util .stream .Stream ;
31
36
32
- import static org .bytedeco .javacpp .opencv_core .CV_8S ;
33
- import static org .bytedeco .javacpp .opencv_core .CV_8U ;
34
-
35
37
/**
36
38
* Publish an M-JPEG stream with the protocol used by SmartDashboard and the FRC Dashboard. This
37
39
* 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 {
41
43
42
44
private static final Logger logger = Logger .getLogger (PublishVideoOperation .class .getName ());
43
45
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
+
44
52
static {
53
+ boolean loaded ;
45
54
try {
46
55
// Loading the CameraServerJNI class will load the appropriate platform-specific OpenCV JNI
47
56
CameraServerJNI .getHostname ();
57
+ loaded = true ;
48
58
} 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 ;
51
61
}
62
+ cscoreLoaded = loaded ;
52
63
}
53
64
54
65
public static final OperationDescription DESCRIPTION =
@@ -58,15 +69,15 @@ public class PublishVideoOperation implements Operation {
58
69
.category (OperationDescription .Category .NETWORK )
59
70
.icon (Icon .iconStream ("publish-video" ))
60
71
.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
62
74
63
75
@ SuppressWarnings ("PMD.AssignmentToNonFinalStatic" )
64
76
private static int totalStepCount ;
65
77
@ SuppressWarnings ("PMD.AssignmentToNonFinalStatic" )
66
78
private static int numSteps ;
67
- private static final int MAX_STEP_COUNT = 10 ; // limit ports to 1180-1189
68
79
private static final Deque <Integer > availablePorts =
69
- Stream .iterate (PORT , i -> i + 1 )
80
+ Stream .iterate (INITIAL_PORT , i -> i + 1 )
70
81
.limit (MAX_STEP_COUNT )
71
82
.collect (Collectors .toCollection (LinkedList ::new ));
72
83
@@ -77,7 +88,7 @@ public class PublishVideoOperation implements Operation {
77
88
78
89
// Write to the /CameraPublisher table so the MJPEG streams are discoverable by other
79
90
// 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
81
92
private final ITable ourTable ;
82
93
private final Mat publishMat = new Mat ();
83
94
private long lastFrame = -1 ;
@@ -95,16 +106,23 @@ public PublishVideoOperation(InputSocket.Factory inputSocketFactory) {
95
106
this .qualitySocket = inputSocketFactory .create (SocketHints .Inputs
96
107
.createNumberSliderSocketHint ("Quality" , 80 , 0 , 100 ));
97
108
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
+ }
108
126
109
127
numSteps ++;
110
128
totalStepCount ++;
@@ -126,39 +144,60 @@ public List<OutputSocket> getOutputSockets() {
126
144
@ Override
127
145
public void perform () {
128
146
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
+
129
153
opencv_core .Mat input = inputSocket .getValue ().get ();
130
154
if (input .empty () || input .isNull ()) {
131
155
throw new IllegalArgumentException ("Input image must not be empty" );
132
156
}
133
157
134
158
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 ());
135
161
serverSource .putFrame (publishMat );
136
162
if (lastFrame != -1 ) {
137
163
long dt = now - lastFrame ;
138
164
serverSource .setFPS ((int ) (1e9 / dt ));
139
165
}
140
166
lastFrame = now ;
141
- server .setSource (serverSource );
142
167
}
143
168
144
169
@ Override
145
170
public synchronized void cleanUp () {
146
- // Stop the video server if there are no Publish Video steps left
147
171
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 ();
155
178
}
179
+ }
156
180
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 {
162
201
// Make the OpenCV Mat object point to the same block of memory as the JavaCV object.
163
202
// This requires no data transfers or copies and is O(1) instead of O(n)
164
203
if (javaCvMat .address () != openCvMat .nativeObj ) {
@@ -168,7 +207,44 @@ private void copyJavaCvToOpenCvMat(opencv_core.Mat javaCvMat, Mat openCvMat) {
168
207
nativeObjField .setLong (openCvMat , javaCvMat .address ());
169
208
} catch (ReflectiveOperationException e ) {
170
209
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 "" ;
171
238
}
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" ));
172
248
}
173
249
}
174
250
0 commit comments