UNPKG

ts-onvif

Version:

Client to ONVIF devices

944 lines 38.6 kB
"use strict"; /** * Onvif module * @author Andrew D.Laptev <a.d.laptev@gmail.com> * @see https://www.onvif.org/wp-content/uploads/2022/07/ONVIF_Device_Feature_Discovery_Specification_21.12.pdf */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Onvif = void 0; const events_1 = require("events"); const https_1 = __importDefault(require("https")); const http_1 = __importDefault(require("http")); const buffer_1 = require("buffer"); const crypto_1 = __importDefault(require("crypto")); const utils_1 = require("./utils"); const device_1 = __importDefault(require("./device")); const events_2 = __importDefault(require("./events")); const service_1 = require("./service"); class Onvif extends events_1.EventEmitter { /** * Indicates raw xml response from device. * @event rawResponse * @example * ```typescript * onvif.on('rawResponse', (xml) => { console.log('<- response was', xml); }); * ``` */ static RAW_RESPONSE = 'rawResponse'; /** * Indicates raw xml request to device. * @event rawRequest * @example * ```typescript * onvif.on('rawRequest', (xml) => { console.log('-> request was', xml); }); * ``` */ static RAW_REQUEST = 'rawRequest'; /** * Shows body of request * @event */ static REQUEST_BODY = 'requestBody'; /** * Indicates any errors except events errors * @event error * @example * ```typescript * onvif.on('error', console.error); * ``` */ static ERROR = 'error'; /** * Indicates events errors * @event eventsError * @example * ```typescript * onvif.on('eventsError', console.error); * ``` */ static EVENTS_ERROR = 'eventsError'; /** * Indicates any event from Onvif device. * @event event * @example * ```typescript * onvif.on('event', (msg) => { console.log(new Date().toLocaleTimeString(), 'new event', msg); }); * ``` */ static EVENT = 'event'; /** * Indicates any warnings * @event warn * @example * ```typescript * onvif.on('warn', console.warn); * ``` */ static WARN = 'warn'; /** * Indicates successfully connection * @event connect * @example * ```typescript * onvif.on('connect', () => console.log('connected!')); * ``` */ static CONNECT = 'connect'; /** * Core device namespace for device v1.0 methods * @example * ```typescript * const date = await onvif.device.getSystemDateAndTime(); * console.log(date.toLocaleString()); * ``` */ device; /** * Media namespace for media v1.0 methods * @example * ```typescript * const profiles = await onvif.media.getProfiles(); * console.log(profiles); * ``` */ media; /** * Media2 namespace for media2 v1.0 methods * @example * ```typescript * const profiles = await onvif.media2.getProfiles(); * console.log(profiles); * ``` */ media2; /** * PTZ namespace for ptz v1.0 methods * @example * ```typescript * const ptz = await onvif.ptz.getPTZStatus(); * console.log(ptz); * ``` */ ptz; /** * Events namespace for events v1.0 methods * @example * ```typescript * onvif.on('event', (msg) => { console.log('-> request was', xml); }); * ``` */ events; /** * Replay namespace for replay v1.0 methods * @example * ```typescript * const replay = await onvif.replay.getReplayConfiguration(); * console.log(replay); * ``` */ replay; /** * Imaging namespace for imaging v1.0 methods * @example * ```typescript * const imaging = await onvif.imaging.getImagingSettings(); * console.log(imaging); * ``` */ imaging; /** * Recording namespace for recording v1.0 methods * @example * ```typescript * const recording = await onvif.recording.getRecordingConfiguration(); * console.log(recording); * ``` */ recording; /** * DoorControl namespace for doorcontrol v1.0 methods * @example * ```typescript * const doorControl = await onvif.doorControl.getDoorControlConfiguration(); * console.log(doorControl); * ``` */ doorControl; /** * AccessControl namespace for accesscontrol v1.0 methods * @example * ```typescript * const list = await onvif.accessControl.getAccessPointInfoList(); * console.log(list); * ``` */ accessControl; /** * Credential namespace for credential v1.0 methods * @example * ```typescript * const list = await onvif.credential.getCredentialInfoList(); * console.log(list); * ``` */ credential; /** * AccessRules namespace for accessrules v1.0 methods * @example * ```typescript * const list = await onvif.accessRules.getAccessProfileInfoList(); * console.log(list); * ``` */ accessRules; /** * Schedule namespace for schedule v1.0 methods * @example * ```typescript * const list = await onvif.schedule.getScheduleInfoList(); * console.log(list); * ``` */ schedule; /** * Provisioning namespace for provisioning v1.0 methods * @example * ```typescript * const caps = await onvif.provisioning.getServiceCapabilities(); * console.log(caps); * ``` */ provisioning; /** * AdvancedSecurity namespace for advancedsecurity v1.0 methods * @example * ```typescript * const caps = await onvif.advancedSecurity.getServiceCapabilities(); * console.log(caps); * ``` */ advancedSecurity; /** * Thermal namespace for thermal v1.0 methods * @example * ```typescript * const thermal = await onvif.thermal.getConfigurations(); * console.log(thermal); * ``` */ thermal; /** * Analytics namespace for analytics v1.0 methods * @example * ```typescript * const analytics = await onvif.analytics.getAnalyticsConfiguration(); * console.log(analytics); * ``` */ analytics; /** * DeviceIO namespace for deviceio v1.0 methods * @example * ```typescript * const deviceIO = await onvif.deviceIO.getDeviceIOConfiguration(); * console.log(deviceIO); * ``` */ deviceIO; /** * Display namespace for display v1.0 methods * @example * ```typescript * const display = await onvif.display.getDisplayConfiguration(); * console.log(display); * ``` */ display; /** * ActionEngine namespace for actionengine v1.0 methods * @example * ```typescript * const actionEngine = await onvif.actionEngine.getActionEngineConfiguration(); * console.log(actionEngine); * ``` */ actionEngine; /** * Search namespace for search v1.0 methods * @example * ```typescript * const summary = await onvif.search.getRecordingSummary(); * console.log(summary); * ``` */ search; /** * AnalyticsDevice namespace for analytics device v1.0 methods * @example * ```typescript * const controls = await onvif.analyticsDevice.getAnalyticsEngineControls(); * console.log(controls); * ``` */ analyticsDevice; /** * Receiver namespace for receiver v1.0 methods * @example * ```typescript * const receivers = await onvif.receiver.getReceivers(); * console.log(receivers); * ``` */ receiver; /** * Indicates if the device is using secure connection */ useSecure; /** * Secure options for the connection */ secureOptions; /** * Use WS-Security for the connection (this is the default and adds security headers in the SOAP messages) */ useWSSecurity; /** * Nonce for the connection */ nc = 0; /** * Hostname of the ONVIF device */ hostname; /** * Username for the connection */ username; /** * Password for the connection */ password; /** * Port for the connection */ port; /** * Path for the connection */ path; timeout; agent; preserveAddress = false; uri; timeShift; capabilities; defaultProfiles = []; defaultProfile; activeSources = []; activeSource; urn; deviceInformation; constructor(options) { super(); this.useSecure = options.useSecure ?? false; this.secureOptions = options.secureOptions ?? {}; this.useWSSecurity = options.useWSSecurity ?? true; this.hostname = options.hostname; this.username = options.username; this.password = options.password; this.port = options.port ?? (options.useSecure ? 443 : 80); this.path = options.path ?? '/onvif/device_service'; this.timeout = options.timeout || 120000; this.urn = options.urn; const httpLibrary = this.useSecure ? https_1.default : http_1.default; this.agent = options.agent ?? new httpLibrary.Agent({ keepAlive: true, keepAliveMsecs: 10000 }); this.preserveAddress = options.preserveAddress ?? false; this.uri = {}; this.capabilities = {}; this.device = new device_1.default(this); // mandatory module for startup this.media = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./media')))); // mandatory? TODO think about connect() method this.media2 = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./media2')))); this.ptz = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./ptz')))); this.events = new events_2.default(this); // mandatory module for events this.replay = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./replay')))); this.imaging = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./imaging')))); this.recording = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./recording')))); this.doorControl = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./doorcontrol')))); this.accessControl = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./accesscontrol')))); this.credential = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./credential')))); this.accessRules = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./accessrules')))); this.schedule = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./schedule')))); this.provisioning = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./provisioning')))); this.advancedSecurity = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./advancedsecurity')))); this.thermal = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./thermal')))); this.analytics = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./analytics')))); this.deviceIO = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./deviceio')))); this.display = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./display')))); this.actionEngine = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./actionengine')))); this.search = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./search')))); this.analyticsDevice = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./analyticsdevice')))); this.receiver = (0, service_1.lazyService)(this, () => Promise.resolve().then(() => __importStar(require('./receiver')))); /** Bind event handling to the `event` event */ this.on('newListener', (name) => { // if this is the first listener, start pulling subscription if (name === 'event' && this.listeners(name).length === 0) { this.events.globalSubscription.subscribe().catch((error) => this.emit('error', error)); } }); this.on('removeListener', (name) => { if (name === 'event' && this.listeners(name).length === 0) { this.events.globalSubscription.unsubscribe().catch((error) => this.emit('error', error)); } }); if (options.autoConnect) { setImmediate(() => { this.connect().catch((error) => this.emit('error', error)); }); } } envelopeBody(body) { return { $: { 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd': 'http://www.w3.org/2001/XMLSchema', }, ...body, }; } /** * Envelope header for all SOAP messages * @param options * @private */ envelopeHeader(options) { const pd = this.useWSSecurity && this.username && this.password ? this.passwordDigest() : null; return { ...(pd && { Security: { $: { 's:mustUnderstand': '1', xmlns: 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd', }, UsernameToken: { Username: this.username, Password: { $: { Type: 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest', }, _: pd.passDigest, }, Nonce: { $: { EncodingType: 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary', }, _: pd.nonce, }, Created: { $: { xmlns: 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', }, _: pd.timestamp, }, }, }, ...options?.soapHeaders, }), }; } passwordDigest() { const timestamp = new Date(process.uptime() * 1000 + (this.timeShift || 0)).toISOString(); const nonce = buffer_1.Buffer.allocUnsafe(16); nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 0, 4); nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 4, 4); nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 8, 4); nonce.writeUIntLE(Math.ceil(Math.random() * 0x100000000), 12, 4); const cryptoDigest = crypto_1.default.createHash('sha1'); cryptoDigest.update(buffer_1.Buffer.concat([nonce, buffer_1.Buffer.from(timestamp, 'ascii'), buffer_1.Buffer.from(this.password, 'ascii')])); const passDigest = cryptoDigest.digest('base64'); return { passDigest, nonce: nonce.toString('base64'), timestamp, }; } async rawRequest(options) { return new Promise((resolve, reject) => { let alreadyReturned = false; const requestOptions = { ...options, ...(options.url ? { // if we have url property for pull-point event requests hostname: options.url.hostname, path: options.url.pathname, port: options.url.port, } : { // try to guess the right path for the requested service hostname: this.hostname, path: options.service ? this.uri[options.service] ? this.uri[options.service].pathname : this.path : this.path, port: this.port, }), agent: options.agent ?? this.agent, // Supports things like https://www.npmjs.com/package/proxy-agent which provide SOCKS5 and other connections} timeout: this.timeout, }; requestOptions.headers = { ...options.headers, 'Content-Type': 'application/soap+xml', 'Content-Length': buffer_1.Buffer.byteLength(options.body, 'utf8').toString(), charset: 'utf-8', }; requestOptions.method = 'POST'; const httpLibrary = this.useSecure ? https_1.default : http_1.default; if (this.useSecure) { Object.assign(requestOptions, this.secureOptions); } const request = httpLibrary.request(requestOptions, async (response) => { if (response.statusCode === 401) { // Avoid racing with request 'error' from response.destroy() below if (alreadyReturned) { return undefined; } // Digest credentials were already sent and rejected — do not retry forever if (options.headers?.authorization || options.headers?.Authorization) { alreadyReturned = true; response.destroy(); return reject(new Error('Digest authentication failed')); } const digestHeadersArray = (0, utils_1.getDigestHeaders)(response.rawHeaders); if (digestHeadersArray.length > 0) { // Re-request with the digest auth header alreadyReturned = true; response.destroy(); try { options.headers = { ...options.headers, authorization: this.digestAuth(digestHeadersArray, requestOptions), }; const digestResponse = await this.rawRequest(options); return resolve(digestResponse); } catch (e) { return reject(e); } } else { alreadyReturned = true; response.destroy(); return reject(new Error(`Digest authentication headers not found in the server response`)); } } const bufs = []; let length = 0; response.on('data', (chunk) => { bufs.push(chunk); length += chunk.length; }); response.on('end', () => { if (alreadyReturned) { return; } alreadyReturned = true; const xml = buffer_1.Buffer.concat(bufs, length).toString('utf8'); this.emit('rawResponse', xml); resolve((0, utils_1.parseSOAPString)(xml, options)); }); return undefined; }); // let handle timeout by itself and produce network error to catch below request.setTimeout(options.timeout ?? this.timeout, () => { const err = new Error('Network timeout'); err.code = 'ETIMEDOUT'; err.syscall = 'connect'; request.destroy(err); }); request.on('error', (error) => { if (alreadyReturned) { return; } alreadyReturned = true; /* address, port number or IPCam error */ if (error.code === 'ECONNREFUSED' && error.syscall === 'connect') { reject(error); /* network error */ } else if (error.code === 'ECONNRESET' && error.syscall === 'read') { reject(error); } else { reject(error); } }); this.emit('rawRequest', options.body, requestOptions); request.write(options.body); request.end(); }); } digestAuth(digestHeadersArray, requestOptions) { // Process each item in the wwwAuthenticateArray // Most cameras have only 1 item. // HikVision implementing the new MD5-then-SHA256 have two items let bestResult; let bestAlgorithm; for (const digestHeader of digestHeadersArray) { const challenge = this.parseChallenge(digestHeader); // if 'algorithm' is undefined, the Digest RFC says we default to MD5 const algorithm = challenge.algorithm === undefined ? 'MD5' : challenge.algorithm.replace(/-/g, ''); const ha1 = crypto_1.default.createHash(algorithm); ha1.update([this.username, challenge.realm, this.password].join(':')); // Sony SRG-XP1 sends qop="auth,auth-int" it means the Server will accept either "auth" or "auth-int". We select "auth" // We also need to handle spaces in the string e.g.like "auth, auth-int" // So we split the QOP and then see if one item is "auth" using a trim to remove whitespace if (typeof challenge.qop === 'string' && challenge.qop.split(',').some((item) => item.trim() === 'auth')) { challenge.qop = 'auth'; } const ha2 = crypto_1.default.createHash(algorithm); ha2.update([requestOptions.method, requestOptions.path].join(':')); let cnonce; let nc; if (typeof challenge.qop === 'string' && challenge.qop === 'auth') { cnonce = crypto_1.default.randomBytes(4).toString('hex'); nc = this.updateNC(); } // HASH_ALG is usually MD5 but can also be SHA256 // No qop -> Response = HASH_ALG(HA1:nonce:HA2); // With qop -> Response = HASH_ALG(HA1:nonce:nonceCount:cnonce:qop:HA2) const response = crypto_1.default.createHash(algorithm); const responseParams = [ha1.digest('hex'), challenge.nonce]; if (cnonce && nc) { responseParams.push(nc); responseParams.push(cnonce); responseParams.push(challenge.qop); } responseParams.push(ha2.digest('hex')); response.update(responseParams.join(':')); const authParams = { username: `"${this.username}"`, realm: `"${challenge.realm}"`, nonce: `"${challenge.nonce}"`, uri: `"${requestOptions.path}"`, }; // Send back the original algorithm value, if we received one. if ('algorithm' in challenge) { authParams.algorithm = challenge.algorithm; } // RFC says only send qop, nc and cnonce if there was a QOP in the Header // 'qop' and 'nc' do not have quotes around the Values if ('qop' in challenge && nc) { authParams.qop = challenge.qop; // no quotes authParams.nc = nc; // no quotes authParams.cnonce = `"${cnonce}"`; } authParams.response = `"${response.digest('hex')}"`; if (challenge.opaque) { authParams.opaque = `"${challenge.opaque}"`; } // Values that need quotes already include them; qop/nc stay unquoted per RFC const result = `Digest ${Object.entries(authParams) .map(([key, value]) => `${key}=${value}`) .join(',')}`; if (bestResult == null || (algorithm === 'SHA256' && bestAlgorithm === 'MD5')) { // set bestResult or upgrade the bestResult to the stronger algorithm bestResult = result; bestAlgorithm = algorithm; } } return bestResult; } request(options) { options.headers = options.headers ?? {}; const bodyObject = { 's:Envelope': { $: { 'xmlns:s': 'http://www.w3.org/2003/05/soap-envelope', 'xmlns:a': 'http://www.w3.org/2005/08/addressing', }, 's:Header': this.envelopeHeader(options), 's:Body': this.envelopeBody(options.body), }, }; const body = (0, utils_1.build)(bodyObject); this.emit('requestBody', body); return this.rawRequest({ ...options, body, }); } parseChallenge(digest) { const prefix = 'Digest '; const challenge = digest.substring(digest.indexOf(prefix) + prefix.length); const partsArray = (0, utils_1.splitArgs)(challenge); const parts = partsArray.map((part) => part.match(/^\s*?([a-zA-Z0-9]+)="?([^"]*)"?\s*?$/).slice(1)); return Object.fromEntries(parts); } updateNC() { this.nc += 1; if (this.nc > 99999999) { this.nc = 1; } return String(this.nc).padStart(8, '0'); } /** * Parse url with an eye on `preserveAddress` property * @param address * @private */ parseUrl(address) { const parsedAddress = new URL(address); // If host for service and default host differs, also if preserve address property set // we substitute host, hostname and port from settings then rebuild the href using .format if (this.preserveAddress && (this.hostname !== parsedAddress.hostname || this.port.toString() !== parsedAddress.port)) { parsedAddress.hostname = this.hostname; parsedAddress.host = `${this.hostname}:${this.port}`; parsedAddress.port = this.port.toString(); parsedAddress.href = parsedAddress.toString(); } return parsedAddress; } /** * Receive date and time from cam */ async getSystemDateAndTime() { // The ONVIF spec says this should work without a Password as we need to know any difference in the // remote NVT's time relative to our own time clock (called the timeShift) before we can calculate the // correct timestamp in nonce SOAP Authentication header. // But... Panasonic and Digital Barriers both have devices that implement ONVIF that only work with // authenticated getSystemDateAndTime. So for these devices we need to do an authenticated getSystemDateAndTime. // As 'timeShift' is not set, the local clock MUST be set to the correct time AND the NVT/Camera MUST be set // to the correct time if the camera implements Replay Attack Protection (e.g. Axis) const [data, xml] = await this.rawRequest({ // Try the Unauthenticated Request first. Do not use this._envelopeHeader() as we don't have timeShift yet. body: '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' + '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">' + '<GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl"/>' + '</s:Body>' + '</s:Envelope>', }); try { return this.setupSystemDateAndTime(data); } catch (error) { if (xml && xml.toLowerCase().includes('sender not authorized')) { // Try again with a Username and Password const [data] = await this.request({ body: { GetSystemDateAndTime: { $: { xmlns: 'http://www.onvif.org/ver10/device/wsdl' } }, }, }); return this.setupSystemDateAndTime(data); } throw error; } } /** * Receive only date and time from cam (old behaviour, returns only Date object) */ async getOnlySystemDateAndTime() { return (await this.getSystemDateAndTime()).dateTime; } /** * Add time shift to use with ONVIF timestamps * @param data * @private */ setupSystemDateAndTime(data) { const { systemDateAndTime } = (0, utils_1.linerase)(data).getSystemDateAndTimeResponse; // UTCDateTime is mandatory since version 2.0 const UTCDateTime = systemDateAndTime.UTCDateTime || systemDateAndTime.localDateTime; let dateTime; if (UTCDateTime === undefined) { // Seen on a cheap Chinese camera from GWellTimes-IPC. Use the current time. dateTime = new Date(); } else { dateTime = new Date(Date.UTC(UTCDateTime.date.year, UTCDateTime.date.month - 1, UTCDateTime.date.day, UTCDateTime.time.hour, UTCDateTime.time.minute, UTCDateTime.time.second)); } if (!this.timeShift) { this.timeShift = dateTime.getTime() - process.uptime() * 1000; } systemDateAndTime.dateTime = dateTime; return systemDateAndTime; } /** * Set the device system date and time * Supports two possible date and time values: UTCDateTime(ONVIF types) or dateTime(js Date-object, preferred) */ async setSystemDateAndTime(options) { if (!['Manual', 'NTP'].includes(options.dateTimeType)) { throw new Error('DateTimeType should be `Manual` or `NTP`'); } if (options.dateTimeType === 'Manual' && !options.dateTime && !options.UTCDateTime) { throw new Error('`dateTime` or `UTCDateTime` should be defined when the DateTimeType is `Manual`'); } const body = { SetSystemDateAndTime: { $: { xmlns: 'http://www.onvif.org/ver10/device/wsdl' }, DateTimeType: options.dateTimeType, DaylightSavings: options.daylightSavings, ...((options.timezone !== undefined || options.timeZone?.TZ !== undefined) && { TimeZone: { TZ: options.timezone || options.timeZone?.TZ, }, }), ...(options.dateTime !== undefined && options.dateTime instanceof Date ? { UTCDateTime: { Time: { Hour: options.dateTime.getUTCHours(), Minute: options.dateTime.getUTCMinutes(), Second: options.dateTime.getUTCSeconds(), }, Date: { Year: options.dateTime.getUTCFullYear(), Month: options.dateTime.getUTCMonth() + 1, Day: options.dateTime.getUTCDate(), }, }, } : { UTCDateTime: { Time: { Hour: options.UTCDateTime?.time?.hour, Minute: options.UTCDateTime?.time?.minute, Second: options.UTCDateTime?.time?.second, }, Date: { Year: options.UTCDateTime?.date?.year, Month: options.UTCDateTime?.date?.month, Day: options.UTCDateTime?.date?.day, }, }, }), }, }; const [data] = await this.request({ // Try the Unauthenticated Request first. Do not use this._envelopeHeader() as we don't have timeShift yet. body, }); if (data.setSystemDateAndTimeResponse.length !== 0) { throw new Error(`Wrong 'SetSystemDateAndTime' response: '${(0, utils_1.linerase)(data).setSystemDateAndTimeResponse}'`); } // get new system time from device return this.getSystemDateAndTime(); } /** * Check and find out video configuration for device * @private */ async getActiveSources() { if (!this.media.videoSources?.length) { return; } this.media.videoSources.forEach(({ token: videoSrcToken }, idx) => { // let's choose first appropriate profile for our video source and make it default let appropriateProfiles = this.media.profiles.filter((profile) => profile.videoSourceConfiguration?.sourceToken === videoSrcToken && profile.videoEncoderConfiguration !== undefined); // Happytime and some devices return profiles without VideoSourceConfiguration. // Fall back to any profile that has an encoder, then to any profile at all. if (appropriateProfiles.length === 0) { appropriateProfiles = this.media.profiles.filter((profile) => profile.videoEncoderConfiguration !== undefined); } if (appropriateProfiles.length === 0) { appropriateProfiles = [...this.media.profiles]; } if (appropriateProfiles.length === 0) { if (idx === 0) { this.emit(Onvif.WARN, new Error('Unrecognized configuration: no media profiles available for video sources')); } return; } if (idx === 0) { [this.defaultProfile] = appropriateProfiles; } [this.defaultProfiles[idx]] = appropriateProfiles; this.activeSources[idx] = { sourceToken: videoSrcToken, profileToken: this.defaultProfiles[idx].token, videoSourceConfigurationToken: this.defaultProfiles[idx].videoSourceConfiguration?.token ?? videoSrcToken, videoSourceToken: videoSrcToken, }; if (this.defaultProfiles[idx].videoEncoderConfiguration) { const configuration = this.defaultProfiles[idx].videoEncoderConfiguration; this.activeSources[idx].encoding = configuration?.encoding; this.activeSources[idx].width = configuration?.resolution?.width; this.activeSources[idx].height = configuration?.resolution?.height; this.activeSources[idx].fps = configuration?.rateControl?.frameRateLimit; this.activeSources[idx].bitrate = configuration?.rateControl?.bitrateLimit; } if (idx === 0) { this.activeSource = this.activeSources[idx]; } if (this.defaultProfiles[idx].PTZConfiguration) { this.activeSources[idx].ptz = { name: this.defaultProfiles[idx].PTZConfiguration.name, token: this.defaultProfiles[idx].PTZConfiguration.token, }; /* TODO Think about it if (idx === 0) { this.defaultProfile.PTZConfiguration = this.activeSources[idx].PTZConfiguration; } */ } }); } /** * Connect to the camera and fill device information properties */ async connect() { await this.getSystemDateAndTime(); // Try to get services (new approach). If not, get capabilities try { await this.device.getServices(); } catch (error) { await this.device.getCapabilities(); } await Promise.all([this.media.getProfiles(), this.media.getVideoSources()]); await this.getActiveSources(); this.emit('connect'); return this; } } exports.Onvif = Onvif; //# sourceMappingURL=onvif.js.map