Skip to content

Commit ca17591

Browse files
committed
Merge pull request #369 from ThomasJClark/video
Add a "Publish Video" operation
2 parents 36a6d13 + 3f4f557 commit ca17591

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)