├── 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 | --------------------------------------------------------------------------------