Skip to content

Commit 3f4f557

Browse files
committed
Add a "Publish Video" operation
This allows you to view the video feed with any compatible FRC dashboard (tested with SmartDashboard, but the FRC dashboard uses the same protocol when it's in "hardware" mode). Intermediate steps can also be published, allowing teams to visualize parts of the pipeline even when it's running in headless mode.
1 parent f480c73 commit 3f4f557

3 files changed

Lines changed: 199 additions & 0 deletions

File tree

core/src/main/java/edu/wpi/grip/core/operations/Operations.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ public static void addOperations(EventBus eventBus) {
3434
eventBus.post(new OperationAddedEvent(new NTPublishOperation<>(ContoursReport.class)));
3535
eventBus.post(new OperationAddedEvent(new NTPublishOperation<>(BlobsReport.class)));
3636
eventBus.post(new OperationAddedEvent(new NTPublishOperation<>(LinesReport.class)));
37+
eventBus.post(new OperationAddedEvent(new PublishVideoOperation()));
3738
}
3839
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package edu.wpi.grip.core.operations.composite;
2+
3+
import com.google.common.eventbus.EventBus;
4+
import com.google.common.eventbus.Subscribe;
5+
import edu.wpi.grip.core.InputSocket;
6+
import edu.wpi.grip.core.Operation;
7+
import edu.wpi.grip.core.OutputSocket;
8+
import edu.wpi.grip.core.SocketHints;
9+
import edu.wpi.grip.core.events.StepRemovedEvent;
10+
import org.bytedeco.javacv.FrameConverter;
11+
import org.bytedeco.javacv.Java2DFrameConverter;
12+
import org.bytedeco.javacv.OpenCVFrameConverter;
13+
14+
import javax.imageio.IIOImage;
15+
import javax.imageio.ImageIO;
16+
import javax.imageio.ImageWriteParam;
17+
import javax.imageio.ImageWriter;
18+
import java.awt.image.BufferedImage;
19+
import java.awt.image.RenderedImage;
20+
import java.io.*;
21+
import java.net.ServerSocket;
22+
import java.net.Socket;
23+
import java.util.Optional;
24+
import java.util.logging.Level;
25+
import java.util.logging.Logger;
26+
27+
import static org.bytedeco.javacpp.opencv_core.Mat;
28+
29+
/**
30+
* Publish an M-JPEG stream with the protocol used by SmartDashboard and the FRC Dashboard. This allows FRC teams to
31+
* view video streams on their dashboard during competition even when GRIP has exclusive access to the camera. In
32+
* addition, an intermediate processed image in the pipeline could be published instead.
33+
* <p>
34+
* Based on WPILib's CameraServer class: https://github.com/robotpy/allwpilib/blob/master/wpilibj/src/athena/java/edu/wpi/first/wpilibj/CameraServer.java
35+
*/
36+
public class PublishVideoOperation implements Operation {
37+
38+
private final Logger logger = Logger.getLogger(PublishVideoOperation.class.getName());
39+
40+
private static final int PORT = 1180;
41+
private static final byte[] MAGIC_NUMBER = {0x01, 0x00, 0x00, 0x00};
42+
43+
private final Object imageLock = new Object();
44+
private RenderedImage image = null;
45+
private Optional<Thread> serverThread = Optional.empty();
46+
private volatile float compressionQuality = 0.5f;
47+
private volatile boolean connected = false;
48+
private volatile int numSteps = 0;
49+
50+
/**
51+
* Listens for incoming connections on port 1180 and writes JPEG data whenever there's a new frame.
52+
*/
53+
private final Runnable runServer = () -> {
54+
ByteArrayOutputStream baos = new ByteArrayOutputStream();
55+
56+
// Loop forever (or at least until the thread is interrupted). This lets us recover from the dashboard
57+
// disconnecting or the network connection going away temporarily.
58+
while (!Thread.currentThread().isInterrupted()) {
59+
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
60+
logger.info("Starting camera server");
61+
62+
ImageWriter jpegWriter = ImageIO.getImageWritersByFormatName("jpeg").next();
63+
jpegWriter.setOutput(ImageIO.createImageOutputStream(baos));
64+
65+
ImageWriteParam jpegWriteParam = jpegWriter.getDefaultWriteParam();
66+
jpegWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
67+
68+
try (Socket socket = serverSocket.accept()) {
69+
logger.info("Got connection from " + socket.getInetAddress());
70+
connected = true;
71+
72+
DataOutputStream socketOutputStream = new DataOutputStream(socket.getOutputStream());
73+
DataInputStream socketInputStream = new DataInputStream(socket.getInputStream());
74+
75+
final int fps = socketInputStream.readInt();
76+
final int compression = socketInputStream.readInt();
77+
final int size = socketInputStream.readInt();
78+
79+
if (compression != -1) {
80+
logger.warning("Dashboard video should be in HW mode");
81+
}
82+
83+
final long frameDuration = 1000000000L / fps;
84+
long startTime = System.nanoTime();
85+
86+
while (!socket.isClosed() && !Thread.currentThread().isInterrupted()) {
87+
baos.reset();
88+
89+
// Wait for the main thread to put a new image. This happens whenever perform() is called with
90+
// a new input.
91+
synchronized (imageLock) {
92+
imageLock.wait();
93+
jpegWriteParam.setCompressionQuality(compressionQuality / 100.0f);
94+
jpegWriter.write(null, new IIOImage(image, null, null), jpegWriteParam);
95+
}
96+
97+
// The FRC dashboard image protocol consists of a magic number, the size of the image data,
98+
// and the image data itself.
99+
socketOutputStream.write(MAGIC_NUMBER);
100+
socketOutputStream.writeInt(baos.size());
101+
baos.writeTo(socketOutputStream);
102+
103+
// Limit the FPS to whatever the dashboard requested
104+
int remainingTime = (int) (frameDuration - (System.nanoTime() - startTime));
105+
if (remainingTime > 0) {
106+
Thread.sleep(remainingTime / 1000000, remainingTime % 1000000);
107+
}
108+
109+
startTime = System.nanoTime();
110+
}
111+
}
112+
} catch (IOException e) {
113+
logger.log(Level.WARNING, e.getMessage(), e);
114+
} catch (InterruptedException e) {
115+
Thread.currentThread().interrupt(); // This is really unnecessary since the thread is about to exit
116+
logger.info("Shutting down camera server");
117+
serverThread = Optional.empty();
118+
return;
119+
} finally {
120+
connected = false;
121+
}
122+
}
123+
};
124+
125+
@Override
126+
public String getName() {
127+
return "Publish Video";
128+
}
129+
130+
@Override
131+
public String getDescription() {
132+
return "Publish an M-JPEG stream to the dashboard";
133+
}
134+
135+
@Override
136+
public Optional<InputStream> getIcon() {
137+
return Optional.of(getClass().getResourceAsStream("/edu/wpi/grip/ui/icons/publish-video.png"));
138+
}
139+
140+
@Override
141+
public InputSocket<?>[] createInputSockets(EventBus eventBus) {
142+
eventBus.register(this);
143+
return new InputSocket<?>[]{
144+
new InputSocket<>(eventBus, SocketHints.Inputs.createMatSocketHint("Image", false)),
145+
new InputSocket<>(eventBus, SocketHints.Inputs.createNumberSliderSocketHint("Quality", 80, 0, 100)),
146+
};
147+
}
148+
149+
@Override
150+
public OutputSocket<?>[] createOutputSockets(EventBus eventBus) {
151+
return new OutputSocket<?>[0];
152+
}
153+
154+
@Override
155+
public Optional<?> createData() {
156+
numSteps++;
157+
if (!serverThread.isPresent()) {
158+
serverThread = Optional.of(new Thread(runServer, "Camera Server"));
159+
serverThread.get().setDaemon(true);
160+
serverThread.get().start();
161+
}
162+
return Optional.empty();
163+
}
164+
165+
@Override
166+
public void perform(InputSocket<?>[] inputs, OutputSocket<?>[] outputs) {
167+
Mat input = (Mat) inputs[0].getValue().get();
168+
Number quality = (Number) inputs[1].getValue().get();
169+
170+
if (!connected) {
171+
return; // Don't waste any time converting images if there's no dashboard connected
172+
}
173+
174+
if (input.empty()) {
175+
throw new IllegalArgumentException("Input image must not be empty");
176+
}
177+
178+
FrameConverter<Mat> frameConverter = new OpenCVFrameConverter.ToMat();
179+
FrameConverter<BufferedImage> imageConverter = new Java2DFrameConverter();
180+
BufferedImage image = imageConverter.convert(frameConverter.convert(input));
181+
182+
synchronized (imageLock) {
183+
this.image = image;
184+
this.compressionQuality = quality.floatValue();
185+
imageLock.notify();
186+
}
187+
}
188+
189+
@Subscribe
190+
public void onStepRemoved(StepRemovedEvent event) {
191+
if (event.getStep().getOperation() == this) {
192+
// Stop the video server if there are no Publish Video steps left
193+
if (--numSteps == 0) {
194+
serverThread.ifPresent(Thread::interrupt);
195+
}
196+
}
197+
}
198+
}
2.65 KB
Loading

0 commit comments

Comments
 (0)