diff --git a/wpilibc/athena/include/CameraServer.h b/wpilibc/athena/include/CameraServer.h index 4f210b2c63..317000e388 100644 --- a/wpilibc/athena/include/CameraServer.h +++ b/wpilibc/athena/include/CameraServer.h @@ -271,6 +271,7 @@ class CameraServer : public ErrorBase { std::shared_ptr GetSourceTable(CS_Source source); std::vector GetSinkStreamValues(CS_Sink sink); + std::vector GetSourceStreamValues(CS_Source source); void UpdateStreamValues(); static constexpr char const* kPublishName = "/CameraPublisher"; diff --git a/wpilibc/athena/src/CameraServer.cpp b/wpilibc/athena/src/CameraServer.cpp index 4beb745094..7ab586007c 100644 --- a/wpilibc/athena/src/CameraServer.cpp +++ b/wpilibc/athena/src/CameraServer.cpp @@ -11,6 +11,7 @@ #include "WPIErrors.h" #include "llvm/SmallString.h" #include "llvm/raw_ostream.h" +#include "ntcore_cpp.h" using namespace frc; @@ -91,7 +92,7 @@ std::vector CameraServer::GetSinkStreamValues(CS_Sink sink) { return values; } -static std::vector GetSourceStreamValues(CS_Source source) { +std::vector CameraServer::GetSourceStreamValues(CS_Source source) { CS_Status status = 0; // Ignore all but HttpCamera @@ -102,6 +103,19 @@ static std::vector GetSourceStreamValues(CS_Source source) { auto values = cs::GetHttpCameraUrls(source, &status); for (auto& value : values) value = "mjpg:" + value; + // Look to see if we have a passthrough server for this source + for (const auto& i : m_sinks) { + CS_Sink sink = i.second.GetHandle(); + CS_Source sinkSource = cs::GetSinkSource(sink, &status); + if (source == sinkSource && + cs::GetSinkKind(sink, &status) == CS_SINK_MJPEG) { + // Add USB-only passthrough + int port = cs::GetMjpegServerPort(sink, &status); + values.emplace_back(MakeStreamValue("172.22.11.2", port)); + break; + } + } + // Set table value return values; } @@ -115,8 +129,12 @@ void CameraServer::UpdateStreamValues() { // Get the source's subtable (if none exists, we're done) CS_Source source = cs::GetSinkSource(sink, &status); + if (source == 0) continue; auto table = m_tables.lookup(source); if (table) { + // Don't set stream values if this is a HttpCamera passthrough + if (cs::GetSourceKind(source, &status) == CS_SOURCE_HTTP) continue; + // Set table value auto values = GetSinkStreamValues(sink); if (!values.empty()) table->PutStringArray("streams", values); @@ -137,6 +155,158 @@ void CameraServer::UpdateStreamValues() { } } +static std::string PixelFormatToString(int pixelFormat) { + switch (pixelFormat) { + case cs::VideoMode::PixelFormat::kMJPEG: + return "MJPEG"; + case cs::VideoMode::PixelFormat::kYUYV: + return "YUYV"; + case cs::VideoMode::PixelFormat::kRGB565: + return "RGB565"; + case cs::VideoMode::PixelFormat::kBGR: + return "BGR"; + case cs::VideoMode::PixelFormat::kGray: + return "Gray"; + default: + return "Unknown"; + } +} + +static cs::VideoMode::PixelFormat PixelFormatFromString(llvm::StringRef str) { + if (str == "MJPEG" || str == "mjpeg" || str == "JPEG" || str == "jpeg") + return cs::VideoMode::PixelFormat::kMJPEG; + if (str == "YUYV" || str == "yuyv") return cs::VideoMode::PixelFormat::kYUYV; + if (str == "RGB565" || str == "rgb565") + return cs::VideoMode::PixelFormat::kRGB565; + if (str == "BGR" || str == "bgr") return cs::VideoMode::PixelFormat::kBGR; + if (str == "GRAY" || str == "Gray" || str == "gray") + return cs::VideoMode::PixelFormat::kGray; + return cs::VideoMode::PixelFormat::kUnknown; +} + +static cs::VideoMode VideoModeFromString(llvm::StringRef modeStr) { + cs::VideoMode mode; + size_t pos; + + // width: [0-9]+ + pos = modeStr.find_first_not_of("0123456789"); + llvm::StringRef widthStr = modeStr.slice(0, pos); + modeStr = modeStr.drop_front(pos).ltrim(); // drop whitespace too + + // 'x' + if (modeStr.empty() || modeStr[0] != 'x') return mode; + modeStr = modeStr.drop_front(1).ltrim(); // drop whitespace too + + // height: [0-9]+ + pos = modeStr.find_first_not_of("0123456789"); + llvm::StringRef heightStr = modeStr.slice(0, pos); + modeStr = modeStr.drop_front(pos).ltrim(); // drop whitespace too + + // format: all characters until whitespace + pos = modeStr.find_first_of(" \t\n\v\f\r"); + llvm::StringRef formatStr = modeStr.slice(0, pos); + modeStr = modeStr.drop_front(pos).ltrim(); // drop whitespace too + + // fps: [0-9.]+ + pos = modeStr.find_first_not_of("0123456789."); + llvm::StringRef fpsStr = modeStr.slice(0, pos); + modeStr = modeStr.drop_front(pos).ltrim(); // drop whitespace too + + // "fps" + if (!modeStr.startswith("fps")) return mode; + + // make fps an integer string by dropping after the decimal + fpsStr = fpsStr.slice(0, fpsStr.find('.')); + + // convert width, height, and fps to integers + if (widthStr.getAsInteger(10, mode.width)) return mode; + if (heightStr.getAsInteger(10, mode.height)) return mode; + if (fpsStr.getAsInteger(10, mode.fps)) return mode; + + // convert format to enum value + mode.pixelFormat = PixelFormatFromString(formatStr); + + return mode; +} + +static std::string VideoModeToString(const cs::VideoMode& mode) { + std::string rv; + llvm::raw_string_ostream oss{rv}; + oss << mode.width << "x" << mode.height; + oss << " " << PixelFormatToString(mode.pixelFormat) << " "; + oss << mode.fps << " fps"; + return oss.str(); +} + +static std::vector GetSourceModeValues(int source) { + std::vector rv; + CS_Status status = 0; + for (const auto& mode : cs::EnumerateSourceVideoModes(source, &status)) + rv.emplace_back(VideoModeToString(mode)); + return rv; +} + +static inline llvm::StringRef Concatenate(llvm::StringRef lhs, + llvm::StringRef rhs, + llvm::SmallVectorImpl& buf) { + buf.clear(); + llvm::raw_svector_ostream oss{buf}; + oss << lhs << rhs; + return oss.str(); +} + +static void PutSourcePropertyValue(ITable* table, const cs::VideoEvent& event, + bool isNew) { + llvm::SmallString<64> name; + llvm::SmallString<64> infoName; + if (llvm::StringRef{event.name}.startswith("raw_")) { + name = "RawProperty/"; + name += llvm::StringRef{event.name}.substr(4); + infoName = "RawPropertyInfo/"; + infoName += llvm::StringRef{event.name}.substr(4); + } else { + name = "Property/"; + name += event.name; + infoName = "PropertyInfo/"; + infoName += event.name; + } + + llvm::SmallString<64> buf; + CS_Status status = 0; + switch (event.propertyKind) { + case cs::VideoProperty::kBoolean: + if (isNew) + table->SetDefaultBoolean(name, event.value != 0); + else + table->PutBoolean(name, event.value != 0); + break; + case cs::VideoProperty::kInteger: + case cs::VideoProperty::kEnum: + if (isNew) { + table->SetDefaultNumber(name, event.value); + table->PutNumber(Concatenate(infoName, "/min", buf), + cs::GetPropertyMin(event.propertyHandle, &status)); + table->PutNumber(Concatenate(infoName, "/max", buf), + cs::GetPropertyMax(event.propertyHandle, &status)); + table->PutNumber(Concatenate(infoName, "/step", buf), + cs::GetPropertyStep(event.propertyHandle, &status)); + table->PutNumber(Concatenate(infoName, "/default", buf), + cs::GetPropertyDefault(event.propertyHandle, &status)); + } else { + table->PutNumber(name, event.value); + } + break; + case cs::VideoProperty::kString: + if (isNew) + table->SetDefaultString(name, event.valueStr); + else + table->PutString(name, event.valueStr); + break; + default: + break; + } +} + CameraServer::CameraServer() : m_publishTable{NetworkTable::GetTable(kPublishName)}, m_nextPort(kBasePort) { @@ -144,7 +314,12 @@ CameraServer::CameraServer() // "/CameraPublisher/{Source.Name}/" - root // - "source" (string): Descriptive, prefixed with type (e.g. "usb:0") // - "streams" (string array): URLs that can be used to stream data - // - properties (scaled units) + // - "description" (string): Description of the source + // - "connected" (boolean): Whether source is connected + // - "mode" (string): Current video mode + // - "modes" (string array): Available video modes + // - "Property/{Property}" - Property values + // - "PropertyInfo/{Property}" - Property supporting information // Listener for video events m_videoListener = cs::VideoListener{ @@ -169,6 +344,10 @@ CameraServer::CameraServer() event.sourceHandle, &status)); table->PutStringArray("streams", GetSourceStreamValues(event.sourceHandle)); + auto mode = cs::GetSourceVideoMode(event.sourceHandle, &status); + table->SetDefaultString("mode", VideoModeToString(mode)); + table->PutStringArray("modes", + GetSourceModeValues(event.sourceHandle)); break; } case cs::VideoEvent::kSourceDestroyed: { @@ -176,6 +355,7 @@ CameraServer::CameraServer() if (table) { table->PutString("source", ""); table->PutStringArray("streams", std::vector{}); + table->PutStringArray("modes", std::vector{}); } break; } @@ -197,34 +377,43 @@ CameraServer::CameraServer() break; } case cs::VideoEvent::kSourceVideoModesUpdated: { + auto table = GetSourceTable(event.sourceHandle); + if (table) + table->PutStringArray("modes", + GetSourceModeValues(event.sourceHandle)); break; } case cs::VideoEvent::kSourceVideoModeChanged: { + auto table = GetSourceTable(event.sourceHandle); + if (table) table->PutString("mode", VideoModeToString(event.mode)); break; } case cs::VideoEvent::kSourcePropertyCreated: { + auto table = GetSourceTable(event.sourceHandle); + if (table) PutSourcePropertyValue(table.get(), event, true); break; } case cs::VideoEvent::kSourcePropertyValueUpdated: { + auto table = GetSourceTable(event.sourceHandle); + if (table) PutSourcePropertyValue(table.get(), event, false); break; } case cs::VideoEvent::kSourcePropertyChoicesUpdated: { + auto table = GetSourceTable(event.sourceHandle); + if (table) { + llvm::SmallString<64> name{"PropertyInfo/"}; + name += event.name; + name += "/choices"; + auto choices = + cs::GetEnumPropertyChoices(event.propertyHandle, &status); + table->PutStringArray(name, choices); + } break; } - case cs::VideoEvent::kSinkSourceChanged: { - UpdateStreamValues(); - break; - } - case cs::VideoEvent::kSinkCreated: { - break; - } + case cs::VideoEvent::kSinkSourceChanged: + case cs::VideoEvent::kSinkCreated: case cs::VideoEvent::kSinkDestroyed: { - break; - } - case cs::VideoEvent::kSinkEnabled: { - break; - } - case cs::VideoEvent::kSinkDisabled: { + UpdateStreamValues(); break; } case cs::VideoEvent::kNetworkInterfacesChanged: { @@ -235,7 +424,73 @@ CameraServer::CameraServer() break; } }, - 0x7fff, true}; + 0x4fff, true}; + + // Listener for NetworkTable events + llvm::SmallString<64> buf; + m_tableListener = nt::AddEntryListener( + Concatenate(kPublishName, "/", buf), + [=](unsigned int uid, llvm::StringRef key, + std::shared_ptr value, unsigned int flags) { + llvm::StringRef relativeKey = + key.substr(llvm::StringRef(kPublishName).size() + 1); + + // get source (sourceName/...) + auto subKeyIndex = relativeKey.find('/'); + if (subKeyIndex == llvm::StringRef::npos) return; + llvm::StringRef sourceName = relativeKey.slice(0, subKeyIndex); + auto sourceIt = m_sources.find(sourceName); + if (sourceIt == m_sources.end()) return; + + // get subkey + relativeKey = relativeKey.substr(subKeyIndex + 1); + + // handle standard names + llvm::SmallString<64> propNameBuf; + llvm::StringRef propName; + if (relativeKey == "mode") { + if (!value->IsString()) return; + auto mode = VideoModeFromString(value->GetString()); + if (mode.pixelFormat == cs::VideoMode::PixelFormat::kUnknown || + !sourceIt->second.SetVideoMode(mode)) { + // reset to current mode + nt::SetEntryValue(key, nt::Value::MakeString(VideoModeToString( + sourceIt->second.GetVideoMode()))); + } + return; + } else if (relativeKey.startswith("Property/")) { + propName = relativeKey.substr(9); + } else if (relativeKey.startswith("RawProperty/")) { + propNameBuf = "raw_"; + propNameBuf += relativeKey.substr(12); + propName = propNameBuf.str(); + } else { + return; // ignore + } + + // everything else is a property + auto property = sourceIt->second.GetProperty(propName); + switch (property.GetKind()) { + case cs::VideoProperty::kNone: + return; + case cs::VideoProperty::kBoolean: + if (!value->IsBoolean()) return; + property.Set(value->GetBoolean() ? 1 : 0); + return; + case cs::VideoProperty::kInteger: + case cs::VideoProperty::kEnum: + if (!value->IsDouble()) return; + property.Set(static_cast(value->GetDouble())); + return; + case cs::VideoProperty::kString: + if (!value->IsString()) return; + property.SetString(value->GetString()); + return; + default: + return; + } + }, + NT_NOTIFY_IMMEDIATE | NT_NOTIFY_UPDATE); } cs::UsbCamera CameraServer::StartAutomaticCapture() { @@ -285,28 +540,28 @@ cs::AxisCamera CameraServer::AddAxisCamera(llvm::ArrayRef hosts) { cs::AxisCamera CameraServer::AddAxisCamera(llvm::StringRef name, llvm::StringRef host) { cs::AxisCamera camera{name, host}; - AddCamera(camera); + StartAutomaticCapture(camera); return camera; } cs::AxisCamera CameraServer::AddAxisCamera(llvm::StringRef name, const char* host) { cs::AxisCamera camera{name, host}; - AddCamera(camera); + StartAutomaticCapture(camera); return camera; } cs::AxisCamera CameraServer::AddAxisCamera(llvm::StringRef name, const std::string& host) { cs::AxisCamera camera{name, host}; - AddCamera(camera); + StartAutomaticCapture(camera); return camera; } cs::AxisCamera CameraServer::AddAxisCamera(llvm::StringRef name, llvm::ArrayRef hosts) { cs::AxisCamera camera{name, hosts}; - AddCamera(camera); + StartAutomaticCapture(camera); return camera; } diff --git a/wpilibj/src/athena/java/edu/wpi/first/wpilibj/CameraServer.java b/wpilibj/src/athena/java/edu/wpi/first/wpilibj/CameraServer.java index 205ab7bd79..c7ee248624 100644 --- a/wpilibj/src/athena/java/edu/wpi/first/wpilibj/CameraServer.java +++ b/wpilibj/src/athena/java/edu/wpi/first/wpilibj/CameraServer.java @@ -13,9 +13,12 @@ import edu.wpi.cscore.CvSink; import edu.wpi.cscore.CvSource; import edu.wpi.cscore.MjpegServer; import edu.wpi.cscore.UsbCamera; +import edu.wpi.cscore.VideoEvent; import edu.wpi.cscore.VideoException; import edu.wpi.cscore.VideoListener; import edu.wpi.cscore.VideoMode; +import edu.wpi.cscore.VideoMode.PixelFormat; +import edu.wpi.cscore.VideoProperty; import edu.wpi.cscore.VideoSink; import edu.wpi.cscore.VideoSource; import edu.wpi.first.wpilibj.networktables.NetworkTable; @@ -23,6 +26,8 @@ import edu.wpi.first.wpilibj.networktables.NetworkTablesJNI; import edu.wpi.first.wpilibj.tables.ITable; import java.util.ArrayList; import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Singleton class for creating and keeping camera servers. @@ -123,8 +128,8 @@ public class CameraServer { return values.toArray(new String[0]); } - @SuppressWarnings("JavadocMethod") - private static String[] getSourceStreamValues(int source) { + @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"}) + private synchronized String[] getSourceStreamValues(int source) { // Ignore all but HttpCamera if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) != VideoSource.Kind.kHttp) { @@ -137,6 +142,23 @@ public class CameraServer { values[j] = "mjpg:" + values[j]; } + // Look to see if we have a passthrough server for this source + for (VideoSink i : m_sinks.values()) { + int sink = i.getHandle(); + int sinkSource = CameraServerJNI.getSinkSource(sink); + if (source == sinkSource + && VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) == VideoSink.Kind.kMjpeg) { + // Add USB-only passthrough + String[] finalValues = new String[values.length + 1]; + for (int j = 0; j < values.length; j++) { + finalValues[j] = values[j]; + } + int port = CameraServerJNI.getMjpegServerPort(sink); + finalValues[values.length] = makeStreamValue("172.22.11.2", port); + return finalValues; + } + } + return values; } @@ -148,8 +170,17 @@ public class CameraServer { // Get the source's subtable (if none exists, we're done) int source = CameraServerJNI.getSinkSource(sink); + if (source == 0) { + continue; + } ITable table = m_tables.get(source); if (table != null) { + // Don't set stream values if this is a HttpCamera passthrough + if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) + == VideoSource.Kind.kHttp) { + continue; + } + // Set table value String[] values = getSinkStreamValues(sink); if (values.length > 0) { @@ -174,6 +205,134 @@ public class CameraServer { } } + @SuppressWarnings("JavadocMethod") + private static String pixelFormatToString(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"; + } + } + + @SuppressWarnings("JavadocMethod") + private static PixelFormat pixelFormatFromString(String pixelFormatStr) { + switch (pixelFormatStr) { + case "MJPEG": + case "mjpeg": + case "JPEG": + case "jpeg": + return PixelFormat.kMJPEG; + case "YUYV": + case "yuyv": + return PixelFormat.kYUYV; + case "RGB565": + case "rgb565": + return PixelFormat.kRGB565; + case "BGR": + case "bgr": + return PixelFormat.kBGR; + case "GRAY": + case "Gray": + case "gray": + return PixelFormat.kGray; + default: + return PixelFormat.kUnknown; + } + } + + private static final Pattern reMode = + Pattern.compile("(?[0-9]+)\\s*x\\s*(?[0-9]+)\\s+(?.*?)\\s+" + + "(?[0-9.]+)\\s*fps"); + + /// Construct a video mode from a string description. + @SuppressWarnings("JavadocMethod") + private static VideoMode videoModeFromString(String modeStr) { + Matcher matcher = reMode.matcher(modeStr); + if (!matcher.matches()) { + return new VideoMode(PixelFormat.kUnknown, 0, 0, 0); + } + PixelFormat pixelFormat = pixelFormatFromString(matcher.group("format")); + int width = Integer.parseInt(matcher.group("width")); + int height = Integer.parseInt(matcher.group("height")); + int fps = (int) Double.parseDouble(matcher.group("fps")); + return new VideoMode(pixelFormat, width, height, fps); + } + + /// Provide string description of video mode. + /// The returned string is "{width}x{height} {format} {fps} fps". + @SuppressWarnings("JavadocMethod") + private static String videoModeToString(VideoMode mode) { + return mode.width + "x" + mode.height + " " + pixelFormatToString(mode.pixelFormat) + + " " + mode.fps + " fps"; + } + + @SuppressWarnings("JavadocMethod") + 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; + } + + @SuppressWarnings("JavadocMethod") + private static void putSourcePropertyValue(ITable table, VideoEvent event, boolean isNew) { + String name; + String infoName; + if (event.name.startsWith("raw_")) { + name = "RawProperty/" + event.name.substring(4); + infoName = "RawPropertyInfo/" + event.name.substring(4); + } else { + name = "Property/" + event.name; + infoName = "PropertyInfo/" + event.name; + } + + switch (event.propertyKind) { + case kBoolean: + if (isNew) { + table.setDefaultBoolean(name, event.value != 0); + } else { + table.putBoolean(name, event.value != 0); + } + break; + case kInteger: + case kEnum: + if (isNew) { + table.setDefaultNumber(name, event.value); + table.putNumber(infoName + "/min", + CameraServerJNI.getPropertyMin(event.propertyHandle)); + table.putNumber(infoName + "/max", + CameraServerJNI.getPropertyMax(event.propertyHandle)); + table.putNumber(infoName + "/step", + CameraServerJNI.getPropertyStep(event.propertyHandle)); + table.putNumber(infoName + "/default", + CameraServerJNI.getPropertyDefault(event.propertyHandle)); + } else { + table.putNumber(name, event.value); + } + break; + case kString: + if (isNew) { + table.setDefaultString(name, event.valueStr); + } else { + table.putString(name, event.valueStr); + } + break; + default: + break; + } + } + @SuppressWarnings({"JavadocMethod", "PMD.UnusedLocalVariable"}) private CameraServer() { m_sources = new HashMap(); @@ -187,7 +346,12 @@ public class CameraServer { // "/CameraPublisher/{Source.Name}/" - root // - "source" (string): Descriptive, prefixed with type (e.g. "usb:0") // - "streams" (string array): URLs that can be used to stream data - // - properties (scaled units) + // - "description" (string): Description of the source + // - "connected" (boolean): Whether source is connected + // - "mode" (string): Current video mode + // - "modes" (string array): Available video modes + // - "Property/{Property}" - Property values + // - "PropertyInfo/{Property}" - Property supporting information // Listener for video events m_videoListener = new VideoListener(event -> { @@ -203,6 +367,9 @@ public class CameraServer { CameraServerJNI.getSourceDescription(event.sourceHandle)); table.putBoolean("connected", CameraServerJNI.isSourceConnected(event.sourceHandle)); table.putStringArray("streams", getSourceStreamValues(event.sourceHandle)); + VideoMode mode = CameraServerJNI.getSourceVideoMode(event.sourceHandle); + table.setDefaultString("mode", videoModeToString(mode)); + table.putStringArray("modes", getSourceModeValues(event.sourceHandle)); break; } case kSourceDestroyed: { @@ -210,6 +377,7 @@ public class CameraServer { if (table != null) { table.putString("source", ""); table.putStringArray("streams", new String[0]); + table.putStringArray("modes", new String[0]); } break; } @@ -231,34 +399,45 @@ public class CameraServer { break; } case kSourceVideoModesUpdated: { + ITable table = getSourceTable(event.sourceHandle); + if (table != null) { + table.putStringArray("modes", getSourceModeValues(event.sourceHandle)); + } break; } case kSourceVideoModeChanged: { + ITable table = getSourceTable(event.sourceHandle); + if (table != null) { + table.putString("mode", videoModeToString(event.mode)); + } break; } case kSourcePropertyCreated: { + ITable table = getSourceTable(event.sourceHandle); + if (table != null) { + putSourcePropertyValue(table, event, true); + } break; } case kSourcePropertyValueUpdated: { + ITable table = getSourceTable(event.sourceHandle); + if (table != null) { + putSourcePropertyValue(table, event, false); + } break; } case kSourcePropertyChoicesUpdated: { + ITable table = getSourceTable(event.sourceHandle); + if (table != null) { + String[] choices = CameraServerJNI.getEnumPropertyChoices(event.propertyHandle); + table.putStringArray("PropertyInfo/" + event.name + "/choices", choices); + } break; } - case kSinkSourceChanged: { - updateStreamValues(); - break; - } - case kSinkCreated: { - break; - } + case kSinkSourceChanged: + case kSinkCreated: case kSinkDestroyed: { - break; - } - case kSinkEnabled: { - break; - } - case kSinkDisabled: { + updateStreamValues(); break; } case kNetworkInterfacesChanged: { @@ -268,17 +447,63 @@ public class CameraServer { default: break; } - }, 0x7fff, true); + }, 0x4fff, true); // Listener for NetworkTable events - m_tableListener = NetworkTablesJNI.addEntryListener(kPublishName, (uid, key, value, flags) -> { - if (!key.startsWith(kPublishName + "/")) { - return; - } - String relativeKey = key.substring(kPublishName.length()); + m_tableListener = NetworkTablesJNI.addEntryListener(kPublishName + "/", + (uid, key, value, flags) -> { + String relativeKey = key.substring(kPublishName.length() + 1); - }, ITable.NOTIFY_IMMEDIATE | ITable.NOTIFY_UPDATE); + // get source (sourceName/...) + int subKeyIndex = relativeKey.indexOf('/'); + if (subKeyIndex == -1) { + return; + } + String sourceName = relativeKey.substring(0, subKeyIndex); + VideoSource source = m_sources.get(sourceName); + if (source == null) { + return; + } + // get subkey + relativeKey = relativeKey.substring(subKeyIndex + 1); + + // handle standard names + String propName; + if (relativeKey.equals("mode")) { + VideoMode mode = videoModeFromString((String) value); + if (mode.pixelFormat == PixelFormat.kUnknown || !source.setVideoMode(mode)) { + // reset to current mode + NetworkTablesJNI.putString(key, videoModeToString(source.getVideoMode())); + } + return; + } else if (relativeKey.startsWith("Property/")) { + propName = relativeKey.substring(9); + } else if (relativeKey.startsWith("RawProperty/")) { + propName = "raw_" + relativeKey.substring(12); + } else { + return; // ignore + } + + // everything else is a property + VideoProperty property = source.getProperty(propName); + switch (property.getKind()) { + case kNone: + return; + case kBoolean: + property.set(((Boolean) value).booleanValue() ? 1 : 0); + return; + case kInteger: + case kEnum: + property.set(((Double) value).intValue()); + return; + case kString: + property.setString((String) value); + return; + default: + return; + } + }, ITable.NOTIFY_IMMEDIATE | ITable.NOTIFY_UPDATE); } /** @@ -377,7 +602,8 @@ public class CameraServer { */ public AxisCamera addAxisCamera(String name, String host) { AxisCamera camera = new AxisCamera(name, host); - addCamera(camera); + // Create a passthrough MJPEG server for USB access + startAutomaticCapture(camera); return camera; } @@ -389,7 +615,8 @@ public class CameraServer { */ public AxisCamera addAxisCamera(String name, String[] hosts) { AxisCamera camera = new AxisCamera(name, hosts); - addCamera(camera); + // Create a passthrough MJPEG server for USB access + startAutomaticCapture(camera); return camera; }