321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
///
|
|
/// Copyright © 2016-2020 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 {
|
|
Component,
|
|
ElementRef,
|
|
Inject,
|
|
Input,
|
|
NgZone,
|
|
OnDestroy,
|
|
OnInit,
|
|
ViewChild,
|
|
ViewEncapsulation
|
|
} from '@angular/core';
|
|
import { PageComponent } from '@shared/components/page.component';
|
|
import { WidgetContext } from '@home/models/widget-component.models';
|
|
import { Store } from '@ngrx/store';
|
|
import { AppState } from '@core/core.state';
|
|
import { Overlay } from '@angular/cdk/overlay';
|
|
import { UtilsService } from '@core/services/utils.service';
|
|
import { Datasource, DatasourceData, DatasourceType } from '@shared/models/widget.models';
|
|
import { WINDOW } from '@core/services/window.service';
|
|
import { AttributeService } from '@core/http/attribute.service';
|
|
import { EntityId } from '@shared/models/id/entity-id';
|
|
import { AttributeScope, DataKeyType } from '@shared/models/telemetry/telemetry.models';
|
|
import { Observable } from 'rxjs';
|
|
import { isString } from '@core/utils';
|
|
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
|
|
|
interface PhotoCameraInputWidgetSettings {
|
|
widgetTitle: string;
|
|
imageQuality: number;
|
|
imageFormat: string;
|
|
maxWidth: number;
|
|
maxHeight: number;
|
|
}
|
|
|
|
// @dynamic
|
|
@Component({
|
|
selector: 'tb-photo-camera-widget',
|
|
templateUrl: './photo-camera-input.component.html',
|
|
styleUrls: ['./photo-camera-input.component.scss'],
|
|
encapsulation: ViewEncapsulation.None
|
|
})
|
|
export class PhotoCameraInputWidgetComponent extends PageComponent implements OnInit, OnDestroy {
|
|
|
|
constructor(@Inject(WINDOW) private window: Window,
|
|
protected store: Store<AppState>,
|
|
private elementRef: ElementRef,
|
|
private ngZone: NgZone,
|
|
private overlay: Overlay,
|
|
private utils: UtilsService,
|
|
private attributeService: AttributeService,
|
|
private sanitizer: DomSanitizer
|
|
) {
|
|
super(store);
|
|
}
|
|
|
|
public get videoElement() {
|
|
return this.videoStreamRef.nativeElement;
|
|
}
|
|
|
|
public get canvasElement() {
|
|
return this.canvasRef.nativeElement;
|
|
}
|
|
|
|
public get videoWidth() {
|
|
const videoRatio = this.getVideoAspectRatio();
|
|
return Math.min(this.width, this.height * videoRatio);
|
|
}
|
|
|
|
public get videoHeight() {
|
|
const videoRatio = this.getVideoAspectRatio();
|
|
return Math.min(this.height, this.width / videoRatio);
|
|
}
|
|
|
|
private static DEFAULT_IMAGE_TYPE = 'image/jpeg';
|
|
private static DEFAULT_IMAGE_QUALITY = 0.92;
|
|
|
|
@Input()
|
|
ctx: WidgetContext;
|
|
|
|
@ViewChild('videoStream', {static: false}) videoStreamRef: ElementRef<HTMLVideoElement>;
|
|
@ViewChild('canvas', {static: false}) canvasRef: ElementRef<HTMLCanvasElement>;
|
|
|
|
private videoInputsIndex = 0;
|
|
private settings: PhotoCameraInputWidgetSettings;
|
|
private datasource: Datasource;
|
|
private width = 640;
|
|
private height = 480;
|
|
private availableVideoInputs: MediaDeviceInfo[];
|
|
private mediaStream: MediaStream;
|
|
|
|
isEntityDetected = false;
|
|
dataKeyDetected = false;
|
|
isProtocolHttps = false;
|
|
isCameraSupport = false;
|
|
isDeviceDetect = false;
|
|
isShowCamera = false;
|
|
isPreviewPhoto = false;
|
|
isHavePermissionCamera = true;
|
|
isLoading = false;
|
|
singleDevice = true;
|
|
updatePhoto = false;
|
|
previewPhoto: SafeUrl;
|
|
lastPhoto: SafeUrl;
|
|
|
|
private static hasGetUserMedia(): boolean {
|
|
return !!(window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia);
|
|
}
|
|
|
|
private static getAvailableVideoInputs(): Promise<MediaDeviceInfo[]> {
|
|
return new Promise((resolve, reject) => {
|
|
navigator.mediaDevices.enumerateDevices()
|
|
.then((devices: MediaDeviceInfo[]) => {
|
|
resolve(devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput'));
|
|
})
|
|
.catch(err => {
|
|
reject(err.message || err);
|
|
});
|
|
});
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
this.ctx.$scope.photoCameraInputWidget = this;
|
|
this.isLoading = true;
|
|
this.settings = this.ctx.settings;
|
|
this.datasource = this.ctx.datasources[0];
|
|
|
|
if (this.settings.widgetTitle && this.settings.widgetTitle.length) {
|
|
this.ctx.widgetTitle = this.utils.customTranslation(this.settings.widgetTitle, this.settings.widgetTitle);
|
|
} else {
|
|
this.ctx.widgetTitle = this.ctx.widgetConfig.title;
|
|
}
|
|
|
|
this.width = this.settings.maxWidth ? this.settings.maxWidth : 640;
|
|
this.height = this.settings.maxHeight ? this.settings.maxWidth : 480;
|
|
|
|
if (this.datasource.type === DatasourceType.entity) {
|
|
if (this.datasource.entityType && this.datasource.entityId) {
|
|
this.isEntityDetected = true;
|
|
}
|
|
}
|
|
if (this.datasource.dataKeys.length) {
|
|
this.dataKeyDetected = true;
|
|
}
|
|
this.detectAvailableDevices();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.stopMediaTracks();
|
|
}
|
|
|
|
private updateWidgetData(data: Array<DatasourceData>) {
|
|
const keyData = data[0].data;
|
|
if (keyData?.length && isString(keyData[0][1]) && keyData[0][1].startsWith('data:image/')) {
|
|
this.lastPhoto = this.sanitizer.bypassSecurityTrustUrl(keyData[0][1]);
|
|
}
|
|
}
|
|
|
|
public onDataUpdated() {
|
|
this.updateWidgetData(this.ctx.defaultSubscription.data);
|
|
this.ctx.detectChanges();
|
|
}
|
|
|
|
|
|
private detectAvailableDevices(): void {
|
|
if (this.window.location.protocol === 'https:' || this.window.location.hostname === 'localhost') {
|
|
this.isProtocolHttps = true;
|
|
|
|
if (PhotoCameraInputWidgetComponent.hasGetUserMedia()) {
|
|
this.isCameraSupport = true;
|
|
PhotoCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
|
|
this.isLoading = false;
|
|
this.isDeviceDetect = !!devices.length;
|
|
this.singleDevice = devices.length < 2;
|
|
this.availableVideoInputs = devices;
|
|
this.ctx.detectChanges();
|
|
}, () => {
|
|
this.isLoading = false;
|
|
this.availableVideoInputs = [];
|
|
}
|
|
);
|
|
} else {
|
|
this.isLoading = false;
|
|
}
|
|
} else {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
private getVideoAspectRatio(): number {
|
|
if (this.videoElement.videoWidth && this.videoElement.videoWidth > 0 &&
|
|
this.videoElement.videoHeight && this.videoElement.videoHeight > 0) {
|
|
return this.videoElement.videoWidth / this.videoElement.videoHeight;
|
|
}
|
|
return this.width / this.height;
|
|
}
|
|
|
|
private stopMediaTracks() {
|
|
if (this.mediaStream && this.mediaStream.getTracks) {
|
|
this.mediaStream.getTracks()
|
|
.forEach((track: MediaStreamTrack) => track.stop());
|
|
}
|
|
}
|
|
|
|
takePhoto() {
|
|
this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId, true);
|
|
}
|
|
|
|
closeCamera() {
|
|
this.stopMediaTracks();
|
|
this.videoElement.srcObject = null;
|
|
this.isShowCamera = false;
|
|
}
|
|
|
|
cancelPhoto() {
|
|
this.isPreviewPhoto = false;
|
|
this.previewPhoto = '';
|
|
}
|
|
|
|
savePhoto() {
|
|
this.updatePhoto = true;
|
|
let task: Observable<any>;
|
|
const entityId: EntityId = {
|
|
entityType: this.datasource.entityType,
|
|
id: this.datasource.entityId
|
|
};
|
|
const saveData = [{
|
|
key: this.datasource.dataKeys[0].name,
|
|
value: this.previewPhoto
|
|
}];
|
|
if (this.datasource.dataKeys[0].type === DataKeyType.attribute) {
|
|
task = this.attributeService.saveEntityAttributes(entityId, AttributeScope.SERVER_SCOPE, saveData);
|
|
} else if (this.datasource.dataKeys[0].type === DataKeyType.timeseries) {
|
|
task = this.attributeService.saveEntityTimeseries(entityId, 'scope', saveData);
|
|
}
|
|
task.subscribe(() => {
|
|
this.isPreviewPhoto = false;
|
|
this.updatePhoto = false;
|
|
this.closeCamera();
|
|
}, () => {
|
|
this.updatePhoto = false;
|
|
});
|
|
}
|
|
|
|
switchWebCamera() {
|
|
this.videoInputsIndex = (this.videoInputsIndex + 1) % this.availableVideoInputs.length;
|
|
this.stopMediaTracks();
|
|
this.inititedVideoStream(this.availableVideoInputs[this.videoInputsIndex].deviceId);
|
|
}
|
|
|
|
createPhoto() {
|
|
this.canvasElement.width = this.videoWidth;
|
|
this.canvasElement.height = this.videoHeight;
|
|
this.canvasElement.getContext('2d').drawImage(this.videoElement, 0, 0, this.videoWidth, this.videoHeight);
|
|
|
|
const mimeType: string = this.settings.imageFormat ? this.settings.imageFormat : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
|
|
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : PhotoCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
|
|
this.previewPhoto = this.canvasElement.toDataURL(mimeType, quality);
|
|
this.isPreviewPhoto = true;
|
|
}
|
|
|
|
private inititedVideoStream(deviceId?: string, init = false) {
|
|
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
|
|
const videoTrackConstraints = {
|
|
video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
|
|
};
|
|
|
|
window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
|
|
if (init) {
|
|
this.isShowCamera = true;
|
|
}
|
|
this.mediaStream = stream;
|
|
this.videoElement.srcObject = stream;
|
|
this.ctx.detectChanges();
|
|
}, () => {
|
|
this.isHavePermissionCamera = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
get textMessage() {
|
|
if (this.isLoading) {
|
|
return '';
|
|
}
|
|
if (!this.isProtocolHttps) {
|
|
return 'widgets.input-widgets.enable-https-use-widget';
|
|
}
|
|
if (!this.isCameraSupport) {
|
|
return 'widgets.input-widgets.no-support-web-camera';
|
|
}
|
|
if (!this.isEntityDetected) {
|
|
return 'widgets.input-widgets.no-entity-selected';
|
|
}
|
|
if (!this.dataKeyDetected) {
|
|
return 'widgets.input-widgets.no-datakey-selected';
|
|
}
|
|
if (!this.isDeviceDetect) {
|
|
return 'widgets.input-widgets.no-found-your-camera';
|
|
}
|
|
if (!this.isHavePermissionCamera) {
|
|
return 'widgets.input-widgets.no-permission-camera';
|
|
}
|
|
return null;
|
|
}
|
|
}
|