UNPKG

onvif-nvt-ts

Version:

Wrapper for ONVIF spec to control NVT (Network Video Transitter) devices. Forked and added TypeScript support from onvif-nvt.

633 lines (557 loc) 20.5 kB
import URL from 'url-parse' import Analytics from './modules/analytics' import Core from './modules/core' import Events from './modules/events' import Media from './modules/media' import Media2 from './modules/media2' import Ptz from './modules/ptz' import * as Util from './utils/util' const MODULE_MAP = { analytics: './modules/analytics', core: './modules/core', events: './modules/events', media: './modules/media', media2: './modules/media2', ptz: './modules/ptz' } const MODULE_MAP_AFTER = { core: function (this: Camera) { this.core.init(this.serviceAddress, this.username, this.password) }, media: function (this: Camera) { this.media.init(this.timeDiff, this.serviceAddress, this.username, this.password) } // snapshot: function (this: Camera) { // 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). */ export default class Camera { core?: Core analytics?: Analytics events?: Events media?: Media // Onvif 1.x media2?: Media2 // Onvif 2.x ptz?: Ptz rootPath = null serviceAddress: URL = null timeDiff = 0 address = null port = null username = null password = null deviceInformation = null profileList = [] defaultProfile = null /** * Add a module to Camera. The available modules are: * <ul> * <li>analytics - automatically added based on capabilities</li> * <li>core - automatically added</li> * <li>events - 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> * </ul> * @param {string} name The name of the module. */ async add(name: string) { 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 { default: Inst } = await import(mod) // console.log(`Module ${name} instance:`, Inst) // return process.exit(0) const after = MODULE_MAP_AFTER[name] || (() => {}) this[name] = new Inst() // console.log({ Inst }) // console.log({ [name]: this[name] }) // process.exit(0) after.call(this) } /** * Connect to the specified camera. * @param address The camera's address * @param port Optional port (80 used if this is null) * @param username The username for the account on the camera. This is optional if your camera does not require a username. * @param password The password for the account on the camera. This is optional if your camera does not require a password. * @param servicePath The service path for the camera. If null or 'undefined' the default path according to the ONVIF spec will be used. */ connect(address: string, port?: number, username?: string, password?: string, servicePath?: string) { return new Promise(async (resolve, reject) => { // check for valid address let errMsg = '' if ((errMsg = Util.isInvalidValue(address, 'string'))) { return reject(new Error('The "address" argument for connect is invalid: ' + errMsg)) } // 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 try { await this.add('core') } catch (err) { return reject(err) } try { await this.coreGetSystemDateAndTime() await this.coreGetServices() await this.coreGetCapabilities() await this.coreGetDeviceInformation() await this.mediaGetProfiles() await this.mediaGetStreamURI() await this.mediaGetSnapshotUri() await this.coreGetScopes() } catch (error) { console.error(error) } return resolve(this.getInformation()) }) } /** * Change or remove authentication. * @param username The username for the account on the camera. This is optional if your camera does not require a username. * @param password The password for the account on the camera. This is optional if your camera does not require a password. */ setAuth(username?: string, password?: string) { this.username = username ?? null this.password = password ?? null } /** * Returns the ONVIF device's informaton. Available only after connection. */ getInformation() { const info = this.deviceInformation if (info) { return JSON.parse(JSON.stringify(info)) } 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<void>((resolve, reject) => { this.core .getSystemDateAndTime() .then(results => { this.timeDiff = this.core.getTimeDiff() resolve() }) .catch(error => { console.error(error) reject(error) }) }) } coreGetServices() { return new Promise<void>((resolve, reject) => { this.core .getServices(true) .then(async results => { if (!('data' in results)) { return reject(new Error('No "data" field in core.getServices')) } const response = results.data.GetServicesResponse const services: any[] = 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. try { await Promise.all( services.map(async 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') { await this.add('media') if (this.media) { this.media.init( this.timeDiff, Util.URLWithEnforcedPort(service.XAddr, this.port), this.username, this.password ) this.media.version = service.Version } } else if (namespace === 'http://www.onvif.org/ver10/events/wsdl') { await this.add('events') if (this.events) { this.events.init( this.timeDiff, Util.URLWithEnforcedPort(service.XAddr, this.port), this.username, this.password ) this.events.version = service.Version } } else if (namespace === 'http://www.onvif.org/ver20/ptz/wsdl') { await this.add('ptz') if (this.ptz) { this.ptz.init( this.timeDiff, Util.URLWithEnforcedPort(service.XAddr, this.port), this.username, this.password ) this.ptz.version = service.Version } } else if (namespace === 'http://www.onvif.org/ver20/analytics/wsdl') { await this.add('analytics') if (this.analytics) { this.analytics.init( this.timeDiff, Util.URLWithEnforcedPort(service.XAddr, this.port), this.username, this.password ) this.analytics.version = service.Version } } }) ) resolve() } catch (err) { reject(err) } }) .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: { XAddr: string }) { const xaddrPath = new URL(service.XAddr) if (xaddrPath.hostname === this.serviceAddress.hostname) { // no proxy return } // build new path service.XAddr = this.rootPath + xaddrPath.pathname } coreGetCapabilities() { return new Promise<void>((resolve, reject) => { this.core .getCapabilities() .then(async results => { if (!('data' in results)) { return reject(new Error('No "data" field in core.getCapabilities')) } const c = results.data.GetCapabilitiesResponse.Capabilities if (!c) { return reject(new Error('Failed to initialize the device: No capabilities were found.')) } try { // 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) { await this.add('analytics') if (this.analytics) { const serviceAddress = Util.URLWithEnforcedPort(analytics.XAddr, this.port) 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) { await this.add('events') if (this.events) { const serviceAddress = Util.URLWithEnforcedPort(events.XAddr, this.port) 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 ('Media' in c) { const media = c.Media this.checkForProxy(media) if (media && 'XAddr' in media) { if (!this.media) { await this.add('media') if (this.media) { const serviceAddress = Util.URLWithEnforcedPort(media.XAddr, this.port) 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) { await this.add('ptz') if (this.ptz) { const serviceAddress = Util.URLWithEnforcedPort(ptz.XAddr, this.port) this.ptz.init(this.timeDiff, serviceAddress, this.username, this.password) } } } } resolve() } catch (err) { reject(err) } }) .catch(error => { console.error(error) reject(error) }) }) } coreGetDeviceInformation() { return new Promise<void>((resolve, reject) => { this.core .getDeviceInformation() .then(results => { if (!('data' in results)) { return reject(new Error('No "data" field in core.getDeviceInformation')) } this.deviceInformation = results.data.GetDeviceInformationResponse resolve() }) .catch(error => { console.error(error) reject(error) }) }) } coreGetScopes() { return new Promise<void>((resolve, reject) => { this.core .getScopes() .then(results => { if (!('data' in results)) { return reject(new Error('No "data" field in core.getScopes')) } 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<void>((resolve, reject) => { this.media .getProfiles() .then(results => { if (!('data' in results)) { return reject(new Error('No "data" field in media.getProfiles')) } 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: any | any[]) { 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<void>((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 as 'UDP' | 'RTSP' | 'HTTP', token) .then(results => { if (!('data' in results)) { return reject(new Error('No "data" field in media.getStreamUri')) } 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<void>((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 => { if (!('data' in results)) { return reject(new Error('No "data" field in media.getSnapshotUri')) } try { const 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() }) } }