@microblink/photopay-in-browser-sdk
Version:
A simple payment barcode scanning library for WebAssembly-enabled browsers.
286 lines (256 loc) • 8.35 kB
text/typescript
/**
* Copyright (c) Microblink Ltd. All rights reserved.
*/
/**
* Preferred type of camera to be used when opening the camera feed.
*/
export enum PreferredCameraType
{
/** Prefer back facing camera */
BackFacingCamera,
/** Prefer front facing camera */
FrontFacingCamera
}
// inspired by https://unpkg.com/browse/scandit-sdk@4.6.1/src/lib/cameraAccess.ts
const backCameraKeywords: string[] = [
"rear",
"back",
"rück",
"arrière",
"trasera",
"trás",
"traseira",
"posteriore",
"后面",
"後面",
"背面",
"后置", // alternative
"後置", // alternative
"背置", // alternative
"задней",
"الخلفية",
"후",
"arka",
"achterzijde",
"หลัง",
"baksidan",
"bagside",
"sau",
"bak",
"tylny",
"takakamera",
"belakang",
"אחורית",
"πίσω",
"spate",
"hátsó",
"zadní",
"darrere",
"zadná",
"задня",
"stražnja",
"belakang",
"बैक"
];
function isBackCameraLabel( label: string ): boolean
{
const lowercaseLabel = label.toLowerCase();
return backCameraKeywords.some( keyword => lowercaseLabel.includes( keyword ) );
}
export class SelectedCamera
{
readonly deviceId: string;
readonly groupId: string;
readonly facing: PreferredCameraType;
readonly label: string;
constructor( mdi: MediaDeviceInfo, facing: PreferredCameraType )
{
this.deviceId = mdi.deviceId;
this.facing = facing;
this.groupId = mdi.groupId;
this.label = mdi.label;
}
}
export interface CameraDevices
{
frontCameras: SelectedCamera[];
backCameras: SelectedCamera[];
}
export async function getCameraDevices(): Promise< CameraDevices >
{
const frontCameras: SelectedCamera[] = [];
const backCameras: SelectedCamera[] = [];
{
let devices = await navigator.mediaDevices.enumerateDevices();
// if permission is not given, label of video devices will be empty string
if ( devices.filter( device => device.kind === "videoinput" ).every( device => device.label === "" ) )
{
const stream = await navigator.mediaDevices.getUserMedia
(
{
video:
{
facingMode: { ideal: "environment" }
},
audio: false
}
);
// enumerate devices again - now the label field should be non-empty, as we have a stream active
// (even if we didn't get persistent permission for camera)
devices = await navigator.mediaDevices.enumerateDevices();
// close the stream, as we don't need it anymore
stream.getTracks().forEach( track => track.stop() );
}
const cameras = devices.filter( device => device.kind === "videoinput" );
for ( const camera of cameras )
{
if ( isBackCameraLabel( camera.label ) )
{
backCameras.push( new SelectedCamera( camera, PreferredCameraType.BackFacingCamera ) );
}
else
{
frontCameras.push( new SelectedCamera( camera, PreferredCameraType.FrontFacingCamera ) );
}
}
}
return {
frontCameras,
backCameras
};
}
export async function selectCamera(
cameraId: string | null,
preferredCameraType: PreferredCameraType
): Promise< SelectedCamera | null >
{
const { frontCameras, backCameras } = await getCameraDevices();
if ( frontCameras.length > 0 || backCameras.length > 0 )
{
// decide from which array the camera will be selected
let cameraPool: SelectedCamera[] = ( backCameras.length > 0 ? backCameras : frontCameras );
// if there is at least one back facing camera and user prefers back facing camera, use that as a selection pool
if ( preferredCameraType === PreferredCameraType.BackFacingCamera && backCameras.length > 0 )
{
cameraPool = backCameras;
}
// if there is at least one front facing camera and is preferred by user, use that as a selection pool
if ( preferredCameraType === PreferredCameraType.FrontFacingCamera && frontCameras.length > 0 )
{
cameraPool = frontCameras;
}
// otherwise use whichever pool is non-empty
// sort camera pool by label
cameraPool = cameraPool.sort( ( camera1, camera2 ) => camera1.label.localeCompare( camera2.label ) );
// Check if cameras are labeled with resolution information, take the higher-resolution one in that case
// Otherwise pick the first camera
{
let selectedCameraIndex = 0;
const cameraResolutions: number[] = cameraPool.map
(
camera =>
{
const regExp = RegExp( /\b([0-9]+)MP?\b/, "i" );
const match = regExp.exec( camera.label );
if ( match !== null )
{
return parseInt( match[1], 10 );
}
else
{
return NaN;
}
}
);
if ( !cameraResolutions.some( cameraResolution => isNaN( cameraResolution ) ) )
{
selectedCameraIndex = cameraResolutions.lastIndexOf( Math.max( ...cameraResolutions ) );
}
if ( cameraId )
{
let cameraDevice = null;
cameraDevice = frontCameras.filter( device => device.deviceId === cameraId )[0];
if ( !cameraDevice )
{
cameraDevice = backCameras.filter( device => device.deviceId === cameraId )[0];
}
return cameraDevice || null;
}
return cameraPool[ selectedCameraIndex ];
}
}
else
{
// no cameras available on the device
return null;
}
}
/**
* Bind camera device to video feed (HTMLVideoElement).
*
* This function will return `true` in case that video feed of camera device has been flipped,
* and `false` otherwise.
*
* @param camera Camera device which should be binded with the video element.
* @param videoFeed HTMLVideoElement to which camera device should be binded.
* @param preferredCameraType Enum representing whether to use front facing or back facing camera.
*/
export async function bindCameraToVideoFeed(
camera: SelectedCamera,
videoFeed: HTMLVideoElement,
preferredCameraType: PreferredCameraType = PreferredCameraType.BackFacingCamera
): Promise< boolean >
{
const constraints: MediaStreamConstraints =
{
audio: false,
video:
{
width:
{
min: 640,
ideal: 1920,
max: 1920
},
height:
{
min: 480,
ideal: 1080,
max: 1080
}
}
};
if ( camera.deviceId === "" )
{
const isPreferredBackFacing = preferredCameraType === PreferredCameraType.BackFacingCamera;
( constraints.video as MediaTrackConstraints ).facingMode =
{
ideal: isPreferredBackFacing ? "environment" : "user"
};
}
else
{
( constraints.video as MediaTrackConstraints ).deviceId =
{
exact: camera.deviceId
};
}
const stream = await navigator.mediaDevices.getUserMedia( constraints );
videoFeed.controls = false;
videoFeed.srcObject = stream;
let cameraFlipped = false;
if ( camera.facing === PreferredCameraType.FrontFacingCamera )
{
videoFeed.style.transform = "scaleX(-1)";
cameraFlipped = true;
}
return cameraFlipped;
}
export function clearVideoFeed( videoFeed: HTMLVideoElement ): void
{
if ( videoFeed && videoFeed.srcObject !== null )
{
( videoFeed.srcObject as MediaStream ).getTracks().forEach( track => track.stop() );
videoFeed.srcObject = null;
}
}