ts-onvif
Version:
Client to ONVIF devices
944 lines • 38.6 kB
JavaScript
;
/**
* 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