├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── RPOS.njsproj ├── RPOS.sln ├── RPOS_PanTiltHAT.jpg ├── bin └── rtspServer ├── cpp ├── Makefile ├── rtspServer ├── rtspServer.cpp └── rtspServer.o ├── ffserver_mac.conf ├── gulpfile.js ├── lib ├── PTZDriver.ts ├── SoapService.ts ├── camera.ts ├── extension.ts ├── utils.ts └── v4l2ctl.ts ├── package-lock.json ├── package.json ├── python ├── gst-rtsp-launch.py ├── gst-rtsp-launch.sh ├── run-filesrc.sh └── run-testsrc.sh ├── raspberry_pi_missile_launcher.jpg ├── rpos.d.ts ├── rpos.service ├── rpos.ts ├── rposConfig.sample-picam.json ├── rposConfig.sample-proxy.json ├── rposConfig.sample-testfile.json ├── rposConfig.sample-testimage.json ├── rposConfig.sample-usbcam.json ├── sample_image.jpg ├── services ├── device_service.ts ├── discovery_service.ts ├── imaging_service.ts ├── media_service.ts ├── ptz_service.ts └── stubs │ ├── device_service.js │ ├── imaging_service.js │ ├── media2_service.js │ ├── media_service.js │ └── ptz_service.js ├── setup_v4l2rtspserver.sh ├── tsconfig.json ├── typings.json ├── views └── camera.ntl ├── web └── snapshot.jpg └── wsdl ├── device_service.wsdl ├── docs.oasis-open.org.wsn.b-2.xsd ├── docs.oasis-open.org.wsn.t-1.xsd ├── docs.oasis-open.org.wsrf.bf-2.xsd ├── imaging_service.wsdl ├── media2_service.wsdl ├── media_service.wsdl ├── ptz_service.wsdl ├── www.onvif.org.ver10.device.wsdl ├── www.onvif.org.ver10.media.wsdl ├── www.onvif.org.ver10.schema.xsd ├── www.onvif.org.ver20.imaging.wsdl ├── www.onvif.org.ver20.media.wsdl ├── www.onvif.org.ver20.ptz.wsdl ├── www.w3.org.2001.xml.xsd ├── www.w3.org.2003.05.soap-envelope.xsd ├── www.w3.org.2004.08.xop.include.xsd ├── www.w3.org.2005.05.xmlmime.xsd └── www.w3.org.2005.08.addressing.ws-addr.xsd /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Protect User Setting in rposConfig.json (User can start with rposConfig.sample-*.json) 5 | rposConfig.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | build/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | 31 | ###### Visual Studio Code User Setting 32 | ###### .vscode/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | 88 | # TFS 2012 Local Workspace 89 | $tf/ 90 | 91 | # Guidance Automation Toolkit 92 | *.gpState 93 | 94 | # ReSharper is a .NET coding add-in 95 | _ReSharper*/ 96 | *.[Rr]e[Ss]harper 97 | *.DotSettings.user 98 | 99 | # JustCode is a .NET coding addin-in 100 | .JustCode 101 | 102 | # TeamCity is a build add-in 103 | _TeamCity* 104 | 105 | # DotCover is a Code Coverage Tool 106 | *.dotCover 107 | 108 | # NCrunch 109 | _NCrunch_* 110 | .*crunch*.local.xml 111 | 112 | # MightyMoose 113 | *.mm.* 114 | AutoTest.Net/ 115 | 116 | # Web workbench (sass) 117 | .sass-cache/ 118 | 119 | # Installshield output folder 120 | [Ee]xpress/ 121 | 122 | # DocProject is a documentation generator add-in 123 | DocProject/buildhelp/ 124 | DocProject/Help/*.HxT 125 | DocProject/Help/*.HxC 126 | DocProject/Help/*.hhc 127 | DocProject/Help/*.hhk 128 | DocProject/Help/*.hhp 129 | DocProject/Help/Html2 130 | DocProject/Help/html 131 | 132 | # Click-Once directory 133 | publish/ 134 | 135 | # Publish Web Output 136 | *.[Pp]ublish.xml 137 | *.azurePubxml 138 | # TODO: Comment the next line if you want to checkin your web deploy settings 139 | # but database connection strings (with potential passwords) will be unencrypted 140 | *.pubxml 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Others 160 | *.[Cc]ache 161 | ClientBin/ 162 | [Ss]tyle[Cc]op.* 163 | ~$* 164 | *~ 165 | *.dbmdl 166 | *.dbproj.schemaview 167 | *.pfx 168 | *.publishsettings 169 | node_modules/ 170 | bower_components/ 171 | 172 | # RIA/Silverlight projects 173 | Generated_Code/ 174 | 175 | # Backup & report files from converting an old project file 176 | # to a newer Visual Studio version. Backup files are not needed, 177 | # because we have git ;-) 178 | _UpgradeReport_Files/ 179 | Backup*/ 180 | UpgradeLog*.XML 181 | UpgradeLog*.htm 182 | 183 | # SQL Server files 184 | *.mdf 185 | *.ldf 186 | 187 | # Business Intelligence projects 188 | *.rdl.data 189 | *.bim.layout 190 | *.bim_*.settings 191 | 192 | # Microsoft Fakes 193 | FakesAssemblies/ 194 | 195 | # Node.js Tools for Visual Studio 196 | .ntvs_analysis.dat 197 | 198 | # Visual Studio 6 build log 199 | *.plg 200 | 201 | # Visual Studio 6 workspace options file 202 | *.opt 203 | 204 | typings/ 205 | !typings/rpos/rpos.d.ts 206 | v4l2ctl.json 207 | release/ 208 | release.zip 209 | rpos.js 210 | rpos.js.map 211 | lib/**/*.js 212 | lib/**/*.js.map 213 | services/*.js 214 | services/*.js.map 215 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | // changed prelaunch task from "tsc: build - tsconfig.json" to "npx gulp" 6 | // added Out folder to tsconfig.json 7 | "version": "0.2.0", 8 | "configurations": [ 9 | { 10 | "type": "node", 11 | "request": "launch", 12 | "name": "Run RPOS", 13 | "program": "${workspaceFolder}/rpos.js", 14 | "preLaunchTask": "tsc: build - tsconfig.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "node_modules/typescript/lib", 4 | "editor.folding": true 5 | } 6 | 7 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "problemMatcher": [ 8 | "$tsc" 9 | ], 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "label": "tsc: build - tsconfig.json" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeroen Versteege 4 | Copyright (c) 2016,2017,2018 Roger Hardiman 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpos 2 | 3 | Node.js based ONVIF Camera/NVT software that turns a Raspberry Pi, Windows, Linux or Mac computer into an ONVIF Camera and RTSP Server. It implements the key parts of Profile S and Profile T (http://www.onvif.org). It has special support for the Raspberry Pi Camera and Pimoroni Pan-Tilt HAT. 4 | 5 | RPOS won an award in the 2018 ONVIF Open Source Challenge competition. 6 | 7 | ## History and Contributors 8 | 9 | The initial goal (by @BreeeZe) was to provide a ONVIF Media service which is compatible with Synology Surveillance Station to allow the Raspberry Pi to be used as a surveillance camera without the need for adding any custom camera files to your Synology NAS. 10 | 11 | This version uses a patched version of the "node-soap" v0.80 library (https://github.com/vpulim/node-soap/releases/tag/v0.8.0) located @ https://github.com/BreeeZe/node-soap 12 | 13 | The next goal (by @RogerHardiman) was to implement more of the ONVIF standard so that RPOS could be used with a wide range of CCTV systems and with ONVIF Device Manager and ONVIF Device Tool. Additional ONVIF Soap commands were added including the PTZ Service with backend drivers that control the Raspberry Pi Pan-Tit HAT or emit various RS485 based PTZ protocols including Pelco D and Sony Visca. 14 | 15 | Oliver Schwaneberg added GStreamer gst-rtsp-server support as third RTSP Server option. 16 | 17 | Casper Meijn added Relative PTZ support 18 | 19 | Johnny Wan added some USB Camera support for GStreamer RTSP server. 20 | 21 | If I've forgotten to put you in the list, please post an Issue Report and I can add you in. 22 | 23 | ## Features: 24 | 25 | - Implements the ONVIF Standard for a CCTV Camera and NVT (Network Video Transmitter) 26 | - Streams H264 video over RTSP from the Official Raspberry Pi camera (the one that uses the ribbon cable) and some USB cameras 27 | - Uses hardware H264 encoding using the GPU on the Pi 28 | - Implements Camera control (resolution and framerate) through ONVIF 29 | - Can set other camera options through a web interface. 30 | - Discoverable (WS-Discovery) on Pi/Linux by CCTV Viewing Software 31 | - Works with ONVIF Device Manager (Windows) and ONVIF Device Tool (Linux) 32 | - Works with other CCTV Viewing Software that implements the ONVIF standard including Antrica Decoder, Avigilon Control Centre, Bosch BVMS, Milestone, ISpy (Opensource), BenSoft SecuritySpy (Mac), IndigoVision Control Centre and Genetec Security Centre (add camera as ONVIF-BASIC mode) 33 | - Implements ONVIF Authentication 34 | - Implements Absolute, Relative and Continuous PTZ and controls the Pimononi Raspberry Pi Pan-Tilt HAT 35 | - Can also use the Waveshare Pan-Tilt HAT with a custom driver for the PWM chip used but be aware the servos in their kit do not fit so we recommend the Pimoroni model 36 | - Also converts ONVIF PTZ commands into Pelco D and Visca telemetry on a serial port (UART) for other Pan/Tilt platforms (ie a PTZ Proxy or PTZ Protocol Converter) 37 | - Can reference other RTSP servers, which in turn can pull in the video via RTSP, other ONVIF sources, Desktop Capture, MJPEG allowing RPOS to be a Video Stream Proxy 38 | - Implements Imaging service Brightness and Focus commands (for Profile T) 39 | - Implements Relay (digital output) function 40 | - Supports Unicast (UDP/TDP) and Multicast using mpromonet's RTSP server 41 | - Supports Unicast (UDP/TCP) RTSP using GStreamer 42 | - Works as a PTZ Proxy 43 | - Also runs on Mac, Windows and other Linux machines but you need to supply your own RTSP server. An example to use ffserver on the Mac is included. 44 | - USB cameras supported via the GStreamer RTSP server with limited parameters available. Tested with JPEG USB HD camera 45 | 46 | ![Picture of RPOS running on a Pi with the PanTiltHAT and Pi Camera](RPOS_PanTiltHAT.jpg?raw=true "PanTiltHAT") 47 | Picture of RPOS running on a Pi 3 with the PiMoroni PanTiltHAT and Official Pi Camera 48 | 49 | ## How to Install on a Raspberry Pi: 50 | 51 | ### STEP 1 - CONFIG RASPBERRY PI 52 | Windows/Mac/Linux users can skip this step 53 | 54 | #### STEP 1.a - ENABLE RASPBERRY PI CAMERA 55 | (For Raspberry PI camera) 56 | Pi users can run ‘raspi-config’ and enable the camera and reboot 57 | 58 | #### STEP 1.b - ADJUST GPU MEMORY 59 | (For USB camera, and need to use hardware encoding acceleration) 60 | Add ‘gpu_mem=128’ in /boot/bootconf.txt and reboot 61 | 62 | ### STEP 2 - INSTALL NODEJS AND NPM 63 | 64 | [This step was tested in Raspberry Pi OS from June 2021. Older Pis may need some manual steps] 65 | On the Pi you can install nodejs (ver10) and npm (5.8.0) with this command 66 | ``` 67 | sudo apt install nodejs npm 68 | ``` 69 | Next we install 'n', a node version manager and install Node v12 and NPM v6 70 | ``` 71 | sudo npm install -g n 72 | sudo n install 12 73 | ``` 74 | Log out and log back in for the Path changes to take effect. You should now have Node v12 (check with node -v) and NPM v6 (check with npm -v) 75 | 76 | #### STEP 2.1.b - OTHER METHODS 77 | 78 | Windows and Mac users can install Node from the nodejs.org web site. 79 | 80 | Older Raspbian users (eg those running Jessie) can install NodeJS and NPM with these commands 81 | 82 | ``` 83 | curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - 84 | sudo apt-get install nodejs 85 | ``` 86 | 87 | ### STEP 3 - GET RPOS SOURCE, INSTALL DEPENDENCIES 88 | 89 | ``` 90 | git clone https://github.com/BreeeZe/rpos.git 91 | cd rpos 92 | npm install 93 | ``` 94 | 95 | ### STEP 4 - COMPILE TYPESCRIPT(.ts) TO JAVASCRIPT(.js) using GULP 96 | 97 | #### 4.1.a 98 | 99 | Use the `npx` command to run the 'gulp' script: (works for NPM 5.2 and higher) 100 | 101 | ``` 102 | npx gulp 103 | ``` 104 | 105 | ### STEP 5 - PICK YOUR RTSP SERVER 106 | 107 | Select & setup an RTSP option for your platform. 108 | 109 | RTSP Server options for Pi / Linux: 110 | 111 | 1. RPOS comes with a pre-compiled ARM binary for a simple RTSP server. The source is in the ‘cpp’ folder. (option 1) 112 | 1. mpromonet RTSP Server (option 2) 113 | 1. GStreamer RTSP Server (option 3) 114 | 115 | RTSP Server options 2 & 3 offer more features, but require additional setup. See instructions below. 116 | Currently USB camera is only supported by GStreamer RTSP Server 117 | 118 | Windows users will need to run their own RTSP Server. 119 | Mac users can use the ffserver script. 120 | 121 | Note: The choice of RTSP Server is made in rposConfig.json 122 | 123 | #### STEP 5.a - OPTION 1: USING PRE-COMPILED ARM BINARY (deprecated) 124 | 125 | This option is not recommended now. Please use Option 2 or Option 3 126 | RPOS comes with a pre-compiled ARM binary for a simple RTSP server. The source is in the ‘cpp’ folder. No action required to use, this is pre-selected in `rposConfig.json` 127 | 128 | Note that this option can be unstable, recommend option 2 or 3. 129 | 130 | #### STEP 5.b - OPTION 2: USING MPROMONET RTSP SERVER 131 | 132 | Raspberry Pi and Linux users will probably prefer the mpromonet RTSP server, as it has more options and supports multicasting. 133 | 134 | Install dependencies and run setup script: 135 | 136 | ``` 137 | sudo apt-get install liblivemedia-dev 138 | sh setup_v4l2rtspserver.sh 139 | ``` 140 | 141 | #### STEP 5.c - OPTION 3: USING GSTREAMER RTSP SERVER 142 | 143 | Install the precompiled packages using apt, or compile them yourself for latest version. 144 | Installing the packages using apt saves a lot of time, but provides a rather old gstreamer version. 145 | 146 | ##### 5.c.1a - INSTALL GSTREAMER USING APT: 147 | 148 | We will install lots of GStreamer Libraries and then the Python and GIR libraries (GIR allow other languages to access the GStreamer C API) 149 | If you only use USB cameras, some may not be needed but for simplicity I'll install them all here. 150 | 151 | ``` 152 | sudo apt install git gstreamer1.0-plugins-base \ 153 | gstreamer1.0-plugins-bad gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly \ 154 | gstreamer1.0-tools libgstreamer1.0-dev libgstreamer1.0-0-dbg \ 155 | libgstreamer1.0-0 libgstrtspserver-1.0.0 \ 156 | libgstreamer-plugins-base1.0-dev gtk-doc-tools \ 157 | gstreamer1.0-omx-rpi gstreamer1.0-omx 158 | ``` 159 | 160 | You can check it is verson 1.14 with ```gst-launch-1.0 --version``` 161 | 162 | Then install Python Binding, GIR Files (GObjectIntrospection Repository - makes APIs from C libraries) 163 | ``` 164 | sudo apt-get install python-gi gir1.2-gst-plugins-base-1.0 gir1.2-gst-rtsp-server-1.0 165 | ``` 166 | 167 | ##### 5.c.1b - INSTALL GST-RPICAMSRC FROM SOURCE 168 | Currently Raspberry Pi OS installs GStreamer 1.14 which does not include the 'rpicamsrc' module so we will build it from source. 169 | 170 | (starting in /rpos root directory) 171 | 172 | ``` 173 | cd .. 174 | git clone https://github.com/thaytan/gst-rpicamsrc.git 175 | cd gst-rpicamsrc 176 | ./autogen.sh 177 | make 178 | sudo make install 179 | cd .. 180 | ``` 181 | 182 | Check successful plugin installation by executing 183 | 184 | ``` 185 | gst-inspect-1.0 rpicamsrc 186 | ``` 187 | Note: You do not need to load V4L2 modules when using rpicamsrc (option 3). 188 | 189 | ##### 5.c.2 - INSTALL GST-RTSP-SERVER FROM SOURCE 190 | 191 | No longer required. Raspberry Pi OS in June 2021 is shipping with GStreamer 1.14 and the Gst RTSP Server library is included 192 | 193 | 194 | ### STEP 6 - EDIT CONFIG 195 | Go back to the 'rpos' folder 196 | 197 | 198 | Rename or copy `rposConfig.sample-*.json` to `rposConfig.json`. (Choosing the appropriate sample to start with) 199 | 200 | - Add a Username and Password for ONVIF access 201 | - Change the TCP Port for the Camera configuration and the ONVIF Services 202 | - Change the RTSP Port 203 | - Enable PTZ support by selecting Pan-Tilt HAT or RS485 backends (Visca and Pelco D) 204 | - Enable multicast 205 | - Switch to the mpromonet or GStreamer RTSP servers 206 | - Hardcode an IP address in the ONVIF SOAP messages 207 | 208 | ### STEP 6 - CONFIG DETAILS 209 | The Configuation is split into several sections 210 | #### IP Address and Login Permissions 211 | - Network Adapters - Used by RPOS to probe network interfaces to try and work out its own IP Address 212 | - IPAddress - This can be used to override the auto detected IP address found by probing the Network Adapters list 213 | - Service Port - This is the TCP Port that RPOS listens on for ONVIF Connections 214 | - Username - The username used to connect to RPOS with 215 | - Password - The Password used to connect to RPOS with 216 | #### Camera Source 217 | This section helps RPOS know where to get live video from 218 | - Camera Type - Used to help RPOS automatically configure itself. Valid optins are "picam", "usbcam", "filesrc", "testsrc". 'picam' will select the Raspberry Pi camera on the ribbon cable, 'usbcam' will select a USB camera, 'filesrc' will open a JPEG or PNG video file and 'testsrc' displays a bouncing ball with clock overlay 219 | - CameraDevice - Provides extra information to go with the Camera Type. For 'usbcam' use the Video4Linux address of the camera, eg /dev/video0. For the 'filesrc' camera type, use the full path and filename of the jpeg or PNG file eg /home/pi/image.jpg 220 | #### RTSP Server 221 | This section helps RPOS know how to share the video via RTSP with viewers 222 | ... 223 | ... 224 | 225 | 226 | ### STEP 7 - RUN RPOS.JS 227 | 228 | #### First run (Skip with USB camera) 229 | 230 | If you're using RTSP option 1 or 2, before you run RPOS for the first time you'll need to load the Pi V4L2 Camera Driver: 231 | 232 | ``` 233 | sudo modprobe bcm2835-v4l2 234 | ``` 235 | 236 | Initial setup is now complete! 237 | 238 | #### Launch RPOS 239 | 240 | To start the application: 241 | 242 | ``` 243 | node rpos.js 244 | ``` 245 | 246 | ### STEP 8 - EXTRA CONFIGURATION ON PAN-TILT HAT (Pimononi) 247 | 248 | The camera on the Pan-Tilt hat is usually installed upside down. 249 | Goto the Web Page that runs with rpos `http://:8081` and tick the horizontal and vertial flip boxes and apply the changes. 250 | 251 | ## Camera Settings 252 | 253 | You can set camera settings by browsing to : `http://CameraIP:Port/` 254 | These settings are then saved in a file called v4l2ctl.json and are persisted on rpos restart. 255 | The default port for RPOS is 8081. 256 | (Note that a lot of camera settings are now ignored by USB camera) 257 | 258 | ## Known Issues 259 | 260 | - 1920x1080 can cause hangs and crashes with the original RTSP server. The mpromonet one may work better. 261 | - 1920x1080 can cause encoding issue with USB camera pipeline. 1280x720 is recommended now. 262 | - Not all of the ONVIF standard is implemented. 263 | 264 | ## ToDo's (Help is Required) 265 | 266 | - Add MJPEG (implemented in gst-rtsp-server but still needs to return the correct ONVIF XML for MJPEG) 267 | - Support more parameters for USB cameras with GStreamer RTSP server [work underway by RogerHardiman. Help needed] 268 | - Support USB cameras with the Pi's Hardware H264 encoder (OMX) and the mpromonet RTP server (see https://github.com/mpromonet/v4l2tools) 269 | - Implement more ONVIF calls (Events, Analytics) 270 | - Test with ONVIF's own test tools (need a sponsor for this as we need to be ONVIF members to access the Test Tool) 271 | - Add GPIO digital input 272 | - Add two way audio with ONVIF back channel. We understand GStreamer has some support for this now. 273 | - and more... 274 | -------------------------------------------------------------------------------- /RPOS.njsproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 11.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | RPOS 7 | PiOnvifService.Node.js 8 | 9 | 10 | 11 | Debug 12 | 2.0 13 | eb27155c-21e9-49f0-9852-2ac2c20128fa 14 | 15 | 16 | server.js 17 | 18 | 19 | . 20 | . 21 | v4.0 22 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 23 | ShowAllFiles 24 | 1337 25 | true 26 | 27 | 28 | true 29 | 30 | 31 | true 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | False 85 | True 86 | 0 87 | / 88 | http://localhost:48022/ 89 | False 90 | True 91 | http://localhost:1337 92 | False 93 | 94 | 95 | 96 | 97 | 98 | 99 | CurrentPage 100 | True 101 | False 102 | False 103 | False 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | False 113 | False 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /RPOS.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "RPOS", "RPOS.njsproj", "{EB27155C-21E9-49F0-9852-2AC2C20128FA}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {EB27155C-21E9-49F0-9852-2AC2C20128FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {EB27155C-21E9-49F0-9852-2AC2C20128FA}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {EB27155C-21E9-49F0-9852-2AC2C20128FA}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {EB27155C-21E9-49F0-9852-2AC2C20128FA}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /RPOS_PanTiltHAT.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/RPOS_PanTiltHAT.jpg -------------------------------------------------------------------------------- /bin/rtspServer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/bin/rtspServer -------------------------------------------------------------------------------- /cpp/Makefile: -------------------------------------------------------------------------------- 1 | INCLUDES = -I../UsageEnvironment/include -I../groupsock/include -I../liveMedia/include -I../BasicUsageEnvironment/include 2 | # Default library filename suffixes for each library that we link with. The "config.*" file might redefine these later. 3 | libliveMedia_LIB_SUFFIX = $(LIB_SUFFIX) 4 | libBasicUsageEnvironment_LIB_SUFFIX = $(LIB_SUFFIX) 5 | libUsageEnvironment_LIB_SUFFIX = $(LIB_SUFFIX) 6 | libgroupsock_LIB_SUFFIX = $(LIB_SUFFIX) 7 | ##### Change the following for your environment: 8 | COMPILE_OPTS = $(INCLUDES) -I. -O2 -DSOCKLEN_T=socklen_t -D_LARGEFILE_SOURCE=1 -D_FILE_OFFSET_BITS=64 9 | C = c 10 | C_COMPILER = cc 11 | C_FLAGS = $(COMPILE_OPTS) $(CPPFLAGS) $(CFLAGS) 12 | CPP = cpp 13 | CPLUSPLUS_COMPILER = c++ 14 | CPLUSPLUS_FLAGS = $(COMPILE_OPTS) -Wall -DBSD=1 $(CPPFLAGS) $(CXXFLAGS) 15 | OBJ = o 16 | LINK = c++ -o 17 | LINK_OPTS = -L. $(LDFLAGS) 18 | CONSOLE_LINK_OPTS = $(LINK_OPTS) 19 | LIBRARY_LINK = ar cr 20 | LIBRARY_LINK_OPTS = 21 | LIB_SUFFIX = a 22 | LIBS_FOR_CONSOLE_APPLICATION = 23 | LIBS_FOR_GUI_APPLICATION = 24 | EXE = 25 | ##### End of variables to change 26 | 27 | UNICAST_STREAMER_APPS = rtspServer$(EXE) 28 | UNICAST_APPS = $(UNICAST_STREAMER_APPS) 29 | 30 | PREFIX = /usr/local 31 | ALL = $(UNICAST_APPS) 32 | all: $(ALL) 33 | 34 | .$(C).$(OBJ): 35 | $(C_COMPILER) -c $(C_FLAGS) $< 36 | .$(CPP).$(OBJ): 37 | $(CPLUSPLUS_COMPILER) -c $(CPLUSPLUS_FLAGS) $< 38 | 39 | ON_DEMAND_RTSP_SERVER_OBJS = rtspServer.$(OBJ) 40 | 41 | USAGE_ENVIRONMENT_DIR = ../UsageEnvironment 42 | USAGE_ENVIRONMENT_LIB = $(USAGE_ENVIRONMENT_DIR)/libUsageEnvironment.$(libUsageEnvironment_LIB_SUFFIX) 43 | BASIC_USAGE_ENVIRONMENT_DIR = ../BasicUsageEnvironment 44 | BASIC_USAGE_ENVIRONMENT_LIB = $(BASIC_USAGE_ENVIRONMENT_DIR)/libBasicUsageEnvironment.$(libBasicUsageEnvironment_LIB_SUFFIX) 45 | LIVEMEDIA_DIR = ../liveMedia 46 | LIVEMEDIA_LIB = $(LIVEMEDIA_DIR)/libliveMedia.$(libliveMedia_LIB_SUFFIX) 47 | GROUPSOCK_DIR = ../groupsock 48 | GROUPSOCK_LIB = $(GROUPSOCK_DIR)/libgroupsock.$(libgroupsock_LIB_SUFFIX) 49 | LOCAL_LIBS = $(LIVEMEDIA_LIB) $(GROUPSOCK_LIB) \ 50 | $(BASIC_USAGE_ENVIRONMENT_LIB) $(USAGE_ENVIRONMENT_LIB) 51 | LIBS = $(LOCAL_LIBS) $(LIBS_FOR_CONSOLE_APPLICATION) 52 | 53 | rtspServer$(EXE): $(ON_DEMAND_RTSP_SERVER_OBJS) $(LOCAL_LIBS) 54 | $(LINK)$@ $(CONSOLE_LINK_OPTS) $(ON_DEMAND_RTSP_SERVER_OBJS) $(LIBS) 55 | 56 | clean: 57 | -rm -rf *.$(OBJ) $(ALL) core *.core *~ include/*~ 58 | 59 | install: $(ALL) 60 | install -d $(DESTDIR)$(PREFIX)/bin 61 | install -m 755 $(ALL) $(DESTDIR)$(PREFIX)/bin 62 | 63 | ##### Any additional, platform-specific rules come here: 64 | -------------------------------------------------------------------------------- /cpp/rtspServer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/cpp/rtspServer -------------------------------------------------------------------------------- /cpp/rtspServer.cpp: -------------------------------------------------------------------------------- 1 | /********** 2 | This library is free software; you can redistribute it and/or modify it under 3 | the terms of the GNU Lesser General Public License as published by the 4 | Free Software Foundation; either version 2.1 of the License, or (at your 5 | option) any later version. (See .) 6 | 7 | This library is distributed in the hope that it will be useful, but WITHOUT 8 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 9 | FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for 10 | more details. 11 | 12 | You should have received a copy of the GNU Lesser General Public License 13 | along with this library; if not, write to the Free Software Foundation, Inc., 14 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 15 | **********/ 16 | // Copyright (c) 1996-2013, Live Networks, Inc. All rights reserved 17 | // A test program that demonstrates how to stream - via unicast RTP 18 | // - various kinds of file on demand, using a built-in RTSP server. 19 | // main program 20 | 21 | #include "liveMedia.hh" 22 | #include "BasicUsageEnvironment.hh" 23 | 24 | UsageEnvironment* env; 25 | 26 | // To make the second and subsequent client for each stream reuse the same 27 | // input stream as the first client (rather than playing the file from the 28 | // start for each client), change the following "False" to "True": 29 | Boolean reuseFirstSource = True; 30 | 31 | // To stream *only* MPEG-1 or 2 video "I" frames 32 | // (e.g., to reduce network bandwidth), 33 | // change the following "False" to "True": 34 | Boolean iFramesOnly = False; 35 | 36 | static void announceStream(RTSPServer* rtspServer, ServerMediaSession* sms, 37 | char const* streamName, char const* inputFileName); // fwd 38 | 39 | int main(int argc, char** argv) { 40 | 41 | // Begin by setting up our usage environment: 42 | TaskScheduler* scheduler = BasicTaskScheduler::createNew(); 43 | env = BasicUsageEnvironment::createNew(*scheduler); 44 | 45 | UserAuthenticationDatabase* authDB = NULL; 46 | #ifdef ACCESS_CONTROL 47 | // To implement client access control to the RTSP server, do the following: 48 | authDB = new UserAuthenticationDatabase; 49 | authDB->addUserRecord("username1", "password1"); // replace these with real strings 50 | // Repeat the above with each , that you wish to allow 51 | // access to the server. 52 | #endif 53 | 54 | OutPacketBuffer::maxSize = strtol(argv[2],NULL,0);//1024000; 55 | // Create the RTSP server: 56 | RTSPServer* rtspServer = RTSPServer::createNew(*env, strtol(argv[3],NULL,0), authDB); 57 | if (rtspServer == NULL) { 58 | *env << "Failed to create RTSP server: " << env->getResultMsg() << "\n"; 59 | exit(1); 60 | } 61 | 62 | char const* descriptionString = "Session streamed by \"testRTSPServer\""; 63 | 64 | // Set up each of the possible streams that can be served by the 65 | // RTSP server. Each such stream is implemented using a 66 | // "ServerMediaSession" object, plus one or more 67 | // "ServerMediaSubsession" objects for each audio/video substream. 68 | 69 | // A H.264 video elementary stream: 70 | { 71 | char const* streamName = argc > 4 ? argv[5] : ""; 72 | if(streamName == NULL) 73 | streamName = ""; 74 | char const* inputFileName = argv[1]; 75 | 76 | ServerMediaSession* sms = ServerMediaSession::createNew(*env, streamName, streamName, descriptionString); 77 | 78 | sms->addSubsession( H264VideoFileServerMediaSubsession ::createNew(*env, inputFileName, reuseFirstSource)); 79 | 80 | rtspServer->addServerMediaSession(sms); 81 | 82 | announceStream(rtspServer, sms, streamName, inputFileName); // not needed just for better diagnostic 83 | } 84 | 85 | // Also, attempt to create a HTTP server for RTSP-over-HTTP tunneling. 86 | if (argc > 3 && argv[4] != NULL && strtol(argv[4],NULL,0) > 0 && rtspServer->setUpTunnelingOverHTTP(strtol(argv[4],NULL,0))) { 87 | *env << "\n(Using port " << rtspServer->httpServerPortNum() << " for optional RTSP-over-HTTP tunneling.)\n"; 88 | } else { 89 | *env << "\n(RTSP-over-HTTP tunneling is not available.)\n"; 90 | } 91 | 92 | env->taskScheduler().doEventLoop(); // does not return 93 | 94 | return 0; // only to prevent compiler warning 95 | } 96 | 97 | static void announceStream(RTSPServer* rtspServer, ServerMediaSession* sms, 98 | char const* streamName, char const* inputFileName) { 99 | char* url = rtspServer->rtspURL(sms); 100 | UsageEnvironment& env = rtspServer->envir(); 101 | env << "Streaming on URL \"" << url << "\"\n"; 102 | delete[] url; 103 | } 104 | -------------------------------------------------------------------------------- /cpp/rtspServer.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/cpp/rtspServer.o -------------------------------------------------------------------------------- /ffserver_mac.conf: -------------------------------------------------------------------------------- 1 | #Sample ffserver configuration file 2 | 3 | # Port on which the server is listening. You must select a different 4 | # port from your standard HTTP web server if it is running on the same 5 | # computer. 6 | HTTPPort 8090 7 | 8 | # Number of simultaneous HTTP connections that can be handled. It has 9 | # to be defined *before* the MaxClients parameter, since it defines the 10 | # MaxClients maximum limit. 11 | MaxHTTPConnections 2000 12 | 13 | # Number of simultaneous requests that can be handled. Since FFServer 14 | # is very fast, it is more likely that you will want to leave this high 15 | # and use MaxBandwidth, below. 16 | MaxClients 1000 17 | 18 | # This the maximum amount of kbit/sec that you are prepared to 19 | # consume when streaming to clients. 20 | MaxBandwidth 10000 21 | 22 | # Access log file (uses standard Apache log file format) 23 | # '-' is the standard output. 24 | CustomLog - 25 | 26 | 27 | RTSPPort 8554 28 | 29 | ################################################################## 30 | # Definition of the live feeds. Each live feed contains one video 31 | # and/or audio sequence coming from an ffmpeg encoder or another 32 | # ffserver. This sequence may be encoded simultaneously with several 33 | # codecs at several resolutions. 34 | 35 | 36 | 37 | # You must use 'ffmpeg' to send a live feed to ffserver. In this 38 | # example, you can type: 39 | # 40 | # ffmpeg http://localhost:8554/feed1.ffm 41 | 42 | File /tmp/feed1.ffm 43 | FileMaxSize 5M 44 | 45 | 46 | # Specify launch in order to start ffmpeg automatically. 47 | # First ffmpeg must be defined with an appropriate path if needed, 48 | # after that options can follow, but avoid adding the http:// field 49 | Launch ffmpeg -f avfoundation -i "default" 50 | 51 | # Only allow connections from localhost to the feed. 52 | ACL allow 127.0.0.1 53 | 54 | 55 | 56 | 57 | ################################################################## 58 | # RTSP examples 59 | # 60 | # You can access this stream with the RTSP URL: 61 | # rtsp://localhost:5454/test1-rtsp.mpg 62 | # 63 | # A non-standard RTSP redirector is also created. Its URL is: 64 | # http://localhost:8090/test1-rtsp.rtsp 65 | 66 | # Transcode an incoming live feed to another live feed, 67 | # using libx264 and video presets 68 | 69 | 70 | Format rtp 71 | Feed feed1.ffm 72 | VideoCodec libx264 73 | VideoFrameRate 25 74 | VideoBitRate 2048 75 | VideoSize 1280x720 76 | PixelFormat yuv420p 77 | 78 | PreRoll 0 79 | VideoGopSize 1 80 | 81 | AVOptionVideo flags +global_header 82 | NoAudio 83 | 84 | #AudioCodec libfaac 85 | #AudioBitRate 32 86 | #AudioChannels 2 87 | #AudioSampleRate 22050 88 | #AVOptionAudio flags +global_header 89 | 90 | 91 | ################################################################## 92 | # SDP/multicast examples 93 | # 94 | # If you want to send your stream in multicast, you must set the 95 | # multicast address with MulticastAddress. The port and the TTL can 96 | # also be set. 97 | # 98 | # An SDP file is automatically generated by ffserver by adding the 99 | # 'sdp' extension to the stream name (here 100 | # http://localhost:8090/test1-sdp.sdp). You should usually give this 101 | # file to your player to play the stream. 102 | # 103 | # The 'NoLoop' option can be used to avoid looping when the stream is 104 | # terminated. 105 | 106 | # 107 | #Format rtp 108 | #File "/usr/local/httpd/htdocs/test1.mpg" 109 | #MulticastAddress 224.124.0.1 110 | #MulticastPort 5000 111 | #MulticastTTL 16 112 | #NoLoop 113 | # 114 | 115 | 116 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | rename = require('gulp-rename'), 3 | zip = require('gulp-zip'), 4 | pkg = require('./package.json'), 5 | ts = require('gulp-typescript'), 6 | sourcemaps = require('gulp-sourcemaps') 7 | 8 | var version = 'rpos-' + pkg.version; 9 | var releaseDir = 'release/' + version; 10 | 11 | 12 | //Compile task: compiles all .ts files to .js and generates sourcemaps to aid in debugging. 13 | gulp.task('default', function () { 14 | return gulp.src(["**/*.ts", "!./node_modules/**/*", "!./typings/**/*"]) 15 | .pipe(sourcemaps.init()) 16 | .pipe(ts('tsconfig.json')) 17 | .js 18 | .pipe(sourcemaps.write("./")) 19 | .pipe(gulp.dest("./")); 20 | }); 21 | 22 | // --- all partial taks to generate a release. 23 | gulp.task('copy-release-js', function () { 24 | return gulp.src(['**/*.js', '!release/**', '!gulpfile.js', 'README.md', 'package.json', '!node_modules/gulp*/**']) 25 | .pipe(gulp.dest(releaseDir)); 26 | }); 27 | gulp.task('copy-release-config', function () { 28 | return gulp.src('rposConfig.release.json') 29 | .pipe(rename("rposConfig.json")) 30 | .pipe(gulp.dest(releaseDir)); 31 | }); 32 | gulp.task('copy-release-bin', function () { 33 | return gulp.src('bin/*') 34 | .pipe(gulp.dest(releaseDir + '/bin')); 35 | }); 36 | gulp.task('copy-release-modules', function () { 37 | return gulp.src(['node_modules/**/*', '!node_modules/gulp*/**', '!node_modules/gulp**']) 38 | .pipe(gulp.dest(releaseDir + '/node_modules')); 39 | }); 40 | gulp.task('copy-release-views', function () { 41 | return gulp.src('views/**/*') 42 | .pipe(gulp.dest(releaseDir + '/views')); 43 | }); 44 | gulp.task('copy-release-web', function () { 45 | return gulp.src('web/**/*') 46 | .pipe(gulp.dest(releaseDir + '/web')); 47 | }); 48 | gulp.task('copy-release-wsdl', function () { 49 | return gulp.src('wsdl/**/*') 50 | .pipe(gulp.dest(releaseDir + '/wsdl')); 51 | }); 52 | 53 | //Release task: generates a release package. 54 | gulp.task('release', gulp.series('copy-release-js', 'copy-release-bin', 'copy-release-modules', 'copy-release-views', 55 | 'copy-release-web', 'copy-release-wsdl', 'copy-release-config', function () { 56 | return gulp.src([releaseDir + '/**/*', releaseDir + '/*.zip']) 57 | .pipe(zip(version + '.zip')) 58 | .pipe(gulp.dest('release')); 59 | })); 60 | -------------------------------------------------------------------------------- /lib/SoapService.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import fs = require("fs"); 4 | import { Utils } from './utils'; 5 | import { Server } from 'http'; 6 | var soap = require('soap'); 7 | var utils = Utils.utils; 8 | 9 | var NOT_IMPLEMENTED = { 10 | Fault: { 11 | attributes: { // Add namespace here. Really wanted to put it in Envelope but this should be valid 12 | 'xmlns:ter' : 'http://www.onvif.org/ver10/error', 13 | }, 14 | Code: { 15 | Value: "soap:Sender", 16 | Subcode: { 17 | Value: "ter:NotAuthorized", 18 | }, 19 | }, 20 | Reason: { 21 | Text: { 22 | attributes: { 23 | 'xml:lang': 'en', 24 | }, 25 | $value: 'Sender not Authorized', 26 | } 27 | } 28 | } 29 | }; 30 | 31 | 32 | class SoapService { 33 | webserver: Server; 34 | config: rposConfig; 35 | serviceInstance: any; 36 | serviceOptions: SoapServiceOptions; 37 | startedCallbacks: (() => void)[]; 38 | isStarted: boolean; 39 | 40 | constructor(config: rposConfig, server: Server) { 41 | this.webserver = server; 42 | this.config = config; 43 | this.serviceInstance = null; 44 | this.startedCallbacks = []; 45 | this.isStarted = false; 46 | 47 | this.serviceOptions = { 48 | path: '', 49 | services: null, 50 | xml: null, 51 | wsdlPath: '', 52 | onReady: () => { } 53 | }; 54 | 55 | } 56 | 57 | starting() { } 58 | 59 | started() { } 60 | 61 | start() { 62 | this.starting(); 63 | 64 | utils.log.info("Binding %s to http://%s:%s%s", (this.constructor).name, utils.getIpAddress(), this.config.ServicePort, this.serviceOptions.path); 65 | var onReady = this.serviceOptions.onReady; 66 | this.serviceOptions.onReady = () => { 67 | this._started(); 68 | onReady(); 69 | }; 70 | this.serviceInstance = soap.listen(this.webserver, this.serviceOptions); 71 | 72 | this.serviceInstance.on("request", (request: any, methodName: string) => { 73 | utils.log.debug('%s received request %s', (this.constructor).name, methodName); 74 | 75 | // Use the '=>' notation so 'this' refers to the class we are in 76 | // ONVIF allows GetSystemDateAndTime to be sent with no authenticaton header 77 | // So we check the header and check authentication in this function 78 | 79 | // utils.log.info('received soap header'); 80 | if (methodName === "GetSystemDateAndTime") return; 81 | 82 | if (this.config.Username) { 83 | let token: any = null; 84 | try { 85 | token = request.Header.Security.UsernameToken; 86 | } catch (err) { 87 | utils.log.info('No Username/Password (ws-security) supplied for ' + methodName); 88 | throw NOT_IMPLEMENTED; 89 | } 90 | var user = token.Username; 91 | var password = (token.Password.$value || token.Password); 92 | var nonce = (token.Nonce.$value || token.Nonce); // handle 2 ways to map XML to the javascript data structure 93 | var created = token.Created; 94 | 95 | var onvif_username = this.config.Username; 96 | var onvif_password = this.config.Password; 97 | 98 | // digest = base64 ( sha1 ( nonce + created + onvif_password ) ) 99 | var crypto = require('crypto'); 100 | var pwHash = crypto.createHash('sha1'); 101 | var rawNonce = new Buffer(nonce || '', 'base64') 102 | var combined_data = Buffer.concat([rawNonce, 103 | Buffer.from(created, 'ascii'), Buffer.from(onvif_password, 'ascii')]); 104 | pwHash.update(combined_data); 105 | var generated_password = pwHash.digest('base64'); 106 | 107 | var password_ok = (user === onvif_username && password === generated_password); 108 | 109 | if (password_ok == false) { 110 | utils.log.info('Invalid username/password with ' + methodName); 111 | throw NOT_IMPLEMENTED; 112 | } 113 | }; 114 | }); 115 | 116 | this.serviceInstance.log = (type: string, data: any) => { 117 | if (this.config.logSoapCalls) 118 | utils.log.debug('%s - Calltype : %s, Data : %s', (this.constructor).name, type, data); 119 | }; 120 | } 121 | 122 | onStarted(callback: () => {}) { 123 | if (this.isStarted) 124 | callback(); 125 | else 126 | this.startedCallbacks.push(callback); 127 | } 128 | 129 | _started() { 130 | this.isStarted = true; 131 | for (var callback of this.startedCallbacks) 132 | callback(); 133 | this.startedCallbacks = []; 134 | this.started(); 135 | } 136 | } 137 | export = SoapService; 138 | -------------------------------------------------------------------------------- /lib/camera.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { Utils } from './utils'; 4 | import fs = require('fs'); 5 | import parser = require('body-parser'); 6 | import { ChildProcess } from 'child_process'; 7 | import { v4l2ctl } from './v4l2ctl'; 8 | 9 | var utils = Utils.utils; 10 | 11 | class Camera { 12 | options = { 13 | resolutions: [ 14 | { Width: 640, Height: 480 }, 15 | { Width: 800, Height: 600 }, 16 | { Width: 1024, Height: 768 }, 17 | { Width: 1280, Height: 1024 }, 18 | { Width: 1280, Height: 720 }, 19 | { Width: 1640, Height: 1232 }, 20 | { Width: 1920, Height: 1080 } 21 | ], 22 | framerates: [2, 5, 10, 15, 25, 30], 23 | bitrates: [ 24 | 250, 25 | 500, 26 | 1000, 27 | 2500, 28 | 5000, 29 | 7500, 30 | 10000, 31 | 12500, 32 | 15000, 33 | 17500 34 | ] 35 | } 36 | 37 | settings: CameraSettingsBase = { 38 | forceGop: true, 39 | resolution: { Width: 1280, Height: 720 }, 40 | framerate: 25, 41 | } 42 | 43 | config: rposConfig; 44 | rtspServer: ChildProcess; 45 | webserver: any; 46 | 47 | constructor(config: rposConfig, webserver: any) { 48 | this.config = config; 49 | this.rtspServer = null; 50 | if (this.config.RTSPServer != 0) { 51 | if (this.config.CameraType == 'usbcam') { 52 | if (this.config.RTSPServer != 3) { 53 | // Only GStreamer RTSP is supported now 54 | console.log('Only GStreamer RTSP mode is supported for USB Camera video'); 55 | process.exit(1); 56 | } 57 | if (!fs.existsSync(this.config.CameraDevice)) { 58 | // USB cam is not found 59 | console.log(`USB Camera is not found at ${this.config.CameraDevice}`); 60 | process.exit(1); 61 | } 62 | } 63 | if (this.config.CameraType == 'filesrc') { 64 | if (this.config.RTSPServer != 3) { 65 | // Only GStreamer RTSP is supported now 66 | console.log('Only GStreamer RTSP mode is supported for File Source video'); 67 | process.exit(1); 68 | } 69 | if (!fs.existsSync(this.config.CameraDevice)) { 70 | // Filename of image to show in RTSP stream is not found 71 | console.log(`Filesrc file is not found at ${this.config.CameraDevice}`); 72 | process.exit(1); 73 | } 74 | } 75 | if (this.config.CameraType == 'testsrc') { 76 | if (this.config.RTSPServer != 3) { 77 | // Only GStreamer RTSP is supported now 78 | console.log('Only GStreamer RTSP mode is supported for Test Source video'); 79 | process.exit(1); 80 | } 81 | } 82 | if (this.config.CameraType == 'picam') { 83 | if (!fs.existsSync("/dev/video0")) { 84 | // this.loadDriver(); 85 | if (utils.isPi()) { 86 | // Needs a V4L2 Driver to be installed 87 | console.log('Use modprobe to load the Pi Camera V4L2 driver'); 88 | console.log('e.g. sudo modprobe bcm2835-v4l2'); 89 | console.log(' or the uv4l driver'); 90 | process.exit(1); 91 | } 92 | } 93 | } 94 | } 95 | this.webserver = webserver; 96 | 97 | this.setupWebserver(); 98 | this.setupCamera(); 99 | 100 | v4l2ctl.ReadControls(); 101 | 102 | utils.cleanup(() => { 103 | this.stopRtsp(); 104 | var stop = new Date().getTime() + 2000; 105 | while (new Date().getTime() < stop) { 106 | //wait for rtsp server to stop 107 | ; 108 | } 109 | // this.unloadDriver(); 110 | }); 111 | 112 | if (this.config.RTSPServer == 1 )fs.chmodSync("./bin/rtspServer", "0755"); 113 | } 114 | 115 | setupWebserver() { 116 | utils.log.info("Starting camera settings webserver on http://%s:%s/", utils.getIpAddress(), this.config.ServicePort); 117 | this.webserver.use(parser.urlencoded({ extended: true })); 118 | this.webserver.engine('ntl', (filePath, options, callback) => { 119 | this.getSettingsPage(filePath, callback); 120 | }); 121 | this.webserver.set('views', './views'); // specify the views directory 122 | this.webserver.set('view engine', 'ntl'); // register the template engine 123 | this.webserver.get('/', (req, res) => { 124 | res.render('camera', {}); 125 | }); 126 | this.webserver.post('/', (req, res) => { 127 | for (var par in req.body) { 128 | var g = par.split('.')[0]; 129 | var p = par.split('.')[1]; 130 | if (p && g) { 131 | var prop = >v4l2ctl.Controls[g][p]; 132 | var val = req.body[par]; 133 | if (val instanceof Array) 134 | val = (val).pop(); 135 | prop.value = val; 136 | if (prop.isDirty) { 137 | utils.log.debug("Property %s changed to %s", par, prop.value); 138 | } 139 | } 140 | } 141 | v4l2ctl.ApplyControls(); 142 | res.render('camera', {}); 143 | }); 144 | } 145 | 146 | getSettingsPage(filePath, callback) { 147 | v4l2ctl.ReadControls(); 148 | fs.readFile(filePath, (err, content) => { 149 | if (err) 150 | return callback(new Error(err.message)); 151 | 152 | var parseControls = (html: string, displayname: string, propname: string, controls: Object) => { 153 | html += `${displayname}`; 154 | for (var uc in controls) { 155 | var p = >controls[uc]; 156 | if (p.hasSet) { 157 | var set = p.getLookupSet(); 158 | html += `${uc}'; 163 | 164 | } else if (p.type == "Boolean") { 165 | html += `${uc} 166 | 167 | `; 168 | } else { 169 | html += `${uc} 170 | ` 171 | if (p.hasRange) 172 | html += `( min: ${p.getRange().min} max: ${p.getRange().max} )` 173 | html += ``; 174 | } 175 | } 176 | return html; 177 | } 178 | 179 | var html = "

