UNPKG

@privateid/ultra-web-sdk-alpha

Version:
573 lines 26.6 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { UAParser } from 'ua-parser-js'; import { CameraFaceMode, FacingMode } from './camera.types'; import { PORTRAIT, LANDSCAPE, BACK_CAMERA_WORDS, CAMERA_HEIGHT, CAMERA_WIDTH, CAMERA_LOW_RES_WIDTH, } from './camera.constants'; import { printLogs } from '../../global/shared.utils'; const cameraLowResHeight = 480; const cameraHeight = 1440; const cameraWidth = 2560; const mobileCameraResWidth = 1200; const mobileCameraResHeight = 1440; export function getScreenOrientation() { if (window.innerHeight > window.innerWidth) { return PORTRAIT; } return LANDSCAPE; } export function getUserAgent() { const userAgent = typeof window !== 'undefined' && navigator && window.navigator.userAgent; return userAgent || undefined; } export function parseUserAgent() { // This maybe evaluated in next.js Node rather than in Browser if (!getUserAgent()) { return undefined; } const parser = new UAParser(); return parser.getResult(); } export function isPortrait() { return getScreenOrientation() === PORTRAIT; } // TODO: Refactor this to use the FacingMode enum export function isBackCameraAndPortrait(faceMode) { return isPortrait(); } // TODO: Refactor these `isSomethingUA` functions to be one. export function isMobileUA() { const result = parseUserAgent(); if (!result || !result.device || !result.device.type) { return false; } return ['mobile', 'tablet'].includes(result.device.type); } export function isFirefoxUA() { var _a; const result = parseUserAgent(); return ((_a = result === null || result === void 0 ? void 0 : result.browser) === null || _a === void 0 ? void 0 : _a.name) === 'Firefox'; } export function isSafariUA() { var _a; const result = parseUserAgent(); return ((_a = result === null || result === void 0 ? void 0 : result.browser) === null || _a === void 0 ? void 0 : _a.name) === 'Safari'; } export function isChromeUA() { var _a; const result = parseUserAgent(); return ((_a = result === null || result === void 0 ? void 0 : result.browser) === null || _a === void 0 ? void 0 : _a.name) === 'Chrome'; } export function isSamsungUA() { var _a; const result = parseUserAgent(); return ((_a = result === null || result === void 0 ? void 0 : result.device) === null || _a === void 0 ? void 0 : _a.vendor) === 'Samsung'; } export function isAndroid() { var _a; const result = parseUserAgent(); return ((_a = result === null || result === void 0 ? void 0 : result.os) === null || _a === void 0 ? void 0 : _a.name) === 'Android'; } /** * See https://developer.chrome.com/docs/multidevice/user-agent/#webview_user_agent * This onloy detects the WebView after Android 4.4. It'd return false for older * Android version. */ export function isAndroidWebView() { var _a, _b; const result = parseUserAgent(); // If the user-agent string contains "wv", 'ua-parser-js' will return the // string "WebView" as part of its browser name. return ((_a = result === null || result === void 0 ? void 0 : result.os) === null || _a === void 0 ? void 0 : _a.name) === 'Android' && /WebView/.test(String((_b = result === null || result === void 0 ? void 0 : result.browser) === null || _b === void 0 ? void 0 : _b.name)); } export function isAndroid12OrAbove() { return getMajorAndroidVersion() >= 12; } export function isIOSUA() { var _a; const result = parseUserAgent(); return ((_a = result === null || result === void 0 ? void 0 : result.os) === null || _a === void 0 ? void 0 : _a.name) === 'iOS'; } export function getMajorAndroidVersion() { var _a, _b; const result = parseUserAgent(); const matched = ((_a = result === null || result === void 0 ? void 0 : result.os) === null || _a === void 0 ? void 0 : _a.name) === 'Android' ? String(((_b = result === null || result === void 0 ? void 0 : result.os) === null || _b === void 0 ? void 0 : _b.version) || '').match(/^\d+/) : null; return matched ? parseInt(matched[0], 10) : NaN; } export function getMajorSafariVersion() { var _a, _b; const result = parseUserAgent(); const name = (_a = result === null || result === void 0 ? void 0 : result.browser) === null || _a === void 0 ? void 0 : _a.name; if (name === 'Safari' || name === 'Mobile Safari') { return parseInt(((_b = result === null || result === void 0 ? void 0 : result.browser) === null || _b === void 0 ? void 0 : _b.version) || '', 10); } return NaN; } export function getMajorChromeVersion() { var _a, _b, _c; const result = parseUserAgent(); return ((_b = (_a = result === null || result === void 0 ? void 0 : result.browser) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.includes('Chrome')) ? parseInt(((_c = result === null || result === void 0 ? void 0 : result.browser) === null || _c === void 0 ? void 0 : _c.version) || '', 10) : NaN; } export function getMajorIOSVersion() { var _a, _b; const result = parseUserAgent(); const matched = ((_a = result === null || result === void 0 ? void 0 : result.os) === null || _a === void 0 ? void 0 : _a.name) === 'iOS' ? String(((_b = result === null || result === void 0 ? void 0 : result.os) === null || _b === void 0 ? void 0 : _b.version) || '').match(/^\d+/) : null; return matched ? parseInt(matched[0], 10) : NaN; } export function getMajorMinorOsVersion() { var _a; const result = parseUserAgent(); const version = String(((_a = result === null || result === void 0 ? void 0 : result.os) === null || _a === void 0 ? void 0 : _a.version) || ''); const matched = version.match(/(^\d+)(\.\d+)?/); return (matched && matched[0]) || 'unknown'; } export function isWindows() { var _a, _b; return getPlatform() === 'desktop' && ((_b = (_a = parseUserAgent()) === null || _a === void 0 ? void 0 : _a.os) === null || _b === void 0 ? void 0 : _b.name) === 'Windows'; } export function isMac() { var _a, _b; return getPlatform() === 'desktop' && ((_b = (_a = parseUserAgent()) === null || _a === void 0 ? void 0 : _a.os) === null || _b === void 0 ? void 0 : _b.name) === 'Mac OS'; } export function isLinux() { var _a; const ua = parseUserAgent(); return ((_a = ua === null || ua === void 0 ? void 0 : ua.os) === null || _a === void 0 ? void 0 : _a.name) === 'Linux'; } // This returns the "platform"- iphone vs android vs desktop used for reporting metrics. // Do not make this into something with high cardinality- that will impact SignalFX. // If you need to alter/add to these values, please make sure to update the relevant // SignalFX metrics as well. export function getPlatform() { if (isAndroid()) { return 'android'; } if (isIOSUA()) { return 'iphone'; } return 'desktop'; } export function isMobileDevice() { return isMobileUA() || isAndroidDesktop() || isIOSDesktop(); } // Whether it's an IOS browser requesting desktop site. export function isIOSDesktop() { // IOS browser => Request Desktop Site. return isMac() && window.navigator.maxTouchPoints > 1; } export function isIOS() { return isIOSUA() || isIOSDesktop(); } export function isIOS15OrGreater() { if (isIOSDesktop()) { // In the past, the version of Safari has been in sync with the version of // iOS. Therefore, we could use Safari's version as a proxy to determine // the version of iOS. However, it's not possible to determine the iOS // version using other types of browsers. return getMajorSafariVersion() >= 15; } // iOS Mobile. return getMajorIOSVersion() >= 15; } // Whether it's an Android browser requesting desktop site. export function isAndroidDesktop() { // Android browser => Request Desktop Site. return isLinux() && window.navigator.maxTouchPoints > 1; } const isBackCameraLabel = (label) => { const lowerCase = (label === null || label === void 0 ? void 0 : label.toLowerCase()) || ''; return BACK_CAMERA_WORDS.some((keyword) => lowerCase === null || lowerCase === void 0 ? void 0 : lowerCase.includes(keyword)); }; export const isMobileBackCameraPortrait = ({ label, facingMode }) => { const isBackCamera = isBackCameraLabel(label) || (facingMode === null || facingMode === void 0 ? void 0 : facingMode.includes(FacingMode.environment)); return isMobileDevice() && isBackCamera && isPortrait(); }; export function isFaceTimeCamera(label, isDocumentScan) { return isMac() && label.includes('FaceTime') && isDocumentScan; } export const getCameraList = (backOnly) => __awaiter(void 0, void 0, void 0, function* () { try { if (!navigator.mediaDevices) { return []; } let cameraList = yield navigator.mediaDevices.enumerateDevices().then((devices) => { const filteredDevices = devices.filter((device) => { let filteredLabels = true; if (!backOnly) { filteredLabels = !isBackCameraLabel(device.label); } return device.kind === 'videoinput' && filteredLabels; }); return filteredDevices.map((device) => { // @ts-ignore if (device === null || device === void 0 ? void 0 : device.getCapabilities) { // @ts-ignore return Object.assign(Object.assign({}, device === null || device === void 0 ? void 0 : device.getCapabilities()), { deviceId: device.deviceId, label: device.label }); } return { deviceId: device.deviceId, label: device.label }; }); }); if (isAndroid()) { cameraList = cameraList.sort((a, b) => (a.label < b.label ? -1 : 1)); } return cameraList; } catch (error) { // handleException(error, 'error listing cameras'); return []; } }); export const getVideoConstraints = (availableDevices, faceMode, requireHD, isDocumentScan, canvasResolution, hasError = false) => { // Use 1.5 ratio. US DL are 1.58, passport are 1.42, so take something in the middle let deviceId = availableDevices === null || availableDevices === void 0 ? void 0 : availableDevices[0]; if (isIOS() && isDocumentScan && availableDevices && (availableDevices === null || availableDevices === void 0 ? void 0 : availableDevices.length) > 2) { const ultraWideDevice = availableDevices.find(({ label }) => label.includes('Dual Wide') || label.includes('Back Triple')); deviceId = ultraWideDevice || availableDevices[0]; } let resizeMode = 'crop-and-scale'; if (isMobileDevice()) { // Known cases that 'crop-and-scale' does not work. if ( // Chrome 107+ on Android shows broken video in 'crop-and-scale'. (isAndroid() && getMajorChromeVersion() >= 107) || // Samsung video driver has a bug when using resizeMode (isSamsungUA() && !isAndroid12OrAbove())) { resizeMode = 'none'; } let videoWidth; let videoHeight; let aspectRatio; if ((canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.width) && (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.height)) { videoWidth = canvasResolution.width; videoHeight = canvasResolution.height; aspectRatio = 1.7777777778; } else if (faceMode) { videoWidth = isBackCameraAndPortrait(faceMode) ? // ? { min: cameraLowResHeight, max: mobileCameraResHeight } { ideal: mobileCameraResHeight } : { ideal: CAMERA_LOW_RES_WIDTH }; videoHeight = isBackCameraAndPortrait(faceMode) ? // ? { min: cameraLowResHeight, max: mobileCameraResHeight } { ideal: mobileCameraResHeight } : undefined; aspectRatio = isBackCameraAndPortrait(faceMode) ? 1 : 1.7777777778; } return { audio: false, video: { facingMode: faceMode, resizeMode, width: videoWidth, height: videoHeight, deviceId: deviceId ? deviceId.deviceId : undefined, aspectRatio, focusMode: 'continuous', }, }; } return { audio: false, video: { aspectRatio: 1.5, // deviceId: deviceId ? deviceId.deviceId : undefined, height: { min: cameraLowResHeight }, resizeMode, }, }; }; export function scaleImageData(imageData, scaleFactor) { const { width, height, data } = imageData; // Create a new canvas and context for temporary scaling const tempCanvas = document.createElement('canvas'); const tempCtx = tempCanvas.getContext('2d'); // Set the dimensions of the temporary canvas tempCanvas.width = width * scaleFactor; tempCanvas.height = height * scaleFactor; // Draw the original image data onto the temporary canvas tempCtx === null || tempCtx === void 0 ? void 0 : tempCtx.putImageData(imageData, 0, 0); // Get the scaled image data from the temporary canvas const scaledImageData = tempCtx === null || tempCtx === void 0 ? void 0 : tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); return scaledImageData; } export function zoomInCanvasImage(canvas, scaleFactor) { // Get the current width and height of the canvas const originalWidth = canvas.width; const originalHeight = canvas.height; // Calculate the new dimensions after applying the scale factor const newWidth = Math.round(originalWidth / scaleFactor); const newHeight = Math.round(originalHeight / scaleFactor); // Calculate the amount of width and height to be cropped const cropWidth = originalWidth - newWidth; const cropHeight = originalHeight - newHeight; // Create an offscreen canvas to hold the cropped image const offscreenCanvas = document.createElement('canvas'); const offscreenCtx = offscreenCanvas.getContext('2d'); // Set the dimensions for the offscreen canvas offscreenCanvas.width = originalWidth; offscreenCanvas.height = originalHeight; const centerX = originalWidth / 2; const centerY = originalHeight / 2; const sx = centerX - newWidth / (2 * scaleFactor); const sy = centerY - newHeight / (2 * scaleFactor); offscreenCtx === null || offscreenCtx === void 0 ? void 0 : offscreenCtx.drawImage(canvas, sx, sy, newWidth / scaleFactor, newHeight / scaleFactor, // cropWidth / 2, // cropHeight / 2, // sourceX, sourceY // originalWidth - cropWidth, // originalHeight - cropHeight, // sourceWidth, sourceHeight 0, 0, // destinationX, destinationY // changing this for testing newWidth, newHeight); // Get the image data from the offscreen canvas const imageData = offscreenCtx === null || offscreenCtx === void 0 ? void 0 : offscreenCtx.getImageData(0, 0, newWidth, newHeight); // Return the image data return imageData; } export const checkDeviceIsVirtualCamera = (deviceLabel) => { const virtualCameraKeywords = ['virtual', 'obs', 'software', 'manycam']; return virtualCameraKeywords.some((keyword) => deviceLabel.toLowerCase().includes(keyword)); }; export function getFilteredVideoDevices() { return __awaiter(this, void 0, void 0, function* () { const permission = yield navigator.permissions.query({ name: 'camera' }); if (permission.state === 'denied') { printLogs('Camera permission is denied', '', 'WARN'); return []; } // Request camera permission first to ensure device labels are available try { const stream = yield navigator.mediaDevices.getUserMedia({ video: true, audio: false }); stream.getTracks().forEach((track) => track.stop()); } catch (error) { printLogs('Camera permission denied or not available', error, 'WARN'); } const devices = yield navigator.mediaDevices.enumerateDevices(); const filtered = devices.filter((d) => d.kind === 'videoinput' && !checkDeviceIsVirtualCamera(d.label)); return filtered; }); } export function getVideoInputDevices() { return __awaiter(this, void 0, void 0, function* () { const devices = yield navigator.mediaDevices.enumerateDevices(); return devices.filter((d) => d.kind === 'videoinput'); }); } export function filterDevicesByPattern(devices, pattern) { return devices.filter((d) => pattern.test(d.label)); } export function getExternalDevice(devices) { return devices.length > 1 ? devices.find((device) => !device.label.includes('FaceTime')) : devices[0]; } export function isWindowsDevice() { return ['windows', 'win16', 'win32', 'wince'].includes(navigator.platform.toUpperCase()); } export function getDefaultDevice(devices, deviceId) { return devices.reduce((acc, val) => { if (val.deviceId === deviceId) { // @ts-ignore if (val === null || val === void 0 ? void 0 : val.getCapabilities) { // @ts-ignore acc.push(Object.assign(Object.assign({}, val.getCapabilities()), { label: val.label })); } else { acc.push(val); } } return acc; }, []); } export function getBestResolution(isMacFaceTimeCamera, deviceCapabilites, canvasResolution) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { let resolution = (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.width) || Math.min(((_b = (_a = deviceCapabilites[0]) === null || _a === void 0 ? void 0 : _a.width) === null || _b === void 0 ? void 0 : _b.max) || CAMERA_HEIGHT, CAMERA_WIDTH); let hasError = true; let constraints = { audio: false, video: { deviceId: deviceCapabilites[0].deviceId, width: { ideal: resolution }, height: (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.height) ? { ideal: canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.height } : { ideal: CAMERA_HEIGHT }, aspectRatio: isMacFaceTimeCamera ? 1 : 1.777777778, resizeMode: 'none', facingMode: isWindowsDevice() ? 'user' : undefined, }, }; let stream; while (hasError) { try { stream = yield navigator.mediaDevices.getUserMedia(constraints); if (stream) hasError = false; } catch (e) { resolution = getTheNextResolutionAvailable(resolution); constraints = Object.assign(Object.assign({}, constraints), { video: Object.assign(Object.assign({}, constraints.video), { width: { ideal: resolution } }) }); } } return stream; }); } /** * This function open camera, and returns the stream, current faceMode and the list of available media devices * @category Face * @param domElement id of the video tag */ export function getTheNextResolutionAvailable(currentResolution) { printLogs(`getTheNextResolutionAvailable `, currentResolution); const resolutions = [2560, 1920, 1600, 1552, 1440, 1280, 1024, 960, 800, 720, 704, 640].sort((a, b) => b - a); return resolutions.find((e) => e < currentResolution) || 640; } export function startVideo(videoElement, stream) { return __awaiter(this, void 0, void 0, function* () { const element = document.getElementById(videoElement); element.srcObject = stream; element.play(); yield new Promise((resolve) => (element.onplaying = resolve)); enableMutationObserver(element, stream); }); } export function enableMutationObserver(videoElement, stream) { const videoMutationObserver = new MutationObserver(() => { if (videoElement.srcObject !== stream) { printLogs('Unauthorized video source change detected! Resetting to correct camera.', '', 'WARN'); // eslint-disable-next-line no-param-reassign videoElement.srcObject = stream; } }); videoMutationObserver.observe(videoElement, { attributes: true, attributeFilter: ['srcObject'] }); } export function getDevicesWithCapabilities(devices) { return devices.map((device) => { // @ts-ignore if (device.getCapabilities) { // @ts-ignore const capabilities = device.getCapabilities(); return Object.assign(Object.assign({}, capabilities), { label: device.label }); } return device; }); } export function calculateWidth(canvasResolution, isPortraitMobileCamera, resolution) { if (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.width) { return resolution; } if (isPortraitMobileCamera) { return CAMERA_HEIGHT; } return resolution; } export function calculateHeight(canvasResolution) { if (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.height) { return canvasResolution.height; } return CAMERA_HEIGHT; } export function calculateAspectRatio(canvasResolution, isPortraitMobileCamera) { if (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.width) { return 1.7777777778; } if (isPortraitMobileCamera) { return 1; } return 1.7777777778; } export function createBasicVideoConstraints(options) { const videoConstraints = {}; if (options.deviceId) videoConstraints.deviceId = options.deviceId; if (options.width) videoConstraints.width = { ideal: options.width }; if (options.height) videoConstraints.height = { ideal: options.height }; if (options.aspectRatio) videoConstraints.aspectRatio = options.aspectRatio; if (options.facingMode) videoConstraints.facingMode = options.facingMode; if (options.focusMode) videoConstraints.focusMode = options.focusMode; if (options.resizeMode) videoConstraints.resizeMode = options.resizeMode; return { audio: false, video: videoConstraints, }; } export function createFirefoxVideoConstraints(faceMode, canvasResolution) { return createBasicVideoConstraints({ width: (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.width) || CAMERA_LOW_RES_WIDTH, height: (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.height) || 1080, aspectRatio: 1.7777777778, focusMode: 'continuous', facingMode: faceMode === CameraFaceMode.back ? 'environment' : 'user', }); } export function createMacSafariVideoConstraints(deviceId, resolution, canvasResolution) { return createBasicVideoConstraints({ deviceId, width: resolution, height: canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.height, }); } export function createBestResolutionStream(deviceCapabilites, canvasResolution) { var _a, _b, _c, _d; return __awaiter(this, void 0, void 0, function* () { let resolution = (canvasResolution === null || canvasResolution === void 0 ? void 0 : canvasResolution.width) || Math.min(((_b = (_a = deviceCapabilites[0]) === null || _a === void 0 ? void 0 : _a.width) === null || _b === void 0 ? void 0 : _b.max) || CAMERA_HEIGHT, CAMERA_HEIGHT); const isPortraitMobileCamera = isMobileBackCameraPortrait(deviceCapabilites[0]); let hasError = true; const videoConstraints = { deviceId: deviceCapabilites[0].deviceId, width: { ideal: calculateWidth(canvasResolution, isPortraitMobileCamera, resolution) }, height: { ideal: calculateHeight(canvasResolution) }, facingMode: ((_d = (_c = deviceCapabilites[0]) === null || _c === void 0 ? void 0 : _c.facingMode) === null || _d === void 0 ? void 0 : _d[0]) || undefined, focusMode: 'continuous', aspectRatio: calculateAspectRatio(canvasResolution, isPortraitMobileCamera), resizeMode: 'none', }; const constraints = { audio: false, video: videoConstraints, }; let stream; while (hasError) { try { stream = yield navigator.mediaDevices.getUserMedia(constraints); hasError = false; } catch (e) { resolution = getTheNextResolutionAvailable(resolution); constraints.video = Object.assign(Object.assign({}, constraints.video), { width: { ideal: resolution } }); } } printLogs(`bestconstraints: `, constraints); return stream; }); } export function prepareCameraResult(stream, devices, faceMode, videoElement) { return __awaiter(this, void 0, void 0, function* () { const track = stream.getVideoTracks()[0]; const capabilities = track.getCapabilities ? track.getCapabilities() : null; const settings = track.getSettings(); printLogs('settings: ', settings); if (capabilities) { printLogs('capabilities: ', capabilities); } yield startVideo(videoElement, stream); return { status: true, stream, devices, faceMode, settings, capabilities, errorMessage: null, }; }); } //# sourceMappingURL=camera.utils.js.map