Browse Source
* create widget and work to video * style widget stream photo and create preview photo * Add translate, save and style from widgetpull/1992/head
committed by
Igor Kulikov
6 changed files with 360 additions and 1 deletions
@ -0,0 +1,196 @@ |
|||||
|
/* |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
import './web-camera-input-widget.scss'; |
||||
|
|
||||
|
/* eslint-disable import/no-unresolved, import/default */ |
||||
|
import webCameraWidgetTemplate from './web-camera-input-widget.tpl.html'; |
||||
|
/* eslint-enable import/no-unresolved, import/default */ |
||||
|
|
||||
|
export default angular.module('thingsboard.widgets.webCameraWidget', []) |
||||
|
.directive('tbWebCameraWidget', webCameraWidget) |
||||
|
.name; |
||||
|
|
||||
|
/*@ngInject*/ |
||||
|
function webCameraWidget() { |
||||
|
return { |
||||
|
restrict: "E", |
||||
|
scope: true, |
||||
|
bindToController: { |
||||
|
ctx: '=' |
||||
|
}, |
||||
|
controller: WebCameraWidgetController, |
||||
|
controllerAs: 'vm', |
||||
|
templateUrl: webCameraWidgetTemplate |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function WebCameraWidgetController($element, $scope, $window, types, utils, attributeService, Fullscreen) { |
||||
|
let vm = this; |
||||
|
|
||||
|
vm.videoInput = []; |
||||
|
vm.videoDevice = ""; |
||||
|
vm.previewPhoto = ""; |
||||
|
vm.isShowCamera = false; |
||||
|
vm.isPreviewPhoto = false; |
||||
|
|
||||
|
let streamDevice = null; |
||||
|
let indexWebCamera = 0; |
||||
|
let videoElement = null; |
||||
|
let canvas = null; |
||||
|
let photoCamera = null; |
||||
|
let dataKeyType = ""; |
||||
|
|
||||
|
vm.getStream = getStream; |
||||
|
vm.createPhoto = createPhoto; |
||||
|
vm.takePhoto = takePhoto; |
||||
|
vm.switchWebCamera = switchWebCamera; |
||||
|
vm.cancelPhoto = cancelPhoto; |
||||
|
vm.closeCamera = closeCamera; |
||||
|
vm.savePhoto = savePhoto; |
||||
|
|
||||
|
vm.isEntityDetected = false; |
||||
|
vm.dataKeyDetected = false; |
||||
|
vm.isCameraSupport = false; |
||||
|
vm.isDeviceDetect = false; |
||||
|
|
||||
|
$scope.$watch('vm.ctx', function () { |
||||
|
if (vm.ctx && vm.ctx.datasources && vm.ctx.datasources.length) { |
||||
|
let datasource = vm.ctx.datasources[0]; |
||||
|
if (datasource.type === types.datasourceType.entity) { |
||||
|
if (datasource.entityType && datasource.entityId) { |
||||
|
if (vm.ctx.settings.widgetTitle && vm.ctx.settings.widgetTitle.length) { |
||||
|
$scope.titleTemplate = utils.customTranslation(vm.ctx.settings.widgetTitle, vm.ctx.settings.widgetTitle); |
||||
|
} else { |
||||
|
$scope.titleTemplate = vm.ctx.widgetConfig.title; |
||||
|
} |
||||
|
vm.isEntityDetected = true; |
||||
|
} |
||||
|
} |
||||
|
if (datasource.dataKeys.length) { |
||||
|
$scope.currentKey = datasource.dataKeys[0].name; |
||||
|
dataKeyType = datasource.dataKeys[0].type; |
||||
|
vm.dataKeyDetected = true; |
||||
|
} |
||||
|
if (hasGetUserMedia()) { |
||||
|
vm.isCameraSupport = true; |
||||
|
getDevices().then(gotDevices).then(() => { |
||||
|
vm.isDeviceDetect = !!vm.videoInput.length; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
function hasGetUserMedia() { |
||||
|
return !!($window.navigator.mediaDevices && $window.navigator.mediaDevices.getUserMedia); |
||||
|
} |
||||
|
|
||||
|
function takePhoto(){ |
||||
|
vm.isShowCamera = true; |
||||
|
videoElement = $element[0].querySelector('#videoStream'); |
||||
|
photoCamera = $element[0].querySelector('#photoCamera'); |
||||
|
canvas = $element[0].querySelector('canvas'); |
||||
|
Fullscreen.enable(photoCamera); |
||||
|
getStream(); |
||||
|
} |
||||
|
|
||||
|
function cancelPhoto() { |
||||
|
vm.isPreviewPhoto = false; |
||||
|
vm.previewPhoto = ""; |
||||
|
} |
||||
|
|
||||
|
function switchWebCamera() { |
||||
|
indexWebCamera = (indexWebCamera+1)%vm.videoInput.length; |
||||
|
vm.videoDevice = vm.videoInput[indexWebCamera].deviceId; |
||||
|
getStream(); |
||||
|
} |
||||
|
|
||||
|
function getDevices() { |
||||
|
return $window.navigator.mediaDevices.enumerateDevices(); |
||||
|
} |
||||
|
|
||||
|
function gotDevices(deviceInfos) { |
||||
|
for (const deviceInfo of deviceInfos) { |
||||
|
let device = { |
||||
|
deviceId: deviceInfo.deviceId, |
||||
|
label: "" |
||||
|
}; |
||||
|
if (deviceInfo.kind === 'videoinput') { |
||||
|
device.label = deviceInfo.label || `Camera ${vm.videoInput.length + 1}`; |
||||
|
vm.videoInput.push(device); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getStream() { |
||||
|
if (streamDevice !== null) { |
||||
|
streamDevice.getTracks().forEach(track => { |
||||
|
track.stop(); |
||||
|
}); |
||||
|
} |
||||
|
const constraints = { |
||||
|
video: {deviceId: vm.videoDevice !== "" ? {exact: vm.videoDevice} : undefined} |
||||
|
}; |
||||
|
return $window.navigator.mediaDevices.getUserMedia(constraints).then(gotStream); |
||||
|
} |
||||
|
|
||||
|
function gotStream(stream) { |
||||
|
streamDevice = stream; |
||||
|
if(vm.videoDevice === ""){ |
||||
|
indexWebCamera = vm.videoInput.findIndex(option => option.label === stream.getVideoTracks()[0].label); |
||||
|
indexWebCamera = indexWebCamera === -1 ? 0 : indexWebCamera; |
||||
|
vm.videoDevice = vm.videoInput[indexWebCamera].deviceId; |
||||
|
} |
||||
|
videoElement.srcObject = stream; |
||||
|
} |
||||
|
|
||||
|
function createPhoto() { |
||||
|
canvas.width = videoElement.videoWidth; |
||||
|
canvas.height = videoElement.videoHeight; |
||||
|
canvas.getContext('2d').drawImage(videoElement, 0, 0); |
||||
|
vm.previewPhoto = canvas.toDataURL('image/png'); |
||||
|
vm.isPreviewPhoto = true; |
||||
|
} |
||||
|
|
||||
|
function closeCamera(){ |
||||
|
Fullscreen.cancel(photoCamera); |
||||
|
vm.isShowCamera = false; |
||||
|
if (streamDevice !== null) { |
||||
|
streamDevice.getTracks().forEach(track => { |
||||
|
track.stop(); |
||||
|
}); |
||||
|
} |
||||
|
streamDevice = null; |
||||
|
videoElement.srcObject = null; |
||||
|
} |
||||
|
|
||||
|
function savePhoto(){ |
||||
|
let promiseData = null; |
||||
|
let datasource = vm.ctx.datasources[0]; |
||||
|
let saveData = [{ |
||||
|
key: datasource.dataKeys[0].name, |
||||
|
value: vm.previewPhoto |
||||
|
}]; |
||||
|
if(dataKeyType === types.dataKeyType.attribute){ |
||||
|
promiseData = attributeService.saveEntityAttributes(datasource.entityType, datasource.entityId, types.attributesScope.server.value, saveData); |
||||
|
} else if(dataKeyType === types.dataKeyType.timeseries){ |
||||
|
promiseData = attributeService.saveEntityTimeseries(datasource.entityType, datasource.entityId, "scope", saveData); |
||||
|
} |
||||
|
promiseData.then(()=>{ |
||||
|
vm.isPreviewPhoto = false; |
||||
|
closeCamera(); |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2019 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
|
||||
|
.tb-web-camera { |
||||
|
height: 100%; |
||||
|
|
||||
|
&__last-photo{ |
||||
|
width: 100%; |
||||
|
margin: 5px 0; |
||||
|
text-align: center; |
||||
|
border: solid 1px; |
||||
|
|
||||
|
&_text{ |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
left: 50%; |
||||
|
margin-top: -.625em; |
||||
|
transform: translate(-50%, -50%); |
||||
|
} |
||||
|
|
||||
|
&_img{ |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: contain; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.camera { |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
overflow: hidden; |
||||
|
|
||||
|
.camera-stream{ |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
object-fit: contain; |
||||
|
} |
||||
|
|
||||
|
.camera-controls { |
||||
|
position: absolute; |
||||
|
bottom: 0; |
||||
|
width: 100%; |
||||
|
padding: 0 5px 5px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.message-text{ |
||||
|
font-size: 18px; |
||||
|
color: #a0a0a0; |
||||
|
text-align: center; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,73 @@ |
|||||
|
<!-- |
||||
|
|
||||
|
Copyright © 2016-2019 The Thingsboard Authors |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
|
|
||||
|
--> |
||||
|
<div layout="column" layout-align="center center" class="tb-web-camera"> |
||||
|
<div ng-if="vm.isEntityDetected && vm.dataKeyDetected && vm.isCameraSupport && vm.isDeviceDetect" layout-fill> |
||||
|
<div ng-show="!vm.isShowCamera" layout="column" layout-align="space-between center" layout-fill> |
||||
|
<div class="tb-web-camera__last-photo" flex> |
||||
|
<span ng-show="!vm.ctx.data[0].data[0][1]" class="tb-web-camera__last-photo_text" translate>widgets.input-widgets.no-image</span> |
||||
|
<img ng-show="vm.ctx.data[0].data[0][1]" class="tb-web-camera__last-photo_img" ng-src="{{vm.ctx.data[0].data[0][1]}}" /> |
||||
|
</div> |
||||
|
<md-button class="md-raised md-primary" ng-click="vm.takePhoto()"">{{ "widgets.input-widgets.take-photo" | translate }}</md-button> |
||||
|
</div> |
||||
|
<div ng-show="vm.isShowCamera" layout="column" layout-align="center center" id="photoCamera"> |
||||
|
<div class="camera" ng-show="!vm.isPreviewPhoto"> |
||||
|
<video autoplay muted playsinline id="videoStream" class="camera-stream"></video> |
||||
|
<div class="camera-controls" layout="row" layout-wrap="" layout-align="space-between end"> |
||||
|
<div flex></div> |
||||
|
<md-button class="md-fab md-primary md-mini" aria-label="{{ 'widgets.input-widgets.switch-camera' | translate }}" |
||||
|
ng-click="vm.switchWebCamera()" ng-disabled="{{vm.videoInput.length < 2}}"> |
||||
|
<md-icon md-font-icon="switch_camera" aaria-label="{{ 'widgets.input-widgets.switch-camera' | translate }}">switch_camera</md-icon> |
||||
|
</md-button> |
||||
|
<md-button class="md-fab md-hue-2" aria-label="{{ 'widgets.input-widgets.take-photo' | translate }}" ng-click="vm.createPhoto()"> |
||||
|
<md-icon md-font-icon="photo_camera" aria-label="Take Photo">photo_camera</md-icon> |
||||
|
</md-button> |
||||
|
<md-button class="md-fab md-primary md-mini" aria-label="{{ 'action.cancel' | translate }}" ng-click="vm.closeCamera()"> |
||||
|
<md-icon md-font-icon="close" aria-label="{{ 'action.cancel' | translate }}">close</md-icon> |
||||
|
</md-button> |
||||
|
<div flex></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="camera" ng-show="vm.isPreviewPhoto"> |
||||
|
<img class="camera-stream" ng-src="{{vm.previewPhoto}}"> |
||||
|
<canvas style="display:none;"></canvas> |
||||
|
<div class="camera-controls" layout="row" layout-wrap="" layout-align="space-between end"> |
||||
|
<div flex></div> |
||||
|
<md-button class="md-fab md-primary" aria-label="{{ 'action.cancel' | translate }}" ng-click="vm.cancelPhoto()"> |
||||
|
<md-icon md-font-icon="close" aria-label="{{ 'action.cancel' | translate }}">close</md-icon> |
||||
|
</md-button> |
||||
|
<md-button class="md-fab md-hue-2" aria-label="{{ 'action.save' | translate }}" ng-click="vm.savePhoto()"> |
||||
|
<md-icon md-font-icon="check" aria-label="{{ 'action.save' | translate }}">check</md-icon> |
||||
|
</md-button> |
||||
|
<div flex></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="message-text" ng-hide="vm.isEntityDetected"> |
||||
|
{{ 'widgets.input-widgets.no-entity-selected' | translate }} |
||||
|
</div> |
||||
|
<div class="message-text" ng-if="vm.isEntityDetected && !vm.dataKeyDetected"> |
||||
|
{{ 'widgets.input-widgets.no-datakey-selected' | translate }} |
||||
|
</div> |
||||
|
<div class="message-text" ng-if="vm.isEntityDetected && vm.dataKeyDetected && !vm.isCameraSupport"> |
||||
|
{{ 'widgets.input-widgets.no-support-web-camera' | translate }} |
||||
|
</div> |
||||
|
<div class="message-text" ng-if="vm.isEntityDetected && vm.dataKeyDetected && vm.isCameraSupport && !vm.isDeviceDetect"> |
||||
|
{{ 'widgets.input-widgets.no-support-web-camera' | translate }} |
||||
|
</div> |
||||
|
</div> |
||||
Loading…
Reference in new issue