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}