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