10
10
import com .google .common .collect .ImmutableList ;
11
11
12
12
import edu .umd .cs .findbugs .annotations .SuppressFBWarnings ;
13
+ import edu .wpi .cscore .CameraServerJNI ;
14
+ import edu .wpi .cscore .CvSource ;
15
+ import edu .wpi .cscore .MjpegServer ;
16
+ import edu .wpi .cscore .VideoMode ;
17
+ import edu .wpi .first .wpilibj .networktables .NetworkTable ;
13
18
14
- import org .bytedeco .javacpp .BytePointer ;
15
- import org .bytedeco . javacpp . IntPointer ;
19
+ import org .bytedeco .javacpp .opencv_core ;
20
+ import org .opencv . core . Mat ;
16
21
17
- import java .io .DataInputStream ;
18
- import java .io .DataOutputStream ;
19
- import java .io .IOException ;
20
- import java .net .ServerSocket ;
21
- import java .net .Socket ;
22
+ import java .lang .reflect .Field ;
22
23
import java .util .List ;
23
24
import java .util .logging .Level ;
24
25
import java .util .logging .Logger ;
25
26
26
- import static org .bytedeco .javacpp .opencv_core .Mat ;
27
- import static org .bytedeco .javacpp .opencv_imgcodecs .CV_IMWRITE_JPEG_QUALITY ;
28
- import static org .bytedeco .javacpp .opencv_imgcodecs .imencode ;
27
+ import static org .bytedeco .javacpp .opencv_core .CV_8S ;
28
+ import static org .bytedeco .javacpp .opencv_core .CV_8U ;
29
29
30
30
/**
31
31
* Publish an M-JPEG stream with the protocol used by SmartDashboard and the FRC Dashboard. This
32
32
* allows FRC teams to view video streams on their dashboard during competition even when GRIP has
33
- * exclusive access to the camera. In addition, an intermediate processed image in the pipeline
34
- * could be published instead. Based on WPILib's CameraServer class:
35
- * https://github.com/robotpy/allwpilib/blob/master/wpilibj/src/athena/java/edu/wpi/first/wpilibj
36
- * /CameraServer.java
33
+ * exclusive access to the camera. Uses cscore to host the image streaming server.
37
34
*/
38
35
public class PublishVideoOperation implements Operation {
39
36
40
37
private static final Logger logger = Logger .getLogger (PublishVideoOperation .class .getName ());
38
+
39
+ static {
40
+ try {
41
+ // Loading the CameraServerJNI class will load the appropriate platform-specific OpenCV JNI
42
+ CameraServerJNI .getHostname ();
43
+ } catch (Throwable e ) {
44
+ logger .log (Level .SEVERE , "CameraServerJNI load failed! Exiting" , e );
45
+ System .exit (31 );
46
+ }
47
+ }
48
+
41
49
public static final OperationDescription DESCRIPTION =
42
50
OperationDescription .builder ()
43
51
.name ("Publish Video" )
@@ -46,110 +54,40 @@ public class PublishVideoOperation implements Operation {
46
54
.icon (Icon .iconStream ("publish-video" ))
47
55
.build ();
48
56
private static final int PORT = 1180 ;
49
- private static final byte [] MAGIC_NUMBER = {0x01 , 0x00 , 0x00 , 0x00 };
50
57
51
58
@ SuppressWarnings ("PMD.AssignmentToNonFinalStatic" )
52
59
private static int numSteps ;
53
- private final Object imageLock = new Object ();
54
- private final BytePointer imagePointer = new BytePointer ();
55
- private final Thread serverThread ;
56
- private final InputSocket <Mat > inputSocket ;
60
+ private static final int MAX_STEP_COUNT = 10 ;
61
+
62
+ private final InputSocket <opencv_core .Mat > inputSocket ;
57
63
private final InputSocket <Number > qualitySocket ;
58
- @ SuppressWarnings ("PMD.SingularField" )
59
- private volatile boolean connected = false ;
60
- /**
61
- * Listens for incoming connections on port 1180 and writes JPEG data whenever there's a new
62
- * frame.
63
- */
64
- private final Runnable runServer = () -> {
65
- // Loop forever (or at least until the thread is interrupted). This lets us recover from the
66
- // dashboard
67
- // disconnecting or the network connection going away temporarily.
68
- while (!Thread .currentThread ().isInterrupted ()) {
69
- try (ServerSocket serverSocket = new ServerSocket (PORT )) {
70
- logger .info ("Starting camera server" );
71
-
72
- try (Socket socket = serverSocket .accept ()) {
73
- logger .info ("Got connection from " + socket .getInetAddress ());
74
- connected = true ;
75
-
76
- DataOutputStream socketOutputStream = new DataOutputStream (socket .getOutputStream ());
77
- DataInputStream socketInputStream = new DataInputStream (socket .getInputStream ());
78
-
79
- byte [] buffer = new byte [128 * 1024 ];
80
- int bufferSize ;
81
-
82
- final int fps = socketInputStream .readInt ();
83
- final int compression = socketInputStream .readInt ();
84
- final int size = socketInputStream .readInt ();
85
-
86
- if (compression != -1 ) {
87
- logger .warning ("Dashboard video should be in HW mode" );
88
- }
89
-
90
- final long frameDuration = 1000000000L / fps ;
91
- long startTime = System .nanoTime ();
92
-
93
- while (!socket .isClosed () && !Thread .currentThread ().isInterrupted ()) {
94
- // Wait for the main thread to put a new image. This happens whenever perform() is
95
- // called with
96
- // a new input.
97
- synchronized (imageLock ) {
98
- imageLock .wait ();
99
-
100
- // Copy the image data into a pre-allocated buffer, growing it if necessary
101
- bufferSize = imagePointer .limit ();
102
- if (bufferSize > buffer .length ) {
103
- buffer = new byte [imagePointer .limit ()];
104
- }
105
- imagePointer .get (buffer , 0 , bufferSize );
106
- }
107
-
108
- // The FRC dashboard image protocol consists of a magic number, the size of the image
109
- // data,
110
- // and the image data itself.
111
- socketOutputStream .write (MAGIC_NUMBER );
112
- socketOutputStream .writeInt (bufferSize );
113
- socketOutputStream .write (buffer , 0 , bufferSize );
114
-
115
- // Limit the FPS to whatever the dashboard requested
116
- int remainingTime = (int ) (frameDuration - (System .nanoTime () - startTime ));
117
- if (remainingTime > 0 ) {
118
- Thread .sleep (remainingTime / 1000000 , remainingTime % 1000000 );
119
- }
120
-
121
- startTime = System .nanoTime ();
122
- }
123
- }
124
- } catch (IOException e ) {
125
- logger .log (Level .WARNING , e .getMessage (), e );
126
- } catch (InterruptedException e ) {
127
- Thread .currentThread ().interrupt (); // This is really unnecessary since the thread is
128
- // about to exit
129
- logger .info ("Shutting down camera server" );
130
- return ;
131
- } finally {
132
- connected = false ;
133
- }
134
- }
135
- };
64
+ private final MjpegServer server ;
65
+ private final CvSource serverSource ;
66
+ private static final NetworkTable cameraPublisherTable =
67
+ NetworkTable .getTable ("/CameraPublisher" );
68
+ private final Mat publishMat = new Mat ();
69
+ private long lastFrame = -1 ;
136
70
137
71
@ SuppressWarnings ("JavadocMethod" )
138
72
@ SuppressFBWarnings (value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD" ,
139
73
justification = "Do not need to synchronize inside of a constructor" )
140
74
public PublishVideoOperation (InputSocket .Factory inputSocketFactory ) {
141
- if (numSteps != 0 ) {
142
- throw new IllegalStateException ("Only one instance of PublishVideoOperation may exist" );
75
+ if (numSteps >= MAX_STEP_COUNT ) {
76
+ throw new IllegalStateException (
77
+ "Only " + MAX_STEP_COUNT + " instances of PublishVideoOperation may exist" );
143
78
}
144
79
this .inputSocket = inputSocketFactory .create (SocketHints .Inputs .createMatSocketHint ("Image" ,
145
80
false ));
146
81
this .qualitySocket = inputSocketFactory .create (SocketHints .Inputs
147
82
.createNumberSliderSocketHint ("Quality" , 80 , 0 , 100 ));
148
- numSteps ++;
149
83
150
- serverThread = new Thread (runServer , "Camera Server" );
151
- serverThread .setDaemon (true );
152
- serverThread .start ();
84
+ server = new MjpegServer ("GRIP server " + numSteps , PORT + numSteps );
85
+ serverSource = new CvSource ("GRIP CvSource" + numSteps , VideoMode .PixelFormat .kMJPEG , 0 , 0 , 0 );
86
+ server .setSource (serverSource );
87
+ cameraPublisherTable .putStringArray ("streams" ,
88
+ new String []{CameraServerJNI .getHostname () + ":" + server .getPort ()});
89
+
90
+ numSteps ++;
153
91
}
154
92
155
93
@ Override
@@ -167,25 +105,49 @@ public List<OutputSocket> getOutputSockets() {
167
105
168
106
@ Override
169
107
public void perform () {
170
- if (!connected ) {
171
- return ; // Don't waste any time converting images if there's no dashboard connected
172
- }
173
-
174
- if (inputSocket .getValue ().get ().empty ()) {
108
+ final long now = System .nanoTime ();
109
+ opencv_core .Mat input = inputSocket .getValue ().get ();
110
+ if (input .empty () || input .isNull ()) {
175
111
throw new IllegalArgumentException ("Input image must not be empty" );
176
112
}
177
113
178
- synchronized (imageLock ) {
179
- imencode (".jpeg" , inputSocket .getValue ().get (), imagePointer ,
180
- new IntPointer (CV_IMWRITE_JPEG_QUALITY , qualitySocket .getValue ().get ().intValue ()));
181
- imageLock .notifyAll ();
114
+ copyJavaCvToOpenCvMat (input , publishMat );
115
+ serverSource .putFrame (publishMat );
116
+ if (lastFrame != -1 ) {
117
+ long dt = now - lastFrame ;
118
+ serverSource .setFPS ((int ) (1e9 / dt ));
182
119
}
120
+ lastFrame = now ;
121
+ server .setSource (serverSource );
183
122
}
184
123
185
124
@ Override
186
125
public synchronized void cleanUp () {
187
126
// Stop the video server if there are no Publish Video steps left
188
- serverThread .interrupt ();
189
127
numSteps --;
190
128
}
129
+
130
+ private void copyJavaCvToOpenCvMat (opencv_core .Mat javaCvMat , Mat openCvMat ) {
131
+ if (javaCvMat .depth () != CV_8U && javaCvMat .depth () != CV_8S ) {
132
+ throw new IllegalArgumentException ("Only 8-bit depth images are supported" );
133
+ }
134
+
135
+ final opencv_core .Size size = javaCvMat .size ();
136
+
137
+ // Make sure the output resolution is up to date
138
+ serverSource .setResolution (size .width (), size .height ());
139
+
140
+ // Make the OpenCV Mat object point to the same block of memory as the JavaCV object.
141
+ // This requires no data transfers or copies and is O(1) instead of O(n)
142
+ if (javaCvMat .address () != openCvMat .nativeObj ) {
143
+ try {
144
+ Field nativeObjField = Mat .class .getField ("nativeObj" );
145
+ nativeObjField .setAccessible (true );
146
+ nativeObjField .setLong (openCvMat , javaCvMat .address ());
147
+ } catch (ReflectiveOperationException e ) {
148
+ logger .log (Level .WARNING , "Could not set native object pointer" , e );
149
+ }
150
+ }
151
+ }
152
+
191
153
}
0 commit comments