Source: camera.js

const Util = require('./utils/util')
const URL = require('url-parse')

const MODULE_MAP = {
  access: './modules/access',
  accessrules: './modules/accessrules',
  action: './modules/action',
  analytics: './modules/analytics',
  core: './modules/core',
  credential: './modules/credential',
  deviceio: './modules/deviceio',
  display: './modules/display',
  door: './modules/door',
  events: './modules/events',
  imaging: './modules/imaging',
  media: './modules/media',
  media2: './modules/media2',
  ptz: './modules/ptz',
  receiver: './modules/receiver',
  recording: './modules/recording',
  replay: './modules/replay',
  schedule: './modules/schedule',
  search: './modules/search',
  security: './modules/security',
  snapshot: './utils/snapshot',
  thermal: './modules/thermal',
  videoanalytics: './modules/videoanalytics'
}

const MODULE_MAP_AFTER = {
  core: function () {
    this.core.init(this.serviceAddress, this.username, this.password)
  },
  media: function () {
    this.media.init(this.timeDiff, this.serviceAddress, this.username, this.password)
  },
  snapshot: function () {
    const defaultProfile = this.getDefaultProfile()
    if (defaultProfile) {
      const snapshotUri = defaultProfile.SnapshotUri.Uri
      this.snapshot.init(snapshotUri, this.username, this.password)
    }
  }
}

/**
 * Wrapper class for all onvif modules to manage an Onvif device (camera).
 */
class Camera {
  constructor () {
    this.core = null

    this.access = null
    this.accessrules = null
    this.action = null
    this.analytics = null
    this.credential = null
    this.deviceio = null
    this.display = null
    this.door = null
    this.events = null
    this.imaging = null
    this.media = null // Onvif 1.x
    this.media2 = null // Onvif 2.x
    this.ptz = null
    this.receiver = null
    this.recording = null
    this.replay = null
    this.schedule = null
    this.search = null
    this.security = null
    this.snapshot = null
    this.thermal = null
    this.videoanalytics = null

    this.rootPath = null
    this.serviceAddress = null
    this.timeDiff = 0
    this.address = null
    this.port = null
    this.username = null
    this.password = null

    this.deviceInformation = null
    this.profileList = []
    this.defaultProfile = null
  }

  /**
   * Add a module to Camera. The available modules are:
   * <ul>
   * <li>access</li>
   * <li>accessrules</li>
   * <li>action</li>
   * <li>analytics - automatically added based on capabilities</li>
   * <li>core - automatically added</li>
   * <li>credential</li>
   * <li>deviceio</li>
   * <li>display</li>
   * <li>door</li>
   * <li>events - automatically added based on capabilities</li>
   * <li>imaging - automatically added based on capabilities</li>
   * <li>media - automatically added based on capabilities</li>
   * <li>media2</li>
   * <li>ptz - automatically added based on capabilities</li>
   * <li>receiver</li>
   * <li>recording</li>
   * <li>replay</li>
   * <li>schedule</li>
   * <li>search</li>
   * <li>security</li>
   * <li>snapshot</li>
   * <li>thermal</li>
   * <li>videoanalytics</li>
   * </ul>
   * @param {string} name The name of the module.
   */
  add (name) {
    const mod = MODULE_MAP[name]
    if (!MODULE_MAP[name]) {
      throw new Error(`Module '${name}' does not exist. Cannot add to Camera.`)
    }
    if (this[name]) {
      return
    }
    const Inst = require(mod)
    const after = MODULE_MAP_AFTER[name] || (() => {})

    this[name] = new Inst()
    after.call(this)
  }

  /**
   * Connect with the specified camera
   * @param {string} address The camera's address
   * @param {integer=} port Optional port (80 used if this is null)
   * @param {string=} username The username for the account on the camera. This is optional if your camera does not require a username.
   * @param {string=} password The password for the account on the camera. This is optional if your camera does not require a password.
   * @param {string=} servicePath The service path for the camera. If null or 'undefined' the default path according to the ONVIF spec will be used.
   * @param {callback=} callback Optional callback, instead of a Promise.
   */
  connect (address, port, username, password, servicePath, callback) {
    return new Promise((resolve, reject) => {
      // check for valid address
      let errMsg = ''
      if ((errMsg = Util.isInvalidValue(address, 'string'))) {
        reject(new Error('The "address" argument for connect is invalid: ' + errMsg))
        return
      }

      // provide defaults if not provided
      port = port || 80
      username = username || null
      password = password || null
      servicePath = servicePath || '/onvif/device_service'

      this.address = address
      this.port = port

      this.setAuth(username, password)

      // set up the service address
      let serviceAddress = 'http://' + address
      if (port && port !== 80) {
        serviceAddress = serviceAddress + ':' + port
      }
      this.rootPath = serviceAddress
      serviceAddress = serviceAddress + servicePath

      this.serviceAddress = new URL(serviceAddress)

      // add core module
      this.add('core')

      return this.coreGetSystemDateAndTime()
        .then(() => {
          return this.coreGetServices()
        })
        .then(() => {
          return this.coreGetCapabilities()
        })
        .then(() => {
          return this.coreGetDeviceInformation()
        })
        .then(() => {
          return this.mediaGetProfiles()
        })
        .then(() => {
          return this.mediaGetStreamURI()
        })
        .then(() => {
          return this.mediaGetSnapshotUri()
        })
        .then(() => {
          return this.coreGetScopes()
        })
        .then(() => {
          const info = this.getInformation()
          resolve(info)
        })
        .catch(error => {
          reject(error)
        })
    })
  }

