UNPKG

ngx-simple-webcam

Version:

A simple Angular 4+ Webcam-Component. Pure &amp; minimal, no Flash-Fallback. <a href="https://basst314.github.io/ngx-simple-webcam/?" target="_blank">See the Demo!</a>

555 lines (545 loc) 21.3 kB
import { Component, EventEmitter, Input, NgModule, Output, ViewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; /** * Container class for a captured webcam image * @author basst314 */ class WebcamImage { /** * @param {?} imageAsDataUrl * @param {?} mimeType */ constructor(imageAsDataUrl, mimeType) { this._mimeType = null; this._imageAsBase64 = null; this._imageAsDataUrl = null; this._mimeType = mimeType; this._imageAsDataUrl = imageAsDataUrl; } /** * Get the base64 encoded image data * @return {?} */ get imageAsBase64() { return this._imageAsBase64 ? this._imageAsBase64 : this._imageAsBase64 = this.getDataFromDataUrl(this._imageAsDataUrl); } /** * Get the encoded image as dataUrl * @return {?} */ get imageAsDataUrl() { return this._imageAsDataUrl; } /** * Extracts the Base64 data out of the given dataUrl. * @param {?} dataUrl * @return {?} */ getDataFromDataUrl(dataUrl) { return dataUrl.replace("data:" + this._mimeType + ";base64,", ""); } } class WebcamUtil { /** * Lists available videoInput devices * @return {?} */ static getAvailableVideoInputs() { if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { return Promise.reject("enumerateDevices() not supported."); } return new Promise((resolve, reject) => { navigator.mediaDevices.enumerateDevices() .then((devices) => { resolve(devices.filter((device) => device.kind === 'videoinput')); }) .catch(err => { reject(err.message || err); }); }); } } class WebcamComponent { constructor() { /** * Defines the max width of the webcam area in px */ this.width = 640; /** * Defines the max height of the webcam area in px */ this.height = 480; /** * Defines base constraints to apply when requesting video track from UserMedia */ this.videoOptions = WebcamComponent.DEFAULT_VIDEO_OPTIONS; /** * Flag to enable/disable camera switch. If enabled, a switch icon will be displayed if multiple cameras were found */ this.allowCameraSwitch = true; /** * MediaStream object in use for streaming UserMedia data */ this.mediaStream = null; /** * available video devices */ this.availableVideoInputs = []; /** * Index of active video in availableVideoInputs */ this.activeVideoInputIndex = -1; /** * Indicates whether the video device is ready to be switched */ this.videoInitialized = false; /** * EventEmitter which fires when an image has been captured */ this.imageCapture = new EventEmitter(); /** * Emits a mediaError if webcam cannot be initialized (e.g. missing user permissions) */ this.initError = new EventEmitter(); /** * Emits when the webcam video was clicked */ this.imageClick = new EventEmitter(); /** * Emits the active deviceId after the active video device was switched */ this.cameraSwitched = new EventEmitter(); this.activeVideoSettings = null; } /** * @return {?} */ ngAfterViewInit() { this.detectAvailableDevices() .then((devices) => { // start first device this.switchToVideoInput(devices.length > 1 ? devices[1].deviceId : (devices.length > 0 ? devices[0].deviceId : null)); }) .catch((err) => { this.initError.next(/** @type {?} */ ({ message: err })); // fallback: still try to load webcam, even if device enumeration failed this.switchToVideoInput(null); }); } /** * @return {?} */ ngOnDestroy() { this.stopMediaTracks(); this.unsubscribeFromSubscriptions(); } /** * If the given Observable emits, an image will be captured and emitted through 'imageCapture' EventEmitter * @param {?} trigger * @return {?} */ set trigger(trigger) { if (this.triggerSubscription) { this.triggerSubscription.unsubscribe(); } // Subscribe to events from this Observable to take snapshots this.triggerSubscription = trigger.subscribe(() => { this.takeSnapshot(); }); } /** * @param {?} switchCamera * @return {?} */ set switchCamera(switchCamera) { if (this.switchCameraSubscription) { this.switchCameraSubscription.unsubscribe(); } // Subscribe to events from this Observable to switch video device this.switchCameraSubscription = switchCamera.subscribe((value) => { if (typeof value === 'string') { // deviceId was specified this.switchToVideoInput(value); } else { // direction was specified this.rotateVideoInput(value !== false); } }); } /** * Get MediaTrackConstraints to request streaming the given device * @param {?} deviceId * @param {?} baseMediaTrackConstraints base constraints to merge deviceId-constraint into * @return {?} */ static getMediaConstraintsForDevice(deviceId, baseMediaTrackConstraints) { let /** @type {?} */ result = baseMediaTrackConstraints ? baseMediaTrackConstraints : this.DEFAULT_VIDEO_OPTIONS; if (deviceId) { result.deviceId = { exact: deviceId }; } return result; } /** * Tries to harvest the deviceId from the given mediaStreamTrack object. * Browsers populate this object differently; this method tries some different approaches * to read the id. * @param {?} mediaStreamTrack * @return {?} */ static getDeviceIdFromMediaStreamTrack(mediaStreamTrack) { if (mediaStreamTrack.getSettings && mediaStreamTrack.getSettings() && mediaStreamTrack.getSettings().deviceId) { return mediaStreamTrack.getSettings().deviceId; } else if (mediaStreamTrack.getConstraints && mediaStreamTrack.getConstraints() && mediaStreamTrack.getConstraints().deviceId) { let /** @type {?} */ deviceIdObj = mediaStreamTrack.getConstraints().deviceId; return WebcamComponent.getValueFromConstrainDOMString(deviceIdObj); } } /** * Tries to harvest the facingMode from the given mediaStreamTrack object. * Browsers populate this object differently; this method tries some different approaches * to read the value. * @param {?} mediaStreamTrack * @return {?} */ static getFacingModeFromMediaStreamTrack(mediaStreamTrack) { if (mediaStreamTrack) { if (mediaStreamTrack.getSettings && mediaStreamTrack.getSettings() && mediaStreamTrack.getSettings().facingMode) { return mediaStreamTrack.getSettings().facingMode; } else if (mediaStreamTrack.getConstraints && mediaStreamTrack.getConstraints() && mediaStreamTrack.getConstraints().facingMode) { let /** @type {?} */ facingModeConstraint = mediaStreamTrack.getConstraints().facingMode; return WebcamComponent.getValueFromConstrainDOMString(facingModeConstraint); } } } /** * Determines whether the given mediaStreamTrack claims itself as user facing * @param {?} mediaStreamTrack * @return {?} */ static isUserFacing(mediaStreamTrack) { let /** @type {?} */ facingMode = WebcamComponent.getFacingModeFromMediaStreamTrack(mediaStreamTrack); return facingMode ? "user" === facingMode.toLowerCase() : false; } /** * Extracts the value from the given ConstrainDOMString * @param {?} constrainDOMString * @return {?} */ static getValueFromConstrainDOMString(constrainDOMString) { if (constrainDOMString) { if (constrainDOMString instanceof String) { return String(constrainDOMString); } else if (Array.isArray(constrainDOMString) && Array(constrainDOMString).length > 0) { return String(constrainDOMString[0]); } else if (typeof constrainDOMString === "object") { if (constrainDOMString["exact"]) { return String(constrainDOMString["exact"]); } else if (constrainDOMString["ideal"]) { return String(constrainDOMString["ideal"]); } } } return null; } /** * Takes a snapshot of the current webcam's view and emits the image as an event * @return {?} */ takeSnapshot() { // set canvas size to actual video size let /** @type {?} */ _video = this.video.nativeElement; let /** @type {?} */ dimensions = { width: this.width, height: this.height }; if (_video.videoWidth) { dimensions.width = _video.videoWidth; dimensions.height = _video.videoHeight; } let /** @type {?} */ _canvas = this.canvas.nativeElement; _canvas.width = dimensions.width; _canvas.height = dimensions.height; // paint snapshot image to canvas _canvas.getContext('2d').drawImage(this.video.nativeElement, 0, 0); // read canvas content as image // TODO allow mimeType options as Input() let /** @type {?} */ mimeType = "image/jpeg"; let /** @type {?} */ dataUrl = _canvas.toDataURL(mimeType); this.imageCapture.next(new WebcamImage(dataUrl, mimeType)); } /** * Switches to the next/previous video device * @param {?} forward * @return {?} */ rotateVideoInput(forward) { if (this.availableVideoInputs && this.availableVideoInputs.length > 1) { let /** @type {?} */ increment = forward ? 1 : (this.availableVideoInputs.length - 1); this.switchToVideoInput(this.availableVideoInputs[(this.activeVideoInputIndex + increment) % this.availableVideoInputs.length].deviceId); } } /** * Switches the camera-view to the specified video device * @param {?} deviceId * @return {?} */ switchToVideoInput(deviceId) { this.videoInitialized = false; this.stopMediaTracks(); this.initWebcam(deviceId, this.videoOptions); } /** * @return {?} */ get videoWidth() { let /** @type {?} */ videoRatio = this.getVideoAspectRatio(this.activeVideoSettings); return Math.min(this.width, this.height * videoRatio); } /** * @return {?} */ get videoHeight() { let /** @type {?} */ videoRatio = this.getVideoAspectRatio(this.activeVideoSettings); return Math.min(this.height, this.width / videoRatio); } /** * @return {?} */ get videoStyleClasses() { let /** @type {?} */ classes = ""; if (this.isMirrorImage()) { classes += "mirrored "; } return classes.trim(); } /** * Return the video aspect ratio from the given mediaTrackSettings, if possible; * Otherwise, calculate given the width/height parameters only * @param {?} mediaTrackSettings * @return {?} */ getVideoAspectRatio(mediaTrackSettings) { if (mediaTrackSettings) { if (mediaTrackSettings.aspectRatio) { // if ratio is present - use it return mediaTrackSettings.aspectRatio; } else if (mediaTrackSettings.width && mediaTrackSettings.width > 0 && mediaTrackSettings.height && mediaTrackSettings.height > 0) { // if width+height are present - calculate ratio return mediaTrackSettings.width / mediaTrackSettings.height; } } // nothing present in mediaTrackSettings - calculate ratio based on width/height params return this.width / this.height; } /** * Init webcam live view * @param {?} deviceId * @param {?} userVideoTrackConstraints * @return {?} */ initWebcam(deviceId, userVideoTrackConstraints) { let /** @type {?} */ _video = this.video.nativeElement; if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { // merge deviceId -> userVideoTrackConstraints let /** @type {?} */ videoTrackConstraints = WebcamComponent.getMediaConstraintsForDevice(deviceId, userVideoTrackConstraints); navigator.mediaDevices.getUserMedia(/** @type {?} */ ({ video: videoTrackConstraints })) .then((stream) => { this.mediaStream = stream; _video.srcObject = stream; _video.play(); this.activeVideoSettings = stream.getVideoTracks()[0].getSettings(); let /** @type {?} */ activeDeviceId = WebcamComponent.getDeviceIdFromMediaStreamTrack(stream.getVideoTracks()[0]); this.activeVideoInputIndex = activeDeviceId ? this.availableVideoInputs .findIndex((mediaDeviceInfo) => mediaDeviceInfo.deviceId === activeDeviceId) : -1; this.videoInitialized = true; this.cameraSwitched.next(activeDeviceId); }) .catch((err) => { this.initError.next(/** @type {?} */ ({ message: err.message, mediaStreamError: err })); }); } else { this.initError.next(/** @type {?} */ ({ message: "Cannot read UserMedia from MediaDevices." })); } } /** * @return {?} */ getActiveVideoTrack() { return this.mediaStream ? this.mediaStream.getVideoTracks()[0] : null; } /** * @return {?} */ isMirrorImage() { if (!this.getActiveVideoTrack()) { return false; } // check for explicit mirror override parameter { let /** @type {?} */ mirror = "auto"; if (this.mirrorImage) { if (typeof this.mirrorImage === "string") { mirror = String(this.mirrorImage).toLowerCase(); } else { // WebcamMirrorProperties if (this.mirrorImage.x) { mirror = this.mirrorImage.x.toLowerCase(); } } } switch (mirror) { case "always": return true; case "never": return false; } } // default: enable mirroring if webcam is user facing return WebcamComponent.isUserFacing(this.getActiveVideoTrack()); } /** * Stops all active media tracks. * This prevents the webcam from being indicated as active, * even if it is no longer used by this component. * @return {?} */ stopMediaTracks() { if (this.mediaStream && this.mediaStream.getTracks) { // getTracks() returns all media tracks (video+audio) this.mediaStream.getTracks() .forEach((track) => track.stop()); } } /** * Unsubscribe from all open subscriptions * @return {?} */ unsubscribeFromSubscriptions() { if (this.triggerSubscription) { this.triggerSubscription.unsubscribe(); } if (this.switchCameraSubscription) { this.switchCameraSubscription.unsubscribe(); } } /** * Reads available input devices * @return {?} */ detectAvailableDevices() { return new Promise((resolve, reject) => { WebcamUtil.getAvailableVideoInputs() .then((devices) => { this.availableVideoInputs = devices; resolve(devices); }) .catch(err => { this.availableVideoInputs = []; reject(err); }); }); } } WebcamComponent.DEFAULT_VIDEO_OPTIONS = { facingMode: 'environment' }; WebcamComponent.decorators = [ { type: Component, args: [{ selector: 'webcam', template: ` <div class="webcam-wrapper" (click)="imageClick.next();"> <video #video [width]="videoWidth" [height]="videoHeight" [class]="videoStyleClasses" autoplay muted playsinline></video> <div class="camera-switch" *ngIf="allowCameraSwitch && availableVideoInputs.length > 1 && videoInitialized" (click)="rotateVideoInput(true)"></div> <canvas #canvas [width]="width" [height]="height"></canvas> </div> `, styles: [` .webcam-wrapper { display: inline-block; position: relative; line-height: 0; } .webcam-wrapper video.mirrored { -webkit-transform: scale(-1, 1); transform: scale(-1, 1); } .webcam-wrapper canvas { display: none; } .webcam-wrapper .camera-switch { background-color: rgba(0, 0, 0, 0.1); background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAE9UlEQVR42u2aT2hdRRTGf+cRQqghSqihdBFDkRISK2KDfzDWxHaRQHEhaINKqa1gKQhd6EZLN+IidCH+Q0oWIkVRC21BQxXRitVaSbKoJSGtYGoK2tQ/tU1jY5v0c5F54Xl7b/KSO/PyEt+3e5f75p7zzZwzZ74zUEIJJfyfYaEGllQGVAGZlENdBy6Z2cSiYFTSKkkfS/pH/nBF0kFJdUW9AiRVASeAukD8DgNrzOySrwEzng18KaDzALXuG8W3AiStAvqBisBRNg40mtlPxbYCOgvgPO4bncWW+JpVeDQXRQhIygDfA00F5r0XuNfMrgclQFI98DDQCNQA5ZFXqoCWBVp8XwHRHeEqcN7loy/NbHBesyqpQ1KfFj/6nC+ZvFaApFrgPaCZpYVvgCfNbDiRAElNwGFg+RIt/X8H2s2s9wYCJDUAR4HqJX7++RN40MwGpgmQVAH0AQ2BPz4AHHPl8nBOAqtyFWQjsA6oL4Ada81sPDv7uwImod8kvSJp9RyS8O2SXnb/DYVd2Y9VSroQ4ANXJO2WVJmixqh0kzMWwL4LkiqRtDnA4D1zmfE8j9g9AezcnAHaPcfXdbfdnPZ2Yps6+DwAvO/Z1naTdApY7Xng48BDZnY1MpMVQBuw3iXc5Tnb0wBwBPjUzP6eoezuArZ6svM0geJLkvZEYnl3nkntoqROSbckSW2Suj3ZOIangc7GPJuUtNGdFIfmMeavktoSSKiW9LMPw30Q8JqkekmjCbOZRhuclLQjgYSNxUBAj6RyZ9ATgUJpUtJTCSR8vpAEXHAyWK5BXYFIGHOlepSAloUk4NEYgyoknQhEwhFJ0e8h6VSaQeerCb5uZgdi9utxYBNwOUD93hIVXswM4INCi6K9wAszFC2DwLOBDjHbYp59karIUnRdzYy/3ClqVklaUhfwTICj7K25OqA7a4wWagVsm4Me/xzwg2cCqqONFzO7DPxSCAJi436GUBgHHguQD2oTlJ55oSzP9ybccsttSJw1szdjFOSnI/8dTCGZHwcORp4Nx7y3B1iZ8/sm4MW8/Euxg5wIsS/HaAp3zeP4/G7obRDXI4jiTIA22H7Xdc7X+S3A5lC7QBQ357aq3VAjCeSkwUfAJrfvz+R8A9ADLAtZB+TinpjC5JMA+//jwPZZnF8G7J+L8z4IWB/zbG+gIujVWfLBW/NStVMmqaG4POJRsIjix7h8IGnLQuoBbQki5sVAJHyYm7YkNaRRtXwQ8G1cHpX0iKRrgUjYno17Sf0LrQhJUkdCeHWkVITGJI0k1QeS3ikGSUzOyJUJJNznYneuOCnpTldcxa2kP3xJYqOeSDjqZG8ShJLnE8TTuMS6Iyu1BW7djZqkfo9N0QOuYJmYQddfB7RG+gLTNzqAY9FrL+5/nwEbvDdJJe3zzOrhNP3AWRqmk55t3ZcBuj3b2gb0Sbrbo/NNzk7fFzu7s/E5EiC+rrmeQU0Kx2skvRFoOx2ZzlmSdgbsw49JetvtBpk8nM64d/cGbNtJ0s7cGyJlwHeEv+t3nqnLSgPAUOSGyG3AHUxdzqoJbEcvcL+ZTeTeEapzJKxgaeOcc/7Mf06D7kFrguS0VDAMtGadv+E47DT9tcChJej8ISfpD+abgTe45uOkFi8mnQ+JBVQ+d4VXuOptjavcyot8pq86mfwk8LWZnaOEEkoooYQSSojDv8AhQNeGfe0jAAAAAElFTkSuQmCC"); background-repeat: no-repeat; border-radius: 5px; position: absolute; right: 13px; top: 10px; height: 48px; width: 48px; background-size: 80%; cursor: pointer; background-position: center; -webkit-transition: background-color 0.2s ease; transition: background-color 0.2s ease; } .webcam-wrapper .camera-switch:hover { background-color: rgba(0, 0, 0, 0.18); } `] },] }, ]; /** * @nocollapse */ WebcamComponent.ctorParameters = () => []; WebcamComponent.propDecorators = { 'width': [{ type: Input },], 'height': [{ type: Input },], 'videoOptions': [{ type: Input },], 'allowCameraSwitch': [{ type: Input },], 'mirrorImage': [{ type: Input },], 'imageCapture': [{ type: Output },], 'initError': [{ type: Output },], 'imageClick': [{ type: Output },], 'cameraSwitched': [{ type: Output },], 'video': [{ type: ViewChild, args: ['video',] },], 'canvas': [{ type: ViewChild, args: ['canvas',] },], 'trigger': [{ type: Input },], 'switchCamera': [{ type: Input },], }; const COMPONENTS = [ WebcamComponent ]; class WebcamModule { } WebcamModule.decorators = [ { type: NgModule, args: [{ imports: [ CommonModule ], declarations: [ COMPONENTS ], exports: [ COMPONENTS ] },] }, ]; /** * @nocollapse */ WebcamModule.ctorParameters = () => []; class WebcamInitError { constructor() { this.message = null; this.mediaStreamError = null; } } class WebcamMirrorProperties { } /** * Generated bundle index. Do not edit. */ export { WebcamModule, WebcamComponent, WebcamImage, WebcamInitError, WebcamMirrorProperties, WebcamUtil }; //# sourceMappingURL=ngx-simple-webcam.js.map