package io.evercam.network;
import io.evercam.Vendor;
import io.evercam.network.discovery.Device;
import io.evercam.network.discovery.DiscoveredCamera;
import io.evercam.network.discovery.IpScan;
import io.evercam.network.discovery.MacAddress;
import io.evercam.network.discovery.NatMapEntry;
import io.evercam.network.discovery.NetworkInfo;
import io.evercam.network.discovery.ScanRange;
import io.evercam.network.discovery.ScanResult;
import io.evercam.network.discovery.UpnpDevice;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class EvercamDiscover {
public static final int DEFAULT_FIXED_POOL = 20;
private ArrayList<String> activeIpList = new ArrayList<String>();
private ArrayList<UpnpDevice> deviceList = new ArrayList<UpnpDevice>();// UPnP
// device
// list
private ArrayList<NatMapEntry> mapEntries = new ArrayList<NatMapEntry>();// NAT
// table
private ArrayList<DiscoveredCamera> cameraList = new ArrayList<DiscoveredCamera>();
private ArrayList<DiscoveredCamera> onvifDeviceList = new ArrayList<DiscoveredCamera>();
private ArrayList<Device> nonCameraDeviceList = new ArrayList<Device>();
private boolean upnpDone = false;
private boolean natDone = false;
private int countDone = 0;
private int queryCountDone = 0;
private String externalIp = "";
private boolean withDefaults = false;
public ExecutorService pool;
public static long NAT_TIMEOUT = 5000; // 5 secs
public static long IDENTIFICATION_TIMEOUT = 16000; // 16 secs
public static long QUERY_TIMEOUT = 12000; // 12 secs
/**
* Include camera defaults(username, password, paths, and thumbnail URLs) in
* the scanning result or not
*
* @param withDefaults
* true if include camera defaults
*/
public EvercamDiscover withDefaults(boolean withDefaults) {
this.withDefaults = withDefaults;
return this;
}
/**
* The wrapped method to scan for cameras in Android.
*
* @param scanRange
* the range of IP addresses to scan
* @param routerIp
* gateway/router IP address
* @return a list of discovered camera devices
* @throws Exception
*/
public DiscoveryResult discoverAllLinux(ScanRange scanRange)
throws Exception {
pool = Executors.newFixedThreadPool(DEFAULT_FIXED_POOL);
// Request for external IP address
externalIp = NetworkInfo.getExternalIP();
if (!pool.isShutdown()) {
// ONVIF discovery
pool.execute(onvifRunnable);
printLogMessage("Discovering ONVIF devices......");
// Start UPnP discovery
pool.execute(upnpRunnable);
if (scanRange.getRouterIpString().equals(
NetworkInfo.getLinuxRouterIp())) {
// Start UPnP router discovery
printLogMessage("Discovering UPnP devices......");
pool.execute(new NatRunnable(scanRange.getRouterIpString()) {
@Override
public void onFinished(ArrayList<NatMapEntry> mapEntries) {
printLogMessage("NAT discovery finished.");
if (mapEntries != null) {
EvercamDiscover.this.mapEntries = mapEntries;
}
natDone = true;
}
});
}
printLogMessage("Discovering NAT table......");
}
// Scan to get a list of active IP addresses.
IpScan ipScan = new IpScan(new ScanResult() {
@Override
public void onActiveIp(String ip) {
printLogMessage("Active IP: " + ip);
activeIpList.add(ip);
}
@Override
public void onIpScanned(String ip) {
// TODO Auto-generated method stub
}
});
ipScan.scanAll(scanRange);
long natWaitingTime = 0;
while (!upnpDone || !natDone) {
if (natWaitingTime < NAT_TIMEOUT) {
printLogMessage("Waiting for UPnP & NAT discovery...");
Thread.sleep(2000);
natWaitingTime += 2000;
} else {
printLogMessage("UPnP & NAT discovery timeout.");
break;
}
}
printLogMessage("Identifying cameras......");
// For each active IP, request for MAC address and vendor
for (int index = 0; index < activeIpList.size(); index++) {
if (!pool.isShutdown()) {
pool.execute(new IdentifyCameraRunnable(activeIpList.get(index)) {
@Override
public void onCameraFound(
DiscoveredCamera discoveredCamera, Vendor vendor) {
discoveredCamera.setExternalIp(externalIp);
// Add details discovered from UPnP to camera object
discoveredCamera = mergeUpnpDevicesToCamera(
discoveredCamera, deviceList);
// Add details in discovered NAT table(mainly
// forwarded ports)
discoveredCamera = mergeNatTableToCamera(
discoveredCamera, mapEntries);
synchronized (cameraList) {
cameraList.add(discoveredCamera);
}
}
@Override
public void onFinished() {
countDone++;
}
@Override
public void onNonCameraDeviceFound(Device device) {
device.setExternalIp(externalIp);
synchronized (nonCameraDeviceList) {
nonCameraDeviceList.add(device);
}
}
});
}
}
long identificationWaitingTime = 0;
while (countDone != activeIpList.size()) {
if (identificationWaitingTime < IDENTIFICATION_TIMEOUT) {
printLogMessage("Identifying cameras..." + countDone + '/'
+ activeIpList.size());
Thread.sleep(4000);
identificationWaitingTime += 4000;
} else {
printLogMessage("Camera identification timeout.");
break;
}
}
discardOnvifDeviceIfNotInScanRange(scanRange);
// Merge ONVIF devices to discovered camera list
mergeOnvifDeviceListToCameraList();
if (!pool.isShutdown()) {
for (DiscoveredCamera discoveredCamera : cameraList) {
pool.execute(new EvercamQueryRunnable(discoveredCamera) {
@Override
public void onFinished() {
queryCountDone++;
}
}.withDefaults(withDefaults));
}
}
long queryWaitingTime = 0;
while (queryCountDone != cameraList.size()) {
if (queryWaitingTime < QUERY_TIMEOUT) {
printLogMessage("Retrieving camera defaults..."
+ queryCountDone + '/' + cameraList.size());
Thread.sleep(4000);
queryWaitingTime += 4000;
} else {
printLogMessage("Evercam query timeout.");
break;
}
}
pool.shutdown();
try {
if (!pool.awaitTermination(3600, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
mergeDuplicateCameraFromList(cameraList);
// Query ARP table again if MAC address is still empty after merging
fillMacAddressIfNotExist(cameraList);
return new DiscoveryResult(cameraList, nonCameraDeviceList);
}
public static DiscoveredCamera mergeSingleUpnpDeviceToCamera(
UpnpDevice upnpDevice, DiscoveredCamera discoveredCamera) {
int port = upnpDevice.getPort();
String model = upnpDevice.getModel();
if (port > 0) {
discoveredCamera.setHttp(port);
}
discoveredCamera.setName(upnpDevice.getFriendlyName());
discoveredCamera.setModel(model);
return discoveredCamera;
}
public static DiscoveredCamera mergeUpnpDevicesToCamera(
DiscoveredCamera camera, ArrayList<UpnpDevice> upnpDeviceList) {
try {
if (upnpDeviceList.size() > 0) {
for (UpnpDevice upnpDevice : upnpDeviceList) {
// If IP address matches
String ipFromUpnp = upnpDevice.getIp();
if (ipFromUpnp != null && !ipFromUpnp.isEmpty()) {
if (camera.getIP().equals(ipFromUpnp)) {
mergeSingleUpnpDeviceToCamera(upnpDevice, camera);
break;
}
}
}
}
} catch (Exception e) {
printLogMessage("Exception while merging UPnP device: "
+ e.getStackTrace().toString());
}
return camera;
}
public static DiscoveredCamera mergeNatEntryToCamera(
DiscoveredCamera camera, NatMapEntry mapEntry) {
int natInternalPort = mapEntry.getInternalPort();
int natExternalPort = mapEntry.getExternalPort();
if (camera.getHttp() == natInternalPort) {
camera.setExthttp(natExternalPort);
}
if (camera.getRtsp() == natInternalPort) {
camera.setExtrtsp(natExternalPort);
}
return camera;
}
public static DiscoveredCamera mergeNatTableToCamera(
DiscoveredCamera camera, ArrayList<NatMapEntry> mapEntries) {
if (mapEntries != null && mapEntries.size() > 0) {
for (NatMapEntry mapEntry : mapEntries) {
String natIp = mapEntry.getIpAddress();
if (camera.getIP().equals(natIp)) {
mergeNatEntryToCamera(camera, mapEntry);
}
}
}
return camera;
}
/**
* 1. Review the camera list and merge cameras with the same IP address 2.
* Review the camera list and if any of them has duplicate MAC address but
* are actually the same device, then discard one of them and add a note.
*
* @param the
* re-organized camera list
*/
public static void mergeDuplicateCameraFromList(
ArrayList<DiscoveredCamera> cameraList) {
boolean duplicate = false;
do {
duplicate = false;
int listSize = cameraList.size();
outsideLoop: for (int index1 = 0; index1 < listSize; index1++) {
DiscoveredCamera camera1 = cameraList.get(index1);
String ip1 = camera1.getIP();
String mac1 = camera1.getMAC();
for (int index2 = index1 + 1; index2 < listSize; index2++) {
DiscoveredCamera camera2 = cameraList.get(index2);
String ip2 = camera2.getIP();
String mac2 = camera2.getMAC();
if (ip1.equals(ip2)) {
duplicate = true;
// Merge camera object on the original list
camera1.merge(camera2);
// Remove camera from the original list
cameraList.remove(index2);
break outsideLoop;
}
/**
* If the two cameras has different IP but have the same MAC
* address,
*
*/
else if (!mac1.isEmpty() && !mac2.isEmpty()
&& mac1.equals(mac2)
&& camera1.isduplicateWith(camera2)) {
duplicate = true;
camera1.setNotes("Duplicate MAC address with another IP address: "
+ ip2);
cameraList.remove(camera2);
break outsideLoop;
}
}
}
} while (duplicate);
}
/**
* If MAC address doesn't exist in camera object, query ARP table again
*/
public static void fillMacAddressIfNotExist(
ArrayList<DiscoveredCamera> cameraList) {
for (DiscoveredCamera camera : cameraList) {
if (!camera.hasMac()) {
camera.setMAC(MacAddress.getByIpLinux(camera.getIP()));
}
}
}
private OnvifRunnable onvifRunnable = new OnvifRunnable() {
@Override
public void onFinished() {
printLogMessage("ONVIF discovery finished.");
}
@Override
public void onDeviceFound(DiscoveredCamera discoveredCamera) {
printLogMessage("Found ONVIF device: " + discoveredCamera.getIP());
discoveredCamera.setExternalIp(externalIp);
onvifDeviceList.add(discoveredCamera);
}
};
private void discardOnvifDeviceIfNotInScanRange(ScanRange scanRange) {
@SuppressWarnings("unchecked")
ArrayList<DiscoveredCamera> clonedList = (ArrayList<DiscoveredCamera>) onvifDeviceList
.clone();
if (onvifDeviceList.size() > 0) {
for (DiscoveredCamera discoveredCamera : onvifDeviceList) {
try {
if (!scanRange.containIp(discoveredCamera.getIP())) {
EvercamDiscover
.printLogMessage("Removing ONVIF device: "
+ discoveredCamera.getIP());
clonedList.remove(discoveredCamera);
}
} catch (Exception e) {
if (Constants.ENABLE_LOGGING) {
e.printStackTrace();
}
}
}
onvifDeviceList = clonedList;
}
}
private void mergeOnvifDeviceListToCameraList() {
if (onvifDeviceList.size() > 0) {
for (DiscoveredCamera onvifCamera : onvifDeviceList) {
boolean matched = false;
if (cameraList.size() > 0) {
for (DiscoveredCamera discoveredCamera : cameraList) {
if (discoveredCamera.getIP()
.equals(onvifCamera.getIP())) {
matched = true;
if (onvifCamera.hasModel()) {
discoveredCamera.setModel(onvifCamera
.getModel());
discoveredCamera.setHttp(onvifCamera.getHttp());
}
break;
}
}
}
if (!matched) {
cameraList.add(onvifCamera);
}
}
}
}
private UpnpRunnable upnpRunnable = new UpnpRunnable() {
@Override
public void onFinished(ArrayList<UpnpDevice> upnpDeviceList) {
printLogMessage("UPnP discovery finished.");
if (upnpDeviceList != null) {
deviceList = upnpDeviceList;
}
upnpDone = true;
}
@Override
public void onDeviceFound(UpnpDevice upnpDevice) {
printLogMessage("Found UPnP device: " + upnpDevice.getIp());
}
};
/**
* Only print the logging message when logging is enabled
*
* @param message
* The logging message to be printed in console
*/
public static void printLogMessage(String message) {
if (Constants.ENABLE_LOGGING) {
System.out.println(message);
}
}
}