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
text/typescript
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()
})
}
}