Skip to content

Commit

Permalink
Add mirroring functionality to enhance user experience when running a…
Browse files Browse the repository at this point in the history
… user-facing webcam
  • Loading branch information
basst314 committed Aug 26, 2018
1 parent 99dd4c9 commit 6bd8a01
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 15 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Try out the <a href="https://basst314.github.io/ngx-webcam/?" target="_blank">Li
Runtime Dependencies:
* Angular: `^4.0.0 || ^5.0.0 || ^6.0.0`
* RxJs: `^5.0.0 || ^6.0.0`
* App must be served on secure context (https or localhost)
* App must be served on a secure context (https or localhost)

Client:
* [Current Browser](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Browser_compatibility) w/ HTML5 and WebRTC/UserMedia support (Chrome >53, Safari >11, Firefox >38, Edge)
Expand Down Expand Up @@ -64,6 +64,7 @@ This section describes the basic Inputs/Outputs of the component.
* `width: number`: The maximal video width of the webcam live view.
* `height: number`: The maximal video height of the webcam live view. The actual view will be placed within these boundaries, respecting the aspect ratio of the video stream.
* `videoOptions: MediaTrackConstraints`: Defines constraints ([MediaTrackConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints)) to apply when requesting the video track.
* `mirrorImage: string | WebcamMirrorProperties`: Flag to control image mirroring. Default: If a webcam claims to be user-facing, the image will be mirrored (x-axis) to provide a better user experience. A value of `never` will prevent mirroring, whereas a value of `always` will mirror every webcam stream, even if the webcam cannot be detected as user-facing. For future extensions, the `WebcamMirrorProperties` object can also be used to set these properties.
* `allowCameraSwitch: boolean`: Flag to enable/disable camera switch. If enabled, a switch icon will be displayed if multiple cameras are found.
* `switchCamera: Observable<boolean|string>`: Can be used to cycle through available cameras (true=forward, false=backwards), or to switch to a specific device by deviceId (string).

Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
<link rel="stylesheet" href="styles.34c57ab7888ec1573f9c.css"></head>
<body>
<app-root></app-root>
<script type="text/javascript" src="runtime.a66f828dca56eeb90e02.js"></script><script type="text/javascript" src="polyfills.58bd45eebb9814dfcc0a.js"></script><script type="text/javascript" src="main.15e19bcb6ef46f19ec17.js"></script></body>
<script type="text/javascript" src="runtime.a66f828dca56eeb90e02.js"></script><script type="text/javascript" src="polyfills.58bd45eebb9814dfcc0a.js"></script><script type="text/javascript" src="main.4a9bc930d379c3f4ea24.js"></script></body>
</html>

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export * from './src/app/modules/webcam/webcam.module';
export * from './src/app/modules/webcam/webcam/webcam.component';
export * from './src/app/modules/webcam/domain/webcam-image';
export * from './src/app/modules/webcam/domain/webcam-init-error';
export * from './src/app/modules/webcam/domain/webcam-mirror-properties';
export * from './src/app/modules/webcam/util/webcam.util';
3 changes: 3 additions & 0 deletions src/app/modules/webcam/domain/webcam-mirror-properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class WebcamMirrorProperties {
public x: string; // ["auto", "always", "never"]
}
2 changes: 1 addition & 1 deletion src/app/modules/webcam/webcam/webcam.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="webcam-wrapper" (click)="imageClick.next();">
<video #video [width]="videoWidth" [height]="videoHeight" autoplay muted playsinline></video>
<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>
4 changes: 4 additions & 0 deletions src/app/modules/webcam/webcam/webcam.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
position: relative;
line-height: 0;

video.mirrored {
transform: scale(-1, 1);
}

canvas {
display: none;
}
Expand Down
110 changes: 99 additions & 11 deletions src/app/modules/webcam/webcam/webcam.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {WebcamInitError} from "../domain/webcam-init-error";
import {WebcamImage} from "../domain/webcam-image";
import {Observable, Subscription} from "rxjs";
import {WebcamUtil} from "../util/webcam.util";
import {WebcamMirrorProperties} from "../domain/webcam-mirror-properties";

