diff --git a/platypush/backend/http/app/routes/plugins/camera/pi.py b/platypush/backend/http/app/routes/plugins/camera/pi.py index f440c69f..aff9405e 100644 --- a/platypush/backend/http/app/routes/plugins/camera/pi.py +++ b/platypush/backend/http/app/routes/plugins/camera/pi.py @@ -2,7 +2,6 @@ import os import tempfile from flask import Response, Blueprint, send_from_directory -from typing import Optional from platypush.backend.http.app import template_folder from platypush.backend.http.app.utils import authenticate, send_request @@ -18,7 +17,6 @@ __routes__ = [ def video_feed(): - camera: Optional[CameraPiPlugin] = None camera_conf = Config.get('camera.pi') or {} camera = CameraPiPlugin(**camera_conf) diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/camera.android.ipcam b/platypush/backend/http/static/css/source/webpanel/plugins/camera.android.ipcam new file mode 120000 index 00000000..5f839334 --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/camera.android.ipcam @@ -0,0 +1 @@ +camera \ No newline at end of file diff --git a/platypush/backend/http/static/js/plugins/camera.android.ipcam/index.js b/platypush/backend/http/static/js/plugins/camera.android.ipcam/index.js new file mode 100644 index 00000000..7b0f57d9 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/camera.android.ipcam/index.js @@ -0,0 +1,108 @@ +Vue.component('camera-android-ipcam', { + template: '#tmpl-camera-android-ipcam', + props: ['config'], + + data: function() { + return { + bus: new Vue({}), + loading: false, + streaming: false, + capturing: false, + recording: false, + cameras: {}, + selectedCamera: undefined, + }; + }, + + computed: { + hasMultipleCameras: function () { + return Object.keys(this.cameras).length > 1; + }, + }, + + methods: { + startStreaming: function() { + if (this.streaming) + return; + + const cam = this.cameras[this.selectedCamera]; + this.streaming = true; + this.capturing = false; + this.$refs.frame.setAttribute('src', cam.stream_url); + }, + + stopStreaming: function() { + if (!this.streaming) + return; + + this.streaming = false; + this.capturing = false; + this.$refs.frame.removeAttribute('src'); + }, + + capture: function() { + if (this.capturing) + return; + + const cam = this.cameras[this.selectedCamera]; + this.streaming = false; + this.capturing = true; + this.$refs.frame.setAttribute('src', cam.image_url + '?t=' + (new Date()).getTime()); + }, + + onFrameLoaded: function(event) { + if (this.capturing) + this.capturing = false; + }, + + onCameraSelected: function(event) { + this.selectedCamera = event.target.value; + }, + + flipCamera: async function() { + const cam = this.cameras[this.selectedCamera]; + this.loading = true; + + try { + const value = !cam.ffc; + await request('camera.android.ipcam.set_front_facing_camera', { + activate: value, camera: cam.name + }); + + this.cameras[this.selectedCamera].ffc = value; + } finally { + this.loading = false; + } + }, + + updateCameraStatus: async function() { + this.loading = true; + + try { + const cameras = await request('camera.android.ipcam.status'); + this.cameras = cameras.reduce((cameras, cam) => { + for (const attr of ['stream_url', 'image_url', 'audio_url']) { + if (cam[attr].startsWith('https://')) { + cam[attr] = cam[attr].replace('https://', 'http://'); + } + } + + cameras[cam.name] = cam; + return cameras; + }, {}); + + if (cameras.length) + this.selectedCamera = cameras[0].name; + + } finally { + this.loading = false; + } + }, + }, + + mounted: function() { + this.$refs.frame.addEventListener('load', this.onFrameLoaded); + this.updateCameraStatus(); + }, +}); + diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html index ef6a7a29..39047423 100644 --- a/platypush/backend/http/templates/nav.html +++ b/platypush/backend/http/templates/nav.html @@ -1,6 +1,7 @@ {% with pluginIcons = { 'camera': 'fas fa-camera', + 'camera.android.ipcam': 'fab fa-android', 'camera.pi': 'fab fa-raspberry-pi', 'camera.ir.mlx90640': 'fas fa-sun', 'execute': 'fas fa-play', diff --git a/platypush/backend/http/templates/plugins/camera.android.ipcam/index.html b/platypush/backend/http/templates/plugins/camera.android.ipcam/index.html new file mode 100644 index 00000000..c9109fe8 --- /dev/null +++ b/platypush/backend/http/templates/plugins/camera.android.ipcam/index.html @@ -0,0 +1,53 @@ + + diff --git a/platypush/message/response/camera/android.py b/platypush/message/response/camera/android.py index 1d048134..ed18f0c8 100644 --- a/platypush/message/response/camera/android.py +++ b/platypush/message/response/camera/android.py @@ -10,6 +10,9 @@ class AndroidCameraStatusResponse(CameraResponse): ..code-block:: json { + "stream_url": "https://192.168.1.30:8080/video", + "image_url": "https://192.168.1.30:8080/photo.jpg", + "audio_url": "https://192.168.1.30:8080/audio.wav", "orientation": "landscape", "idle": "off", "audio_only": "off", @@ -71,6 +74,10 @@ class AndroidCameraStatusResponse(CameraResponse): } def __init__(self, *args, + name: str = None, + stream_url: str = None, + image_url: str = None, + audio_url: str = None, orientation: str = None, idle: str = None, audio_only: str = None, @@ -117,6 +124,10 @@ class AndroidCameraStatusResponse(CameraResponse): **kwargs): self.status = { + "name": name, + "stream_url": stream_url, + "image_url": image_url, + "audio_url": audio_url, "orientation": orientation, "idle": True if idle == "on" else False, "audio_only": True if audio_only == "on" else False, diff --git a/platypush/plugins/camera/android/ipcam.py b/platypush/plugins/camera/android/ipcam.py index 5d812da9..86acac63 100644 --- a/platypush/plugins/camera/android/ipcam.py +++ b/platypush/plugins/camera/android/ipcam.py @@ -148,19 +148,29 @@ class CameraAndroidIpcamPlugin(Plugin): :return: True if the camera is available, False otherwise """ cameras = self._camera_name_to_idx.keys() if camera is None else [camera] - status = [] + statuses = [] - for cam in cameras: + for c in cameras: try: - status_data = self._exec('status.json', params={'show_avail': 1}, camera=cam).get('curvals', {}) - status.append(AndroidCameraStatusResponse( + if isinstance(camera, int): + cam = self.cameras[c] + else: + cam = self.cameras[self._camera_name_to_idx[c]] + + status_data = self._exec('status.json', params={'show_avail': 1}, camera=cam.name).get('curvals', {}) + status = AndroidCameraStatusResponse( + name=cam.name, + stream_url=cam.stream_url, + image_url=cam.image_url, + audio_url=cam.audio_url, **{k: v for k, v in status_data.items() if k in AndroidCameraStatusResponse.attrs}) - ) - except Exception as e: - self.logger.warning('Could not get the status of {}: {}'.format(cam, str(e))) - return AndroidCameraStatusListResponse(status) + statuses.append(status) + except Exception as e: + self.logger.warning('Could not get the status of {}: {}'.format(c, str(e))) + + return AndroidCameraStatusListResponse(statuses) @action def set_front_facing_camera(self, activate: bool = True, camera: Union[int, str] = None) -> bool: @@ -246,23 +256,5 @@ class CameraAndroidIpcamPlugin(Plugin): """Set video orientation.""" return self.change_setting('scenemode', scenemode, camera=camera) - @action - def get_stream_url(self, camera: Union[int, str] = None) -> str: - """ Get the streaming URL for the specified camera. """ - cam = self._get_camera(camera) - return cam.stream_url - - @action - def get_image_url(self, camera: Union[int, str] = None) -> str: - """ Get the URL to the camera static picture. """ - cam = self._get_camera(camera) - return cam.image_url - - @action - def get_audio_url(self, camera: Union[int, str] = None) -> str: - """ Get the URL to the camera audio source. """ - cam = self._get_camera(camera) - return cam.audio_url - # vim:sw=4:ts=4:et: