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