├── LICENSE.txt
├── README.md
├── __init__.py
├── addon.xml
├── changelog.txt
├── default.py
├── fanart.jpg
├── icon.png
├── resources
├── __init__.py
├── language
│ └── English
│ │ └── strings.xml
├── lib
│ ├── __init__.py
│ ├── allcameraplayer.py
│ ├── cameraplayer.py
│ ├── camerapreview.py
│ ├── camerasettings.py
│ ├── ipcam_api_foscamhd.py
│ ├── ipcam_api_foscamsd.py
│ ├── ipcam_api_generic.py
│ ├── ipcam_api_wrapper.py
│ ├── monitor.py
│ ├── settings.py
│ └── utils.py
├── media
│ ├── addon_settings.png
│ ├── addon_settings_nofocus.png
│ ├── back.png
│ ├── black.png
│ ├── bottom_left.png
│ ├── bottom_left_nofocus.png
│ ├── bottom_right.png
│ ├── bottom_right_nofocus.png
│ ├── camera_settings.png
│ ├── camera_settings_nofocus.png
│ ├── close.png
│ ├── close_nofocus.png
│ ├── down.png
│ ├── down_nofocus.png
│ ├── error.png
│ ├── home.png
│ ├── home_nofocus.png
│ ├── icon-advanced-menu.png
│ ├── icon-foscam-hd-ptz.png
│ ├── icon-foscam-hd-ptz_old.png
│ ├── icon-foscam-hd.png
│ ├── icon-foscam-hd_old.png
│ ├── icon-foscam-sd-ptz.png
│ ├── icon-foscam-sd.png
│ ├── icon-generic.png
│ ├── icon-settings.png
│ ├── left.png
│ ├── left_down.png
│ ├── left_down_nofocus.png
│ ├── left_nofocus.png
│ ├── left_up.png
│ ├── left_up_nofocus.png
│ ├── loader_old.gif
│ ├── placeholder.jpg
│ ├── radio-off.png
│ ├── radio-on.png
│ ├── right.png
│ ├── right_down.png
│ ├── right_down_nofocus.png
│ ├── right_nofocus.png
│ ├── right_up.png
│ ├── right_up_nofocus.png
│ ├── settings.png
│ ├── settings_nofocus.png
│ ├── top_left.png
│ ├── top_left_nofocus.png
│ ├── top_right.png
│ ├── top_right_nofocus.png
│ ├── trans.png
│ ├── up.png
│ ├── up_nofocus.png
│ ├── zoom_in.png
│ ├── zoom_in_nofocus.png
│ ├── zoom_out.png
│ └── zoom_out_nofocus.png
├── settings.xml
└── textures
│ ├── AddonWindow
│ ├── ContentPanel.png
│ ├── DialogCloseButton-focus.png
│ ├── DialogCloseButton.png
│ ├── SKINDEFAULT.jpg
│ └── dialogheader.png
│ ├── Button
│ ├── KeyboardKey.png
│ └── KeyboardKeyNF.png
│ └── RadioButton
│ ├── MenuItemFO.png
│ ├── MenuItemNF.png
│ ├── radiobutton-focus.png
│ └── radiobutton-nofocus.png
└── service.py
/README.md:
--------------------------------------------------------------------------------
1 | plugin.video.surveillanceroom
2 |
3 | a Kodi add-on by Maikito26
4 |
5 | -- Summary --
6 |
7 | If motion or sound is detected a small image preview will slide onto the screen. Pressing select *will* stop any playing media and open the main video feed with basic controls for pan/tilt and mirror/flip. Exit with the back button or click the close control, and the previously playing file will resume. This works for up to 4 cameras simultaneously
8 | Also, there is a menu to select these cameras individually or all of them to play at once.
9 |
10 |
11 | -- Features --
12 |
13 | - Connect up to 4 IP/Foscam Cameras
14 | - Supports credentials for Foscam, but you can overwrite the URL manually to support non-Foscam cameras, or the C model which has RTSP port hard coded to 554.
15 | - Watch in multiple streaming formats, with camera controls displayed overtop of a single camera view.
16 | - Preview cameras while watching content, with Motion and Sound Detection, or by calling it manually using RunPlugin()
17 | - Open the camera stream from a preview, and will resume what you were watching when you close the stream.
18 | - Logic to determine when preview is allowed to display. Configure which windows not to display for.
19 | - Set a home location to move PTZ enabled Foscam cameras to when Kodi starts
20 |
21 |
22 | -- Quick Start Guide --
23 |
24 | 1. Install the Kodi Add-on
25 | 2. Open the add-on settings
26 | 3. Configure the camera specific settings (additional configure preview settings if desired)
27 | 4. Enable the camera that is configured
28 | 5. Access the add-on through the Programs or Video add-on windows and view cameras
29 |
30 |
31 | -- Calling commands from an External Source --
32 | You can call any action available in default.py and encoding it into the URL with the parameters:
33 | action=
34 | camera_number=
35 |
36 | Some example actions are:
37 |
38 | 1. Showing a single preview window
39 |
40 | XBMC.RunPlugin(plugin://plugin.video.surveillanceroom?action=show_preview&camera_number=1)
41 |
42 | 2. Showing all cameras on fullscreen
43 |
44 | XBMC.RunPlugin(plugin://plugin.video.surveillanceroom?action=all_cameras)
45 |
46 | 3. Showing a single camera on fullscreen, with controls
47 |
48 | XBMC.RunPlugin(plugin://plugin.video.surveillanceroom?action=single_camera&camera_number=1)
49 |
50 | 4. Showing a single camera on fullscreen, without controls
51 |
52 | XBMC.RunPlugin(plugin://plugin.video.surveillanceroom?action=single_camera_no_controls&camera_number=1)
53 |
54 |
55 | Mapping a remote button example:
56 |
57 | XBMC.RunPlugin(plugin://plugin.video.surveillanceroom?action=show_preview&camera_number=1)
58 |
59 |
60 |
61 | This add-on was developed in the following environment:
62 | - Windows 10
63 | - Kodi 15.2
64 | - Foscam HD Camers: F19831w & F19804p, with firmware v2.11.1.118
65 | - D-Link DCS-932L generic IP camera
66 |
67 |
68 | Credit and thanks to the following add-ons/developers for inspiration and a lot of the groundwork:
69 | * https://github.com/LS80/script.foscam (http://forum.xbmc.org/showthread.php?tid=190439)
70 | * https://github.com/RyanMelenaNoesis/Xbmc...ecuritycam (http://forum.xbmc.org/showthread.php?tid=182540)
71 | * https://github.com/Shigoru/script.securitycams (http://forum.kodi.tv/showthread.php?tid=218815)
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/__init__.py
--------------------------------------------------------------------------------
/addon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | executable video
9 |
10 |
11 |
12 | all
13 | Surveillance Room is a full featured IP Camera viewer for up to 6 IP Cameras. This add-on also supports Foscam IP Cameras APIs.
14 | Originally designed for Foscam HD IP cameras, this addon can now live stream and monitor motion and sound alarms for up to 6 IP Cameras. The add-on can show the video feed for up to 4 IP Cameras simultaneously, or 6 cameras individaully. Motion or sound alarms for Foscam IP Cameras can be made to trigger a preview of the camera, or the preview windows can be called by a button, script or externally.
15 | This add-on is not affiliated with Foscam. This add-on will modify some settings on the camera itself such as motion and sound detection settings.
16 | http://forum.kodi.tv/showthread.php?tid=240768
17 | https://github.com/maikito26
18 | https://github.com/maikito26/plugin.video.surveillanceroom.git
19 | GNU GENERAL PUBLIC LICENSE. Version 2, June 1991
20 |
21 |
22 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | v1.2.3 (Nov 30 2015)
2 | --------------------
3 | - Removed import of previewgui in default.py
4 |
5 | v1.2.2 (Nov 22 2015)
6 | --------------------
7 | - Few more fixes to logic when playing fullscreen from preview
8 | - Added settings to control how playing fullscreen from preview works, including choosing between all camera player, or itself with/without controls
9 | - Made changes to what is logging, including fixing the service changing the log level if it changes.
10 |
11 | v1.2.1 (Nov 15 2015)
12 | --------------------
13 | - Few fixes to logic when playing fullscreen from preview
14 | - Resolved negative wait time issue when trigger_interval is reported as 0 from the Foscam HD camera
15 |
16 | v1.2.0 (Nov 14 2015)
17 | --------------------
18 | - New icons for SD/HD Foscam Cameras to differentiate
19 | - Updated description in addon.xml
20 | - Added Context Menu options to main menu cameras
21 | - Fixed MjpegInterlace setting on Previews
22 | - Decoupled the service from the preview window to make the add-on more capable and efficient. Open and Close now done through requests
23 | - Ability to restart the service manually
24 | - Added hard timeout for mjpeg frame extractor code due to it causing a 'crashed' camera to never exit a loop
25 | - 5 and 6 camera support added for previews and menu, not integrated yet into All Camera player which still is cameras 1-4 only
26 | - Ability now to remotely change the URL of the Preview Camera as requested by @mrjd
27 | - Fixed any found bugs
28 |
29 | v1.1.1 (Nov 08 2015)
30 | --------------------
31 | - Broke a few things in the process of finalizing, fixed this...
32 |
33 | v1.1.0 (Nov 06 2015)
34 | --------------------
35 | - Initial Foscam SD Support. Core functions covered but waiting on official SDK approval from Foscam to make sure I can properly implement the features.
36 | - Revamped Camera API system so add-on can be made to support more camera types if the API is known
37 | - Support for Python 2.6 builds of Kodi fixed
38 | - Added in a feature setting to Toggle Preview Window Open or Closed using the same command (useful if mapped to a button)
39 | - Altered Trigger Interval setting to original way as a camera with continued alarm detection could close then reopen instead of continuing to stay open until the alarm condition cleared
40 | - PTZ Sensitivity is now set individually by camera
41 | - Improvements made to error handling and detection when acquiring images
42 | - Added a Timeout setting for HTTP requests so if one camera goes offline, it doesn't slow down the Add-on responsiveness when accessing the menu. Also errors out faster if images can't be acquired.
43 | - Fixed Dismissing previews so all previews are dismissed properly if that is how it is configured in settings (previously only affected interval)
44 | - Collapsed image updating for previews into the preview window class
45 |
46 | Foscam SD Current Feature Support
47 | - All playback methods including Previews
48 | - Motion and sound detection work, but must be configured on the Camera's web interface
49 | - Pan and Tilt Controls
50 | - Mirror and Flip
51 | - Reboot
52 |
53 | Foscam SD Features not yet Supported (Pending SDK Approval and Delivery from Foscam)
54 | - All Configurations from within the Add-on (Camera Settings)
55 | - Presets and Home Location
56 | - IR LED Settings
57 |
58 |
59 | v1.0.7 (Oct 24 2015)
60 | --------------------
61 | New Features:
62 | - Ability to configure behavior of calling script to show preview multiple times. Now can toggle window open and closed.
63 |
64 | v1.0.6 (Oct 24 2015)
65 | --------------------
66 | Fixed Issues:
67 | - Several small fixes to work on first time config
68 |
69 | v1.0.5 (Oct 24 2015)
70 | --------------------
71 | Fixed Issues:
72 | - Previous fix didn't work. Using try/except.
73 |
74 | v1.0.4 (Oct 24 2015)
75 | --------------------
76 | Fixed Issues:
77 | - Removing comment that was placed in error
78 |
79 | v1.0.3 (Oct 24 2015)
80 | --------------------
81 | Fixed Issues:
82 | - Further improved utils.py to check if data before executing commands
83 |
84 | v1.0.2 (Oct 24 2015)
85 | --------------------
86 | Fixed Issues:
87 | - Removed a log line in utils.py which failed if artwork has never been cached.
88 |
89 | v1.0.1 (Oct 24 2015)
90 | --------------------
91 | Fixed Issues:
92 | - Removed reference to old foscam.py in service.py
93 |
94 | v1.0.0 (Oct 24 2015) - Official Release
95 | ---------------------------------------
96 | Fixed Issues:
97 | - Camera controls don't display on generic IP cameras
98 | - Code cleaned up, performance improvements and documented code
99 | - Image cleanup doesn't remove fanart
100 | - Improved generic IP camera support
101 |
102 | New Features:
103 | - New settings menu overhaul with added features
104 | - Extended Menu with advanced features
105 | - Foscam Camera Settings window added to configure the Foscam Camera
106 | - Icons by camera type
107 | - Fanart updates using snapshot image from camera
108 | - New interlaced mjpeg mode can reduce flicker on slower computers
109 | - IR LED controls
110 | - Notifications showing add-on information added. This can be turned off in settings.
111 | - Configure preview behavior, separated by service and manually requested
112 |
113 | Known Issues:
114 | - *HIGH PRIORITY* When using MJPEG for all player or preview, its possible there might be a lag that grows overtime depending on network speed and computer speed. Work Around: Use snapshots
115 | - *MEDIUM PRIORITY* Zoom buttons are not included in remote/keyboard navigation scheme yet. Work around: Use the mouse
116 | - *MEDIUM PRIORITY* Screensaver will come on during All Camera player when its playing
117 | - *LOW PRIORITY* Trigger interval is allowed to be 0 in the code which causes a camera thread to hang. This is a bug from the camera itself which I discovered so I'm not sure if I should put a check into it. Camera value should be 5-15 but somehow my one camera became 0-10 which is not correct. I will try to reset it to factory to see if this resolves it.
118 | - *LOW PRIORITY* Z-Order of windows isn't optimized if calling from the script for multiple player types. Not a likely scenario but some 'error' logic can be added to prevent any potential issues
119 | - *LOW PRIORITY* If multiple preview windows are opened, you can only close the window by mouse which was opened last.
120 |
121 |
122 | v0.2.0 (Oct 7 2015)
123 | -------------------
124 | Fixed Issues:
125 | - If playback ends on its own, controls stay on screen until closed manually. -> Controls are now not drawn until playback has started and will close the control window through a callback
126 | - Camera perceived responsiveness to move button presses improved through a hack which plays the video starting ahead of current time. Still requires good network connection.
127 | - Fixed sleep interval time as it was halfed
128 | - Reduced flicker in all_cameras even more on MJPEG by making it interlaced. Considered a hack now, and will be included in a future setting option (as flicker was seen on AMD A6 but not i3 HTPCs)
129 | - Fixed issue where settings werent taking the MJPEG url manually due to a typo
130 | - Mjpeg -> JPEG stream used in the all camera player and previews were bound to foscam cameras for no real reason.
131 | - Several fixes related to generic IP camera support, tested on a DLink DCS-932L.
132 | .. Fixed settings to show preview settings for generic IP Cam
133 | .. Fixed issue where an array was out of bounds with 4th camera, no convesion in camera number since arrays start at 0
134 | .. Changed the MJPEG -> JPEG converter code to work with both Foscam and my D-Link. Hopefully other MJPEG Generic IP Cameras will work similarly.
135 |
136 | New Features:
137 | - Added Support for Kodi 14 (Helix)
138 | - Renamed add-on as Surveillance Room, with new icon and fanart. RunScripts will need to be updated to plugin.video.surveillanceroom
139 | - Improved Generic IP Camera Support, tested on
140 | - Ability to rename cameras and it will display, but not update the FoscamHD camera itself.
141 | - Ability to set a preset of the current location in the single camera player. If set, when the add-on service starts it will go to this preset location. Preset can be removed too.
142 | - Zoom Buttons added to single player for supported FoscamHD cameras
143 | - All Camera Move Buttons are now FoscamHD functional
144 | - Camera Flip and Mirror buttons are now FoscamHD functional
145 |
146 | Features in Progress:
147 | - New Extended Menu with added options and tests. To help in choosing configuration settings.
148 | - Moving camera API from foscam.py to foscam2.py for improved capabilities and reliability.
149 | - Adding options to settings to include: Anti-Flicker hack options; reset to preset home location on/off; ptz sensitivity; ptz speed; ability to service on/off globally
150 |
151 | Known Issues:
152 | - Trigger interval is allowed to be 0 in the code which causes a camera thread to hang. This is a bug from the camera itself which I discovered so I'm not sure if I should put a check into it. Camera value should be 5-15 but somehow my one camera became 0-10 which is not correct. I will try to reset it to factory to see if this resolves it.
153 | - Z-Order of windows isn't optimized if calling from the script for multiple player types. Not a likely scenario but some 'error' logic can be added to prevent any potential issues
154 | - If multiple preview windows are opened, you can only close the window by mouse which was opened last.
155 | **NEW** - Preset and Zoom buttons are not included in remote/keyboard navigation scheme yet
156 | **NEW** - Screensaver will come on during All Camera player when its playing
157 | **NEW** - Controls show on single camera player for generic IP Cameras when API doesn't work for them
158 |
159 |
160 |
161 | v0.1.0 (Oct 7 2015)
162 | -------------------
163 | Fixed Issues:
164 | - No loader picture for previews. -> Didn't add loader, but added Black background which also helps any potential image flicker
165 |
166 | New Features:
167 | - Camera controls will now display depending on what is supported by the camera on the single camera player (Excluding Zoom which is not yet implemented)
168 | - Ability to open camera stream/all camera player/preview RunScript() to use with key configuration or for use externally (home automation, etc)... and Future Context Menu integration capability as well!
169 | All Camera Player: RunScript(plugin.video.foscam4kodi, fullscreen, 0)
170 | Single Camera Player: RunScript(plugin.video.foscam4kodi, fullscreen, )
171 | Preview Window: RunScript(plugin.video.foscam4kodi, preview, )
172 | - Slight Overhaul of the settings menu to make more sense. It's not pretty yet but allows for customization of more features described as follows.
173 | .. Ability to choose between mainstream or *new* substream and MJPEG for single camera stream
174 | .. Ability to configure using snapshots or MJPEG stream for previews and all camera player. MJPEG is approx 10 FPS minimum, snapshots are 1FPS minimum by my tests.
175 | .. Ability to choose which windows not to show for if opened (ie home screen, system settings, content selection panes, etc) (Works but haven't performed exhaustive testing!)
176 | .. Configurable playback start time after preview is selected to play fullscreen - (Works but haven't performed exhaustive testing!)
177 | .. Configurable dismissal time (hardcoded at 15 right now) and configurable behavior (all cameras or just that one camera) -> (Works but haven't performed exhaustive testing!)
178 |
179 | Features in Progress:
180 | - Ability to rename cameras and display that while playing for better user experience. -> Added to settings but not used in main code yet.
181 |
182 | Known Issues:
183 | - Timeout of 2 seconds added to camera test connection which sometimes is flaky since the result is cached until kodi restarts or the settings change.
184 | - Trigger interval is allowed to be 0 in the code which causes a camera thread to hang. This is a bug from the camera itself which I discovered so I'm not sure if I should put a check into it. Camera value should be 5-15 but somehow my one camera became 0-10 which is not correct. I will try to reset it to factory to see if this resolves it.
185 | - Occasionally I found that a camera can 'die'. This requires a reboot of Kodi to resolve since its the Kodi window manager having an issue with a control. I think I have a way to detect it and try to programatically fix it, but if it still fails I should be able to send a notifaction dialog to reboot kodi and try again. (While still allowing it to run, albeit glitchier...erm the image in the all camera view stutters)
186 | - If playback ends on its own, controls stay on screen until closed manually.
187 | - Icons and fanart aren't correct for this plugin.
188 | **NEW** - Z-Order of windows isn't optimized if calling from the script for multiple player types. Not a likely scenario but some 'error' logic can be added to prevent any potential issues
189 | **NEW** - If multiple preview windows are opened, you can only close the window by mouse which was opened last.
190 | **NEW** - Mjpeg stream used in the all camera player and previews are bound to foscam cameras for no real reason. This needs to be corrected. Work Around: Choose snapshot from the settings
191 | **NEW** - Black background not added to all camera player
192 |
193 |
194 | v0.0.2 (Oct 2 2015)
195 | -------------------
196 | Fixed Issues:
197 | - 2 addons showing for this in the Program Addons view. -> Fixed by changing the extension point type in addon.xml from xbmc.python.script to xbmc.python.library
198 | - Single camera is slow to load. This is because it is testing the connection first. -> Fixed by changing the settings call for Level 2 instead of Level 3. Level 3 tests the connection first and Level 2 uses the cached result.
199 | - Dismissal time was not captured. -> Fixed now by making sure preview window sets the dismissed time on a close action.
200 |
201 | New Features:
202 | - Ability to open the camera to full display if selected from the preview (Select or Enter). This will stop any currently playing media. When Exited, the previously playing media will play again from where it stopped minue 10 seconds.
203 |
204 | Features in Progress:
205 | - Ability to choose which windows not to show for if opened (ie home screen, system settings, content selection panes, etc) -> Just needs settings implementation... like the 2 below.
206 | - Configurable playback start time after preview is selected to play fullscreen - Half implemented. Just need to create settings and the globalSettings() function, then add the variable to the monitor.reset() call
207 | - Configurable dismissal time (hardcoded at 15 right now) and configurable behavior (all cameras or just that one camera) -> Half implented by adding logic to the global monitor. globalSettings will need to update on monitor.reset() call.
208 |
209 | Known Issues:
210 | - Timeout of 2 seconds added to camera test connection which sometimes is flaky since the result is cached until kodi restarts or the settings change.
211 | - Trigger interval is allowed to be 0 in the code which causes a camera thread to hang. This is a bug from the camera itself which I discovered so I'm not sure if I should put a check into it. Camera value should be 5-15 but somehow my one camera became 0-10 which is not correct. I will try to reset it to factory to see if this resolves it.
212 | - Occasionally I found that a camera can 'die'. This requires a reboot of Kodi to resolve since its the Kodi window manager having an issue with a control. I think I have a way to detect it and try to programatically fix it, but if it still fails I should be able to send a notifaction dialog to reboot kodi and try again. (While still allowing it to run, albeit glitchier...erm the image in the all camera view stutters)
213 | - If playback ends on its own, controls stay on screen until closed manually.
214 | - Icons and fanart aren't correct for this new plugin name.
215 |
216 |
217 | v0.0.1 (Oct 1 2015)
218 | -------------------
219 | - Initial Alpha Test Release
220 | - Tested on F19804p and F19831w using Firmware 2.11.1.118
221 |
222 |
223 |
224 |
225 |
--------------------------------------------------------------------------------
/default.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 | A Kodi Add-on by Maikito26
4 |
5 | Main Menu and External Functionality
6 | """
7 |
8 | import sys, os, urllib
9 | import xbmc, xbmcgui, xbmcplugin, xbmcaddon, xbmcvfs
10 | from resources.lib import settings, monitor, allcameraplayer, cameraplayer, camerasettings, utils
11 | from resources.lib.ipcam_api_wrapper import CameraAPIWrapper as Camera
12 |
13 | __addon__ = xbmcaddon.Addon()
14 | __addonid__ = __addon__.getAddonInfo('id')
15 | _path = xbmc.translatePath(('special://home/addons/{0}').format(__addonid__)).decode('utf-8')
16 |
17 |
18 | def param_to_dict(parameters):
19 | """
20 | Convert parameters encoded in a URL to a dict
21 | """
22 |
23 | paramDict = {}
24 | if parameters:
25 | paramPairs = parameters[1:].split("&")
26 | for paramsPair in paramPairs:
27 | paramSplits = paramsPair.split('=')
28 | if (len(paramSplits)) == 2:
29 | paramDict[paramSplits[0]] = paramSplits[1]
30 | return paramDict
31 |
32 |
33 | def addDirectoryItem(name, url = None, isFolder = False, icon = None, fanart = None, li = None, parameters = {}):
34 | """
35 | Function which adds the directory line item into the Kodi navigation menu
36 | """
37 |
38 | if li == None:
39 | li = xbmcgui.ListItem(name)
40 |
41 | if icon != None:
42 | li.setIconImage(icon)
43 | li.setArt({'thumb': icon,
44 | 'poster': icon})
45 |
46 | if fanart != None:
47 | li.setArt({'fanart': fanart,
48 | 'landscape': fanart})
49 |
50 | li.setInfo(type = 'Video',
51 | infoLabels = {'Title': name})
52 |
53 | if url == None:
54 | url = sys.argv[0] + '?' + urllib.urlencode(parameters)
55 |
56 | return xbmcplugin.addDirectoryItem(handle = handle,
57 | url = url,
58 | listitem = li,
59 | isFolder = isFolder)
60 |
61 |
62 |
63 |
64 |
65 | def advanced_camera_menu(camera_number):
66 | """ Third Level Advanced Menu for additional IP Camera Functions """
67 |
68 | #EXTENDED MENU IDEAS
69 | #FPS Test
70 | #Force Show preview mjpeg / snapshot
71 | #Show snapshot
72 |
73 | if settings.getSetting('enabled_preview', camera_number) == 'true':
74 |
75 | #Show Preview
76 | addDirectoryItem(name = utils.translation(32210),
77 | icon = utils.get_icon('settings'),
78 | fanart = utils.get_fanart(camera_number),
79 | parameters = {'action': 'show_preview',
80 | 'camera_number': camera_number})
81 |
82 | #Disable Preview
83 | addDirectoryItem(name = utils.translation(32212),
84 | icon = utils.get_icon('settings'),
85 | fanart = utils.get_fanart(camera_number),
86 | parameters = {'action': 'disable_preview',
87 | 'camera_number': camera_number})
88 |
89 | else:
90 |
91 | #Enable Preview
92 | addDirectoryItem(name = utils.translation(32211),
93 | icon = utils.get_icon('settings'),
94 | fanart = utils.get_fanart(camera_number),
95 | parameters = {'action': 'enable_preview',
96 | 'camera_number': camera_number})
97 |
98 | if settings.getSetting_int('fanart') == 1:
99 |
100 | #Update Fanart
101 | addDirectoryItem(name = utils.translation(32213),
102 | icon = utils.get_icon('settings'),
103 | fanart = utils.get_fanart(camera_number),
104 | parameters = {'action': 'update_fanart',
105 | 'camera_number': camera_number})
106 |
107 | camera_type = settings.getCameraType(camera_number)
108 |
109 | if camera_type < 3:
110 |
111 | #Play Stream no Controls
112 | addDirectoryItem(name = utils.translation(32214),
113 | icon = utils.get_icon('settings'),
114 | fanart = utils.get_fanart(camera_number),
115 | parameters = {'action': 'single_camera_no_controls',
116 | 'camera_number': camera_number})
117 |
118 |
119 | #Camera Settings
120 | addDirectoryItem(name = utils.translation(32215),
121 | icon = utils.get_icon('settings'),
122 | fanart = utils.get_fanart(camera_number),
123 | parameters = {'action': 'camera_settings',
124 | 'camera_number': camera_number})
125 |
126 | #Reboot Camera
127 | addDirectoryItem(name = utils.translation(32216),
128 | icon = utils.get_icon('settings'),
129 | fanart = utils.get_fanart(camera_number),
130 | parameters = {'action': 'reboot',
131 | 'camera_number': camera_number})
132 |
133 |
134 | xbmcplugin.endOfDirectory(handle=handle, succeeded=True)
135 |
136 |
137 |
138 | def advanced_menu():
139 | """ Second Level Menu which provides advanced options """
140 |
141 | for camera_number in "123456":
142 |
143 | if settings.enabled_camera(camera_number):
144 |
145 | list_label = settings.getCameraName(camera_number)
146 |
147 | # List submenus for each enabled camera
148 | addDirectoryItem(name = list_label + ' ' + utils.translation(32029),
149 | isFolder = True,
150 | icon = utils.get_icon(camera_number),
151 | fanart = utils.get_fanart(camera_number),
152 | parameters = {'action': 'advanced_camera',
153 | 'camera_number': camera_number})
154 |
155 |
156 | # Toggle Preview Ability to be activated by alarms
157 | addDirectoryItem(name = utils.translation(32217),
158 | icon = utils.get_icon('settings'),
159 | fanart = utils.get_fanart('default'),
160 | parameters = {'action': 'toggle_preview'})
161 |
162 | # Add-on Settings
163 | addDirectoryItem(name = utils.translation(32028),
164 | icon = utils.get_icon('settings'),
165 | fanart = utils.get_fanart('default'),
166 | parameters = {'action': 'settings'})
167 |
168 | # Restart the preview service
169 | addDirectoryItem(name = 'Restart Preview Service',
170 | icon = utils.get_icon('settings'),
171 | fanart = utils.get_fanart('default'),
172 | parameters = {'action': 'restart_service'})
173 |
174 | xbmcplugin.endOfDirectory(handle=handle, succeeded=True)
175 |
176 |
177 |
178 | def main_menu():
179 | """ First Level Menu to access main functions """
180 |
181 | if settings.atLeastOneCamera():
182 |
183 | # All Camera Player
184 | addDirectoryItem(name = utils.translation(32027),
185 | icon = utils.get_icon('default'),
186 | fanart = utils.get_fanart('default'),
187 | parameters = {'action': 'all_cameras'})
188 |
189 | for camera_number in "123456":
190 |
191 | if settings.enabled_camera(camera_number):
192 |
193 | camera = Camera(camera_number)
194 | list_label = settings.getCameraName(camera_number)
195 |
196 | # Build Context Menu
197 | li = li = xbmcgui.ListItem(list_label)
198 | context_items = []
199 |
200 | if settings.getSetting('enabled_preview', camera_number) == 'true':
201 | #Show Preview
202 | context_items.append((utils.translation(32210), 'RunPlugin(plugin://plugin.video.surveillanceroom?action=show_preview&camera_number=%s)' %camera_number))
203 |
204 | #Disable Preview
205 | context_items.append((utils.translation(32212), 'RunPlugin(plugin://plugin.video.surveillanceroom?action=disable_preview&camera_number=%s)' %camera_number))
206 | else:
207 | #Enable Preview
208 | context_items.append((utils.translation(32211), 'RunPlugin(plugin://plugin.video.surveillanceroom?action=enable_preview&camera_number=%s)' %camera_number))
209 |
210 | camera_type = settings.getCameraType(camera_number)
211 | if camera_type < 3:
212 | #Play Stream no Controls
213 | context_items.append((utils.translation(32214), 'RunPlugin(plugin://plugin.video.surveillanceroom?action=single_camera_no_controls&camera_number=%s)' %camera_number))
214 |
215 | #Camera Settings
216 | context_items.append((utils.translation(32215), 'RunPlugin(plugin://plugin.video.surveillanceroom?action=camera_settings&camera_number=%s)' %camera_number))
217 |
218 | # Update Fanart
219 | if settings.getSetting_int('fanart') == 1:
220 | context_items.append((utils.translation(32213), 'RunPlugin(plugin://plugin.video.surveillanceroom?action=update_fanart&camera_number=%s)' %camera_number))
221 |
222 | li.addContextMenuItems(context_items, replaceItems=True)
223 |
224 | # Fanart URL
225 | new_art_url = None
226 | if camera.Connected(monitor):
227 | new_art_url = camera.getSnapShotUrl()
228 | else:
229 | if camera.Connected(monitor, False):
230 | new_art_url = camera.getSnapShotUrl()
231 |
232 | # Single Camera Player for enabled cameras
233 | addDirectoryItem(name = list_label,
234 | icon = utils.get_icon(camera_number),
235 | fanart = utils.get_fanart(camera_number, new_art_url),
236 | li = li,
237 | parameters = {'action': 'single_camera',
238 | 'camera_number': camera_number})
239 |
240 | # Link to Second Level Advanced Menu
241 | addDirectoryItem(name = utils.translation(32029),
242 | isFolder = True,
243 | icon = utils.get_icon('advanced'),
244 | fanart = utils.get_fanart('default'),
245 | parameters={'action': 'advanced'})
246 |
247 | else:
248 |
249 | # Add-on Settings if no cameras are configured
250 | addDirectoryItem(name = utils.translation(32028),
251 | icon = utils.get_icon('settings'),
252 | fanart = utils.get_fanart('default'),
253 | parameters = {'action': 'settings'})
254 |
255 | xbmcplugin.endOfDirectory(handle=handle, succeeded=True)
256 | utils.cleanup_images()
257 |
258 |
259 | if __name__ == "__main__":
260 |
261 | handle = int(sys.argv[1])
262 | params = param_to_dict(sys.argv[2])
263 | action = params.get('action', ' ')
264 | camera_number = params.get('camera_number', '')
265 | monitor = monitor.AddonMonitor()
266 | utils.log(2, 'REQUEST :: Params: %s' %params)
267 |
268 |
269 |
270 | # Main Menu
271 | if action == ' ':
272 | main_menu()
273 |
274 |
275 | # Settings
276 | elif action == 'settings':
277 | __addon__.openSettings()
278 | xbmc.executebuiltin('Container.Refresh')
279 |
280 |
281 | # Advanced Menu
282 | elif action == 'advanced':
283 | advanced_menu()
284 |
285 |
286 | # Advanced Camera Menu
287 | elif action == 'advanced_camera':
288 | advanced_camera_menu(camera_number)
289 |
290 |
291 | # All Cameras Player
292 | elif action == 'all_cameras':
293 | allcameraplayer.play()
294 |
295 |
296 | # Single Camera Stream
297 | elif action == 'single_camera':
298 | cameraplayer.play(camera_number)
299 |
300 |
301 | # Single Camera Stream without Controls
302 | elif action == 'single_camera_no_controls':
303 | cameraplayer.play(camera_number, False)
304 |
305 |
306 | # Reboot Camera
307 | elif action == 'reboot':
308 | with Camera(camera_number) as camera:
309 | response = camera.reboot()
310 | if response[0] == 0:
311 | utils.dialog_ok(utils.translation(32218))
312 | else:
313 | utils.dialog_ok(utils.translation(32219))
314 |
315 |
316 | # Camera settings
317 | elif action == 'camera_settings':
318 | window = camerasettings.CameraSettingsWindow(camera_number)
319 | window.doModal()
320 | del window
321 | utils.dialog_ok(utils.translation(32220))
322 |
323 |
324 | # Show Preview
325 | elif action == 'show_preview':
326 | if settings.enabled_preview(camera_number):
327 | if settings.getSetting_int('cond_manual_toggle', camera_number) == 1 and monitor.previewOpened(camera_number):
328 | monitor.closeRequest(camera_number)
329 | else:
330 | monitor.openRequest_manual(camera_number)
331 | else:
332 | utils.notify(utils.translation(32228))
333 |
334 |
335 | # Disable Preview
336 | elif action == 'disable_preview':
337 | settings.setSetting('enabled_preview', camera_number, 'false')
338 | xbmc.executebuiltin('Container.Refresh')
339 |
340 |
341 | # Enable Preview
342 | elif action == 'enable_preview':
343 | settings.setSetting('enabled_preview', camera_number, 'true')
344 | xbmc.executebuiltin('Container.Refresh')
345 |
346 |
347 | # Toggle All Preview
348 | elif action == 'toggle_preview':
349 | monitor.togglePreview()
350 |
351 |
352 | # Update Fanart
353 | elif action == 'update_fanart':
354 | camera = Camera(camera_number)
355 | if camera.Connected(monitor, False):
356 | utils.get_fanart(camera_number, camera.getSnapShotUrl(), update = True)
357 | xbmc.executebuiltin('Container.Refresh')
358 |
359 | else:
360 | utils.notify(utils.translation(32222))
361 |
362 |
363 | # Restart Preview Service
364 | elif action == 'restart_service':
365 | monitor.stop()
366 |
367 |
368 | # Preliminary attempt to show an overlay based on a URL, not fully tested and does not close on its own yet
369 | elif action == 'show_preview_custom':
370 | url = params.get('url', '')
371 | if url != '':
372 | monitor.overrideURL(camera_number, url)
373 | monitor.openRequest_manual(camera_number)
374 | monitor.waitForAbort(2)
375 | monitor.clear_overrideURL(camera_number)
376 |
377 |
378 |
--------------------------------------------------------------------------------
/fanart.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/fanart.jpg
--------------------------------------------------------------------------------
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/icon.png
--------------------------------------------------------------------------------
/resources/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/__init__.py
--------------------------------------------------------------------------------
/resources/language/English/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Surveillance Room
4 | Camera 1
5 | Camera 2
6 | Camera 3
7 | Camera 4
8 | Camera 5
9 | Camera 6
10 |
11 |
12 | Camera 1 Enabled
13 | Camera 2 Enabled
14 | Camera 3 Enabled
15 | Camera 4 Enabled
16 | Camera 5 Enabled
17 | Camera 6 Enabled
18 |
19 |
20 | Preview Service Enabled
21 | Control
22 | Global
23 | Update Camera Fanart
24 | Resume Time Adjustment
25 | PTZ Button Sensitivity
26 | Log Level
27 | Notifications On
28 | Hack - Attempt to improve player control response
29 | Timeout on Requests
30 | All Cameras
31 | Add-on Settings
32 | Advanced Menu
33 | Always
34 | Manually
35 | Off
36 | Normal
37 | Verbose
38 | None
39 | No Action
40 | Preview
41 | Duration
42 | Debug
43 | Dismissing affects
44 | Time Dismissed
45 | Resume Current Playing File after Fullscreen Preview
46 | Play Fullscreen from Preview
47 |
48 | All Camera Previews
49 | Dismissed Camera Preview Only
50 | All Camera Player
51 | Camera with Controls
52 | Camera without Controls
53 | Name
54 | Camera Type
55 | Host
56 | Port
57 | Username
58 | Password
59 | Source - Player
60 | Source - All Cameras
61 | Source - Preview
62 | Snapshot URL
63 | Video Stream URL
64 | MJPEG URL
65 |
66 |
67 |
68 | Foscam HD
69 | Foscam SD
70 | Foscam HD - Override (for C1)
71 | Generic IP Camera
72 |
73 |
74 | Video Stream
75 | MJPEG
76 | Snapshot
77 | MJPEG - Interlaced (Flicker Reduction)
78 |
79 |
80 |
81 |
82 |
83 | Camera Features
84 | Pan/Tilt/Zoom
85 | On Connection to Camera
86 | Alarms
87 | Pan/Tilt
88 | Go to Add-on Preset Point (if defined)
89 | Reset to Default Camera Location
90 | Motion
91 | Motion and Sound
92 |
93 | Preview Settings
94 | Activate on Motion Detection
95 | Activate on Sound Detection
96 | Alarm Check Interval (seconds)
97 | Position
98 | Scale Factor
99 | Duration - Service (seconds)
100 | Duration - Manual (seconds)
101 | Close Behavior - Service
102 | Close Behavior - Manual
103 | Bottom Right
104 | Bottom Left
105 | Top Left
106 | Top Right
107 | Duration and no Alarm is detected
108 | No Alarm is detected
109 | 'Opening Manually' Multiple Times
110 | Does Nothing
111 | Toggles Preview Open/Closed
112 |
113 | Disable Preview Service on
114 | Settings Menus
115 | Context Menu
116 | Home Screen
117 | Library Navigation
118 | System Information
119 | Virtual Keyboard
120 | Player Controls
121 | Window IDs (separated by comma)
122 | Width
123 | Height
124 |
125 | No host specified
126 | Please check your network connection and the camera host and port. You must use an administrator account.
127 | Error sending camera command
128 | Error configuring camera
129 | The following characters cannot be used in the password:
130 | The following characters cannot be used in the user name:
131 |
132 | Show Preview
133 | Enable Preview
134 | Disable Preview
135 | Update Fanart with new Snapshot
136 | Play Video Without Controls
137 | Camera Settings
138 | Reboot Camera
139 | Toggle Previews On/Off
140 | Camera was rebooted. Service might need to be manually restarted before changes take effect
141 | Camera was not rebooted. There might be a problem communicating with this device.
142 | Some changes may not take affect until the service is restarts.
143 | Fanart Saved. Add-on must be re-opened to take effect.
144 | Problem connecting to Camera %s
145 | Connection restored to Camera %s
146 | Service started.
147 | Service is restarting.
148 | Previews enabled
149 | Previews disabled
150 | Preview not enabled. Enable from add-on settings.
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/resources/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/lib/__init__.py
--------------------------------------------------------------------------------
/resources/lib/allcameraplayer.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | This module is used to show all cameras on fullscreen
7 | """
8 |
9 | import xbmc, xbmcaddon, xbmcvfs, xbmcgui
10 | import threading, os, requests#, time
11 | from urllib import urlretrieve
12 | import settings, monitor, utils
13 | from resources.lib.ipcam_api_wrapper import CameraAPIWrapper as Camera
14 | import socket
15 | TIMEOUT = settings.getSetting_int('request_timeout')
16 | socket.setdefaulttimeout(TIMEOUT)
17 |
18 | __addon__ = xbmcaddon.Addon()
19 | __addonid__ = __addon__.getAddonInfo('id')
20 |
21 | _datapath = xbmc.translatePath(('special://profile/addon_data/{0}').format(__addonid__)).decode('utf-8')
22 | _loader = xbmc.translatePath(('special://home/addons/{0}/resources/media/loader_old.gif').format(__addonid__)).decode('utf-8')
23 | _error = xbmc.translatePath(('special://home/addons/{0}/resources/media/error.png').format(__addonid__)).decode('utf-8')
24 | _holder = xbmc.translatePath(('special://home/addons/{0}/resources/media/placeholder.jpg').format(__addonid__)).decode('utf-8')
25 | _black = xbmc.translatePath('special://home/addons/%s/resources/media/black.png' %__addonid__ ).decode('utf-8')
26 |
27 | ACTION_PREVIOUS_MENU = 10
28 | ACTION_STOP = 13
29 | ACTION_NAV_BACK = 92
30 | ACTION_BACKSPACE = 110
31 |
32 | monitor = monitor.AddonMonitor()
33 |
34 | class AllCameraDisplay(xbmcgui.WindowDialog):
35 | """ Window class used to show all cameras on """
36 |
37 | def __init__(self):
38 | self.isRunning = True
39 |
40 | # Black Background
41 | background_fade_animation = [
42 | ('WindowOpen', ("effect=fade time=200")),
43 | ('WindowClose', ("effect=fade time=1500"))
44 | ]
45 |
46 | img = xbmcgui.ControlImage(0, 0, 1280, 720, filename = _black, aspectRatio = 0)
47 | self.addControl(img)
48 | img.setAnimations(background_fade_animation)
49 |
50 | # Individual Camera positions setup
51 | urls = []
52 | files = []
53 | imgs = []
54 | imgs2 = []
55 |
56 | coords = [ (0, 0, 640, 360),
57 | (640, 0, 640, 360),
58 | (0, 360, 640, 360),
59 | (640, 360, 640, 360) ]
60 |
61 | effect = ['slide', 'slide']
62 | time = [1200, 1000]
63 | tween = ['back', 'back']
64 | easing = ['Out', 'InOut']
65 | animations = [
66 | [ ('WindowOpen', ("effect={0} start=-640,-360 time={1} tween={2} easing={3}").format( effect[0], time[0], tween[0], easing[0])),
67 | ('WindowClose', ("effect={0} end=-640,-360 time={1} tween={2} easing={3}").format( effect[1], time[1], tween[1], easing[1] )) ],
68 | [ ('WindowOpen', ("effect={0} start=640,-360 time={1} tween={2} easing={3}").format( effect[0], time[0], tween[0], easing[0] )),
69 | ('WindowClose', ("effect={0} end=640,-360 time={1} tween={2} easing={3}").format( effect[1], time[1], tween[1], easing[1] )) ],
70 | [ ('WindowOpen', ("effect={0} start=-640,360 time={1} tween={2} easing={3}").format( effect[0], time[0], tween[0], easing[0] )),
71 | ('WindowClose', ("effect={0} end=-640,360 time={1} tween={2} easing={3}").format( effect[1], time[1], tween[1], easing[1] )) ],
72 | [ ('WindowOpen', ("effect={0} start=640,360 time={1} tween={2} easing={3}").format( effect[0], time[0], tween[0], easing[0] )),
73 | ('WindowClose', ("effect={0} end=640,360 time={1} tween={2} easing={3}").format( effect[1], time[1], tween[1], easing[1] )) ]
74 | ]
75 |
76 | # Acquire all Enabled & Connected cameras
77 | enabled_cameras = settings.getAllEnabledCameras(monitor)
78 |
79 | # Logic to ensure enabled cameras are placed in the correct position
80 | threads = []
81 | for window_position in '1234':
82 | position = int(window_position) - 1
83 |
84 | # Sets the initial image to the loader gif
85 | img1 = xbmcgui.ControlImage(*coords[position], filename = _loader, aspectRatio = 0)
86 | self.addControl(img1) #Bug was seen here previously, hence the 'try' and this will need to be investigated in future
87 | img1.setAnimations(animations[position])
88 |
89 | # Connected and Enabled Camera
90 | if len(enabled_cameras) > position:
91 | img2 = xbmcgui.ControlImage(*coords[position], filename = '', aspectRatio = 0)
92 | self.addControl(img2)
93 | img2.setAnimations(animations[position])
94 |
95 | control = [img1, img2]
96 |
97 | with Camera(enabled_cameras[position]) as camera:
98 | stream_type = camera.getStreamType(1)
99 | url = camera.getStreamUrl(1, stream_type)
100 | prefix = 'AllCamera'
101 |
102 | if stream_type == 0: #MJPEG
103 | t = threading.Thread(target = self.getImagesMjpeg, args = (camera, url, control, prefix))
104 |
105 | elif stream_type == 2: #MJPEG Interlaced
106 | t = threading.Thread(target = self.getImagesMjpegInterlace, args = (camera, url, control, prefix))
107 |
108 | else: #Snapshot
109 | t = threading.Thread(target = self.getImagesSnapshot, args = (camera, url, control, prefix))
110 |
111 | threads.append(t)
112 | t.start()
113 |
114 |
115 | # No Camera so set the place holder image
116 | else:
117 | img1.setImage(_holder, useCache = False)
118 |
119 | if len(threads) > 0:
120 | self.doModal()
121 |
122 | #while not monitor.abortRequested() and self.isRunning:
123 | # monitor.waitForAbort(1)
124 |
125 | monitor.maybe_resume_previous()
126 | monitor.waitForAbort(1)
127 | utils.remove_leftover_images('AllCamera')
128 |
129 | else:
130 | utils.log(2, 'Unable to start All Camera Player')
131 | utils.notify('Player did not start. Check camera settings.')
132 |
133 |
134 | def getImagesSnapshot(self, camera, url, control, prefix):
135 | """ Update camera position with snapshots """
136 |
137 | x = 0
138 | while not monitor.abortRequested() and self.isRunning:
139 |
140 | try:
141 | filename = os.path.join(_datapath, '%s_%s.%d.jpg') %(prefix, camera.number, x)
142 | urlretrieve(url, filename)
143 |
144 | if os.path.exists(filename):
145 | control[0].setImage(filename, useCache = False)
146 | xbmcvfs.delete(os.path.join(_datapath, '%s_%s.%d.jpg') %(prefix, camera.number, x - 1))
147 | control[1].setImage(filename, useCache = False)
148 | x += 1
149 |
150 | except Exception, e:
151 | utils.log(3, 'Camera %s - Error on MJPEG: %s' %(camera.number, e))
152 | control[0].setImage(_error, useCache = False)
153 | return
154 |
155 |
156 | def getImagesMjpeg(self, camera, url, control, prefix):
157 | """ Update camera position with mjpeg frames """
158 |
159 | try:
160 | stream = requests.get(url, stream = True, timeout = TIMEOUT).raw
161 |
162 | except requests.RequestException as e:
163 | utils.log(3, e)
164 | control[0].setImage(_error, useCache = False)
165 | return
166 |
167 | x = 0
168 | while not monitor.abortRequested() and self.isRunning:
169 |
170 | filename = os.path.join(_datapath, '%s_%s.%d.jpg') %(prefix, camera.number, x)
171 | filename_exists = utils.get_mjpeg_frame(stream, filename)
172 |
173 | if filename_exists:
174 | control[0].setImage(filename, useCache = False)
175 | control[1].setImage(filename, useCache = False)
176 | xbmcvfs.delete(os.path.join(_datapath, '%s_%s.%d.jpg') %(prefix, camera.number, x - 1))
177 | x += 1
178 |
179 | else:
180 | utils.log(3, 'Camera %s - Error on MJPEG' %camera.number)
181 | control[0].setImage(_error, useCache = False)
182 | return
183 |
184 |
185 | def getImagesMjpegInterlace(self, camera, url, control, prefix):
186 | """ Update camera position with interlaced mjpeg frames """
187 |
188 | try:
189 | stream = requests.get(url, stream = True, timeout = TIMEOUT).raw
190 |
191 | except requests.RequestException as e:
192 | utils.log(3, e)
193 | control[0].setImage(_error, useCache = False)
194 | return
195 |
196 | x = 0
197 | while not monitor.abortRequested() and self.isRunning:
198 |
199 | filename = os.path.join(_datapath, '%s_%s.%d.jpg') %(prefix, camera.number, x)
200 | filename_exists = utils.get_mjpeg_frame(stream, filename)
201 |
202 | if filename_exists:
203 | if x % 2 == 0: #Interlacing for flicker reduction/elimination
204 | control[0].setImage(filename, useCache = False)
205 | else:
206 | control[1].setImage(filename, useCache = False)
207 | xbmcvfs.delete(os.path.join(_datapath, '%s_%s.%d.jpg') %(prefix, camera.number, x - 2))
208 | x += 1
209 |
210 | else:
211 | utils.log(3, 'Camera %s - Error on MJPEG' %camera.number)
212 | control[0].setImage(_error, useCache = False)
213 | return
214 |
215 |
216 | def onAction(self, action):
217 | if action in (ACTION_PREVIOUS_MENU, ACTION_STOP, ACTION_NAV_BACK, ACTION_BACKSPACE):
218 | self.isRunning = False
219 | monitor.waitForAbort(.2)
220 | self.close()
221 |
222 |
223 | def play():
224 | """ Main function to show all cameras """
225 |
226 | if settings.atLeastOneCamera():
227 | monitor.set_playingCamera('0')
228 | PlayerWindow = AllCameraDisplay()
229 | del PlayerWindow
230 | monitor.clear_playingCamera('0')
231 |
232 | else:
233 | utils.log(2, 'No Cameras Configured')
234 | utils.notify('You must configure a camera first')
235 |
236 |
237 |
238 |
239 | if __name__ == "__main__":
240 | play()
241 |
--------------------------------------------------------------------------------
/resources/lib/cameraplayer.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | Module which controls how a single IP camera plays fullscreen
7 | """
8 |
9 | import xbmc, xbmcgui, xbmcaddon
10 | import os
11 | import settings, utils, camerasettings, monitor
12 | from resources.lib.ipcam_api_wrapper import CameraAPIWrapper as Camera
13 | from resources.lib.ipcam_api_wrapper import GENERIC_IPCAM, FOSCAM_SD
14 | import socket
15 | socket.setdefaulttimeout(settings.getSetting_int('request_timeout'))
16 |
17 | __addon__ = xbmcaddon.Addon()
18 | __addonid__ = __addon__.getAddonInfo('id')
19 | __path__ = __addon__.getAddonInfo('path')
20 |
21 | _btnimage = xbmc.translatePath('special://home/addons/%s/resources/media/{0}.png' %__addonid__ ).decode('utf-8')
22 |
23 | monitor = monitor.AddonMonitor()
24 |
25 | # Kodi key action codes.
26 | ACTION_PREVIOUS_MENU = 10
27 | ACTION_NAV_BACK = 92
28 | ACTION_BACKSPACE = 110
29 | ACTION_STOP = 13
30 | ACTION_SELECT_ITEM = 7
31 |
32 |
33 | class Button(xbmcgui.ControlButton):
34 | """ Class reclasses the ControlButton class for use in this addon. """
35 |
36 | WIDTH = HEIGHT = 32
37 |
38 | def __new__(cls, parent, action, x, y, scaling = 1.0):
39 | focusTexture = _btnimage.format(action)
40 | noFocusTexture = _btnimage.format(action+ '_nofocus')
41 | width = int(round(cls.WIDTH * scaling))
42 | height = int(round(cls.HEIGHT * scaling))
43 |
44 | self = super(Button, cls).__new__(cls, x, y, width, height, '',
45 | focusTexture, noFocusTexture)
46 | parent.buttons.append(self)
47 | return self
48 |
49 |
50 | class ToggleButton(xbmcgui.ControlRadioButton):
51 | """ Class reclasses the RadioButton class for use in this addon. """
52 |
53 | WIDTH = 110
54 | HEIGHT = 40
55 |
56 | def __new__(cls, parent, action, x, y):
57 | focusOnTexture = _btnimage.format('radio-on')
58 | noFocusOnTexture = _btnimage.format('radio-on')
59 | focusOffTexture = _btnimage.format('radio-off')
60 | noFocusOffTexture = _btnimage.format('radio-off')
61 | focusTexture = _btnimage.format('back')
62 | noFocusTexture = _btnimage.format('trans')
63 | textOffsetX = 12
64 |
65 | self = super(ToggleButton, cls).__new__(cls, x, y, cls.WIDTH, cls.HEIGHT, action.title(),
66 | focusOnTexture, noFocusOnTexture,
67 | focusOffTexture, noFocusOffTexture,
68 | focusTexture, noFocusTexture,
69 | textOffsetX)
70 |
71 | self.action = action
72 | parent.buttons.append(self)
73 | return self
74 |
75 |
76 |
77 | class CameraControlsWindow(xbmcgui.WindowDialog):
78 | """
79 | Class is used to create a single camera playback window of the camera view with controls
80 | """
81 |
82 | def __init__(self, camera, monitor):
83 | self.camera = camera
84 | self.monitor = monitor
85 |
86 | def start(self):
87 | url = self.camera.getStreamUrl(0)
88 | listitem = xbmcgui.ListItem()
89 |
90 | #Hack to improve perceived responsiveness of Stream and Button Presets
91 | hack_enabled = settings.getSetting_bool('hack1')
92 | if hack_enabled:
93 | utils.log(2, 'Hack enabled to better sync playback and control feedback')
94 | listitem.setProperty('StartOffset', '20')
95 |
96 | utils.log(1, 'Camera %s :: *** Playing Fullscreen with Controls *** URL: %s' %(self.camera.number, url))
97 | self.monitor.set_playingCamera(self.camera.number)
98 | self.player = KODIPlayer(**{'callback1': self.setupUi, 'callback2': self.stop })
99 | self.player.play(url, listitem)
100 |
101 | self.doModal() # Anything after self.stop() will be acted upon here
102 |
103 | self.monitor.clear_playingCamera(self.camera.number)
104 | self.monitor.maybe_resume_previous()
105 |
106 |
107 | def setupUi(self):
108 | response_code, response = self.camera.get_mirror_and_flip_setting()
109 | if response_code == 0:
110 |
111 | # Button Placement settings
112 | Y_OFFSET = 100
113 | X_OFFSET = 20
114 | OFFSET1 = 32
115 | OFFSET2 = 64
116 |
117 | self.buttons = []
118 |
119 | # Default Foscam Buttons
120 | self.flip_button = ToggleButton(self, 'flip', 30, Y_OFFSET+200)
121 | self.mirror_button = ToggleButton(self, 'mirror', 30, Y_OFFSET+260)
122 | self.close_button = Button(self, 'close', 1280-60, 20)
123 | self.addon_settings_button = Button(self, 'addon_settings', 1280-120, 20)
124 | self.camera_settings_button = Button(self, 'camera_settings', 1280-180, 20)
125 |
126 | self.addControl(self.addon_settings_button)
127 | self.addControl(self.camera_settings_button)
128 | self.addControl(self.close_button)
129 | self.addControl(self.flip_button)
130 | self.addControl(self.mirror_button)
131 |
132 | self.mirror_button.setSelected(int(response['isMirror']))
133 | self.flip_button.setSelected(int(response['isFlip']))
134 |
135 | self.flip_button.setNavigation(self.camera_settings_button, self.mirror_button, self.close_button, self.camera_settings_button)
136 | self.mirror_button.setNavigation(self.flip_button, self.close_button, self.close_button, self.camera_settings_button)
137 | self.addon_settings_button.setNavigation(self.mirror_button, self.flip_button, self.camera_settings_button, self.close_button)
138 | self.camera_settings_button.setNavigation(self.mirror_button, self.flip_button, self.flip_button, self.addon_settings_button)
139 | self.close_button.setNavigation (self.mirror_button, self.flip_button, self.addon_settings_button, self.flip_button)
140 |
141 | # PTZ Buttons
142 | ptz = settings.getSetting_int('ptz', self.camera.number)
143 |
144 | self.pan_tilt = False
145 | if ptz > 0:
146 | self.pan_tilt = True
147 | self.sensitivity = self.camera.ptz_sensitivity
148 |
149 | self.zoom = False
150 | if ptz > 1:
151 | self.zoom = True
152 |
153 | if self.pan_tilt:
154 |
155 | self.up_button = Button(self, 'up', OFFSET1+X_OFFSET, Y_OFFSET)
156 | self.left_button = Button(self, 'left', X_OFFSET, OFFSET1+Y_OFFSET)
157 | self.down_button = Button(self, 'down', OFFSET1+X_OFFSET, OFFSET2+Y_OFFSET)
158 | self.right_button = Button(self, 'right', OFFSET2+X_OFFSET, OFFSET1+Y_OFFSET)
159 | self.top_left_button = Button(self, 'top_left', X_OFFSET, Y_OFFSET)
160 | self.top_right_button = Button(self, 'top_right', OFFSET2+X_OFFSET, Y_OFFSET)
161 | self.bottom_right_button = Button(self, 'bottom_right', OFFSET2+X_OFFSET, OFFSET2+Y_OFFSET)
162 | self.bottom_left_button = Button(self, 'bottom_left', X_OFFSET, OFFSET2+Y_OFFSET)
163 | self.home_button = Button(self, 'home', OFFSET1+X_OFFSET, OFFSET1+Y_OFFSET)
164 | self.preset_button = ToggleButton(self, 'preset', 30, Y_OFFSET+320)
165 |
166 | self.addControl(self.up_button)
167 | self.addControl(self.left_button)
168 | self.addControl(self.down_button)
169 | self.addControl(self.right_button)
170 | self.addControl(self.top_left_button)
171 | self.addControl(self.top_right_button)
172 | self.addControl(self.bottom_right_button)
173 | self.addControl(self.bottom_left_button)
174 | self.addControl(self.home_button)
175 | self.addControl(self.preset_button)
176 |
177 | self.flip_button.setNavigation(self.down_button, self.mirror_button, self.close_button, self.camera_settings_button)
178 | self.mirror_button.setNavigation(self.flip_button, self.preset_button, self.close_button, self.camera_settings_button)
179 | self.preset_button.setNavigation(self.mirror_button, self.up_button, self.close_button, self.camera_settings_button)
180 | self.addon_settings_button.setNavigation(self.preset_button, self.preset_button, self.camera_settings_button, self.close_button)
181 | self.camera_settings_button.setNavigation(self.preset_button, self.preset_button, self.right_button, self.addon_settings_button)
182 | self.close_button.setNavigation(self.preset_button, self.preset_button, self.addon_settings_button, self.left_button)
183 | self.up_button.setNavigation(self.preset_button, self.home_button, self.top_left_button, self.top_right_button)
184 | self.left_button.setNavigation(self.top_left_button, self.bottom_left_button, self.close_button, self.home_button)
185 | self.right_button.setNavigation(self.top_right_button, self.bottom_right_button, self.home_button, self.camera_settings_button)
186 | self.down_button.setNavigation(self.home_button, self.flip_button, self.bottom_left_button, self.bottom_right_button)
187 | self.top_left_button.setNavigation(self.preset_button, self.left_button, self.close_button, self.up_button)
188 | self.top_right_button.setNavigation(self.preset_button, self.right_button, self.up_button, self.camera_settings_button)
189 | self.bottom_right_button.setNavigation(self.right_button, self.flip_button, self.down_button, self.camera_settings_button)
190 | self.bottom_left_button.setNavigation(self.left_button, self.flip_button, self.close_button, self.down_button)
191 | self.home_button.setNavigation(self.up_button, self.down_button, self.left_button, self.right_button)
192 |
193 | # Work Around until Full API is implemented
194 | if not self.camera._type == FOSCAM_SD:
195 | home_location = self.camera.ptz_home_location(0)
196 | self.preset_button.setSelected(home_location)
197 |
198 | if self.zoom:
199 | self.zoom_in_button = Button(self, 'zoom_in', OFFSET2+X_OFFSET+32, Y_OFFSET)
200 | self.zoom_out_button = Button(self, 'zoom_out', OFFSET2+X_OFFSET+32, OFFSET2+Y_OFFSET)
201 | self.addControl(self.zoom_in_button)
202 | self.addControl(self.zoom_out_button)
203 |
204 | # Navigation still requires to be set #
205 |
206 | # Work Around until Full API is implemented
207 | if self.camera._type == FOSCAM_SD:
208 | self.preset_button.setEnabled(False)
209 | self.camera_settings_button.setEnabled(False)
210 | self.home_button.setEnabled(False)
211 |
212 |
213 | self.setFocus(self.close_button)
214 | self.setFocus(self.close_button) #Set twice as sometimes it doesnt set?
215 |
216 | def getControl(self, control):
217 | return next(button for button in self.buttons if button == control)
218 |
219 | def onControl(self, control):
220 | if control == self.close_button: self.stop()
221 | elif control == self.flip_button: self.camera.flip_video()
222 | elif control == self.mirror_button: self.camera.mirror_video()
223 | elif control == self.addon_settings_button: __addon__.openSettings()
224 | elif control == self.camera_settings_button: self.open_camera_settings()
225 |
226 | elif self.pan_tilt:
227 | if control == self.home_button: home_location = self.camera.ptz_home_location(2)
228 | elif control == self.preset_button: self.toggle_preset()
229 | else:
230 | if control == self.up_button: self.camera.ptz_move_up()
231 | elif control == self.down_button: self.camera.ptz_move_down()
232 | elif control == self.left_button: self.camera.ptz_move_left()
233 | elif control == self.right_button: self.camera.ptz_move_right()
234 | elif control == self.top_left_button: self.camera.ptz_move_top_left()
235 | elif control == self.top_right_button: self.camera.ptz_move_top_right()
236 | elif control == self.bottom_left_button: self.camera.ptz_move_bottom_left()
237 | elif control == self.bottom_right_button: self.camera.ptz_move_bottom_right()
238 |
239 | monitor.waitForAbort(self.sensitivity) #Move Button Sensitivity
240 | self.camera.ptz_stop_run()
241 |
242 | elif self.zoom:
243 | if control == self.zoom_in_button: self.camera.ptz_zoom_in()
244 | elif control == self.zoom_out_button: self.camera.ptz_zoom_out()
245 |
246 | self.monitor.waitForAbort(self.sensitivity) #Move Button Sensitivity
247 | self.camera.ptz_zoom_stop()
248 |
249 | def open_camera_settings(self):
250 | settings_window = camerasettings.CameraSettingsWindow(self.camera.number)
251 | settings_window.doModal()
252 | del settings_window
253 | utils.notify('Some changes may not take affect until the service is restarts.')
254 |
255 | def toggle_preset(self):
256 | if self.preset_button.isSelected():
257 | self.camera.ptz_add_preset()
258 | utils.notify('Home Location is now Current Location')
259 | else:
260 | self.camera.ptz_delete_preset()
261 | utils.notify('Home Location is now Default Location')
262 |
263 | def onAction(self, action):
264 | if action in (ACTION_PREVIOUS_MENU,
265 | ACTION_BACKSPACE,
266 | ACTION_NAV_BACK,
267 | ACTION_STOP):
268 | self.stop()
269 |
270 | def stop(self):
271 | self.player.stop()
272 | #xbmc.executebuiltin('PlayerControl(Stop)') # Because player.stop() was losing the player and didn't work *sad face*
273 | self.close()
274 |
275 |
276 |
277 |
278 | class KODIPlayer(xbmc.Player):
279 | """
280 | Kodi Video Player reclassed to include added functionality.
281 | Allows stopping the currently playing video to view a preview in fullscreen
282 | and then resume the original playing video.
283 | """
284 |
285 | def __init__(self, **kwargs):
286 | super(KODIPlayer, self).__init__()
287 | self.SetupUIcallback = kwargs.get('callback1', None)
288 | self.StopCallback = kwargs.get('callback2', None)
289 |
290 | def onPlayBackStarted(self):
291 | self.SetupUIcallback() #SetupUi() - for camera controls, waits until it is playing to draw controls for User Experience
292 |
293 | def onPlayBackEnded(self):
294 | self.StopCallback() #stop() - for the player controls window
295 |
296 | def onPlayBackStopped(self):
297 | self.StopCallback() #stop() - for the player controls window
298 |
299 |
300 |
301 |
302 | def play(camera_number, show_controls = None):
303 | """
304 | Function to call to play the IP Camera feed. Determines if controls are shown or not.
305 | """
306 |
307 | camera = Camera(camera_number)
308 |
309 | if camera.Connected(monitor):
310 |
311 | if show_controls == None:
312 | show_controls = False # Generic IP Cameras default without Controls
313 | if camera._type != GENERIC_IPCAM: # Foscam Cameras default with Controls
314 | show_controls = True
315 |
316 | if show_controls:
317 | player = CameraControlsWindow(camera, monitor)
318 | player.start()
319 |
320 | else:
321 | url = camera.getStreamUrl(0)
322 | name = settings.getCameraName(camera.number)
323 | utils.log(2, 'Camera %s :: Name: %s; Url: %s' %(camera.number, name, url))
324 |
325 | listitem = xbmcgui.ListItem()
326 | listitem.setInfo(type = 'Video', infoLabels = {'Title': name})
327 | listitem.setArt({'thumb': utils.get_icon(camera.number)})
328 |
329 | utils.log(1, 'Camera %s :: *** Playing Fullscreen *** URL: %s' %(camera.number, url))
330 | player = xbmc.Player()
331 | player.play(url, listitem)
332 |
333 | if monitor.resume_previous_file():
334 | while not player.isPlaying() and not monitor.stopped() and not monitor.abortRequested():
335 | monitor.waitForAbort(.5)
336 | while player.isPlaying() and not monitor.stopped() and not monitor.abortRequested():
337 | monitor.waitForAbort(.5)
338 | monitor.maybe_resume_previous()
339 | else:
340 |
341 | utils.log(3, 'Camera %s :: Camera is not configured correctly' %camera.number)
342 | utils.notify('Camera %s not configured correctly' %camera.number)
343 |
344 |
345 | if __name__ == "__main__":
346 | pass
347 |
348 |
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
--------------------------------------------------------------------------------
/resources/lib/camerapreview.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | This module is used to draw and show the preview window
7 | """
8 |
9 | import xbmc, xbmcaddon, xbmcgui, xbmcvfs
10 | import os, requests, time
11 | import utils, settings, cameraplayer, allcameraplayer, monitor
12 | from urllib import urlretrieve
13 | import threading
14 | import socket
15 | TIMEOUT = settings.getSetting_int('request_timeout')
16 | socket.setdefaulttimeout(TIMEOUT)
17 |
18 | __addon__ = xbmcaddon.Addon()
19 | __addonid__ = __addon__.getAddonInfo('id')
20 |
21 | _black = xbmc.translatePath('special://home/addons/%s/resources/media/black.png' %__addonid__ ).decode('utf-8')
22 | _btnimage = xbmc.translatePath('special://home/addons/%s/resources/media/{0}.png' %__addonid__ ).decode('utf-8')
23 | _error = xbmc.translatePath('special://home/addons/%s/resources/media/error.png' %__addonid__ ).decode('utf-8')
24 | _datapath = xbmc.translatePath('special://profile/addon_data/%s' %__addonid__ ).decode('utf-8')
25 |
26 | ACTION_PREVIOUS_MENU = 10
27 | ACTION_BACKSPACE = 110
28 | ACTION_NAV_BACK = 92
29 | ACTION_STOP = 13
30 | ACTION_SELECT_ITEM = 7
31 |
32 | #Close Conditions
33 | CONDITION_DURATION_NO_ALARM = 0
34 | CONDITION_DURATION = 1
35 | CONDITION_MANUAL = 2
36 | CONDITION_NO_ALARM = 3
37 |
38 | #Fullscreen Player types
39 | CAMERAWITHOUTCONTROLS = 0
40 | CAMERAWITHCONTROLS = 1
41 | ALLCAMERAPLAYER = 2
42 |
43 | class Button(xbmcgui.ControlButton):
44 | """ Class reclasses the ControlButton class for use in this addon. """
45 |
46 | WIDTH = HEIGHT = 32
47 |
48 | def __new__(cls, parent, action, x, y, camera = None, scaling = 1.0):
49 | focusTexture = _btnimage.format(action)
50 | noFocusTexture = _btnimage.format(action+ '_nofocus')
51 | width = int(round(cls.WIDTH * scaling))
52 | height = int(round(cls.HEIGHT * scaling))
53 |
54 | self = super(Button, cls).__new__(cls, x, y, width, height, '',
55 | focusTexture, noFocusTexture)
56 | parent.buttons.append(self)
57 | return self
58 |
59 | class CameraPreviewWindow(xbmcgui.WindowDialog):
60 | """ Class is used to create the picture-in-picture window of the camera view """
61 |
62 | def __init__(self, camera, monitor):
63 | self.camera = camera
64 | self.monitor = monitor
65 | self.cond_service = settings.getSetting_int('cond_service', self.camera.number)
66 | self.cond_manual = settings.getSetting_int('cond_manual', self.camera.number)
67 | self.dur_service = settings.getSetting_int('dur_service', self.camera.number)
68 | self.dur_manual = settings.getSetting_int('dur_manual', self.camera.number)
69 | self.prefix = 'Preview'
70 | self.buttons = []
71 |
72 | # Positioning of the window
73 | WIDTH = settings.getSetting_int('width', self.camera.number)
74 | HEIGHT = settings.getSetting_int('height', self.camera.number)
75 |
76 | scaling = settings.getSetting_float('scaling', self.camera.number)
77 |
78 | width = int(float(WIDTH * scaling))
79 | height = int(float(HEIGHT * scaling))
80 |
81 | button_scaling = 0.5 * scaling
82 | button_width = int(round(Button.WIDTH * button_scaling))
83 |
84 | position = settings.getSetting('position', self.camera.number).lower()
85 | if 'bottom' in position:
86 | y = 720 - height
87 | else:
88 | y = 0
89 |
90 | if 'left' in position:
91 | x = 0
92 | start = - width
93 | else:
94 | x = 1280 - width
95 | start = width
96 |
97 | animations = [('WindowOpen', ("effect=slide start={0:d} time=1300 tween=cubic easing=out").format(start)),
98 | ('WindowClose', ("effect=slide end={0:d} time=900 tween=back easing=inout").format(start))]
99 |
100 | self.black = xbmcgui.ControlImage(x, y, width, height, _black)
101 | self.addControl(self.black)
102 | self.black.setAnimations(animations)
103 |
104 | self.img1 = xbmcgui.ControlImage(x, y, width, height, '')
105 | self.img2 = xbmcgui.ControlImage(x, y, width, height, '')
106 | self.addControl(self.img1)
107 | self.addControl(self.img2)
108 | self.img1.setAnimations(animations)
109 | self.img2.setAnimations(animations)
110 |
111 | self.close_button = Button(self, 'close', x + width - button_width - 10, y + 10, scaling=button_scaling)
112 | self.addControl(self.close_button)
113 | self.close_button.setAnimations(animations)
114 |
115 | self.setProperty('zorder', "99")
116 |
117 | self.playFullscreen = False
118 | self.stop() #Initializes state and makes ready to be used
119 |
120 | def start(self):
121 | url = self.monitor.get_overrideURL(self.camera.number) #Request to test by @mrjd in forum
122 | stream_type = self.camera.getStreamType(2)
123 |
124 | if url == '':
125 | url = self.camera.getStreamUrl(2, stream_type)
126 |
127 | utils.log(2, 'Camera %s :: Preview Window Opened - Manual: %s; Stream Type: %d; URL: %s' %(self.camera.number, self.monitor.openRequest_manual(self.camera.number), stream_type, url))
128 |
129 | if stream_type == 0:
130 | t = threading.Thread(target = self.getImagesMjpeg, args = (url,))
131 | elif stream_type == 1:
132 | t = threading.Thread(target = self.getImagesSnapshot, args = (url,))
133 | elif stream_type == 2:
134 | t = threading.Thread(target = self.getImagesMjpegInterlace, args = (url,))
135 |
136 | t.daemon = True
137 |
138 | self.monitor.openPreview(self.camera.number)
139 | t.start()
140 | self.show()
141 | self.wait_closeRequest()
142 |
143 |
144 | def stop(self, playFullscreen = False):
145 | self.monitor.closePreview(self.camera.number)
146 | self.close()
147 | utils.log(2, 'Camera %s :: Preview Window Closed' %self.camera.number)
148 |
149 | if not self.monitor.abortRequested() and not self.monitor.stopped():
150 |
151 | if playFullscreen:
152 | self.monitor.maybe_stop_current()
153 | fullscreenplayer = settings.getSetting_int('fullscreen_player')
154 |
155 | if fullscreenplayer == CAMERAWITHCONTROLS:
156 | cameraplayer.play(self.camera.number)
157 |
158 | elif fullscreenplayer == CAMERAWITHOUTCONTROLS:
159 | cameraplayer.play(self.camera.number, show_controls = False)
160 |
161 | elif fullscreenplayer == ALLCAMERAPLAYER:
162 | allcameraplayer.play()
163 |
164 | self.wait_openRequest()
165 |
166 |
167 | def wait_openRequest(self):
168 | while not self.monitor.abortRequested() and not self.monitor.stopped() and not self.monitor.openRequested(self.camera.number):
169 | self.monitor.waitForAbort(.5)
170 |
171 | if not self.monitor.abortRequested() and not self.monitor.stopped():
172 | self.openRequest_manual = self.monitor.openRequested_manual(self.camera.number)
173 | self.start()
174 |
175 |
176 | def wait_closeRequest(self):
177 | duration = 0 # Duration is 0 if Close Condition is Manual or Alarm only, otherwise set based on source
178 |
179 | if not self.monitor.openRequested_manual(self.camera.number):
180 | if self.cond_service == CONDITION_DURATION_NO_ALARM or self.cond_service == CONDITION_DURATION:
181 | duration = self.dur_service
182 | else:
183 | if self.cond_manual == CONDITION_DURATION_NO_ALARM or self.cond_manual == CONDITION_DURATION:
184 | duration = self.dur_manual
185 |
186 | openDuration = time.time() + duration
187 |
188 | #print 'Wait for close - duration: %d; openDuration: %d' %(duration, openDuration)
189 |
190 | # Loop Condition Checking
191 | while not self.monitor.abortRequested() and not self.monitor.stopped() and self.monitor.previewOpened(self.camera.number) and not self.monitor.closeRequested(self.camera.number):
192 | if ((self.cond_service == CONDITION_DURATION_NO_ALARM and not self.monitor.openRequested_manual(self.camera.number)) or \
193 | (self.cond_manual == CONDITION_DURATION_NO_ALARM and self.monitor.openRequested_manual(self.camera.number))) \
194 | and self.monitor.alarmActive(self.camera.number):
195 |
196 | openDuration = time.time() + duration
197 | #print '%s, %s, %s, %s' %(self.cond_service, self.cond_manual, self.monitor.openRequested_manual(self.camera.number), self.monitor.alarmActive(self.camera.number))
198 | #print 'Wait for close Loop - duration: %d; openDuration: %d; time: %d' %(duration, openDuration, time.time())
199 |
200 | # Duration Check if Close Condition is not Manual or Alarm only
201 | if (duration > 0 and time.time() > openDuration):
202 | self.monitor.closeRequest(self.camera.number)
203 |
204 | # Check if close Request
205 | if self.monitor.closeRequested(self.camera.number):
206 | break
207 |
208 | self.monitor.waitForAbort(.5)
209 |
210 | self.stop()
211 |
212 |
213 | def onControl(self, control):
214 | if control == self.close_button:
215 | utils.log(2, 'Camera %s :: Closing Preview Manually - Mouse Request' %self.camera.number)
216 | self.monitor.dismissPreview(self.camera.number)
217 | self.stop()
218 |
219 | def onAction(self, action):
220 | if action in (ACTION_PREVIOUS_MENU, ACTION_BACKSPACE, ACTION_NAV_BACK):
221 | utils.log(2, 'Camera %s :: Closing Preview Manually - Keyboard Request' %self.camera.number)
222 | self.monitor.dismissPreview(self.camera.number)
223 | self.stop()
224 |
225 | elif action == ACTION_SELECT_ITEM:
226 | utils.log(2, 'Camera %s :: Playing Fullscreen from Preview.' %self.camera.number)
227 | self.monitor.dismissPreview(self.camera.number)
228 | self.stop(playFullscreen = True)
229 |
230 |
231 |
232 |
233 | def getImagesMjpeg(self, url):
234 | """ Update camera position with mjpeg frames """
235 |
236 | try:
237 | stream = requests.get(url, stream = True, timeout = TIMEOUT).raw
238 |
239 | except requests.RequestException as e:
240 | utils.log(3, e)
241 | self.img1.setImage(_error, useCache = False)
242 | return
243 |
244 | x = 0
245 | while not self.monitor.abortRequested() and not self.monitor.stopped() and self.monitor.previewOpened(self.camera.number):
246 | filename = os.path.join(_datapath, '%s_%s.%d.jpg') %(self.prefix, self.camera.number, x)
247 | filename_exists = utils.get_mjpeg_frame(stream, filename)
248 |
249 | if filename_exists:
250 | self.img1.setImage(filename, useCache = False)
251 | self.img2.setImage(filename, useCache = False)
252 | xbmcvfs.delete(os.path.join(_datapath, '%s_%s.%d.jpg') %(self.prefix, self.camera.number, x - 1))
253 | x += 1
254 |
255 | else:
256 | utils.log(3, 'Camera %s :: Error updating preview image on MJPEG' %self.camera.number)
257 | self.img1.setImage(_error, useCache = False)
258 | break
259 |
260 | if not not self.monitor.abortRequested() and not self.monitor.stopped():
261 | utils.remove_leftover_images('%s_%s.' %(self.prefix, self.camera.number))
262 |
263 |
264 |
265 |
266 | def getImagesSnapshot(self, url, *args, **kwargs):
267 | """ Update camera position with snapshots """
268 |
269 | x = 0
270 | while not self.monitor.abortRequested() and not self.monitor.stopped() and self.monitor.previewOpened(self.camera.number):
271 |
272 | try:
273 | filename = os.path.join(_datapath, '%s_%s.%d.jpg') %(self.prefix, self.camera.number, x)
274 | urlretrieve(url, filename)
275 |
276 | if os.path.exists(filename):
277 | self.img1.setImage(filename, useCache = False)
278 | xbmcvfs.delete(os.path.join(_datapath, '%s_%s.%d.jpg') %(self.prefix, self.camera.number, x - 1))
279 | self.img2.setImage(filename, useCache = False)
280 | x += 1
281 |
282 | except Exception, e:
283 | utils.log(3, 'Camera %s :: Error updating preview image on Snapshot: %s' %(self.camera.number, e))
284 | self.img1.setImage(_error, useCache = False)
285 | break
286 |
287 | if not not self.monitor.abortRequested() and not self.monitor.stopped():
288 | utils.remove_leftover_images('%s_%s.' %(self.prefix, self.camera.number))
289 |
290 |
291 |
292 |
293 | def getImagesMjpegInterlace(self, url, *args, **kwargs):
294 | """ Update camera position with interlaced mjpeg frames """
295 |
296 | try:
297 | stream = requests.get(url, stream = True, timeout = TIMEOUT).raw
298 |
299 | except requests.RequestException as e:
300 | utils.log(3, e)
301 | self.img1.setImage(_error, useCache = False)
302 | return
303 |
304 | x = 0
305 | while not self.monitor.abortRequested() and not self.monitor.stopped() and self.monitor.previewOpened(self.camera.number):
306 |
307 | filename = os.path.join(_datapath, '%s_%s.%d.jpg') %(self.prefix, self.camera.number, x)
308 | filename_exists = utils.get_mjpeg_frame(stream, filename)
309 |
310 | if filename_exists:
311 | if x % 2 == 0: #Interlacing for flicker reduction/elimination
312 | self.img1.setImage(filename, useCache = False)
313 | else:
314 | self.img2.setImage(filename, useCache = False)
315 | xbmcvfs.delete(os.path.join(_datapath, '%s_%s.%d.jpg') %(self.prefix, self.camera.number, x - 2))
316 | x += 1
317 |
318 | else:
319 | utils.log(3, 'Camera %s :: Error updating preview image on MJPEG' %self.camera.number)
320 | self.img1.setImage(_error, useCache = False)
321 | break
322 |
323 | if not not self.monitor.abortRequested() and not self.monitor.stopped():
324 | utils.remove_leftover_images('%s_%s.' %(self.prefix, self.camera.number))
325 |
326 |
327 | if __name__ == "__main__":
328 | pass
329 |
330 |
--------------------------------------------------------------------------------
/resources/lib/ipcam_api_foscamsd.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | This module is to exploit Foscam IP Cameras, ie models starting with F189xx.
7 |
8 | Refernced API from:
9 | http://foscam.devcenter.me/
10 | """
11 |
12 | import urllib
13 | from threading import Thread
14 | import utils, settings
15 | import socket
16 | socket.setdefaulttimeout(settings.getSetting_int('request_timeout'))
17 |
18 | # Foscam error code.
19 | FOSCAM_SUCCESS = 0
20 | ERROR_FOSCAM_FORMAT = -1
21 | ERROR_FOSCAM_AUTH = -2
22 | ERROR_FOSCAM_CMD = -3 # Access deny. May the cmd is not supported.
23 | ERROR_FOSCAM_EXE = -4 # CGI execute fail.
24 | ERROR_FOSCAM_TIMEOUT = -5
25 | ERROR_FOSCAM_UNKNOWN = -7 # -6 and -8 are reserved.
26 | ERROR_FOSCAM_UNAVAILABLE = -8 # Disconnected or not a cam.
27 |
28 | class FoscamError(Exception):
29 | def __init__(self, code):
30 | super(FoscamError, self).__init__()
31 | self.code = int(code)
32 |
33 | def __str__(self):
34 | return 'ErrorCode: %s' % self.code
35 |
36 | class FoscamCamera(object):
37 | '''A python implementation of the foscam SD'''
38 |
39 | def __init__(self, camera_settings, daemon = False, verbose = True):
40 | '''
41 | If ``daemon`` is True, the command will be sent unblockedly.
42 | '''
43 | self.number = camera_settings[0]
44 | self.host = camera_settings[1]
45 | self.port = camera_settings[2]
46 | self.usr = camera_settings[3]
47 | self.pwd = camera_settings[4]
48 | self.daemon = daemon
49 | self.verbose = verbose
50 |
51 | @property
52 | def url(self):
53 | _url = '%s:%s' % (self.host, self.port)
54 | return _url
55 |
56 | @property
57 | def video_url(self):
58 | _videoUrl = "http://{0}/videostream.asf?user={1}&pwd={2}&resolution=32&rate=0".format(self.url, self.usr, self.pwd)
59 | return _videoUrl
60 |
61 | @property
62 | def mjpeg_url(self):
63 | _mjpegUrl = "http://{0}/videostream.cgi?user={1}&pwd={2}&resolution=32&rate=0".format(self.url, self.usr, self.pwd)
64 | return _mjpegUrl #MJPEG stream is VGA resolution @ 15fps
65 |
66 | @property
67 | def snapshot_url(self):
68 | _snapshotUrl = "http://{0}/snapshot.cgi?user={1}&pwd={2}".format(self.url, self.usr, self.pwd)
69 | return _snapshotUrl
70 |
71 | def __enter__(self):
72 | return self
73 |
74 | def __exit__(self, exc_type, exc_value, traceback):
75 | return None
76 |
77 | def send_command(self, cmd, params = None):
78 | '''
79 | Send command to foscam.
80 | '''
81 | paramstr = ''
82 | if params:
83 | paramstr = urllib.urlencode(params)
84 | paramstr = '&' + paramstr if paramstr else ''
85 | cmdurl = 'http://%s/%s.cgi?user=%s&pwd=%s%s' %(self.url, cmd, self.usr, self.pwd, paramstr)
86 |
87 | # Parse parameters from response string.
88 | if self.verbose:
89 | utils.log(4, 'Camera %s :: Send Foscam command: %s' %(self.number, cmdurl))
90 |
91 | code = ERROR_FOSCAM_UNKNOWN
92 | try:
93 | raw_string = ''
94 | raw_string = urllib.urlopen(cmdurl).read()
95 | #print raw_string
96 | code = FOSCAM_SUCCESS
97 |
98 | except:
99 | if self.verbose:
100 | utils.log(3, 'Camera %s :: Foscam exception: %s' %(self.number, raw_string))
101 | return ERROR_FOSCAM_UNAVAILABLE, None
102 |
103 | params = dict()
104 | response = raw_string.replace('var ','').replace('\n','').split(';')
105 |
106 | for child in response:
107 | child_split = child.split('=')
108 | if len(child_split) == 2:
109 | params[child_split[0]] = child_split[1]
110 |
111 | if self.verbose:
112 | utils.log(4, 'Camera %s :: Received Foscam response: %s, %s' %(self.number, code, params))
113 | return code, params
114 |
115 | def execute_command(self, cmd, params = None, callback = None):
116 | '''
117 | Execute a command and return a parsed response.
118 | '''
119 | def execute_with_callbacks(cmd, params = None, callback = None):
120 | code, params = self.send_command(cmd, params)
121 | if callback:
122 | callback(code, params)
123 | return code, params
124 |
125 | if self.daemon:
126 | t = Thread(target=execute_with_callbacks,
127 | args=(cmd, ), kwargs={'params': params, 'callback': callback})
128 | t.daemon = True
129 | t.start()
130 | else:
131 | return execute_with_callbacks(cmd, params, callback)
132 |
133 | # *************** Device manage *******************
134 | def get_dev_state(self, callback = None):
135 | '''
136 | Get all device state
137 | cmd: getDevState
138 | return args:
139 | ......
140 | id:
141 | device id sys_ver:
142 | firmware version app_ver:
143 | webpage gui version alias:aliasname
144 | now:the lapse second from 1970-1-1 0:0:0 to device current time.
145 | Tz:device current time zone setting and the number of seconds deviation of GMT T
146 | alarm_status: device current alarm status,
147 | 0 no alarm; 1 motion detection alarm; 2 input alarm; 3 voice detection alarm
148 | ......
149 | '''
150 | return self.execute_command('get_status', callback = callback)
151 |
152 | def reboot(self, callback = None):
153 | ''' Reboots device '''
154 | return self.execute_command('reboot', callback=callback)
155 |
156 |
157 |
158 | # *************** AV Settings ******************
159 | def get_camera_params(self, callback = None):
160 | '''
161 | resolution: 8 qvga; 32 vga
162 | brightness: 0~255
163 | contrast: 0~6
164 | mode: 0 50hz; 1 60hz; 2 outdoor
165 | flip: 0 initial; 1 vertical rotate; 2 horizontal rotate; 3 is vertical + horizontal rotate
166 | '''
167 | return self.execute_command('get_camera_params', None, callback = callback)
168 |
169 | def set_camera_params(self, param, value, callback = None):
170 | '''
171 | /camera_control.cgi?param=&value=[&user=&pwd=&next_url=]
172 |
173 | 0: resolution: 2 qqvga; 8 qvga; 32 vga
174 | 1: brightness: 0~255
175 | 2: contrast: 0~6
176 | 3: mode: 0 50hz; 1 60hz; 2 outdoor5
177 | 5: flip: 0 initial; 1 vertical rotate; 2 horizontal rotate; 3 is vertical + horizontal rotate
178 | '''
179 | params = {'value': value,
180 | 'param': param}
181 | return self.execute_command('camera_control', params, callback = callback)
182 |
183 | def get_mirror_and_flip_setting(self, callback = None):
184 | response_ok, response = self.get_camera_params()
185 | if response['flip'] == '0':
186 | return response_ok, {'isMirror': '0', 'isFlip': '0'}
187 | elif response['flip'] == '1':
188 | return response_ok, {'isMirror': '0', 'isFlip': '1'}
189 | elif response['flip'] == '2':
190 | return response_ok, {'isMirror': '1', 'isFlip': '0'}
191 | elif response['flip'] == '3':
192 | return response_ok, {'isMirror': '1', 'isFlip': '1'}
193 |
194 | def mirror_video(self, is_mirror = None, callback = None):
195 | '''
196 | is_mirror: 0 not mirror; 1 mirror
197 | '''
198 | response_ok, response = self.get_mirror_and_flip_setting()
199 | print response
200 | if is_mirror == None:
201 | is_mirror = response['isMirror']
202 | value = ''
203 | if is_mirror == '0':
204 | if response['isFlip'] == '0':
205 | value = '2'
206 | else:
207 | value = '3'
208 | elif is_mirror == '1':
209 | if response['isFlip'] == '0':
210 | value = '0'
211 | else:
212 | value = '1'
213 | return self.set_camera_params('5', value)
214 |
215 | def flip_video(self, is_flip = None, callback = None):
216 | '''
217 | is_flip: 0 Not flip; 1 Flip
218 | '''
219 | response_ok, response = self.get_mirror_and_flip_setting()
220 | if is_flip == None:
221 | is_flip = response['isFlip']
222 | value = ''
223 | if is_flip == '0':
224 | if response['isMirror'] == '0':
225 | value = '1'
226 | else:
227 | value = '3'
228 | elif is_flip == '1':
229 | if response['isMirror'] == '0':
230 | value = '0'
231 | else:
232 | value = '2'
233 | return self.set_camera_params('5', value)
234 |
235 |
236 | def get_misc(self, callback = None):
237 | ''' See set_misc() '''
238 | return self.execute_command('get_misc', None, callback = callback)
239 |
240 | def set_misc(self, params, callback = None):
241 | '''
242 | led_mode: 0 mode1; 1 mode2; 2 LED Off
243 | led_mode=0 - the green led blinks only once connected.
244 | led_mode=1 - the green led blinks while searching for a connection and when connected.
245 | led_mode=2 - the green led is always off.
246 | ptz_center_onstart: 0 disable; 1 enable
247 | ptz_center_onstart=0 the camera won't auto-rotate any more when restarting, so you won't need to re-position it any longer upon rebooting.
248 | ptz_auto_patrol_interval: 0 none; 1 auto
249 | ptz_auto_patrol_type: 0 no rotate; 1 horizontal; 2 vertical; 3 horizontal+vertical
250 | ptz_patrol_h_rounds: 0 infinite; n Number of rounds
251 | ptz_patrol_v_rounds: 0 infinite; n Number of rounds
252 | ptz_patrol_rate: 0-100; 0 slowest, 100 fastest
253 | ptz_patrol_up_rate: 0-100; 0 slowest, 100 fastest
254 | ptz_patrol_down_rate: 0-100; 0 slowest, 100 fastest
255 | ptz_patrol_left_rate: 0-100; 0 slowest, 100 fastest
256 | ptz_patrol_right_rate: 0-100; 0 slowest, 100 fastest
257 | ptz_disable_preset: 0 no; 1 yes (after reboot)
258 | ptz_preset_onstart: 0 disable; 1 enabled
259 |
260 | http://[ipcam]/set_misc.cgi?ptz_auto_patrol_interval=30
261 | This function is currently not implemented in the user interface and instructs the camera to start a patrol at a defined interval, here 30 seconds.
262 | The patrol type is defined by this other command:
263 | http://[ipcam]/set_misc.cgi?ptz_auto_patrol_type=1
264 | Possible values: 0: None; 1: horizontal; 2: vertical; 3: Horizontal + Vertical
265 |
266 | http://[ipcam]/set_misc.cgi?ptz_patrol_rate= 20
267 | The value provided will defined how fast the camera will rotate on patrol, here 20 is the default.
268 | Fastest speed = 0. Slowest speed = 100.
269 | '''
270 | return self.execute_command('set_misc', params, callback = callback)
271 |
272 | def set_ir_on(self, callback = None): # NEED VERIFICATION
273 | #params = {'led_mode': '0'}
274 | #return self.set_misc(params, callback)
275 | return self.decoder_control(95)
276 |
277 | def set_ir_off(self, callback = None): # NEED VERIFICATION
278 | #params = {'led_mode': '2'}
279 | #return self.set_misc(params, callback)
280 | return self.decoder_control(94)
281 |
282 | def get_ir_config(self, callback = None): # NOT GOOD
283 | '''
284 | Get IR Config
285 | mode: 0 Auto
286 | 1 Manual
287 | '''
288 | response_ok, data = self.get_misc()
289 | params = {}
290 | if data[1]['led_mode'] == '0':
291 | params['mode'] = '1'
292 | else:
293 | params['mode'] = '0'
294 | return response_ok, params
295 |
296 |
297 | def set_ir_config(self, mode, callback=None): # NEED VERIFICATION
298 | '''
299 | Set IR Config
300 | mode: 0 Auto
301 | 1 Manual
302 | '''
303 | params = {}
304 | if mode == '0':
305 | params['mode'] = '1'
306 | else:
307 | params['mode'] = '0'
308 | return set_misc(params, callback)
309 |
310 |
311 | def get_dev_name(self, callback=None): # NEED VERIFICATION
312 | '''
313 | Get camera name.
314 | '''
315 | return self.execute_command('getDevName', callback=callback)
316 |
317 | def set_dev_name(self, devname, callback=None): # NEED VERIFICATION
318 | '''
319 | Set camera name
320 | '''
321 | params = {'devName': devname.encode('gbk')}
322 | return self.execute_command('setDevName', params, callback=callback)
323 |
324 | def set_pwr_freq(self, mode, callback=None): # NEED VERIFICATION
325 | '''
326 | Set power frequency of sensor
327 | mode:
328 | 0 60HZ
329 | 1 50Hz
330 | 2 Outdoor
331 | '''
332 | params = {'freq': mode}
333 | return self.execute_command('setPwrFreq', params, callback=callback)
334 |
335 |
336 |
337 |
338 |
339 | def get_params(self, callback = None): # NEED VERIFICATION
340 | '''
341 | re
342 | '''
343 | return self.execute_command('get_params', None, callback = callback)
344 |
345 |
346 |
347 |
348 | # *************** PTZ Control *******************
349 |
350 | def decoder_control(self, command, onestep = None, degree = None, callback = None): ### NEW FUNCTION ###
351 | '''
352 | /decoder_control.cgi?command=[&onestep=°ree=&user=&pwd=&next_url=
353 |
354 | onestep=1: indicate the PTZ control is one step then stop, it is only for camera
355 | with ptz originally and it is only for up, down,left and right.
356 |
357 | Value 485port extra connection Internal motor
358 | 0 up
359 | 1 stop up
360 | 2 down
361 | 3 stop down
362 | 4 right
363 | 5 stop right
364 | 6 left
365 | 7 stop left
366 | 16 Zoom close
367 | 17 Stop zoom close
368 | 18 Zoom far
369 | 19 Stop zoom far
370 | 20 Auto patrol
371 | 21 Stop auto patrol
372 | 25 center
373 | 26 Up & down patrol
374 | 27 Stop up & down patrol
375 | 28 Left & right patrol
376 | 29 Left & right patrol
377 |
378 | 30 Set preset1
379 | 31 Go to preset1
380 | ..
381 | 60 Set preset 16 Set preset 16
382 | 61 Go to preset 16 Go to preset 16
383 |
384 | 90 Upper right
385 | 91 Upper left
386 | 92 Down right
387 | 93 Down left
388 | 94 IR LED OFF (IO high?)
389 | 95 IR LED ON (IO low?)
390 | 255 Motor test mode
391 | '''
392 | params = {'command': command}
393 | if onestep != None:
394 | params['onestep'] = onestep
395 | if degree != None:
396 | params['degree'] = degree
397 | else:
398 | params['degree'] = 3
399 | return self.execute_command('decoder_control', params, callback=callback)
400 |
401 | def ptz_set_sensitivity(self, degree):
402 | self.degree = degree
403 | return float(self.degree)
404 |
405 | def ptz_move_up(self, callback = None):
406 | return self.decoder_control(0, 1, self.degree)
407 |
408 | def ptz_move_down(self, callback = None):
409 | return self.decoder_control(2, 1, self.degree)
410 |
411 | def ptz_move_left(self, callback = None):
412 | return self.decoder_control(6, 1, self.degree)
413 |
414 | def ptz_move_right(self, callback = None):
415 | return self.decoder_control(4, 1, self.degree)
416 |
417 | def ptz_move_top_left(self, callback = None):
418 | return self.decoder_control(91, 1, self.degree)
419 |
420 | def ptz_move_top_right(self, callback = None):
421 | return self.decoder_control(90, 1, self.degree)
422 |
423 | def ptz_move_bottom_left(self, callback = None):
424 | return self.decoder_control(93, 1, self.degree)
425 |
426 | def ptz_move_bottom_right(self, callback = None):
427 | return self.decoder_control(92, 1, self.degree)
428 |
429 | def ptz_stop_run(self, callback = None):
430 | return None
431 |
432 | def get_ptz_speed(self, callback = None):
433 | return None
434 |
435 | def set_ptz_speed(self, speed, callback = None):
436 | return None
437 |
438 | def ptz_zoom_in(self, callback = None):
439 | return self.decoder_control(16, 1, self.degree)
440 |
441 | def ptz_zoom_out(self, callback = None):
442 | return self.decoder_control(18, 1, self.degree)
443 |
444 | def ptz_zoom_stop(self, callback = None):
445 | return None
446 |
447 | def get_ptz_zoom_speed(self, callback = None):
448 | return None
449 |
450 | def set_ptz_zoom_speed(self, speed, callback = None):
451 | return None
452 |
453 |
454 |
455 |
456 | def ptz_reset(self, callback = None):
457 | '''
458 | Reset PT to default position.
459 | '''
460 | return self.decoder_control(25)
461 |
462 | def ptz_home_location(self, mode, callback = None): # NEED VERIFICATION
463 | '''
464 | Reset PT to home position.
465 | mode: 0 Return if add-on default preset point exists or not
466 | 1 Go to add-on default preset if it exists and no action if it doesn't
467 | 2 Return if add-on default preset point exists or not, and go to point if it does or camera default if it doesn't
468 | 3 Reset to camera default location
469 | '''
470 | mode = 3
471 | self.set_misc({'ptz_disable_preset': '0'})
472 | self.get_misc()
473 |
474 | self.ptz_goto_preset()
475 | return mode
476 | if mode < 3:
477 | response_code, response = self.ptz_get_preset()
478 | try:
479 | qty = int(response.get('cnt'))
480 | for x in range(4, qty):
481 | point = response.get('point%d' %x)
482 | if 'surveillanceroom_default' in point:
483 | if mode == 0:
484 | return True
485 | else:
486 | return True, self.ptz_goto_preset('surveillanceroom_default')
487 |
488 | except:
489 | pass
490 |
491 | if mode == 2:
492 | self.ptz_reset()
493 | return False
494 |
495 | #return self.ptz_reset()
496 |
497 | def ptz_get_preset(self, callback=None): # NEED VERIFICATION
498 | '''
499 | Get presets.
500 | cnt: Current preset point count
501 | pointN: The name of point N
502 | '''
503 | return self.execute_command('getPTZPresetPointList', callback=callback)
504 |
505 | def ptz_goto_preset(self, callback=None): # NEED VERIFICATION
506 | '''
507 | Move to preset.
508 | 4 points are default: LeftMost\RightMost\TopMost\BottomMost
509 | '''
510 | return self.decoder_control(31)
511 |
512 | def ptz_add_preset(self, name = None, callback=None):
513 | return self.decoder_control(30)
514 |
515 | def ptz_delete_preset(self, name = None, callback=None): # NEED VERIFICATION
516 | '''
517 | Delete a preset point from the preset point list.
518 | '''
519 | if name == None:
520 | name = 'surveillanceroom_default'
521 | params = {'name': name}
522 | return self.execute_command('ptzDeletePresetPoint', params, callback=callback)
523 |
524 |
525 |
526 | def get_ptz_selftestmode(self, callback=None): # NEED VERIFICATION
527 | '''
528 | Get the selftest mode of PTZ
529 | '''
530 | return self.execute_command('getPTZSelfTestMode', callback=callback)
531 |
532 | def set_ptz_selftestmode(self, mode=0, callback=None): # NEED VERIFICATION
533 | '''
534 | Set the selftest mode of PTZ
535 | mode = 0: No selftest
536 | mode = 1: Normal selftest
537 | mode = 1: After normal selftest, then goto presetpoint-appointed
538 | '''
539 | return self.execute_command('setPTZSelfTestMode',
540 | {'mode':mode},
541 | callback=callback
542 | )
543 |
544 |
545 |
546 | # *************** Alarm Function *******************
547 | def is_alarm_active(self, motion_enabled, sound_enabled):
548 | '''
549 | Returns the state of the alarm on the camera.
550 | '''
551 | success_code, response = self.get_dev_state()
552 |
553 | if success_code == 0:
554 |
555 | if int(response.get('alarm_status')) == 1 and motion_enabled:
556 | return success_code, True, 'Motion Alarm Detect'
557 |
558 | if int(response.get('alarm_status')) == 3 and sound_enabled:
559 | return success_code, True, 'Sound Alarm Detect'
560 |
561 | return success_code, False, None
562 |
563 |
564 |
565 |
566 | def get_sound_detect_config(self, callback=None): # NEED VERIFICATION
567 | '''
568 | Get sound detect config
569 | '''
570 | return self.execute_command('getAudioAlarmConfig', callback=callback)
571 |
572 | def set_sound_detect_config(self, params, callback=None): # NEED VERIFICATION
573 | '''
574 | Set sound detect config
575 | '''
576 | return self.execute_command('setAudioAlarmConfig', params, callback=callback)
577 |
578 | def set_sound_detection(self, enabled=1): # NEED VERIFICATION
579 | '''
580 | Get the current config and set the sound detection on or off
581 | '''
582 | result, current_config = self.get_sound_detect_config()
583 | current_config['isEnable'] = enabled
584 | self.set_sound_detect_config(current_config)
585 |
586 | def enable_sound_detection(self): # NEED VERIFICATION
587 | '''
588 | Enable sound detection
589 | '''
590 | self.set_sound_detection(1)
591 |
592 | def disable_sound_detection(self): # NEED VERIFICATION
593 | '''
594 | disable sound detection
595 | '''
596 | self.set_sound_detection(0)
597 |
598 | def get_sound_sensitivity(self): # NEED VERIFICATION
599 | '''
600 | Get the current config and set the sound detection on or off
601 | '''
602 | result, current_config = self.get_sound_detect_config()
603 | return current_config['sensitivity']
604 |
605 | def set_sound_sensitivity(self, sensitivity): # NEED VERIFICATION
606 | '''
607 | Get the current config and set the sound detection on or off
608 | '''
609 | result, current_config = self.get_sound_detect_config()
610 | current_config['sensitivity'] = sensitivity
611 | self.set_sound_detect_config(current_config)
612 |
613 | def get_sound_triggerinterval(self): # NEED VERIFICATION
614 | '''
615 | Get the current config and set the sound detection on or off
616 | '''
617 | result, current_config = self.get_sound_detect_config()
618 | return current_config['triggerInterval']
619 |
620 | def set_sound_triggerinterval(self, triggerInterval): # NEED VERIFICATION
621 | '''
622 | Get the current config and set the sound detection on or off
623 | '''
624 | result, current_config = self.get_sound_detect_config()
625 | current_config['triggerInterval'] = triggerInterval
626 | self.set_sound_detect_config(current_config)
627 |
628 |
629 |
630 |
631 | def get_motion_detect_config(self, callback=None): # NEED VERIFICATION
632 | '''
633 | Get motion detect config
634 | '''
635 | return self.execute_command('getMotionDetectConfig', callback=callback)
636 |
637 | def set_motion_detect_config(self, params, callback=None):
638 | '''
639 | Set motion detect config
640 | '''
641 | return self.execute_command('setMotionDetectConfig', params, callback=callback)
642 |
643 | def set_motion_detection(self, enabled=1): # NEED VERIFICATION
644 | '''
645 | Get the current config and set the motion detection on or off
646 | '''
647 | result, current_config = self.get_motion_detect_config()
648 | current_config['isEnable'] = enabled
649 | self.set_motion_detect_config(current_config)
650 |
651 | def enable_motion_detection(self): # NEED VERIFICATION
652 | '''
653 | Enable motion detection
654 | '''
655 | self.set_motion_detection(1)
656 |
657 | def disable_motion_detection(self): # NEED VERIFICATION
658 | '''
659 | disable motion detection
660 | '''
661 | self.set_motion_detection(0)
662 |
663 | def get_motion_sensitivity(self): # NEED VERIFICATION
664 | '''
665 | Get the current config and set the motion detection on or off
666 | '''
667 | result, current_config = self.get_motion_detect_config()
668 | return current_config['sensitivity']
669 |
670 | def set_motion_sensitivity(self, sensitivity): # NEED VERIFICATION
671 | '''
672 | Get the current config and set the motion detection on or off
673 | '''
674 | result, current_config = self.get_motion_detect_config()
675 | current_config['sensitivity'] = sensitivity
676 | self.set_motion_detect_config(current_config)
677 |
678 | def get_motion_triggerinterval(self): # NEED VERIFICATION
679 | '''
680 | Get the current config and set the motion detection on or off
681 | '''
682 | result, current_config = self.get_motion_detect_config()
683 | return current_config['triggerInterval']
684 |
685 | def set_motion_triggerinterval(self, triggerInterval): # NEED VERIFICATION
686 | '''
687 | Get the current config and set the motion detection on or off
688 | '''
689 | result, current_config = self.get_motion_detect_config()
690 | current_config['triggerInterval'] = triggerInterval
691 | self.set_motion_detect_config(current_config)
692 |
693 |
694 |
695 |
696 |
--------------------------------------------------------------------------------
/resources/lib/ipcam_api_generic.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | """
7 |
8 | import utils, settings
9 |
10 | class GenericIPCam(object):
11 | '''A python implementation of a Generic IP Camera'''
12 |
13 | def __init__(self, camera_settings, daemon = False, verbose = True):
14 | '''
15 | If ``daemon`` is True, the command will be sent unblockedly.
16 | '''
17 | self.daemon = daemon
18 | self.verbose = verbose
19 | self.camera_number = camera_settings[0]
20 | #self.host = camera_settings[1]
21 | #self.port = camera_settings[2]
22 | #self.usr = camera_settings[3]
23 | #self.pwd = camera_settings[4]
24 |
25 | @property
26 | def video_url(self):
27 | _videoUrl = settings.getSetting('stream_url', self.camera_number)
28 | return _videoUrl
29 |
30 | @property
31 | def mjpeg_url(self):
32 | _mjpegUrl = settings.getSetting('mjpeg_url', self.camera_number)
33 | return _mjpegUrl
34 |
35 | @property
36 | def snapshot_url(self):
37 | _snapshotUrl = settings.getSetting('snapshot_url', self.camera_number)
38 | return _snapshotUrl
39 |
40 | def __enter__(self):
41 | return self
42 |
43 | def __exit__(self, exc_type, exc_value, traceback):
44 | return None
45 |
46 |
--------------------------------------------------------------------------------
/resources/lib/ipcam_api_wrapper.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | Wrapper for Camera API
7 |
8 | """
9 | import utils, settings
10 | import ipcam_api_foscamhd, ipcam_api_foscamsd, ipcam_api_generic
11 |
12 | FOSCAM_HD = 0
13 | FOSCAM_SD = 1
14 | FOSCAM_HD_OVERRIDE = 2
15 | GENERIC_IPCAM = 3
16 |
17 | SINGLE_CAMERA_PLAYER = 0
18 | ALL_CAMERA_PLAYER = 1
19 | PREVIEW_WINDOW = 2
20 |
21 | class CameraAPIWrapper(object):
22 | '''A python implementation of the foscam HD816W'''
23 |
24 | def __init__(self, camera_number, daemon = False, verbose = True):
25 |
26 | self.camera_number = str(camera_number)
27 | self.camera_type = settings.getCameraType(self.camera_number)
28 | camera_settings = self.getCameraSettings()
29 |
30 | # This is where we determine which API to use!
31 | if self.camera_type == FOSCAM_HD:
32 | self.camera = ipcam_api_foscamhd.FoscamCamera(camera_settings, daemon, verbose)
33 |
34 | elif self.camera_type == FOSCAM_SD:
35 | self.camera = ipcam_api_foscamsd.FoscamCamera(camera_settings, daemon, verbose)
36 |
37 | elif self.camera_type == FOSCAM_HD_OVERRIDE:
38 | self.camera = ipcam_api_foscamhd.FoscamCameraOverride(camera_settings, daemon, verbose)
39 |
40 | elif self.camera_type == GENERIC_IPCAM:
41 | self.camera = ipcam_api_generic.GenericIPCam(camera_settings, daemon, verbose)
42 |
43 | def __enter__(self):
44 | return self
45 |
46 | def __exit__(self, exc_type, exc_value, traceback):
47 | return None
48 |
49 | def getCameraSettings(self):
50 | """ Returns the login details of the camera """
51 | #utils.log(4, 'SETTINGS :: Use Cache: %s; Camera %s; Camera Type: %s' %(useCache, self.camera_number, self.camera_type))
52 |
53 | ''' Foscam Camera '''
54 | if self.camera_type != GENERIC_IPCAM:
55 |
56 | host = settings.getSetting('host', self.camera_number)
57 | if not host:
58 | utils.log(3, 'Camera %s :: No host specified.' %self.camera_number)
59 | host = ''
60 |
61 | port = settings.getSetting('port', self.camera_number)
62 | if not port:
63 | utils.log(3, 'Camera %s :: No port specified.' %self.camera_number)
64 | port = ''
65 |
66 | username = settings.getSetting('user', self.camera_number)
67 | invalid = settings.invalid_user_char(username)
68 | if invalid:
69 | utils.log(3, 'Camera %s :: Invalid character in user name: %s' %(self.camera_number, invalid))
70 | username = ''
71 |
72 | password = settings.getSetting('pass', self.camera_number)
73 | invalid = settings.invalid_password_char(password)
74 | if invalid:
75 | utils.log(3, 'SETTINGS :: Camera %s - Invalid character in password: %s' %(self.camera_number, invalid))
76 | password = ''
77 |
78 | return [self.camera_number, host, port, username, password]
79 |
80 | else:
81 | ''' Generic IP Camera '''
82 | return [self.camera_number, '', '', '', '']
83 |
84 | def Connected(self, monitor = None, useCache = True):
85 | # Camera test and caching logic
86 | if monitor:
87 | if useCache:
88 | utils.log(2, 'Camera %s :: Checking previous camera test connection result...' %self.camera_number)
89 | return monitor.testResult(self.camera_number)
90 |
91 | else:
92 | if self.camera_type != GENERIC_IPCAM:
93 | utils.log(2, 'Camera %s :: Testing network connection to camera...' %self.camera_number)
94 |
95 | success_code, response = self.camera.get_dev_state()
96 | monitor.write_testResult(self.camera_number, success_code)
97 |
98 | if success_code != 0:
99 | return False
100 |
101 | utils.log(2, 'Camera %s :: Connection successful.' %self.camera_number)
102 |
103 | # MJPEG Enable - for Service Run. Ensures MJPEG URLs are Successful MAYBE MOVE THIS LATER SOMEWHERE???
104 | if settings.getSetting_int('stream', self.camera_number) == 1 or \
105 | settings.getSetting_int('allstream', self.camera_number) != 1 or \
106 | settings.getSetting_int('preview_stream', self.camera_number) != 1:
107 | self.enable_mjpeg()
108 |
109 | else:
110 | # Set result for Generic IP Camera
111 | monitor.write_testResult(self.camera_number, 0)
112 |
113 | return True
114 | return False
115 |
116 | @property
117 | def number(self):
118 | return self.camera_number
119 |
120 | @property
121 | def _type(self):
122 | return self.camera_type
123 |
124 | @property
125 | def url(self):
126 | return self.camera.url
127 |
128 | @property
129 | def video_url(self):
130 | return self.camera.video_url
131 |
132 | @property
133 | def mjpeg_url(self):
134 | return self.camera.mjpeg_url
135 |
136 | @property
137 | def snapshot_url(self):
138 | return self.camera.snapshot_url
139 |
140 | @property
141 | def ptz_sensitivity(self):
142 | return self.ptz_get_sensitivity()
143 |
144 | # *********************** SETTING COMMANDS ***********************
145 |
146 | def ptz_get_sensitivity(self):
147 | if self.camera_type == FOSCAM_HD or self.camera_type == FOSCAM_HD_OVERRIDE:
148 | return settings.getSetting_float('ptz_hd_sensitivity%s' %self.camera_number) / 10
149 | elif self.camera_type == FOSCAM_SD:
150 | return self.camera.ptz_set_sensitivity(settings.getSetting('ptz_sd_sensitivity%s' %self.camera_number))
151 |
152 | def getUrl(self, source, stream_type):
153 | '''
154 | Source Stream_type:
155 | 0 Video Stream: 0 Video; 1 Mjpeg
156 | 1 All Camera Player: 0 Mjpeg; 1 Snapshot; 2 Mjpeg
157 | 2 Preview: 0 Mjpeg; 1 Snapshot; 2 Mjpeg
158 | '''
159 |
160 | if (source != SINGLE_CAMERA_PLAYER and stream_type != 1) or (source == SINGLE_CAMERA_PLAYER and stream_type == 1):
161 | return self.camera.mjpeg_url
162 | elif (source == SINGLE_CAMERA_PLAYER and stream_type == 0):
163 | return self.camera.video_url
164 | else:
165 | return self.camera.snapshot_url
166 |
167 | def getStreamType(self, source):
168 | '''
169 | Source: 0 Video Stream; 1 All Camera Player; 2 Preview
170 | '''
171 |
172 | if source == SINGLE_CAMERA_PLAYER:
173 | return settings.getSetting_int('stream', self.camera_number)
174 | elif source == ALL_CAMERA_PLAYER:
175 | return settings.getSetting_int('allstream', self.camera_number)
176 | elif source == PREVIEW_WINDOW:
177 | return settings.getSetting_int('preview_stream', self.camera_number)
178 |
179 | def getStreamUrl(self, source, stream_type = None):
180 | '''
181 | Source: 0 Video Stream; 1 All Camera Player; 2 Preview
182 | '''
183 | if not stream_type:
184 | stream_type = self.getStreamType(source)
185 |
186 | return self.getUrl(source, stream_type)
187 |
188 | def getSnapShotUrl(self):
189 | return self.getStreamUrl(1, 1)
190 |
191 | def resetLocation(self):
192 | if settings.getSetting_int('ptz', self.camera_number) > 0:
193 | reset_mode = settings.getSetting_int('conn', self.camera_number)
194 | if reset_mode > 0:
195 | if reset_mode == 2:
196 | reset_mode = 3
197 | self.camera.ptz_home_location(reset_mode)
198 | utils.log(2, 'Camera %s :: Resetting to the home location' %self.camera_number)
199 |
200 | def getTriggerInterval(self, motion_enabled, sound_enabled):
201 | """ Gets the alarm trigger interval from the camera """
202 | trigger_interval = settings.getSetting_int('interval', self.camera_number)
203 |
204 | if self.camera_type != FOSCAM_SD and \
205 | self.camera_type != GENERIC_IPCAM:
206 | try:
207 | motion_trigger_interval = int(self.camera.get_motion_detect_config()[1]['triggerInterval'])
208 | sound_trigger_interval = int(self.camera.get_sound_detect_config()[1]['triggerInterval'])
209 |
210 | if motion_enabled and sound_enabled: trigger_interval = min(motion_trigger_interval, sound_trigger_interval)
211 | elif motion_enabled: trigger_interval = motion_trigger_interval
212 | elif sound_enabled: trigger_interval = sound_trigger_interval
213 |
214 | except:
215 | pass
216 |
217 | return trigger_interval
218 |
219 |
220 | # *********************** PASSTHROUGH COMMANDS ***********************
221 | def get_dev_state(self):
222 | return self.camera.get_dev_state()
223 |
224 | def is_alarm_active(self, motion_enabled, sound_enabled):
225 | return self.camera.is_alarm_active(motion_enabled, sound_enabled)
226 |
227 |
228 | # *************** AV Functions ******************
229 | def enable_mjpeg(self):
230 | if self.camera_type != FOSCAM_SD and \
231 | self.camera_type != GENERIC_IPCAM:
232 | return self.camera.enable_mjpeg()
233 | return None
234 |
235 | def disable_mjpeg(self):
236 | if self.camera_type != FOSCAM_SD and \
237 | self.camera_type != GENERIC_IPCAM:
238 | return self.camera.disable_mjpeg()
239 | return None
240 |
241 | def set_snapshot_config(self, quality, callback = None):
242 | return self.camera.set_snapshot_config(quality)
243 |
244 | def get_snapshot_config(self, callback = None):
245 | return self.camera.get_snapshot_config()
246 |
247 | def set_ir_on(self, callback=None):
248 | return self.camera.set_ir_on()
249 |
250 | def set_ir_off(self, callback=None):
251 | return self.camera.set_ir_off()
252 |
253 | def get_ir_config(self, callback=None):
254 | return self.camera.get_ir_config()
255 |
256 | def set_ir_config(self, mode, callback=None):
257 | return self.camera.set_ir_config(mode)
258 |
259 | def mirror_video(self, is_mirror = None, callback = None):
260 | return self.camera.mirror_video(is_mirror)
261 |
262 | def flip_video(self, is_flip = None, callback = None):
263 | return self.camera.flip_video(is_flip)
264 |
265 | def get_mirror_and_flip_setting(self, callback = None):
266 | return self.camera.get_mirror_and_flip_setting()
267 |
268 |
269 |
270 | # *************** Device Managing Functions *******************
271 | def get_dev_name(self, callback = None):
272 | return self.camera.get_dev_name()
273 |
274 | def set_dev_name(self, devname, callback = None):
275 | return self.camera.set_dev_name(devname)
276 |
277 | def set_pwr_freq(self, mode, callback = None):
278 | return self.camera.set_pwr_freq(mode)
279 |
280 | def get_osd_setting(self, callback = None):
281 | return self.camera.get_osd_setting()
282 |
283 | def reboot(self, callback = None):
284 | return self.camera.reboot()
285 |
286 | # *************** PTZ Control Functions *******************
287 | def ptz_move_up(self, callback = None):
288 | return self.camera.ptz_move_up()
289 |
290 | def ptz_move_down(self, callback = None):
291 | return self.camera.ptz_move_down()
292 |
293 | def ptz_move_left(self, callback = None):
294 | return self.camera.ptz_move_left()
295 |
296 | def ptz_move_right(self, callback = None):
297 | return self.camera.ptz_move_right()
298 |
299 | def ptz_move_top_left(self, callback = None):
300 | return self.camera.ptz_move_top_left()
301 |
302 | def ptz_move_top_right(self, callback = None):
303 | return self.camera.ptz_move_top_right()
304 |
305 | def ptz_move_bottom_left(self, callback = None):
306 | return self.camera.ptz_move_bottom_left()
307 |
308 | def ptz_move_bottom_right(self, callback = None):
309 | return self.camera.ptz_move_bottom_right()
310 |
311 | def ptz_stop_run(self, callback = None):
312 | return self.camera.ptz_stop_run()
313 |
314 | def ptz_reset(self, callback = None):
315 | return self.camera.ptz_reset()
316 |
317 | def ptz_home_location(self, mode, callback = None):
318 | return self.camera.ptz_home_location(mode)
319 |
320 | def ptz_get_preset(self, callback = None):
321 | return self.camera.ptz_get_preset()
322 |
323 | def ptz_goto_preset(self, name, callback = None):
324 | return self.camera.ptz_goto_preset(name)
325 |
326 | def ptz_add_preset(self, name = None, callback = None):
327 | return self.camera.ptz_add_preset(name)
328 |
329 | def ptz_delete_preset(self, name = None, callback = None):
330 | return self.camera.ptz_delete_preset(name)
331 |
332 | def get_ptz_speed(self, callback = None):
333 | return self.camera.get_ptz_speed()
334 |
335 | def set_ptz_speed(self, speed, callback = None):
336 | return self.camera.set_ptz_speed(speed)
337 |
338 | def get_ptz_selftestmode(self, callback = None):
339 | return self.camera.get_ptz_selfttestmode()
340 |
341 | def set_ptz_selftestmode(self, mode = 0, callback = None):
342 | return self.camera.set_ptz_selftestmode(mode)
343 |
344 | def ptz_zoom_in(self, callback = None):
345 | return self.camera.ptz_zoom_in()
346 |
347 | def ptz_zoom_out(self, callback = None):
348 | return self.camera.ptz_zoom_out()
349 |
350 | def ptz_zoom_stop(self, callback = None):
351 | return self.camera.ptz_zoom_stop()
352 |
353 | def get_ptz_zoom_speed(self, callback = None):
354 | return self.camera.get_ptz_zoom_speed()
355 |
356 | def set_ptz_zoom_speed(self, speed, callback = None):
357 | return self.camera.set_ptz_zoom_speed(speed)
358 |
359 |
360 | # *************** Sound Alarm Functions *******************
361 | def get_sound_detect_config(self, callback = None):
362 | return self.camera.get_sound_detect_config()
363 |
364 | def enable_sound_detection(self):
365 | return self.camera.enable_sound_detection()
366 |
367 | def disable_sound_detection(self):
368 | return self.camera.disable_sound_detection()
369 |
370 | def get_sound_sensitivity(self):
371 | return self.camera.get_sound_sensitivity()
372 |
373 | def set_sound_sensitivity(self, level):
374 | return self.camera.set_sound_sensitivity(level)
375 |
376 | def get_sound_triggerinterval(self):
377 | return self.camera.get_sound_trigger_interval()
378 |
379 | def set_sound_triggerinterval(self, interval):
380 | return self.camera.set_sound_triggerinterval(interval)
381 |
382 | # *************** Motion Alarm Functions *******************
383 | def get_motion_detect_config(self, callback = None):
384 | return self.camera.get_motion_detect_config()
385 |
386 | def enable_motion_detection(self):
387 | return self.camera.enable_motion_detection()
388 |
389 | def disable_motion_detection(self):
390 | return self.camera.disable_motion_detection()
391 |
392 | def get_motion_sensitivity(self):
393 | return self.camera.get_motion_sensitivity()
394 |
395 | def set_motion_sensitivity(self, level):
396 | return self.camera.set_motion_sensitivity(level)
397 |
398 | def get_motion_triggerinterval(self):
399 | return self.camera.get_motion_triggerinterval()
400 |
401 | def set_motion_triggerinterval(self, interval):
402 | return self.camera.set_motion_triggerinterval(interval)
403 |
404 |
405 |
406 |
407 |
--------------------------------------------------------------------------------
/resources/lib/monitor.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | This module is used to monitor the entire add-on and allow communication between events outside of the main process loop
7 | """
8 |
9 | import xbmc
10 | from xbmcgui import Window, getCurrentWindowId, getCurrentWindowDialogId, ListItem
11 | from time import time
12 | import settings, utils
13 |
14 | class AddonMonitor(xbmc.Monitor):
15 | """ Addon monitor class is used to monitor the entire addon and make changes on a global level """
16 |
17 | def __init__(self):
18 | xbmc.Monitor.__init__(self)
19 |
20 | def reset(self):
21 | ''' Reinitializes monitor settings '''
22 | Window(10000).setProperty('SR_monitor', '1')
23 | self.dismissed_time = [0, 0, 0, 0, 0, 0, 0]
24 | self._dismissed_behavior = settings.getSetting_int('dismissed_behavior') #0 - All dismissed, 1 - Just the window itself
25 | self._dismissed_duration = settings.getSetting_int('dismissed_duration')
26 | self._preview_disabled_window_id = settings.getDisabledWindowIds()
27 |
28 | def stop(self):
29 | Window(10000).clearProperty('SR_monitor')
30 |
31 | def stopped(self):
32 | if Window(10000).getProperty('SR_monitor') == '1':
33 | return False
34 | return True
35 |
36 | def onSettingsChanged(self):
37 | utils.log(2, 'MONITOR :: Settings change was detected')
38 | if not self.stopped():
39 | self.stop()
40 |
41 |
42 | # Improves UI speed by caching the result when the camera is first connected
43 | def write_testResult(self, camera_number, success_code):
44 | if success_code == 0:
45 | Window(10000).setProperty('SR_result_%s' %camera_number, '1')
46 | else:
47 | Window(10000).clearProperty('SR_result_%s' %camera_number)
48 |
49 | def testResult(self, camera_number):
50 | if Window(10000).getProperty('SR_result_%s' %camera_number) == '1':
51 | return True
52 | return False
53 |
54 |
55 |
56 |
57 | # NEW @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
58 | def openRequest(self, camera_number):
59 | Window(10000).setProperty('SR_openRequest_%s' %camera_number, '1')
60 |
61 | def openRequest_manual(self, camera_number):
62 | Window(10000).setProperty('SR_openRequest_manual_%s' %camera_number, '1')
63 | self.openRequest(camera_number)
64 |
65 | def closeRequest(self, camera_number):
66 | Window(10000).setProperty('SR_closeRequest_%s' %camera_number, '1')
67 |
68 | def set_alarmActive(self, camera_number):
69 | Window(10000).setProperty('SR_alarmActive_%s' %camera_number, '1')
70 |
71 |
72 | def clear_openRequest(self, camera_number):
73 | Window(10000).clearProperty('SR_openRequest_%s' %camera_number)
74 |
75 | def clear_openRequest_manual(self, camera_number):
76 | Window(10000).clearProperty('SR_openRequest_manual_%s' %camera_number)
77 |
78 | def clear_closeRequest(self, camera_number):
79 | Window(10000).clearProperty('SR_closeRequest_%s' %camera_number)
80 |
81 | def clear_alarmActive(self, camera_number):
82 | Window(10000).clearProperty('SR_alarmActive_%s' %camera_number)
83 |
84 |
85 | def openRequested(self, camera_number):
86 | if Window(10000).getProperty('SR_openRequest_%s' %camera_number) == '1':
87 | return True
88 | return False
89 |
90 | def openRequested_manual(self, camera_number):
91 | if Window(10000).getProperty('SR_openRequest_manual_%s' %camera_number) == '1':
92 | return True
93 | return False
94 |
95 | def closeRequested(self, camera_number):
96 | if Window(10000).getProperty('SR_closeRequest_%s' %camera_number) == '1':
97 | return True
98 | return False
99 |
100 | def alarmActive(self, camera_number):
101 | if Window(10000).getProperty('SR_alarmActive_%s' %camera_number) == '1':
102 | return True
103 | return False
104 |
105 |
106 | def openPreview(self, camera_number):
107 | self.clear_closeRequest(camera_number)
108 | Window(10000).setProperty('SR_openPreview_%s' %camera_number, '1')
109 | self.clear_openRequest(camera_number)
110 |
111 | def closePreview(self, camera_number):
112 | self.clear_openRequest(camera_number)
113 | self.clear_openRequest_manual(camera_number)
114 | Window(10000).clearProperty('SR_openPreview_%s' %camera_number)
115 | self.clear_closeRequest(camera_number)
116 |
117 | def previewOpened(self, camera_number):
118 | if Window(10000).getProperty('SR_openPreview_%s' %camera_number) == '1':
119 | return True
120 | return False
121 |
122 |
123 | # Used to disable or enable the preview service from activating without restarting the service
124 | def togglePreview(self):
125 | if self.toggledPreview():
126 | Window(10000).clearProperty('SR_togglePreview')
127 | utils.notify(utils.translation(32226))
128 | else:
129 | Window(10000).setProperty('SR_togglePreview', '1')
130 | utils.notify(utils.translation(32227))
131 |
132 | def toggledPreview(self):
133 | if Window(10000).getProperty('SR_togglePreview') == '1':
134 | return True
135 | return False
136 |
137 |
138 | # Used to make the add-on globally aware that a camera is playing fullscreen or not
139 | def set_playingCamera(self, camera_number):
140 | Window(10000).setProperty('SR_playingCamera_%s' %camera_number, '1')
141 | self.dismissAllPreviews()
142 |
143 | def clear_playingCamera(self, camera_number):
144 | Window(10000).clearProperty('SR_playingCamera_%s' %camera_number)
145 |
146 | def playingCamera(self, camera_number):
147 | allcameras = Window(10000).getProperty('SR_playingCamera_0')
148 | singlecamera = Window(10000).getProperty('SR_playingCamera_%s' %camera_number)
149 | if allcameras == '1' or singlecamera == '1':
150 | return True
151 | return False
152 |
153 |
154 | # Used to delay the next time the preview window is shown if it is manually dismissed
155 | def dismissAllPreviews(self):
156 | self.closeRequest('1')
157 | self.closeRequest('2')
158 | self.closeRequest('3')
159 | self.closeRequest('4')
160 | self.closeRequest('5')
161 | self.closeRequest('6')
162 |
163 |
164 | def dismissPreview(self, camera_number):
165 | dismissed_until = time() + self._dismissed_duration
166 | if self._dismissed_behavior == 0: # Dismiss All
167 | self.dismissed_time[0] = dismissed_until
168 | self.dismissAllPreviews()
169 | else: # Individual Only
170 | self.dismissed_time[int(camera_number)] = dismissed_until
171 |
172 | def clear_dismissedPreview(self, camera_number):
173 | self.dismissed_time[int(camera_number)] = 0
174 |
175 | def previewDismissed(self, camera_number):
176 | if self._dismissed_behavior == 0: # Dismiss All
177 | if self.dismissed_time[0] == 0:
178 | return False
179 | if time() > self.dismissed_time[0]:
180 | self.clear_dismissedPreview(0)
181 | return False
182 | else: # Individual Only
183 | if self.dismissed_time[int(camera_number)] == 0:
184 | return False
185 | if time() > self.dismissed_time[int(camera_number)]:
186 | self.clear_dismissedPreview(camera_number)
187 | return False
188 | return True
189 |
190 |
191 | # Used to determine if the window in focus is allowed to lose focus due to a preview window opening
192 | def checkWindowID(self):
193 | current_dialog_id = getCurrentWindowDialogId()
194 | current_window_id = getCurrentWindowId()
195 |
196 | for window_id in self._preview_disabled_window_id:
197 | if current_window_id == window_id or current_dialog_id == window_id:
198 | return True
199 | return False
200 |
201 |
202 | # Function that determines if a preview is allowed to be shown considering the global state
203 | def previewAllowed(self, camera_number):
204 | allowed = not (self.playingCamera(camera_number) or self.previewDismissed(camera_number) or self.checkWindowID() or self.toggledPreview())
205 | return ((not self.previewOpened(camera_number)) and allowed)
206 |
207 | #Request to test by @mrjd in forum
208 | def overrideURL(self, camera_number, url):
209 | Window(10000).setProperty('SR_urlOverride_%s' %camera_number, url)
210 |
211 | def clear_overrideURL(self, camera_number):
212 | Window(10000).clearProperty('SR_urlOverride_%s' %camera_number)
213 |
214 | def get_overrideURL(self, camera_number):
215 | return Window(10000).getProperty('SR_urlOverride_%s' %camera_number)
216 |
217 |
218 | def maybe_stop_current(self):
219 | """ If there is a video playing, it will capture the source and current playback time """
220 | if settings.getSetting_bool('resume'):
221 | player = xbmc.Player()
222 | if player.isPlaying():
223 | Window(10000).setProperty('SR_resumeTime', str(player.getTime()))
224 | Window(10000).setProperty('SR_previousFile', player.getPlayingFile())
225 | player.stop()
226 | xbmc.executebuiltin('PlayerControl(Stop)') # Because player.stop() was losing the player and didn't work *sad face*
227 |
228 | else:
229 | Window(10000).clearProperty('SR_resumeTime')
230 | Window(10000).clearProperty('SR_previousFile')
231 |
232 | def maybe_resume_previous(self):
233 | """ If a video was playing previously, it will restart it at the resume time """
234 | if settings.getSetting_bool('resume'):
235 | try:
236 | previous_file = Window(10000).getProperty('SR_previousFile')
237 | except:
238 | previous_file = ''
239 |
240 | if previous_file != '':
241 | resume_time = float(Window(10000).getProperty('SR_resumeTime'))
242 | Window(10000).clearProperty('SR_resumeTime')
243 | Window(10000).clearProperty('SR_previousFile')
244 |
245 | resume_time_adjustment = settings.getSetting_int('resume_time')
246 | resume_time_str = "{0:.1f}".format(resume_time - resume_time_adjustment)
247 | listitem = ListItem()
248 | listitem.setProperty('StartOffset', resume_time_str)
249 |
250 | player = xbmc.Player()
251 | player.play(previous_file, listitem)
252 |
253 | def resume_previous_file(self):
254 | if settings.getSetting_bool('resume') and Window(10000).getProperty('SR_previousFile') != '':
255 | return True
256 | return False
257 |
258 |
259 |
260 |
261 | if __name__ == "__main__":
262 | pass
263 |
--------------------------------------------------------------------------------
/resources/lib/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | This module is used to obtain add-on settings and camera settings
7 | """
8 |
9 | import xbmc, xbmcgui, xbmcaddon
10 | import utils
11 |
12 | __addon__ = xbmcaddon.Addon()
13 |
14 | '''
15 | # --> This should exist on all modules that log
16 | import inspect
17 | stck = inspect.stack()
18 | frm = stck[1]
19 | mod = inspect.getmodule(frm[0])
20 | mod1 = (str(mod).split("'"))
21 | mod2 = mod1[len(mod1)-2]
22 | mod3 = mod2.split(".")
23 | mod4 = mod3[len(mod3)-2]
24 | mod5 = mod4.split("\\")
25 | mod6 = mod5[len(mod5)-1]
26 |
27 | __logid__ = ('SETTINGS_{0}').format(mod6.upper())
28 |
29 | from logging import log
30 | # --
31 | '''
32 |
33 | INVALID_PASSWORD_CHARS = ('{', '}', ':', ';', '!', '?', '@', '\\', '/')
34 | INVALID_USER_CHARS = ('@',)
35 |
36 |
37 | ### Basic Addon Setting Controls ###
38 |
39 | def refreshAddonSettings():
40 | global __addon__
41 | __addon__ = xbmcaddon.Addon()
42 |
43 | def setSetting(setting, camera_number='', value=''):
44 | utils.log(2, 'SETTINGS :: %s%s Value Set as - %s' %(setting, camera_number, value))
45 | return __addon__.setSetting(setting + camera_number, value)
46 |
47 | def getSetting(setting, camera_number=''):
48 | return __addon__.getSetting(setting + camera_number)
49 |
50 | def getSetting_int(setting, camera_number=''):
51 | return int(__addon__.getSetting(setting + camera_number))
52 |
53 | def getSetting_bool(setting, camera_number=''):
54 | if 'true' in __addon__.getSetting(setting + camera_number):
55 | return True
56 | return False
57 |
58 | def getSetting_float(setting, camera_number=''):
59 | return float(__addon__.getSetting(setting + camera_number))
60 |
61 |
62 | ### Specialize setting Functions ###
63 |
64 | def enabled_camera(camera_number):
65 | return getSetting_bool('enabled', camera_number)
66 |
67 | def enabled_preview(camera_number):
68 | if enabled_camera(camera_number):
69 | return getSetting_bool('enabled_preview', camera_number)
70 | else:
71 | return False
72 |
73 | def atLeastOneCamera(cameras_to_check="123456"):
74 | for camera_number in cameras_to_check:
75 | if enabled_camera(camera_number):
76 | return True
77 | return False
78 |
79 | def getAllEnabledCameras(monitor):
80 | enabled_cameras = []
81 | for camera_number in "123456":
82 |
83 | enabled = enabled_camera(camera_number)
84 |
85 | if enabled:
86 | enabled_cameras.append(camera_number)
87 |
88 | return enabled_cameras
89 |
90 | def getCameraType(camera_number):
91 | return getSetting_int('type', camera_number)
92 |
93 | def getSupportedAlarms(camera_number):
94 | return getSetting_int('alarm', camera_number)
95 |
96 | def getEnabledAlarms(camera_number):
97 | motion_enabled = False
98 | sound_enabled = False
99 |
100 | supported = getSupportedAlarms(camera_number)
101 |
102 | if supported > 0:
103 | motion_enabled = getSetting_bool('motion', camera_number)
104 |
105 | if supported > 1:
106 | sound_enabled = getSetting_bool('sound', camera_number)
107 |
108 | return motion_enabled, sound_enabled
109 |
110 | def getCameraName(camera_number):
111 | name = getSetting('name', camera_number)
112 | if name == '':
113 | name = '%s' %utils.translation(32000 + int(camera_number))
114 | return name
115 |
116 | def getDisabledWindowIds():
117 | window_ids = []
118 | if 'true' in __addon__.getSetting('w_setting'):
119 | window_ids.extend([10140, 10004, 10011, 10012, 10013, 10014, 10015, 10016, 10017, 10018, 10019, 10021])
120 | if 'true' in __addon__.getSetting('w_context'):
121 | window_ids.extend([10106])
122 | if 'true' in __addon__.getSetting('w_home'):
123 | window_ids.extend([10000])
124 | if 'true' in __addon__.getSetting('w_library'):
125 | window_ids.extend([10001, 10002, 10003, 10005, 10006, 10025, 10028, 10040])
126 | if 'true' in __addon__.getSetting('w_sysinfo'):
127 | window_ids.extend([10007, 10100])
128 | if 'true' in __addon__.getSetting('w_keyboard'):
129 | window_ids.extend([10103, 10109, 10110])
130 | if 'true' in __addon__.getSetting('w_controls'):
131 | window_ids.extend([10114, 10115, 10120, 101222, 10123, 10124, 12901, 12902, 12903, 12904])
132 |
133 | other_window_ids = __addon__.getSetting('w_windowid')
134 | if other_window_ids != '':
135 | window_ids_list = other_window_ids.split(',')
136 | for window_id in window_ids_list:
137 | window_ids.extend(int(window_id))
138 |
139 | return window_ids
140 |
141 |
142 | ### Error Checking Support ###
143 |
144 | def invalid_char(credential, chars, stringid, show_dialog):
145 | for char in chars:
146 | if char in credential:
147 | if show_dialog:
148 | utils.dialog_ok(utils.translation(stringid), ' ', ' '.join(chars))
149 | return char
150 | return False
151 |
152 | def invalid_password_char(password, show_dialog=False):
153 | return invalid_char(password, INVALID_PASSWORD_CHARS, 32205, show_dialog)
154 |
155 | def invalid_user_char(user, show_dialog=False):
156 | return invalid_char(user, INVALID_USER_CHARS, 32206, show_dialog)
157 |
158 |
--------------------------------------------------------------------------------
/resources/lib/utils.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | Supporting functions that have no dependencies from the main add-on
7 | """
8 |
9 | import xbmc, xbmcaddon, xbmcvfs, xbmcgui
10 | import os, urllib, requests, sys
11 | import sqlite3 as lite
12 | import socket
13 |
14 |
15 | __addon__ = xbmcaddon.Addon()
16 | __addonid__ = __addon__.getAddonInfo('id')
17 | __version__ = __addon__.getAddonInfo('version')
18 | __icon__ = __addon__.getAddonInfo('icon').decode("utf-8")
19 | __path__ = xbmc.translatePath(('special://home/addons/{0}').format(__addonid__)).decode('utf-8')
20 | __data_path__ = xbmc.translatePath('special://profile/addon_data/%s' %__addonid__ ).decode('utf-8')
21 | __log_level__ = int(__addon__.getSetting('log_level'))
22 | __log_info__ = __addonid__ + ' v' + __version__ + ': '
23 | TIMEOUT = int(__addon__.getSetting('request_timeout'))
24 | socket.setdefaulttimeout(TIMEOUT)
25 |
26 | _atleast_python27 = False
27 | if '2.7.' in sys.version:
28 | _atleast_python27 = True
29 |
30 | # Makes sure folder path exists
31 | if not xbmcvfs.exists(__data_path__):
32 | try:
33 | xbmcvfs.mkdir(__data_path__)
34 | except:
35 | pass
36 |
37 | def log_level():
38 | global __addon__
39 | global __log_level__
40 | __addon__ = xbmcaddon.Addon()
41 | __log_level__ = int(__addon__.getSetting('log_level'))
42 |
43 | if __log_level__ == 0:
44 | return 'Off'
45 | elif __log_level__ == 1:
46 | return 'Normal'
47 | elif __log_level__ == 2:
48 | return 'Verbose'
49 | else:
50 | return 'Debug'
51 |
52 | def translation(id):
53 | return __addon__.getLocalizedString(id)
54 |
55 | def dialog_ok(msg):
56 | addon_name = translation(32000)
57 | xbmcgui.Dialog().ok(addon_name, msg)
58 |
59 | def notify(msg):
60 | if 'true' in __addon__.getSetting('notifications'):
61 | addon_name = translation(32000)
62 | xbmcgui.Dialog().notification(addon_name, msg, icon = __icon__)
63 |
64 | def log(level=4, value=''):
65 | msg = str(value)
66 | if level == 3: #Error
67 | xbmc.log(__log_info__ + '### ERROR ### : ' + msg, xbmc.LOGERROR)
68 |
69 | elif __log_level__ > 0 and level == 1: #Normal
70 | xbmc.log(__log_info__ + msg, xbmc.LOGNOTICE)
71 |
72 | elif __log_level__ > 1 and level == 2: #Verbose
73 | xbmc.log(__log_info__ + msg, xbmc.LOGNOTICE)
74 |
75 | elif __log_level__ > 2 and level == 4: #DEBUG
76 | xbmc.log(__log_info__ + msg, xbmc.LOGNOTICE)
77 |
78 | def cleanup_images():
79 | """ Final Cleanup of images when Kodi shuts down """
80 |
81 | for i in xbmcvfs.listdir(__data_path__)[1]:
82 | if (i <> 'settings.xml') and (not 'fanart_camera' in i):
83 | xbmcvfs.delete(os.path.join(__data_path__, i))
84 | log(4, 'CLEANUP IMAGES :: %s' %i)
85 |
86 | def remove_leftover_images(filename_prefix):
87 | """ Attempts to remove leftover images after player stops """
88 | xbmc.sleep(1000)
89 | for i in xbmcvfs.listdir(__data_path__)[1]:
90 | if filename_prefix in i:
91 | xbmcvfs.delete(os.path.join(__data_path__, i))
92 | log(4, 'CLEANUP IMAGES :: %s' %i)
93 |
94 |
95 | def remove_cached_art(art):
96 | """ Removes cached art from textures database and cached folder """
97 |
98 | _db_path = xbmc.translatePath('special://home/userdata/Database').decode('utf-8')
99 | _tbn_path = xbmc.translatePath('special://home/userdata/Thumbnails').decode('utf-8')
100 | db = None
101 |
102 | try:
103 | db = lite.connect(os.path.join(_db_path, 'Textures13.db'))
104 | db = db.cursor()
105 |
106 | #Get cached image name to remove
107 | db.execute("SELECT cachedurl FROM texture WHERE url = '%s';" %art)
108 | data = db.fetchone()
109 |
110 | try:
111 |
112 | log(4, 'Removing Cached Art :: SQL Output: %s' %data[0])
113 | file_to_delete = os.path.join(_tbn_path, data[0])
114 | log(4, 'Removing Cached Art :: File to be removed: %s' %file_to_delete)
115 |
116 | xbmcvfs.delete(file_to_delete)
117 | db.execute("DELETE FROM texture WHERE url = '%s';" %art)
118 |
119 | except:
120 | pass
121 |
122 | except lite.Error, e:
123 | log(3, "Error %s:" %e.args[0])
124 | #sys.exit(1)
125 |
126 | finally:
127 | if db:
128 | db.close()
129 |
130 | try:
131 | log(4, 'Removing Original Artwork if Exists :: File to be removed: %s' %art)
132 | xbmcvfs.delete(art)
133 |
134 | except:
135 | pass
136 |
137 | def get_icon(name_or_number):
138 | """ Determines which icon to display """
139 | #Copied from api_camera_wrapper.py
140 | FOSCAM_HD = 0
141 | FOSCAM_SD = 1
142 | FOSCAM_HD_OVERRIDE = 2
143 | GENERIC_IPCAM = 3
144 |
145 | if name_or_number == 'default':
146 | icon = os.path.join(__path__, 'icon.png')
147 |
148 | elif name_or_number == 'settings':
149 | icon = os.path.join(__path__, 'resources', 'media', 'icon-settings.png')
150 |
151 | elif name_or_number == 'advanced':
152 | icon = os.path.join(__path__, 'resources', 'media', 'icon-advanced-menu.png')
153 |
154 | else:
155 | camera_type = int(__addon__.getSetting('type%s' %name_or_number))
156 | ptz = int(__addon__.getSetting('ptz%s' %name_or_number))
157 |
158 | if camera_type == FOSCAM_HD or camera_type == FOSCAM_HD_OVERRIDE:
159 | if ptz > 0:
160 | icon = os.path.join(__path__, 'resources', 'media', 'icon-foscam-hd-ptz.png')
161 | else:
162 | icon = os.path.join(__path__, 'resources', 'media', 'icon-foscam-hd.png')
163 |
164 | elif camera_type == FOSCAM_SD:
165 | if ptz > 0:
166 | icon = os.path.join(__path__, 'resources', 'media', 'icon-foscam-sd-ptz.png')
167 | else:
168 | icon = os.path.join(__path__, 'resources', 'media', 'icon-foscam-sd.png')
169 |
170 | else:
171 | icon = os.path.join(__path__, 'resources', 'media', 'icon-generic.png')
172 |
173 | return icon
174 |
175 | def get_fanart(name_or_number, new_art_url = None, update = False):
176 | """ Determines which fanart to show """
177 | if str(name_or_number) == 'default':
178 | fanart = os.path.join(__path__, 'fanart.jpg')
179 |
180 | else:
181 | fanart = os.path.join(__data_path__,'fanart_camera' + str(name_or_number) + '.jpg')
182 |
183 | if __addon__.getSetting('fanart') == 0 or update == True:
184 | remove_cached_art(fanart)
185 |
186 | if not xbmcvfs.exists(fanart) and new_art_url != None:
187 | try:
188 | log(4, 'Retrieving new Fanart for camera %s : %s' %(name_or_number, new_art_url))
189 | urllib.urlretrieve(new_art_url, fanart)
190 | except:
191 | log(4, 'Failed to Retrieve Snapshot from camera %s.' %name_or_number)
192 | fanart = os.path.join(__path__, 'fanart.jpg')
193 |
194 | return fanart
195 |
196 | def get_mjpeg_frame(stream, filename):
197 | """ Extracts JPEG image from MJPEG """
198 |
199 | line = ''
200 | try:
201 | x = 0
202 | while not 'length' in line.lower():
203 | if '500 - Internal Server Error' in line or x > 10:
204 | return False
205 | #log(4, 'GETMJPEGFRAME: %s' %line)
206 | line = stream.readline()
207 | x += 1
208 |
209 |
210 | bytes = int(line.split(':')[-1])
211 |
212 | while len(line) > 3:
213 | line = stream.readline()
214 |
215 | frame = stream.read(bytes)
216 |
217 | except requests.RequestException as e:
218 | log(3, str(e))
219 | return False
220 |
221 | if frame:
222 | with open(filename, 'wb') as jpeg_file:
223 | jpeg_file.write(frame)
224 |
225 | return True
226 |
227 |
228 |
229 |
230 |
231 |
--------------------------------------------------------------------------------
/resources/media/addon_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/addon_settings.png
--------------------------------------------------------------------------------
/resources/media/addon_settings_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/addon_settings_nofocus.png
--------------------------------------------------------------------------------
/resources/media/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/back.png
--------------------------------------------------------------------------------
/resources/media/black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/black.png
--------------------------------------------------------------------------------
/resources/media/bottom_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/bottom_left.png
--------------------------------------------------------------------------------
/resources/media/bottom_left_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/bottom_left_nofocus.png
--------------------------------------------------------------------------------
/resources/media/bottom_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/bottom_right.png
--------------------------------------------------------------------------------
/resources/media/bottom_right_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/bottom_right_nofocus.png
--------------------------------------------------------------------------------
/resources/media/camera_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/camera_settings.png
--------------------------------------------------------------------------------
/resources/media/camera_settings_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/camera_settings_nofocus.png
--------------------------------------------------------------------------------
/resources/media/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/close.png
--------------------------------------------------------------------------------
/resources/media/close_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/close_nofocus.png
--------------------------------------------------------------------------------
/resources/media/down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/down.png
--------------------------------------------------------------------------------
/resources/media/down_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/down_nofocus.png
--------------------------------------------------------------------------------
/resources/media/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/error.png
--------------------------------------------------------------------------------
/resources/media/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/home.png
--------------------------------------------------------------------------------
/resources/media/home_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/home_nofocus.png
--------------------------------------------------------------------------------
/resources/media/icon-advanced-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-advanced-menu.png
--------------------------------------------------------------------------------
/resources/media/icon-foscam-hd-ptz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-foscam-hd-ptz.png
--------------------------------------------------------------------------------
/resources/media/icon-foscam-hd-ptz_old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-foscam-hd-ptz_old.png
--------------------------------------------------------------------------------
/resources/media/icon-foscam-hd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-foscam-hd.png
--------------------------------------------------------------------------------
/resources/media/icon-foscam-hd_old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-foscam-hd_old.png
--------------------------------------------------------------------------------
/resources/media/icon-foscam-sd-ptz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-foscam-sd-ptz.png
--------------------------------------------------------------------------------
/resources/media/icon-foscam-sd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-foscam-sd.png
--------------------------------------------------------------------------------
/resources/media/icon-generic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-generic.png
--------------------------------------------------------------------------------
/resources/media/icon-settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/icon-settings.png
--------------------------------------------------------------------------------
/resources/media/left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/left.png
--------------------------------------------------------------------------------
/resources/media/left_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/left_down.png
--------------------------------------------------------------------------------
/resources/media/left_down_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/left_down_nofocus.png
--------------------------------------------------------------------------------
/resources/media/left_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/left_nofocus.png
--------------------------------------------------------------------------------
/resources/media/left_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/left_up.png
--------------------------------------------------------------------------------
/resources/media/left_up_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/left_up_nofocus.png
--------------------------------------------------------------------------------
/resources/media/loader_old.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/loader_old.gif
--------------------------------------------------------------------------------
/resources/media/placeholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/placeholder.jpg
--------------------------------------------------------------------------------
/resources/media/radio-off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/radio-off.png
--------------------------------------------------------------------------------
/resources/media/radio-on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/radio-on.png
--------------------------------------------------------------------------------
/resources/media/right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/right.png
--------------------------------------------------------------------------------
/resources/media/right_down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/right_down.png
--------------------------------------------------------------------------------
/resources/media/right_down_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/right_down_nofocus.png
--------------------------------------------------------------------------------
/resources/media/right_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/right_nofocus.png
--------------------------------------------------------------------------------
/resources/media/right_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/right_up.png
--------------------------------------------------------------------------------
/resources/media/right_up_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/right_up_nofocus.png
--------------------------------------------------------------------------------
/resources/media/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/settings.png
--------------------------------------------------------------------------------
/resources/media/settings_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/settings_nofocus.png
--------------------------------------------------------------------------------
/resources/media/top_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/top_left.png
--------------------------------------------------------------------------------
/resources/media/top_left_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/top_left_nofocus.png
--------------------------------------------------------------------------------
/resources/media/top_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/top_right.png
--------------------------------------------------------------------------------
/resources/media/top_right_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/top_right_nofocus.png
--------------------------------------------------------------------------------
/resources/media/trans.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/trans.png
--------------------------------------------------------------------------------
/resources/media/up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/up.png
--------------------------------------------------------------------------------
/resources/media/up_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/up_nofocus.png
--------------------------------------------------------------------------------
/resources/media/zoom_in.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/zoom_in.png
--------------------------------------------------------------------------------
/resources/media/zoom_in_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/zoom_in_nofocus.png
--------------------------------------------------------------------------------
/resources/media/zoom_out.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/zoom_out.png
--------------------------------------------------------------------------------
/resources/media/zoom_out_nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/media/zoom_out_nofocus.png
--------------------------------------------------------------------------------
/resources/settings.xml:
--------------------------------------------------------------------------------
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 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
--------------------------------------------------------------------------------
/resources/textures/AddonWindow/ContentPanel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/AddonWindow/ContentPanel.png
--------------------------------------------------------------------------------
/resources/textures/AddonWindow/DialogCloseButton-focus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/AddonWindow/DialogCloseButton-focus.png
--------------------------------------------------------------------------------
/resources/textures/AddonWindow/DialogCloseButton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/AddonWindow/DialogCloseButton.png
--------------------------------------------------------------------------------
/resources/textures/AddonWindow/SKINDEFAULT.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/AddonWindow/SKINDEFAULT.jpg
--------------------------------------------------------------------------------
/resources/textures/AddonWindow/dialogheader.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/AddonWindow/dialogheader.png
--------------------------------------------------------------------------------
/resources/textures/Button/KeyboardKey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/Button/KeyboardKey.png
--------------------------------------------------------------------------------
/resources/textures/Button/KeyboardKeyNF.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/Button/KeyboardKeyNF.png
--------------------------------------------------------------------------------
/resources/textures/RadioButton/MenuItemFO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/RadioButton/MenuItemFO.png
--------------------------------------------------------------------------------
/resources/textures/RadioButton/MenuItemNF.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/RadioButton/MenuItemNF.png
--------------------------------------------------------------------------------
/resources/textures/RadioButton/radiobutton-focus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/RadioButton/radiobutton-focus.png
--------------------------------------------------------------------------------
/resources/textures/RadioButton/radiobutton-nofocus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/maikito26/plugin.video.surveillanceroom/1c40c1901605a2ded70eba0ce9f665fdfd530549/resources/textures/RadioButton/radiobutton-nofocus.png
--------------------------------------------------------------------------------
/service.py:
--------------------------------------------------------------------------------
1 | """
2 | plugin.video.surveillanceroom
3 |
4 | A Kodi add-on by Maikito26
5 |
6 | Service loop which enables preview window capability
7 | """
8 |
9 | import xbmc, xbmcaddon, xbmcvfs
10 | import threading, time, os, Queue, sys
11 | from resources.lib import monitor, camerapreview, settings, utils
12 | from resources.lib.ipcam_api_wrapper import CameraAPIWrapper as Camera
13 |
14 | __addon__ = xbmcaddon.Addon()
15 | __addonid__ = __addon__.getAddonInfo('id')
16 |
17 |
18 | class CameraPreviewThread(threading.Thread):
19 | """
20 | This class is a stoppable thread. It controls the entire process for a single camera.
21 | Each camera will check the status of itself if it is playing in the main player. It calls an image
22 | worker thread to update it's image
23 | """
24 |
25 | def __init__(self, camera, monitor):
26 | super(CameraPreviewThread, self).__init__()
27 | self._stop = False
28 | self.camera = camera
29 | self.monitor = monitor
30 |
31 | def run(self):
32 | """ This runs the main loop for the camera """
33 |
34 | #Reset PTZ Camera on Service Start
35 | self.camera.resetLocation()
36 |
37 | # Settings
38 | check_interval = settings.getSetting_int('interval', self.camera.number)
39 | cond_service = settings.getSetting_int('cond_service', self.camera.number)
40 | self.motion_enabled, self.sound_enabled = settings.getEnabledAlarms(self.camera.number)
41 | trigger_interval = self.camera.getTriggerInterval(self.motion_enabled, self.sound_enabled)
42 |
43 | if trigger_interval < 1: # Fix for Foscam Cameras that apparently get the data messed up
44 | trigger_interval = check_interval
45 |
46 |
47 | ### MAIN LOOP ###
48 | while not self.monitor.abortRequested() and not self.monitor.stopped():
49 |
50 | alarmActive = self.alarmStateHealthCheck()
51 |
52 | #PREVIEW WINDOW IS CURRENTLY CLOSED
53 | if self.monitor.previewAllowed(self.camera.number):
54 |
55 | #Open Condition: Alarm is Detected
56 | if alarmActive:
57 | self.monitor.openRequest(self.camera.number)
58 | utils.log(2, 'Camera %s :: Alarm is detected. Preview window is opening' %self.camera.number)
59 |
60 |
61 | #PREVIEW WINDOW IS OPEN
62 | elif self.monitor.previewOpened(self.camera.number):
63 |
64 | #Close Condition: No Alarm is Detected
65 | if cond_service == camerapreview.CONDITION_NO_ALARM and not self.monitor.openRequest_manual(self.camera.number):
66 | if not alarmActive:
67 | self.monitor.closeRequest(self.camera.number)
68 | utils.log(2, 'Camera %s :: The alarm is no longer detected. The preview window will close.' %self.camera.number)
69 |
70 |
71 | # Sleep Logic
72 | if not alarmActive:
73 | sleep = check_interval
74 | else:
75 | sleep = trigger_interval - 1
76 |
77 | #print '%s, %s, %d, %d, %d, %s' %(self.camera.number, self.monitor.previewOpened(self.camera.number), sleep, check_interval, trigger_interval, alarmActive)
78 | self.monitor.waitForAbort(sleep)
79 | ### /MAIN LOOP ###
80 |
81 |
82 | if self.monitor.previewOpened(self.camera.number):
83 | self.monitor.closeRequest(self.camera.number)
84 |
85 | utils.log(1, 'Camera %s :: **SERVICE SHUTDOWN** :: Thread Stopped.' %self.camera.number)
86 |
87 |
88 |
89 |
90 | def alarmStateHealthCheck(self):
91 | """ Function to determine state of alarms on cameras, and also connectivity health of camera """
92 |
93 | # Non-alarm enabled or Generic IP Cameras return this
94 | if not self.motion_enabled and not self.sound_enabled:
95 | return False
96 |
97 | alarmActive = False
98 | success_code, alarmActive, alarm = self.camera.is_alarm_active(self.motion_enabled, self.sound_enabled)
99 |
100 | ### Health Check code for Foscam Camera ###
101 | if success_code != 0:
102 |
103 | #Timeout is ~20 seconds before determining camera is not connected
104 | for x in range(1,2):
105 |
106 | utils.log(2, 'Camera %s :: SERVICE HEALTH CHECK :: Did not receive response 0, received response %d. Retry # %d in 5 seconds' %(self.camera.number, success_code, x))
107 | self.monitor.waitForAbort(5)
108 | success_code, alarmActive, alarm = self.camera.is_alarm_active(self.motion_enabled, self.sound_enabled)
109 |
110 | if success_code == 0:
111 | break
112 |
113 | #Camera is not connected, so notify the user
114 | if success_code != 0:
115 |
116 | self.monitor.closeRequest(self.camera.number)
117 | utils.notify(utils.translation(32222) %self.camera.number)
118 | self.monitor.write_testResult(self.camera.number, success_code)
119 |
120 | #Loop to keep retrying the connection ever 60 seconds
121 | x = 0
122 | while success_code != 0:
123 | if self.monitor.abortRequested() or self.monitor.stopped():
124 | return False
125 |
126 | if x > 60:
127 | x = 0
128 | utils.log(3, 'Camera %s :: SERVICE HEALTH CHECK :: Did not receive response 0, received response %d. Retrying every 60 seconds.' %(self.camera.number, success_code))
129 | success_code, alarmActive, alarm = self.camera.is_alarm_active(self.motion_enabled, self.sound_enabled)
130 |
131 | self.monitor.waitForAbort(1)
132 | x += 1
133 |
134 | utils.notify(utils.translation(32223) %self.camera.number)
135 | self.monitor.write_testResult(self.camera.number, success_code)
136 |
137 | #Reset PTZ Camera on Service Start
138 | self.camera.resetLocation()
139 |
140 | ### End of Health Check code for Foscam HD camera ###
141 |
142 |
143 | if alarmActive:
144 | self.monitor.set_alarmActive(self.camera.number)
145 | utils.log(2, 'Camera %s :: Alarm detected: (%sed).' %(self.camera.number, alarm))
146 | return True
147 |
148 | self.monitor.clear_alarmActive(self.camera.number)
149 | return False
150 |
151 |
152 |
153 | class service():
154 | """
155 | This is the main service loop which controls the entire process globally,
156 | and creates all of the threads.
157 | """
158 |
159 | def run(self, monitor):
160 | self.monitor = monitor
161 | self.monitor.reset()
162 | preview_enabled_cameras = []
163 | self.threads = []
164 |
165 | for camera_number in "123456":
166 |
167 | utils.log(2, 'Camera %s :: Enabled: %s; Preview Enabled: %s' %(camera_number, settings.enabled_camera(camera_number), settings.enabled_preview(camera_number)))
168 | if settings.enabled_camera(camera_number):
169 | camera = Camera(camera_number)
170 |
171 | if settings.enabled_preview(camera_number):
172 |
173 | if camera.Connected(self.monitor, useCache=False):
174 |
175 | previewWindow = threading.Thread(target = camerapreview.CameraPreviewWindow, args = (camera, self.monitor, ))
176 | previewWindow.daemon = True
177 | previewWindow.start()
178 |
179 | t = CameraPreviewThread(camera, self.monitor, )
180 | t.daemon = True
181 | self.threads.append(t)
182 | t.start()
183 |
184 | utils.log(1, 'Camera %s :: Preview Thread started.' %camera_number)
185 |
186 | else:
187 | utils.log(1, 'Camera %s :: Preview thread did not start because camera is not properly configured.' %camera_number)
188 | utils.notify('Error Connecting to Camera %s.' %camera_number)
189 |
190 | utils.notify(utils.translation(32224)) #Service Started
191 |
192 | xbmc.executebuiltin('Container.Refresh')
193 |
194 | while not self.monitor.stopped() and not self.monitor.abortRequested():
195 | self.monitor.waitForAbort(1)
196 |
197 | if self.monitor.stopped() and not self.monitor.abortRequested():
198 | utils.notify(utils.translation(32225)) #Service Restarting
199 | self.restart()
200 |
201 | '''
202 | else:
203 | utils.notify('Service stopped.')
204 | self.stop()
205 | '''
206 |
207 |
208 | def restart(self):
209 | self.stop()
210 | self.monitor.waitForAbort(2)
211 | start()
212 |
213 | def stop(self):
214 | for t in self.threads:
215 | t.join()
216 |
217 |
218 |
219 | def start():
220 | """
221 | Function which starts the service. Called on Kodi login as well as when restarted due to settings changes
222 | """
223 |
224 | settings.refreshAddonSettings()
225 | utils.log(1, 'SERVICE :: **START**')
226 | utils.log(1, 'SERVICE :: Log Level: %s' %utils.log_level())
227 | utils.log(1, 'SERVICE :: Python Version: %s; At Least 2.7: %s' %(sys.version, utils._atleast_python27))
228 | instance = service()
229 | instance.run(monitor)
230 |
231 |
232 | if __name__ == "__main__":
233 | monitor = monitor.AddonMonitor()
234 | start()
235 | monitor.waitForAbort(1)
236 | utils.cleanup_images()
237 |
238 |
239 |
240 |
241 |
--------------------------------------------------------------------------------