RPOS - ONVIF NVT Camera

"; 180 | html += "Video Stream: rtsp://username:password@deviceIPaddress:" + this.config.RTSPPort.toString() + "/" + this.config.RTSPName.toString(); 181 | html += "
"; 182 | 183 | html = parseControls(html, 'User Controls', 'UserControls', v4l2ctl.Controls.UserControls); 184 | html = parseControls(html, 'Codec Controls', 'CodecControls', v4l2ctl.Controls.CodecControls); 185 | html = parseControls(html, 'Camera Controls', 'CameraControls', v4l2ctl.Controls.CameraControls); 186 | html = parseControls(html, 'JPG Compression Controls', 'JPEGCompressionControls', v4l2ctl.Controls.JPEGCompressionControls); 187 | 188 | var rendered = content.toString().replace('{{row}}', html); 189 | return callback(null, rendered); 190 | }) 191 | } 192 | 193 | loadDriver() { 194 | try { 195 | utils.execSync("sudo modprobe bcm2835-v4l2"); // only on PI, and not needed with USB Camera 196 | } catch (err) {} 197 | } 198 | 199 | unloadDriver(){ 200 | try { 201 | utils.execSync("sudo modprobe -r bcm2835-v4l2"); 202 | } catch (err) {} 203 | } 204 | 205 | setupCamera() { 206 | v4l2ctl.SetPixelFormat(v4l2ctl.Pixelformat.H264) 207 | v4l2ctl.SetResolution(this.settings.resolution); 208 | v4l2ctl.SetFrameRate(this.settings.framerate); 209 | v4l2ctl.SetPriority(v4l2ctl.ProcessPriority.record); 210 | v4l2ctl.ReadFromFile(); 211 | v4l2ctl.ApplyControls(); 212 | } 213 | 214 | setSettings(newsettings: CameraSettingsParameter) { 215 | v4l2ctl.SetResolution(newsettings.resolution); 216 | v4l2ctl.SetFrameRate(newsettings.framerate); 217 | 218 | v4l2ctl.Controls.CodecControls.video_bitrate.value = newsettings.bitrate * 1000; 219 | v4l2ctl.Controls.CodecControls.video_bitrate_mode.value = newsettings.quality > 0 ? 0 : 1; 220 | v4l2ctl.Controls.CodecControls.h264_i_frame_period.value = this.settings.forceGop ? v4l2ctl.Controls.CodecControls.h264_i_frame_period.value : newsettings.gop; 221 | v4l2ctl.ApplyControls(); 222 | } 223 | 224 | startRtsp() { 225 | if (this.rtspServer) { 226 | utils.log.warn("Cannot start rtspServer, already running"); 227 | return; 228 | } 229 | utils.log.info("Starting rtsp server"); 230 | 231 | if (this.config.MulticastEnabled) { 232 | this.rtspServer = utils.spawn("v4l2rtspserver", ["-P", this.config.RTSPPort.toString(), "-u" , this.config.RTSPName.toString(), "-m", this.config.RTSPMulticastName, "-M", this.config.MulticastAddress.toString() + ":" + this.config.MulticastPort.toString(), "-W",this.settings.resolution.Width.toString(), "-H", this.settings.resolution.Height.toString(), "/dev/video0"]); 233 | } else { 234 | if (this.config.RTSPServer == 1) this.rtspServer = utils.spawn("./bin/rtspServer", ["/dev/video0", "2088960", this.config.RTSPPort.toString(), "0", this.config.RTSPName.toString()]); 235 | if (this.config.RTSPServer == 2) this.rtspServer = utils.spawn("v4l2rtspserver", ["-P",this.config.RTSPPort.toString(), "-u" , this.config.RTSPName.toString(),"-W",this.settings.resolution.Width.toString(),"-H",this.settings.resolution.Height.toString(),"/dev/video0"]); 236 | if (this.config.RTSPServer == 3) this.rtspServer = utils.spawn("./python/gst-rtsp-launch.sh", ["-P",this.config.RTSPPort.toString(), "-u" , this.config.RTSPName.toString(),"-W",this.settings.resolution.Width.toString(),"-H",this.settings.resolution.Height.toString(), "-t", this.config.CameraType, "-d", (this.config.CameraDevice == "" ? "auto" : this.config.CameraDevice)]); 237 | } 238 | 239 | if (this.rtspServer) { 240 | this.rtspServer.stdout.on('data', data => utils.log.debug("rtspServer: %s", data)); 241 | this.rtspServer.stderr.on('data', data => utils.log.error("rtspServer: %s", data)); 242 | this.rtspServer.on('error', err=> utils.log.error("rtspServer error: %s", err)); 243 | this.rtspServer.on('exit', (code, signal) => { 244 | if (code) 245 | utils.log.error("rtspServer exited with code: %s", code); 246 | else 247 | utils.log.debug("rtspServer exited") 248 | }); 249 | } 250 | } 251 | 252 | stopRtsp() { 253 | if (this.rtspServer) { 254 | utils.log.info("Stopping rtsp server"); 255 | this.rtspServer.kill(); 256 | this.rtspServer = null; 257 | } 258 | } 259 | } 260 | 261 | export = Camera; 262 | -------------------------------------------------------------------------------- /lib/extension.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | Date.prototype.stdTimezoneOffset = function() { 4 | var jan = new Date(this.getFullYear(), 0, 1); 5 | var jul = new Date(this.getFullYear(), 6, 1); 6 | return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset()); 7 | } 8 | 9 | Date.prototype.dst = function() { 10 | return this.getTimezoneOffset() < this.stdTimezoneOffset(); 11 | } -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { networkInterfaces } from 'os'; 4 | import { spawn, ChildProcess } from 'child_process'; 5 | import { EventEmitter} from "events"; 6 | import { Writable, Readable } from "stream"; 7 | import * as crypto from "crypto"; 8 | 9 | import clc = require('cli-color'); 10 | 11 | export module Utils { 12 | export enum logLevel { 13 | None = 0, 14 | Error = 1, 15 | Warn = 2, 16 | Info = 3, 17 | Debug = 4 18 | } 19 | 20 | // Dummy Process is used when we don't want to spawn a local excutable, eg a dummy RTSP server 21 | class DummyProcess extends EventEmitter implements ChildProcess { 22 | stdin: Writable; 23 | stdout: Readable; 24 | stderr: Readable; 25 | stdio: [Writable, Readable, Readable, Writable | Readable, Writable | Readable]; 26 | readonly killed: boolean; 27 | readonly pid: number; 28 | readonly connected: boolean; 29 | readonly exitCode: number | null; 30 | readonly signalCode: number | null; 31 | readonly spawnargs: string[]; 32 | readonly spawnfile: string; 33 | constructor() { 34 | super(); 35 | this.stdin = new Writable(); 36 | this.stderr = this.stdout = new DummyReadable(); 37 | } 38 | kill(signal?: any): boolean { return true }; 39 | send(message: any, sendHandle?: any): boolean { return true }; 40 | disconnect() { }; 41 | unref() { }; 42 | ref() { }; 43 | 44 | } 45 | 46 | class DummyReadable extends Readable { 47 | read() { return null; } 48 | } 49 | 50 | export class utils { 51 | private static config: rposConfig; 52 | static setConfig(config: rposConfig) { 53 | this.config = config; 54 | } 55 | 56 | static getSerial() { 57 | // Extract serial from cpuinfo file 58 | var cpuserial = "0000000000000000"; 59 | try { 60 | var f = utils.execSync('cat /proc/cpuinfo').toString(); 61 | cpuserial = f.match(/Serial[\t]*: ([0-9a-f]{16})/)[1]; 62 | } catch (ex) { 63 | this.log.error("Failed to read serial : %s", ex.message); 64 | cpuserial = "000000000"; 65 | } 66 | return cpuserial; 67 | } 68 | 69 | static testIpAddress() { 70 | var ip, interfaces = networkInterfaces(); 71 | for (var inf of this.config.NetworkAdapters) { 72 | ip = this.getAddress(interfaces[inf], "IPv4"); 73 | if (!ip) 74 | utils.log.debug("Read IP address from %s failed", inf); 75 | else { 76 | utils.log.info("Read IP address %s from %s", ip, inf); 77 | return; 78 | } 79 | } 80 | utils.log.info("Using IP address from config: %s", this.config.IpAddress); 81 | } 82 | private static getAddress = (ni: any[], type) => { 83 | ni = ni || []; 84 | var address = ""; 85 | for (var nif of ni) { 86 | if (nif.family == type) 87 | address = nif.address; 88 | } 89 | return address; 90 | } 91 | 92 | static getIpAddress(type?: string) { 93 | type = type || "IPv4"; 94 | var interfaces = networkInterfaces(); 95 | for (var inf of this.config.NetworkAdapters) { 96 | var ip = this.getAddress(interfaces[inf], type); 97 | if (ip) 98 | return ip; 99 | } 100 | return this.config.IpAddress; 101 | } 102 | 103 | // Various methods to detect if this is a Pi 104 | // a) Try Device Tree (newer Linux versions) 105 | // b) Check /proc/cpuinfo Revision ID 106 | 107 | static isPi() { 108 | // Try Device-Tree. Only in kernels from 2017 onwards 109 | try { 110 | var f = utils.execSync('cat /proc/device-tree/model').toString(); 111 | if (f.includes('Raspberry Pi')) return true; 112 | } catch (ex) { 113 | // Try /proc/cpuinfo and a valid Raspberry Pi Model ID 114 | try { 115 | var model = require('rpi-version')(); 116 | if (typeof model != "undefined") return true; 117 | } catch (ex) { 118 | } 119 | } 120 | return false; 121 | } 122 | 123 | 124 | static notPi() { 125 | return /^win/.test(process.platform) || /^darwin/.test(process.platform); 126 | } 127 | 128 | static isWindows() { 129 | return /^win/.test(process.platform); 130 | } 131 | 132 | static isLinux() { 133 | return /^linux/.test(process.platform); 134 | } 135 | 136 | static isMac() { 137 | return /^darwin/.test(process.platform); 138 | } 139 | 140 | static log = { 141 | level: logLevel.Error, 142 | error(message: string, ...args) { 143 | if (utils.log.level > logLevel.None) { 144 | message = clc.red(message); 145 | console.log.apply(this, [message, ...args]); 146 | } 147 | }, 148 | warn(message: string, ...args) { 149 | if (utils.log.level > logLevel.Error) { 150 | message = clc.yellow(message); 151 | console.log.apply(this, [message, ...args]); 152 | } 153 | }, 154 | info(message: string, ...args) { 155 | if (utils.log.level > logLevel.Warn) 156 | console.log.apply(this, [message, ...args]); 157 | }, 158 | debug(message: string, ...args) { 159 | if (utils.log.level > logLevel.Info) { 160 | message = clc.green(message); 161 | console.log.apply(this, [message, ...args]); 162 | } 163 | } 164 | } 165 | static execSync(cmd: string) { 166 | utils.log.debug(["execSync('", cmd, "')"].join('')); 167 | return utils.notPi() ? "" : require('child_process').execSync(cmd); 168 | } 169 | static spawn(cmd: string, args?: string[], options?: {}): ChildProcess { 170 | utils.log.debug(`spawn('${cmd}', [${args.join()}], ${options})`); 171 | if (utils.notPi()) { 172 | return new DummyProcess(); 173 | } 174 | else { 175 | return spawn(cmd, args, options); 176 | } 177 | } 178 | 179 | static cleanup(callback: () => void) { 180 | // https://stackoverflow.com/questions/14031763/doing-a-cleanup-action-just-before-node-js-exits 181 | // attach user callback to the process event emitter 182 | // if no callback, it will still exit gracefully on Ctrl-C 183 | callback = callback || (() => { }); 184 | 185 | // do app specific cleaning before exiting 186 | process.on('exit', () => { 187 | callback; 188 | }); 189 | 190 | // catch ctrl+c event and exit normally 191 | process.on('SIGINT', () => { 192 | console.log('Ctrl-C...'); 193 | process.exit(2); 194 | }); 195 | 196 | //catch uncaught exceptions, trace, then exit normally 197 | process.on('uncaughtException', (e) => { 198 | utils.log.error('Uncaught Exception... : %s', e.stack); 199 | process.exit(99); 200 | }); 201 | } 202 | 203 | static uuid5(str: string) { 204 | var out = crypto.createHash("sha1").update(str).digest(); 205 | 206 | out[8] = out[8] & 0x3f | 0xa0; // set variant 207 | out[6] = out[6] & 0x0f | 0x50; // set version 208 | 209 | let hex = out.toString("hex", 0, 16); 210 | 211 | return [ 212 | hex.substring(0, 8), 213 | hex.substring(8, 12), 214 | hex.substring(12, 16), 215 | hex.substring(16, 20), 216 | hex.substring(20, 32) 217 | ].join("-"); 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /lib/v4l2ctl.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { Utils } from './utils'; 4 | import { writeFileSync, readFileSync } from 'fs'; 5 | var stringifyBool = (v: boolean) => { return v ? "1" : "0"; } 6 | var utils = Utils.utils; 7 | 8 | export module v4l2ctl { 9 | export enum Pixelformat { 10 | YU12 = 0, // 4:2:0, packed YUV 11 | YUYV = 1, // 4:2:2, packed, YUYV 12 | RGB3 = 2, // RGB24 (LE) 13 | JPEG = 3, // JPEG 14 | H264 = 4, // H264 15 | MJPG = 5, // MJPEG 16 | YVYU = 6, // 4:2:2, packed, YVYU 17 | VYUY = 7, // 4:2:2, packed, VYUY 18 | UYVY = 8, // 4:2:2, packed, UYVY 19 | NV12 = 9, // 4:2:0, packed, NV12 20 | BGR3 = 10,// RGB24 (BE) 21 | YV12 = 11,// 4:2:0, packed YVU 22 | NV21 = 12,// 4:2:0, packed, NV21 23 | BGR4 = 13,// RGB32 (BE) 24 | } 25 | 26 | export enum ProcessPriority { 27 | background = 1, 28 | interactive = 2, 29 | record = 3 30 | } 31 | 32 | export class UserControl { 33 | private typeConstructor: TypeConstructor; 34 | private dirty: boolean; 35 | private _value: T; 36 | private options: UserControlOptions; 37 | 38 | constructor(value: T, options: UserControlOptions) { 39 | if (value === undefined || value === null) 40 | throw "'value' is required"; 41 | 42 | this.typeConstructor = value.constructor; 43 | this.dirty = false; 44 | this._value = value === undefined ? null : value; 45 | this.options = options || {}; 46 | this.options.stringify = this.options.stringify || (value => value.toString()); 47 | } 48 | 49 | get value(): T { return this._value; }; 50 | 51 | set value(value: T) { 52 | var val: any = value; 53 | if (this.typeConstructor.name == "Boolean") { 54 | val = (val === true || val === 1 || (val) + "".toLowerCase() === "true" || val === "1"); 55 | } else if (this.typeConstructor.name != "Object") 56 | val = this.typeConstructor(val); 57 | 58 | if (val !== null && val !== undefined) { 59 | if (this.hasRange && (val < this.options.range.min || val > this.options.range.max) && (val !== 0 || !this.options.range.allowZero)) 60 | throw (`value: ${val} not in range: ${this.options.range.min} - ${this.options.range.max}`); 61 | if (this.hasSet && this.options.lookupSet.map(ls=> ls.value).indexOf(val) == -1) 62 | throw (`value: ${val} not in set: ${this.options.lookupSet.map(ls=> `{ value:${ls.value} desc:${ls.desc} }`).join() }`); 63 | } 64 | 65 | if (this.hasRange && this.options.range.step && (val) % (this.options.range.step) !== 0) 66 | val = Math.round(val / (this.options.range.step)) * (this.options.range.step); 67 | 68 | if (val !== this._value) 69 | this.dirty = true; 70 | 71 | this._value = val; 72 | } 73 | 74 | get desc():string{ 75 | if (this.hasSet) 76 | // search the lookup set for this.value and return desc 77 | for (var l of this.options.lookupSet) { 78 | if (l.value === this.value) return l.desc; 79 | } 80 | throw "'lookup value' not in lookup set"; 81 | } 82 | 83 | get type(): string { 84 | return this.typeConstructor.name; 85 | } 86 | 87 | get hasSet(): boolean { 88 | return (this.options.lookupSet || []).length > 0; 89 | }; 90 | 91 | getLookupSet(): UserControlsLookupSet { 92 | var result = new Array>(0); 93 | for (var l of this.options.lookupSet) { 94 | result.push({ 95 | value: l.value, 96 | desc: l.desc 97 | }) 98 | } 99 | return result; 100 | }; 101 | get hasRange(): boolean { 102 | return !!this.options.range; 103 | }; 104 | 105 | getRange() { 106 | if (this.hasRange) 107 | return { min: this.options.range.min, max: this.options.range.max }; 108 | 109 | return null; 110 | } 111 | 112 | get isDirty(): boolean { 113 | return this.dirty; 114 | }; 115 | 116 | reset() { 117 | this.dirty = false; 118 | }; 119 | 120 | toString(): string { 121 | return this.options.stringify(this._value); 122 | } 123 | } 124 | 125 | export var Controls = { 126 | UserControls: { 127 | // min=0 max=100 step=1 default=50 value=50 flags=slider 128 | brightness: new UserControl(50, { range: { min: 0, max: 100 } }), 129 | 130 | // min=-100 max=100 step=1 default=0 value=0 flags=slider 131 | contrast: new UserControl(0, { range: { min: -100, max: 100 } }), 132 | 133 | // min=-100 max=100 step=1 default=0 value=0 flags=slider 134 | saturation: new UserControl(0, { range: { min: -100, max: 100 } }), 135 | 136 | // min=1 max=7999 step=1 default=1000 value=1000 flags=slider 137 | red_balance: new UserControl(1000, { range: { min: 1, max: 7999 } }), 138 | 139 | // min=1 max=7999 step=1 default=1000 value=1000 flags=slider 140 | blue_balance: new UserControl(1000, { range: { min: 1, max: 7999 } }), 141 | 142 | // default=0 value=0 143 | horizontal_flip: new UserControl(false, { stringify: stringifyBool }), 144 | 145 | // default=0 value=0 146 | vertical_flip: new UserControl(false, { stringify: stringifyBool }), 147 | 148 | // min=0 max=3 default=1 value=1 | 0: Disabled,1: 50 Hz,2: 60 Hz,3: Auto 149 | power_line_frequency: new UserControl(1, { lookupSet: [{ value: 0, desc: 'Disabled' }, { value: 1, desc: '50 Hz' }, { value: 2, desc: '60 Hz' }, { value: 3, desc: 'Auto' }] }), 150 | 151 | // min=-100 max=100 step=1 default=0 value=0 flags=slider 152 | sharpness: new UserControl(0, { range: { min: -100, max: 100 } }), 153 | 154 | // min=0 max=15 default=0 value=0 | 0: None,1: Black & White,2: Sepia,3: Negative,4: Emboss,5: Sketch,6: Sky Blue,7: Grass Green,8: Skin Whiten,9: Vivid,10: Aqua,11: Art Freeze,12: Silhouette,13: Solarization,14: Antique,15: Set Cb/Cr 155 | color_effects: new UserControl(0, { lookupSet: [{ value: 0, desc: 'None' }, { value: 1, desc: 'Black & White' }, { value: 2, desc: 'Sepia' }, { value: 3, desc: 'Negative' }, { value: 4, desc: 'Emboss' }, { value: 5, desc: 'Sketch' }, { value: 6, desc: 'Sky Blue' }, { value: 7, desc: 'Grass Green' }, { value: 8, desc: 'Skin Whiten' }, { value: 9, desc: 'Vivid' }, { value: 10, desc: 'Aqua' }, { value: 11, desc: 'Art Freeze' }, { value: 12, desc: 'Silhouette' }, { value: 13, desc: 'Solarization' }, { value: 14, desc: 'Antique' }, { value: 15, desc: 'Set Cb/Cr' }] }), 156 | // min=0 max=360 step=90 default=0 value=0 157 | rotate: new UserControl(0, { range: { min: 0, max: 360 } }), 158 | // min=0 max=65535 step=1 default=32896 value=32896 159 | color_effects_cbcr: new UserControl(32896, { range: { min: 0, max: 65535 } }), 160 | }, 161 | CodecControls: { 162 | // min=0 max=1 default=0 value=0 flags=update | 0: Variable Bitrate,1: Constant Bitrate 163 | video_bitrate_mode: new UserControl(0, { lookupSet: [{ value: 0, desc: 'Variable Bitrate' }, { value: 1, desc: 'Constant Bitrate' }] }), 164 | // min=25000 max=25000000 step=25000 default=10000000 value=10000000 165 | video_bitrate: new UserControl(10000000, { range: { min: 25000, max: 25000000, step: 25000, allowZero: true } }), 166 | // default=0 value=0 167 | repeat_sequence_header: new UserControl(false, { stringify: stringifyBool }), 168 | // min=0 max=2147483647 step=1 default=60 value=60 169 | h264_i_frame_period: new UserControl(60, { range: { min: 0, max: 2147483647 } }), 170 | // min=0 max=11 default=11 value=11 | 0:1,1:1b,2:1.1,3:1.2,4:1.3,5:2,6:2.1,7:2.2,8:3,9:3.1,10:3.2,11:4 171 | h264_level: new UserControl(11, { lookupSet: [{ value: 0, desc: '1' }, { value: 1, desc: '1b' }, { value: 2, desc: '1.1' }, { value: 3, desc: '1.2' }, { value: 4, desc: '1.3' }, { value: 5, desc: '2' }, { value: 6, desc: '2.1' }, { value: 7, desc: '2.2' }, { value: 8, desc: '3' }, { value: 9, desc: '3.1' }, { value: 10, desc: '3.2' }, { value: 11, desc: '4' }] }), 172 | // min=0 max=4 default=4 value=4 | 0:Baseline,1:Constrained Baseline,2:Main,4:High 173 | h264_profile: new UserControl(4, { lookupSet: [{ value: 0, desc: 'Baseline' }, { value: 1, desc: 'Constrained Baseline' }, { value: 2, desc: 'Main' }, { value: 4, desc: 'High' }] }) 174 | }, 175 | CameraControls: { 176 | // min=0 max=3 default=0 value=0 177 | auto_exposure: new UserControl(false, { stringify: stringifyBool }), 178 | // min=1 max=10000 step=1 default=1000 value=1000 179 | exposure_time_absolute: new UserControl(1000, { range: { min: 0, max: 10000 } }), 180 | // default=0 value=0 181 | exposure_dynamic_framerate: new UserControl(false, { stringify: stringifyBool }), 182 | // min=0 max=24 default=12 value=12 183 | auto_exposure_bias: new UserControl(12, { range: { min: 0, max: 24 } }), 184 | // min=0 max=9 default=1 value=1 | 0:Manual,1:Auto,2:Incandescent,3:Fluorescent,4:Fluorescent,5:Horizon,6:Daylight,7:Flash,8:Cloudy,9:Shade 185 | white_balance_auto_preset: new UserControl(1, { lookupSet: [{ value: 0, desc: 'Manual' }, { value: 1, desc: 'Auto' }, { value: 2, desc: 'Incandescent' }, { value: 3, desc: 'Fluorescent' }, { value: 4, desc: 'Fluorescent' }, { value: 5, desc: 'Horizon' }, { value: 6, desc: 'Daylight' }, { value: 7, desc: 'Flash' }, { value: 8, desc: 'Cloudy' }, { value: 9, desc: 'Shade' }] }), 186 | // default=0 value=0 187 | image_stabilization: new UserControl(false, { stringify: stringifyBool }), 188 | // min=0 max=4 default=0 value=0 | 0: 0,1: 100,2: 200,3: 400,4: 800, 189 | iso_sensitivity: new UserControl(0, { lookupSet: [{ value: 0, desc: '0' }, { value: 1, desc: '100' }, { value: 2, desc: '200' }, { value: 3, desc: '400' }, { value: 4, desc: '800' }] }), 190 | // min=0 max=2 default=0 value=0 | 0: Average,1: Center Weighted,2: Spot, 191 | exposure_metering_mode: new UserControl(0, { lookupSet: [{ value: 0, desc: 'Average' }, { value: 1, desc: 'Center Weighted' }, { value: 2, desc: 'Spot' }] }), 192 | // min=0 max=13 default=0 value=0 | 0: None,8: Night,11: Sports 193 | scene_mode: new UserControl(0, { lookupSet: [{ value: 0, desc: 'None' }, { value: 8, desc: 'Night' }, { value: 11, desc: 'Sport' }] }) 194 | }, 195 | JPEGCompressionControls: { 196 | // min=1 max=100 step=1 default=30 value=30 197 | compression_quality: new UserControl(30, { range: { min: 1, max: 100 } }) 198 | } 199 | }; 200 | 201 | function execV4l2(cmd: string): string { 202 | try { 203 | return utils.execSync(`v4l2-ctl ${cmd}`).toString(); 204 | } catch (err) { 205 | return ''; 206 | } 207 | } 208 | 209 | export function ApplyControls() { 210 | var usercontrols = Controls.UserControls; 211 | var codeccontrols = Controls.CodecControls; 212 | var cameracontrols = Controls.CameraControls; 213 | var jpgcontrols = Controls.JPEGCompressionControls; 214 | 215 | var getChanges = function(controls: {}) { 216 | var changes = []; 217 | for (var c in controls) { 218 | var control = >controls[c]; 219 | if (!control.isDirty) 220 | continue; 221 | 222 | changes.push([c, "=", control].join('')); 223 | control.reset(); 224 | } 225 | return changes; 226 | }; 227 | 228 | var changedcontrols = getChanges(usercontrols) 229 | .concat(getChanges(codeccontrols)) 230 | .concat(getChanges(cameracontrols)) 231 | .concat(getChanges(jpgcontrols)); 232 | 233 | if (changedcontrols.length > 0) { 234 | execV4l2(`--set-ctrl ${changedcontrols.join(',') }`); 235 | WriteToFile(); 236 | } 237 | } 238 | 239 | export function WriteToFile() { 240 | var data = {}; 241 | for (var ct in Controls) { 242 | data[ct] = {}; 243 | for (var k in Controls[ct]) { 244 | var uc = >Controls[ct][k]; 245 | data[ct][k] = uc.value; 246 | } 247 | } 248 | var json = JSON.stringify(data); 249 | json = json.replace(/{"/g,"{\n\"").replace(/:{/g, ":\n{").replace(/,"/g, ",\n\"").replace(/}/g,"}\n"); 250 | writeFileSync("v4l2ctl.json", json); 251 | } 252 | 253 | export function ReadFromFile() { 254 | try { 255 | var data = JSON.parse(readFileSync("v4l2ctl.json").toString()); 256 | for (var ct in data) { 257 | for (var k in data[ct]) { 258 | var uc = >Controls[ct][k]; 259 | uc.value = data[ct][k]; 260 | } 261 | } 262 | } catch (ex) { 263 | utils.log.error("v4l2ctl.json does not exist yet or invalid.") 264 | } 265 | } 266 | 267 | export function ReadControls() { 268 | var settings = execV4l2("-l"); 269 | var regexPart = "\\s.*value=([0-9]*)"; 270 | 271 | var getControls = function(controls) { 272 | for (var c in controls) { 273 | var control = controls[c]; 274 | var value = settings.match(new RegExp([c, regexPart].join(''))); 275 | if (!value || (value.length > 1 && value[1] === "" && c == "auto_exposure")) //-- fix for typo in camera driver! 276 | value = settings.match(new RegExp([c.substr(0, c.length - 1), regexPart].join(''))); 277 | if (value && value.length > 1) { 278 | utils.log.debug("Controlvalue '%s' : %s", c, value[1]); 279 | try { 280 | control.value = value[1]; 281 | control.reset(); 282 | } catch (ex) { 283 | utils.log.error(ex); 284 | } 285 | } else { 286 | utils.log.error("Could not retrieve Controlvalue '%s'", c); 287 | } 288 | } 289 | }; 290 | 291 | var usercontrols = Controls.UserControls; 292 | var codeccontrols = Controls.CodecControls; 293 | var cameracontrols = Controls.CameraControls; 294 | var jpgcontrols = Controls.JPEGCompressionControls; 295 | getControls(usercontrols); 296 | getControls(codeccontrols); 297 | getControls(cameracontrols); 298 | getControls(jpgcontrols); 299 | 300 | WriteToFile(); 301 | } 302 | 303 | export function SetFrameRate(framerate: number) { 304 | execV4l2(`--set-parm=${framerate}`); 305 | } 306 | 307 | export function SetResolution(resolution: Resolution) { 308 | execV4l2(`--set-fmt-video=width=${resolution.Width},height=${resolution.Height}`); 309 | } 310 | 311 | export function SetPixelFormat(pixelformat: Pixelformat) { 312 | execV4l2(`--set-fmt-video=pixelformat=${pixelformat}`); 313 | } 314 | 315 | export function SetPriority(priority: ProcessPriority) { 316 | execV4l2(`--set-priority=${priority}`); 317 | } 318 | 319 | export function SetBrightness(brightness: number) { 320 | execV4l2(`--set-ctrl brightness=${brightness}`); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rpos", 3 | "version": "2.1.0", 4 | "description": "rpos", 5 | "main": "rpos.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Jeroen Versteege", 9 | "email": "jeroen@versteege.com" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Roger Hardiman" 14 | } 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Breeeze/rpos.git" 19 | }, 20 | "dependencies": { 21 | "body-parser": "1.19.0", 22 | "cli-color": "2.0.0", 23 | "express": "^4.17.1", 24 | "ip": "^1.1.5", 25 | "macos-release": "~2.0.0", 26 | "node-pelcod": "^0.3.0", 27 | "node-uuid": "1.4.8", 28 | "pan-tilt-hat": "^1.2.1", 29 | "rpi-version": "^1.4.3", 30 | "serialport": "^9.1.0", 31 | "soap": "https://github.com/BreeeZe/node-soap.git", 32 | "tenx-usb-missile-launcher-driver": "^0.2.0", 33 | "xml2js": "0.4.23" 34 | }, 35 | "devDependencies": { 36 | "@types/body-parser": "^1.19.0", 37 | "@types/cli-color": "^2.0.0", 38 | "@types/express": "^4.17.11", 39 | "@types/node": "^13.1.5", 40 | "@types/node-uuid": "0.0.28", 41 | "@types/xml2js": "^0.4.8", 42 | "gulp": "^4.0.2", 43 | "gulp-rename": "^2.0.0", 44 | "gulp-sourcemaps": "^3.0.0", 45 | "gulp-typescript": "^5.0.1", 46 | "gulp-zip": "^5.1.0", 47 | "typescript": "^4.3.2" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /python/gst-rtsp-launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo $@ 3 | 4 | # Ubuntu 21 installs Python3 by default and appears to have dropped the Python 2 'Gi' package that 5 | # is required for Gstreamer GObject Introspection 6 | 7 | FILE=/usr/bin/python3 8 | if test -f "$FILE"; then 9 | echo "Found /usr/bin/python3" 10 | /usr/bin/python3 ./python/gst-rtsp-launch.py $@ -v 11 | else 12 | echo "using /usr/bin/python" 13 | /usr/bin/python ./python/gst-rtsp-launch.py $@ -v 14 | fi 15 | 16 | -------------------------------------------------------------------------------- /python/run-filesrc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo View with rtsp://localhost:8554/h264 4 | /usr/bin/python ./python/gst-rtsp-launch.py --type filesrc --device "testimage.jpg" --rtspresolutionwidth 900 --rtspresolutionheight 800 --rtspport 8554 --rtspname h264 -v 5 | -------------------------------------------------------------------------------- /python/run-testsrc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo View with rtsp://localhost:8554/h264 4 | /usr/bin/python ./python/gst-rtsp-launch.py --type testsrc --rtspresolutionwidth 640 --rtspresolutionheight 480 --rtspport 8554 --rtspname h264 -v 5 | -------------------------------------------------------------------------------- /raspberry_pi_missile_launcher.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/raspberry_pi_missile_launcher.jpg -------------------------------------------------------------------------------- /rpos.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | interface rposConfig { 3 | NetworkAdapters: string[]; 4 | IpAddress: string; 5 | ServicePort: number; 6 | Username: string; 7 | Password: string; 8 | CameraType: string; 9 | CameraDevice: string; 10 | RTSPAddress: string; 11 | RTSPPort: number; 12 | RTSPName: string; 13 | RTSPServer: number; 14 | MulticastEnabled: boolean; 15 | RTSPMulticastName : string; 16 | MulticastAddress: string; 17 | MulticastPort: number; 18 | PTZDriver: string; 19 | PTZOutput: string; 20 | PTZSerialPort: string; 21 | PTZSerialPortSettings: PTZSerialPortSettings; 22 | PTZOutputURL: string; 23 | PTZCameraAddress: number; 24 | DeviceInformation: DeviceInformation; 25 | logLevel: number; 26 | logSoapCalls: Boolean; 27 | } 28 | 29 | interface PTZSerialPortSettings { 30 | baudRate: number; 31 | dataBits: number; 32 | parity: string; 33 | stopBits: number; 34 | } 35 | 36 | interface DeviceInformation { 37 | Manufacturer: string; 38 | Model: string; 39 | HardwareId: string; 40 | SerialNumber: string; 41 | FirmwareVersion: string; 42 | } 43 | 44 | interface TypeConstructor extends Function { 45 | name: string; 46 | } 47 | 48 | interface SoapServiceOptions { 49 | path: string, 50 | services: any, 51 | xml: any, 52 | wsdlPath: string, 53 | onReady: () => void; 54 | } 55 | 56 | interface Date { 57 | stdTimezoneOffset: () => number; 58 | dst: () => boolean; 59 | } 60 | 61 | interface UserControlOptions { 62 | stringify?: (T) => string, 63 | range?: { 64 | min: T, 65 | max: T, 66 | allowZero?: boolean, 67 | step?: T 68 | } 69 | lookupSet?: UserControlsLookupSet; 70 | } 71 | 72 | interface UserControlsLookup { 73 | value: T; 74 | desc: string; 75 | } 76 | interface UserControlsLookupSet extends Array> { 77 | 78 | } 79 | 80 | interface Resolution { 81 | Width: number; 82 | Height: number; 83 | } 84 | interface CameraSettingsParameter { 85 | gop: number; //keyframe every X sec. 86 | resolution: Resolution; 87 | framerate: number; 88 | bitrate: number; 89 | profile: string; 90 | quality: number; 91 | } 92 | interface CameraSettingsBase { 93 | forceGop: boolean; // Use iframe interval setting from v4l2ctl.json instead of Onvif 94 | resolution: Resolution; 95 | framerate: number; 96 | } 97 | -------------------------------------------------------------------------------- /rpos.service: -------------------------------------------------------------------------------- 1 | # RPOS Service. 2 | # Contributed by Charbel Daher cdaher78 3 | 4 | [Unit] 5 | Description=ONVIF/RTSP Server 6 | Requires=network.target 7 | After=network.target 8 | 9 | [Service] 10 | Type=simple 11 | Restart=always 12 | StandardOutput=syslog 13 | StandardError=syslog 14 | SyslogIdentifier=onvifcam 15 | User=pi 16 | WorkingDirectory=/home/pi/rpos 17 | ExecStart=/usr/bin/node rpos.js 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /rpos.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* 4 | The MIT License(MIT) 5 | 6 | Copyright(c) 2015 Jeroen Versteege 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files(the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject tothe following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | */ 26 | require("./lib/extension"); 27 | 28 | import http = require("http"); 29 | import express = require("express"); 30 | import fs = require('fs'); 31 | import os = require('os'); 32 | import { Utils } from "./lib/utils"; 33 | import Camera = require("./lib/camera"); 34 | import PTZDriver = require("./lib/PTZDriver"); 35 | import DeviceService = require("./services/device_service"); 36 | import MediaService = require("./services/media_service"); 37 | import PTZService = require("./services/ptz_service"); 38 | import ImagingService = require("./services/imaging_service"); 39 | import DiscoveryService = require("./services/discovery_service"); 40 | 41 | import { exit } from "process"; 42 | 43 | var utils = Utils.utils; 44 | let pjson = require("./package.json"); 45 | 46 | let configFile = './rposConfig.json'; 47 | 48 | let ptr = 0; 49 | let remaining = process.argv.length; 50 | 51 | // Skip over the executable name (eg node.exe) and the rpos.js script name 52 | ptr += 2; 53 | remaining -= 2; 54 | 55 | // parse any other parameters 56 | while (remaining > 0) { 57 | if (process.argv[ptr] == '--help' || process.argv[ptr] == '-h') { 58 | console.log("RPOS ONVIF Server\r\n"); 59 | console.log(" -h --help Show Commands"); 60 | console.log(" --config Config Filename"); 61 | exit(); 62 | } 63 | else if (process.argv[ptr] == '--config' && remaining >= 2) { 64 | configFile = process.argv[ptr + 1]; 65 | ptr += 2; 66 | remaining -= 2; 67 | } else { 68 | ptr += 1; 69 | remaining -= 1; 70 | } 71 | } 72 | 73 | // Load the Config File 74 | let data = fs.readFileSync(configFile, 'utf8'); 75 | if (typeof data == 'string' && data.charCodeAt(0) === 0xFEFF) { 76 | data = data.slice(1); // strip off the utf8 BO marker bytes 77 | } 78 | let config = JSON.parse(data); 79 | 80 | utils.log.level = config.logLevel; 81 | 82 | // config.DeviceInformation has Manufacturer, Model, SerialNumer, FirmwareVersion, HardwareId 83 | // Probe hardware for values, unless they are given in rposConfig.json 84 | config.DeviceInformation = config.DeviceInformation || {}; 85 | 86 | if (utils.isPi()) { 87 | var model = require('rpi-version')(); 88 | if (config.DeviceInformation.Manufacturer == undefined) config.DeviceInformation.Manufacturer = 'RPOS Raspberry Pi'; 89 | if (config.DeviceInformation.Model == undefined) config.DeviceInformation.Model = model; 90 | } 91 | 92 | if (utils.isMac()) { 93 | const macosRelease = require('macos-release'); 94 | if (config.DeviceInformation.Manufacturer == undefined) config.DeviceInformation.Manufacturer = 'RPOS AppleMac'; 95 | if (config.DeviceInformation.Model == undefined) config.DeviceInformation.Model = macosRelease()['name'] + ' ' + macosRelease()['version']; 96 | } 97 | 98 | if (utils.isWindows()) { 99 | if (config.DeviceInformation.Manufacturer == undefined) config.DeviceInformation.Manufacturer = 'RPOS Windows'; 100 | if (config.DeviceInformation.Model == undefined) config.DeviceInformation.Model = os.version; 101 | } 102 | 103 | if (config.DeviceInformation.Manufacturer == undefined) config.DeviceInformation.Manufacturer = 'RPOS'; 104 | if (config.DeviceInformation.Model == undefined) config.DeviceInformation.Model = 'RPOS'; 105 | if (config.DeviceInformation.SerialNumber == undefined) config.DeviceInformation.SerialNumber = utils.getSerial(); 106 | if (config.DeviceInformation.FirmwareVersion == undefined) config.DeviceInformation.FirmwareVersion = pjson.version; 107 | if (config.DeviceInformation.HardwareId == undefined) config.DeviceInformation.HardwareId = '1001'; 108 | 109 | utils.setConfig(config); 110 | utils.testIpAddress(); 111 | 112 | for (var i in config.DeviceInformation) { 113 | utils.log.info("%s : %s", i, config.DeviceInformation[i]); 114 | } 115 | 116 | let webserver = express(); 117 | let httpserver = http.createServer(webserver); 118 | httpserver.listen(config.ServicePort); 119 | 120 | let ptz_driver = new PTZDriver(config); 121 | 122 | let camera = new Camera(config, webserver); 123 | let device_service = new DeviceService(config, httpserver, ptz_driver.process_ptz_command); 124 | let ptz_service = new PTZService(config, httpserver, ptz_driver.process_ptz_command, ptz_driver); 125 | let imaging_service = new ImagingService(config, httpserver, ptz_driver.process_ptz_command); 126 | let media_service = new MediaService(config, httpserver, camera, ptz_service); // note ptz_service dependency 127 | let discovery_service = new DiscoveryService(config); 128 | 129 | device_service.start(); 130 | media_service.start(); 131 | ptz_service.start(); 132 | imaging_service.start(); 133 | discovery_service.start(); 134 | -------------------------------------------------------------------------------- /rposConfig.sample-picam.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkAdapters" : ["awdl0","eth0", "wlan0", "en0"], 3 | "IpAddress" : "192.168.1.15", 4 | "ServicePort" : 8081, 5 | "Username" : "", 6 | "Password" : "", 7 | "CameraType" : "picam", 8 | "RTSPAddress" : "", "//":"Normally left blank. Used to set RTSP Server Address", 9 | "RTSPPort" : 8554, 10 | "RTSPName" : "h264", 11 | "MulticastEnabled" : false, 12 | "RTSPMulticastName" : "h264m", 13 | "MulticastAddress" : "224.0.0.1", 14 | "MulticastPort" : "10001", 15 | "RTSPServer" : 1, "RtspServerComment" : "## Select RTSP Server > 1:RPOS RTSP Server 2:V4L2 RTSP Server by mpromonet (auto selected if MulticastEnabled=true)", 16 | "PTZDriver" : "none", "PTZDriverComment": "## valid values are none,tenx,pelcod,visca and pan-tilt-hat", 17 | "PTZOutput" : "none", "PTZOutputComment": "## values are none (eg Tenx), serial and tcp", 18 | "PTZSerialPort" : "/dev/ttyUSB0", 19 | "PTZSerialPortSettings" : { "baudRate":2400, "dataBits":8, "parity":"none", "stopBits":1 }, 20 | "PTZOutputURL": "127.0.0.1:9999", 21 | "PTZCameraAddress": 1, 22 | "DeviceInformation" : { 23 | "Manufacturer" : "Raspberry Pi", 24 | "Model" : "2 B", 25 | "HardwareId" : "" 26 | }, 27 | "logLevel" : 3, "logLevelComment": "## LogLevels are > 1:Error 2:Warning 3:Info 4:Debug", 28 | "logSoapCalls" : false 29 | } 30 | -------------------------------------------------------------------------------- /rposConfig.sample-proxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkAdapters" : ["awdl0","eth0", "wlan0", "en0"], 3 | "IpAddress" : "192.168.1.15", 4 | "ServicePort" : 8081, 5 | "Username" : "proxyuser", 6 | "Password" : "proxypassword", 7 | "CameraType" : "picam", 8 | "RTSPAddress" : "hostname_of_remote_rtsp_server", "//":"Normally left blank. Used to set RTSP Server Address", 9 | "RTSPPort" : 8554, 10 | "RTSPName" : "path_to_video_tream", 11 | "MulticastEnabled" : false, 12 | "RTSPMulticastName" : "h264m", 13 | "MulticastAddress" : "224.0.0.1", 14 | "MulticastPort" : "10001", 15 | "RTSPServer" : 0, "RtspServerComment" : "## Select RTSP Server > 0:Use RTSP Server from 'RTSPAddress:' 1:RPOS RTSP Server 2:V4L2 RTSP Server by mpromonet (auto selected if MulticastEnabled=true) 3:GStreamer RTSP Server", 16 | "PTZDriver" : "none", "PTZDriverComment": "## valid values are none,tenx,pelcod,visca and pan-tilt-hat", 17 | "PTZOutput" : "none", "PTZOutputComment": "## values are none (eg Tenx), serial and tcp", 18 | "PTZSerialPort" : "/dev/ttyUSB0", 19 | "PTZSerialPortSettings" : { "baudRate":2400, "dataBits":8, "parity":"none", "stopBits":1 }, 20 | "PTZOutputURL": "127.0.0.1:9999", 21 | "PTZCameraAddress": 1, 22 | "DeviceInformation" : { 23 | "Manufacturer" : "RPOS Proxy", 24 | "Model" : "PROXY", 25 | "HardwareId" : "" 26 | }, 27 | "logLevel" : 3, "logLevelComment": "## LogLevels are > 1:Error 2:Warning 3:Info 4:Debug", 28 | "logSoapCalls" : false 29 | } 30 | -------------------------------------------------------------------------------- /rposConfig.sample-testfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkAdapters" : ["awdl0","eth0", "wlan0", "en0"], 3 | "IpAddress" : "192.168.1.185", 4 | "ServicePort" : 8081, 5 | "Username" : "admin", 6 | "Password" : "admin", 7 | "CameraType" : "testsrc", 8 | "CameraDevice" : "", 9 | "RTSPAddress" : "", "//":"Normally left blank. Used to set RTSP Server Address", 10 | "RTSPPort" : 8554, 11 | "RTSPName" : "h264", 12 | "MulticastEnabled" : false, "MulticastEnabledComment" : "## Multicast is not supported for USB camera", 13 | "RTSPMulticastName" : "h264m", 14 | "MulticastAddress" : "224.0.0.1", 15 | "MulticastPort" : "10001", 16 | "RTSPServer" : 3, "RtspServerComment" : "## Select RTSP Server > 1:RPOS RTSP Server 2:V4L2 RTSP Server by mpromonet (auto selected if MulticastEnabled=true) 3:GStreamer", 17 | "PTZDriver" : "pelcod", "PTZDriverComment": "## valid values are none,tenx,pelcod,visca and pan-tilt-hat", 18 | "PTZOutput" : "none", "PTZOutputComment": "## values are none (eg Tenx), serial and tcp", 19 | "PTZSerialPort" : "", 20 | "PTZSerialPortSettings" : { "baudRate":2400, "dataBits":8, "parity":"none", "stopBits":1 }, 21 | "PTZOutputURL": "127.0.0.1:9999", 22 | "PTZCameraAddress": 1, 23 | "DeviceInformation" : { 24 | "Manufacturer" : "RPOS Test Card", 25 | "Model" : "Test Card", 26 | "HardwareId" : "" 27 | }, 28 | "logLevel" : 3, "logLevelComment": "## LogLevels are > 1:Error 2:Warning 3:Info 4:Debug", 29 | "logSoapCalls" : false 30 | } 31 | -------------------------------------------------------------------------------- /rposConfig.sample-testimage.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkAdapters" : ["awdl0","eth0", "wlan0", "en0"], 3 | "IpAddress" : "192.168.1.185", 4 | "ServicePort" : 8081, 5 | "Username" : "admin", 6 | "Password" : "admin", 7 | "CameraType" : "testsrc", 8 | "CameraDevice" : "", 9 | "RTSPAddress" : "", "//":"Normally left blank. Used to set RTSP Server Address", 10 | "RTSPPort" : 8554, 11 | "RTSPName" : "h264", 12 | "MulticastEnabled" : false, "MulticastEnabledComment" : "## Multicast is not supported for USB camera", 13 | "RTSPMulticastName" : "h264m", 14 | "MulticastAddress" : "224.0.0.1", 15 | "MulticastPort" : "10001", 16 | "RTSPServer" : 3, "RtspServerComment" : "## Select RTSP Server > 1:RPOS RTSP Server 2:V4L2 RTSP Server by mpromonet (auto selected if MulticastEnabled=true) 3:GStreamer", 17 | "PTZDriver" : "pelcod", "PTZDriverComment": "## valid values are none,tenx,pelcod,visca and pan-tilt-hat", 18 | "PTZOutput" : "none", "PTZOutputComment": "## values are none (eg Tenx), serial and tcp", 19 | "PTZSerialPort" : "", 20 | "PTZSerialPortSettings" : { "baudRate":2400, "dataBits":8, "parity":"none", "stopBits":1 }, 21 | "PTZOutputURL": "127.0.0.1:9999", 22 | "PTZCameraAddress": 1, 23 | "DeviceInformation" : { 24 | "Manufacturer" : "RPOS Test Card", 25 | "Model" : "Test Card", 26 | "HardwareId" : "" 27 | }, 28 | "logLevel" : 3, "logLevelComment": "## LogLevels are > 1:Error 2:Warning 3:Info 4:Debug", 29 | "logSoapCalls" : false 30 | } 31 | -------------------------------------------------------------------------------- /rposConfig.sample-usbcam.json: -------------------------------------------------------------------------------- 1 | { 2 | "NetworkAdapters" : ["awdl0","eth0", "wlan0", "en0"], 3 | "IpAddress" : "192.168.1.15", 4 | "ServicePort" : 8081, 5 | "Username" : "", 6 | "Password" : "", 7 | "CameraType" : "usbcam", 8 | "CameraDevice" : "/dev/video0", 9 | "RTSPAddress" : "", "//":"Normally left blank. Used to set RTSP Server Address", 10 | "RTSPPort" : 8554, 11 | "RTSPName" : "h264", 12 | "MulticastEnabled" : false, "MulticastEnabledComment" : "## Multicast is not supported for USB camera", 13 | "RTSPMulticastName" : "h264m", 14 | "MulticastAddress" : "224.0.0.1", 15 | "MulticastPort" : "10001", 16 | "RTSPServer" : 3, "RtspServerComment" : "## Select RTSP Server > 1:RPOS RTSP Server 2:V4L2 RTSP Server by mpromonet (auto selected if MulticastEnabled=true)", 17 | "PTZDriver" : "none", "PTZDriverComment": "## valid values are none,tenx,pelcod,visca and pan-tilt-hat", 18 | "PTZOutput" : "none", "PTZOutputComment": "## values are none (eg Tenx), serial and tcp", 19 | "PTZSerialPort" : "/dev/ttyUSB0", 20 | "PTZSerialPortSettings" : { "baudRate":2400, "dataBits":8, "parity":"none", "stopBits":1 }, 21 | "PTZOutputURL": "127.0.0.1:9999", 22 | "PTZCameraAddress": 1, 23 | "DeviceInformation" : { 24 | "Manufacturer" : "Raspberry Pi", 25 | "Model" : "2 B", 26 | "HardwareId" : "" 27 | }, 28 | "logLevel" : 3, "logLevelComment": "## LogLevels are > 1:Error 2:Warning 3:Info 4:Debug", 29 | "logSoapCalls" : false 30 | } 31 | -------------------------------------------------------------------------------- /sample_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/sample_image.jpg -------------------------------------------------------------------------------- /services/device_service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import fs = require("fs"); 4 | import util = require("util"); 5 | import os = require('os'); 6 | import SoapService = require('../lib/SoapService'); 7 | import { Utils } from '../lib/utils'; 8 | import { Server } from 'http'; 9 | import ip = require('ip'); 10 | var utils = Utils.utils; 11 | 12 | class DeviceService extends SoapService { 13 | device_service: any; 14 | callback: any; 15 | 16 | constructor(config: rposConfig, server: Server, callback) { 17 | super(config, server); 18 | 19 | this.device_service = require('./stubs/device_service.js').DeviceService; 20 | this.callback = callback; 21 | 22 | this.serviceOptions = { 23 | path: '/onvif/device_service', 24 | services: this.device_service, 25 | xml: fs.readFileSync('./wsdl/device_service.wsdl', 'utf8'), 26 | wsdlPath: 'wsdl/device_service.wsdl', 27 | onReady: () => console.log('device_service started') 28 | }; 29 | 30 | this.extendService(); 31 | } 32 | 33 | extendService() { 34 | var port = this.device_service.DeviceService.Device; 35 | 36 | port.GetDeviceInformation = (args /*, cb, headers*/) => { 37 | var GetDeviceInformationResponse = { 38 | Manufacturer: this.config.DeviceInformation.Manufacturer, 39 | Model: this.config.DeviceInformation.Model, 40 | FirmwareVersion: this.config.DeviceInformation.FirmwareVersion, 41 | SerialNumber: this.config.DeviceInformation.SerialNumber, 42 | HardwareId: this.config.DeviceInformation.HardwareId 43 | }; 44 | return GetDeviceInformationResponse; 45 | }; 46 | 47 | port.GetSystemDateAndTime = (args /*, cb, headers*/) => { 48 | var now = new Date(); 49 | 50 | // Ideally this code would compute a full POSIX TZ string with daylight saving 51 | // For now we will compute the current time zone as a UTC offset 52 | // Note that what we call UTC+ 1 in called UTC-1 in Posix TZ format 53 | var offset = now.getTimezoneOffset(); 54 | var abs_offset = Math.abs(offset); 55 | var hrs_offset = Math.floor(abs_offset / 60); 56 | var mins_offset = (abs_offset % 60); 57 | var tz = "UTC" + (offset < 0 ? '-' : '+') + hrs_offset + (mins_offset === 0 ? '' : ':' + mins_offset); 58 | 59 | var GetSystemDateAndTimeResponse = { 60 | SystemDateAndTime: { 61 | DateTimeType: "NTP", 62 | DaylightSavings: now.dst(), 63 | TimeZone: { 64 | TZ: tz 65 | }, 66 | UTCDateTime: { 67 | Time: { Hour: now.getUTCHours(), Minute: now.getUTCMinutes(), Second: now.getUTCSeconds() }, 68 | Date: { Year: now.getUTCFullYear(), Month: now.getUTCMonth() + 1, Day: now.getUTCDate() } 69 | }, 70 | LocalDateTime: { 71 | Time: { Hour: now.getHours(), Minute: now.getMinutes(), Second: now.getSeconds() }, 72 | Date: { Year: now.getFullYear(), Month: now.getMonth() + 1, Day: now.getDate() } 73 | }, 74 | Extension: {} 75 | } 76 | }; 77 | return GetSystemDateAndTimeResponse; 78 | }; 79 | 80 | port.SetSystemDateAndTime = (args /*, cb, headers*/) => { 81 | var SetSystemDateAndTimeResponse = {}; 82 | return SetSystemDateAndTimeResponse; 83 | }; 84 | 85 | port.SystemReboot = (args /*, cb, headers*/) => { 86 | var SystemRebootResponse = { 87 | Message: utils.execSync("sudo reboot") 88 | }; 89 | return SystemRebootResponse; 90 | }; 91 | 92 | port.GetServices = (args /*, cb, headers*/) => { 93 | // ToDo. Check value of args.IncludeCapability 94 | 95 | var GetServicesResponse = { 96 | Service : [ 97 | { 98 | Namespace : "http://www.onvif.org/ver10/device/wsdl", 99 | XAddr : `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/device_service`, 100 | Version : { 101 | Major : 2, 102 | Minor : 5, 103 | } 104 | }, 105 | { 106 | Namespace : "http://www.onvif.org/ver20/imaging/wsdl", 107 | XAddr : `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/imaging_service`, 108 | Version : { 109 | Major : 2, 110 | Minor : 5, 111 | } 112 | }, 113 | { 114 | Namespace : "http://www.onvif.org/ver10/media/wsdl", 115 | XAddr : `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/media_service`, 116 | Version : { 117 | Major : 2, 118 | Minor : 5, 119 | } 120 | }, 121 | { 122 | Namespace : "http://www.onvif.org/ver20/ptz/wsdl", 123 | XAddr : `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/ptz_service`, 124 | Version : { 125 | Major : 2, 126 | Minor : 5, 127 | }, 128 | }] 129 | }; 130 | 131 | return GetServicesResponse; 132 | }; 133 | 134 | 135 | port.GetCapabilities = (args /*, cb, headers*/) => { 136 | var category = args.Category; // Category is Optional and may be undefined 137 | //{ 'All', 'Analytics', 'Device', 'Events', 'Imaging', 'Media', 'PTZ' } 138 | var GetCapabilitiesResponse = { 139 | Capabilities: {} 140 | }; 141 | 142 | if (category === undefined || category == "All" || category == "Device") { 143 | GetCapabilitiesResponse.Capabilities["Device"] = { 144 | XAddr: `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/device_service`, 145 | Network: { 146 | IPFilter: false, 147 | ZeroConfiguration: false, 148 | IPVersion6: false, 149 | DynDNS: false, 150 | Extension: { 151 | Dot11Configuration: false, 152 | Extension: {} 153 | } 154 | }, 155 | System: { 156 | DiscoveryResolve: false, 157 | DiscoveryBye: false, 158 | RemoteDiscovery: false, 159 | SystemBackup: false, 160 | SystemLogging: false, 161 | FirmwareUpgrade: false, 162 | SupportedVersions: { 163 | Major: 2, 164 | Minor: 5 165 | }, 166 | Extension: { 167 | HttpFirmwareUpgrade: false, 168 | HttpSystemBackup: false, 169 | HttpSystemLogging: false, 170 | HttpSupportInformation: false, 171 | Extension: {} 172 | } 173 | }, 174 | IO: { 175 | InputConnectors: 0, 176 | RelayOutputs: 1, 177 | Extension: { 178 | Auxiliary: false, 179 | AuxiliaryCommands: "", 180 | Extension: {} 181 | } 182 | }, 183 | Security: { 184 | "TLS1.1": false, 185 | "TLS1.2": false, 186 | OnboardKeyGeneration: false, 187 | AccessPolicyConfig: false, 188 | "X.509Token": false, 189 | SAMLToken: false, 190 | KerberosToken: false, 191 | RELToken: false, 192 | Extension: { 193 | "TLS1.0": false, 194 | Extension: { 195 | Dot1X: false, 196 | RemoteUserHandling: false 197 | } 198 | } 199 | }, 200 | Extension: {} 201 | }; 202 | } 203 | if (category == undefined || category == "All" || category == "Events") { 204 | GetCapabilitiesResponse.Capabilities["Events"] = { 205 | XAddr: `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/events_service`, 206 | WSSubscriptionPolicySupport: false, 207 | WSPullPointSupport: false, 208 | WSPausableSubscriptionManagerInterfaceSupport: false 209 | } 210 | } 211 | if (category === undefined || category == "All" || category == "Imaging") { 212 | GetCapabilitiesResponse.Capabilities["Imaging"] = { 213 | XAddr: `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/imaging_service` 214 | } 215 | } 216 | if (category === undefined || category == "All" || category == "Media") { 217 | GetCapabilitiesResponse.Capabilities["Media"] = { 218 | XAddr: `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/media_service`, 219 | StreamingCapabilities: { 220 | RTPMulticast: this.config.MulticastEnabled, 221 | RTP_TCP: true, 222 | RTP_RTSP_TCP: true, 223 | Extension: {} 224 | }, 225 | Extension: { 226 | ProfileCapabilities: { 227 | MaximumNumberOfProfiles: 1 228 | } 229 | } 230 | } 231 | } 232 | if (category === undefined || category == "All" || category == "PTZ") { 233 | GetCapabilitiesResponse.Capabilities["PTZ"] = { 234 | XAddr: `http://${utils.getIpAddress() }:${this.config.ServicePort}/onvif/ptz_service` 235 | } 236 | } 237 | return GetCapabilitiesResponse; 238 | }; 239 | 240 | port.GetHostname = (args /*, cb, headers*/) => { 241 | var GetHostnameResponse = { 242 | HostnameInformation: { 243 | FromDHCP: false, 244 | Name: os.hostname(), 245 | Extension: {} 246 | } 247 | }; 248 | return GetHostnameResponse; 249 | }; 250 | 251 | port.SetHostname = (args /*, cb, headers*/) => { 252 | var SetHostnameResponse = {}; 253 | return SetHostnameResponse; 254 | }; 255 | 256 | port.SetHostnameFromDHCP = (args /*, cb, headers*/) => { 257 | var SetHostnameFromDHCPResponse = { 258 | RebootNeeded: false 259 | }; 260 | return SetHostnameFromDHCPResponse; 261 | }; 262 | 263 | port.GetDNS = (args /*, cb, headers*/) => { 264 | var GetDNSResponse = { 265 | DNSInformation : { 266 | FromDHCP : true, 267 | Extension : { } 268 | } 269 | 270 | }; 271 | return GetDNSResponse; 272 | }; 273 | 274 | port.GetScopes = (args) => { 275 | var GetScopesResponse = { Scopes: [] }; 276 | GetScopesResponse.Scopes.push({ 277 | ScopeDef: "Fixed", 278 | ScopeItem: "onvif://www.onvif.org/location/unknow" 279 | }); 280 | 281 | GetScopesResponse.Scopes.push({ 282 | ScopeDef: "Fixed", 283 | ScopeItem: ("onvif://www.onvif.org/hardware/" + this.config.DeviceInformation.Model) 284 | }); 285 | 286 | GetScopesResponse.Scopes.push({ 287 | ScopeDef: "Fixed", 288 | ScopeItem: ("onvif://www.onvif.org/name/" + this.config.DeviceInformation.Manufacturer) 289 | }); 290 | 291 | return GetScopesResponse; 292 | }; 293 | 294 | 295 | port.GetDiscoveryMode = (args /*, cb, headers*/) => { 296 | var GetDiscoveryModeResponse = { 297 | DiscoveryMode : true 298 | }; 299 | return GetDiscoveryModeResponse; 300 | }; 301 | 302 | port.GetServiceCapabilities = (args /*, cb, headers*/) => { 303 | var GetServiceCapabilitiesResponse = { 304 | Capabilities: { 305 | Network: { 306 | attributes: { 307 | IPFilter: false, 308 | ZeroConfiguration: false, 309 | IPVersion6: false, 310 | DynDNS: false, 311 | Dot11Configuration: false, 312 | Dot1XConfigurations: 0, 313 | HostnameFromDHCP: false, 314 | NTP: 0, 315 | DHCPv6: false 316 | } 317 | }, 318 | Security: { 319 | attributes: { 320 | "TLS1.0": false, 321 | "TLS1.1": false, 322 | "TLS1.2": false, 323 | OnboardKeyGeneration: false, 324 | AccessPolicyConfig: false, 325 | DefaultAccessPolicy: false, 326 | Dot1X: false, 327 | RemoteUserHandling: false, 328 | "X.509Token": false, 329 | SAMLToken: false, 330 | KerberosToken: false, 331 | UsernameToken: false, 332 | HttpDigest: false, 333 | RELToken: false, 334 | SupportedEAPMethods: 0, 335 | MaxUsers: 1, 336 | MaxUserNameLength: 10, 337 | MaxPasswordLength: 256 338 | } 339 | }, 340 | System: { 341 | attributes: { 342 | DiscoveryResolve: false, 343 | DiscoveryBye: false, 344 | RemoteDiscovery: false, 345 | SystemBackup: false, 346 | SystemLogging: false, 347 | FirmwareUpgrade: false, 348 | HttpFirmwareUpgrade: false, 349 | HttpSystemBackup: false, 350 | HttpSystemLogging: false, 351 | HttpSupportInformation: false, 352 | StorageConfiguration: false 353 | } 354 | }, 355 | //Misc : { 356 | // attributes : { 357 | // AuxiliaryCommands : {tt:StringAttrList} 358 | // } 359 | //} 360 | } 361 | }; 362 | return GetServiceCapabilitiesResponse; 363 | }; 364 | 365 | port.GetNTP = (args /*, cb, headers*/) => { 366 | var GetNTPResponse = { 367 | NTPInformation : { 368 | FromDHCP : false, 369 | //NTPFromDHCP : [{ 370 | // Type : { xs:string}, 371 | // IPv4Address : { xs:token}, 372 | // IPv6Address : { xs:token}, 373 | // DNSname : { xs:token}, 374 | // Extension : { } 375 | //}], 376 | NTPManual : [{ 377 | Type : "DNS", 378 | //IPv4Address : { xs:token}, 379 | //IPv6Address : { xs:token}, 380 | DNSname : "pool.ntp.org", 381 | Extension : { } 382 | }], 383 | Extension : { } 384 | } 385 | }; 386 | return GetNTPResponse; 387 | }; 388 | 389 | port.SetNTP = (args /*, cb, headers*/) => { 390 | var SetNTPResponse = {}; 391 | return SetNTPResponse; 392 | }; 393 | 394 | port.GetNetworkInterfaces = (args /*, cb, headers*/) => { 395 | var GetNetworkInterfacesResponse = { 396 | NetworkInterfaces: [] 397 | }; 398 | var nwifs = os.networkInterfaces(); 399 | for (var nwif in nwifs) { 400 | for (var addr in nwifs[nwif]) { 401 | if (nwifs[nwif][addr].family === 'IPv4' && nwif !== 'lo0' && nwif !== 'lo') { 402 | var mac = (nwifs[nwif][addr].mac).replace(/:/g,'-'); 403 | var ipv4_addr = nwifs[nwif][addr].address; 404 | var netmask = nwifs[nwif][addr].netmask; 405 | var prefix_len = ip.subnet(ipv4_addr,netmask).subnetMaskLength; 406 | GetNetworkInterfacesResponse.NetworkInterfaces.push({ 407 | attributes: { 408 | token: nwif 409 | }, 410 | Enabled: true, 411 | Info: { 412 | Name: nwif, 413 | HwAddress: mac, 414 | MTU: 1500 415 | }, 416 | IPv4: { 417 | Enabled: true, 418 | Config: { 419 | Manual: { 420 | Address: ipv4_addr, 421 | PrefixLength: prefix_len 422 | }, 423 | DHCP: false 424 | } 425 | } 426 | }); 427 | } 428 | } 429 | } 430 | return GetNetworkInterfacesResponse; 431 | }; 432 | 433 | port.GetNetworkProtocols = (args /*, cb, headers*/) => { 434 | var GetNetworkProtocolsResponse = { 435 | NetworkProtocols: [{ 436 | Name: "RTSP", 437 | Enabled: true, 438 | Port: this.config.RTSPPort 439 | }] 440 | }; 441 | return GetNetworkProtocolsResponse; 442 | }; 443 | 444 | port.GetNetworkDefaultGateway = (args /*, cb, headers*/) => { 445 | let GetNetworkDefaultGatewayResponse = {} 446 | if (utils.isLinux) { 447 | // Linux method for now. Need to include Windows and Mac 448 | const spawn = require('child_process').spawnSync; 449 | 450 | const child = spawn('bash', ['-c', 'ip route']).stdout.toString(); 451 | const gateway = child.match(/default via (.*?)\s/)[1]; // Look for text "default via " and then get everything up to the next Space or Tab 452 | GetNetworkDefaultGatewayResponse = { 453 | NetworkGateway : { 454 | IPv4Address : [gateway], // FIXME. Need to ask the OS for this information 455 | //IPv6Address : [{ xs:token}] 456 | } 457 | }; 458 | } else { 459 | // TODO 460 | // return empty result 461 | } 462 | return GetNetworkDefaultGatewayResponse; 463 | }; 464 | 465 | port.GetRelayOutputs = (args /*, cb, headers*/) => { 466 | var GetRelayOutputsResponse = { 467 | RelayOutputs: [{ 468 | attributes: { 469 | token: "relay1" 470 | }, 471 | Properties : { 472 | Mode: "Bistable", 473 | // DelayTime: "", 474 | IdleState: "open" 475 | } 476 | }] 477 | }; 478 | return GetRelayOutputsResponse; 479 | }; 480 | 481 | port.SetRelayOutputState = (args /*, cb, headers*/) => { 482 | var SetRelayOutputStateResponse = {}; 483 | if (this.callback) { 484 | if (args.LogicalState === 'active') this.callback('relayactive', { name: args.RelayOutputToken }); 485 | if (args.LogicalState === 'inactive') this.callback('relayinactive', { name: args.RelayOutputToken }); 486 | } 487 | return SetRelayOutputStateResponse; 488 | }; 489 | 490 | port.GetUsers = (args /*, cb, headers*/) => { 491 | var GetUsersResponse = { 492 | // User : [{ 493 | // Username : '', 494 | // Password : '', 495 | // UserLevel : 'Administrator', 496 | // }] 497 | }; 498 | return GetUsersResponse; 499 | } 500 | 501 | 502 | } 503 | } 504 | export = DeviceService; 505 | -------------------------------------------------------------------------------- /services/discovery_service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /* 4 | The MIT License(MIT) 5 | 6 | Copyright(c) 2016 Roger Hardiman 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files(the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and / or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject tothe following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | */ 26 | 27 | /* 28 | * WS-Discovery 29 | * Listens on Port 3702 on 239.255.255.250 for UDP WS-Discovery Messages 30 | * and sends back a reply containing the ONVIF Xaddr 31 | * 32 | * Raspberry Pi: Works fine. 33 | 34 | * Windows: Works, after I was able to make the UDP port non exclusive with reuseAddr 35 | * as Winodws also listens on that port. 36 | * 37 | * Mac: OS 10.10 worked but only if you ran it as root. Seems that a process 38 | * called SpotlightNetHelper takes the address. 39 | * Mac: OS 10.11 did not work (even as root) 40 | * 41 | */ 42 | 43 | import dgram = require('dgram'); 44 | import uuid = require('node-uuid'); 45 | import xml2js = require('xml2js'); 46 | import { Utils } from '../lib/utils'; 47 | var utils = Utils.utils; 48 | 49 | class DiscoveryService { 50 | 51 | config: rposConfig; 52 | 53 | constructor(config: rposConfig) { 54 | this.config = config; 55 | } 56 | 57 | 58 | start() { 59 | 60 | // if (process.platform != 'linux') { 61 | // utils.log.info("discovery_service not started (requires linux)"); 62 | // return; 63 | // } 64 | var opts: dgram.SocketOptions = { 65 | type: 'udp4', 66 | reuseAddr: true 67 | }; 68 | var discover_socket = dgram.createSocket(opts); 69 | var reply_socket = dgram.createSocket('udp4'); 70 | 71 | discover_socket.on('error', (err) => { 72 | throw err; 73 | }); 74 | 75 | discover_socket.on('message', (received_msg, rinfo) => { 76 | 77 | utils.log.debug("Discovery received from " + rinfo.address); 78 | 79 | // Filter xmlns namespaces from XML before calling XML2JS 80 | let filtered_msg = received_msg.toString().replace(/xmlns(.*?)=(".*?")/g, ''); 81 | 82 | var parseString = xml2js.parseString; 83 | var strip = xml2js['processors'].stripPrefix; 84 | parseString(filtered_msg, { tagNameProcessors: [strip] }, (err, result) => { 85 | let probe_uuid = result['Envelope']['Header'][0]['MessageID'][0]; 86 | let probe_type = ""; 87 | try { 88 | probe_type = result['Envelope']['Body'][0]['Probe'][0]['Types'][0]; 89 | } catch (err) { 90 | probe_type = ""; // For a VMS that does not send Types 91 | } 92 | 93 | if (probe_type === "" || probe_type.indexOf("NetworkVideoTransmitter") > -1) { 94 | 95 | let reply = ` 96 | 97 | 98 | uuid:${uuid.v1()} 99 | ${probe_uuid} 100 | http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous 101 | http://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches 102 | 103 | 104 | 105 | 106 | 107 | 108 | urn:uuid:${utils.uuid5(utils.getIpAddress() + this.config.ServicePort + this.config.RTSPPort)} 109 | 110 | dn:NetworkVideoTransmitter 111 | 112 | onvif://www.onvif.org/type/video_encoder 113 | onvif://www.onvif.org/type/ptz 114 | onvif://www.onvif.org/hardware/${encodeURIComponent(this.config.DeviceInformation.Model)} 115 | onvif://www.onvif.org/name/${encodeURIComponent(this.config.DeviceInformation.Manufacturer + ' ' + this.config.DeviceInformation.Model)} 116 | onvif://www.onvif.org/location/ 117 | 118 | http://${utils.getIpAddress()}:${this.config.ServicePort}/onvif/device_service 119 | 1 120 | 121 | 122 | 123 | `; 124 | 125 | let reply_bytes = new Buffer(reply); 126 | 127 | // Mac needed replies from a different UDP socket (ie not the bounded socket) 128 | return reply_socket.send(reply_bytes, 0, reply_bytes.length, rinfo.port, rinfo.address); 129 | } 130 | }); 131 | }); 132 | 133 | discover_socket.bind(3702, () => { 134 | return discover_socket.addMembership('239.255.255.250', utils.getIpAddress()); 135 | }); 136 | 137 | utils.log.info("discovery_service started"); 138 | 139 | }; 140 | 141 | } // end class Discovery 142 | 143 | export = DiscoveryService; 144 | -------------------------------------------------------------------------------- /services/imaging_service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import fs = require("fs"); 4 | import util = require("util"); 5 | import os = require('os'); 6 | import SoapService = require('../lib/SoapService'); 7 | import { Utils } from '../lib/utils'; 8 | import { Server } from 'http'; 9 | 10 | var utils = Utils.utils; 11 | 12 | class ImagingService extends SoapService { 13 | imaging_service: any; 14 | callback: any; 15 | 16 | brightness = 0; 17 | autoFocusMode = ''; 18 | focusNearLimit = 0; 19 | focusFarLimit = 0; 20 | focusDefaultSpeed = 0; 21 | 22 | constructor(config: rposConfig, server: Server, callback) { 23 | super(config, server); 24 | 25 | this.imaging_service = require('./stubs/imaging_service.js').ImagingService; 26 | this.callback = callback; 27 | 28 | this.serviceOptions = { 29 | path: '/onvif/imaging_service', 30 | services: this.imaging_service, 31 | xml: fs.readFileSync('./wsdl/imaging_service.wsdl', 'utf8'), 32 | wsdlPath: 'wsdl/imaging_service.wsdl', 33 | onReady: () => console.log('imaging_service started') 34 | }; 35 | 36 | this.brightness = 50; // range is 0..100 37 | this.autoFocusMode = "MANUAL"; // MANUAL or AUTO 38 | this.focusDefaultSpeed = 0.5; // range 0.1 to 1.0. See GetMoveOptions valid range 39 | this.focusNearLimit = 1.0; // range 0.1 to 3.0 in Metres 40 | this.focusFarLimit = 0.0; // range 0.0 to 0.0. 0=Infinity 41 | 42 | this.extendService(); 43 | } 44 | 45 | extendService() { 46 | var port = this.imaging_service.ImagingService.Imaging; 47 | 48 | 49 | 50 | //var GetServiceCapabilitiesResponse = { 51 | //Capabilities : { 52 | //attributes : { 53 | //ImageStabilization : {xs:boolean}, 54 | //Presets : {xs:boolean} 55 | //} 56 | //} 57 | // 58 | //}; 59 | 60 | port.GetServiceCapabilities = (args) => { 61 | var GetServiceCapabilitiesResponse = { 62 | Capabilities : { 63 | attributes : { 64 | ImageStabilization : false, 65 | Presets : false 66 | } 67 | } 68 | }; 69 | return GetServiceCapabilitiesResponse; 70 | }; 71 | 72 | //var GetOptions = { 73 | //VideoSourceToken : { xs:string} 74 | // 75 | //}; 76 | port.GetOptions = (args /*, cb, headers*/) => { 77 | var GetOptionsResponse = { 78 | ImagingOptions : { 79 | //BacklightCompensation : { 80 | //Mode : { xs:string}, 81 | //Level : { 82 | //Min : { xs:float}, 83 | //Max : { xs:float} 84 | //} 85 | //}, 86 | Brightness : { 87 | Min : 0, 88 | Max : 100 89 | }, 90 | //ColorSaturation : { 91 | //Min : { xs:float}, 92 | //Max : { xs:float} 93 | //}, 94 | //Contrast : { 95 | //Min : { xs:float}, 96 | //Max : { xs:float} 97 | //}, 98 | //Exposure : { 99 | //Mode : { xs:string}, 100 | //Priority : [{ xs:string}], 101 | //MinExposureTime : { 102 | //Min : { xs:float}, 103 | //Max : { xs:float} 104 | //}, 105 | //MaxExposureTime : { 106 | //Min : { xs:float}, 107 | //Max : { xs:float} 108 | //}, 109 | //MinGain : { 110 | //Min : { xs:float}, 111 | //Max : { xs:float} 112 | //}, 113 | //MaxGain : { 114 | //Min : { xs:float}, 115 | //Max : { xs:float} 116 | //}, 117 | //MinIris : { 118 | //Min : { xs:float}, 119 | //Max : { xs:float} 120 | //}, 121 | //MaxIris : { 122 | //Min : { xs:float}, 123 | //Max : { xs:float} 124 | //}, 125 | //ExposureTime : { 126 | //Min : { xs:float}, 127 | //Max : { xs:float} 128 | //}, 129 | //Gain : { 130 | //Min : { xs:float}, 131 | //Max : { xs:float} 132 | //}, 133 | //Iris : { 134 | //Min : { xs:float}, 135 | //Max : { xs:float} 136 | //} 137 | //}, 138 | Focus : { 139 | AutoFocusModes : ['AUTO','MANUAL'], 140 | DefaultSpeed : { 141 | Min : 0.1, 142 | Max : 1.0 143 | }, 144 | NearLimit : { 145 | Min : 0.1, 146 | Max : 3.0 147 | }, 148 | FarLimit : { 149 | Min : 0.0, 150 | Max : 0.0 151 | }, 152 | //Extension : { } 153 | //}, 154 | //IrCutFilterModes : [{ xs:string}], 155 | //Sharpness : { 156 | //Min : { xs:float}, 157 | //Max : { xs:float} 158 | //}, 159 | //WideDynamicRange : { 160 | //Mode : { xs:string}, 161 | //Level : { 162 | //Min : { xs:float}, 163 | //Max : { xs:float} 164 | //} 165 | //}, 166 | //WhiteBalance : { 167 | //Mode : { xs:string}, 168 | //YrGain : { 169 | //Min : { xs:float}, 170 | //Max : { xs:float} 171 | //}, 172 | //YbGain : { 173 | //Min : { xs:float}, 174 | //Max : { xs:float} 175 | //}, 176 | //Extension : { } 177 | //}, 178 | //Extension : { 179 | //ImageStabilization : { 180 | //Mode : { xs:string}, 181 | //Level : { 182 | //Min : { xs:float}, 183 | //Max : { xs:float} 184 | //}, 185 | //Extension : { } 186 | //}, 187 | //Extension : { 188 | //IrCutFilterAutoAdjustment : { 189 | //BoundaryType : { xs:string}, 190 | //BoundaryOffset : { xs:boolean}, 191 | //ResponseTimeRange : { 192 | //Min : { xs:duration}, 193 | //Max : { xs:duration} 194 | //}, 195 | //Extension : { } 196 | //}, 197 | //Extension : { 198 | //ToneCompensationOptions : { 199 | //Mode : { xs:string}, 200 | //Level : { xs:boolean} 201 | //}, 202 | //DefoggingOptions : { 203 | //Mode : { xs:string}, 204 | //Level : { xs:boolean} 205 | //}, 206 | //NoiseReductionOptions : { 207 | //Level : { xs:boolean} 208 | //}, 209 | //Extension : { } 210 | //} 211 | //} 212 | } 213 | } 214 | } 215 | return GetOptionsResponse; 216 | }, 217 | 218 | 219 | 220 | port.GetImagingSettings = (args /*, cb, headers*/) => { 221 | var GetImagingSettingsResponse = { 222 | ImagingSettings : { 223 | Brightness : this.brightness, 224 | Focus : { 225 | AutoFocusMode : this.autoFocusMode, 226 | DefaultSpeed : this.focusDefaultSpeed, 227 | NearLimit : this.focusNearLimit, 228 | FarLimit : this.focusFarLimit, // Infinity 229 | //Extension : { } 230 | }, 231 | } 232 | }; 233 | return GetImagingSettingsResponse; 234 | }; 235 | 236 | //var SetImagingSettings = { 237 | //VideoSourceToken : { xs:string}, 238 | //ImagingSettings : { 239 | //BacklightCompensation : { 240 | //Mode : { xs:string}, 241 | //Level : { xs:float} 242 | //}, 243 | //Brightness : { xs:float}, 244 | //ColorSaturation : { xs:float}, 245 | //Contrast : { xs:float}, 246 | //Exposure : { 247 | //Mode : { xs:string}, 248 | //Priority : { xs:string}, 249 | //Window : { 250 | //attributes : { 251 | //bottom : {xs:float}, 252 | //top : {xs:float}, 253 | //right : {xs:float}, 254 | //left : {xs:float} 255 | //} 256 | //}, 257 | //MinExposureTime : { xs:float}, 258 | //MaxExposureTime : { xs:float}, 259 | //MinGain : { xs:float}, 260 | //MaxGain : { xs:float}, 261 | //MinIris : { xs:float}, 262 | //MaxIris : { xs:float}, 263 | //ExposureTime : { xs:float}, 264 | //Gain : { xs:float}, 265 | //Iris : { xs:float} 266 | //}, 267 | //Focus : { 268 | //AutoFocusMode : { xs:string}, 269 | //DefaultSpeed : { xs:float}, 270 | //NearLimit : { xs:float}, 271 | //FarLimit : { xs:float}, 272 | //Extension : { } 273 | //}, 274 | //IrCutFilter : { xs:string}, 275 | //Sharpness : { xs:float}, 276 | //WideDynamicRange : { 277 | //Mode : { xs:string}, 278 | //Level : { xs:float} 279 | //}, 280 | //WhiteBalance : { 281 | //Mode : { xs:string}, 282 | //CrGain : { xs:float}, 283 | //CbGain : { xs:float}, 284 | //Extension : { } 285 | //}, 286 | //Extension : { 287 | //ImageStabilization : { 288 | //Mode : { xs:string}, 289 | //Level : { xs:float}, 290 | //Extension : { } 291 | //}, 292 | //Extension : { 293 | //IrCutFilterAutoAdjustment : [{ 294 | //BoundaryType : { xs:string}, 295 | //BoundaryOffset : { xs:float}, 296 | //ResponseTime : { xs:duration}, 297 | //Extension : { } 298 | //}], 299 | //Extension : { 300 | //ToneCompensation : { 301 | //Mode : { xs:string}, 302 | //Level : { xs:float}, 303 | //Extension : { } 304 | //}, 305 | //Defogging : { 306 | //Mode : { xs:string}, 307 | //Level : { xs:float}, 308 | //Extension : { } 309 | //}, 310 | //NoiseReduction : { 311 | //Level : { xs:float} 312 | //}, 313 | //Extension : { } 314 | //} 315 | //} 316 | //} 317 | //}, 318 | //ForcePersistence : [{ xs:boolean}] 319 | // 320 | //}; 321 | 322 | port.SetImagingSettings = (args) => { 323 | var SetImagingSettingsResponse = { }; 324 | 325 | // Check for Brightness value 326 | if (args.ImagingSettings) { 327 | if (args.ImagingSettings.Brightness) { 328 | this.brightness = args.ImagingSettings.Brightness; 329 | // emit the 'brightness' message to the parent 330 | if (this.callback) this.callback('brightness', {value: this.brightness}); 331 | } 332 | if (args.ImagingSettings.Focus) { 333 | if (args.ImagingSettings.Focus.AutoFocusMode) { 334 | this.autoFocusMode = args.ImagingSettings.Focus.AutoFocusMode; 335 | if (this.callback) this.callback('focusmode', {value: this.autoFocusMode}); 336 | } 337 | if (args.ImagingSettings.Focus.DefaultSpeed) { 338 | this.focusDefaultSpeed = args.ImagingSettings.Focus.DefaultSpeed; 339 | if (this.callback) this.callback('focusdefaultspeed', {value: this.focusDefaultSpeed}); 340 | } 341 | if (args.ImagingSettings.Focus.NearLimit) { 342 | this.focusNearLimit = args.ImagingSettings.Focus.NearLimit; 343 | if (this.callback) this.callback('focusnearlimit', {value: this.focusNearLimit}); 344 | } 345 | if (args.ImagingSettings.Focus.FarLimit) { 346 | this.focusFarLimit = args.ImagingSettings.Focus.FarLimit; 347 | if (this.callback) this.callback('focusfarlimit', {value: this.focusFarLimit}); 348 | } 349 | } 350 | } 351 | 352 | return SetImagingSettingsResponse; 353 | }; 354 | 355 | //var Move = { 356 | //VideoSourceToken : { xs:string}, 357 | //Focus : { 358 | //Absolute : { 359 | //Position : { xs:float}, 360 | //Speed : { xs:float} 361 | //}, 362 | //Relative : { 363 | //Distance : { xs:float}, 364 | //Speed : { xs:float} 365 | //}, 366 | //Continuous : { 367 | //Speed : { xs:float} 368 | //} 369 | //} 370 | // 371 | //}; 372 | port.Move = (args) => { 373 | var MoveResponse = { }; 374 | 375 | if (args.Focus) { 376 | if (args.Focus.Continuous) { 377 | if (this.callback) this.callback('focus', {value: args.Focus.Continuous.Speed}); 378 | } 379 | } 380 | 381 | return MoveResponse; 382 | }; 383 | 384 | //var GetMoveOptions = { 385 | //VideoSourceToken : { xs:string} 386 | // 387 | //}; 388 | port.GetMoveOptions = (args) => { 389 | var GetMoveOptionsResponse = { 390 | MoveOptions : { 391 | //Absolute : { 392 | //Position : { 393 | //Min : { xs:float}, 394 | //Max : { xs:float} 395 | //}, 396 | //Speed : { 397 | //Min : { xs:float}, 398 | //Max : { xs:float} 399 | //} 400 | //}, 401 | //Relative : { 402 | //Distance : { 403 | //Min : { xs:float}, 404 | //Max : { xs:float} 405 | //}, 406 | //Speed : { 407 | //Min : { xs:float}, 408 | //Max : { xs:float} 409 | //} 410 | //}, 411 | Continuous : { 412 | Speed : { 413 | Min : -1.0, 414 | Max : 1.0 415 | } 416 | } 417 | } 418 | }; 419 | return GetMoveOptionsResponse; 420 | }; 421 | 422 | //var Stop = { 423 | //VideoSourceToken : { xs:string} 424 | // 425 | //}; 426 | port.Stop = (args) => { 427 | var StopResponse = { }; 428 | 429 | if (this.callback) this.callback('focusstop', {}); 430 | 431 | return StopResponse; 432 | }; 433 | 434 | //var GetStatus = { 435 | //VideoSourceToken : { xs:string} 436 | // 437 | //}; 438 | port.GetStatus = (args) => { 439 | var GetStatusResponse = { 440 | Status : { 441 | FocusStatus20 : { 442 | Position : 5.0, // Need to read current focus position 443 | MoveStatus : 'UNKNOWN', // MOVING IDLE or UNKNOWN 444 | //Error : '', 445 | //Extension : { } 446 | }, 447 | //Extension : { } 448 | } 449 | }; 450 | return GetStatusResponse; 451 | }; 452 | 453 | 454 | 455 | 456 | } 457 | } 458 | export = ImagingService; 459 | -------------------------------------------------------------------------------- /services/media_service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import fs = require("fs"); 4 | import util = require("util"); 5 | import SoapService = require('../lib/SoapService'); 6 | import { Utils } from '../lib/utils'; 7 | import url = require('url'); 8 | import { Server } from 'http'; 9 | import Camera = require('../lib/camera'); 10 | import { v4l2ctl } from '../lib/v4l2ctl'; 11 | import { exec } from 'child_process'; 12 | import PTZService = require('./ptz_service'); 13 | var utils = Utils.utils; 14 | 15 | class MediaService extends SoapService { 16 | media_service: any; 17 | camera: Camera; 18 | ptz_service: PTZService; 19 | ffmpeg_process: any = null; 20 | ffmpeg_responses: any[] = []; 21 | 22 | constructor(config: rposConfig, server: Server, camera: Camera, ptz_service: PTZService) { 23 | super(config, server); 24 | this.media_service = require('./stubs/media_service.js').MediaService; 25 | 26 | this.camera = camera; 27 | this.ptz_service = ptz_service; 28 | this.serviceOptions = { 29 | path: '/onvif/media_service', 30 | services: this.media_service, 31 | xml: fs.readFileSync('./wsdl/media_service.wsdl', 'utf8'), 32 | wsdlPath: 'wsdl/media_service.wsdl', 33 | onReady: function() { 34 | utils.log.info('media_service started'); 35 | } 36 | }; 37 | 38 | this.extendService(); 39 | } 40 | 41 | starting() { 42 | var listeners = this.webserver.listeners('request').slice(); 43 | this.webserver.removeAllListeners('request'); 44 | this.webserver.addListener('request', (request, response, next) => { 45 | utils.log.debug('web request received : %s', request.url); 46 | 47 | var uri = url.parse(request.url, true); 48 | var action = uri.pathname; 49 | if (action == '/web/snapshot.jpg') { 50 | try { 51 | if (this.ffmpeg_process != null) { 52 | utils.log.info("ffmpeg - already running"); 53 | this.ffmpeg_responses.push(response); 54 | } else { 55 | var cmd = `ffmpeg -fflags nobuffer -probesize 256 -rtsp_transport tcp -i rtsp://127.0.0.1:${this.config.RTSPPort}/${this.config.RTSPName} -vframes 1 -r 1 -s 640x360 -y /dev/shm/snapshot.jpg`; 56 | var options = { timeout: 15000 }; 57 | utils.log.info("ffmpeg - starting"); 58 | this.ffmpeg_responses.push(response); 59 | this.ffmpeg_process = exec(cmd, options, (error, stdout, stderr) => { 60 | // callback 61 | utils.log.info("ffmpeg - finished"); 62 | if (error) { 63 | utils.log.warn('ffmpeg exec error: %s', error); 64 | } 65 | // deliver the JPEG (or the logo jpeg file) 66 | for (let responseItem of this.ffmpeg_responses) { 67 | this.deliver_jpg(responseItem); // response.Write() and response.End() 68 | } 69 | // empty the list of responses 70 | this.ffmpeg_responses = []; 71 | this.ffmpeg_process = null; 72 | }); 73 | } 74 | } catch (err) { 75 | utils.log.warn('Error ' + err); 76 | } 77 | } else { 78 | for (var i = 0, len = listeners.length; i < len; i++) { 79 | listeners[i].call(this, request, response, next); 80 | } 81 | } 82 | }); 83 | } 84 | 85 | deliver_jpg(response: any){ 86 | try { 87 | var img = fs.readFileSync('/dev/shm/snapshot.jpg'); 88 | response.writeHead(200, { 'Content-Type': 'image/jpg' }); 89 | response.end(img, 'binary'); 90 | return; 91 | } catch (err) { 92 | utils.log.debug("Error opening snapshot : %s", err); 93 | } 94 | try { 95 | var img = fs.readFileSync('./web/snapshot.jpg'); 96 | response.writeHead(200, { 'Content-Type': 'image/jpg' }); 97 | response.end(img, 'binary'); 98 | return; 99 | } catch (err) { 100 | utils.log.debug("Error opening snapshot : %s", err); 101 | } 102 | 103 | // Return 400 error 104 | response.writeHead(400, { 'Content-Type': 'text/plain' }); 105 | response.end('JPEG unavailable'); 106 | } 107 | 108 | started() { 109 | this.camera.startRtsp(); 110 | } 111 | 112 | extendService() { 113 | var port = this.media_service.MediaService.Media; 114 | 115 | var cameraOptions = this.camera.options; 116 | var cameraSettings = this.camera.settings; 117 | var camera = this.camera; 118 | 119 | var h264Profiles = v4l2ctl.Controls.CodecControls.h264_profile.getLookupSet().map(ls=>ls.desc); 120 | h264Profiles.splice(1, 1); 121 | 122 | var videoConfigurationOptions = { 123 | QualityRange: { 124 | Min: 1, 125 | Max: 1 126 | }, 127 | H264: { 128 | ResolutionsAvailable: cameraOptions.resolutions, 129 | GovLengthRange: { 130 | Min: v4l2ctl.Controls.CodecControls.h264_i_frame_period.getRange().min, 131 | Max: v4l2ctl.Controls.CodecControls.h264_i_frame_period.getRange().max 132 | }, 133 | FrameRateRange: { 134 | Min: cameraOptions.framerates[0], 135 | Max: cameraOptions.framerates[cameraOptions.framerates.length - 1] 136 | }, 137 | EncodingIntervalRange: { Min: 1, Max: 1 }, 138 | H264ProfilesSupported: h264Profiles 139 | }, 140 | Extension: { 141 | H264: { 142 | ResolutionsAvailable: cameraOptions.resolutions, 143 | GovLengthRange: { 144 | Min: v4l2ctl.Controls.CodecControls.h264_i_frame_period.getRange().min, 145 | Max: v4l2ctl.Controls.CodecControls.h264_i_frame_period.getRange().max 146 | }, 147 | FrameRateRange: { 148 | Min: cameraOptions.framerates[0], 149 | Max: cameraOptions.framerates[cameraOptions.framerates.length - 1] 150 | }, 151 | EncodingIntervalRange: { Min: 1, Max: 1 }, 152 | H264ProfilesSupported: h264Profiles, 153 | BitrateRange: { 154 | Min: cameraOptions.bitrates[0], 155 | Max: cameraOptions.bitrates[cameraOptions.bitrates.length - 1] 156 | } 157 | } 158 | } 159 | }; 160 | 161 | var videoEncoderConfiguration = { 162 | attributes: { 163 | token: "encoder_config_token" 164 | }, 165 | Name: "PiCameraConfiguration", 166 | UseCount: 0, 167 | Encoding: "H264", 168 | Resolution: { 169 | Width: cameraSettings.resolution.Width, 170 | Height: cameraSettings.resolution.Height 171 | }, 172 | Quality: v4l2ctl.Controls.CodecControls.video_bitrate.value ? 1 : 1, 173 | RateControl: { 174 | FrameRateLimit: cameraSettings.framerate, 175 | EncodingInterval: 1, 176 | BitrateLimit: v4l2ctl.Controls.CodecControls.video_bitrate.value / 1000 177 | }, 178 | H264: { 179 | GovLength: v4l2ctl.Controls.CodecControls.h264_i_frame_period.value, 180 | H264Profile: v4l2ctl.Controls.CodecControls.h264_profile.desc 181 | }, 182 | Multicast: { 183 | Address: { 184 | Type: "IPv4", 185 | IPv4Address: "0.0.0.0" 186 | }, 187 | Port: 0, 188 | TTL: 1, 189 | AutoStart: false 190 | }, 191 | SessionTimeout: "PT1000S" 192 | }; 193 | 194 | var videoSource = { 195 | attributes: { 196 | token: "video_src_token" 197 | }, 198 | Framerate: 25, 199 | Resolution: { Width: 1920, Height: 1280 } 200 | }; 201 | 202 | var videoSourceConfiguration = { 203 | Name: "Primary Source", 204 | UseCount: 0, 205 | attributes: { 206 | token: "video_src_config_token" 207 | }, 208 | SourceToken: "video_src_token", 209 | Bounds: { attributes: { x: 0, y: 0, width: 1920, height: 1080 } } 210 | }; 211 | 212 | var audioEncoderConfigurationOptions = { 213 | Options: [] 214 | }; 215 | 216 | var profile = { 217 | Name: "CurrentProfile", 218 | attributes: { 219 | token: "profile_token" 220 | }, 221 | VideoSourceConfiguration: videoSourceConfiguration, 222 | VideoEncoderConfiguration: videoEncoderConfiguration, 223 | PTZConfiguration: this.ptz_service.ptzConfiguration 224 | }; 225 | 226 | port.GetServiceCapabilities = (args /*, cb, headers*/) => { 227 | var GetServiceCapabilitiesResponse = { 228 | Capabilities: { 229 | attributes: { 230 | SnapshotUri: true, 231 | Rotation: false, 232 | VideoSourceMode: true, 233 | OSD: false 234 | }, 235 | ProfileCapabilities: { 236 | attributes: { 237 | MaximumNumberOfProfiles: 1 238 | } 239 | }, 240 | StreamingCapabilities: { 241 | attributes: { 242 | RTPMulticast: this.config.MulticastEnabled, 243 | RTP_TCP: true, 244 | RTP_RTSP_TCP: true, 245 | NonAggregateControl: false, 246 | NoRTSPStreaming: false 247 | } 248 | } 249 | } 250 | }; 251 | return GetServiceCapabilitiesResponse; 252 | }; 253 | 254 | //var GetStreamUri = { 255 | //StreamSetup : { 256 | //Stream : { xs:string} 257 | //}, 258 | //ProfileToken : { xs:string} 259 | // 260 | //}; 261 | port.GetStreamUri = (args /*, cb, headers*/) => { 262 | 263 | // Usually RTSP server is on same IP Address as the ONVIF Service 264 | // Setting RTSPAddress in the config file lets you to use another IP Address 265 | let rtspAddress = utils.getIpAddress(); 266 | if (this.config.RTSPAddress.length > 0) rtspAddress = this.config.RTSPAddress; 267 | 268 | var GetStreamUriResponse = { 269 | MediaUri: { 270 | Uri: (args.StreamSetup.Stream == "RTP-Multicast" && this.config.MulticastEnabled ? 271 | `rtsp://${rtspAddress}:${this.config.RTSPPort}/${this.config.RTSPMulticastName}` : 272 | `rtsp://${rtspAddress}:${this.config.RTSPPort}/${this.config.RTSPName}`), 273 | InvalidAfterConnect: false, 274 | InvalidAfterReboot: false, 275 | Timeout: "PT30S" 276 | } 277 | }; 278 | return GetStreamUriResponse; 279 | }; 280 | 281 | port.GetProfile = (args) => { 282 | var GetProfileResponse = { Profile: profile }; 283 | return GetProfileResponse; 284 | }; 285 | 286 | port.GetProfiles = (args) => { 287 | var GetProfilesResponse = { Profiles: [profile] }; 288 | return GetProfilesResponse; 289 | }; 290 | 291 | port.CreateProfile = (args) => { 292 | var CreateProfileResponse = { Profile: profile }; 293 | return CreateProfileResponse; 294 | }; 295 | 296 | port.DeleteProfile = (args) => { 297 | var DeleteProfileResponse = {}; 298 | return DeleteProfileResponse; 299 | }; 300 | 301 | port.GetVideoSources = (args) => { 302 | var GetVideoSourcesResponse = { VideoSources: [videoSource] }; 303 | return GetVideoSourcesResponse; 304 | } 305 | 306 | port.GetVideoSourceConfigurations = (args) => { 307 | var GetVideoSourceConfigurationsResponse = { Configurations: [videoSourceConfiguration] }; 308 | return GetVideoSourceConfigurationsResponse; 309 | }; 310 | 311 | port.GetVideoSourceConfiguration = (args) => { 312 | var GetVideoSourceConfigurationResponse = { Configurations: videoSourceConfiguration }; 313 | return GetVideoSourceConfigurationResponse; 314 | }; 315 | 316 | port.GetVideoEncoderConfigurations = (args) => { 317 | var GetVideoEncoderConfigurationsResponse = { Configurations: [videoEncoderConfiguration] }; 318 | return GetVideoEncoderConfigurationsResponse; 319 | }; 320 | 321 | port.GetVideoEncoderConfiguration = (args) => { 322 | var GetVideoEncoderConfigurationResponse = { Configuration: videoEncoderConfiguration }; 323 | return GetVideoEncoderConfigurationResponse; 324 | }; 325 | 326 | port.SetVideoEncoderConfiguration = (args) => { 327 | var settings = { 328 | bitrate: args.Configuration.RateControl.BitrateLimit, 329 | framerate: args.Configuration.RateControl.FrameRateLimit, 330 | gop: args.Configuration.H264.GovLength, 331 | profile: args.Configuration.H264.H264Profile, 332 | quality: args.Configuration.Quality instanceof Object ? 1 : args.Configuration.Quality, 333 | resolution: args.Configuration.Resolution 334 | }; 335 | camera.setSettings(settings); 336 | 337 | var SetVideoEncoderConfigurationResponse = {}; 338 | return SetVideoEncoderConfigurationResponse; 339 | }; 340 | 341 | port.GetVideoEncoderConfigurationOptions = (args) => { 342 | var GetVideoEncoderConfigurationOptionsResponse = { Options: videoConfigurationOptions }; 343 | return GetVideoEncoderConfigurationOptionsResponse; 344 | }; 345 | 346 | port.GetGuaranteedNumberOfVideoEncoderInstances = (args) => { 347 | var GetGuaranteedNumberOfVideoEncoderInstancesResponse = { 348 | TotalNumber: 1, 349 | H264: 1 350 | } 351 | return GetGuaranteedNumberOfVideoEncoderInstancesResponse; 352 | }; 353 | 354 | port.GetSnapshotUri = (args) => { 355 | var GetSnapshotUriResponse = { 356 | MediaUri : { 357 | Uri : "http://" + utils.getIpAddress() + ":" + this.config.ServicePort + "/web/snapshot.jpg", 358 | InvalidAfterConnect : false, 359 | InvalidAfterReboot : false, 360 | Timeout : "PT30S" 361 | } 362 | }; 363 | return GetSnapshotUriResponse; 364 | }; 365 | 366 | port.GetAudioEncoderConfigurationOptions = (args) => { 367 | var GetAudioEncoderConfigurationOptionsResponse = { Options: [{}] }; 368 | return GetAudioEncoderConfigurationOptionsResponse; 369 | }; 370 | 371 | port.GetCompatibleVideoSourceConfigurations = (args) => { 372 | // Args contains a ProfileToken 373 | // We will return all Video Sources as being compatible 374 | 375 | let GetCompatibleVideoSourceConfigurationsResponse = { Configurations: [videoSourceConfiguration] }; 376 | return GetCompatibleVideoSourceConfigurationsResponse; 377 | } 378 | 379 | port.GetVideoSourceConfigurationOptions = (Args) => { 380 | // Args will contain a ConfigurationToken or ProfileToken 381 | var GetVideoSourceConfigurationOptionsResponse = { 382 | Options : { 383 | BoundsRange : { 384 | XRange : { 385 | Min : 0, 386 | Max : 0 387 | }, 388 | YRange : { 389 | Min : 0, 390 | Max : 0 391 | }, 392 | WidthRange : { 393 | Min : 1920, 394 | Max : 1920 395 | }, 396 | HeightRange : { 397 | Min : 1080, 398 | Max : 1080 399 | } 400 | }, 401 | VideoSourceTokensAvailable : "video_src_token" 402 | //Extension : { 403 | //Rotate : { 404 | //Mode : { xs:string}, 405 | //DegreeList : { 406 | //Items : [{ xs:int}] 407 | //}, 408 | //Extension : { } 409 | //}, 410 | //Extension : { } 411 | //} 412 | } 413 | }; 414 | return GetVideoSourceConfigurationOptionsResponse; 415 | } 416 | } 417 | } 418 | export = MediaService; 419 | -------------------------------------------------------------------------------- /services/ptz_service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import fs = require("fs"); 4 | import util = require("util"); 5 | import os = require('os'); 6 | import SoapService = require('../lib/SoapService'); 7 | import { Utils } from '../lib/utils'; 8 | import { Server } from 'http'; 9 | import PTZDriver = require('../lib/PTZDriver'); 10 | 11 | var utils = Utils.utils; 12 | 13 | class PTZService extends SoapService { 14 | ptz_service: any; 15 | callback: any; 16 | ptz_driver: PTZDriver; 17 | 18 | presetArray = []; 19 | 20 | public ptzConfiguration: any; 21 | 22 | 23 | constructor(config: rposConfig, server: Server, callback, ptz_driver: PTZDriver) { 24 | super(config, server); 25 | 26 | this.ptz_service = require('./stubs/ptz_service.js').PTZService; 27 | this.callback = callback; 28 | this.ptz_driver = ptz_driver; 29 | 30 | this.serviceOptions = { 31 | path: '/onvif/ptz_service', 32 | services: this.ptz_service, 33 | xml: fs.readFileSync('./wsdl/ptz_service.wsdl', 'utf8'), 34 | wsdlPath: 'wsdl/ptz_service.wsdl', 35 | onReady: () => console.log('ptz_service started') 36 | }; 37 | 38 | for (var i = 1; i <= 255; i++) { 39 | this.presetArray.push({profileToken: 'profile_token', presetName: '', presetToken: i.toString(), used: false}); 40 | } 41 | 42 | this.extendService(); 43 | } 44 | 45 | leftPad(number, targetLength) { 46 | var output = number + ''; 47 | while (output.length < targetLength) { 48 | output = '0' + output; 49 | } 50 | return output; 51 | } 52 | 53 | extendService() { 54 | var port = this.ptz_service.PTZService.PTZ; 55 | 56 | var node = { 57 | attributes : { 58 | token : 'ptz_node_token_0', 59 | FixedHomePosition: this.ptz_driver.hasFixedHomePosition, 60 | GeoMove: false 61 | }, 62 | Name : 'PTZ Node 0', 63 | SupportedPTZSpaces : {}, 64 | MaximumNumberOfPresets : 255, 65 | HomeSupported : this.ptz_driver.supportsGoToHome, 66 | AuxiliaryCommands : ['AUX1on','AUX1off','AUX2on','AUX2off', 67 | 'AUX3on','AUX3off','AUX4on','AUX4off', 68 | 'AUX5on','AUX5off','AUX6on','AUX6off', 69 | 'AUX7on','AUX7off','AUX8on','AUX8off'] 70 | } 71 | 72 | if (this.ptz_driver.supportsAbsolutePTZ) { 73 | node.SupportedPTZSpaces['AbsolutePanTiltPositionSpace'] = [{ 74 | URI : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace', 75 | XRange : { 76 | Min : -1.0, 77 | Max : 1.0 78 | }, 79 | YRange : { 80 | Min : -1.0, 81 | Max : 1.0 82 | } 83 | }]; 84 | } 85 | if (this.ptz_driver.supportsRelativePTZ) { 86 | node.SupportedPTZSpaces['RelativePanTiltTranslationSpace'] = [{ 87 | URI : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace', 88 | XRange : { 89 | Min : -1.0, 90 | Max : 1.0 91 | }, 92 | YRange : { 93 | Min : -1.0, 94 | Max : 1.0 95 | } 96 | }]; 97 | } 98 | if (this.ptz_driver.supportsContinuousPTZ) { 99 | node.SupportedPTZSpaces['ContinuousPanTiltVelocitySpace'] = [{ 100 | URI : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace', 101 | XRange : { 102 | Min : -1.0, 103 | Max : 1.0 104 | }, 105 | YRange : { 106 | Min : -1.0, 107 | Max : 1.0 108 | } 109 | }]; 110 | node.SupportedPTZSpaces['ContinuousZoomVelocitySpace'] = [{ 111 | URI : 'http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace', 112 | XRange : { 113 | Min : -1.0, 114 | Max : 1.0 115 | } 116 | }]; 117 | } 118 | if (this.ptz_driver.supportsRelativePTZ || this.ptz_driver.supportsAbsolutePTZ) { 119 | node.SupportedPTZSpaces['PanTiltSpeedSpace'] = [{ 120 | URI : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace', 121 | XRange : { 122 | Min : 0, 123 | Max : 1 124 | } 125 | }]; 126 | node.SupportedPTZSpaces['ZoomSpeedSpace'] = [{ 127 | URI : 'http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace', 128 | XRange : { 129 | Min : 0, 130 | Max : 1 131 | } 132 | }]; 133 | } 134 | 135 | // ptzConfigurations is an Array. 136 | var ptzConfigurationOptions = { 137 | Spaces: node.SupportedPTZSpaces, 138 | PTZTimeout : { 139 | Min : 'PT0S', 140 | Max : 'PT10S' 141 | }, 142 | }; 143 | 144 | this.ptzConfiguration = { 145 | attributes: { 146 | token: "ptz_config_token_0" 147 | }, 148 | Name: "PTZ Configuration", 149 | UseCount: 1, 150 | NodeToken: "ptz_node_token_0", 151 | DefaultAbsolutePantTiltPositionSpace : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace', 152 | DefaultAbsoluteZoomPositionSpace : 'http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace', 153 | DefaultRelativePanTiltTranslationSpace : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace', 154 | DefaultRelativeZoomTranslationSpace : 'http://www.onvif.org/ver10/tptz/ZoomSpaces/TranslationGenericSpace', 155 | DefaultContinuousPanTiltVelocitySpace : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace', 156 | DefaultContinuousZoomVelocitySpace : 'http://www.onvif.org/ver10/tptz/ZoomSpaces/VelocityGenericSpace', 157 | DefaultPTZSpeed : { 158 | PanTilt : { 159 | attributes : { 160 | x : 1.0, 161 | y : 1.0, 162 | space : 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace' 163 | } 164 | }, 165 | Zoom : { 166 | attributes : { 167 | x : 1, 168 | space : 'http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace' 169 | } 170 | } 171 | }, 172 | DefaultPTZTimeout : 'PT5S' 173 | } 174 | 175 | 176 | port.GetServiceCapabilities = (args) => { 177 | var GetServiceCapabilitiesResponse = { 178 | Capabilities : { 179 | attributes : { 180 | EFlip : false, 181 | Reverse : false, 182 | GetCompatibleConfigurations : false, 183 | MoveStatus : false, 184 | StatusPosition : false 185 | } 186 | } 187 | }; 188 | return GetServiceCapabilitiesResponse; 189 | }; 190 | 191 | port.GetConfigurationOptions = (args) => { 192 | // ToDo. Check token and return a valid response or an error reponse 193 | var GetConfigurationOptionsResponse = { PTZConfigurationOptions: ptzConfigurationOptions }; 194 | return GetConfigurationOptionsResponse; 195 | }; 196 | 197 | 198 | port.GetConfiguration = (args) => { 199 | // ToDo. Check token and return a valid response or an error reponse 200 | var GetConfigurationResponse = { PTZConfiguration: this.ptzConfiguration }; 201 | return GetConfigurationResponse; 202 | }; 203 | 204 | port.GetConfigurations = (args) => { 205 | var GetConfigurationsResponse = { PTZConfiguration: this.ptzConfiguration }; 206 | return GetConfigurationsResponse; 207 | }; 208 | 209 | // port.GetCompatibleConfigurations = (args) => { 210 | // var GetCompatibleConfigurationsResponse = { }; 211 | // return GetCompatibleConfigurationsResponse; 212 | // }; 213 | 214 | port.GetNode = (args) => { 215 | // ToDo. Check token and return a valid response or an error reponse 216 | var GetNodeResponse = { PTZNode: node }; 217 | return GetNodeResponse; 218 | }; 219 | 220 | port.GetNodes = (args) => { 221 | var GetNodesResponse = { PTZNode: node }; 222 | return GetNodesResponse; 223 | }; 224 | 225 | port.GetStatus = (arg) => { 226 | // ToDo. Check token and return a valid response or an error reponse 227 | 228 | var now = new Date(); 229 | var utc = now.getUTCFullYear() + '-' + this.leftPad((now.getUTCMonth()+1),2) + '-' + this.leftPad(now.getUTCDate(),2) + 'T' 230 | + this.leftPad(now.getUTCHours(),2) + ':' + this.leftPad(now.getUTCMinutes(),2) + ':' + this.leftPad(now.getUTCSeconds(),2) + 'Z'; 231 | 232 | var GetStatusResponse = { 233 | PTZStatus: { 234 | UtcTime: utc 235 | } 236 | }; 237 | return GetStatusResponse; 238 | }; 239 | 240 | port.SetHomePosition = (args) => { 241 | if (this.callback) this.callback('sethome', {}); 242 | var SetHomePositionResponse = { }; 243 | return SetHomePositionResponse; 244 | }; 245 | 246 | port.GotoHomePosition = (args) => { 247 | if (this.callback) this.callback('gotohome', {}); 248 | var GotoHomePositionResponse = { }; 249 | return GotoHomePositionResponse; 250 | }; 251 | 252 | var pan = 0; 253 | var tilt = 0; 254 | var zoom = 0; 255 | var timeout = ''; 256 | 257 | port.ContinuousMove = (args) => { 258 | // Update values or keep last known value 259 | // ODM sends PanTilt OR Zoom but not both 260 | // Other VMS systems can send PanTilt AND Zoom together 261 | try {pan = args.Velocity.PanTilt.attributes.x} catch (err){}; 262 | try {tilt = args.Velocity.PanTilt.attributes.y} catch (err){}; 263 | try {zoom = args.Velocity.Zoom.attributes.x} catch (err){}; 264 | try {timeout = args.Timeout} catch (err){}; 265 | if (this.callback) this.callback('ptz', { pan: pan, tilt: tilt, zoom: zoom}); 266 | var ContinuousMoveResponse = { }; 267 | return ContinuousMoveResponse; 268 | }; 269 | 270 | port.AbsoluteMove = (args) => { 271 | // Update values or keep last known value 272 | try {pan = args.Position.PanTilt.attributes.x} catch (err){}; 273 | try {tilt = args.Position.PanTilt.attributes.y} catch (err){}; 274 | try {zoom = args.Position.Zoom.attributes.x} catch (err){}; 275 | if (this.callback) this.callback('absolute-ptz', { pan: pan, tilt: tilt, zoom: zoom}); 276 | var AbsoluteMoveResponse = { }; 277 | return AbsoluteMoveResponse; 278 | }; 279 | 280 | port.RelativeMove = (args) => { 281 | // Update values or keep last known value 282 | try {pan = args.Translation.PanTilt.attributes.x} catch (err){}; 283 | try {tilt = args.Translation.PanTilt.attributes.y} catch (err){}; 284 | try {zoom = args.Translation.Zoom.attributes.x} catch (err){}; 285 | if (this.callback) this.callback('relative-ptz', { pan: pan, tilt: tilt, zoom: zoom}); 286 | var RelativeMoveResponse = { }; 287 | return RelativeMoveResponse; 288 | }; 289 | 290 | port.Stop = (args) => { 291 | // Update values (to zero) or keep last known value 292 | // ODM just sends Zoom:true or PanTilt:true 293 | // Other VMS systems could stop Zoom and PanTilt in one command 294 | var pan_tilt_stop = false; 295 | var zoom_stop = false; 296 | try {pan_tilt_stop = args.PanTilt} catch (err){}; 297 | try {zoom_stop = args.Zoom} catch (err){}; 298 | if (pan_tilt_stop) { 299 | pan = 0; 300 | tilt = 0; 301 | } 302 | if (zoom_stop) { 303 | zoom = 0; 304 | } 305 | if (this.callback) this.callback('ptz', { pan: pan, tilt: tilt, zoom: zoom}); 306 | var StopResponse = { }; 307 | return StopResponse; 308 | }; 309 | 310 | 311 | //var SendAuxiliaryCommand = { 312 | // ProfileToken : { xs:string}, 313 | // AuxiliaryData : { xs:string} 314 | //}; 315 | port.SendAuxiliaryCommand = (args) => { 316 | if (this.callback) this.callback('aux', { name: args.AuxiliaryData }); 317 | var SendAuxiliaryCommandResponse = { 318 | AuxiliaryResponse : true // no idea what the value should be 319 | }; 320 | return SendAuxiliaryCommandResponse; 321 | }; 322 | 323 | port.GetPresets = (args) => { 324 | var GetPresetsResponse = { Preset: [] }; 325 | var matching_profileToken = args.ProfileToken; 326 | 327 | for (var i = 0 ; i < this.presetArray.length; i++) { 328 | if (this.presetArray[i].profileToken === matching_profileToken 329 | && this.presetArray[i].used == true) { 330 | var p = { 331 | attributes: { 332 | token: this.presetArray[i].presetToken 333 | }, 334 | Name: this.presetArray[i].presetName 335 | }; 336 | GetPresetsResponse.Preset.push(p); 337 | } 338 | } 339 | return GetPresetsResponse; 340 | }; 341 | 342 | 343 | port.GotoPreset = (args) => { 344 | var GotoPresetResponse = { }; 345 | var matching_profileToken = args.ProfileToken; 346 | var matching_presetToken = args.PresetToken; 347 | 348 | for (var i = 0 ; i < this.presetArray.length; i++) { 349 | if (matching_profileToken === this.presetArray[i].profileToken 350 | && matching_presetToken === this.presetArray[i].presetToken 351 | && this.presetArray[i].used == true) { 352 | if (this.callback) this.callback('gotopreset', { name: this.presetArray[i].presetName, 353 | value: this.presetArray[i].presetToken }); 354 | break; 355 | } 356 | } 357 | return GotoPresetResponse; 358 | }; 359 | 360 | port.RemovePreset = (args) => { 361 | var RemovePresetResponse = { }; 362 | 363 | var matching_profileToken = args.ProfileToken; 364 | var matching_presetToken = args.PresetToken; 365 | 366 | for (var i = 0 ; i < this.presetArray.length; i++) { 367 | if (matching_profileToken === this.presetArray[i].profileToken 368 | && matching_presetToken === this.presetArray[i].presetToken) { 369 | this.presetArray[i].used = false; 370 | if (this.callback) this.callback('clearpreset', { name: this.presetArray[i].presetName, 371 | value: this.presetArray[i].presetToken }); 372 | break; 373 | } 374 | } 375 | 376 | return RemovePresetResponse; 377 | }; 378 | 379 | port.SetPreset = (args) => { 380 | 381 | var SetPresetResponse; 382 | 383 | var profileToken = args.ProfileToken; 384 | var presetName = args.PresetName; // used when creating a preset 385 | var presetToken = args.PresetToken; // used when updating an existing preset 386 | 387 | 388 | if (presetToken) { 389 | for (var i = 0; i < this.presetArray.length; i++) { 390 | if (profileToken === this.presetArray[i] 391 | && presetToken === this.presetArray[i]) { 392 | this.presetArray[i].presetName = presetName; 393 | this.presetArray[i].used = true; 394 | if (this.callback) this.callback('setpreset', { name: presetName, 395 | value: presetToken }); 396 | break; 397 | } 398 | SetPresetResponse = { PresetToken : presetToken}; 399 | 400 | return SetPresetResponse; 401 | } 402 | } else { 403 | // Check if the preset name is a number (special case) 404 | var special_case_name = false; 405 | try { 406 | var preset_name_value = parseInt(presetName); 407 | if (preset_name_value > 0 && preset_name_value < 255) { 408 | special_case_name = true; 409 | } 410 | } catch (err) { 411 | } 412 | if (special_case_name) { 413 | if (this.callback) this.callback('setpreset', { name: presetName, 414 | value: presetName }); 415 | SetPresetResponse = { PresetToken : presetName}; 416 | return SetPresetResponse; 417 | } else { 418 | // Find the first unused token and use it 419 | var new_presetToken = ''; 420 | for (var i = 0; i < this.presetArray.length; i++) { 421 | if (profileToken === this.presetArray[i].profileToken 422 | && this.presetArray[i].used == false) { 423 | this.presetArray[i].presetName = presetName; 424 | this.presetArray[i].used = true; 425 | new_presetToken = this.presetArray[i].presetToken; 426 | if (this.callback) this.callback('setpreset', { name: presetName, 427 | value: new_presetToken }); 428 | break; 429 | } 430 | } 431 | SetPresetResponse = { PresetToken : new_presetToken}; 432 | return SetPresetResponse; 433 | } 434 | } 435 | }; 436 | } 437 | } 438 | export = PTZService; 439 | -------------------------------------------------------------------------------- /services/stubs/imaging_service.js: -------------------------------------------------------------------------------- 1 | // This file is generated by the 'node-soap-servicegenerator' 2 | // visit : https://github.com/BreeeZe/node-soap-servicegenerator for more info 3 | 4 | var NOT_IMPLEMENTED = { 5 | Fault: { 6 | Code: { 7 | Value: "soap:client" 8 | }, 9 | Reason: { 10 | Text: "Method not implemented" 11 | } 12 | } 13 | }; 14 | var exports = module.exports = {}; 15 | 16 | exports.ImagingService = { 17 | ImagingService : { 18 | Imaging : { 19 | //var GetServiceCapabilities = { }; 20 | GetServiceCapabilities : function(args /*, cb, headers*/) { 21 | throw NOT_IMPLEMENTED; 22 | //var GetServiceCapabilitiesResponse = { 23 | //Capabilities : { 24 | //attributes : { 25 | //ImageStabilization : {xs:boolean}, 26 | //Presets : {xs:boolean} 27 | //} 28 | //} 29 | // 30 | //}; 31 | //return GetServiceCapabilitiesResponse; 32 | }, 33 | 34 | //var GetImagingSettings = { 35 | //VideoSourceToken : { xs:string} 36 | // 37 | //}; 38 | GetImagingSettings : function(args /*, cb, headers*/) { 39 | throw NOT_IMPLEMENTED; 40 | //var GetImagingSettingsResponse = { 41 | //ImagingSettings : { 42 | //BacklightCompensation : { 43 | //Mode : { xs:string}, 44 | //Level : { xs:float} 45 | //}, 46 | //Brightness : { xs:float}, 47 | //ColorSaturation : { xs:float}, 48 | //Contrast : { xs:float}, 49 | //Exposure : { 50 | //Mode : { xs:string}, 51 | //Priority : { xs:string}, 52 | //Window : { 53 | //attributes : { 54 | //bottom : {xs:float}, 55 | //top : {xs:float}, 56 | //right : {xs:float}, 57 | //left : {xs:float} 58 | //} 59 | //}, 60 | //MinExposureTime : { xs:float}, 61 | //MaxExposureTime : { xs:float}, 62 | //MinGain : { xs:float}, 63 | //MaxGain : { xs:float}, 64 | //MinIris : { xs:float}, 65 | //MaxIris : { xs:float}, 66 | //ExposureTime : { xs:float}, 67 | //Gain : { xs:float}, 68 | //Iris : { xs:float} 69 | //}, 70 | //Focus : { 71 | //AutoFocusMode : { xs:string}, 72 | //DefaultSpeed : { xs:float}, 73 | //NearLimit : { xs:float}, 74 | //FarLimit : { xs:float}, 75 | //Extension : { } 76 | //}, 77 | //IrCutFilter : { xs:string}, 78 | //Sharpness : { xs:float}, 79 | //WideDynamicRange : { 80 | //Mode : { xs:string}, 81 | //Level : { xs:float} 82 | //}, 83 | //WhiteBalance : { 84 | //Mode : { xs:string}, 85 | //CrGain : { xs:float}, 86 | //CbGain : { xs:float}, 87 | //Extension : { } 88 | //}, 89 | //Extension : { 90 | //ImageStabilization : { 91 | //Mode : { xs:string}, 92 | //Level : { xs:float}, 93 | //Extension : { } 94 | //}, 95 | //Extension : { 96 | //IrCutFilterAutoAdjustment : [{ 97 | //BoundaryType : { xs:string}, 98 | //BoundaryOffset : { xs:float}, 99 | //ResponseTime : { xs:duration}, 100 | //Extension : { } 101 | //}], 102 | //Extension : { 103 | //ToneCompensation : { 104 | //Mode : { xs:string}, 105 | //Level : { xs:float}, 106 | //Extension : { } 107 | //}, 108 | //Defogging : { 109 | //Mode : { xs:string}, 110 | //Level : { xs:float}, 111 | //Extension : { } 112 | //}, 113 | //NoiseReduction : { 114 | //Level : { xs:float} 115 | //}, 116 | //Extension : { } 117 | //} 118 | //} 119 | //} 120 | //} 121 | // 122 | //}; 123 | //return GetImagingSettingsResponse; 124 | }, 125 | 126 | //var SetImagingSettings = { 127 | //VideoSourceToken : { xs:string}, 128 | //ImagingSettings : { 129 | //BacklightCompensation : { 130 | //Mode : { xs:string}, 131 | //Level : { xs:float} 132 | //}, 133 | //Brightness : { xs:float}, 134 | //ColorSaturation : { xs:float}, 135 | //Contrast : { xs:float}, 136 | //Exposure : { 137 | //Mode : { xs:string}, 138 | //Priority : { xs:string}, 139 | //Window : { 140 | //attributes : { 141 | //bottom : {xs:float}, 142 | //top : {xs:float}, 143 | //right : {xs:float}, 144 | //left : {xs:float} 145 | //} 146 | //}, 147 | //MinExposureTime : { xs:float}, 148 | //MaxExposureTime : { xs:float}, 149 | //MinGain : { xs:float}, 150 | //MaxGain : { xs:float}, 151 | //MinIris : { xs:float}, 152 | //MaxIris : { xs:float}, 153 | //ExposureTime : { xs:float}, 154 | //Gain : { xs:float}, 155 | //Iris : { xs:float} 156 | //}, 157 | //Focus : { 158 | //AutoFocusMode : { xs:string}, 159 | //DefaultSpeed : { xs:float}, 160 | //NearLimit : { xs:float}, 161 | //FarLimit : { xs:float}, 162 | //Extension : { } 163 | //}, 164 | //IrCutFilter : { xs:string}, 165 | //Sharpness : { xs:float}, 166 | //WideDynamicRange : { 167 | //Mode : { xs:string}, 168 | //Level : { xs:float} 169 | //}, 170 | //WhiteBalance : { 171 | //Mode : { xs:string}, 172 | //CrGain : { xs:float}, 173 | //CbGain : { xs:float}, 174 | //Extension : { } 175 | //}, 176 | //Extension : { 177 | //ImageStabilization : { 178 | //Mode : { xs:string}, 179 | //Level : { xs:float}, 180 | //Extension : { } 181 | //}, 182 | //Extension : { 183 | //IrCutFilterAutoAdjustment : [{ 184 | //BoundaryType : { xs:string}, 185 | //BoundaryOffset : { xs:float}, 186 | //ResponseTime : { xs:duration}, 187 | //Extension : { } 188 | //}], 189 | //Extension : { 190 | //ToneCompensation : { 191 | //Mode : { xs:string}, 192 | //Level : { xs:float}, 193 | //Extension : { } 194 | //}, 195 | //Defogging : { 196 | //Mode : { xs:string}, 197 | //Level : { xs:float}, 198 | //Extension : { } 199 | //}, 200 | //NoiseReduction : { 201 | //Level : { xs:float} 202 | //}, 203 | //Extension : { } 204 | //} 205 | //} 206 | //} 207 | //}, 208 | //ForcePersistence : [{ xs:boolean}] 209 | // 210 | //}; 211 | SetImagingSettings : function(args /*, cb, headers*/) { 212 | throw NOT_IMPLEMENTED; 213 | //var SetImagingSettingsResponse = { }; 214 | //return SetImagingSettingsResponse; 215 | }, 216 | 217 | //var GetOptions = { 218 | //VideoSourceToken : { xs:string} 219 | // 220 | //}; 221 | GetOptions : function(args /*, cb, headers*/) { 222 | throw NOT_IMPLEMENTED; 223 | //var GetOptionsResponse = { 224 | //ImagingOptions : { 225 | //BacklightCompensation : { 226 | //Mode : { xs:string}, 227 | //Level : { 228 | //Min : { xs:float}, 229 | //Max : { xs:float} 230 | //} 231 | //}, 232 | //Brightness : { 233 | //Min : { xs:float}, 234 | //Max : { xs:float} 235 | //}, 236 | //ColorSaturation : { 237 | //Min : { xs:float}, 238 | //Max : { xs:float} 239 | //}, 240 | //Contrast : { 241 | //Min : { xs:float}, 242 | //Max : { xs:float} 243 | //}, 244 | //Exposure : { 245 | //Mode : { xs:string}, 246 | //Priority : [{ xs:string}], 247 | //MinExposureTime : { 248 | //Min : { xs:float}, 249 | //Max : { xs:float} 250 | //}, 251 | //MaxExposureTime : { 252 | //Min : { xs:float}, 253 | //Max : { xs:float} 254 | //}, 255 | //MinGain : { 256 | //Min : { xs:float}, 257 | //Max : { xs:float} 258 | //}, 259 | //MaxGain : { 260 | //Min : { xs:float}, 261 | //Max : { xs:float} 262 | //}, 263 | //MinIris : { 264 | //Min : { xs:float}, 265 | //Max : { xs:float} 266 | //}, 267 | //MaxIris : { 268 | //Min : { xs:float}, 269 | //Max : { xs:float} 270 | //}, 271 | //ExposureTime : { 272 | //Min : { xs:float}, 273 | //Max : { xs:float} 274 | //}, 275 | //Gain : { 276 | //Min : { xs:float}, 277 | //Max : { xs:float} 278 | //}, 279 | //Iris : { 280 | //Min : { xs:float}, 281 | //Max : { xs:float} 282 | //} 283 | //}, 284 | //Focus : { 285 | //AutoFocusModes : [{ xs:string}], 286 | //DefaultSpeed : { 287 | //Min : { xs:float}, 288 | //Max : { xs:float} 289 | //}, 290 | //NearLimit : { 291 | //Min : { xs:float}, 292 | //Max : { xs:float} 293 | //}, 294 | //FarLimit : { 295 | //Min : { xs:float}, 296 | //Max : { xs:float} 297 | //}, 298 | //Extension : { } 299 | //}, 300 | //IrCutFilterModes : [{ xs:string}], 301 | //Sharpness : { 302 | //Min : { xs:float}, 303 | //Max : { xs:float} 304 | //}, 305 | //WideDynamicRange : { 306 | //Mode : { xs:string}, 307 | //Level : { 308 | //Min : { xs:float}, 309 | //Max : { xs:float} 310 | //} 311 | //}, 312 | //WhiteBalance : { 313 | //Mode : { xs:string}, 314 | //YrGain : { 315 | //Min : { xs:float}, 316 | //Max : { xs:float} 317 | //}, 318 | //YbGain : { 319 | //Min : { xs:float}, 320 | //Max : { xs:float} 321 | //}, 322 | //Extension : { } 323 | //}, 324 | //Extension : { 325 | //ImageStabilization : { 326 | //Mode : { xs:string}, 327 | //Level : { 328 | //Min : { xs:float}, 329 | //Max : { xs:float} 330 | //}, 331 | //Extension : { } 332 | //}, 333 | //Extension : { 334 | //IrCutFilterAutoAdjustment : { 335 | //BoundaryType : { xs:string}, 336 | //BoundaryOffset : { xs:boolean}, 337 | //ResponseTimeRange : { 338 | //Min : { xs:duration}, 339 | //Max : { xs:duration} 340 | //}, 341 | //Extension : { } 342 | //}, 343 | //Extension : { 344 | //ToneCompensationOptions : { 345 | //Mode : { xs:string}, 346 | //Level : { xs:boolean} 347 | //}, 348 | //DefoggingOptions : { 349 | //Mode : { xs:string}, 350 | //Level : { xs:boolean} 351 | //}, 352 | //NoiseReductionOptions : { 353 | //Level : { xs:boolean} 354 | //}, 355 | //Extension : { } 356 | //} 357 | //} 358 | //} 359 | //} 360 | // 361 | //}; 362 | //return GetOptionsResponse; 363 | }, 364 | 365 | //var Move = { 366 | //VideoSourceToken : { xs:string}, 367 | //Focus : { 368 | //Absolute : { 369 | //Position : { xs:float}, 370 | //Speed : { xs:float} 371 | //}, 372 | //Relative : { 373 | //Distance : { xs:float}, 374 | //Speed : { xs:float} 375 | //}, 376 | //Continuous : { 377 | //Speed : { xs:float} 378 | //} 379 | //} 380 | // 381 | //}; 382 | Move : function(args /*, cb, headers*/) { 383 | throw NOT_IMPLEMENTED; 384 | //var MoveResponse = { }; 385 | //return MoveResponse; 386 | }, 387 | 388 | //var GetMoveOptions = { 389 | //VideoSourceToken : { xs:string} 390 | // 391 | //}; 392 | GetMoveOptions : function(args /*, cb, headers*/) { 393 | throw NOT_IMPLEMENTED; 394 | //var GetMoveOptionsResponse = { 395 | //MoveOptions : { 396 | //Absolute : { 397 | //Position : { 398 | //Min : { xs:float}, 399 | //Max : { xs:float} 400 | //}, 401 | //Speed : { 402 | //Min : { xs:float}, 403 | //Max : { xs:float} 404 | //} 405 | //}, 406 | //Relative : { 407 | //Distance : { 408 | //Min : { xs:float}, 409 | //Max : { xs:float} 410 | //}, 411 | //Speed : { 412 | //Min : { xs:float}, 413 | //Max : { xs:float} 414 | //} 415 | //}, 416 | //Continuous : { 417 | //Speed : { 418 | //Min : { xs:float}, 419 | //Max : { xs:float} 420 | //} 421 | //} 422 | //} 423 | // 424 | //}; 425 | //return GetMoveOptionsResponse; 426 | }, 427 | 428 | //var Stop = { 429 | //VideoSourceToken : { xs:string} 430 | // 431 | //}; 432 | Stop : function(args /*, cb, headers*/) { 433 | throw NOT_IMPLEMENTED; 434 | //var StopResponse = { }; 435 | //return StopResponse; 436 | }, 437 | 438 | //var GetStatus = { 439 | //VideoSourceToken : { xs:string} 440 | // 441 | //}; 442 | GetStatus : function(args /*, cb, headers*/) { 443 | throw NOT_IMPLEMENTED; 444 | //var GetStatusResponse = { 445 | //Status : { 446 | //FocusStatus20 : { 447 | //Position : { xs:float}, 448 | //MoveStatus : { xs:string}, 449 | //Error : { xs:string}, 450 | //Extension : { } 451 | //}, 452 | //Extension : { } 453 | //} 454 | // 455 | //}; 456 | //return GetStatusResponse; 457 | }, 458 | 459 | //var GetPresets = { 460 | //VideoSourceToken : { xs:string} 461 | // 462 | //}; 463 | GetPresets : function(args /*, cb, headers*/) { 464 | throw NOT_IMPLEMENTED; 465 | //var GetPresetsResponse = { 466 | //Preset : { 467 | //attributes : { 468 | //token : {tt:ReferenceToken}, 469 | //type : {xs:string} 470 | //}, 471 | //Name : { xs:string} 472 | //} 473 | // 474 | //}; 475 | //return GetPresetsResponse; 476 | }, 477 | 478 | //var GetCurrentPreset = { 479 | //VideoSourceToken : { xs:string} 480 | // 481 | //}; 482 | GetCurrentPreset : function(args /*, cb, headers*/) { 483 | throw NOT_IMPLEMENTED; 484 | //var GetCurrentPresetResponse = { 485 | //Preset : { 486 | //attributes : { 487 | //token : {tt:ReferenceToken}, 488 | //type : {xs:string} 489 | //}, 490 | //Name : { xs:string} 491 | //} 492 | // 493 | //}; 494 | //return GetCurrentPresetResponse; 495 | }, 496 | 497 | //var SetCurrentPreset = { 498 | //VideoSourceToken : { xs:string}, 499 | //PresetToken : { xs:string} 500 | // 501 | //}; 502 | SetCurrentPreset : function(args /*, cb, headers*/) { 503 | throw NOT_IMPLEMENTED; 504 | //var SetCurrentPresetResponse = { }; 505 | //return SetCurrentPresetResponse; 506 | }, 507 | 508 | } 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /setup_v4l2rtspserver.sh: -------------------------------------------------------------------------------- 1 | git clone https://github.com/mpromonet/v4l2rtspserver 2 | sudo apt install -y liblivemedia-dev liblog4cpp5-dev cmake libasound2-dev 3 | cd v4l2rtspserver 4 | cmake . && make 5 | sudo make install 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "preserveConstEnums": false, 8 | "sourceMap": true, 9 | "watch": false 10 | } 11 | } -------------------------------------------------------------------------------- /typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rpos", 3 | "dependencies": {}, 4 | "devDependencies": {}, 5 | "ambientDependencies": { 6 | "body-parser": "github:DefinitelyTyped/DefinitelyTyped/body-parser/body-parser.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd", 7 | "express": "github:DefinitelyTyped/DefinitelyTyped/express/express.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd", 8 | "express-serve-static-core": "github:DefinitelyTyped/DefinitelyTyped/express-serve-static-core/express-serve-static-core.d.ts#fb2e78984b7076eda628892770a4314d618ac0d7", 9 | "ip": "registry:dt/ip#0.0.0+20161128184045", 10 | "mime": "github:DefinitelyTyped/DefinitelyTyped/mime/mime.d.ts#56295f5058cac7ae458540423c50ac2dcf9fc711", 11 | "node": "github:DefinitelyTyped/DefinitelyTyped/node/node.d.ts#6d0e826824da52204c1c93f92cecbc961ac84fa9", 12 | "node-uuid": "github:DefinitelyTyped/DefinitelyTyped/node-uuid/node-uuid.d.ts#56295f5058cac7ae458540423c50ac2dcf9fc711", 13 | "node-uuid-base": "github:DefinitelyTyped/DefinitelyTyped/node-uuid/node-uuid-base.d.ts#56295f5058cac7ae458540423c50ac2dcf9fc711", 14 | "node-uuid-cjs": "github:DefinitelyTyped/DefinitelyTyped/node-uuid/node-uuid-cjs.d.ts#56295f5058cac7ae458540423c50ac2dcf9fc711", 15 | "serve-static": "github:DefinitelyTyped/DefinitelyTyped/serve-static/serve-static.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd", 16 | "xml2js": "github:DefinitelyTyped/DefinitelyTyped/xml2js/xml2js.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /views/camera.ntl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Camera settings 6 | 7 | 8 |
9 | 10 | 11 | {{row}} 12 | 13 |
14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /web/snapshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BreeeZe/rpos/22ea0df47fb920dc8b0e9a630d0523ac984d7623/web/snapshot.jpg -------------------------------------------------------------------------------- /wsdl/device_service.wsdl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /wsdl/docs.oasis-open.org.wsn.t-1.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | TopicPathExpression ::= TopicPath ( '|' TopicPath )* 87 | TopicPath ::= RootTopic ChildTopicExpression* 88 | RootTopic ::= NamespacePrefix? ('//')? (NCName | '*') 89 | NamespacePrefix ::= NCName ':' 90 | ChildTopicExpression ::= '/' '/'? (QName | NCName | '*'| '.') 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | The pattern allows strings matching the following EBNF: 102 | ConcreteTopicPath ::= RootTopic ChildTopic* 103 | RootTopic ::= QName 104 | ChildTopic ::= '/' (QName | NCName) 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | The pattern allows strings matching the following EBNF: 116 | RootTopic ::= QName 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /wsdl/docs.oasis-open.org.wsrf.bf-2.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Get access to the xml: attribute groups for xml:lang as declared on 'schema' 8 | and 'documentation' below 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /wsdl/imaging_service.wsdl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /wsdl/media2_service.wsdl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /wsdl/media_service.wsdl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /wsdl/ptz_service.wsdl: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /wsdl/www.w3.org.2001.xml.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 |

About the XML namespace

7 |
8 |

9 | This schema document describes the XML namespace, in a form 10 | suitable for import by other schema documents. 11 |

12 |

13 | See 14 | http://www.w3.org/XML/1998/namespace.html and 15 | 16 | http://www.w3.org/TR/REC-xml for information 17 | about this namespace. 18 |

19 |

20 | Note that local names in this namespace are intended to be 21 | defined only by the World Wide Web Consortium or its subgroups. 22 | The names currently defined in this namespace are listed below. 23 | They should not be used with conflicting semantics by any Working 24 | Group, specification, or document instance. 25 |

26 |

27 | See further below in this document for more information about how to refer to this schema document from your own 28 | XSD schema documents and about the 29 | namespace-versioning policy governing this schema document. 30 |

31 |
32 |
33 |
34 |
35 | 36 | 37 | 38 |
39 |

lang (as an attribute name)

40 |

41 | denotes an attribute whose value 42 | is a language code for the natural language of the content of 43 | any element; its value is inherited. This name is reserved 44 | by virtue of its definition in the XML specification.

45 |
46 |
47 |

Notes

48 |

49 | Attempting to install the relevant ISO 2- and 3-letter 50 | codes as the enumerated possible values is probably never 51 | going to be a realistic possibility. 52 |

53 |

54 | See BCP 47 at 55 | http://www.rfc-editor.org/rfc/bcp/bcp47.txt 56 | and the IANA language subtag registry at 57 | 58 | http://www.iana.org/assignments/language-subtag-registry 59 | for further information. 60 |

61 |

62 | The union allows for the 'un-declaration' of xml:lang with 63 | the empty string. 64 |

65 |
66 |
67 |
68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 | 80 | 81 |
82 |

space (as an attribute name)

83 |

84 | denotes an attribute whose 85 | value is a keyword indicating what whitespace processing 86 | discipline is intended for the content of the element; its 87 | value is inherited. This name is reserved by virtue of its 88 | definition in the XML specification.

89 |
90 |
91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 |
99 | 100 | 101 | 102 |
103 |

base (as an attribute name)

104 |

105 | denotes an attribute whose value 106 | provides a URI to be used as the base for interpreting any 107 | relative URIs in the scope of the element on which it 108 | appears; its value is inherited. This name is reserved 109 | by virtue of its definition in the XML Base specification.

110 |

111 | See http://www.w3.org/TR/xmlbase/ 112 | for information about this attribute. 113 |

114 |
115 |
116 |
117 |
118 | 119 | 120 | 121 |
122 |

id (as an attribute name)

123 |

124 | denotes an attribute whose value 125 | should be interpreted as if declared to be of type ID. 126 | This name is reserved by virtue of its definition in the 127 | xml:id specification.

128 |

129 | See http://www.w3.org/TR/xml-id/ 130 | for information about this attribute. 131 |

132 |
133 |
134 |
135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
145 |

Father (in any context at all)

146 |
147 |

148 | denotes Jon Bosak, the chair of 149 | the original XML Working Group. This name is reserved by 150 | the following decision of the W3C XML Plenary and 151 | XML Coordination groups: 152 |

153 |
154 |

155 | In appreciation for his vision, leadership and 156 | dedication the W3C XML Plenary on this 10th day of 157 | February, 2000, reserves for Jon Bosak in perpetuity 158 | the XML name "xml:Father". 159 |

160 |
161 |
162 |
163 |
164 |
165 | 166 | 167 |
168 |

169 | About this schema document 170 |

171 |
172 |

173 | This schema defines attributes and an attribute group suitable 174 | for use by schemas wishing to allow xml:base, 175 | xml:lang, xml:space or 176 | xml:id attributes on elements they define. 177 |

178 |

179 | To enable this, such a schema must import this schema for 180 | the XML namespace, e.g. as follows: 181 |

182 |
183 |           <schema . . .>
184 |            . . .
185 |            <import namespace="http://www.w3.org/XML/1998/namespace"
186 |                       schemaLocation="http://www.w3.org/2001/xml.xsd"/>
187 |      
188 |

189 | or 190 |

191 |
192 |            <import namespace="http://www.w3.org/XML/1998/namespace"
193 |                       schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
194 |      
195 |

196 | Subsequently, qualified reference to any of the attributes or the 197 | group defined below will have the desired effect, e.g. 198 |

199 |
200 |           <type . . .>
201 |            . . .
202 |            <attributeGroup ref="xml:specialAttrs"/>
203 |      
204 |

205 | will define a type which will schema-validate an instance element 206 | with any of those attributes. 207 |

208 |
209 |
210 |
211 |
212 | 213 | 214 |
215 |

216 | Versioning policy for this schema document 217 |

218 |
219 |

220 | In keeping with the XML Schema WG's standard versioning 221 | policy, this schema document will persist at 222 | 223 | http://www.w3.org/2009/01/xml.xsd. 224 |

225 |

226 | At the date of issue it can also be found at 227 | 228 | http://www.w3.org/2001/xml.xsd. 229 |

230 |

231 | The schema document at that URI may however change in the future, 232 | in order to remain compatible with the latest version of XML 233 | Schema itself, or with the XML namespace itself. In other words, 234 | if the XML Schema or XML namespaces change, the version of this 235 | document at 236 | http://www.w3.org/2001/xml.xsd 237 | 238 | will change accordingly; the version at 239 | 240 | http://www.w3.org/2009/01/xml.xsd 241 | 242 | will not change. 243 |

244 |

245 | Previous dated (and unchanging) versions of this schema 246 | document are at: 247 |

248 | 266 |
267 |
268 |
269 |
270 |
-------------------------------------------------------------------------------- /wsdl/www.w3.org.2003.05.soap-envelope.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Elements replacing the wildcard MUST be namespace qualified, but can be in the targetNamespace 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Fault reporting structure 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /wsdl/www.w3.org.2004.08.xop.include.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /wsdl/www.w3.org.2005.05.xmlmime.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /wsdl/www.w3.org.2005.08.addressing.ws-addr.xsd: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | --------------------------------------------------------------------------------