├── .gitignore
├── LICENSE
├── README.md
├── icons
├── device.png
├── discovery.png
├── event.png
├── imaging.png
├── media.png
├── ptz.png
└── recording.png
├── images
├── onvif_config_comm.png
├── onvif_config_node.png
├── onvif_device.png
├── onvif_discovery_debug.png
├── onvif_discovery_flow.png
├── onvif_discovery_status.png
├── onvif_events.png
├── onvif_example_profile.png
├── onvif_get_profiles.png
├── onvif_media_audio.png
├── onvif_media_profiles.png
├── onvif_media_snapshot.png
├── onvif_media_snapshot_http.png
├── onvif_media_snapshot_short.png
├── onvif_media_streamurl.png
├── onvif_media_video.png
├── onvif_ptz_flow.png
├── onvif_ptz_home.png
├── onvif_ptz_info.png
├── onvif_ptz_preset.png
└── onvif_snapshot_debug.png
├── onvif_config.html
├── onvif_config.js
├── onvif_device.html
├── onvif_device.js
├── onvif_discovery.html
├── onvif_discovery.js
├── onvif_events.html
├── onvif_events.js
├── onvif_imaging.html
├── onvif_imaging.js
├── onvif_media.html
├── onvif_media.js
├── onvif_ptz.html
├── onvif_ptz.js
├── onvif_recording.html
├── onvif_recording.js
├── package.json
└── utils.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/icons/device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/device.png
--------------------------------------------------------------------------------
/icons/discovery.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/discovery.png
--------------------------------------------------------------------------------
/icons/event.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/event.png
--------------------------------------------------------------------------------
/icons/imaging.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/imaging.png
--------------------------------------------------------------------------------
/icons/media.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/media.png
--------------------------------------------------------------------------------
/icons/ptz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/ptz.png
--------------------------------------------------------------------------------
/icons/recording.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/icons/recording.png
--------------------------------------------------------------------------------
/images/onvif_config_comm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_config_comm.png
--------------------------------------------------------------------------------
/images/onvif_config_node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_config_node.png
--------------------------------------------------------------------------------
/images/onvif_device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_device.png
--------------------------------------------------------------------------------
/images/onvif_discovery_debug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_discovery_debug.png
--------------------------------------------------------------------------------
/images/onvif_discovery_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_discovery_flow.png
--------------------------------------------------------------------------------
/images/onvif_discovery_status.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_discovery_status.png
--------------------------------------------------------------------------------
/images/onvif_events.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_events.png
--------------------------------------------------------------------------------
/images/onvif_example_profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_example_profile.png
--------------------------------------------------------------------------------
/images/onvif_get_profiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_get_profiles.png
--------------------------------------------------------------------------------
/images/onvif_media_audio.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_audio.png
--------------------------------------------------------------------------------
/images/onvif_media_profiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_profiles.png
--------------------------------------------------------------------------------
/images/onvif_media_snapshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_snapshot.png
--------------------------------------------------------------------------------
/images/onvif_media_snapshot_http.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_snapshot_http.png
--------------------------------------------------------------------------------
/images/onvif_media_snapshot_short.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_snapshot_short.png
--------------------------------------------------------------------------------
/images/onvif_media_streamurl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_streamurl.png
--------------------------------------------------------------------------------
/images/onvif_media_video.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_media_video.png
--------------------------------------------------------------------------------
/images/onvif_ptz_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_ptz_flow.png
--------------------------------------------------------------------------------
/images/onvif_ptz_home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_ptz_home.png
--------------------------------------------------------------------------------
/images/onvif_ptz_info.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_ptz_info.png
--------------------------------------------------------------------------------
/images/onvif_ptz_preset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_ptz_preset.png
--------------------------------------------------------------------------------
/images/onvif_snapshot_debug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bartbutenaers/node-red-contrib-onvif-nodes/de93eb7c0e27760cae4e3fdbdf593d7b96685457/images/onvif_snapshot_debug.png
--------------------------------------------------------------------------------
/onvif_config.html:
--------------------------------------------------------------------------------
1 |
13 |
48 |
49 |
82 |
83 |
97 |
--------------------------------------------------------------------------------
/onvif_config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 |
20 | function setOnvifStatus(node, onvifStatus) {
21 | node.onvifStatus = onvifStatus;
22 | // Pass the new status to all the available listeners
23 | node.emit('onvif_status', onvifStatus);
24 | }
25 |
26 | function OnVifConfigNode(config) {
27 | RED.nodes.createNode(this, config);
28 | this.xaddress = config.xaddress;
29 | this.port = parseInt(config.port || 80);
30 | this.name = config.name;
31 | this.timeout = config.timeout || 3;
32 | this.checkConnectionInterval = config.checkConnectionInterval || 5;
33 | // Remark: user name and password are stored in this.credentials
34 |
35 | var node = this;
36 |
37 | // All Onvif nodes can add a listener to track the 'onvif_status' events.
38 | // However by default only 10 listeners are allowed, which results in a warning when more Onvif nodes use this config node:
39 | // MaxListenersExceededWarning: Possible EventEmitter memory leak detected
40 | // To avoid that, we will allow an infinite number of listeners.
41 | // Caution: when you have suspicion that the listeners are leaking, put the next line in comment !!!
42 | node.setMaxListeners(0);
43 |
44 | this.getProfiles = function(clientConfig, response) {
45 | var profileNames = [];
46 | var config = {};
47 |
48 | // TODO checken of er altijd een this.credentials bestaat, indien username en paswoord niet ingevuld is.
49 |
50 | // The client credentials will only contain the data (i.e. user name or password) which has changed.
51 | // The other data is not changed, so we will need use the original data stored on the server.
52 | clientConfig.username = clientConfig.user || this.credentials.user;
53 | clientConfig.password = clientConfig.password || this.credentials.password;
54 |
55 | // When the user appends some new text to the existing password, then the original password is passed via the client as __PWRD__
56 | // So replace __PWRD__ again by the original password.
57 | if (clientConfig.password && this.credentials.password) {
58 | clientConfig.password.replace('___PWRD__', this.credentials.password);
59 | }
60 |
61 | if (this.credentials.user !== clientConfig.user || this.credentials.password !== clientConfig.password || this.xaddress !== clientConfig.hostname){
62 | var cam = new onvif.Cam(clientConfig, function(err) {
63 | if (!err) {
64 | if (cam.profiles) {
65 | for(var i = 0; i < cam.profiles.length; i++) {
66 | profileNames.push({
67 | label: cam.profiles[i].name,
68 | value: cam.profiles[i].$.token
69 | });
70 | }
71 | }
72 |
73 | response.json(profileNames);
74 | }
75 | });
76 | }
77 | else {
78 | if (this.cam.profiles) {
79 | // The current deployed cam is still up-to-date, so let’s use that one (for performance reasons)
80 | for(var i = 0; i < this.cam.profiles.length; i++) {
81 | profileNames.push({
82 | label: this.cam.profiles[i].name,
83 | value: this.cam.profiles[i].$.token
84 | });
85 | }
86 | }
87 |
88 | response.json(profileNames);
89 | }
90 | }
91 |
92 | this.getProfileTokenByName = function(profileName) {
93 | if (this.cam.profiles) {
94 | // Try to find a profile with the specified name, and return the token
95 | for(var i = 0; i < this.cam.profiles.length; i++) {
96 | if (this.cam.profiles[i].name === profileName) {
97 | return this.cam.profiles[i].$.token;
98 | }
99 | }
100 | }
101 |
102 | // No token found with the specified name
103 | return null;
104 | }
105 |
106 | // This should be called by all nodes that use this config node
107 | this.initialize = function() {
108 | // This config node can only be initialized oncellchange
109 | if (this.cam) {
110 | return;
111 | }
112 |
113 | // Without an xaddress, it is impossible to connect to an Onvif device
114 | if (!this.xaddress) {
115 | // Make sure the Catch-node can catch the error
116 | node.error( "Cannot connect to unconfigured Onvif device", {} );
117 |
118 | this.cam = null;
119 | setOnvifStatus(node, "unconfigured");
120 | return;
121 | }
122 |
123 | setOnvifStatus(node, "initializing");
124 |
125 | var options = {};
126 | options.hostname = this.xaddress;
127 | options.port = this.port;
128 | options.timeout = this.timeout * 1000;
129 |
130 | if (this.credentials && this.credentials.user) {
131 | options.username = this.credentials.user;
132 | options.password = this.credentials.password;
133 | }
134 |
135 | // Create a new camera instance, which will automatically connect to the device (to load configuration data)
136 | this.cam = new onvif.Cam(options, function(err) {
137 | if (err) {
138 | // Make sure the Catch-node can catch the error
139 | node.error( err, {} );
140 |
141 | setOnvifStatus(node, "disconnected");
142 | }
143 | else {
144 | setOnvifStatus(node, "connected");
145 | }
146 | });
147 |
148 | // When an checkConnection timer is running, then stop it
149 | if (this.checkConnectionTimer) {
150 | clearInterval(this.checkConnectionTimer);
151 | this.checkConnectionTimer = null;
152 | }
153 |
154 | if (this.checkConnectionInterval > 0) {
155 | // Start a new checkConnection timer, that checks at the specified interval whether the Onvif device is disconnected.
156 | // This way we can keep the node status in the flow editor up to date ...
157 | this.checkConnectionTimer = setInterval(function() {
158 | // Check whether the Onvif device is connected, by calling the device system and time
159 | node.cam.getSystemDateAndTime(function(err, date, xml) {
160 | if (err) {
161 | setOnvifStatus(node, "disconnected");
162 | }
163 | else {
164 | if (!node.cam.capabilities && !node.cam.services) {
165 | // Probably the device was unavailable when this node was being started, so the autoConnect was not able
166 | // to load all the data from the camera (like e.g. its capabilities). Afterwards the device became available,
167 | // so it is now time to load the device data into the Cam instance (by calling its 'connect' function).
168 | // Note that this needs to be executed before setOnvifStatus, because that function uses the capabilities.
169 | node.cam.connect(function(err, date, xml) {
170 | if (err) {
171 | node.error("The device is now connected, but the data cannot be loaded");
172 | }
173 | });
174 | }
175 |
176 | setOnvifStatus(node, "connected");
177 | }
178 | });
179 | }, this.checkConnectionInterval * 1000);
180 | }
181 | }
182 |
183 | node.on('close', function(){
184 | setOnvifStatus(this, "");
185 |
186 | this.removeAllListeners("onvif_status");
187 |
188 | // When an checkConnection timer is running, then stop it
189 | if (this.checkConnectionTimer) {
190 | clearInterval(this.checkConnectionTimer);
191 | this.checkConnectionTimer = null;
192 | }
193 | });
194 | }
195 |
196 | RED.nodes.registerType("onvif-config",OnVifConfigNode,{
197 | credentials: {
198 | user: {type:"text"},
199 | password: {type: "password"}
200 | }
201 | });
202 |
203 | // Make all the available profiles accessible for the node's config screen
204 | RED.httpAdmin.get('/onvifdevice/:cmd/:config_node_id', RED.auth.needsPermission('onvifdevice.read'), function(req, res){
205 | var configNode = RED.nodes.getNode(req.params.config_node_id);
206 |
207 | switch (req.params.cmd) {
208 | case "profiles":
209 | if (!configNode) {
210 | console.log("Cannot determine profile list from node " + req.params.config_node_id);
211 | return;
212 | }
213 |
214 | // Get the profiles of the camera, based on the config data on the client, instead of the config data
215 | // stored inside this config node. Reason is that the config data on the client might be 'dirty', i.e. changed
216 | // by the user but not deployed yet on this config node. But the client still needs to be able to get the profiles
217 | // corresponding to that dirty config node. That way the config screen can be filled with profiles already...
218 | // But when the config data is not dirty, we will just use the profiles already loaded in this config node (which is faster).
219 | // See https://discourse.nodered.org/t/initializing-config-screen-based-on-new-config-node/7327/10?u=bartbutenaers
220 | configNode.getProfiles(req.query, res);
221 |
222 | break;
223 | }
224 | });
225 | }
226 |
--------------------------------------------------------------------------------
/onvif_device.html:
--------------------------------------------------------------------------------
1 |
13 |
37 |
38 |
66 |
67 |
74 |
75 |
--------------------------------------------------------------------------------
/onvif_device.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 | const utils = require('./utils');
20 |
21 | function OnVifDeviceNode(config) {
22 | RED.nodes.createNode(this, config);
23 | this.action = config.action;
24 |
25 | var node = this;
26 |
27 | // Retrieve the config node, where the device is configured
28 | node.deviceConfig = RED.nodes.getNode(config.deviceConfig);
29 |
30 | if (node.deviceConfig) {
31 | node.listener = function(onvifStatus) {
32 | utils.setNodeStatus(node, 'device', onvifStatus);
33 | }
34 |
35 | // Start listening for Onvif config nodes status changes
36 | node.deviceConfig.addListener("onvif_status", node.listener);
37 |
38 | // Show the current Onvif config node status already
39 | utils.setNodeStatus(node, 'device', node.deviceConfig.onvifStatus);
40 |
41 | node.deviceConfig.initialize();
42 | }
43 |
44 | node.on("input", function(msg) {
45 | var newMsg = {};
46 |
47 | var action = node.action || msg.action;
48 |
49 | if (!action) {
50 | console.warn('When no action specified in the node, it should be specified in the msg.action');
51 | return;
52 | }
53 |
54 | // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet)
55 | if (action !== "reconnect") {
56 | if (!node.deviceConfig || node.deviceConfig.onvifStatus !== "connected") {
57 | node.error("This node is not connected to a device");
58 | return;
59 | }
60 |
61 | if (!utils.hasService(node.deviceConfig.cam, 'device')) {
62 | node.error("The device has no support for a device service");
63 | return;
64 | }
65 | }
66 |
67 | newMsg.xaddr = this.deviceConfig.xaddress;
68 | newMsg.action = action;
69 |
70 | try {
71 | switch (action) {
72 | case "getDeviceInformation":
73 | node.deviceConfig.cam.getDeviceInformation(function(err, date, xml) {
74 | utils.handleResult(node, err, date, xml, newMsg);
75 | });
76 | break;
77 | case "getHostname":
78 | node.deviceConfig.cam.getHostname(function(err, date, xml) {
79 | utils.handleResult(node, err, date, xml, newMsg);
80 | });
81 | break;
82 | case "getSystemDateAndTime":
83 | node.deviceConfig.cam.getSystemDateAndTime(function(err, date, xml) {
84 | utils.handleResult(node, err, date, xml, newMsg);
85 | });
86 | break;
87 | case "getServices":
88 | node.deviceConfig.cam.getCapabilities(function(err, date, xml) {
89 | utils.handleResult(node, err, date, xml, newMsg);
90 | });
91 | break;
92 | case "getCapabilities":
93 | node.deviceConfig.cam.getCapabilities(function(err, date, xml) {
94 | utils.handleResult(node, err, date, xml, newMsg);
95 | });
96 | break;
97 | case "getScopes":
98 | node.deviceConfig.cam.getScopes(function(err, date, xml) {
99 | utils.handleResult(node, err, date, xml, newMsg);
100 | });
101 | break;
102 | case "systemReboot":
103 | node.deviceConfig.cam.systemReboot(function(err, date, xml) {
104 | utils.handleResult(node, err, date, xml, newMsg);
105 | });
106 | break;
107 | case "getServiceCapabilities":
108 | node.deviceConfig.cam.getCapabilities(function(err, date, xml) {
109 | utils.handleResult(node, err, date, xml, newMsg);
110 | });
111 | break;
112 | case "reconnect":
113 | node.deviceConfig.cam.connect(function(err) {
114 | utils.handleResult(node, err, "", null, newMsg);
115 | });
116 | break
117 | default:
118 | //node.status({fill:"red",shape:"dot",text: "unsupported action"});
119 | node.error("Action " + action + " is not supported");
120 | }
121 | }
122 | catch (exc) {
123 | node.error("Action " + action + " failed: " + exc);
124 | }
125 | });
126 |
127 | node.on("close",function() {
128 | if (node.listener) {
129 | node.deviceConfig.removeListener("onvif_status", node.listener);
130 | }
131 | });
132 | }
133 | RED.nodes.registerType("onvif-device",OnVifDeviceNode);
134 | }
135 |
--------------------------------------------------------------------------------
/onvif_discovery.html:
--------------------------------------------------------------------------------
1 |
13 |
30 |
31 |
47 |
48 |
56 |
--------------------------------------------------------------------------------
/onvif_discovery.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 |
20 | function OnVifDiscoveryNode(config) {
21 | RED.nodes.createNode(this, config);
22 | this.timeout = parseInt(config.timeout) * 1000; // Seconds
23 | this.separate = config.separate;
24 | this.discovering = false;
25 |
26 | var node = this;
27 |
28 | // Simplify the result, so it becomes more easy to parse it with Node-Red
29 | function simplifyResult(result) {
30 | var probeMatch;
31 |
32 | // Reduce the property depth
33 | probeMatch = result.probeMatches.probeMatch;
34 |
35 | // The result can be an array of strings, or a long (space separated) string.
36 | // In case of a long string, split it to an array of strings...
37 |
38 | if (typeof probeMatch.types === 'string' || probeMatch.types instanceof String) {
39 | probeMatch.types = probeMatch.types.trim().split(" ");
40 | }
41 |
42 | if (typeof probeMatch.scopes === 'string' || probeMatch.scopes instanceof String) {
43 | probeMatch.scopes = probeMatch.scopes.trim().split(" ");
44 | }
45 |
46 | if (typeof probeMatch.XAddrs === 'string' || probeMatch.XAddrs instanceof String) {
47 | probeMatch.XAddrs = probeMatch.XAddrs.trim().split(" ");
48 | }
49 |
50 | return probeMatch;
51 | }
52 |
53 | node.on("input", function(msg) {
54 | if (node.discovering) {
55 | console.info("Discovery request ignored, since other discovery is active");
56 | return;
57 | }
58 |
59 | node.status({fill:"yellow",shape:"dot",text:"discovering"});
60 | node.discovering = true;
61 |
62 | var options = {
63 | timeout: node.timeout, // Discovery should end after the specified timeout
64 | resolve: false // Return discovered devices as data objects, instead of Cam instances
65 | };
66 |
67 | // For every discovery we will need to remove all previous ('device' and 'error') listeners, and add new listeners.
68 | // See https://discourse.nodered.org/t/object-property-becomes-undefined/50647/2?u=bartbutenaers
69 | onvif.Discovery.removeAllListeners();
70 |
71 | // When a separate output message per device is required, then listen to every separate device being detected
72 | if (node.separate) {
73 | onvif.Discovery.on('device', function (result) {
74 | // Since the same input message will be resend for every Onvif device found, we need to clone the input message
75 | var outputMsg = RED.util.cloneMessage(msg);
76 | outputMsg.payload = simplifyResult(result);
77 |
78 | // Send a separate output message for every discovered OnVif-compliant IP device
79 | node.send(outputMsg);
80 | });
81 | }
82 |
83 | // The discovery must have an error handler to catch bad replies from the network (which cannot be parsed by this library)
84 | onvif.Discovery.on('error', function (err, xml) {
85 | node.error('Discovery error ' + err);
86 | });
87 |
88 | // Start discovery of the ONVIF network devices.
89 | // The callback function will be called only once, when the broadcast is finished (after the timeout).
90 | onvif.Discovery.probe(options, function(err, result) {
91 | if (err) {
92 | node.error(err.message);
93 | node.status({fill:"red",shape:"dot",text: "failed"});
94 | }
95 | else {
96 | node.status({fill:"green",shape:"dot",text: "completed (" + result.length + "x)"});
97 | }
98 |
99 | // When a single message needs to be send (containing all discovered devices)...
100 | if (!node.separate) {
101 | var devices = [];
102 |
103 | // Convert the array to an easy format
104 | for (var i = 0; i < result.length; i++) {
105 | devices.push(simplifyResult(result[i]));
106 | }
107 |
108 | // Send a single message, containing an array of ALL discovered OnVif-compliant IP devices
109 | var outputMsg = msg;
110 | outputMsg.payload = devices;
111 | node.send(outputMsg);
112 | }
113 |
114 | node.discovering = false;
115 | });
116 | });
117 |
118 | node.on('close', function(){
119 | node.status({});
120 | onvif.Discovery.removeAllListeners();
121 | node.discovering = false;
122 | });
123 | }
124 | RED.nodes.registerType("onvif-discovery",OnVifDiscoveryNode);
125 | }
126 |
127 |
--------------------------------------------------------------------------------
/onvif_events.html:
--------------------------------------------------------------------------------
1 |
13 |
37 |
38 |
61 |
62 |
69 |
70 |
--------------------------------------------------------------------------------
/onvif_events.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 | const utils = require('./utils');
20 |
21 | function OnVifEventsNode(config) {
22 | RED.nodes.createNode(this, config);
23 | this.action = config.action;
24 |
25 | var node = this;
26 |
27 | // Retrieve the config node, where the device is configured
28 | node.deviceConfig = RED.nodes.getNode(config.deviceConfig);
29 |
30 | if (node.deviceConfig) {
31 | node.listener = function(onvifStatus) {
32 | utils.setNodeStatus(node, 'event', onvifStatus);
33 |
34 | if (onvifStatus !== "connected" && node.eventListener) {
35 | // When the device isn't connected anymore, stop listening to events from the camera
36 | node.deviceConfig.cam.removeListener('events', node.eventListener);
37 | node.eventListener = null;
38 | }
39 | }
40 |
41 | // Start listening for Onvif config nodes status changes
42 | node.deviceConfig.addListener("onvif_status", node.listener);
43 |
44 | // Show the current Onvif config node status already
45 | utils.setNodeStatus(node, 'event', node.deviceConfig.onvifStatus);
46 |
47 | node.deviceConfig.initialize();
48 | }
49 |
50 | node.on("input", function(msg) {
51 | var newMsg = {};
52 |
53 | // Note: the node's config screen has no 'action' input field yet ...
54 | var action = node.action || msg.action;
55 |
56 | if (!action) {
57 | // When no action specified in the node, it should be specified in the msg.action
58 | node.error("No action specified (in node or msg)");
59 | return;
60 | }
61 |
62 | // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet)
63 | if (action !== "reconnect") {
64 | if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") {
65 | node.error("This node is not connected to a device");
66 | return;
67 | }
68 |
69 | if (!utils.hasService(node.deviceConfig.cam, 'event')) {
70 | node.error("The device has no support for an event service");
71 | return;
72 | }
73 | }
74 |
75 | // Seems that some Axis cams support pull point, although they return WSPullPointSupport 'false'
76 | /*if (!node.deviceConfig.cam.capabilities.events.WSPullPointSupport == true) {
77 | //console.warn('Ignoring input message since the device does not support pull point subscription');
78 | return;
79 | }*/
80 |
81 | newMsg.xaddr = this.deviceConfig.xaddress;
82 | newMsg.action = action;
83 |
84 | try {
85 | switch (action) {
86 | case "start":
87 | if (node.eventListener) {
88 | node.error("This node is already listening to device events");
89 | return;
90 | }
91 |
92 | // Overwrite the device status text
93 | node.status({fill:"green",shape:"dot",text:"listening"});
94 |
95 | node.eventListener = function(camMessage) {
96 | var sourceName = null;
97 | var sourceValue = null;
98 | var dataName = null;
99 | var dataValue = null;
100 |
101 | // Events have a Topic
102 | // Events have (optionally) a Source, a Key and Data fields
103 | // The Source,Key and Data fields can be single items or an array of items
104 | // The Source,Key and Data fields can be of type SimpleItem or a Complex Item
105 | // - Topic
106 | // - Message/Message/$
107 | // - Message/Message/Source...
108 | // - Message/Message/Key...
109 | // - Message/Message/Data/SimpleItem/[index]/$/name (array of items)
110 | // OR - Message/Message/Data/SimpleItem/$/name (single item)
111 | // - Message/Message/Data/SimpleItem/[index]/$/value (array of items)
112 | // OR - Message/Message/Data/SimpleItem/$/value (single item)
113 |
114 | var eventTopic = camMessage.topic._;
115 |
116 | // Strip the namespaces from the topic (e.g. tns1:MediaControl/tnsavg:ConfigurationUpdateAudioEncCfg)
117 | // Split on '/', then remove any namespace for each part, and at the end recombine parts that were split with '/'
118 | let parts = eventTopic.split('/');
119 | eventTopic = "";
120 | for (var index = 0; index < parts.length; index++) {
121 | var stringNoNamespace = parts[index].split(':').pop();
122 | if (eventTopic.length == 0) {
123 | eventTopic += stringNoNamespace;
124 | } else {
125 | eventTopic += '/' + stringNoNamespace;
126 | }
127 | }
128 |
129 | var outputMsg = {
130 | topic: eventTopic,
131 | time: camMessage.message.message.$.UtcTime,
132 | property: camMessage.message.message.$.PropertyOperation // Initialized, Deleted or Changed but missing/undefined on the Avigilon 4 channel encoder
133 | };
134 |
135 | // Only handle simpleItem
136 | // Only handle one 'source' item
137 | // Ignore the 'key' item (nothing I own produces it)
138 | // Handle all the 'Data' items
139 |
140 | // SOURCE (Name:Value)
141 | if (camMessage.message.message.source && camMessage.message.message.source.simpleItem) {
142 | if (Array.isArray(camMessage.message.message.source.simpleItem)) {
143 | // TODO : currently we only process the first event source item ...
144 | outputMsg.source = {
145 | name: camMessage.message.message.source.simpleItem[0].$.Name,
146 | value: camMessage.message.message.source.simpleItem[0].$.Value
147 | }
148 | }
149 | else {
150 | outputMsg.source = {
151 | name: camMessage.message.message.source.simpleItem.$.Name,
152 | value: camMessage.message.message.source.simpleItem.$.Value
153 | }
154 | }
155 | }
156 |
157 | //KEY
158 | if (camMessage.message.message.key) {
159 | outputMsg.key = camMessage.message.message.key;
160 | }
161 |
162 | // DATA (Name:Value)
163 | if (camMessage.message.message.data && camMessage.message.message.data.simpleItem) {
164 | if (Array.isArray(camMessage.message.message.data.simpleItem)) {
165 | outputMsg.data = [];
166 | for (var x = 0; x < camMessage.message.message.data.simpleItem.length; x++) {
167 | outputMsg.data.push({
168 | name: camMessage.message.message.data.simpleItem[x].$.Name,
169 | value: camMessage.message.message.data.simpleItem[x].$.Value
170 | })
171 | }
172 | }
173 | else {
174 | outputMsg.data = {
175 | name: camMessage.message.message.data.simpleItem.$.Name,
176 | value: camMessage.message.message.data.simpleItem.$.Value
177 | }
178 | }
179 | }
180 | else if (camMessage.message.message.data && camMessage.message.message.data.elementItem) {
181 | outputMsg.data = {
182 | dataName: 'elementItem',
183 | dataValue: JSON.stringify(camMessage.message.message.data.elementItem)
184 | }
185 | }
186 |
187 | // As soon as we get an event from the camera, we will send it to the output of this node
188 | node.send(outputMsg);
189 | }
190 |
191 | // Start listening to events from the camera
192 | node.deviceConfig.cam.on('event', node.eventListener);
193 | break;
194 | case "stop":
195 | if (!node.eventListener) {
196 | node.error("This node was not listening to events anyway");
197 | return;
198 | }
199 |
200 | // Stop listening to events from the camera
201 | node.deviceConfig.cam.removeListener('event', node.eventListener);
202 | node.eventListener = null;
203 |
204 | // Overwrite the device status text
205 | node.status({fill:"green",shape:"ring",text:"not listening"});
206 | break;
207 | case "getEventProperties":
208 | node.deviceConfig.cam.getEventProperties(function(err, date, xml) {
209 | if (!err) {
210 | var simplifiedDate = {};
211 |
212 | // Simplify the soap message to a compact message, by keeping only all relevant information
213 | function simplifyNode(node, simplifiedDateChild) {
214 | // loop over all the child nodes in this node
215 | for (const child in node) {
216 | switch (child) {
217 | case "$":
218 | // Continue to the next child in the list (same level)
219 | continue;
220 | case "messageDescription":
221 | // Collect the details that belong to the event
222 | var source = '';
223 | var date = '';
224 |
225 | if (node[child].source && node[child].source.simpleItemDescription) {
226 | simplifiedDateChild.source = node[child].source.simpleItemDescription.$;
227 | }
228 | if (node[child].data && node[child].data.simpleItemDescriptio) {
229 | simplifiedDateChild.date = node[child].data.simpleItemDescription.$;
230 | }
231 |
232 | return;
233 | default:
234 | // Decend recursively into the child node, looking for the messageDescription
235 | simplifiedDateChild[child] = {};
236 | simplifyNode(node[child], simplifiedDateChild[child]);
237 | }
238 | }
239 | }
240 | simplifyNode(date.topicSet, simplifiedDate)
241 | }
242 |
243 | utils.handleResult(node, err, simplifiedDate, null, newMsg);
244 | });
245 | break;
246 | case "getEventServiceCapabilities":
247 | node.deviceConfig.cam.getEventServiceCapabilities(function(err, date, xml) {
248 | utils.handleResult(node, err, date, xml, newMsg);
249 | });
250 | break;
251 | case "reconnect":
252 | node.deviceConfig.cam.connect(function(err) {
253 | utils.handleResult(node, err, "", null, newMsg);
254 | });
255 | break
256 | default:
257 | //node.status({fill:"red",shape:"dot",text: "unsupported action"});
258 | node.error("Action " + action + " is not supported");
259 | }
260 | }
261 | catch (exc) {
262 | node.error("Action " + action + " failed: " + exc);
263 | }
264 | });
265 |
266 | node.on("close",function() {
267 | if (node.listener) {
268 | node.deviceConfig.removeListener("onvif_status", node.listener);
269 | }
270 |
271 | // Stop listening to events from the camera
272 | if (node.eventListener) {
273 | node.deviceConfig.cam.removeListener('event', node.eventListener);
274 | node.eventListener = null;
275 | }
276 | });
277 | }
278 | RED.nodes.registerType("onvif-events",OnVifEventsNode);
279 | }
280 |
--------------------------------------------------------------------------------
/onvif_imaging.html:
--------------------------------------------------------------------------------
1 |
13 |
38 |
39 |
65 |
66 |
73 |
--------------------------------------------------------------------------------
/onvif_imaging.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 | const utils = require('./utils');
20 |
21 | function OnVifImagingNode(config) {
22 | RED.nodes.createNode(this, config);
23 | this.action = config.action;
24 |
25 | var node = this;
26 |
27 | // Retrieve the config node, where the device is configured
28 | node.deviceConfig = RED.nodes.getNode(config.deviceConfig);
29 |
30 | if (node.deviceConfig) {
31 | node.listener = function(onvifStatus) {
32 | utils.setNodeStatus(node, 'imaging', onvifStatus);
33 | }
34 |
35 | // Start listening for Onvif config nodes status changes
36 | node.deviceConfig.addListener("onvif_status", node.listener);
37 |
38 | // Show the current Onvif config node status already
39 | utils.setNodeStatus(node, 'imaging', node.deviceConfig.onvifStatus);
40 |
41 | node.deviceConfig.initialize();
42 | }
43 |
44 | node.on("input", function(msg) {
45 | var newMsg = {};
46 |
47 | var action = node.action || msg.action;
48 |
49 | if (!action) {
50 | // When no action specified in the node, it should be specified in the msg.action
51 | node.error("No action specified (in node or msg)");
52 | return;
53 | }
54 |
55 | // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet)
56 | if (action !== "reconnect") {
57 | if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") {
58 | node.error("This node is not connected to a device");
59 | return;
60 | }
61 |
62 | if (!utils.hasService(node.deviceConfig.cam, 'imaging')) {
63 | node.error("The device has no support for an imaging service");
64 | return;
65 | }
66 | }
67 |
68 | newMsg.xaddr = this.deviceConfig.xaddress;
69 | newMsg.action = action;
70 |
71 | try {
72 | switch (action) {
73 | case 'getImagingSettings':
74 | node.deviceConfig.cam.getImagingSettings(function(err, stream, xml) {
75 | utils.handleResult(node, err, stream, xml, newMsg);
76 | });
77 | break;
78 | case 'setImagingSettings':
79 | var options = {};
80 |
81 | if (msg.payload) {
82 | if (msg.payload.backlightCompensation) {
83 | options.backlightCompensation = msg.payload.backlightCompensation;
84 | }
85 |
86 | if (msg.payload.brightness) {
87 | options.brightness = msg.payload.brightness;
88 | }
89 |
90 | if (msg.payload.colorSaturation) {
91 | options.colorSaturation = msg.payload.colorSaturation;
92 | }
93 |
94 | if (msg.payload.contrast) {
95 | options.contrast = msg.payload.contrast;
96 | }
97 |
98 | if (msg.payload.exposure) {
99 | options.exposure = msg.payload.exposure;
100 | }
101 |
102 | if (msg.payload.focus) {
103 | options.focus = msg.payload.focus;
104 | }
105 |
106 | if (msg.payload.irCutFilter) {
107 | options.irCutFilter = msg.payload.irCutFilter;
108 | }
109 |
110 | if (msg.payload.sharpness) {
111 | options.sharpness = msg.payload.sharpness;
112 | }
113 |
114 | if (msg.payload.wideDynamicRange) {
115 | options.wideDynamicRange = msg.payload.wideDynamicRange;
116 | }
117 |
118 | if (msg.payload.whiteBalance) {
119 | options.whiteBalances = msg.payload.whiteBalance;
120 | }
121 |
122 | if (msg.payload.focus) {
123 | options.focus = msg.payload.focus;
124 | }
125 |
126 | if (msg.payload.extension) {
127 | options.extension = msg.payload.extension;
128 | }
129 | }
130 |
131 | // TODO we should be able to specify (in the options) the video source token, because by default the active video source will be updated.
132 |
133 | if (Object.keys(options).length === 0) {
134 | node.error('No image settings have been specified.');
135 | return;
136 | }
137 |
138 | node.deviceConfig.cam.setImagingSettings(options, function(err, stream, xml) {
139 | utils.handleResult(node, err, stream, xml, newMsg);
140 | });
141 | break;
142 | case 'getServiceCapabilities':
143 | node.deviceConfig.cam.getImagingServiceCapabilities(function(err, stream, xml) {
144 | utils.handleResult(node, err, stream, xml, newMsg);
145 | });
146 | break;
147 | case "reconnect":
148 | node.deviceConfig.cam.connect();
149 | break;
150 | default:
151 | //node.status({fill:"red",shape:"dot",text: "unsupported action"});
152 | node.error("Action " + action + " is not supported");
153 | }
154 | }
155 | catch (exc) {
156 | node.error("Action " + action + " failed: " + exc);
157 | }
158 | });
159 |
160 | node.on("close",function() {
161 | if (node.listener) {
162 | node.deviceConfig.removeListener("onvif_status", node.listener);
163 | }
164 | });
165 | }
166 | RED.nodes.registerType("onvif-imaging",OnVifImagingNode);
167 | }
168 |
--------------------------------------------------------------------------------
/onvif_media.html:
--------------------------------------------------------------------------------
1 |
13 |
146 |
147 |
230 |
231 |
240 |
241 |
--------------------------------------------------------------------------------
/onvif_media.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 | const url = require('url');
20 | const https = require('http');
21 | const utils = require('./utils');
22 |
23 | function OnVifMediaNode(config) {
24 | RED.nodes.createNode(this, config);
25 | this.action = config.action;
26 | this.profileToken = config.profileToken;
27 | this.profileName = config.profileName;
28 | this.videoEncoderConfigToken = config.videoEncoderConfigToken;
29 | this.protocol = config.protocol;
30 | this.stream = config.stream;
31 | this.snapshotUriMap = new Map();
32 |
33 | var node = this;
34 |
35 | // Retrieve the config node, where the device is configured
36 | node.deviceConfig = RED.nodes.getNode(config.deviceConfig);
37 |
38 | if (node.deviceConfig) {
39 | node.listener = function(onvifStatus) {
40 | utils.setNodeStatus(node, 'media', onvifStatus);
41 | }
42 |
43 | // Start listening for Onvif config nodes status changes
44 | node.deviceConfig.addListener("onvif_status", node.listener);
45 |
46 | // Show the current Onvif config node status already
47 | utils.setNodeStatus(node, 'media', node.deviceConfig.onvifStatus);
48 |
49 | node.deviceConfig.initialize();
50 | }
51 |
52 | function getSnapshot(uri, newMsg) {
53 | require('request').get(uri, {
54 | 'auth': {
55 | 'user': node.deviceConfig.credentials.user,
56 | 'pass': node.deviceConfig.credentials.password,
57 | 'sendImmediately': false
58 | }
59 | }).on('response', function(response) {
60 | if(response.statusCode === 200) {
61 | // Concatenate all the data chunks, to get a complete image (as a Buffer)
62 | newMsg.payload = response.body;
63 |
64 | newMsg.contentType = response.headers['content-type'];
65 | if(!newMsg.contentType) {
66 | newMsg.contentType = 'image/jpeg';
67 | }
68 |
69 | // Remember all body chunks and concatenate them into a single buffer when entire image received
70 | var bodyChunks = [];
71 | response.on("data", function (chunk) {
72 | bodyChunks.push(chunk);
73 | });
74 | response.on("end", function () {
75 | newMsg.payload = Buffer.concat(bodyChunks);
76 | node.send(newMsg);
77 | });
78 | }
79 | else {
80 | console.log(response.statusCode + ' ' + response.statusMessage);
81 | }
82 | }).on('error', function(err) {
83 | console.error(err.message)
84 | })
85 | }
86 |
87 | node.on("input", function(msg) {
88 | var newMsg = {};
89 |
90 | var action = node.action || msg.action;
91 |
92 | if (!action) {
93 | // When no action specified in the node, it should be specified in the msg.action
94 | node.error("No action specified (in node or msg)");
95 | return;
96 | }
97 |
98 | // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet)
99 | if (action !== "reconnect") {
100 | if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") {
101 | node.error("This node is not connected to a device");
102 | return;
103 | }
104 |
105 | if (!utils.hasService(node.deviceConfig.cam, 'media')) {
106 | node.error("The device has no support for a media service");
107 | return;
108 | }
109 | }
110 |
111 | var protocol = node.protocol || msg.protocol;
112 | var stream = node.stream || msg.stream;
113 | var profileToken = node.profileToken || msg.profileToken;
114 | var profileName = node.profileName || msg.profileName;
115 | var videoEncoderConfigToken = node.videoEncoderConfigToken || msg.videoEncoderConfigToken;
116 |
117 | // TODO check this only for actions where profileToken is needed
118 | // TODO when device disconnected, this gives "Cannot read property '$' of undefined" due to missing videosources...
119 | /*if (!profileToken) {
120 | if (!node.deviceConfig.cam.getActiveSources()) {
121 | console.warn('No default video source available');
122 | return;
123 | }
124 | }*/
125 |
126 | // To make things easier for a user, we will let the user specify profile names.
127 | // The corresponding profile token (which is required in Onvif to work with profiles) will be searched here...
128 | if (!profileToken && profileName) {
129 | profileToken = node.deviceConfig.getProfileTokenByName(profileName);
130 | }
131 |
132 | newMsg.xaddr = this.deviceConfig.xaddress;
133 | newMsg.action = action;
134 |
135 | try {
136 | switch (action) {
137 | case "getStreamUri":
138 | // All these 3 fields are optional. If not specified, the device will use the defaults.
139 | var options = {
140 | 'stream': stream,
141 | 'profileToken': profileToken,
142 | 'protocol': protocol
143 | };
144 |
145 | node.deviceConfig.cam.getStreamUri(options, function(err, stream, xml) {
146 | utils.handleResult(node, err, stream, xml, newMsg);
147 | });
148 | break;
149 | case "getSnapshotUri":
150 | // The profileToken is optional. When not specified, the device will use the default profile token.
151 | var options = {
152 | 'profileToken': profileToken
153 | };
154 |
155 | node.deviceConfig.cam.getSnapshotUri(options, function(err, stream, xml) {
156 | utils.handleResult(node, err, stream, xml, newMsg);
157 | });
158 | break;
159 | case "getVideoEncoderConfiguration":
160 | // If the videoEncoderConfigToken doesn't exist, the first element from the videoEncoderConfigurations array will be returned
161 | node.deviceConfig.cam.getVideoEncoderConfiguration(videoEncoderConfigToken, function(err, stream, xml) {
162 | utils.handleResult(node, err, stream, xml, newMsg);
163 | });
164 | break;
165 | case "getVideoEncoderConfigurations":
166 | node.deviceConfig.cam.getVideoEncoderConfigurations(function(err, stream, xml) {
167 | utils.handleResult(node, err, stream, xml, newMsg);
168 | });
169 | break;
170 | case "getVideoEncoderConfigurationOptions":
171 | // If the videoEncoderConfigToken doesn't exist, the first element from the videoEncoderConfigurations array will be used
172 | node.deviceConfig.cam.getVideoEncoderConfigurationOptions(videoEncoderConfigToken, function(err, stream, xml) {
173 | utils.handleResult(node, err, stream, xml, newMsg);
174 | });
175 | break;
176 | case "getProfiles":
177 | node.deviceConfig.cam.getProfiles(function(err, stream, xml) {
178 | utils.handleResult(node, err, stream, xml, newMsg);
179 | });
180 | break;
181 | case "getVideoSources":
182 | node.deviceConfig.cam.getVideoSources(function(err, stream, xml) {
183 | utils.handleResult(node, err, stream, xml, newMsg);
184 | });
185 | break;
186 | case "getVideoSourceConfigurations":
187 | node.deviceConfig.cam.getVideoSourceConfigurations(function(err, stream, xml) {
188 | utils.handleResult(node, err, stream, xml, newMsg);
189 | });
190 | break;
191 | case "getAudioSources":
192 | node.deviceConfig.cam.getAudioSources(function(err, stream, xml) {
193 | utils.handleResult(node, err, stream, xml, newMsg);
194 | });
195 | break;
196 | case "getAudioSourceConfigurations":
197 | node.deviceConfig.cam.getAudioSourceConfigurations(function(err, stream, xml) {
198 | utils.handleResult(node, err, stream, xml, newMsg);
199 | });
200 | break;
201 | case "getAudioEncoderConfigurations":
202 | node.deviceConfig.cam.getAudioEncoderConfigurations(function(err, stream, xml) {
203 | utils.handleResult(node, err, stream, xml, newMsg);
204 | });
205 | break;
206 | case "getAudioOutputs":
207 | node.deviceConfig.cam.getAudioOutputs(function(err, stream, xml) {
208 | utils.handleResult(node, err, stream, xml, newMsg);
209 | });
210 | break;
211 | case "getAudioOutputConfigurations":
212 | node.deviceConfig.cam.getAudioOutputConfigurations(function(err, stream, xml) {
213 | utils.handleResult(node, err, stream, xml, newMsg);
214 | });
215 | break;
216 | case "getOSDs":
217 | // If the videoEncoderConfigToken doesn't exist, all available OSD's will be requested
218 | node.deviceConfig.cam.getOSDs(videoEncoderConfigToken, function(err, stream, xml) {
219 | // Onvif OSD implementation is optional and most manufactures did not implement it.
220 | // This means we will arrive here often ...
221 | // Most camera's will offer another REST interface to check and configure OSD texts, but not based on OnVif.
222 | utils.handleResult(node, err, stream, xml, newMsg);
223 | });
224 | break;
225 | case "getSnapshot":
226 | var snapshotUri = node.snapshotUriMap.get(profileToken);
227 |
228 | // Get the uri once (and cache it), when it hasn't been retrieved yet
229 | if (!snapshotUri) {
230 | // When no token is specified, the camera will use the default token
231 | var options = {
232 | 'profileToken': profileToken
233 | };
234 |
235 | node.deviceConfig.cam.getSnapshotUri(options, function(err, stream, xml) {
236 | utils.handleResult(node, err, stream, xml, newMsg);
237 |
238 | if (!err) {
239 | // Cache the URL for the next time
240 | node.snapshotUriMap.set(profileToken, stream.uri);
241 | getSnapshot(stream.uri, newMsg);
242 | }
243 | });
244 | }
245 | else {
246 | getSnapshot(snapshotUri, newMsg);
247 | }
248 |
249 | break;
250 | case "createProfile":
251 | // The profile name is optional
252 | var options = {
253 | 'name': profileName,
254 | 'profileToken': profileToken // TODO This is not being used (the device generates a new token) ...
255 | };
256 |
257 | // Create an empty new deletable media profile
258 | node.deviceConfig.cam.createProfile(options, function(err, stream, xml) {
259 | utils.handleResult(node, err, stream, xml, newMsg);
260 | });
261 |
262 | break;
263 | case "deleteProfile":
264 | node.deviceConfig.cam.deleteProfile(profileToken, function(err, stream, xml) {
265 | utils.handleResult(node, err, stream, xml, newMsg);
266 | });
267 |
268 | break;
269 | /* TODO
270 | case "setVideoEncoderConfiguration":
271 | var options = {
272 | 'token': node.videoEncoderConfigToken,
273 | 'name': node.videoEncoderConfigName,
274 | //'useCount': ???,
275 | 'encoding': node.videoEncoderConfigEncoding, //JPEG | H264 | MPEG4
276 | 'resolution': {
277 | 'width': node.videoEncoderConfigWidth,
278 | 'height': node.videoEncoderConfigHeight,
279 | },
280 | //'quality': ?????,
281 | 'rateControl': {
282 | 'frameRateLimit': node.videoEncoderConfigFrameRateLimit,
283 | 'encodingInterval': node.videoEncoderConfigEncodingInterval,
284 | 'bitrateLimit': node.videoEncoderConfigBitrateLimit
285 | },
286 | 'MPEG4': {
287 | 'govLength': node.videoEncoderConfigMpeg4GovLength,
288 | 'profile': node.videoEncoderConfigMpeg4Profile // SP | ASP
289 | },
290 | 'H264': {
291 | 'govLength': node.videoEncoderConfigH264GovLength,
292 | 'profile': node.videoEncoderConfigH264Profile // Baseline | Main | Extended | High
293 | },
294 | 'multicast': {
295 | 'address': node.videoEncoderConfigMulticastAddress,
296 | 'type': node.videoEncoderConfigMulticastType, // IPv4 | IPv6
297 | 'IPv4Address': node.videoEncoderConfigMulticastIpv4Address,
298 | 'IPv6Address': node.videoEncoderConfigMulticastIpv6Address,
299 | 'port': node.videoEncoderConfigMulticastPort,
300 | 'TTL': node.videoEncoderConfigMulticastTtl,
301 | 'autoStart': node.videoEncoderConfigMulticastAutoStart,
302 | },
303 | 'sessionTimeout': node.videoEncoderConfigName
304 | };
305 |
306 | node.deviceConfig.cam.setVideoEncoderConfiguration(options, function(err, stream, xml) {
307 | utils.handleResult(node, err, stream, xml, newMsg);
308 | });;
309 |
310 | break; */
311 | case "reconnect":
312 | node.deviceConfig.cam.connect();
313 | break;
314 | default:
315 | //node.status({fill:"red",shape:"dot",text: "unsupported action"});
316 | node.error("Action " + action + " is not supported");
317 | }
318 | }
319 | catch (exc) {
320 | node.error("Action " + action + " failed: " + exc);
321 | }
322 | });
323 |
324 | node.on("close",function() {
325 | if (node.listener) {
326 | node.deviceConfig.removeListener("onvif_status", node.listener);
327 | }
328 | });
329 | }
330 | RED.nodes.registerType("onvif-media",OnVifMediaNode);
331 | }
332 |
--------------------------------------------------------------------------------
/onvif_ptz.html:
--------------------------------------------------------------------------------
1 |
13 |
187 |
188 |
293 |
294 |
311 |
--------------------------------------------------------------------------------
/onvif_ptz.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 | const utils = require('./utils');
20 |
21 | function OnVifPtzNode(config) {
22 | RED.nodes.createNode(this, config);
23 | this.panSpeed = parseFloat(config.panSpeed);
24 | this.tiltSpeed = parseFloat(config.tiltSpeed);
25 | this.zoomSpeed = parseFloat(config.zoomSpeed);
26 | this.panPosition = parseFloat(config.panPosition);
27 | this.tiltPosition = parseFloat(config.tiltPosition);
28 | this.zoomPosition = parseFloat(config.zoomPosition);
29 | this.panTranslation = parseFloat(config.panTranslation);
30 | this.tiltTranslation = parseFloat(config.tiltTranslation);
31 | this.zoomTranslation = parseFloat(config.zoomTranslation);
32 | this.time = parseInt(config.time);
33 | this.profileName = config.profileName;
34 | this.action = config.action;
35 | this.preset = config.preset;
36 | this.presetName = config.presetName;
37 | this.stopPanTilt = config.stopPanTilt;
38 | this.stopZoom = config.stopZoom
39 | this.configurationToken = config.configurationToken;
40 |
41 | var node = this;
42 |
43 | // Retrieve the config node, where the device is configured
44 | node.deviceConfig = RED.nodes.getNode(config.deviceConfig);
45 |
46 | if (node.deviceConfig) {
47 | node.listener = function(onvifStatus) {
48 | utils.setNodeStatus(node, 'ptz', onvifStatus);
49 | }
50 |
51 | // Start listening for Onvif config nodes status changes
52 | node.deviceConfig.addListener("onvif_status", node.listener);
53 |
54 | // Show the current Onvif config node status already
55 | utils.setNodeStatus(node, 'ptz', node.deviceConfig.onvifStatus);
56 |
57 | node.deviceConfig.initialize();
58 | }
59 |
60 | node.on("input", function(msg) {
61 | var newMsg = {};
62 |
63 | var profileToken = msg.profileToken;
64 | var profileName = node.profileName || msg.profileName;
65 |
66 | var panSpeed = node.panSpeed;
67 | var tiltSpeed = node.tiltSpeed;
68 | var zoomSpeed = node.zoomSpeed;
69 | var panPosition = node.panPosition;
70 | var tiltPosition = node.tiltPosition;
71 | var zoomPosition = node.zoomPosition;
72 | var panTranslation = node.panTranslation;
73 | var tiltTranslation = node.tiltTranslation;
74 | var zoomTranslation = node.zoomTranslation;
75 | var stopPanTilt = node.stopPanTilt;
76 | var stopZoom = node.stopZoom;
77 | var configurationToken = node.configurationToken;
78 |
79 | var action = node.action || msg.action;
80 |
81 | if (!action) {
82 | // When no action specified in the node, it should be specified in the msg.action
83 | node.error("No action specified (in node or msg)");
84 | return;
85 | }
86 |
87 | // Don't perform these checks when e.g. the device is currently disconnected (because then e.g. no capabilities are loaded yet)
88 | if (action !== "reconnect") {
89 | if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") {
90 | node.error("This node is not connected to a device");
91 | return;
92 | }
93 |
94 | if (!utils.hasService(node.deviceConfig.cam, 'ptz')) {
95 | node.error("The device has no support for a ptz service");
96 | return;
97 | }
98 | }
99 |
100 | var preset = node.preset || msg.preset;
101 | var presetName = node.presetName || msg.presetName;
102 |
103 | /*var currentProfile = node.device.getCurrentProfile();
104 |
105 | if (!currentProfile) {
106 | // Avoid errors during ptzMove, by ensuring that the device has a profile.
107 | // This can be a temporary issue at flow startup, since the device initialization above can take some time...
108 | console.warn('Ignoring input message because the OnVif device has no current profile');
109 | return;
110 | }*/
111 |
112 | // TODO check whether the node.token exists in the ONVIF device
113 |
114 | // Check whether a 'pan_speed' value is specified in the input message
115 | if (msg.hasOwnProperty('pan_speed') || msg.pan_speed < -1.0 || msg.pan_speed > 1.0) {
116 | if (isNaN(msg.pan_speed)) {
117 | node.error('The msg.pan_speed value should be a number between -1.0 and 1.0');
118 | }
119 | else {
120 | panSpeed = msg.pan_speed;
121 | }
122 | }
123 |
124 | // Check whether a 'tilt_speed' value is specified in the input message
125 | if (msg.hasOwnProperty('tilt_speed') || msg.tilt_speed < -1.0 || msg.tilt_speed > 1.0) {
126 | if (isNaN(msg.tilt_speed)) {
127 | node.error('The msg.tilt_speed value should be a number between -1.0 and 1.0');
128 | }
129 | else {
130 | tiltSpeed = msg.tilt_speed;
131 | }
132 | }
133 |
134 | // Check whether a 'zoom_speed' value is specified in the input message
135 | if (msg.hasOwnProperty('zoom_speed')) {
136 | if (isNaN(msg.zoom_speed) || msg.zoom_speed < -1.0 || msg.zoom_speed > 1.0) {
137 | node.error('The msg.zoom_speed value should be a number between -1.0 and 1.0');
138 | }
139 | else {
140 | zoomSpeed = msg.zoom_speed;
141 | }
142 | }
143 |
144 | // Check whether a 'pan_position' value is specified in the input message
145 | if (msg.hasOwnProperty('pan_position') /*TODO CHECK VIA PROFILE RANGE || msg.pan_position < -1.0 || msg.pan_position > 1.0*/) {
146 | if (isNaN(msg.pan_position)) {
147 | node.error('The msg.pan_position value should be a number between ?? and ??'); // TODO find boundaries in profile
148 | }
149 | else {
150 | panPosition = msg.pan_position;
151 | }
152 | }
153 |
154 | // Check whether a 'tilt_position' value is specified in the input message
155 | if (msg.hasOwnProperty('tilt_position') /*TODO CHECK VIA PROFILE RANGE || msg.tilt_position < -1.0 || msg.tilt_position > 1.0*/) {
156 | if (isNaN(msg.tilt_position)) {
157 | node.error('The msg.tilt_position value should be a number between ?? and ??'); // TODO find boundaries in profile
158 | }
159 | else {
160 | tiltPosition = msg.tilt_position;
161 | }
162 | }
163 |
164 | // Check whether a 'zoom_position' value is specified in the input message
165 | if (msg.hasOwnProperty('zoom_position') /*TODO CHECK VIA PROFILE RANGE || msg.zoom_position < -1.0 || msg.zoom_position > 1.0*/) {
166 | if (isNaN(msg.zoom_position)) {
167 | node.error('The msg.zoom_position value should be a number between ?? and ??'); // TODO find boundaries in profile
168 | }
169 | else {
170 | zoomPosition = msg.zoom_position;
171 | }
172 | }
173 |
174 | // Check whether a 'pan_translation' value is specified in the input message
175 | if (msg.hasOwnProperty('pan_translation') /*TODO CHECK VIA PROFILE RANGE || msg.pan_translation < -1.0 || msg.pan_translation > 1.0*/) {
176 | if (isNaN(msg.pan_translation)) {
177 | node.error('The msg.pan_translation value should be a number between ?? and ??'); // TODO find boundaries in profile
178 | }
179 | else {
180 | panTranslation = msg.pan_translation;
181 | }
182 | }
183 |
184 | // Check whether a 'tilt_translation' value is specified in the input message
185 | if (msg.hasOwnProperty('tilt_translation') /*TODO CHECK VIA PROFILE RANGE || msg.tilt_translation < -1.0 || msg.tilt_translation > 1.0*/) {
186 | if (isNaN(msg.tilt_translation)) {
187 | node.error('The msg.tilt_translation value should be a number between ?? and ??'); // TODO find boundaries in profile
188 | }
189 | else {
190 | tiltTranslation = msg.tilt_translation;
191 | }
192 | }
193 |
194 | // Check whether a 'zoom_translation' value is specified in the input message
195 | if (msg.hasOwnProperty('zoom_translation') /*TODO CHECK VIA PROFILE RANGE || msg.zoom_translation < -1.0 || msg.zoom_translation > 1.0*/) {
196 | if (isNaN(msg.zoom_translation)) {
197 | node.error('The msg.zoom_translation value should be a number between ?? and ??'); // TODO find boundaries in profile
198 | }
199 | else {
200 | zoomTranslation = msg.zoom_translation;
201 | }
202 | }
203 |
204 | // Make sure the values are between -1.0 and 1.0
205 | panSpeed = Math.min(Math.max(panSpeed, -1.0), 1.0);
206 | tiltSpeed = Math.min(Math.max(tiltSpeed, -1.0), 1.0);
207 | zoomSpeed = Math.min(Math.max(zoomSpeed, -1.0), 1.0);
208 |
209 | // To make things easier for a user, we will let the user specify profile names.
210 | // The corresponding profile token (which is required in Onvif to work with profiles) will be searched here...
211 | if (!profileToken && profileName) {
212 | profileToken = node.deviceConfig.getProfileTokenByName(profileName);
213 | }
214 |
215 | newMsg.xaddr = this.deviceConfig.xaddress;
216 | newMsg.action = action;
217 |
218 | try {
219 | switch (action) {
220 | case "continuousMove":
221 | var options = {
222 | 'profileToken': profileToken,
223 | 'x': panSpeed,
224 | 'y': tiltSpeed,
225 | 'zoom': zoomSpeed,
226 | 'timeout': node.time
227 | };
228 |
229 | // Move the camera with the specified speed(s) and during the specified time
230 | node.deviceConfig.cam.continuousMove(options, function(err, stream, xml) {
231 | utils.handleResult(node, err, stream, xml, newMsg);
232 | });
233 |
234 | break;
235 | case "absoluteMove":
236 | // TODO The 'position' value range is specified in the profile
237 | var options = {
238 | 'profileToken': profileToken,
239 | 'x': panPosition,
240 | 'y': tiltPosition,
241 | 'zoom': zoomPosition,
242 | 'speed': {
243 | 'x': panSpeed,
244 | 'y': tiltSpeed,
245 | 'zoom': zoomSpeed
246 | }
247 | };
248 |
249 | // Move the camera with the specified speed(s) and during the specified time
250 | node.deviceConfig.cam.absoluteMove(options, function(err, stream, xml) {
251 | utils.handleResult(node, err, stream, xml, newMsg);
252 | });
253 |
254 | break;
255 | case "relativeMove":
256 | var options = {
257 | 'profileToken': profileToken,
258 | 'x': panTranslation,
259 | 'y': tiltTranslation,
260 | 'zoom': zoomTranslation,
261 | 'speed': {
262 | 'x': panSpeed,
263 | 'y': tiltSpeed,
264 | 'zoom': zoomSpeed
265 | }
266 | };
267 |
268 | // Move the camera with the specified speed(s) and during the specified time
269 | node.deviceConfig.cam.relativeMove(options, function(err, stream, xml) {
270 | utils.handleResult(node, err, stream, xml, newMsg);
271 | });
272 |
273 | break;
274 | case "gotoHomePosition":
275 | var options = {
276 | 'profileToken': profileToken,
277 | 'speed': {
278 | 'x': panSpeed,
279 | 'y': tiltSpeed,
280 | 'zoom': zoomSpeed
281 | }
282 | };
283 |
284 | // Let the camera go to the home position.
285 | // Make sure a home position is set in advance, otherwise you get a 'No HomePosition' error.
286 | node.deviceConfig.cam.gotoHomePosition(options, function(err, stream, xml) {
287 | utils.handleResult(node, err, stream, xml, newMsg);
288 | });
289 |
290 | break;
291 | case "setHomePosition":
292 | var options = {
293 | 'profileToken': profileToken,
294 | };
295 |
296 | // Set the CURRENT camera position as the home position
297 | node.deviceConfig.cam.setHomePosition(options, function(err, stream, xml) {
298 | utils.handleResult(node, err, stream, xml, newMsg);
299 | });
300 |
301 | break;
302 | case "getPresets":
303 | var options = {
304 | 'profileToken': profileToken,
305 | };
306 |
307 | node.deviceConfig.cam.getPresets(options, function(err, stream, xml) {
308 | utils.handleResult(node, err, stream, xml, newMsg);
309 | });
310 |
311 | break;
312 | case "setPreset":
313 | var options = {
314 | 'profileToken': profileToken
315 | };
316 |
317 | // We will not ask the user to specify the preset token in the input message, to avoid that the preset tokens will
318 | // need to be stored somewhere in the Node-Red flow. Instead the user can specify a preset NAME, and we will lookup
319 | // to which existing preset token this name corresponds...
320 | node.deviceConfig.cam.getPresets(options, function(err, stream, xml) {
321 | // Get the preset token of the specified preset name
322 | var presetToken = stream[presetName];
323 |
324 | // When the preset token exists, we are dealing with an already existing preset.
325 | // Then pass the preset token to the device, so it will UPDATE the existing preset.
326 | // When the preset token is not passed to the device, the device will create a NEW preset (and a new preset token).
327 | if (presetToken) {
328 | options.presetToken = presetToken;
329 | }
330 |
331 | options.presetName = presetName;
332 |
333 | // Create/update the preset, based on the preset token. The device will save the current camera parameters
334 | // (XY coordinates, zoom level and a focus adjustment) so that the device can move afterwards to that saved
335 | // preset position (via the GotoPreset action).
336 | node.deviceConfig.cam.setPreset(options, function(err, stream, xml) {
337 | // The response contains the PresetToken which uniquely identifies the Preset.
338 | // The operation will fail when the PTZ device is moving during the SetPreset operation.
339 | utils.handleResult(node, err, stream, xml, newMsg);
340 | });
341 | });
342 |
343 | break;
344 | case "removePreset":
345 | var options = {
346 | 'profileToken': profileToken
347 | };
348 |
349 | // We will not ask the user to specify the preset token in the input message, to avoid that the preset tokens will
350 | // need to be stored somewhere in the Node-Red flow. Instead the user can specify a preset NAME, and we will lookup
351 | // to which existing preset token this name corresponds...
352 | node.deviceConfig.cam.getPresets(options, function(err, stream, xml) {
353 | // Get the preset token of the specified preset name
354 | var presetToken = stream[presetName];
355 |
356 | // When the preset token exists, we are dealing with an already existing preset.
357 | // Then pass the preset token to the device, so it will UPDATE the existing preset.
358 | // When the preset token is not passed to the device, the device will create a NEW preset (and a new preset token).
359 | if (presetToken) {
360 | options.presetToken = presetToken;
361 | }
362 |
363 | options.presetName = presetName;
364 |
365 | // Create/update the preset, based on the preset token. The device will save the current camera parameters
366 | // (XY coordinates, zoom level and a focus adjustment) so that the device can move afterwards to that saved
367 | // preset position (via the GotoPreset action).
368 | node.deviceConfig.cam.removePreset(options, function(err, stream, xml) {
369 | utils.handleResult(node, err, stream, xml, newMsg);
370 | });
371 | });
372 |
373 | break;
374 | case "gotoPreset":
375 | var options = {
376 | 'profileToken': profileToken
377 | };
378 |
379 | // We will not ask the user to specify the preset token in the input message, to avoid that the preset tokens will
380 | // need to be stored somewhere in the Node-Red flow. Instead the user can specify a preset NAME, and we will lookup
381 | // to which existing preset token this name corresponds...
382 | node.deviceConfig.cam.getPresets(options, function(err, stream, xml) {
383 | // Get the preset token of the specified preset name
384 | var preset = stream[presetName];
385 |
386 | // When the preset token doesn't exists, we cannot goto it ...
387 | if (!preset) {
388 | console.warn("Preset token with name " + presetName + " does not exist.");
389 | return;
390 | }
391 |
392 | options.preset = preset;
393 |
394 | // TODO enkel doorgeven indien gedefinieerd.
395 | options.speed = {
396 | x: panSpeed,
397 | y: tiltSpeed,
398 | z: zoomSpeed
399 | };
400 |
401 | // Create/update the preset, based on the preset token. The device will save the current camera parameters
402 | // (XY coordinates, zoom level and a focus adjustment) so that the device can move afterwards to that saved
403 | // preset position (via the GotoPreset action).
404 | node.deviceConfig.cam.gotoPreset(options, function(err, stream, xml) {
405 | utils.handleResult(node, err, stream, xml, newMsg);
406 | });
407 | });
408 |
409 | break;
410 | case "getNodes":
411 | node.deviceConfig.cam.getNodes(function(err, stream, xml) {
412 | utils.handleResult(node, err, stream, xml, newMsg);
413 | });
414 |
415 | break;
416 | case "getConfigurations":
417 | node.deviceConfig.cam.getConfigurations(function(err, stream, xml) {
418 | utils.handleResult(node, err, stream, xml, newMsg);
419 | });
420 |
421 | break;
422 | case "getConfigurationOptions":
423 | configurationToken = 'PtzConf1'; // TODO make adjustable (is reeds voorzien op config screen, maar nog niet via input message)
424 |
425 | // getConfigurationOptions will result in an uncaught exception when getConfigurations hasn't been called in advance...
426 | if (!node.deviceConfig.cam.configurations) {
427 | node.deviceConfig.cam.getConfigurations(function(err, stream, xml) {
428 | node.deviceConfig.cam.getConfigurationOptions(configurationToken, function(err, stream, xml) {
429 | utils.handleResult(node, err, stream, xml, newMsg);
430 | });
431 | });
432 | }
433 | else {
434 | node.deviceConfig.cam.getConfigurationOptions(configurationToken, function(err, stream, xml) {
435 | utils.handleResult(node, err, stream, xml, newMsg);
436 | });
437 | }
438 |
439 | break;
440 | case "getStatus":
441 | // TODO error in onvif library due to missing zoom in Panasonic cam (see https://github.com/agsh/onvif/issues/103)
442 |
443 | var options = {
444 | 'profileToken': profileToken,
445 | };
446 |
447 | node.deviceConfig.cam.getStatus(options, function(err, stream, xml) {
448 | utils.handleResult(node, err, stream, xml, newMsg);
449 | });
450 |
451 | break;
452 | case "stop":
453 | var options = {
454 | 'profileToken': profileToken,
455 | 'panTilt': node.stopPanTilt,
456 | 'zoom': node.stopZoom
457 | };
458 |
459 | node.deviceConfig.cam.stop(options, function(err, stream, xml) {
460 | utils.handleResult(node, err, stream, xml, newMsg);
461 | });
462 |
463 | break;
464 | case "reconnect":
465 | node.deviceConfig.cam.connect();
466 | break;
467 | default:
468 | //node.status({fill:"red",shape:"dot",text: "unsupported action"});
469 | node.error("Action " + action + " is not supported");
470 | }
471 | }
472 | catch (exc) {
473 | node.error("Action " + action + " failed: " + exc);
474 | }
475 | });
476 |
477 | node.on("close",function() {
478 | if (node.listener) {
479 | node.deviceConfig.removeListener("onvif_status", node.listener);
480 | }
481 | });
482 | }
483 | RED.nodes.registerType("onvif-ptz",OnVifPtzNode);
484 | }
485 |
--------------------------------------------------------------------------------
/onvif_recording.html:
--------------------------------------------------------------------------------
1 |
13 |
36 |
37 |
49 |
50 |
55 |
--------------------------------------------------------------------------------
/onvif_recording.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | module.exports = function(RED) {
17 | var settings = RED.settings;
18 | const onvif = require('onvif');
19 | const utils = require('./utils');
20 |
21 | function OnVifRecordingNode(config) {
22 | RED.nodes.createNode(this, config);
23 |
24 | var node = this;
25 |
26 | // Retrieve the config node, where the device is configured
27 | node.deviceConfig = RED.nodes.getNode(config.deviceConfig);
28 |
29 | if (node.deviceConfig) {
30 | node.listener = function(onvifStatus) {
31 | utils.setNodeStatus(node, 'recording', onvifStatus);
32 | }
33 |
34 | // Start listening for Onvif config nodes status changes
35 | node.deviceConfig.addListener("onvif_status", node.listener);
36 |
37 | // Show the current Onvif config node status already
38 | utils.setNodeStatus(node, 'recording', node.deviceConfig.onvifStatus);
39 |
40 | node.deviceConfig.initialize();
41 | }
42 |
43 | node.on("input", function(msg) {
44 | var newMsg = {};
45 |
46 | if (!node.deviceConfig || node.deviceConfig.onvifStatus != "connected") {
47 | node.error("This node is not connected to a device");
48 | return;
49 | }
50 |
51 | if (!utils.hasService(node.deviceConfig.cam, 'recording')) {
52 | node.error("The device does no support for a recording service");
53 | return;
54 | }
55 |
56 | newMsg.xaddr = this.deviceConfig.xaddress;
57 |
58 | // TODO deze function call geeft steeds "Error: Wrong ONVIF SOAP response"
59 | // Kan het zijn dat panasonic geen recording service heeft ???
60 | node.deviceConfig.cam.getRecordings(function(err, stream, xml) {
61 | utils.handleResult(node, err, stream, xml, newMsg);
62 | });
63 | });
64 |
65 | node.on("close",function() {
66 | if (node.listener) {
67 | node.deviceConfig.removeListener("onvif_status", node.listener);
68 | }
69 | });
70 | }
71 | RED.nodes.registerType("onvif-recording",OnVifRecordingNode);
72 | }
73 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "node-red-contrib-onvif-nodes",
3 | "version" : "0.0.1-beta.16",
4 | "description" : "Node Red nodes for communicating with OnVif compliant IP devices",
5 | "dependencies": {
6 | "onvif": "^0.6.7",
7 | "request": ">= 2.88.2"
8 | },
9 | "author": {
10 | "name": "Bart Butenaers"
11 | },
12 | "license": "Apache-2.0",
13 | "keywords": [ "onvif", "ip", "camera", "snapshot", "ptz", "rtsp", "stream", "surveillance" ],
14 | "bugs": {
15 | "url": "https://github.com/bartbutenaers/node-red-contrib-onvif-nodes/issues"
16 | },
17 | "homepage": "https://github.com/bartbutenaers/node-red-contrib-onvif-nodes",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/bartbutenaers/node-red-contrib-onvif-nodes.git"
21 | },
22 | "node-red" : {
23 | "nodes": {
24 | "onvif-config": "onvif_config.js",
25 | "onvif-device": "onvif_device.js",
26 | "onvif-discovery": "onvif_discovery.js",
27 | "onvif-imaging": "onvif_imaging.js",
28 | "onvif-media": "onvif_media.js",
29 | "onvif-ptz": "onvif_ptz.js",
30 | "onvif-recording": "onvif_recording.js",
31 | "onvif-events": "onvif_events.js"
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Bart Butenaers
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 | exports.setNodeStatus = function(node, serviceName, onvifStatus) {
17 | switch(onvifStatus) {
18 | case "unconfigured":
19 | node.status({fill:"red",shape:"ring",text:onvifStatus});
20 | break;
21 | case "initializing":
22 | node.status({fill:"yellow",shape:"dot",text:onvifStatus});
23 | break;
24 | case "disconnected":
25 | node.status({fill:"red",shape:"ring",text:onvifStatus});
26 | break;
27 | case "connected":
28 | // Starting from agsh/onvif version 0.6.5, the cam.capabilities have become obsolete.
29 | // Use services instead. See pull request https://github.com/agsh/onvif/pull/155
30 | // When the cam doesn't offer services, the agsh/onvif library has a fallback to the obsolete capabilities.
31 | if (node.deviceConfig.cam.services || node.deviceConfig.cam.capabilities) {
32 | // When connected to the Onvif device, the status depends on whether the device supports the specified service
33 | if (exports.hasService(node.deviceConfig.cam, serviceName)) {
34 | node.status({fill:"green",shape:"dot",text:onvifStatus});
35 | }
36 | else {
37 | node.status({fill:"red",shape:"ring",text:"unsupported"});
38 | }
39 | }
40 | else {
41 | node.status({fill:"red",shape:"ring",text:"no services"});
42 | }
43 | break;
44 | case "":
45 | node.status({});
46 | break;
47 | default:
48 | node.status({fill:"red",shape:"ring",text:"unknown"});
49 | }
50 | };
51 |
52 | exports.handleResult = function(node, err, date, xml, newMsg) {
53 | if (err) {
54 | node.error(err.message);
55 |
56 | var lowercase = err.message.toLowerCase();
57 |
58 | // Sometimes the OnVif device responds with errors like "Method Not Found", "Action Not Implemented", ...
59 | // In that case we will show an error indicating that the action is not supported by the device.
60 | // WE WON'T SET A TEMPORARY NODE STATUS, BECAUSE OTHERWISE WE SHOULD SHOW THE ORIGINAL STATUS AGAIN AFTER SOME TIME
61 | // (WHICH IS NOT RELEVANT)/ see https://github.com/bartbutenaers/node-red-contrib-onvif-nodes/issues/12
62 | //if (lowercase.includes("not found") || lowercase.includes("not implemented")) {
63 | // node.status({fill:"red",shape:"dot",text: "unsupported action"});
64 | //}
65 | //else {
66 | // node.status({fill:"red",shape:"dot",text: "failed"});
67 | //}
68 |
69 | // When a reconnect action fails, then the status needs to become 'disconnected' (because that is no temporary status unlike the others)
70 | if (newMsg.action == "reconnect") {
71 | node.status({fill:"red",shape:"dot",text: "disconnected"});
72 | }
73 | }
74 | else {
75 | if (newMsg) {
76 | newMsg.payload = date;
77 | node.send(newMsg);
78 | }
79 |
80 | // When a reconnect action succeeds, then the status needs to become 'connected' (because that is no temporary status unlike the others)
81 | if (newMsg.action == "reconnect") {
82 | node.status({fill:"blue",shape:"dot",text: "connected"});
83 | }
84 | }
85 | }
86 |
87 | exports.hasService = function (cam, serviceName) {
88 | if (cam.services) {
89 | // Check whether there is a service available, whose XAddr contains the specified service name
90 | var hasService = cam.services.some(function (service) {
91 | return service.XAddr && service.XAddr.toLowerCase().includes(serviceName.toLowerCase());
92 | });
93 |
94 | if (!hasService) {
95 | // Check whether there is a service available, whose namespace contains the specified service name
96 | hasService = cam.services.some(function (service) {
97 | return service.namespace && service.namespace.toLowerCase().includes(serviceName.toLowerCase());
98 | });
99 | }
100 |
101 | return hasService;
102 | }
103 | else if (cam.capabilities) {
104 | // When the cam doesn't offer services, the agsh/onvif library has a fallback to the obsolete capabilities
105 | return Object.keys(cam.capabilities).some(function (capabilityName) {
106 | var service = cam.capabilities[capabilityName];
107 | return service.XAddr && capabilityName.toLowerCase().includes(serviceName.toLowerCase());
108 | });
109 | }
110 | else {
111 | return false;
112 | }
113 | }
114 |
--------------------------------------------------------------------------------