  /**
   * Used to change or remove the auth information for the camera.
   * @param {string=} username The username for the account on the camera. This is optional if your camera does not require a username.
   * @param {string=} password The password for the account on the camera. This is optional if your camera does not require a password.
   */
  setAuth (username, password) {
    if (typeof username === 'undefined') {
      this.username = null
    }
    else {
      this.username = username
    }
    if (typeof password === 'undefined') {
      this.password = null
    }
    else {
      this.password = password
    }
  }

  /**
   * Returns the ONVIF device's informaton. Available after connection.
  */
  getInformation () {
    const o = this.deviceInformation
    if (o) {
      return JSON.parse(JSON.stringify(o))
    }
    else {
      return null
    }
  }

  /**
   * Returns the default profile that will be used when one is not supplied to functions that require it. Available after connection.
  */
  getDefaultProfile () {
    return this.defaultProfile
  }

  coreGetSystemDateAndTime () {
    return new Promise((resolve, reject) => {
      this.core.getSystemDateAndTime()
        .then(results => {
          this.timeDiff = this.core.getTimeDiff()
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  coreGetServices () {
    return new Promise((resolve, reject) => {
      this.core.getServices(true)
        .then(results => {
          const response = results.data.GetServicesResponse
          const services = response.Service

          // the appropriate modules will be automatically added
          // to camera based on the onvif device's services.
          // if GetServics is not supported, the GetCapabilities
          // fallback will be used.
          services.forEach(service => {
            this.checkForProxy(service)
            const namespace = service.Namespace
            if (namespace === 'http://www.onvif.org/ver10/device/wsdl') {
              this.core.version = service.Version
            }
            else if (namespace === 'http://www.onvif.org/ver10/media/wsdl') {
              this.add('media')
              if (this.media) {
                this.media.init(this.timeDiff, new URL(service.XAddr), this.username, this.password)
                this.media.version = service.Version
              }
            }
            else if (namespace === 'http://www.onvif.org/ver10/events/wsdl') {
              this.add('events')
              if (this.events) {
                this.events.init(this.timeDiff, new URL(service.XAddr), this.username, this.password)
                this.events.version = service.Version
              }
            }
            else if (namespace === 'http://www.onvif.org/ver20/ptz/wsdl') {
              this.add('ptz')
              if (this.ptz) {
                this.ptz.init(this.timeDiff, new URL(service.XAddr), this.username, this.password)
                this.ptz.version = service.Version
              }
            }
            else if (namespace === 'http://www.onvif.org/ver20/imaging/wsdl') {
              this.add('imaging')
              if (this.imaging) {
                this.imaging.init(this.timeDiff, new URL(service.XAddr), this.username, this.password)
                this.imaging.version = service.Version
              }
            }
            else if (namespace === 'http://www.onvif.org/ver10/deviceIO/wsdl') {
              this.add('deviceio')
              if (this.deviceio) {
                this.deviceio.init(this.timeDiff, new URL(service.XAddr), this.username, this.password)
                this.deviceio.version = service.Version
              }
            }
            else if (namespace === 'http://www.onvif.org/ver20/analytics/wsdl') {
              this.add('analytics')
              if (this.analytics) {
                this.analytics.init(this.timeDiff, new URL(service.XAddr), this.username, this.password)
                this.analytics.version = service.Version
              }
            }
          })
          resolve()
        })
        .catch(error => {
          console.error(error)
          // don't fail because this isn't supported by the camera
          // spec says to use fallback of GetCapabilities method.
          resolve()
        })
    })
  }

  // make sure the serviceAddress matches
  // if not, then we may be behind a proxy and it needs
  // do be dealt with
  checkForProxy (service) {
    const xaddrPath = new URL(service.XAddr)
    if (xaddrPath.href === this.serviceAddress.href) {
      // no proxy
      return
    }

    // build new path
    service.XAddr = this.rootPath + xaddrPath.pathname + xaddrPath.query
  }

  coreGetCapabilities () {
    return new Promise((resolve, reject) => {
      this.core.getCapabilities()
        .then(results => {
          const c = results.data.GetCapabilitiesResponse.Capabilities
          if (!c) {
            reject(new Error('Failed to initialize the device: No capabilities were found.'))
            return
          }
          // the appropriate modules will be automatically added
          // to camera based on the onvif device's capabilities.
          if ('Analytics' in c) {
            const analytics = c.Analytics
            this.checkForProxy(analytics)
            if (analytics && 'XAddr' in analytics) {
              if (!this.analytics) {
                this.add('analytics')
                if (this.analytics) {
                  const serviceAddress = new URL(analytics.XAddr)
                  this.analytics.init(this.timeDiff, serviceAddress, this.username, this.password)
                }
              }
              if (this.analytics) {
                if ('RuleSupport' in analytics && analytics.RuleSupport === 'true') {
                  this.analytics.ruleSupport = true
                }
                if ('AnalyticsModuleSupport' in analytics && analytics.AnalyticsModuleSupport === 'true') {
                  this.analytics.analyticsModuleSupport = true
                }
              }
            }
          }
          if ('Events' in c) {
            const events = c.Events
            this.checkForProxy(events)
            if (events && 'XAddr' in events) {
              if (!this.events) {
                this.add('events')
                if (this.events) {
                  const serviceAddress = new URL(events.XAddr)
                  this.events.init(this.timeDiff, serviceAddress, this.username, this.password)
                }
              }
              if (this.events && this.analytics) {
                if ('WSPullPointSupport' in events && events.WSPullPointSupport === 'true') {
                  this.analytics.wsPullPointSupport = true
                }
                if ('WSSubscriptionPolicySupport' in events && events.WSSubscriptionPolicySupport === 'true') {
                  this.analytics.wsSubscriptionPolicySupport = true
                }
              }
            }
          }
          if ('Imaging' in c) {
            const imaging = c.Imaging
            this.checkForProxy(imaging)
            if (imaging && 'XAddr' in imaging) {
              if (!this.imaging) {
                this.add('imaging')
                if (this.imaging) {
                  const serviceAddress = new URL(imaging.XAddr)
                  this.imaging.init(this.timeDiff, serviceAddress, this.username, this.password)
                }
              }
            }
          }
          if ('Media' in c) {
            const media = c.Media
            this.checkForProxy(media)
            if (media && 'XAddr' in media) {
              if (!this.media) {
                this.add('media')
                if (this.media) {
                  const serviceAddress = new URL(media.XAddr)
                  this.media.init(this.timeDiff, serviceAddress, this.username, this.password)
                }
              }
            }
          }
          if ('PTZ' in c) {
            const ptz = c.PTZ
            this.checkForProxy(ptz)
            if (ptz && 'XAddr' in ptz) {
              if (!this.ptz) {
                this.add('ptz')
                if (this.ptz) {
                  const serviceAddress = new URL(ptz.XAddr)
                  this.ptz.init(this.timeDiff, serviceAddress, this.username, this.password)
                }
              }
            }
          }
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  coreGetDeviceInformation () {
    return new Promise((resolve, reject) => {
      this.core.getDeviceInformation()
        .then(results => {
          this.deviceInformation = results.data.GetDeviceInformationResponse
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  coreGetScopes () {
    return new Promise((resolve, reject) => {
      this.core.getScopes()
        .then(results => {
          const scopes = typeof results.data.GetScopesResponse.Scopes === 'undefined' || !Array.isArray(results.data.GetScopesResponse.Scopes) ? [] : results.data.GetScopesResponse.Scopes
          this.deviceInformation.Ptz = false
          scopes.forEach((scope) => {
            const s = scope.ScopeItem
            if (s.indexOf('onvif://www.onvif.org/hardware/') === 0) {
              const hardware = s.split('/').pop()
              this.deviceInformation.Hardware = hardware
            }
            else if (s.indexOf('onvif://www.onvif.org/type/Streaming') === 0) {
              this.deviceInformation.Streaming = true
            }
            else if (s.indexOf('onvif://www.onvif.org/type/video_encoder') === 0) {
              this.deviceInformation.VideoEncoder = true
            }
            else if (s.indexOf('onvif://www.onvif.org/type/audio_encoder') === 0) {
              this.deviceInformation.AudiooEncoder = true
            }
            else if (s.indexOf('onvif://www.onvif.org/type/ptz') === 0) {
              this.deviceInformation.Ptz = true
            }
            else if (s.indexOf('onvif://www.onvif.org/Profile/S') === 0) {
              this.deviceInformation.ProfileS = true
            }
            else if (s.indexOf('onvif://www.onvif.org/Profile/C') === 0) {
              this.deviceInformation.ProfileC = true
            }
            else if (s.indexOf('onvif://www.onvif.org/Profile/G') === 0) {
              this.deviceInformation.ProfileG = true
            }
            else if (s.indexOf('onvif://www.onvif.org/Profile/Q') === 0) {
              this.deviceInformation.ProfileQ = true
            }
            else if (s.indexOf('onvif://www.onvif.org/Profile/A') === 0) {
              this.deviceInformation.ProfileA = true
            }
            else if (s.indexOf('onvif://www.onvif.org/Profile/T') === 0) {
              this.deviceInformation.ProfileT = true
            }
            else if (s.indexOf('onvif://www.onvif.org/location/country/') === 0) {
              const country = s.split('/').pop()
              this.deviceInformation.Country = country
            }
            else if (s.indexOf('onvif://www.onvif.org/location/city/') === 0) {
              const city = s.split('/').pop()
              this.deviceInformation.City = city
            }
            else if (s.indexOf('onvif://www.onvif.org/name/') === 0) {
              let name = s.split('/').pop()
              name = name.replace(/_/g, ' ')
              this.deviceInformation.Name = name
            }
          })

          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  mediaGetProfiles () {
    return new Promise((resolve, reject) => {
      this.media.getProfiles()
        .then(results => {
          const profiles = results.data.GetProfilesResponse.Profiles
          if (!profiles) {
            reject(new Error('Failed to initialize the device: The targeted device does not any media profiles.'))
            return
          }
          const profileList = this.parseProfiles(profiles)
          this.profileList = this.profileList.concat(profileList)
          resolve()
        })
        .catch(error => {
          console.error(error)
          reject(error)
        })
    })
  }

  parseProfiles (profiles) {
    const profileList = []

    // When a single profile is given 'profiles' is the single profile
    if (!Array.isArray(profiles)) {
      profiles = [profiles]
    }

    profiles.forEach((profile) => {
      profileList.push(profile)
      if (!this.defaultProfile) {
        this.defaultProfile = profile
        if (this.ptz) {
          this.ptz.setDefaultProfileToken(profile.$.token)
        }
      }
    })

    return profileList
  }

  /**
   * Returns an array of profiles. Available after connection.
   * The profiles will contain media stream URIs and snapshot URIs for each profile.
  */
  getProfiles () {
    return this.profileList
  }

  mediaGetStreamURI () {
    return new Promise((resolve, reject) => {
      const protocols = ['UDP', 'HTTP', 'RTSP']
      let profileIndex = 0
      let protocolIndex = 0
      const getStreamUri = () => {
        const profile = this.profileList[profileIndex]
        if (profile) {
          const protocol = protocols[protocolIndex]
          if (protocol) {
            const token = profile.$.token
            this.media.getStreamUri('RTP-Unicast', protocol, token)
              .then(results => {
                profile.StreamUri = results.data.GetStreamUriResponse.MediaUri
                ++protocolIndex
                getStreamUri()
              })
              .catch(error => {
                console.error(error)
                ++protocolIndex
                getStreamUri()
              })
          }
          else {
            ++profileIndex
            protocolIndex = 0
            getStreamUri()
          }
        }
        else {
          resolve()
        }
      }
      getStreamUri()
    })
  }

  mediaGetSnapshotUri () {
    return new Promise((resolve, reject) => {
      let profileIndex = 0
      const getSnapshotUri = () => {
        const profile = this.profileList[profileIndex]
        if (profile) {
          // this.media.getSnapshotUri(profile['token'])
          this.media.getSnapshotUri(profile.$.token)
            .then(results => {
              try {
                const service = {}
                service.XAddr = results.data.GetSnapshotUriResponse.MediaUri.Uri
                this.checkForProxy(service)
                profile.SnapshotUri = results.data.GetSnapshotUriResponse.MediaUri
                profile.SnapshotUri.Uri = service.XAddr
              }
              catch (e) {}
              ++profileIndex
              getSnapshotUri()
            })
            .catch(error => {
              console.error(error)
              ++profileIndex
              getSnapshotUri()
            })
        }
        else {
          resolve()
        }
      }
      getSnapshotUri()
    })
  }
}

module.exports = Camera