ngx-simple-webcam
Version:
A simple Angular 4+ Webcam-Component. Pure & 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
JavaScript
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