diff --git a/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java b/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java index 7b1c381ac3..453c64a322 100644 --- a/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java +++ b/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java @@ -52,6 +52,8 @@ public class CameraConfiguration { public int currentPipelineIndex = 0; public Rotation2d camPitch = new Rotation2d(); + public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc... + @JsonIgnore // this ignores the pipes as we serialize them to their own subfolder public List pipelineSettings = new ArrayList<>(); diff --git a/photon-server/src/main/java/org/photonvision/vision/frame/consumer/MJPGFrameConsumer.java b/photon-server/src/main/java/org/photonvision/vision/frame/consumer/MJPGFrameConsumer.java index 4236dc4c44..66597f980c 100644 --- a/photon-server/src/main/java/org/photonvision/vision/frame/consumer/MJPGFrameConsumer.java +++ b/photon-server/src/main/java/org/photonvision/vision/frame/consumer/MJPGFrameConsumer.java @@ -17,10 +17,15 @@ package org.photonvision.vision.frame.consumer; +import edu.wpi.cscore.CameraServerJNI; import edu.wpi.cscore.CvSource; import edu.wpi.cscore.MjpegServer; +import edu.wpi.cscore.VideoEvent; +import edu.wpi.cscore.VideoListener; import edu.wpi.cscore.VideoMode; -import edu.wpi.first.cameraserver.CameraServer; +import edu.wpi.first.networktables.NetworkTable; +import edu.wpi.first.networktables.NetworkTableInstance; +import java.util.ArrayList; import org.opencv.core.Mat; import org.opencv.core.Size; import org.opencv.imgproc.Imgproc; @@ -33,14 +38,63 @@ public class MJPGFrameConsumer { private final MjpegServer mjpegServer; private FrameDivisor divisor = FrameDivisor.NONE; - public MJPGFrameConsumer(String sourceName, int width, int height) { - // TODO h264? + @SuppressWarnings("FieldCanBeLocal") + private final VideoListener listener; + + private final NetworkTable table; + + public MJPGFrameConsumer(String sourceName, int width, int height, int port) { this.cvSource = new CvSource(sourceName, VideoMode.PixelFormat.kMJPEG, width, height, 30); - this.mjpegServer = CameraServer.getInstance().startAutomaticCapture(cvSource); + this.table = + NetworkTableInstance.getDefault().getTable("/CameraPublisher").getSubTable(sourceName); + + this.mjpegServer = new MjpegServer("serve_" + cvSource.getName(), port); + mjpegServer.setSource(cvSource); + + listener = + new VideoListener( + event -> { + if (event.kind == VideoEvent.Kind.kNetworkInterfacesChanged) { + table.getEntry("source").setString("cv:"); + table.getEntry("streams"); + table.getEntry("connected").setBoolean(true); + table.getEntry("mode").setString(videoModeToString(cvSource.getVideoMode())); + table.getEntry("modes").setStringArray(getSourceModeValues(cvSource.getHandle())); + updateStreamValues(); + } + }, + 0x4fff, + true); + } + + private synchronized void updateStreamValues() { + // Get port + int port = mjpegServer.getPort(); + + // Generate values + var addresses = CameraServerJNI.getNetworkInterfaces(); + ArrayList values = new ArrayList<>(addresses.length + 1); + String listenAddress = CameraServerJNI.getMjpegServerListenAddress(mjpegServer.getHandle()); + if (!listenAddress.isEmpty()) { + // If a listen address is specified, only use that + values.add(makeStreamValue(listenAddress, port)); + } else { + // Otherwise generate for hostname and all interface addresses + values.add(makeStreamValue(CameraServerJNI.getHostname() + ".local", port)); + for (String addr : addresses) { + if ("127.0.0.1".equals(addr)) { + continue; // ignore localhost + } + values.add(makeStreamValue(addr, port)); + } + } + + String[] streamAddresses = values.toArray(new String[0]); + table.getEntry("streams").setStringArray(streamAddresses); } - public MJPGFrameConsumer(String name) { - this(name, 320, 240); + public MJPGFrameConsumer(String name, int port) { + this(name, 320, 240, port); } public void setFrameDivisor(FrameDivisor divisor) { @@ -68,4 +122,45 @@ private Size getScaledSize(Size orig, FrameDivisor divisor) { public int getCurrentStreamPort() { return mjpegServer.getPort(); } + + private static String makeStreamValue(String address, int port) { + return "mjpg:http://" + address + ":" + port + "/?action=stream"; + } + + private static String[] getSourceModeValues(int sourceHandle) { + VideoMode[] modes = CameraServerJNI.enumerateSourceVideoModes(sourceHandle); + String[] modeStrings = new String[modes.length]; + for (int i = 0; i < modes.length; i++) { + modeStrings[i] = videoModeToString(modes[i]); + } + return modeStrings; + } + + private static String videoModeToString(VideoMode mode) { + return mode.width + + "x" + + mode.height + + " " + + pixelFormatToString(mode.pixelFormat) + + " " + + mode.fps + + " fps"; + } + + private static String pixelFormatToString(VideoMode.PixelFormat pixelFormat) { + switch (pixelFormat) { + case kMJPEG: + return "MJPEG"; + case kYUYV: + return "YUYV"; + case kRGB565: + return "RGB565"; + case kBGR: + return "BGR"; + case kGray: + return "Gray"; + default: + return "Unknown"; + } + } } diff --git a/photon-server/src/main/java/org/photonvision/vision/processes/VisionModule.java b/photon-server/src/main/java/org/photonvision/vision/processes/VisionModule.java index 4a898138f6..17074bfb7c 100644 --- a/photon-server/src/main/java/org/photonvision/vision/processes/VisionModule.java +++ b/photon-server/src/main/java/org/photonvision/vision/processes/VisionModule.java @@ -95,12 +95,7 @@ public VisionModule(PipelineManager pipelineManager, VisionSource visionSource, DataChangeService.getInstance().addSubscriber(new VisionModuleChangeSubscriber(this)); - dashboardOutputStreamer = - new MJPGFrameConsumer( - visionSource.getSettables().getConfiguration().uniqueName + "-output"); - dashboardInputStreamer = - new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().uniqueName + "-input"); - + createStreams(); fpsLimitedResultConsumers.add(result -> dashboardInputStreamer.accept(result.inputFrame)); fpsLimitedResultConsumers.add(result -> dashboardOutputStreamer.accept(result.outputFrame)); @@ -140,6 +135,21 @@ public VisionModule(PipelineManager pipelineManager, VisionSource visionSource, saveAndBroadcastAll(); } + private void createStreams() { + var camStreamIdx = visionSource.getSettables().getConfiguration().streamIndex; + // If idx = 0, we want (1181, 1182) + var inputStreamPort = 1181 + (camStreamIdx * 2); + var outputStreamPort = 1181 + (camStreamIdx * 2) + 1; + + dashboardOutputStreamer = + new MJPGFrameConsumer( + visionSource.getSettables().getConfiguration().uniqueName + "-output", + outputStreamPort); + dashboardInputStreamer = + new MJPGFrameConsumer( + visionSource.getSettables().getConfiguration().uniqueName + "-input", inputStreamPort); + } + void setDriverMode(boolean isDriverMode) { pipelineManager.setDriverMode(isDriverMode); setVisionLEDs(!isDriverMode); @@ -271,11 +281,7 @@ void setCameraNickname(String newName) { // rename streams fpsLimitedResultConsumers.clear(); - dashboardOutputStreamer = - new MJPGFrameConsumer( - visionSource.getSettables().getConfiguration().uniqueName + "-output"); - dashboardInputStreamer = - new MJPGFrameConsumer(visionSource.getSettables().getConfiguration().uniqueName + "-input"); + createStreams(); fpsLimitedResultConsumers.add(result -> dashboardInputStreamer.accept(result.inputFrame)); fpsLimitedResultConsumers.add(result -> dashboardOutputStreamer.accept(result.outputFrame)); diff --git a/photon-server/src/main/java/org/photonvision/vision/processes/VisionModuleManager.java b/photon-server/src/main/java/org/photonvision/vision/processes/VisionModuleManager.java index 0225c24a82..3cf08cbb56 100644 --- a/photon-server/src/main/java/org/photonvision/vision/processes/VisionModuleManager.java +++ b/photon-server/src/main/java/org/photonvision/vision/processes/VisionModuleManager.java @@ -57,10 +57,26 @@ public List addSources(HashMap for (var entry : visionSources.entrySet()) { var visionSource = entry.getKey(); var pipelineManager = new PipelineManager(entry.getValue()); + + assignCameraIndex(visionSource.getSettables().getConfiguration()); + var module = new VisionModule(pipelineManager, visionSource, visionModules.size()); visionModules.add(module); addedModules.add(module); } return addedModules; } + + private void assignCameraIndex(CameraConfiguration config) { + var max = + visionModules.stream() + .mapToInt(it -> it.visionSource.getSettables().getConfiguration().streamIndex) + .max() + .orElse(-1); + + // If the current stream index is reserved, increase by 1 + if (config.streamIndex <= max) { + config.streamIndex = max + 1; + } + } }