camstreamerlib 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/CameraVapix.js +340 -230
  2. package/package.json +1 -1
package/CameraVapix.js CHANGED
@@ -5,259 +5,369 @@ const prettifyXml = require('prettify-xml')
5
5
  const RtspClient = require('./RtspClient');
6
6
  const httpRequest = require('./HTTPRequest');
7
7
 
8
+ const WebSocket = require('ws');
9
+ const Digest = require('./Digest');
8
10
 
9
- class CameraVapix extends EventEmitter{
10
- constructor (options) {
11
- super();
12
- this.protocol = 'http';
13
- this.ip = '127.0.0.1';
14
- this.port = 80;
15
- this.auth = '';
16
- if (options) {
17
- this.protocol = options['protocol'] || this.protocol;
18
- this.ip = options['ip'] || this.ip;
19
- this.port = options['port'];
20
- if (this.port == undefined) {
21
- this.port = this.protocol == 'http' ? 80 : 443
22
- }
23
- this.auth = options['auth'] || this.auth;
24
- }
25
11
 
26
- this.rtsp = null;
27
- }
12
+ class CameraVapix extends EventEmitter {
13
+ constructor(options) {
14
+ super();
15
+ this.protocol = 'http';
16
+ this.ip = '127.0.0.1';
17
+ this.port = 80;
18
+ this.auth = '';
28
19
 
29
- getParameterGroup(groupNames) {
30
- let promise = new Promise(function(resolve, reject) {
31
- this.vapixGet('/axis-cgi/param.cgi?action=list&group=' + encodeURIComponent(groupNames)).then(function(response) {
32
- let params = {};
33
- let lines = response.split(/[\r\n]/);
34
- for (let i = 0; i < lines.length; i++) {
35
- if (lines[i].length) {
36
- let p = lines[i].split('=');
37
- if (p.length >= 2) {
38
- params[p[0]] = p[1];
39
- }
20
+ if (options) {
21
+ this.protocol = options['protocol'] || this.protocol;
22
+ this.ip = options['ip'] || this.ip;
23
+ this.port = options['port'];
24
+ if (this.port == undefined) {
25
+ this.port = this.protocol == 'http' ? 80 : 443
26
+ }
27
+ this.auth = options['auth'] || this.auth;
40
28
  }
41
- }
42
- resolve(params);
43
- }, reject);
44
- }.bind(this));
45
- return promise;
46
- }
47
29
 
48
- setParameter(params) {
49
- let postData = 'action=update&';
50
- Object.keys(params).forEach(function(key) {
51
- postData += key + '=' + params[key] + '&';
52
- });
53
- postData = postData.slice(0, postData.length - 1);
54
- return this.vapixPost('/axis-cgi/param.cgi', postData);
55
- }
30
+ this.rtsp = null;
31
+ this.ws = null;
32
+ }
56
33
 
57
- getPTZPresetList(channel) {
58
- let promise = new Promise(function(resolve, reject) {
59
- this.vapixGet('/axis-cgi/com/ptz.cgi?query=presetposcam&camera=' + encodeURIComponent(channel)).then(function(response) {
60
- let positions = [];
61
- let lines = response.split(/[\r\n]/);
62
- for (let i = 0; i < lines.length; i++) {
63
- if (lines[i].length && lines[i].indexOf('presetposno') != -1) {
64
- let p = lines[i].split('=');
65
- if (p.length >= 2) {
66
- positions.push(p[1]);
67
- }
68
- }
69
- }
70
- resolve(positions);
71
- }, reject);
72
- }.bind(this));
73
- return promise;
74
- }
34
+ getParameterGroup(groupNames) {
35
+ let promise = new Promise((resolve, reject) => {
36
+ this.vapixGet('/axis-cgi/param.cgi?action=list&group=' + encodeURIComponent(groupNames)).then((response) => {
37
+ let params = {};
38
+ let lines = response.split(/[\r\n]/);
39
+ for (let i = 0; i < lines.length; i++) {
40
+ if (lines[i].length) {
41
+ let p = lines[i].split('=');
42
+ if (p.length >= 2) {
43
+ params[p[0]] = p[1];
44
+ }
45
+ }
46
+ }
47
+ resolve(params);
48
+ }, reject);
49
+ });
50
+ return promise;
51
+ }
75
52
 
76
- goToPreset (channel, presetName) {
77
- return this.vapixPost('/axis-cgi/com/ptz.cgi', 'camera=' + encodeURIComponent(channel) + '&gotoserverpresetname=' + encodeURIComponent(presetName));
78
- }
53
+ setParameter(params) {
54
+ let postData = 'action=update&';
55
+ Object.keys(params).forEach((key) => {
56
+ postData += key + '=' + params[key] + '&';
57
+ });
58
+ postData = postData.slice(0, postData.length - 1);
59
+ return this.vapixPost('/axis-cgi/param.cgi', postData);
60
+ }
79
61
 
80
- getGuardTourList() {
81
- let promise = new Promise(function(resolve, reject) {
82
- let gTourList = [];
83
- this.getParameterGroup('GuardTour').then(function(response) {
84
- for (let i = 0; i < 20; i++) {
85
- let gTourBaseName = 'root.GuardTour.G' + i;
86
- if (gTourBaseName + '.CamNbr' in response) {
87
- let gTour = {
88
- 'ID': gTourBaseName,
89
- 'CamNbr': response[gTourBaseName + '.CamNbr'],
90
- 'Name': response[gTourBaseName + '.Name'],
91
- 'RandomEnabled': response[gTourBaseName + '.RandomEnabled'],
92
- 'Running': response[gTourBaseName + '.Running'],
93
- 'TimeBetweenSequences': response[gTourBaseName + '.TimeBetweenSequences'],
94
- 'Tour': []
95
- };
96
- for (let j = 0; j < 100; j++) {
97
- let tourBaseName = 'root.GuardTour.G' + i + '.Tour.T' + j;
98
- if (tourBaseName + '.MoveSpeed' in response) {
99
- let tour = {
100
- 'MoveSpeed': response[tourBaseName + '.MoveSpeed'],
101
- 'Position': response[tourBaseName + '.Position'],
102
- 'PresetNbr': response[tourBaseName + '.PresetNbr'],
103
- 'WaitTime': response[tourBaseName + '.WaitTime'],
104
- 'WaitTimeViewType': response[tourBaseName + '.WaitTimeViewType']
105
- };
106
- gTour.Tour.push(tour);
107
- }
108
- }
109
- gTourList.push(gTour);
110
- } else {
111
- break;
112
- }
113
- }
114
- resolve(gTourList);
115
- }, reject)
116
- }.bind(this));
117
- return promise;
118
- }
62
+ getPTZPresetList(channel) {
63
+ let promise = new Promise((resolve, reject) => {
64
+ this.vapixGet('/axis-cgi/com/ptz.cgi?query=presetposcam&camera=' + encodeURIComponent(channel)).then((response) => {
65
+ let positions = [];
66
+ let lines = response.split(/[\r\n]/);
67
+ for (let i = 0; i < lines.length; i++) {
68
+ if (lines[i].length && lines[i].indexOf('presetposno') != -1) {
69
+ let p = lines[i].split('=');
70
+ if (p.length >= 2) {
71
+ positions.push(p[1]);
72
+ }
73
+ }
74
+ }
75
+ resolve(positions);
76
+ }, reject);
77
+ });
78
+ return promise;
79
+ }
119
80
 
120
- setGuardTourEnabled(gourTourID, enable) {
121
- let options = {};
122
- options[gourTourID + '.Running'] = enable ? 'yes' : 'no';
123
- return this.setParameter(options);
124
- }
81
+ goToPreset(channel, presetName) {
82
+ return this.vapixPost('/axis-cgi/com/ptz.cgi', 'camera=' + encodeURIComponent(channel) + '&gotoserverpresetname=' + encodeURIComponent(presetName));
83
+ }
125
84
 
126
- getInputState(port) {
127
- let promise = new Promise(function(resolve, reject) {
128
- this.vapixPost('/axis-cgi/io/port.cgi', 'checkactive=' + encodeURIComponent(port)).then(function(response) {
129
- resolve(response.split('=')[1].indexOf('active') == 0);
130
- }, reject);
131
- }.bind(this));
132
- return promise;
133
- }
85
+ getGuardTourList() {
86
+ let promise = new Promise((resolve, reject) => {
87
+ let gTourList = [];
88
+ this.getParameterGroup('GuardTour').then((response) => {
89
+ for (let i = 0; i < 20; i++) {
90
+ let gTourBaseName = 'root.GuardTour.G' + i;
91
+ if (gTourBaseName + '.CamNbr' in response) {
92
+ let gTour = {
93
+ 'ID': gTourBaseName,
94
+ 'CamNbr': response[gTourBaseName + '.CamNbr'],
95
+ 'Name': response[gTourBaseName + '.Name'],
96
+ 'RandomEnabled': response[gTourBaseName + '.RandomEnabled'],
97
+ 'Running': response[gTourBaseName + '.Running'],
98
+ 'TimeBetweenSequences': response[gTourBaseName + '.TimeBetweenSequences'],
99
+ 'Tour': []
100
+ };
101
+ for (let j = 0; j < 100; j++) {
102
+ let tourBaseName = 'root.GuardTour.G' + i + '.Tour.T' + j;
103
+ if (tourBaseName + '.MoveSpeed' in response) {
104
+ let tour = {
105
+ 'MoveSpeed': response[tourBaseName + '.MoveSpeed'],
106
+ 'Position': response[tourBaseName + '.Position'],
107
+ 'PresetNbr': response[tourBaseName + '.PresetNbr'],
108
+ 'WaitTime': response[tourBaseName + '.WaitTime'],
109
+ 'WaitTimeViewType': response[tourBaseName + '.WaitTimeViewType']
110
+ };
111
+ gTour.Tour.push(tour);
112
+ }
113
+ }
114
+ gTourList.push(gTour);
115
+ } else {
116
+ break;
117
+ }
118
+ }
119
+ resolve(gTourList);
120
+ }, reject)
121
+ });
122
+ return promise;
123
+ }
134
124
 
135
- setOutputState(port, active) {
136
- return this.vapixPost('/axis-cgi/io/port.cgi', 'action=' + encodeURIComponent(port) + ':' + (active ? '/' : '\\'));
137
- }
125
+ setGuardTourEnabled(gourTourID, enable) {
126
+ let options = {};
127
+ options[gourTourID + '.Running'] = enable ? 'yes' : 'no';
128
+ return this.setParameter(options);
129
+ }
130
+
131
+ getInputState(port) {
132
+ let promise = new Promise((resolve, reject) => {
133
+ this.vapixPost('/axis-cgi/io/port.cgi', 'checkactive=' + encodeURIComponent(port)).then((response) => {
134
+ resolve(response.split('=')[1].indexOf('active') == 0);
135
+ }, reject);
136
+ });
137
+ return promise;
138
+ }
139
+
140
+ setOutputState(port, active) {
141
+ return this.vapixPost('/axis-cgi/io/port.cgi', 'action=' + encodeURIComponent(port) + ':' + (active ? '/' : '\\'));
142
+ }
138
143
 
139
- getApplicationList() {
140
- const promise = new Promise((resolve, reject) => {
141
- this.vapixGet('/axis-cgi/applications/list.cgi').then(function(xml) {
142
- parseString(xml, function (err, result) {
143
- if (err) {
144
- reject(err);
145
- return;
144
+ getApplicationList() {
145
+ const promise = new Promise((resolve, reject) => {
146
+ this.vapixGet('/axis-cgi/applications/list.cgi').then((xml) => {
147
+ parseString(xml, (err, result) => {
148
+ if (err) {
149
+ reject(err);
150
+ return;
151
+ }
152
+ let apps = [];
153
+ for (let i = 0; i < result.reply.application.length; i++) {
154
+ apps.push(result.reply.application[i].$);
155
+ }
156
+ resolve(apps);
157
+ });
158
+ }, reject);
159
+ });
160
+ return promise;
161
+ }
162
+
163
+ getEventDeclarations() {
164
+ const promise = new Promise((resolve, reject) => {
165
+ let data =
166
+ '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' +
167
+ '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
168
+ 'xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
169
+ '<GetEventInstances xmlns="http://www.axis.com/vapix/ws/event1"/>' +
170
+ '</s:Body>' +
171
+ '</s:Envelope>';
172
+ this.vapixPost('/vapix/services', data, 'application/soap+xml').then((declarations) => {
173
+ resolve(prettifyXml(declarations));
174
+ }, reject);
175
+ });
176
+ return promise;
177
+ }
178
+
179
+ isReservedEventName(eventName) {
180
+ return (eventName == 'eventsConnect' || eventName == 'eventsDisconnect');
181
+ }
182
+
183
+ eventsConnect(channel = "RTSP") {
184
+ if (this.ws != null) {
185
+ throw new Error("Websocket is already opened.");
146
186
  }
147
- let apps = [];
148
- for (let i = 0; i < result.reply.application.length; i++) {
149
- apps.push(result.reply.application[i].$);
187
+ if (this.rtsp != null) {
188
+ throw new Error("RTSP is already opened.");
150
189
  }
151
- resolve(apps);
152
- });
153
- }, reject);
154
- });
155
- return promise;
156
- }
157
-
158
- getEventDeclarations() {
159
- const promise = new Promise((resolve, reject) => {
160
- let data =
161
- '<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">' +
162
- '<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' +
163
- 'xmlns:xsd="http://www.w3.org/2001/XMLSchema">' +
164
- '<GetEventInstances xmlns="http://www.axis.com/vapix/ws/event1"/>' +
165
- '</s:Body>' +
166
- '</s:Envelope>';
167
- this.vapixPost('/vapix/services', data, 'application/soap+xml').then(function(declarations) {
168
- resolve(prettifyXml(declarations));
169
- }, reject);
170
- });
171
- return promise;
172
- }
190
+ if (channel == "RTSP") {
191
+ this.rtspConnect();
192
+ }
193
+ else if (channel == "websocket") {
194
+ this.websocketConnect();
195
+ }
196
+ else {
197
+ throw new Error("Unknown channel.");
198
+ }
199
+ }
173
200
 
174
- eventsConnect(){
175
- this.rtsp = new RtspClient({
176
- 'ip': this.ip,
177
- 'port': this.port,
178
- 'auth': this.auth,
179
- });
180
-
181
- this.rtsp.on('connect', function() { this.emit('eventsConnect'); }.bind(this));
182
- this.rtsp.on('disconnect', function(err) { this.emit('eventsDisconnect', err); }.bind(this));
183
- this.rtsp.on('event', (event) => {
184
- let eventNames = this.eventNames();
185
- for (let i = 0; i < eventNames.length; i++) {
186
- if (eventNames[i] != 'eventsConnect' && eventNames[i] != 'eventsDisconnect') {
187
- let name = eventNames[i];
188
- // Remove special chars from the end
189
- while (name[name.length - 1] == '.' || name[name.length - 1] == '/') {
190
- name = name.substring(0, name.length - 1);
201
+ eventsDisconnect() {
202
+ if (this.rtsp != null) {
203
+ this.rtsp.disconnect();
191
204
  }
192
- // Find registered event name in the message
193
- if (event.indexOf(name) != -1) {
194
- // Convert to JSON and emit signal
195
- parseString(event, function (err, eventJson) {
196
- if (err) {
197
- this.eventsDisconnect();
198
- return;
199
- }
200
- this.emit(eventNames[i], eventJson);
201
- }.bind(this));
202
- break;
205
+ if (this.ws != null) {
206
+ this.ws.close();
203
207
  }
204
- }
205
208
  }
206
- });
207
-
208
- let eventTopicFilter = '';
209
- let eventNames = this.eventNames();
210
- for (let i = 0; i < eventNames.length; i++) {
211
- if (eventNames[i] != 'eventsConnect' && eventNames[i] != 'eventsDisconnect') {
212
- if (eventTopicFilter.length != 0) {
213
- eventTopicFilter += '|';
214
- }
215
-
216
- let topic = eventNames[i].replace(/tns1/g, 'onvif');
217
- topic = topic.replace(/tnsaxis/g, 'axis');
218
- eventTopicFilter += topic;
209
+
210
+ rtspConnect() {
211
+ this.rtsp = new RtspClient({
212
+ 'ip': this.ip,
213
+ 'port': this.port,
214
+ 'auth': this.auth,
215
+ });
216
+
217
+ this.rtsp.on('connect', () => {
218
+ this.emit('eventsConnect');
219
+ });
220
+ this.rtsp.on('disconnect', (err) => {
221
+ this.emit('eventsDisconnect', err);
222
+ this.rtsp = null;
223
+ });
224
+ this.rtsp.on('event', (event) => {
225
+ let eventNames = this.eventNames();
226
+ for (let i = 0; i < eventNames.length; i++) {
227
+ if (!this.isReservedEventName(eventNames[i])) {
228
+ let name = eventNames[i];
229
+ // Remove special chars from the end
230
+ while (name[name.length - 1] == '.' || name[name.length - 1] == '/') {
231
+ name = name.substring(0, name.length - 1);
232
+ }
233
+ // Find registered event name in the message
234
+ if (event.indexOf(name) != -1) {
235
+ // Convert to JSON and emit signal
236
+ parseString(event, (err, eventJson) => {
237
+ if (err) {
238
+ this.eventsDisconnect();
239
+ return;
240
+ }
241
+ this.emit(eventNames[i], eventJson);
242
+ });
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ });
248
+
249
+ let eventTopicFilter = '';
250
+ let eventNames = this.eventNames();
251
+ for (let i = 0; i < eventNames.length; i++) {
252
+ if (!this.isReservedEventName(eventNames[i])) {
253
+ if (eventTopicFilter.length != 0) {
254
+ eventTopicFilter += '|';
255
+ }
256
+
257
+ let topic = eventNames[i].replace(/tns1/g, 'onvif');
258
+ topic = topic.replace(/tnsaxis/g, 'axis');
259
+ eventTopicFilter += topic;
260
+ }
261
+ }
262
+ this.rtsp.connect(eventTopicFilter);
219
263
  }
220
- }
221
- this.rtsp.connect(eventTopicFilter);
222
- }
223
264
 
224
- eventsDisconnect() {
225
- if (this.rtsp) {
226
- this.rtsp.disconnect();
227
- this.rtsp = null;
228
- }
229
- }
265
+ websocketConnect(digestHeader) {
266
+ const address = `ws://${this.ip}:${this.port}/vapix/ws-data-stream?sources=events`;
230
267
 
231
- vapixGet(path, noWaitForData) {
232
- let options = this.getBaseVapixConnectionParams();
233
- options['path'] = encodeURI(path);
234
- return httpRequest(options, undefined, noWaitForData);
235
- }
268
+ let options =
269
+ {
270
+ 'auth': this.auth
271
+ };
236
272
 
237
- async getCameraImage(camera, compression, resolution, outputStream){
238
- const path = `/axis-cgi/jpg/image.cgi?resolution=${resolution}&compression=${compression}&camera=${camera}`;
239
- const res = await this.vapixGet(path,true);
240
- res.pipe(outputStream);
241
- return outputStream;
242
- }
273
+ if (digestHeader !== undefined) {
274
+ let userPass = this.auth.split(':');
275
+ options.headers = options.headers || {};
276
+ options['headers']['Authorization'] = Digest.getAuthHeader(userPass[0], userPass[1], 'GET', '/vapix/ws-data-stream?sources=events', digestHeader);
277
+ }
243
278
 
244
- vapixPost(path, data, contentType) {
245
- let options = this.getBaseVapixConnectionParams();
246
- options['method'] = 'POST';
247
- options['path'] = path;
248
- if (contentType) {
249
- options['headers'] = {'Content-Type': contentType};
250
- }
251
- return httpRequest(options, data);
252
- }
279
+ return new Promise((resolve, reject) => {
280
+ this.ws = new WebSocket(address, options);
253
281
 
254
- getBaseVapixConnectionParams(options, postData) {
255
- return {
256
- 'protocol': this.protocol + ':',
257
- 'host': this.ip,
258
- 'port': this.port,
259
- 'auth': this.auth
260
- };
261
- }
282
+ this.ws.on('open', () => {
283
+ let topics = [];
284
+ let eventNames = this.eventNames();
285
+ for (let i = 0; i < eventNames.length; i++) {
286
+ if (!this.isReservedEventName(eventNames[i])) {
287
+ let topic =
288
+ {
289
+ "topicFilter": eventNames[i]
290
+ }
291
+ topics.push(topic);
292
+ }
293
+ }
294
+
295
+ const topicFilter = {
296
+ "apiVersion": "1.0",
297
+ "method": "events:configure",
298
+ "params": {
299
+ "eventFilterList": topics
300
+ }
301
+ }
302
+ this.ws.send(JSON.stringify(topicFilter));
303
+ });
304
+
305
+ this.ws.on('unexpected-response', (req, res) => {
306
+ if (res.statusCode == 401 && res.headers['www-authenticate'] != undefined)
307
+ this.websocketConnect(res.headers['www-authenticate']).then(resolve, reject);
308
+ else {
309
+ reject('Error: status code: ' + res.statusCode + ', ' + res.data);
310
+ }
311
+ });
312
+
313
+ this.ws.on('message', (data) => {
314
+ let dataJSON = JSON.parse(data);
315
+ if (dataJSON.method === 'events:configure') {
316
+ if (dataJSON.error === undefined) {
317
+ this.emit("eventsConnect");
318
+ }
319
+ else {
320
+ this.emit("eventsDisconnect", dataJSON.error);
321
+ this.eventsDisconnect();
322
+ }
323
+ return;
324
+ }
325
+ let eventName = dataJSON.params.notification.topic;
326
+ this.emit(eventName, dataJSON);
327
+ });
328
+ this.ws.on('error', (error) => {
329
+ this.emit("eventsDisconnect", error);
330
+ this.ws = null;
331
+ });
332
+ this.ws.on('close', () => {
333
+ if (this.ws !== null) {
334
+ this.emit("websocketDisconnect");
335
+ }
336
+ this.ws = null;
337
+ });
338
+ });
339
+ }
340
+
341
+ vapixGet(path, noWaitForData) {
342
+ let options = this.getBaseVapixConnectionParams();
343
+ options['path'] = encodeURI(path);
344
+ return httpRequest(options, undefined, noWaitForData);
345
+ }
346
+
347
+ async getCameraImage(camera, compression, resolution, outputStream) {
348
+ const path = `/axis-cgi/jpg/image.cgi?resolution=${resolution}&compression=${compression}&camera=${camera}`;
349
+ const res = await this.vapixGet(path, true);
350
+ res.pipe(outputStream);
351
+ return outputStream;
352
+ }
353
+
354
+ vapixPost(path, data, contentType) {
355
+ let options = this.getBaseVapixConnectionParams();
356
+ options['method'] = 'POST';
357
+ options['path'] = path;
358
+ if (contentType) {
359
+ options['headers'] = { 'Content-Type': contentType };
360
+ }
361
+ return httpRequest(options, data);
362
+ }
363
+
364
+ getBaseVapixConnectionParams(options, postData) {
365
+ return {
366
+ 'protocol': this.protocol + ':',
367
+ 'host': this.ip,
368
+ 'port': this.port,
369
+ 'auth': this.auth
370
+ };
371
+ }
262
372
  }
263
373
  module.exports = CameraVapix;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstreamerlib",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Helper library for CamStreamer ACAP applications.",
5
5
  "main": "CameraVapix.js",
6
6
  "dependencies": {