thingsboard/ui-ngx/src/app/modules/home/components/widget/lib/web-camera-input.component.ts
2020-08-11 19:20:14 +03:00

279 lines
8.9 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 WebCameraInputWidgetSettings {
widgetTitle: string;
imageQuality: number;
imageFormat: string;
maxWidth: number;
maxHeight: number;
}
// @dynamic
@Component({
selector: 'tb-web-camera-widget',
templateUrl: './web-camera-input.component.html',
styleUrls: ['./web-camera-input.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class WebCameraInputWidgetComponent 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: true}) videoStreamRef: ElementRef<HTMLVideoElement>;
@ViewChild('canvas', {static: true}) canvasRef: ElementRef<HTMLCanvasElement>;
private videoInputsIndex = 0;
private settings: WebCameraInputWidgetSettings;
private datasource: Datasource;
private width = 640;
private height = 480;
private availableVideoInputs: MediaDeviceInfo[];
private mediaStream: MediaStream;
isEntityDetected = false;
dataKeyDetected = false;
isCameraSupport = false;
isDeviceDetect = false;
isShowCamera = false;
isPreviewPhoto = 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.webCameraInputWidget = this;
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.ngZone.run(() => {
this.updateWidgetData(this.ctx.defaultSubscription.data);
this.ctx.detectChanges();
});
}
private detectAvailableDevices(): void {
if (WebCameraInputWidgetComponent.hasGetUserMedia()) {
this.isCameraSupport = true;
WebCameraInputWidgetComponent.getAvailableVideoInputs().then((devices) => {
this.isDeviceDetect = !!devices.length;
this.singleDevice = devices.length < 2;
this.availableVideoInputs = devices;
this.ctx.detectChanges();
}, () => {
this.availableVideoInputs = [];
}
)
}
}
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.isShowCamera = true;
this.initWebCamera(this.availableVideoInputs[this.videoInputsIndex].deviceId);
}
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.initWebCamera(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 : WebCameraInputWidgetComponent.DEFAULT_IMAGE_TYPE;
const quality: number = this.settings.imageQuality ? this.settings.imageQuality : WebCameraInputWidgetComponent.DEFAULT_IMAGE_QUALITY;
this.previewPhoto = this.sanitizer.bypassSecurityTrustUrl(this.canvasElement.toDataURL(mimeType, quality));
this.isPreviewPhoto = true;
}
private initWebCamera(deviceId?: string) {
if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
const videoTrackConstraints = {
video: {deviceId: deviceId !== '' ? {exact: deviceId} : undefined}
};
window.navigator.mediaDevices.getUserMedia(videoTrackConstraints).then((stream: MediaStream) => {
this.mediaStream = stream;
this.videoElement.srcObject = stream;
})
}
}
}