@Component({
selector: 'webcam',
Expand All @@ -23,6 +24,8 @@ export class WebcamComponent implements AfterViewInit, OnDestroy {
@Input() public videoOptions: MediaTrackConstraints = WebcamComponent.DEFAULT_VIDEO_OPTIONS;
/** Flag to enable/disable camera switch. If enabled, a switch icon will be displayed if multiple cameras were found */
@Input() public allowCameraSwitch: boolean = true;
/** Parameter to control image mirroring (i.e. for user-facing camera). ["auto", "always", "never"] */
@Input() public mirrorImage: string | WebcamMirrorProperties;

/** Subscription to switchCamera events */
private switchCameraSubscription: Subscription;
Expand Down Expand Up @@ -127,22 +130,61 @@ export class WebcamComponent implements AfterViewInit, OnDestroy {
* @returns {string} deviceId if found in the mediaStreamTrack
*/
private static getDeviceIdFromMediaStreamTrack(mediaStreamTrack: MediaStreamTrack): string {
if (mediaStreamTrack.getSettings() && mediaStreamTrack.getSettings().deviceId) {
if (mediaStreamTrack.getSettings && mediaStreamTrack.getSettings() && mediaStreamTrack.getSettings().deviceId) {
return mediaStreamTrack.getSettings().deviceId;
} else if (mediaStreamTrack.getConstraints() && mediaStreamTrack.getConstraints().deviceId) {
} else if (mediaStreamTrack.getConstraints && mediaStreamTrack.getConstraints() && mediaStreamTrack.getConstraints().deviceId) {
let deviceIdObj: ConstrainDOMString = mediaStreamTrack.getConstraints().deviceId;
if (deviceIdObj instanceof String) {
return String(deviceIdObj);
} else if (Array.isArray(deviceIdObj) && Array(deviceIdObj).length > 0) {
return deviceIdObj[0];
} else if (typeof deviceIdObj === "object") {
if (deviceIdObj["exact"]) {
return deviceIdObj["exact"];
} else if (deviceIdObj["ideal"]) {
return deviceIdObj["ideal"];
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} mediaStreamTrack
* @returns {string} facingMode if found in the mediaStreamTrack
*/
private static getFacingModeFromMediaStreamTrack(mediaStreamTrack: MediaStreamTrack): string {
if (mediaStreamTrack) {
if (mediaStreamTrack.getSettings && mediaStreamTrack.getSettings() && mediaStreamTrack.getSettings().facingMode) {
return mediaStreamTrack.getSettings().facingMode;
} else if (mediaStreamTrack.getConstraints && mediaStreamTrack.getConstraints() && mediaStreamTrack.getConstraints().facingMode) {
let facingModeConstraint: ConstrainDOMString = mediaStreamTrack.getConstraints().facingMode;
return WebcamComponent.getValueFromConstrainDOMString(facingModeConstraint);
}
}
}

/**
* Determines whether the given mediaStreamTrack claims itself as user facing
* @param mediaStreamTrack
*/
private static isUserFacing(mediaStreamTrack: MediaStreamTrack): boolean {
let facingMode: string = WebcamComponent.getFacingModeFromMediaStreamTrack(mediaStreamTrack);
return facingMode ? "user" === facingMode.toLowerCase() : false;
}

/**
* Extracts the value from the given ConstrainDOMString
* @param constrainDOMString
*/
private static getValueFromConstrainDOMString(constrainDOMString: ConstrainDOMString): string {
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;
}

/**
Expand Down Expand Up @@ -202,6 +244,16 @@ export class WebcamComponent implements AfterViewInit, OnDestroy {
return Math.min(this.height, this.width / videoRatio);
}

public get videoStyleClasses() {
let classes: string = "";

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
Expand Down Expand Up @@ -256,6 +308,42 @@ export class WebcamComponent implements AfterViewInit, OnDestroy {
}
}

private getActiveVideoTrack(): MediaStreamTrack {
return this.mediaStream ? this.mediaStream.getVideoTracks()[0] : null;
}

private isMirrorImage(): boolean {
if (!this.getActiveVideoTrack()) {
return false;
}

// check for explicit mirror override parameter
{
let mirror: string = "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,
Expand Down

0 comments on commit 6bd8a01

Please sign in to comment.