forked from platypush/platypush
Added camera.android.ipcam web panel
This commit is contained in:
parent
416c9ceb93
commit
93a3c72d4e
7 changed files with 192 additions and 28 deletions
|
@ -2,7 +2,6 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from flask import Response, Blueprint, send_from_directory
|
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 import template_folder
|
||||||
from platypush.backend.http.app.utils import authenticate, send_request
|
from platypush.backend.http.app.utils import authenticate, send_request
|
||||||
|
@ -18,7 +17,6 @@ __routes__ = [
|
||||||
|
|
||||||
|
|
||||||
def video_feed():
|
def video_feed():
|
||||||
camera: Optional[CameraPiPlugin] = None
|
|
||||||
camera_conf = Config.get('camera.pi') or {}
|
camera_conf = Config.get('camera.pi') or {}
|
||||||
camera = CameraPiPlugin(**camera_conf)
|
camera = CameraPiPlugin(**camera_conf)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
camera
|
|
@ -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();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{%
|
{%
|
||||||
with pluginIcons = {
|
with pluginIcons = {
|
||||||
'camera': 'fas fa-camera',
|
'camera': 'fas fa-camera',
|
||||||
|
'camera.android.ipcam': 'fab fa-android',
|
||||||
'camera.pi': 'fab fa-raspberry-pi',
|
'camera.pi': 'fab fa-raspberry-pi',
|
||||||
'camera.ir.mlx90640': 'fas fa-sun',
|
'camera.ir.mlx90640': 'fas fa-sun',
|
||||||
'execute': 'fas fa-play',
|
'execute': 'fas fa-play',
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script type="text/x-template" id="tmpl-camera-android-ipcam">
|
||||||
|
<div class="camera">
|
||||||
|
<div class="camera-selector">
|
||||||
|
<select ref="cameraSelector" @change="onCameraSelected" v-if="hasMultipleCameras">
|
||||||
|
<option v-for="(cam, name) in cameras"
|
||||||
|
:key="name" :value="name">
|
||||||
|
{% raw %}{{ name }}{% endraw %}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="camera-container">
|
||||||
|
<div class="no-frame" v-if="!streaming && !capturing">The camera is not active</div>
|
||||||
|
<img class="frame" ref="frame">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button type="button" @click="startStreaming" :disabled="capturing || loading" v-if="!streaming">
|
||||||
|
<i class="fa fa-play"></i> Start streaming video
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="stopStreaming" :disabled="capturing || loading" v-else>
|
||||||
|
<i class="fa fa-stop"></i> Stop streaming video
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="capture" :disabled="streaming || capturing || loading">
|
||||||
|
<i class="fas fa-camera"></i> Take snapshot
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button type="button" @click="recording = true" v-if="!recording" :disabled="loading">
|
||||||
|
<i class="fa fa-play"></i> Start streaming audio
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="recording = false" v-else :disabled="loading">
|
||||||
|
<i class="fa fa-stop"></i> Stop streaming audio
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" @click="flipCamera" :disabled="loading">
|
||||||
|
<i class="fas fa-retweet"></i> Flip camera
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sound-container">
|
||||||
|
<audio autoplay preload="none" ref="player" v-if="recording">
|
||||||
|
<source :src="cameras[selectedCamera].audio_url" type="audio/x-wav;codec=pcm">
|
||||||
|
Your browser does not support audio elements
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
|
@ -10,6 +10,9 @@ class AndroidCameraStatusResponse(CameraResponse):
|
||||||
..code-block:: json
|
..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",
|
"orientation": "landscape",
|
||||||
"idle": "off",
|
"idle": "off",
|
||||||
"audio_only": "off",
|
"audio_only": "off",
|
||||||
|
@ -71,6 +74,10 @@ class AndroidCameraStatusResponse(CameraResponse):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args,
|
def __init__(self, *args,
|
||||||
|
name: str = None,
|
||||||
|
stream_url: str = None,
|
||||||
|
image_url: str = None,
|
||||||
|
audio_url: str = None,
|
||||||
orientation: str = None,
|
orientation: str = None,
|
||||||
idle: str = None,
|
idle: str = None,
|
||||||
audio_only: str = None,
|
audio_only: str = None,
|
||||||
|
@ -117,6 +124,10 @@ class AndroidCameraStatusResponse(CameraResponse):
|
||||||
**kwargs):
|
**kwargs):
|
||||||
|
|
||||||
self.status = {
|
self.status = {
|
||||||
|
"name": name,
|
||||||
|
"stream_url": stream_url,
|
||||||
|
"image_url": image_url,
|
||||||
|
"audio_url": audio_url,
|
||||||
"orientation": orientation,
|
"orientation": orientation,
|
||||||
"idle": True if idle == "on" else False,
|
"idle": True if idle == "on" else False,
|
||||||
"audio_only": True if audio_only == "on" else False,
|
"audio_only": True if audio_only == "on" else False,
|
||||||
|
|
|
@ -148,19 +148,29 @@ class CameraAndroidIpcamPlugin(Plugin):
|
||||||
:return: True if the camera is available, False otherwise
|
:return: True if the camera is available, False otherwise
|
||||||
"""
|
"""
|
||||||
cameras = self._camera_name_to_idx.keys() if camera is None else [camera]
|
cameras = self._camera_name_to_idx.keys() if camera is None else [camera]
|
||||||
status = []
|
statuses = []
|
||||||
|
|
||||||
for cam in cameras:
|
for c in cameras:
|
||||||
try:
|
try:
|
||||||
status_data = self._exec('status.json', params={'show_avail': 1}, camera=cam).get('curvals', {})
|
if isinstance(camera, int):
|
||||||
status.append(AndroidCameraStatusResponse(
|
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()
|
**{k: v for k, v in status_data.items()
|
||||||
if k in AndroidCameraStatusResponse.attrs})
|
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
|
@action
|
||||||
def set_front_facing_camera(self, activate: bool = True, camera: Union[int, str] = None) -> bool:
|
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."""
|
"""Set video orientation."""
|
||||||
return self.change_setting('scenemode', scenemode, camera=camera)
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
Loading…
Reference in a new issue