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}