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}