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