├── .gitignore ├── LICENSE ├── README.md └── python └── osc ├── __init__.py ├── bubl.py ├── gear360_2017.py ├── osc.py └── theta.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Haarm-Pieter Duiker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenSphericalCamera Client 2 | = 3 | 4 | A Python library for interfacing with cameras that implement the [OpenSphericalCamera](https://developers.google.com/streetview/open-spherical-camera/) API 5 | 6 | Supported Cameras 7 | - 8 | 9 | - [Ricoh Theta S](https://developers.theta360.com/en/) 10 | - [Samsung Gear 360 (2017)](https://www.samsung.com/global/galaxy/gear-360/) 11 | - Untested OSC cameras 12 | - [BublCam](http://www.bublcam.com/) 13 | - [Giroptic](http://us.360.tv/en/) 14 | - [360fly](https://360fly.com/) 15 | 16 | Usage 17 | - 18 | 19 | Usage of the pure OSC API 20 | 21 | ```python 22 | from osc.osc import * 23 | 24 | # Initializing the class starts a session 25 | camera = OpenSphericalCamera() 26 | camera.state() 27 | camera.info() 28 | 29 | # Only need to call this if there was a problem 30 | # when 'camera' was created 31 | camera.startSession() 32 | 33 | # Capture image 34 | response = camera.takePicture() 35 | 36 | # Wait for the stitching to finish 37 | camera.waitForProcessing(response['id']) 38 | 39 | # Copy image to computer 40 | camera.getLatestImage() 41 | 42 | # Close the session 43 | camera.closeSession() 44 | ``` 45 | 46 | Usage of the Ricoh Theta S extended API 47 | 48 | ```python 49 | from osc.theta import RicohThetaS 50 | 51 | thetas = RicohThetaS() 52 | thetas.state() 53 | thetas.info() 54 | 55 | # Capture image 56 | thetas.setCaptureMode( 'image' ) 57 | response = thetas.takePicture() 58 | 59 | # Wait for the stitching to finish 60 | thetas.waitForProcessing(response['id']) 61 | 62 | # Copy image to computer 63 | thetas.getLatestImage() 64 | 65 | # Stream the livePreview video to disk 66 | # for 3 seconds 67 | thetas.getLivePreview(timeLimitSeconds=3) 68 | 69 | # Capture video 70 | thetas.setCaptureMode( '_video' ) 71 | thetas.startCapture() 72 | thetas.stopCapture() 73 | 74 | # Copy video to computer 75 | thetas.getLatestVideo() 76 | 77 | # Close the session 78 | thetas.closeSession() 79 | ``` 80 | 81 | Notes 82 | - 83 | The BublOscClient.js client is the only documentation I can find for the Bublcam custom commands. The client was not tested with actual hardware. 84 | 85 | Requirements 86 | - 87 | The Python [Requests](http://docs.python-requests.org/en/master/) library is used to manage all calls to the OSC API. 88 | 89 | Install requests with pip: 90 | ``` 91 | pip install requests 92 | ``` 93 | 94 | References 95 | - 96 | 97 | - [RICOH THETA API v2](https://developers.theta360.com/en/docs/v2/api_reference/) 98 | 99 | - [RICOH THETA API v2 Overview](https://developers.theta360.com/en/docs/introduction/) 100 | 101 | - [Ricoh Developer Forums](https://developers.theta360.com/en/forums/) 102 | 103 | - [Unofficial and Unauthorized THETA S Hacking Guide.](https://codetricity.github.io/theta-s/index.html) 104 | 105 | - [Github with experiments supporting the Ricoh Theta](https://github.com/codetricity/theta-s-api-tests) 106 | 107 | - [Github site with links to a number of Ricoh related projects](https://github.com/theta360developers) 108 | 109 | - [BublCam Javascript API tests](https://github.com/BublTechnology/ScarletTests) 110 | 111 | - [BublCam OSC client](https://github.com/BublTechnology/osc-client) 112 | 113 | - [Alternate OSC Python library](https://github.com/florianl/pyOSCapi) 114 | 115 | - [Samsung Gear 360 (2016) OSC Python library](https://github.com/baardove/osc) 116 | 117 | Thanks 118 | ------ 119 | Many thanks to Craig Oda, the author and maintainer of [Theta S API Tests](https://github.com/codetricity/theta-s-api-tests) repo. 120 | 121 | Author 122 | ------ 123 | The original author of this library is: 124 | 125 | - Haarm-Pieter Duiker 126 | 127 | Testing 128 | - 129 | 130 | This library was tested with a Ricoh Theta S using Python 2.7 on OSX Yosemite, and a Samsung Gear 360 (2017) on macOS High Sierra. 131 | 132 | License 133 | - 134 | 135 | Copyright (c) 2016 Haarm-Pieter Duiker 136 | 137 | See the LICENSE file in this repo 138 | 139 | 140 | -------------------------------------------------------------------------------- /python/osc/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'Haarm-Pieter Duiker' 5 | __copyright__ = 'Copyright (C) 2016 - Duiker Research Corp' 6 | __license__ = '' 7 | __maintainer__ = 'Haarm-Pieter Duiker' 8 | __email__ = 'support@duikerresearch.org' 9 | __status__ = 'Production' 10 | 11 | __major_version__ = '1' 12 | __minor_version__ = '0' 13 | __change_version__ = '0' 14 | __version__ = '.'.join((__major_version__, 15 | __minor_version__, 16 | __change_version__)) 17 | 18 | from osc import OpenSphericalCamera 19 | from theta import RicohThetaS 20 | from bubl import Bublcam 21 | 22 | -------------------------------------------------------------------------------- /python/osc/bubl.py: -------------------------------------------------------------------------------- 1 | """ 2 | ********************* 3 | ********************* 4 | ********************* 5 | This code has not been tested with a BublCam. 6 | The BublOscClient.js client is the only documentation I can find for the 7 | custom commands. 8 | ********************* 9 | ********************* 10 | ********************* 11 | """ 12 | 13 | """ 14 | Extensions to the Open Spherical Camera API specific to the Bubl Cam. 15 | Documentation / Examples here: 16 | https://bubltechnology.github.io/ScarletTests/ 17 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js 18 | 19 | Open Spherical Camera API proposed here: 20 | https://developers.google.com/streetview/open-spherical-camera/reference 21 | 22 | The library is an evolution of: 23 | https://github.com/codetricity/theta-s-api-tests/blob/master/thetapylib.py 24 | 25 | Usage: 26 | At the top of your Python script, use 27 | 28 | from osc.bubl import Bublcam 29 | 30 | After you import the library, you can use the commands like this: 31 | 32 | bublcam = Bublcam() 33 | bublcam.state() 34 | bublcam.info() 35 | 36 | # Capture image 37 | response = bublcam.takePicture() 38 | 39 | # Wait for the stitching to finish 40 | bublcam.waitForProcessing(response['id']) 41 | 42 | # Copy image to computer 43 | bublcam.getLatestImage() 44 | 45 | # Capture video 46 | response = bublcam.captureVideo() 47 | bublcam.stop(response['id']) 48 | 49 | # Get the port and end point for live streaming 50 | (bublStreamPort, bublStreamEndPoint) = bublcam.stream() 51 | 52 | # For rtsp video streaming, open the following URI 53 | # rtsp streaming not implemented here. 54 | rtspUri = "rtsp://%s:%s/%s" % (bubl._ip, bublStreamPort, bublStreamEndPoint) 55 | 56 | # Turn the camera off in 30 seconds 57 | bublcam.shutdown(30) 58 | """ 59 | 60 | import json 61 | import requests 62 | import timeit 63 | 64 | import osc 65 | 66 | __author__ = 'Haarm-Pieter Duiker' 67 | __copyright__ = 'Copyright (C) 2016 - Duiker Research Corp' 68 | __license__ = '' 69 | __maintainer__ = 'Haarm-Pieter Duiker' 70 | __email__ = 'support@duikerresearch.org' 71 | __status__ = 'Production' 72 | 73 | __major_version__ = '1' 74 | __minor_version__ = '0' 75 | __change_version__ = '0' 76 | __version__ = '.'.join((__major_version__, 77 | __minor_version__, 78 | __change_version__)) 79 | 80 | __all__ = ['Bublcam'] 81 | 82 | # 83 | # Bubl cam 84 | # 85 | class Bublcam(osc.OpenSphericalCamera): 86 | 87 | def __init__(self, ip_base="192.168.0.100", httpPort=80): 88 | osc.OpenSphericalCamera.__init__(self, ip_base, httpPort) 89 | 90 | def updateFirmware(self, firmwareFilename): 91 | """ 92 | _bublUpdate 93 | 94 | Update the camera firmware 95 | 96 | Reference: 97 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L25 98 | """ 99 | url = self._request("_bublUpdate") 100 | with open(firmwareFilename, 'rb') as handle: 101 | body = handle.read() 102 | 103 | try: 104 | req = requests.post(url, data=body, 105 | headers={'Content-Type': 'application/octet-stream'}) 106 | except Exception, e: 107 | self._httpError(e) 108 | return None 109 | 110 | if req.status_code == 200: 111 | response = req.json() 112 | else: 113 | self._oscError(req) 114 | response = None 115 | return response 116 | 117 | def bublGetImage(self, fileUri): 118 | """ 119 | _bublGetImage 120 | 121 | Transfer the file from the camera to computer and save the 122 | binary data to local storage. This works, but is clunky. 123 | There are easier ways to do this. 124 | 125 | Not currently applying the equivalent of Javascript's encodeURIComponent 126 | to the fileUri 127 | 128 | Reference: 129 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L31 130 | """ 131 | acquired = False 132 | if fileUri: 133 | url = self._request("_bublGetImage/%s" % fileUri) 134 | fileName = fileUri.split("/")[1] 135 | 136 | try: 137 | response = requests.get(url, stream=True) 138 | except Exception, e: 139 | self._httpError(e) 140 | return acquired 141 | 142 | if response.status_code == 200: 143 | with open(fileName, 'wb') as handle: 144 | for block in response.iter_content(1024): 145 | handle.write(block) 146 | acquired = True 147 | else: 148 | self._oscError(req) 149 | 150 | return acquired 151 | 152 | def stop(self, commandId): 153 | """ 154 | _bublStop 155 | 156 | Reference: 157 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L37 158 | """ 159 | url = self._request("commands/_bublStop") 160 | body = json.dumps({ 161 | "id": commandId 162 | }) 163 | try: 164 | req = requests.post(url, data=body) 165 | except Exception, e: 166 | self._httpError(e) 167 | return None 168 | 169 | if req.status_code == 200: 170 | response = req.json() 171 | else: 172 | self._oscError(req) 173 | response = None 174 | return response 175 | 176 | def poll(self, commandId, fingerprint, waitTimeout): 177 | """ 178 | _bublPoll 179 | 180 | Reference: 181 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L43 182 | """ 183 | url = self._request("commands/_bublPoll") 184 | body = json.dumps({ 185 | "id": commandId, 186 | "fingerprint" : fingerprint, 187 | "waitTimeout" : waitTimeout 188 | }) 189 | try: 190 | req = requests.post(url, data=body) 191 | except Exception, e: 192 | self._httpError(e) 193 | return None 194 | 195 | if req.status_code == 200: 196 | response = req.json() 197 | else: 198 | self._oscError(req) 199 | response = None 200 | return response 201 | 202 | def captureVideo(self): 203 | """ 204 | _bublCaptureVideo 205 | 206 | Reference: 207 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L49 208 | """ 209 | url = self._request("commands/execute") 210 | body = json.dumps({"name": "camera._bublCaptureVideo", 211 | "parameters": { 212 | "sessionId": self.sid 213 | } 214 | }) 215 | try: 216 | req = requests.post(url, data=body) 217 | except Exception, e: 218 | self._httpError(e) 219 | return None 220 | 221 | if req.status_code == 200: 222 | response = req.json() 223 | else: 224 | self._oscError(req) 225 | response = None 226 | return response 227 | 228 | def shutdown(self, shutdownDelay): 229 | """ 230 | _bublShutdown 231 | 232 | Reference: 233 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L64 234 | """ 235 | url = self._request("commands/execute") 236 | body = json.dumps({"name": "camera._bublShutdown", 237 | "parameters": { 238 | "sessionId": self.sid, 239 | "shutdownDelay" : shutdownDelay 240 | } 241 | }) 242 | try: 243 | req = requests.post(url, data=body) 244 | except Exception, e: 245 | self._httpError(e) 246 | return None 247 | 248 | if req.status_code == 200: 249 | response = req.json() 250 | else: 251 | self._oscError(req) 252 | response = None 253 | return response 254 | 255 | def stream(self): 256 | """ 257 | _bublStream 258 | 259 | Return the port and end point to use for rtsp video streaming 260 | 261 | Reference: 262 | https://github.com/BublTechnology/osc-client/blob/master/lib/BublOscClient.js#L59 263 | """ 264 | acquired = False 265 | 266 | url = self._request("commands/execute") 267 | body = json.dumps({"name": "camera._bublStream", 268 | "parameters": { 269 | "sessionId": self.sid 270 | }}) 271 | 272 | try: 273 | response = requests.post(url, data=body, stream=True) 274 | except Exception, e: 275 | self._httpError(e) 276 | return acquired 277 | 278 | if response.status_code == 200: 279 | response = req.json() 280 | bublStreamPort = response['_bublStreamPort'] 281 | bublStreamEndPoint = response['_bublStreamEndPoint'] 282 | else: 283 | bublStreamPort = None 284 | bublStreamEndPoint = None 285 | self._oscError(response) 286 | 287 | return (bublStreamPort, bublStreamEndPoint) 288 | # Bublcam 289 | 290 | 291 | 292 | -------------------------------------------------------------------------------- /python/osc/gear360_2017.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extensions to the Open Spherical Camera API specific to the Gear 360 camera (2017). 3 | 4 | Usage: 5 | At the top of your Python script, use 6 | 7 | from osc.gear360_2017 import Gear360_2017 8 | 9 | After you import the library, you can use the commands like this: 10 | 11 | gear360 = Gear360_2017() 12 | gear360.state() 13 | gear360.info() 14 | 15 | # Capture image 16 | response = gear360.takePicture() 17 | 18 | # Wait for the stitching to finish 19 | gear360.waitForProcessing(response['id']) 20 | 21 | # Copy image to computer 22 | gear360.getLatestImage() 23 | 24 | gear360.closeSession() 25 | """ 26 | 27 | import osc 28 | 29 | class Gear360_2017(osc.OpenSphericalCamera): 30 | 31 | # Instance variables / methods 32 | def __init__(self, ip_base="192.168.43.1", httpPort=80): 33 | self.sid = None 34 | self.fingerprint = None 35 | self._api = None 36 | 37 | self._ip = ip_base 38 | self._httpPort = httpPort 39 | self._httpUpdatesPort = httpPort 40 | 41 | # Try to start a session 42 | self.startSession() 43 | 44 | # Use 'info' command to retrieve more information 45 | self._info = self.info() 46 | if self._info: 47 | self._api = self._info['api'] 48 | self._httpPort = self._info['endpoints']['httpPort'] 49 | self._httpUpdatesPort = self._info['endpoints']['httpUpdatePort'] 50 | 51 | def latestFileUri(self): 52 | x = self.listImages(1) 53 | if 'results' in x: 54 | if 'entries' in x['results']: 55 | if len(x['results']['entries']) == 1: 56 | if 'uri' in x['results']['entries'][0]: 57 | return x['results']['entries'][0]['uri'] 58 | return None 59 | 60 | -------------------------------------------------------------------------------- /python/osc/osc.py: -------------------------------------------------------------------------------- 1 | """ 2 | An implementation of the Open Spherical Camera API proposed here: 3 | https://developers.google.com/streetview/open-spherical-camera/reference 4 | 5 | There is minimal error checking, and likely a few places where the expected 6 | workflow isn't adhered to but this should get you started if you're using 7 | Python and a camera that supports the Open Spherical Camera API. 8 | 9 | Usage: 10 | At the top of your Python script, use 11 | 12 | from osc.osc import * 13 | 14 | After you import the library, you can use the commands like this: 15 | 16 | # Initializing the class starts a session 17 | camera = OpenSphericalCamera() 18 | camera.state() 19 | camera.info() 20 | 21 | # Only need to call this if there was a problem 22 | # when 'camera' was created 23 | camera.startSession() 24 | 25 | # Capture image 26 | response = camera.takePicture() 27 | 28 | # Wait for the stitching to finish 29 | camera.waitForProcessing(response['id']) 30 | 31 | # Copy image to computer 32 | camera.getLatestImage() 33 | 34 | camera.closeSession() 35 | """ 36 | 37 | import json 38 | import requests 39 | import time 40 | 41 | __author__ = 'Haarm-Pieter Duiker' 42 | __copyright__ = 'Copyright (C) 2016 - Duiker Research Corp' 43 | __license__ = '' 44 | __maintainer__ = 'Haarm-Pieter Duiker' 45 | __email__ = 'support@duikerresearch.org' 46 | __status__ = 'Production' 47 | 48 | __major_version__ = '1' 49 | __minor_version__ = '0' 50 | __change_version__ = '0' 51 | __version__ = '.'.join((__major_version__, 52 | __minor_version__, 53 | __change_version__)) 54 | 55 | __all__ = ['g_oscOptions', 56 | 'shutterSpeedNames', 57 | 'shutterSpeeds', 58 | 'exposurePrograms', 59 | 'whiteBalance', 60 | 'OpenSphericalCamera'] 61 | 62 | # 63 | # Options 64 | # 65 | ''' 66 | Reference: 67 | https://developers.google.com/streetview/open-spherical-camera/reference/options 68 | ''' 69 | g_oscOptions = [ 70 | # Read-only values 71 | "remainingPictures", 72 | "remainingSpace", 73 | "totalSpace", 74 | 75 | # Reference options 76 | "gpsInfo", 77 | "dateTimeZone", 78 | 79 | "aperture", 80 | "apertureSupport", 81 | "captureMode", 82 | "captureModeSupport", 83 | "exposureCompensation", 84 | "exposureCompensationSupport", 85 | "exposureProgram", 86 | "exposureProgramSupport", 87 | "fileFormat", 88 | "fileFormatSupport", 89 | "iso", 90 | "isoSupport", 91 | "offDelay", 92 | "offDelaySupport", 93 | "shutterSpeed", 94 | "shutterSpeedSupport", 95 | "sleepDelay", 96 | "sleepDelaySupport", 97 | "whiteBalance", 98 | "whiteBalanceSupport", 99 | 100 | "exposureDelay", 101 | "exposureDelaySupport", 102 | "hdr", 103 | "hdrSupport", 104 | "exposureBracket", 105 | "exposureBracketSupport", 106 | "gyro", 107 | "gyroSupport", 108 | "imageStabilization", 109 | "imageStabilizationSupport", 110 | "wifiPassword" 111 | ] 112 | 113 | # 114 | # Known options values 115 | # 116 | shutterSpeedNames = { 117 | 0.00015625 : "1/6400", 118 | 0.0002 : "1/5000", 119 | 0.00025 : "1/4000", 120 | 0.0003125 : "1/3200", 121 | 0.0004 : "1/2500", 122 | 0.0005 : "1/2000", 123 | 0.000625 : "1/1600", 124 | 0.0008 : "1/1250", 125 | 0.001 : "1/1000", 126 | 0.00125 : "1/800", 127 | 0.0015625 : "1/640", 128 | 0.002 : "1/500", 129 | 0.0025 : "1/400", 130 | 0.003125 : "1/320", 131 | 0.004 : "1/250", 132 | 0.005 : "1/200", 133 | 0.00625 : "1/160", 134 | 0.008 : "1/125", 135 | 0.01 : "1/100", 136 | 0.0125 : "1/80", 137 | 0.01666666 : "1/60", 138 | 0.02 : "1/50", 139 | 0.025 : "1/40", 140 | 0.03333333 : "1/30", 141 | 0.04 : "1/25", 142 | 0.05 : "1/20", 143 | 0.06666666 : "1/15", 144 | 0.07692307 : "1/13", 145 | 0.1 : "1/10", 146 | 0.125 : "1/8", 147 | 0.16666666 : "1/6", 148 | 0.2 : "1/5", 149 | 0.25 : "1/4", 150 | 0.33333333 : "1/3", 151 | 0.4 : "1/2.5", 152 | 0.5 : "1/2", 153 | 0.625 : "1/1.6", 154 | 0.76923076 : "1/1.3", 155 | 1 : "1", 156 | 1.3 : "1.3", 157 | 1.6 : "1.6", 158 | 2 : "2", 159 | 2.5 : "2.5", 160 | 3.2 : "3.2", 161 | 4 : "4", 162 | 5 : "5", 163 | 6 : "6", 164 | 8 : "8", 165 | 10 : "10", 166 | 13 : "13", 167 | 15 : "15", 168 | 20 : "20", 169 | 25 : "25", 170 | 30 : "30", 171 | 60 : "60" 172 | } 173 | 174 | shutterSpeeds = [ 175 | 0.00015625, 176 | 0.0002, 177 | 0.00025, 178 | 0.0003125, 179 | 0.0004, 180 | 0.0005, 181 | 0.000625, 182 | 0.0008, 183 | 0.001, 184 | 0.00125, 185 | 0.0015625, 186 | 0.002, 187 | 0.0025, 188 | 0.003125, 189 | 0.004, 190 | 0.005, 191 | 0.00625, 192 | 0.008, 193 | 0.01, 194 | 0.0125, 195 | 0.01666666, 196 | 0.02, 197 | 0.025, 198 | 0.03333333, 199 | 0.04, 200 | 0.05, 201 | 0.06666666, 202 | 0.07692307, 203 | 0.1, 204 | 0.125, 205 | 0.16666666, 206 | 0.2, 207 | 0.25, 208 | 0.33333333, 209 | 0.4, 210 | 0.5, 211 | 0.625, 212 | 0.76923076, 213 | 1, 214 | 1.3, 215 | 1.6, 216 | 2, 217 | 2.5, 218 | 3.2, 219 | 4, 220 | 5, 221 | 6, 222 | 8, 223 | 10, 224 | 13, 225 | 15, 226 | 20, 227 | 25, 228 | 30, 229 | 60 230 | ] 231 | 232 | exposurePrograms = { 233 | "manual" : 1, 234 | "automatic" : 2, 235 | "shutter priority" : 4, 236 | "iso priority" : 9 237 | } 238 | 239 | whiteBalance = { 240 | "Auto" : "auto", 241 | "Outdoor" : "daylight", 242 | "Shade" : "shade", 243 | "Cloudy" : "cloudy-daylight", 244 | "Incandescent light 1" : "incandescent", 245 | "Incandescent light 2" : "_warmWhiteFluorescent", 246 | "Fluorescent light 1 (daylight)" : "_dayLightFluorescent", 247 | "Fluorescent light 2 (natural white)" : "_dayWhiteFluorescent", 248 | "Fluorescent light 3 (white)" : "fluorescent", 249 | "Fluorescent light 4 (light bulb color)" : "_bulbFluorescent" 250 | } 251 | 252 | # 253 | # Error codes 254 | # 255 | 256 | ''' 257 | Reference: 258 | https://developers.google.com/streetview/open-spherical-camera/guides/osc/error-handling 259 | https://developers.theta360.com/en/docs/v2/api_reference/protocols/errors.html 260 | 261 | Error code - HTTP Status code - Description 262 | unknownCommand - 400 - Invalid command is issued 263 | missingParameter - 400 - Insufficient required parameters to issue the command 264 | invalidParameterName - 400 - Parameter name or option name is invalid 265 | invalidParameterValue - 400 - Parameter value when command was issued is invalid 266 | cameraInExclusiveUse - 400 - Session start not possible when camera is in exclusive use 267 | 268 | disabledCommand - 403 - Command cannot be executed due to the camera status 269 | invalidSessionId - 403 - sessionID when command was issued is invalid 270 | corruptedFile - 403 - Process request for corrupted file 271 | powerOffSequenceRunning - 403 - Process request when power supply is off 272 | invalidFileFormat - 403 - Invalid file format specified 273 | 274 | serviceUnavailable - 503 - Processing requests cannot be received temporarily 275 | unexpected - 503 - Other errors 276 | ''' 277 | 278 | # 279 | # Generic OpenSphericalCamera 280 | # 281 | class OpenSphericalCamera: 282 | # Class variables / methods 283 | oscOptions = g_oscOptions 284 | 285 | # Instance variables / methods 286 | def __init__(self, ip_base="192.168.1.1", httpPort=80): 287 | self.sid = None 288 | self.fingerprint = None 289 | self._api = None 290 | 291 | self._ip = ip_base 292 | self._httpPort = httpPort 293 | self._httpUpdatesPort = httpPort 294 | 295 | # Try to start a session 296 | self.startSession() 297 | 298 | # Use 'info' command to retrieve more information 299 | self._info = self.info() 300 | if self._info: 301 | self._api = self._info['api'] 302 | self._httpPort = self._info['endpoints']['httpPort'] 303 | self._httpUpdatesPort = self._info['endpoints']['httpUpdatesPort'] 304 | 305 | def __del__(self): 306 | if self.sid: 307 | self.closeSession() 308 | 309 | def _request(self, url_request, update=False): 310 | """ 311 | Generate the URI to send to the Open Spherical Camera. 312 | All calls start with /osc/ 313 | """ 314 | osc_request = unicode("/osc/" + url_request) 315 | 316 | url_base = "http://%s:%s" % (self._ip, self._httpPort if not update else self._httpUpdatesPort) 317 | 318 | if self._api: 319 | if osc_request in self._api: 320 | url = url_base + osc_request 321 | else: 322 | print( "OSC Error - Unsupported API : %s" % osc_request ) 323 | print( "OSC Error - Supported API is : %s" % self._api ) 324 | url = None 325 | else: 326 | url = url_base + osc_request 327 | 328 | return url 329 | 330 | def _httpError(self, exception): 331 | print( "HTTP Error - begin" ) 332 | print( repr(exception) ) 333 | print( "HTTP Error - end" ) 334 | 335 | def _oscError(self, request): 336 | status = request.status_code 337 | 338 | try: 339 | error = request.json() 340 | 341 | print( "OSC Error - HTTP Status : %s" % status) 342 | if 'error' in error: 343 | print( "OSC Error - Code : %s" % error['error']['code']) 344 | print( "OSC Error - Message : %s" % error['error']['message']) 345 | print( "OSC Error - Name : %s" % error['name']) 346 | print( "OSC Error - State : %s" % error['state']) 347 | except: 348 | print( "OSC Error - HTTP Status : %s" % status) 349 | 350 | return status 351 | 352 | def getOptionNames(self): 353 | return self.oscOptions 354 | 355 | def info(self): 356 | """ 357 | Get basic information on the camera. Note that this is a GET call 358 | and not a POST. Most of the calls are POST. 359 | 360 | Reference: 361 | https://developers.google.com/streetview/open-spherical-camera/guides/osc/info 362 | """ 363 | url = self._request("info") 364 | try: 365 | req = requests.get(url) 366 | except Exception, e: 367 | self._httpError(e) 368 | return None 369 | 370 | if req.status_code == 200: 371 | response = req.json() 372 | else: 373 | self._oscError(req) 374 | response = None 375 | return response 376 | 377 | def state(self): 378 | """ 379 | Get the state of the camera, which will include the sessionsId and also the 380 | latestFileUri if you've just taken a picture. 381 | 382 | Reference: 383 | https://developers.google.com/streetview/open-spherical-camera/guides/osc/state 384 | """ 385 | url = self._request("state") 386 | try: 387 | req = requests.post(url) 388 | except Exception, e: 389 | self._httpError(e) 390 | return None 391 | 392 | if req.status_code == 200: 393 | response = req.json() 394 | self.fingerprint = response['fingerprint'] 395 | state = response['state'] 396 | else: 397 | self._oscError(req) 398 | state = None 399 | return state 400 | 401 | def status(self, command_id): 402 | """ 403 | Returns the status for previous inProgress commands. 404 | 405 | Reference: 406 | https://developers.google.com/streetview/open-spherical-camera/guides/osc/commands/status 407 | """ 408 | url = self._request("commands/status") 409 | body = json.dumps({"id": command_id}) 410 | try: 411 | req = requests.post(url, data=body) 412 | except Exception, e: 413 | self._httpError(e) 414 | return None 415 | 416 | if req.status_code == 200: 417 | response = req.json() 418 | state = response['state'] 419 | else: 420 | self._oscError(req) 421 | state = None 422 | return state 423 | 424 | def checkForUpdates(self): 425 | """ 426 | Check for updates on the camera, using the current state fingerprint. 427 | 428 | Reference: 429 | https://developers.google.com/streetview/open-spherical-camera/guides/osc/checkforupdates 430 | """ 431 | if self.fingerprint is None: 432 | self.state() 433 | 434 | url = self._request("checkForUpdates") 435 | body = json.dumps({"stateFingerprint": self.fingerprint}) 436 | try: 437 | req = requests.post(url, data=body) 438 | except Exception, e: 439 | self._httpError(e) 440 | return False 441 | 442 | if req.status_code == 200: 443 | response = req.json() 444 | newFingerprint = response['stateFingerprint'] 445 | if newFingerprint != self.fingerprint: 446 | print( "Update - new, old fingerprint : %s, %s" % (newFingerprint, self.fingerprint) ) 447 | self.fingerprint = newFingerprint 448 | response = True 449 | else: 450 | print( "No update - fingerprint : %s" % self.fingerprint ) 451 | response = False 452 | else: 453 | self._oscError(req) 454 | response = False 455 | return response 456 | 457 | def waitForProcessing(self, command_id, maxWait=20): 458 | """ 459 | Helper function that will poll the camera until the status to changes 460 | to 'done' or the timeout is hit. 461 | 462 | Reference: 463 | https://developers.google.com/streetview/open-spherical-camera/guides/osc/commands/status 464 | """ 465 | 466 | print( "Waiting for processing") 467 | for i in range(maxWait): 468 | status = self.status(command_id) 469 | if status == "done": 470 | print( "Image processing finished" ) 471 | break 472 | elif not status or "error" in status: 473 | print( "Status failed. Stopping wait." ) 474 | break 475 | print( "%d - %s" % (i, status) ) 476 | time.sleep( 1 ) 477 | 478 | return 479 | 480 | def startSession(self): 481 | """ 482 | Start a new session. Grab the sessionId number and return it. 483 | You'll need the sessionId to take a video or image. 484 | 485 | Reference: 486 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/startsession 487 | """ 488 | url = self._request("commands/execute") 489 | body = json.dumps({"name": "camera.startSession", 490 | "parameters": {} 491 | }) 492 | try: 493 | req = requests.post(url, data=body) 494 | except Exception, e: 495 | self._httpError(e) 496 | self.sid = None 497 | return self.sid 498 | 499 | if req.status_code == 200: 500 | response = req.json() 501 | self.sid = (response["results"]["sessionId"]) 502 | else: 503 | self._oscError(req) 504 | self.sid = None 505 | return self.sid 506 | 507 | def updateSession(self): 508 | """ 509 | Update a session, using the sessionId. 510 | 511 | Reference: 512 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/updatesession 513 | """ 514 | url = self._request("commands/execute") 515 | body = json.dumps({"name": "camera.updateSession", 516 | "parameters": { "sessionId":self.sid } 517 | }) 518 | try: 519 | req = requests.post(url, data=body) 520 | except Exception, e: 521 | self._httpError(e) 522 | return None 523 | 524 | response = None 525 | if req.status_code == 200: 526 | response = req.json() 527 | else: 528 | self._oscError(req) 529 | response = None 530 | 531 | return response 532 | 533 | def closeSession(self): 534 | """ 535 | Close a session. 536 | 537 | Reference: 538 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/closesession 539 | """ 540 | url = self._request("commands/execute") 541 | body = json.dumps({"name": "camera.closeSession", 542 | "parameters": { "sessionId":self.sid } 543 | }) 544 | try: 545 | req = requests.post(url, data=body) 546 | except Exception, e: 547 | self._httpError(e) 548 | return None 549 | 550 | if req.status_code == 200: 551 | response = req.json() 552 | self.sid = None 553 | else: 554 | self._oscError(req) 555 | response = None 556 | 557 | return response 558 | 559 | def takePicture(self): 560 | """ 561 | Take a still image. The sessionId is either taken from 562 | startSession or from state. You can change the mode 563 | from video to image with captureMode in the options. 564 | 565 | Reference: 566 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/takepicture 567 | """ 568 | if self.sid == None: 569 | response = None 570 | return response 571 | url = self._request("commands/execute") 572 | body = json.dumps({"name": "camera.takePicture", 573 | "parameters": { 574 | "sessionId": self.sid 575 | } 576 | }) 577 | try: 578 | req = requests.post(url, data=body) 579 | except Exception, e: 580 | self._httpError(e) 581 | return None 582 | 583 | if req.status_code == 200: 584 | response = req.json() 585 | else: 586 | self._oscError(req) 587 | response = None 588 | return response 589 | 590 | def listImages(self, entryCount = 3, maxSize = None, 591 | continuationToken = None, includeThumb = True ): 592 | """ 593 | entryCount: 594 | Integer No. of still images and video files to be acquired 595 | maxSize: 596 | Integer (Optional) Maximum size of thumbnail images; 597 | max(thumbnail_width, thumbnail_height). 598 | continuationToken 599 | String (Optional) An opaque continuation token of type string, 600 | returned by previous listImages call, used to retrieve next 601 | images. Omit this parameter for the first listImages 602 | includeThumb: 603 | Boolean (Optional) Defaults to true. Use false to omit 604 | thumbnail images from the result. 605 | 606 | Reference: 607 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/listimages 608 | """ 609 | parameters = { 610 | "entryCount": entryCount, 611 | "includeThumb": includeThumb, 612 | } 613 | if maxSize is not None: 614 | parameters['maxSize'] = maxSize 615 | if continuationToken is not None: 616 | parameters['continuationToken'] = continuationToken 617 | 618 | url = self._request("commands/execute") 619 | body = json.dumps({"name": "camera.listImages", 620 | "parameters": parameters 621 | }) 622 | try: 623 | req = requests.post(url, data=body) 624 | except Exception, e: 625 | self._httpError(e) 626 | return None 627 | 628 | if req.status_code == 200: 629 | response = req.json() 630 | else: 631 | self._oscError(req) 632 | response = None 633 | return response 634 | 635 | def delete(self, fileUri): 636 | """ 637 | Delete the image with the named fileUri 638 | 639 | Reference: 640 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/delete 641 | """ 642 | url = self._request("commands/execute") 643 | body = json.dumps({"name": "camera.delete", 644 | "parameters": { 645 | "fileUri": fileUri 646 | } 647 | }) 648 | try: 649 | req = requests.post(url, data=body) 650 | except Exception, e: 651 | self._httpError(e) 652 | return None 653 | 654 | if req.status_code == 200: 655 | response = req.json() 656 | else: 657 | self._oscError(req) 658 | response = None 659 | return response 660 | 661 | def getImage(self, fileUri, imageType="image"): 662 | """ 663 | Transfer the file from the camera to computer and save the 664 | binary data to local storage. This works, but is clunky. 665 | There are easier ways to do this. The __type parameter 666 | can be set to "thumb" for a thumbnail or "image" for the 667 | full-size image. The default is "image". 668 | 669 | Reference: 670 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/getimage 671 | """ 672 | url = self._request("commands/execute") 673 | body = json.dumps({"name": "camera.getImage", 674 | "parameters": { 675 | "fileUri": fileUri, 676 | "_type": imageType 677 | } 678 | }) 679 | fileName = fileUri.split("/")[1] 680 | print( "Writing image : %s" % fileName ) 681 | 682 | acquired = False 683 | try: 684 | response = requests.post(url, data=body, stream=True) 685 | except Exception, e: 686 | self._httpError(e) 687 | return acquired 688 | 689 | if response.status_code == 200: 690 | with open(fileName, 'wb') as handle: 691 | for block in response.iter_content(1024): 692 | handle.write(block) 693 | acquired = True 694 | else: 695 | self._oscError(response) 696 | 697 | return acquired 698 | 699 | def getMetadata(self, fileUri): 700 | """ 701 | Get the exif and xmp metadata associated with the named fileUri 702 | 703 | Reference: 704 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/getmetadata 705 | """ 706 | url = self._request("commands/execute") 707 | body = json.dumps({"name": "camera.getMetadata", 708 | "parameters": { 709 | "fileUri": fileUri 710 | } 711 | }) 712 | try: 713 | req = requests.post(url, data=body) 714 | except Exception, e: 715 | self._httpError(e) 716 | return None 717 | 718 | if req.status_code == 200: 719 | response = req.json() 720 | else: 721 | self._oscError(req) 722 | response = None 723 | 724 | return response 725 | 726 | def setOption(self, option, value): 727 | """ 728 | Set an option to a value. The validity of the option is checked. The 729 | validity of the value is not. 730 | 731 | Reference: 732 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/setoptions 733 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera.set_options.html 734 | """ 735 | if self.sid == None or option not in self.getOptionNames(): 736 | response = None 737 | return response 738 | 739 | print( "setOption - %s : %s" % (option, value) ) 740 | 741 | url = self._request("commands/execute") 742 | body = json.dumps({"name": "camera.setOptions", 743 | "parameters": { 744 | "sessionId": self.sid, 745 | "options": { 746 | option: value, 747 | } 748 | } 749 | }) 750 | try: 751 | req = requests.post(url, data=body) 752 | except Exception, e: 753 | self._httpError(e) 754 | return None 755 | 756 | if req.status_code == 200: 757 | response = req.json() 758 | #print( "setOption suceeeded - %s " % response ) 759 | else: 760 | self._oscError(req) 761 | response = None 762 | 763 | return response 764 | 765 | def getOption(self, option): 766 | """ 767 | Get an option value. The validity of the option is not checked. 768 | 769 | Reference: 770 | https://developers.google.com/streetview/open-spherical-camera/reference/camera/getoptions 771 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera.get_options.html 772 | """ 773 | url = self._request("commands/execute") 774 | body = json.dumps({"name": "camera.getOptions", 775 | "parameters": { 776 | "sessionId": self.sid, 777 | "optionNames": [ 778 | option] 779 | } 780 | }) 781 | try: 782 | req = requests.post(url, data=body) 783 | except Exception, e: 784 | self._httpError(e) 785 | return None 786 | 787 | if req.status_code == 200: 788 | response = req.json() 789 | value = response["results"]["options"][option] 790 | else: 791 | self._oscError(req) 792 | value = None 793 | return value 794 | 795 | def getSid(self): 796 | """ 797 | Helper function that will refresh the cache of the sessionsId and 798 | return it's value 799 | """ 800 | url = self._request("state") 801 | try: 802 | req = requests.post(url) 803 | except Exception, e: 804 | self._httpError(e) 805 | self.sid = None 806 | return None 807 | 808 | if req.status_code == 200: 809 | response = req.json() 810 | self.sid = response["state"]["sessionId"] 811 | else: 812 | self._oscError(req) 813 | self.sid = None 814 | return self.sid 815 | 816 | # Extensions 817 | def getAllOptions(self): 818 | """ 819 | Helper function that will get the value for all options. 820 | """ 821 | url = self._request("commands/execute") 822 | body = json.dumps({"name": "camera.getOptions", 823 | "parameters": { 824 | "sessionId": self.sid, 825 | "optionNames": self.getOptionNames() 826 | } 827 | }) 828 | try: 829 | req = requests.post(url, data=body) 830 | except Exception, e: 831 | self._httpError(e) 832 | return None 833 | 834 | if req.status_code == 200: 835 | response = req.json() 836 | returnOptions = response["results"]["options"] 837 | else: 838 | self._oscError(req) 839 | returnOptions = None 840 | return returnOptions 841 | 842 | def latestFileUri(self): 843 | """ 844 | Get the name of the last captured image or video from the state 845 | """ 846 | try: 847 | state_data = self.state() 848 | except: 849 | return None 850 | if state_data: 851 | latestFileUri = state_data["_latestFileUri"] 852 | else: 853 | latestFileUri = None 854 | return latestFileUri 855 | 856 | def getLatestImage(self, imageType="image"): 857 | """ 858 | Transfer the latest file from the camera to computer and save the 859 | binary data to local storage. The __type parameter 860 | can be set to "thumb" for a thumbnail or "image" for the 861 | full-size image. The default is "image". 862 | """ 863 | fileUri = self.latestFileUri() 864 | if fileUri: 865 | self.getImage(fileUri, imageType) 866 | 867 | def getLatestImageMetadata(self): 868 | """ 869 | Get the metadata for the last image 870 | """ 871 | fileUri = self.latestFileUri() 872 | if fileUri: 873 | metadata = self.getImageMetadata(fileUri) 874 | else: 875 | metadata = None 876 | return metadata 877 | 878 | # OpenSphericalCamera 879 | 880 | -------------------------------------------------------------------------------- /python/osc/theta.py: -------------------------------------------------------------------------------- 1 | """ 2 | Extensions to the Open Spherical Camera API specific to the Ricoh Theta S. 3 | Documentation here: 4 | https://developers.theta360.com/en/docs/v2/api_reference/ 5 | 6 | Open Spherical Camera API proposed here: 7 | https://developers.google.com/streetview/open-spherical-camera/reference 8 | 9 | The library is an evolution of: 10 | https://github.com/codetricity/theta-s-api-tests/blob/master/thetapylib.py 11 | 12 | Usage: 13 | At the top of your Python script, use 14 | 15 | from osc.theta import RicohThetaS 16 | 17 | After you import the library, you can use the commands like this: 18 | 19 | thetas = RicohThetaS() 20 | thetas.state() 21 | thetas.info() 22 | 23 | # Capture image 24 | thetas.setCaptureMode( 'image' ) 25 | response = thetas.takePicture() 26 | 27 | # Wait for the stitching to finish 28 | thetas.waitForProcessing(response['id']) 29 | 30 | # Copy image to computer 31 | thetas.getLatestImage() 32 | 33 | # Stream the livePreview video to disk 34 | # for 3 seconds 35 | thetas.getLivePreview(timeLimitSeconds=3) 36 | 37 | # Capture video 38 | thetas.setCaptureMode( '_video' ) 39 | thetas.startCapture() 40 | thetas.stopCapture() 41 | 42 | # Copy video to computer 43 | thetas.getLatestVideo() 44 | 45 | thetas.closeSession() 46 | """ 47 | 48 | import json 49 | import requests 50 | import timeit 51 | 52 | import osc 53 | 54 | __author__ = 'Haarm-Pieter Duiker' 55 | __copyright__ = 'Copyright (C) 2016 - Duiker Research Corp' 56 | __license__ = '' 57 | __maintainer__ = 'Haarm-Pieter Duiker' 58 | __email__ = 'support@duikerresearch.org' 59 | __status__ = 'Production' 60 | 61 | __major_version__ = '1' 62 | __minor_version__ = '0' 63 | __change_version__ = '0' 64 | __version__ = '.'.join((__major_version__, 65 | __minor_version__, 66 | __change_version__)) 67 | 68 | __all__ = ['g_ricohOptions', 69 | 'ricohFileFormats', 70 | 'RicohThetaS'] 71 | 72 | # 73 | # Ricoh Theta S 74 | # 75 | 76 | # 77 | # Ricoh-specific options 78 | # 79 | 80 | ''' 81 | Reference: 82 | https://developers.theta360.com/en/docs/v2/api_reference/options/ 83 | ''' 84 | 85 | g_ricohOptions = [ 86 | # Custom read-only options 87 | "_wlanChannel", 88 | "_remainingVideos", 89 | 90 | # Custom options 91 | "_captureInterval", 92 | "_captureIntervalSupport", 93 | "_captureNumber", 94 | "_captureNumberSupport", 95 | "_filter", 96 | "_filterSupport", 97 | "_HDMIreso", 98 | "_HDMIresoSupport", 99 | "_shutterVolume", 100 | "_shutterVolumeSupport" 101 | ] 102 | 103 | ricohFileFormats = { 104 | "image_5k" : {'width': 5376, 'type': 'jpeg', 'height': 2688}, 105 | "image_2k" : {'width': 2048, 'type': 'jpeg', 'height': 1024}, 106 | "video_HD_1080" : {"type": "mp4", "width": 1920, "height": 1080}, 107 | "video_HD_720" : {"type": "mp4", "width": 1280, "height": 720} 108 | } 109 | 110 | class RicohThetaS(osc.OpenSphericalCamera): 111 | # Class variables / methods 112 | ricohOptions = g_ricohOptions 113 | 114 | def __init__(self, ip_base="192.168.1.1", httpPort=80): 115 | osc.OpenSphericalCamera.__init__(self, ip_base, httpPort) 116 | 117 | def getOptionNames(self): 118 | return self.oscOptions + self.ricohOptions 119 | 120 | # 'image', '_video' 121 | def setCaptureMode(self, mode): 122 | return self.setOption("captureMode", mode) 123 | 124 | def getCaptureMode(self): 125 | return self.getOption("captureMode") 126 | 127 | def listAll(self, entryCount = 3, detail = False, sortType = "newest", ): 128 | """ 129 | entryCount: 130 | Integer No. of still images and video files to be acquired 131 | detail: 132 | Boolean (Optional) Whether or not file details are acquired 133 | true is acquired by default. Only values that can be acquired 134 | when false is specified are "name", "uri", "size" and "dateTime" 135 | sort: 136 | String (Optional) Specify the sort order 137 | newest (dateTime descending order)/ oldest (dateTime ascending order) 138 | Default is newest 139 | 140 | Reference: 141 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera._list_all.html 142 | """ 143 | url = self._request("commands/execute") 144 | body = json.dumps({"name": "camera._listAll", 145 | "parameters": { 146 | "entryCount": entryCount, 147 | "detail": detail, 148 | "sort": sortType 149 | } 150 | }) 151 | try: 152 | req = requests.post(url, data=body) 153 | except Exception, e: 154 | self._httpError(e) 155 | return None 156 | 157 | if req.status_code == 200: 158 | response = req.json() 159 | else: 160 | self._oscError(req) 161 | response = None 162 | return response 163 | 164 | def finishWlan(self): 165 | """ 166 | Turns the wireless LAN off. 167 | 168 | Reference: 169 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera._finish_wlan.html 170 | """ 171 | url = self._request("commands/execute") 172 | body = json.dumps({"name": "camera._finishWlan", 173 | "parameters": { 174 | "sessionId": self.sid 175 | } 176 | }) 177 | try: 178 | req = requests.post(url, data=body) 179 | except Exception, e: 180 | self._httpError(e) 181 | return None 182 | 183 | if req.status_code == 200: 184 | response = req.json() 185 | else: 186 | self._oscError(req) 187 | response = None 188 | return response 189 | 190 | def startCapture(self): 191 | """ 192 | Begin video capture if the captureMode is _video. If the 193 | captureMode is set to image, the camera will take multiple 194 | still images. The captureMode can be set in the options. 195 | Note that this will not work with streaming video using the 196 | HDMI or USB cable. 197 | 198 | Reference: 199 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera._start_capture.html 200 | """ 201 | url = self._request("commands/execute") 202 | body = json.dumps({"name": "camera._startCapture", 203 | "parameters": { 204 | "sessionId": self.sid 205 | } 206 | }) 207 | try: 208 | req = requests.post(url, data=body) 209 | except Exception, e: 210 | self._httpError(e) 211 | return None 212 | 213 | if req.status_code == 200: 214 | response = req.json() 215 | else: 216 | self._oscError(req) 217 | response = None 218 | return response 219 | 220 | def stopCapture(self): 221 | """ 222 | Stop video capture. If in image mode, will stop 223 | automatic image taking. 224 | 225 | Reference: 226 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera._stop_capture.html 227 | """ 228 | url = self._request("commands/execute") 229 | body = json.dumps({"name": "camera._stopCapture", 230 | "parameters": { 231 | "sessionId": self.sid 232 | } 233 | }) 234 | try: 235 | req = requests.post(url, data=body) 236 | except Exception, e: 237 | self._httpError(e) 238 | return None 239 | 240 | if req.status_code == 200: 241 | response = req.json() 242 | else: 243 | self._oscError(req) 244 | response = None 245 | return response 246 | 247 | def getVideo(self, fileUri, imageType="full"): 248 | """ 249 | Transfer the video file from the camera to computer and save the 250 | binary data to local storage. This works, but is clunky. 251 | There are easier ways to do this. The __type parameter 252 | can be set to "thumb" for a thumbnail or "full" for the 253 | full-size video. The default is "full". 254 | 255 | Reference: 256 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera._get_video.html 257 | """ 258 | acquired = False 259 | if fileUri: 260 | url = self._request("commands/execute") 261 | body = json.dumps({"name": "camera._getVideo", 262 | "parameters": { 263 | "fileUri": fileUri, 264 | "type": imageType 265 | } 266 | }) 267 | fileName = fileUri.split("/")[1] 268 | 269 | try: 270 | response = requests.post(url, data=body, stream=True) 271 | except Exception, e: 272 | self._httpError(e) 273 | return acquired 274 | 275 | if response.status_code == 200: 276 | with open(fileName, 'wb') as handle: 277 | for block in response.iter_content(1024): 278 | handle.write(block) 279 | acquired = True 280 | else: 281 | self._oscError(req) 282 | 283 | return acquired 284 | 285 | def getLatestVideo(self, imageType="full"): 286 | """ 287 | Transfer the latest file from the camera to computer and save the 288 | binary data to local storage. The __type parameter 289 | can be set to "thumb" for a thumbnail or "full" for the 290 | full-size video. The default is "full". 291 | """ 292 | fileUri = self.latestFileUri() 293 | if fileUri: 294 | self.getVideo(fileUri, imageType) 295 | 296 | def getLivePreview(self, fileNamePrefix = "livePreview", timeLimitSeconds=10): 297 | """ 298 | Save the live preview video stream to disk as a series of jpegs. 299 | The capture mode must be 'image'. 300 | 301 | Credit for jpeq decoding: 302 | https://stackoverflow.com/questions/21702477/how-to-parse-mjpeg-http-stream-from-ip-camera 303 | 304 | Reference: 305 | https://developers.theta360.com/en/docs/v2/api_reference/commands/camera._get_live_preview.html 306 | """ 307 | acquired = False 308 | 309 | url = self._request("commands/execute") 310 | body = json.dumps({"name": "camera._getLivePreview", 311 | "parameters": { 312 | "sessionId": self.sid 313 | }}) 314 | 315 | try: 316 | response = requests.post(url, data=body, stream=True) 317 | except Exception, e: 318 | self._httpError(e) 319 | return acquired 320 | 321 | if response.status_code == 200: 322 | bytes='' 323 | t0 = timeit.default_timer() 324 | i = 0 325 | for block in response.iter_content(16384): 326 | bytes += block 327 | 328 | # Search the current block of bytes for the jpq start and end 329 | a = bytes.find('\xff\xd8') 330 | b = bytes.find('\xff\xd9') 331 | 332 | # If you have a jpg, write it to disk 333 | if a !=- 1 and b != -1: 334 | #print( "Writing frame %04d - Byte range : %d to %d" % (i, a, b) ) 335 | # Found a jpg, write to disk 336 | frameFileName = "%s.%04d.jpg" % (fileNamePrefix, i) 337 | with open(frameFileName, 'wb') as handle: 338 | jpg = bytes[a:b+2] 339 | handle.write(jpg) 340 | 341 | # Reset the buffer to point to the next set of bytes 342 | bytes = bytes[b+2:] 343 | #print( "Wrote frame %04d - %2.2f seconds" % (i, elapsed) ) 344 | 345 | i += 1 346 | 347 | t1 = timeit.default_timer() 348 | elapsed = t1 - t0 349 | if elapsed > timeLimitSeconds: 350 | #print( "Breaking" ) 351 | break 352 | 353 | acquired = True 354 | else: 355 | self._oscError(response) 356 | # RicohThetaS 357 | 358 | 359 | 360 | --------------------------------------------------------------------------------