001/*----------------------------------------------------------------------------*/
002/* Copyright (c) FIRST 2016-2017. All Rights Reserved.                        */
003/* Open Source Software - may be modified and shared by FRC teams. The code   */
004/* must be accompanied by the FIRST BSD license file in the root directory of */
005/* the project.                                                               */
006/*----------------------------------------------------------------------------*/
007
008package edu.wpi.first.wpilibj;
009
010import edu.wpi.cscore.AxisCamera;
011import edu.wpi.cscore.CameraServerJNI;
012import edu.wpi.cscore.CvSink;
013import edu.wpi.cscore.CvSource;
014import edu.wpi.cscore.MjpegServer;
015import edu.wpi.cscore.UsbCamera;
016import edu.wpi.cscore.VideoEvent;
017import edu.wpi.cscore.VideoException;
018import edu.wpi.cscore.VideoListener;
019import edu.wpi.cscore.VideoMode;
020import edu.wpi.cscore.VideoMode.PixelFormat;
021import edu.wpi.cscore.VideoProperty;
022import edu.wpi.cscore.VideoSink;
023import edu.wpi.cscore.VideoSource;
024import edu.wpi.first.wpilibj.networktables.NetworkTable;
025import edu.wpi.first.wpilibj.networktables.NetworkTablesJNI;
026import edu.wpi.first.wpilibj.tables.ITable;
027import java.util.concurrent.atomic.AtomicInteger;
028import java.util.ArrayList;
029import java.util.Hashtable;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033/**
034 * Singleton class for creating and keeping camera servers.
035 * Also publishes camera information to NetworkTables.
036 */
037public class CameraServer {
038  public static final int kBasePort = 1181;
039
040  @Deprecated
041  public static final int kSize640x480 = 0;
042  @Deprecated
043  public static final int kSize320x240 = 1;
044  @Deprecated
045  public static final int kSize160x120 = 2;
046
047  private static final String kPublishName = "/CameraPublisher";
048  private static CameraServer server;
049
050  /**
051   * Get the CameraServer instance.
052   */
053  public static synchronized CameraServer getInstance() {
054    if (server == null) {
055      server = new CameraServer();
056    }
057    return server;
058  }
059
060  private AtomicInteger m_defaultUsbDevice;
061  private String m_primarySourceName;
062  private final Hashtable<String, VideoSource> m_sources;
063  private final Hashtable<String, VideoSink> m_sinks;
064  private final Hashtable<Integer, ITable> m_tables;  // indexed by source handle
065  private final ITable m_publishTable;
066  private final VideoListener m_videoListener; //NOPMD
067  private final int m_tableListener; //NOPMD
068  private int m_nextPort;
069  private String[] m_addresses;
070
071  @SuppressWarnings("JavadocMethod")
072  private static String makeSourceValue(int source) {
073    switch (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))) {
074      case kUsb:
075        return "usb:" + CameraServerJNI.getUsbCameraPath(source);
076      case kHttp: {
077        String[] urls = CameraServerJNI.getHttpCameraUrls(source);
078        if (urls.length > 0) {
079          return "ip:" + urls[0];
080        } else {
081          return "ip:";
082        }
083      }
084      case kCv:
085        // FIXME: Should be "cv:", but LabVIEW dashboard requires "usb:".
086        // https://github.com/wpilibsuite/allwpilib/issues/407
087        return "usb:";
088      default:
089        return "unknown:";
090    }
091  }
092
093  @SuppressWarnings("JavadocMethod")
094  private static String makeStreamValue(String address, int port) {
095    return "mjpg:http://" + address + ":" + port + "/?action=stream";
096  }
097
098  @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"})
099  private synchronized String[] getSinkStreamValues(int sink) {
100    // Ignore all but MjpegServer
101    if (VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) != VideoSink.Kind.kMjpeg) {
102      return new String[0];
103    }
104
105    // Get port
106    int port = CameraServerJNI.getMjpegServerPort(sink);
107
108    // Generate values
109    ArrayList<String> values = new ArrayList<String>(m_addresses.length + 1);
110    String listenAddress = CameraServerJNI.getMjpegServerListenAddress(sink);
111    if (!listenAddress.isEmpty()) {
112      // If a listen address is specified, only use that
113      values.add(makeStreamValue(listenAddress, port));
114    } else {
115      // Otherwise generate for hostname and all interface addresses
116      values.add(makeStreamValue(CameraServerJNI.getHostname() + ".local", port));
117      for (String addr : m_addresses) {
118        if (addr.equals("127.0.0.1")) {
119          continue;  // ignore localhost
120        }
121        values.add(makeStreamValue(addr, port));
122      }
123    }
124
125    return values.toArray(new String[0]);
126  }
127
128  @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"})
129  private synchronized String[] getSourceStreamValues(int source) {
130    // Ignore all but HttpCamera
131    if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))
132            != VideoSource.Kind.kHttp) {
133      return new String[0];
134    }
135
136    // Generate values
137    String[] values = CameraServerJNI.getHttpCameraUrls(source);
138    for (int j = 0; j < values.length; j++) {
139      values[j] = "mjpg:" + values[j];
140    }
141
142    // Look to see if we have a passthrough server for this source
143    for (VideoSink i : m_sinks.values()) {
144      int sink = i.getHandle();
145      int sinkSource = CameraServerJNI.getSinkSource(sink);
146      if (source == sinkSource
147          && VideoSink.getKindFromInt(CameraServerJNI.getSinkKind(sink)) == VideoSink.Kind.kMjpeg) {
148        // Add USB-only passthrough
149        String[] finalValues = new String[values.length + 1];
150        for (int j = 0; j < values.length; j++) {
151          finalValues[j] = values[j];
152        }
153        int port = CameraServerJNI.getMjpegServerPort(sink);
154        finalValues[values.length] = makeStreamValue("172.22.11.2", port);
155        return finalValues;
156      }
157    }
158
159    return values;
160  }
161
162  @SuppressWarnings({"JavadocMethod", "PMD.AvoidUsingHardCodedIP"})
163  private synchronized void updateStreamValues() {
164    // Over all the sinks...
165    for (VideoSink i : m_sinks.values()) {
166      int sink = i.getHandle();
167
168      // Get the source's subtable (if none exists, we're done)
169      int source = CameraServerJNI.getSinkSource(sink);
170      if (source == 0) {
171        continue;
172      }
173      ITable table = m_tables.get(source);
174      if (table != null) {
175        // Don't set stream values if this is a HttpCamera passthrough
176        if (VideoSource.getKindFromInt(CameraServerJNI.getSourceKind(source))
177            == VideoSource.Kind.kHttp) {
178          continue;
179        }
180
181        // Set table value
182        String[] values = getSinkStreamValues(sink);
183        if (values.length > 0) {
184          table.putStringArray("streams", values);
185        }
186      }
187    }
188
189    // Over all the sources...
190    for (VideoSource i : m_sources.values()) {
191      int source = i.getHandle();
192
193      // Get the source's subtable (if none exists, we're done)
194      ITable table = m_tables.get(source);
195      if (table != null) {
196        // Set table value
197        String[] values = getSourceStreamValues(source);
198        if (values.length > 0) {
199          table.putStringArray("streams", values);
200        }
201      }
202    }
203  }
204
205  @SuppressWarnings("JavadocMethod")
206  private static String pixelFormatToString(PixelFormat pixelFormat) {
207    switch (pixelFormat) {
208      case kMJPEG:
209        return "MJPEG";
210      case kYUYV:
211        return "YUYV";
212      case kRGB565:
213        return "RGB565";
214      case kBGR:
215        return "BGR";
216      case kGray:
217        return "Gray";
218      default:
219        return "Unknown";
220    }
221  }
222
223  @SuppressWarnings("JavadocMethod")
224  private static PixelFormat pixelFormatFromString(String pixelFormatStr) {
225    switch (pixelFormatStr) {
226      case "MJPEG":
227      case "mjpeg":
228      case "JPEG":
229      case "jpeg":
230        return PixelFormat.kMJPEG;
231      case "YUYV":
232      case "yuyv":
233        return PixelFormat.kYUYV;
234      case "RGB565":
235      case "rgb565":
236        return PixelFormat.kRGB565;
237      case "BGR":
238      case "bgr":
239        return PixelFormat.kBGR;
240      case "GRAY":
241      case "Gray":
242      case "gray":
243        return PixelFormat.kGray;
244      default:
245        return PixelFormat.kUnknown;
246    }
247  }
248
249  private static final Pattern reMode =
250      Pattern.compile("(?<width>[0-9]+)\\s*x\\s*(?<height>[0-9]+)\\s+(?<format>.*?)\\s+"
251          + "(?<fps>[0-9.]+)\\s*fps");
252
253  /// Construct a video mode from a string description.
254  @SuppressWarnings("JavadocMethod")
255  private static VideoMode videoModeFromString(String modeStr) {
256    Matcher matcher = reMode.matcher(modeStr);
257    if (!matcher.matches()) {
258      return new VideoMode(PixelFormat.kUnknown, 0, 0, 0);
259    }
260    PixelFormat pixelFormat = pixelFormatFromString(matcher.group("format"));
261    int width = Integer.parseInt(matcher.group("width"));
262    int height = Integer.parseInt(matcher.group("height"));
263    int fps = (int) Double.parseDouble(matcher.group("fps"));
264    return new VideoMode(pixelFormat, width, height, fps);
265  }
266
267  /// Provide string description of video mode.
268  /// The returned string is "{width}x{height} {format} {fps} fps".
269  @SuppressWarnings("JavadocMethod")
270  private static String videoModeToString(VideoMode mode) {
271    return mode.width + "x" + mode.height + " " + pixelFormatToString(mode.pixelFormat)
272        + " " + mode.fps + " fps";
273  }
274
275  @SuppressWarnings("JavadocMethod")
276  private static String[] getSourceModeValues(int sourceHandle) {
277    VideoMode[] modes = CameraServerJNI.enumerateSourceVideoModes(sourceHandle);
278    String[] modeStrings = new String[modes.length];
279    for (int i = 0; i < modes.length; i++) {
280      modeStrings[i] = videoModeToString(modes[i]);
281    }
282    return modeStrings;
283  }
284
285  @SuppressWarnings("JavadocMethod")
286  private static void putSourcePropertyValue(ITable table, VideoEvent event, boolean isNew) {
287    String name;
288    String infoName;
289    if (event.name.startsWith("raw_")) {
290      name = "RawProperty/" + event.name;
291      infoName = "RawPropertyInfo/" + event.name;
292    } else {
293      name = "Property/" + event.name;
294      infoName = "PropertyInfo/" + event.name;
295    }
296
297    switch (event.propertyKind) {
298      case kBoolean:
299        if (isNew) {
300          table.setDefaultBoolean(name, event.value != 0);
301        } else {
302          table.putBoolean(name, event.value != 0);
303        }
304        break;
305      case kInteger:
306      case kEnum:
307        if (isNew) {
308          table.setDefaultNumber(name, event.value);
309          table.putNumber(infoName + "/min",
310              CameraServerJNI.getPropertyMin(event.propertyHandle));
311          table.putNumber(infoName + "/max",
312              CameraServerJNI.getPropertyMax(event.propertyHandle));
313          table.putNumber(infoName + "/step",
314              CameraServerJNI.getPropertyStep(event.propertyHandle));
315          table.putNumber(infoName + "/default",
316              CameraServerJNI.getPropertyDefault(event.propertyHandle));
317        } else {
318          table.putNumber(name, event.value);
319        }
320        break;
321      case kString:
322        if (isNew) {
323          table.setDefaultString(name, event.valueStr);
324        } else {
325          table.putString(name, event.valueStr);
326        }
327        break;
328      default:
329        break;
330    }
331  }
332
333  @SuppressWarnings({"JavadocMethod", "PMD.UnusedLocalVariable"})
334  private CameraServer() {
335    m_defaultUsbDevice = new AtomicInteger();
336    m_sources = new Hashtable<String, VideoSource>();
337    m_sinks = new Hashtable<String, VideoSink>();
338    m_tables = new Hashtable<Integer, ITable>();
339    m_publishTable = NetworkTable.getTable(kPublishName);
340    m_nextPort = kBasePort;
341    m_addresses = new String[0];
342
343    // We publish sources to NetworkTables using the following structure:
344    // "/CameraPublisher/{Source.Name}/" - root
345    // - "source" (string): Descriptive, prefixed with type (e.g. "usb:0")
346    // - "streams" (string array): URLs that can be used to stream data
347    // - "description" (string): Description of the source
348    // - "connected" (boolean): Whether source is connected
349    // - "mode" (string): Current video mode
350    // - "modes" (string array): Available video modes
351    // - "Property/{Property}" - Property values
352    // - "PropertyInfo/{Property}" - Property supporting information
353
354    // Listener for video events
355    m_videoListener = new VideoListener(event -> {
356      switch (event.kind) {
357        case kSourceCreated: {
358          // Create subtable for the camera
359          ITable table = m_publishTable.getSubTable(event.name);
360          m_tables.put(event.sourceHandle, table);
361          table.putString("source", makeSourceValue(event.sourceHandle));
362          table.putString("description",
363              CameraServerJNI.getSourceDescription(event.sourceHandle));
364          table.putBoolean("connected", CameraServerJNI.isSourceConnected(event.sourceHandle));
365          table.putStringArray("streams", getSourceStreamValues(event.sourceHandle));
366          try {
367            VideoMode mode = CameraServerJNI.getSourceVideoMode(event.sourceHandle);
368            table.setDefaultString("mode", videoModeToString(mode));
369            table.putStringArray("modes", getSourceModeValues(event.sourceHandle));
370          } catch (VideoException ex) {
371            // Do nothing. Let the other event handlers update this if there is an error.
372          }
373          break;
374        }
375        case kSourceDestroyed: {
376          ITable table = m_tables.get(event.sourceHandle);
377          if (table != null) {
378            table.putString("source", "");
379            table.putStringArray("streams", new String[0]);
380            table.putStringArray("modes", new String[0]);
381          }
382          break;
383        }
384        case kSourceConnected: {
385          ITable table = m_tables.get(event.sourceHandle);
386          if (table != null) {
387            // update the description too (as it may have changed)
388            table.putString("description",
389                CameraServerJNI.getSourceDescription(event.sourceHandle));
390            table.putBoolean("connected", true);
391          }
392          break;
393        }
394        case kSourceDisconnected: {
395          ITable table = m_tables.get(event.sourceHandle);
396          if (table != null) {
397            table.putBoolean("connected", false);
398          }
399          break;
400        }
401        case kSourceVideoModesUpdated: {
402          ITable table = m_tables.get(event.sourceHandle);
403          if (table != null) {
404            table.putStringArray("modes", getSourceModeValues(event.sourceHandle));
405          }
406          break;
407        }
408        case kSourceVideoModeChanged: {
409          ITable table = m_tables.get(event.sourceHandle);
410          if (table != null) {
411            table.putString("mode", videoModeToString(event.mode));
412          }
413          break;
414        }
415        case kSourcePropertyCreated: {
416          ITable table = m_tables.get(event.sourceHandle);
417          if (table != null) {
418            putSourcePropertyValue(table, event, true);
419          }
420          break;
421        }
422        case kSourcePropertyValueUpdated: {
423          ITable table = m_tables.get(event.sourceHandle);
424          if (table != null) {
425            putSourcePropertyValue(table, event, false);
426          }
427          break;
428        }
429        case kSourcePropertyChoicesUpdated: {
430          ITable table = m_tables.get(event.sourceHandle);
431          if (table != null) {
432            String[] choices = CameraServerJNI.getEnumPropertyChoices(event.propertyHandle);
433            table.putStringArray("PropertyInfo/" + event.name + "/choices", choices);
434          }
435          break;
436        }
437        case kSinkSourceChanged:
438        case kSinkCreated:
439        case kSinkDestroyed: {
440          updateStreamValues();
441          break;
442        }
443        case kNetworkInterfacesChanged: {
444          m_addresses = CameraServerJNI.getNetworkInterfaces();
445          break;
446        }
447        default:
448          break;
449      }
450    }, 0x4fff, true);
451
452    // Listener for NetworkTable events
453    // We don't currently support changing settings via NT due to
454    // synchronization issues, so just update to current setting if someone
455    // else tries to change it.
456    m_tableListener = NetworkTablesJNI.addEntryListener(kPublishName + "/",
457      (uid, key, eventValue, flags) -> {
458        String relativeKey = key.substring(kPublishName.length() + 1);
459
460        // get source (sourceName/...)
461        int subKeyIndex = relativeKey.indexOf('/');
462        if (subKeyIndex == -1) {
463          return;
464        }
465        String sourceName = relativeKey.substring(0, subKeyIndex);
466        VideoSource source = m_sources.get(sourceName);
467        if (source == null) {
468          return;
469        }
470
471        // get subkey
472        relativeKey = relativeKey.substring(subKeyIndex + 1);
473
474        // handle standard names
475        String propName;
476        if (relativeKey.equals("mode")) {
477          // reset to current mode
478          NetworkTablesJNI.putString(key, videoModeToString(source.getVideoMode()));
479          return;
480        } else if (relativeKey.startsWith("Property/")) {
481          propName = relativeKey.substring(9);
482        } else if (relativeKey.startsWith("RawProperty/")) {
483          propName = relativeKey.substring(12);
484        } else {
485          return;  // ignore
486        }
487
488        // everything else is a property
489        VideoProperty property = source.getProperty(propName);
490        switch (property.getKind()) {
491          case kNone:
492            return;
493          case kBoolean:
494            // reset to current setting
495            NetworkTablesJNI.putBoolean(key, property.get() != 0);
496            return;
497          case kInteger:
498          case kEnum:
499            // reset to current setting
500            NetworkTablesJNI.putDouble(key, property.get());
501            return;
502          case kString:
503            // reset to current setting
504            NetworkTablesJNI.putString(key, property.getString());
505            return;
506          default:
507            return;
508        }
509      }, ITable.NOTIFY_IMMEDIATE | ITable.NOTIFY_UPDATE);
510  }
511
512  /**
513   * Start automatically capturing images to send to the dashboard.
514   *
515   * <p>You should call this method to see a camera feed on the dashboard.
516   * If you also want to perform vision processing on the roboRIO, use
517   * getVideo() to get access to the camera images.
518   *
519   * <p>The first time this overload is called, it calls
520   * {@link #startAutomaticCapture(int)} with device 0, creating a camera
521   * named "USB Camera 0".  Subsequent calls increment the device number
522   * (e.g. 1, 2, etc).
523   */
524  public UsbCamera startAutomaticCapture() {
525    return startAutomaticCapture(m_defaultUsbDevice.getAndIncrement());
526  }
527
528  /**
529   * Start automatically capturing images to send to the dashboard.
530   *
531   * <p>This overload calls {@link #startAutomaticCapture(String, int)} with
532   * a name of "USB Camera {dev}".
533   *
534   * @param dev The device number of the camera interface
535   */
536  public UsbCamera startAutomaticCapture(int dev) {
537    UsbCamera camera = new UsbCamera("USB Camera " + dev, dev);
538    startAutomaticCapture(camera);
539    return camera;
540  }
541
542  /**
543   * Start automatically capturing images to send to the dashboard.
544   *
545   * @param name The name to give the camera
546   * @param dev The device number of the camera interface
547   */
548  public UsbCamera startAutomaticCapture(String name, int dev) {
549    UsbCamera camera = new UsbCamera(name, dev);
550    startAutomaticCapture(camera);
551    return camera;
552  }
553
554  /**
555   * Start automatically capturing images to send to the dashboard.
556   *
557   * @param name The name to give the camera
558   * @param path The device path (e.g. "/dev/video0") of the camera
559   */
560  public UsbCamera startAutomaticCapture(String name, String path) {
561    UsbCamera camera = new UsbCamera(name, path);
562    startAutomaticCapture(camera);
563    return camera;
564  }
565
566  /**
567   * Start automatically capturing images to send to the dashboard from
568   * an existing camera.
569   *
570   * @param camera Camera
571   */
572  public void startAutomaticCapture(VideoSource camera) {
573    addCamera(camera);
574    VideoSink server = addServer("serve_" + camera.getName());
575    server.setSource(camera);
576  }
577
578  /**
579   * Adds an Axis IP camera.
580   *
581   * <p>This overload calls {@link #addAxisCamera(String, String)} with
582   * name "Axis Camera".
583   *
584   * @param host Camera host IP or DNS name (e.g. "10.x.y.11")
585   */
586  public AxisCamera addAxisCamera(String host) {
587    return addAxisCamera("Axis Camera", host);
588  }
589
590  /**
591   * Adds an Axis IP camera.
592   *
593   * <p>This overload calls {@link #addAxisCamera(String, String[])} with
594   * name "Axis Camera".
595   *
596   * @param hosts Array of Camera host IPs/DNS names
597   */
598  public AxisCamera addAxisCamera(String[] hosts) {
599    return addAxisCamera("Axis Camera", hosts);
600  }
601
602  /**
603   * Adds an Axis IP camera.
604   *
605   * @param name The name to give the camera
606   * @param host Camera host IP or DNS name (e.g. "10.x.y.11")
607   */
608  public AxisCamera addAxisCamera(String name, String host) {
609    AxisCamera camera = new AxisCamera(name, host);
610    // Create a passthrough MJPEG server for USB access
611    startAutomaticCapture(camera);
612    return camera;
613  }
614
615  /**
616   * Adds an Axis IP camera.
617   *
618   * @param name The name to give the camera
619   * @param hosts Array of Camera host IPs/DNS names
620   */
621  public AxisCamera addAxisCamera(String name, String[] hosts) {
622    AxisCamera camera = new AxisCamera(name, hosts);
623    // Create a passthrough MJPEG server for USB access
624    startAutomaticCapture(camera);
625    return camera;
626  }
627
628  /**
629   * Get OpenCV access to the primary camera feed.  This allows you to
630   * get images from the camera for image processing on the roboRIO.
631   *
632   * <p>This is only valid to call after a camera feed has been added
633   * with startAutomaticCapture() or addServer().
634   */
635  public CvSink getVideo() {
636    VideoSource source;
637    synchronized (this) {
638      if (m_primarySourceName == null) {
639        throw new VideoException("no camera available");
640      }
641      source = m_sources.get(m_primarySourceName);
642    }
643    if (source == null) {
644      throw new VideoException("no camera available");
645    }
646    return getVideo(source);
647  }
648
649  /**
650   * Get OpenCV access to the specified camera.  This allows you to get
651   * images from the camera for image processing on the roboRIO.
652   *
653   * @param camera Camera (e.g. as returned by startAutomaticCapture).
654   */
655  public CvSink getVideo(VideoSource camera) {
656    String name = "opencv_" + camera.getName();
657
658    synchronized (this) {
659      VideoSink sink = m_sinks.get(name);
660      if (sink != null) {
661        VideoSink.Kind kind = sink.getKind();
662        if (kind != VideoSink.Kind.kCv) {
663          throw new VideoException("expected OpenCV sink, but got " + kind);
664        }
665        return (CvSink) sink;
666      }
667    }
668
669    CvSink newsink = new CvSink(name);
670    newsink.setSource(camera);
671    addServer(newsink);
672    return newsink;
673  }
674
675  /**
676   * Get OpenCV access to the specified camera.  This allows you to get
677   * images from the camera for image processing on the roboRIO.
678   *
679   * @param name Camera name
680   */
681  public CvSink getVideo(String name) {
682    VideoSource source;
683    synchronized (this) {
684      source = m_sources.get(name);
685      if (source == null) {
686        throw new VideoException("could not find camera " + name);
687      }
688    }
689    return getVideo(source);
690  }
691
692  /**
693   * Create a MJPEG stream with OpenCV input. This can be called to pass custom
694   * annotated images to the dashboard.
695   *
696   * @param name Name to give the stream
697   * @param width Width of the image being sent
698   * @param height Height of the image being sent
699   */
700  public CvSource putVideo(String name, int width, int height) {
701    CvSource source = new CvSource(name, VideoMode.PixelFormat.kMJPEG, width, height, 30);
702    startAutomaticCapture(source);
703    return source;
704  }
705
706  /**
707   * Adds a MJPEG server at the next available port.
708   *
709   * @param name Server name
710   */
711  public MjpegServer addServer(String name) {
712    int port;
713    synchronized (this) {
714      port = m_nextPort;
715      m_nextPort++;
716    }
717    return addServer(name, port);
718  }
719
720  /**
721   * Adds a MJPEG server.
722   *
723   * @param name Server name
724   */
725  public MjpegServer addServer(String name, int port) {
726    MjpegServer server = new MjpegServer(name, port);
727    addServer(server);
728    return server;
729  }
730
731  /**
732   * Adds an already created server.
733   *
734   * @param server Server
735   */
736  public void addServer(VideoSink server) {
737    synchronized (this) {
738      m_sinks.put(server.getName(), server);
739    }
740  }
741
742  /**
743   * Removes a server by name.
744   *
745   * @param name Server name
746   */
747  public void removeServer(String name) {
748    synchronized (this) {
749      m_sinks.remove(name);
750    }
751  }
752
753  /**
754   * Get server for the primary camera feed.
755   *
756   * <p>This is only valid to call after a camera feed has been added
757   * with startAutomaticCapture() or addServer().
758   */
759  public VideoSink getServer() {
760    synchronized (this) {
761      if (m_primarySourceName == null) {
762        throw new VideoException("no camera available");
763      }
764      return getServer("serve_" + m_primarySourceName);
765    }
766  }
767
768  /**
769   * Gets a server by name.
770   *
771   * @param name Server name
772   */
773  public VideoSink getServer(String name) {
774    synchronized (this) {
775      return m_sinks.get(name);
776    }
777  }
778
779  /**
780   * Adds an already created camera.
781   *
782   * @param camera Camera
783   */
784  public void addCamera(VideoSource camera) {
785    String name = camera.getName();
786    synchronized (this) {
787      if (m_primarySourceName == null) {
788        m_primarySourceName = name;
789      }
790      m_sources.put(name, camera);
791    }
792  }
793
794  /**
795   * Removes a camera by name.
796   *
797   * @param name Camera name
798   */
799  public void removeCamera(String name) {
800    synchronized (this) {
801      m_sources.remove(name);
802    }
803  }
804
805  /**
806   * Sets the size of the image to use. Use the public kSize constants to set the correct mode, or
807   * set it directly on a camera and call the appropriate startAutomaticCapture method.
808   *
809   * @deprecated Use setResolution on the UsbCamera returned by startAutomaticCapture() instead.
810   * @param size The size to use
811   */
812  @Deprecated
813  public void setSize(int size) {
814    VideoSource source = null;
815    synchronized (this) {
816      if (m_primarySourceName == null) {
817        return;
818      }
819      source = m_sources.get(m_primarySourceName);
820      if (source == null) {
821        return;
822      }
823    }
824    switch (size) {
825      case kSize640x480:
826        source.setResolution(640, 480);
827        break;
828      case kSize320x240:
829        source.setResolution(320, 240);
830        break;
831      case kSize160x120:
832        source.setResolution(160, 120);
833        break;
834      default:
835        throw new IllegalArgumentException("Unsupported size: " + size);
836    }
837  }
838}