001// Copyright (c) FIRST and other WPILib contributors. 002// Open Source Software; you can modify and/or share it under the terms of 003// the WPILib BSD license file in the root directory of this project. 004 005package edu.wpi.first.cameraserver; 006 007import edu.wpi.first.cscore.AxisCamera; 008import edu.wpi.first.cscore.CameraServerJNI; 009import edu.wpi.first.cscore.CvSink; 010import edu.wpi.first.cscore.CvSource; 011import edu.wpi.first.cscore.MjpegServer; 012import edu.wpi.first.cscore.UsbCamera; 013import edu.wpi.first.cscore.VideoEvent; 014import edu.wpi.first.cscore.VideoException; 015import edu.wpi.first.cscore.VideoListener; 016import edu.wpi.first.cscore.VideoMode; 017import edu.wpi.first.cscore.VideoSink; 018import edu.wpi.first.cscore.VideoSource; 019import edu.wpi.first.networktables.BooleanEntry; 020import edu.wpi.first.networktables.BooleanPublisher; 021import edu.wpi.first.networktables.IntegerEntry; 022import edu.wpi.first.networktables.IntegerPublisher; 023import edu.wpi.first.networktables.NetworkTable; 024import edu.wpi.first.networktables.NetworkTableInstance; 025import edu.wpi.first.networktables.StringArrayPublisher; 026import edu.wpi.first.networktables.StringArrayTopic; 027import edu.wpi.first.networktables.StringEntry; 028import edu.wpi.first.networktables.StringPublisher; 029import edu.wpi.first.util.PixelFormat; 030import java.lang.ref.Reference; 031import java.util.ArrayList; 032import java.util.Arrays; 033import java.util.HashMap; 034import java.util.Map; 035import java.util.Objects; 036import java.util.concurrent.atomic.AtomicInteger; 037 038/** 039 * Singleton class for creating and keeping camera servers. Also publishes camera information to 040 * NetworkTables. 041 */ 042public final class CameraServer { 043 /** CameraServer base port. */ 044 public static final int kBasePort = 1181; 045 046 private static final String kPublishName = "/CameraPublisher"; 047 048 private static final class PropertyPublisher implements AutoCloseable { 049 @SuppressWarnings({"PMD.MissingBreakInSwitch", "PMD.ImplicitSwitchFallThrough", "fallthrough"}) 050 PropertyPublisher(NetworkTable table, VideoEvent event) { 051 String name; 052 String infoName; 053 if (event.name.startsWith("raw_")) { 054 name = "RawProperty/" + event.name; 055 infoName = "RawPropertyInfo/" + event.name; 056 } else { 057 name = "Property/" + event.name; 058 infoName = "PropertyInfo/" + event.name; 059 } 060 061 try { 062 switch (event.propertyKind) { 063 case kBoolean: 064 m_booleanValueEntry = table.getBooleanTopic(name).getEntry(false); 065 m_booleanValueEntry.setDefault(event.value != 0); 066 break; 067 case kEnum: 068 m_choicesTopic = table.getStringArrayTopic(infoName + "/choices"); 069 // fall through 070 case kInteger: 071 m_integerValueEntry = table.getIntegerTopic(name).getEntry(0); 072 m_minPublisher = table.getIntegerTopic(infoName + "/min").publish(); 073 m_maxPublisher = table.getIntegerTopic(infoName + "/max").publish(); 074 m_stepPublisher = table.getIntegerTopic(infoName + "/step").publish(); 075 m_defaultPublisher = table.getIntegerTopic(infoName + "/default").publish(); 076 077 m_integerValueEntry.setDefault(event.value); 078 m_minPublisher.set(CameraServerJNI.getPropertyMin(event.propertyHandle)); 079 m_maxPublisher.set(CameraServerJNI.getPropertyMax(event.propertyHandle)); 080 m_stepPublisher.set(CameraServerJNI.getPropertyStep(event.propertyHandle)); 081 m_defaultPublisher.set(CameraServerJNI.getPropertyDefault(event.propertyHandle)); 082 break; 083 case kString: 084 m_stringValueEntry = table.getStringTopic(name).getEntry(""); 085 m_stringValueEntry.setDefault(event.valueStr); 086 break; 087 default: 088 break; 089 } 090 } catch (VideoException ignored) { 091 // ignore 092 } 093 } 094 095 void update(VideoEvent event) { 096 switch (event.propertyKind) { 097 case kBoolean: 098 if (m_booleanValueEntry != null) { 099 m_booleanValueEntry.set(event.value != 0); 100 } 101 break; 102 case kInteger: 103 case kEnum: 104 if (m_integerValueEntry != null) { 105 m_integerValueEntry.set(event.value); 106 } 107 break; 108 case kString: 109 if (m_stringValueEntry != null) { 110 m_stringValueEntry.set(event.valueStr); 111 } 112 break; 113 default: 114 break; 115 } 116 } 117 118 @Override 119 public void close() { 120 if (m_booleanValueEntry != null) { 121 m_booleanValueEntry.close(); 122 } 123 if (m_integerValueEntry != null) { 124 m_integerValueEntry.close(); 125 } 126 if (m_stringValueEntry != null) { 127 m_stringValueEntry.close(); 128 } 129 if (m_minPublisher != null) { 130 m_minPublisher.close(); 131 } 132 if (m_maxPublisher != null) { 133 m_maxPublisher.close(); 134 } 135 if (m_stepPublisher != null) { 136 m_stepPublisher.close(); 137 } 138 if (m_defaultPublisher != null) { 139 m_defaultPublisher.close(); 140 } 141 if (m_choicesPublisher != null) { 142 m_choicesPublisher.close(); 143 } 144 Reference.reachabilityFence(m_videoListener); 145 } 146 147 BooleanEntry m_booleanValueEntry; 148 IntegerEntry m_integerValueEntry; 149 StringEntry m_stringValueEntry; 150 IntegerPublisher m_minPublisher; 151 IntegerPublisher m_maxPublisher; 152 IntegerPublisher m_stepPublisher; 153 IntegerPublisher m_defaultPublisher; 154 StringArrayTopic m_choicesTopic; 155 StringArrayPublisher m_choicesPublisher; 156 } 157 158 private static final class SourcePublisher implements AutoCloseable { 159 SourcePublisher(NetworkTable table, int sourceHandle) { 160 this.m_table = table; 161 m_sourcePublisher = table.getStringTopic("source").publish(); 162 m_descriptionPublisher = table.getStringTopic("description").publish(); 163 m_connectedPublisher = table.getBooleanTopic("connected").publish(); 164 m_streamsPublisher = table.getStringArrayTopic("streams").publish(); 165 m_modeEntry = table.getStringTopic("mode").getEntry(""); 166 m_modesPublisher = table.getStringArrayTopic("modes").publish(); 167 168 m_sourcePublisher.set(makeSourceValue(sourceHandle)); 169 m_descriptionPublisher.set(CameraServerJNI.getSourceDescription(sourceHandle)); 170 m_connectedPublisher.set(CameraServerJNI.isSourceConnected(sourceHandle)); 171 m_streamsPublisher.set(getSourceStreamValues(sourceHandle)); 172 173 try { 174 VideoMode mode = CameraServerJNI.getSourceVideoMode(sourceHandle); 175 m_modeEntry.setDefault(videoModeToString(mode)); 176 m_modesPublisher.set(getSourceModeValues(sourceHandle)); 177 } catch (VideoException ignored) { 178 // Do nothing. Let the other event handlers update this if there is an error. 179 } 180 } 181 182 @Override 183 public void close() throws Exception { 184 m_sourcePublisher.close(); 185 m_descriptionPublisher.close(); 186 m_connectedPublisher.close(); 187 m_streamsPublisher.close(); 188 m_modeEntry.close(); 189 m_modesPublisher.close(); 190 for (PropertyPublisher pp : m_properties.values()) { 191 pp.close(); 192 } 193 } 194 195 final NetworkTable m_table; 196 final StringPublisher m_sourcePublisher; 197 final StringPublisher m_descriptionPublisher; 198 final BooleanPublisher m_connectedPublisher; 199 final StringArrayPublisher m_streamsPublisher; 200 final StringEntry m_modeEntry; 201 final StringArrayPublisher m_modesPublisher; 202 final Map<Integer, PropertyPublisher> m_properties = new HashMap<>(); 203 } 204 205 private static final AtomicInteger m_defaultUsbDevice = new AtomicInteger(); 206 private static String m_primarySourceName; 207 private static final Map<String, VideoSource> m_sources = new HashMap<>(); 208 private static final Map<String, VideoSink> m_sinks = new HashMap<>(); 209 private static final Map<Integer, SourcePublisher> m_publishers = 210 new HashMap<>(); // indexed by source handle 211 // source handle indexed by sink handle 212 private static final Map<Integer, Integer> m_fixedSources = new HashMap<>(); 213 private static final NetworkTable m_publishTable = 214 NetworkTableInstance.getDefault().getTable(kPublishName); 215 216 // We publish sources to NetworkTables using the following structure: 217 // "/CameraPublisher/{Source.Name}/" - root 218 // - "source" (string): Descriptive, prefixed with type (e.g. "usb:0") 219 // - "streams" (string array): URLs that can be used to stream data 220 // - "description" (string): Description of the source 221 // - "connected" (boolean): Whether source is connected 222 // - "mode" (string): Current video mode 223 // - "modes" (string array): Available video modes 224 // - "Property/{Property}" - Property values 225 // - "PropertyInfo/{Property}" - Property supporting information 226 227 // Listener for video events 228 @SuppressWarnings("PMD.AvoidCatchingGenericException") 229 private static final VideoListener m_videoListener = 230 new VideoListener( 231 event -> { 232 synchronized (CameraServer.class) { 233 switch (event.kind) { 234 case kSourceCreated: 235 { 236 // Create subtable for the camera 237 NetworkTable table = m_publishTable.getSubTable(event.name); 238 m_publishers.put( 239 event.sourceHandle, new SourcePublisher(table, event.sourceHandle)); 240 break; 241 } 242 case kSourceDestroyed: 243 { 244 SourcePublisher publisher = m_publishers.remove(event.sourceHandle); 245 if (publisher != null) { 246 try { 247 publisher.close(); 248 } catch (Exception e) { 249 // ignore (nothing we can do about it) 250 } 251 } 252 break; 253 } 254 case kSourceConnected: 255 { 256 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 257 if (publisher != null) { 258 // update the description too (as it may have changed) 259 publisher.m_descriptionPublisher.set( 260 CameraServerJNI.getSourceDescription(event.sourceHandle)); 261 publisher.m_connectedPublisher.set(true); 262 } 263 break; 264 } 265 case kSourceDisconnected: 266 { 267 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 268 if (publisher != null) { 269 publisher.m_connectedPublisher.set(false); 270 } 271 break; 272 } 273 case kSourceVideoModesUpdated: 274 { 275 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 276 if (publisher != null) { 277 publisher.m_modesPublisher.set(getSourceModeValues(event.sourceHandle)); 278 } 279 break; 280 } 281 case kSourceVideoModeChanged: 282 { 283 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 284 if (publisher != null) { 285 publisher.m_modeEntry.set(videoModeToString(event.mode)); 286 } 287 break; 288 } 289 case kSourcePropertyCreated: 290 { 291 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 292 if (publisher != null) { 293 publisher.m_properties.put( 294 event.propertyHandle, new PropertyPublisher(publisher.m_table, event)); 295 } 296 break; 297 } 298 case kSourcePropertyValueUpdated: 299 { 300 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 301 if (publisher != null) { 302 PropertyPublisher pp = publisher.m_properties.get(event.propertyHandle); 303 if (pp != null) { 304 pp.update(event); 305 } 306 } 307 break; 308 } 309 case kSourcePropertyChoicesUpdated: 310 { 311 SourcePublisher publisher = m_publishers.get(event.sourceHandle); 312 if (publisher != null) { 313 PropertyPublisher pp = publisher.m_properties.get(event.propertyHandle); 314 if (pp != null && pp.m_choicesTopic != null) { 315 try { 316 String[] choices = 317 CameraServerJNI.getEnumPropertyChoices(event.propertyHandle); 318 if (pp.m_choicesPublisher == null) { 319 pp.m_choicesPublisher = pp.m_choicesTopic.publish(); 320 } 321 pp.m_choicesPublisher.set(choices); 322 } catch (VideoException ignored) { 323 // ignore (just don't publish choices if we can't get them) 324 } 325 } 326 } 327 break; 328 } 329 case kSinkSourceChanged: 330 case kSinkCreated: 331 case kSinkDestroyed: 332 case kNetworkInterfacesChanged: 333 { 334 m_addresses = CameraServerJNI.getNetworkInterfaces(); 335 updateStreamValues(); 336 break; 337 } 338 default: 339 break; 340 } 341 } 342 }, 343 0x4fff, 344 true); 345 346 private static int m_nextPort = kBasePort; 347 private static String[] m_addresses = new String[0]; 348 349 /** 350 * Return URI of source with the given index. 351 * 352 * @param source Source index. 353 */ 354 private static String makeSourceValue(int source) { 355 switch (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))) { 356 case kUsb: 357 return "usb:" + CameraServerJNI.getUsbCameraPath(source); 358 case kHttp: 359 { 360 String[] urls = CameraServerJNI.getHttpCameraUrls(source); 361 if (urls.length > 0) { 362 return "ip:" + urls[0]; 363 } else { 364 return "ip:"; 365 } 366 } 367 case kCv: 368 return "cv:"; 369 case kRaw: 370 return "raw:"; 371 default: 372 return "unknown:"; 373 } 374 } 375 376 /** 377 * Return URI of stream with the given address and port. 378 * 379 * @param address Stream IP address. 380 * @param port Stream remote port. 381 */ 382 private static String makeStreamValue(String address, int port) { 383 return "mjpg:http://" + address + ":" + port + "/?action=stream"; 384 } 385 386 /** 387 * Return URI of sink stream with the given index. 388 * 389 * @param sink Sink index. 390 */ 391 private static synchronized String[] getSinkStreamValues(int sink) { 392 // Ignore all but MjpegServer 393 if (VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) != VideoSink.Kind.kMjpeg) { 394 return new String[0]; 395 } 396 397 // Get port 398 int port = CameraServerJNI.getMjpegServerPort(sink); 399 400 // Generate values 401 ArrayList<String> values = new ArrayList<>(m_addresses.length + 1); 402 String listenAddress = CameraServerJNI.getMjpegServerListenAddress(sink); 403 if (!listenAddress.isEmpty()) { 404 // If a listen address is specified, only use that 405 values.add(makeStreamValue(listenAddress, port)); 406 } else { 407 // Otherwise generate for hostname and all interface addresses 408 values.add(makeStreamValue(CameraServerJNI.getHostname() + ".local", port)); 409 for (String addr : m_addresses) { 410 if ("127.0.0.1".equals(addr)) { 411 continue; // ignore localhost 412 } 413 values.add(makeStreamValue(addr, port)); 414 } 415 } 416 417 return values.toArray(new String[0]); 418 } 419 420 /** 421 * Return list of stream source URIs for the given source index. 422 * 423 * @param source Source index. 424 */ 425 private static synchronized String[] getSourceStreamValues(int source) { 426 // Ignore all but HttpCamera 427 if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) 428 != VideoSource.Kind.kHttp) { 429 return new String[0]; 430 } 431 432 // Generate values 433 String[] values = CameraServerJNI.getHttpCameraUrls(source); 434 for (int j = 0; j < values.length; j++) { 435 values[j] = "mjpg:" + values[j]; 436 } 437 438 if (CameraServerSharedStore.getCameraServerShared().isRoboRIO()) { 439 // Look to see if we have a passthrough server for this source 440 // Only do this on the roboRIO 441 for (VideoSink i : m_sinks.values()) { 442 int sink = i.getHandle(); 443 int sinkSource = CameraServerJNI.getSinkSource(sink); 444 if (source == sinkSource 445 && VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) 446 == VideoSink.Kind.kMjpeg) { 447 // Add USB-only passthrough 448 String[] finalValues = Arrays.copyOf(values, values.length + 1); 449 int port = CameraServerJNI.getMjpegServerPort(sink); 450 finalValues[values.length] = makeStreamValue("172.22.11.2", port); 451 return finalValues; 452 } 453 } 454 } 455 456 return values; 457 } 458 459 /** Update list of stream URIs. */ 460 private static synchronized void updateStreamValues() { 461 // Over all the sinks... 462 for (VideoSink i : m_sinks.values()) { 463 int sink = i.getHandle(); 464 465 // Get the source's subtable (if none exists, we're done) 466 int source = 467 Objects.requireNonNullElseGet( 468 m_fixedSources.get(sink), () -> CameraServerJNI.getSinkSource(sink)); 469 470 if (source == 0) { 471 continue; 472 } 473 SourcePublisher publisher = m_publishers.get(source); 474 if (publisher != null) { 475 // Don't set stream values if this is a HttpCamera passthrough 476 if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source)) 477 == VideoSource.Kind.kHttp) { 478 continue; 479 } 480 481 // Set table value 482 String[] values = getSinkStreamValues(sink); 483 if (values.length > 0) { 484 publisher.m_streamsPublisher.set(values); 485 } 486 } 487 } 488 489 // Over all the sources... 490 for (VideoSource i : m_sources.values()) { 491 int source = i.getHandle(); 492 493 // Get the source's subtable (if none exists, we're done) 494 SourcePublisher publisher = m_publishers.get(source); 495 if (publisher != null) { 496 // Set table value 497 String[] values = getSourceStreamValues(source); 498 if (values.length > 0) { 499 publisher.m_streamsPublisher.set(values); 500 } 501 } 502 } 503 } 504 505 /** Provide string description of pixel format. */ 506 private static String pixelFormatToString(PixelFormat pixelFormat) { 507 switch (pixelFormat) { 508 case kMJPEG: 509 return "MJPEG"; 510 case kYUYV: 511 return "YUYV"; 512 case kRGB565: 513 return "RGB565"; 514 case kBGR: 515 return "BGR"; 516 case kGray: 517 return "Gray"; 518 default: 519 return "Unknown"; 520 } 521 } 522 523 /** 524 * Provide string description of video mode. 525 * 526 * <p>The returned string is "{width}x{height} {format} {fps} fps". 527 */ 528 private static String videoModeToString(VideoMode mode) { 529 return mode.width 530 + "x" 531 + mode.height 532 + " " 533 + pixelFormatToString(mode.pixelFormat) 534 + " " 535 + mode.fps 536 + " fps"; 537 } 538 539 /** 540 * Get list of video modes for the given source handle. 541 * 542 * @param sourceHandle Source handle. 543 */ 544 private static String[] getSourceModeValues(int sourceHandle) { 545 VideoMode[] modes = CameraServerJNI.enumerateSourceVideoModes(sourceHandle); 546 String[] modeStrings = new String[modes.length]; 547 for (int i = 0; i < modes.length; i++) { 548 modeStrings[i] = videoModeToString(modes[i]); 549 } 550 return modeStrings; 551 } 552 553 private CameraServer() {} 554 555 /** 556 * Start automatically capturing images to send to the dashboard. 557 * 558 * <p>You should call this method to see a camera feed on the dashboard. If you also want to 559 * perform vision processing on the roboRIO, use getVideo() to get access to the camera images. 560 * 561 * <p>The first time this overload is called, it calls {@link #startAutomaticCapture(int)} with 562 * device 0, creating a camera named "USB Camera 0". Subsequent calls increment the device number 563 * (e.g. 1, 2, etc). 564 * 565 * @return The USB camera capturing images. 566 */ 567 public static UsbCamera startAutomaticCapture() { 568 UsbCamera camera = startAutomaticCapture(m_defaultUsbDevice.getAndIncrement()); 569 CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); 570 return camera; 571 } 572 573 /** 574 * Start automatically capturing images to send to the dashboard. 575 * 576 * <p>This overload calls {@link #startAutomaticCapture(String, int)} with a name of "USB Camera 577 * {dev}". 578 * 579 * @param dev The device number of the camera interface 580 * @return The USB camera capturing images. 581 */ 582 public static UsbCamera startAutomaticCapture(int dev) { 583 UsbCamera camera = new UsbCamera("USB Camera " + dev, dev); 584 startAutomaticCapture(camera); 585 CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); 586 return camera; 587 } 588 589 /** 590 * Start automatically capturing images to send to the dashboard. 591 * 592 * @param name The name to give the camera 593 * @param dev The device number of the camera interface 594 * @return The USB camera capturing images. 595 */ 596 public static UsbCamera startAutomaticCapture(String name, int dev) { 597 UsbCamera camera = new UsbCamera(name, dev); 598 startAutomaticCapture(camera); 599 CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); 600 return camera; 601 } 602 603 /** 604 * Start automatically capturing images to send to the dashboard. 605 * 606 * @param name The name to give the camera 607 * @param path The device path (e.g. "/dev/video0") of the camera 608 * @return The USB camera capturing images. 609 */ 610 public static UsbCamera startAutomaticCapture(String name, String path) { 611 UsbCamera camera = new UsbCamera(name, path); 612 startAutomaticCapture(camera); 613 CameraServerSharedStore.getCameraServerShared().reportUsbCamera(camera.getHandle()); 614 return camera; 615 } 616 617 /** 618 * Start automatically capturing images to send to the dashboard from an existing camera. 619 * 620 * @param camera Camera 621 * @return The MJPEG server serving images from the given camera. 622 */ 623 public static MjpegServer startAutomaticCapture(VideoSource camera) { 624 addCamera(camera); 625 MjpegServer server = addServer("serve_" + camera.getName()); 626 server.setSource(camera); 627 return server; 628 } 629 630 /** 631 * Adds an Axis IP camera. 632 * 633 * <p>This overload calls {@link #addAxisCamera(String, String)} with name "Axis Camera". 634 * 635 * @param host Camera host IP or DNS name (e.g. "10.x.y.11") 636 * @return The Axis camera capturing images. 637 * @deprecated Call startAutomaticCapture with a HttpCamera instead. 638 */ 639 @Deprecated(forRemoval = true, since = "2025") 640 @SuppressWarnings("removal") 641 public static AxisCamera addAxisCamera(String host) { 642 return addAxisCamera("Axis Camera", host); 643 } 644 645 /** 646 * Adds an Axis IP camera. 647 * 648 * <p>This overload calls {@link #addAxisCamera(String, String[])} with name "Axis Camera". 649 * 650 * @param hosts Array of Camera host IPs/DNS names 651 * @return The Axis camera capturing images. 652 * @deprecated Call startAutomaticCapture with a HttpCamera instead. 653 */ 654 @Deprecated(forRemoval = true, since = "2025") 655 @SuppressWarnings("removal") 656 public static AxisCamera addAxisCamera(String[] hosts) { 657 return addAxisCamera("Axis Camera", hosts); 658 } 659 660 /** 661 * Adds an Axis IP camera. 662 * 663 * @param name The name to give the camera 664 * @param host Camera host IP or DNS name (e.g. "10.x.y.11") 665 * @return The Axis camera capturing images. 666 * @deprecated Call startAutomaticCapture with a HttpCamera instead. 667 */ 668 @Deprecated(forRemoval = true, since = "2025") 669 @SuppressWarnings("removal") 670 public static AxisCamera addAxisCamera(String name, String host) { 671 AxisCamera camera = new AxisCamera(name, host); 672 // Create a passthrough MJPEG server for USB access 673 startAutomaticCapture(camera); 674 CameraServerSharedStore.getCameraServerShared().reportAxisCamera(camera.getHandle()); 675 return camera; 676 } 677 678 /** 679 * Adds an Axis IP camera. 680 * 681 * @param name The name to give the camera 682 * @param hosts Array of Camera host IPs/DNS names 683 * @return The Axis camera capturing images. 684 * @deprecated Call startAutomaticCapture with a HttpCamera instead. 685 */ 686 @Deprecated(forRemoval = true, since = "2025") 687 @SuppressWarnings("removal") 688 public static AxisCamera addAxisCamera(String name, String[] hosts) { 689 AxisCamera camera = new AxisCamera(name, hosts); 690 // Create a passthrough MJPEG server for USB access 691 startAutomaticCapture(camera); 692 CameraServerSharedStore.getCameraServerShared().reportAxisCamera(camera.getHandle()); 693 return camera; 694 } 695 696 /** 697 * Adds a virtual camera for switching between two streams. Unlike the other addCamera methods, 698 * this returns a VideoSink rather than a VideoSource. Calling setSource() on the returned object 699 * can be used to switch the actual source of the stream. 700 * 701 * @param name The name to give the camera 702 * @return The MJPEG server serving images from the given camera. 703 */ 704 public static MjpegServer addSwitchedCamera(String name) { 705 // create a dummy CvSource 706 CvSource source = new CvSource(name, PixelFormat.kMJPEG, 160, 120, 30); 707 MjpegServer server = startAutomaticCapture(source); 708 synchronized (CameraServer.class) { 709 m_fixedSources.put(server.getHandle(), source.getHandle()); 710 } 711 712 return server; 713 } 714 715 /** 716 * Get OpenCV access to the primary camera feed. This allows you to get images from the camera for 717 * image processing on the roboRIO. 718 * 719 * <p>This is only valid to call after a camera feed has been added with startAutomaticCapture() 720 * or addServer(). 721 * 722 * @return OpenCV sink for the primary camera feed 723 */ 724 public static CvSink getVideo() { 725 VideoSource source; 726 synchronized (CameraServer.class) { 727 if (m_primarySourceName == null) { 728 throw new VideoException("no camera available"); 729 } 730 source = m_sources.get(m_primarySourceName); 731 } 732 if (source == null) { 733 throw new VideoException("no camera available"); 734 } 735 return getVideo(source); 736 } 737 738 /** 739 * Get OpenCV access to the specified camera. This allows you to get images from the camera for 740 * image processing on the roboRIO. 741 * 742 * @param camera Camera (e.g. as returned by startAutomaticCapture). 743 * @return OpenCV sink for the specified camera 744 */ 745 public static CvSink getVideo(VideoSource camera) { 746 String name = "opencv_" + camera.getName(); 747 748 synchronized (CameraServer.class) { 749 VideoSink sink = m_sinks.get(name); 750 if (sink != null) { 751 VideoSink.Kind kind = sink.getKind(); 752 if (kind != VideoSink.Kind.kCv) { 753 throw new VideoException("expected OpenCV sink, but got " + kind); 754 } 755 return (CvSink) sink; 756 } 757 } 758 759 CvSink newsink = new CvSink(name); 760 newsink.setSource(camera); 761 addServer(newsink); 762 return newsink; 763 } 764 765 /** 766 * Get OpenCV access to the specified camera. This allows you to get images from the camera for 767 * image processing on the roboRIO. 768 * 769 * @param camera Camera (e.g. as returned by startAutomaticCapture). 770 * @param pixelFormat Desired pixelFormat of the camera 771 * @return OpenCV sink for the specified camera 772 */ 773 public static CvSink getVideo(VideoSource camera, PixelFormat pixelFormat) { 774 String name = "opencv_" + camera.getName(); 775 776 synchronized (CameraServer.class) { 777 VideoSink sink = m_sinks.get(name); 778 if (sink != null) { 779 VideoSink.Kind kind = sink.getKind(); 780 if (kind != VideoSink.Kind.kCv) { 781 throw new VideoException("expected OpenCV sink, but got " + kind); 782 } 783 return (CvSink) sink; 784 } 785 } 786 787 CvSink newsink = new CvSink(name, pixelFormat); 788 newsink.setSource(camera); 789 addServer(newsink); 790 return newsink; 791 } 792 793 /** 794 * Get OpenCV access to the specified camera. This allows you to get images from the camera for 795 * image processing on the roboRIO. 796 * 797 * @param name Camera name 798 * @return OpenCV sink for the specified camera 799 */ 800 public static CvSink getVideo(String name) { 801 VideoSource source; 802 synchronized (CameraServer.class) { 803 source = m_sources.get(name); 804 if (source == null) { 805 throw new VideoException("could not find camera " + name); 806 } 807 } 808 return getVideo(source); 809 } 810 811 /** 812 * Create a MJPEG stream with OpenCV input. This can be called to pass custom annotated images to 813 * the dashboard. 814 * 815 * @param name Name to give the stream 816 * @param width Width of the image being sent 817 * @param height Height of the image being sent 818 * @return OpenCV source for the MJPEG stream 819 */ 820 public static CvSource putVideo(String name, int width, int height) { 821 CvSource source = new CvSource(name, PixelFormat.kMJPEG, width, height, 30); 822 startAutomaticCapture(source); 823 return source; 824 } 825 826 /** 827 * Adds a MJPEG server at the next available port. 828 * 829 * @param name Server name 830 * @return The MJPEG server 831 */ 832 public static MjpegServer addServer(String name) { 833 int port; 834 synchronized (CameraServer.class) { 835 port = m_nextPort; 836 m_nextPort++; 837 } 838 return addServer(name, port); 839 } 840 841 /** 842 * Adds a MJPEG server. 843 * 844 * @param name Server name 845 * @param port Server port 846 * @return The MJPEG server 847 */ 848 public static MjpegServer addServer(String name, int port) { 849 MjpegServer server = new MjpegServer(name, port); 850 addServer(server); 851 return server; 852 } 853 854 /** 855 * Adds an already created server. 856 * 857 * @param server Server 858 */ 859 public static void addServer(VideoSink server) { 860 synchronized (CameraServer.class) { 861 m_sinks.put(server.getName(), server); 862 } 863 } 864 865 /** 866 * Removes a server by name. 867 * 868 * @param name Server name 869 */ 870 public static void removeServer(String name) { 871 synchronized (CameraServer.class) { 872 m_sinks.remove(name); 873 } 874 } 875 876 /** 877 * Get server for the primary camera feed. 878 * 879 * <p>This is only valid to call after a camera feed has been added with startAutomaticCapture() 880 * or addServer(). 881 * 882 * @return The server for the primary camera feed 883 */ 884 public static VideoSink getServer() { 885 synchronized (CameraServer.class) { 886 if (m_primarySourceName == null) { 887 throw new VideoException("no camera available"); 888 } 889 return getServer("serve_" + m_primarySourceName); 890 } 891 } 892 893 /** 894 * Gets a server by name. 895 * 896 * @param name Server name 897 * @return The server 898 */ 899 public static VideoSink getServer(String name) { 900 synchronized (CameraServer.class) { 901 return m_sinks.get(name); 902 } 903 } 904 905 /** 906 * Adds an already created camera. 907 * 908 * @param camera Camera 909 */ 910 public static void addCamera(VideoSource camera) { 911 String name = camera.getName(); 912 synchronized (CameraServer.class) { 913 if (m_primarySourceName == null) { 914 m_primarySourceName = name; 915 } 916 m_sources.put(name, camera); 917 } 918 } 919 920 /** 921 * Removes a camera by name. 922 * 923 * @param name Camera name 924 */ 925 public static void removeCamera(String name) { 926 synchronized (CameraServer.class) { 927 m_sources.remove(name); 928 } 929 } 930}