UNPKG

veryfi-lens-sdk

Version:

Veryfi Lens web sdk

634 lines (582 loc) 17.2 kB
import DeviceUUID from "./device-uuid.js"; import FingerprintID from "./fingerprint-id.js"; import cv from "@techstark/opencv-js"; const VeryfiLens = (function () { const DEFAULT_BOX_COlOR = "rgba(84, 192, 139, 0.6)"; const DEFAULT_SCALE = 1.0; const INTERVAL = 250; const LENS_DEVICE_ID_SEPARATOR = "LENS_DEVICE_ID"; const LENS_SESSION_KEY_SEPARATOR = "LENS_SESSION_KEY"; const MAX_SHAPE = 512.0; const SOCKET_STATUSES = [ { value: 0, state: "CONNECTING", }, { value: 1, state: "OPEN", }, { value: 2, state: "CLOSING", }, { value: 3, state: "CLOSED", }, { value: -1, state: "UNDEFINED", }, ]; let SOCKET_URL = "wss://lens.veryfi.com/ws/crop"; let VALIDATE_URL = "https://lens.veryfi.com/rest/validate_partner"; let device_fingerprint; let boxRef = null; let cropImgRef = null; let frameRef = null; let videoRef = null; let intervalRef = null; let userAgent = null; let device_uuid = null; let fullSizeImage; let finalImage; let boxColor = DEFAULT_BOX_COlOR; let clientId = ""; let coordinates = []; let currentFrame = ""; let hasCoordinates = false; let hasInit = false; let image = ""; let isDocument = false; let isSocketBusy = false; let lensSessionKey = ""; let scale = DEFAULT_SCALE; let ws = null; let blurStatus = ""; let variance; const releaseCanvas = (canvas) => { canvas.width = 1; canvas.height = 1; const ctx = canvas.getContext("2d"); ctx && ctx.clearRect(0, 0, 1, 1); }; function waitForElement(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { resolve(document.querySelector(selector)); observer.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true, }); }); } const setClientId = (key) => { clientId = key; }; const setCoordinates = (state) => { coordinates = state; }; const setCurrentFrame = (state) => { currentFrame = state; }; const setHasCoordinates = (state) => { hasCoordinates = state; }; const setIsDocument = (state) => { isDocument = state; }; const socketInitializer = async () => { const connectionId = Date.now(); ws = new WebSocket(`${SOCKET_URL}/${connectionId}`); ws.onmessage = function (event) { const payload = JSON.parse(event.data); switch (payload.event) { case "connect": isSocketBusy = false; console.log("[EVENT] Started sending frames"); break; case "cropped": isSocketBusy = false; console.log("[EVENT] Got cropped contours"); if (payload.data.is_receipt === false) { const video = videoRef; let boxCanvas = boxRef; if (video && boxCanvas) { boxCanvas.width = video.videoWidth; boxCanvas.height = video.videoHeight; const ctx = boxCanvas.getContext("2d"); if (ctx) ctx.restore(); } setHasCoordinates(false); return; } const rCorners = payload.data.contours.map((corner) => corner.map((cord) => cord / scale) ); drawContours(rCorners); break; default: isSocketBusy = false; console.log("[EVENT] Unknown event"); } }; }; const displayVideo = () => { const video = videoRef; const frameCanvas = frameRef; if (video && frameCanvas) { frameCanvas.width = video.videoWidth; frameCanvas.height = video.videoHeight; const ctx = frameCanvas.getContext("2d"); if (ctx) ctx.drawImage(video, 0, 0); } }; const sendFrame = () => { if (isSocketBusy) return; const video = videoRef; const frameCanvas = frameRef; if (video && frameCanvas) { let videoHeight = Number(video.videoHeight); let videoWidth = Number(video.videoWidth); const fullSizeCanvas = document.createElement("canvas"); fullSizeCanvas.width = videoWidth; fullSizeCanvas.height = videoHeight; const fullSizeCtx = fullSizeCanvas.getContext("2d"); if (fullSizeCtx) { fullSizeCtx.save(); fullSizeCtx.drawImage(video, 0, 0, videoWidth, videoHeight); fullSizeCtx.restore(); const imgString = fullSizeCanvas.toDataURL("image/jpeg"); fullSizeImage = new Image(); fullSizeImage.src = imgString; } if (videoWidth > videoHeight) { if (videoWidth > MAX_SHAPE) { scale = MAX_SHAPE / videoWidth; videoHeight = videoHeight * scale; videoWidth = MAX_SHAPE; } } else { if (videoHeight > MAX_SHAPE) { scale = MAX_SHAPE / videoHeight; videoWidth = videoWidth * scale; videoHeight = MAX_SHAPE; } } frameCanvas.width = videoWidth; frameCanvas.height = videoHeight; const ctx = frameCanvas.getContext("2d"); if (ctx) { ctx.save(); ctx.drawImage(video, 0, 0, videoWidth, videoHeight); ctx.restore(); } const imgString = frameCanvas.toDataURL("image/jpeg"); const payload = imgString.split("data:image/jpeg;base64,")[1]; setCurrentFrame(payload); if (ws.readyState === 1) { isSocketBusy = true; ws.send( getDeviceID(device_uuid, device_fingerprint) + LENS_DEVICE_ID_SEPARATOR + lensSessionKey + LENS_SESSION_KEY_SEPARATOR + payload ); } releaseCanvas(frameCanvas); } }; const getVideo = () => { const isDesktop = window.screen.width > window.screen.height; if (navigator) { navigator.mediaDevices .getUserMedia({ video: { aspectRatio: isDesktop ? 9 / 16 : 16 / 9, facingMode: "environment", width: { ideal: 2160 }, height: { ideal: 4096 }, }, }) .then((stream) => { console.log("started stream"); const video = videoRef; video.srcObject = stream; }) .catch((err) => { console.log(`[Event] Error: ${err}`); }); } }; const drawContours = (contours) => { const video = videoRef; const BoxCanvas = boxRef; if (video && BoxCanvas) { BoxCanvas.width = video.videoWidth; BoxCanvas.height = video.videoHeight; let ctx = BoxCanvas.getContext("2d"); if (ctx) { ctx.save(); ctx.beginPath(); ctx.moveTo(contours[0][0], contours[0][1]); ctx.lineTo(contours[1][0], contours[1][1]); ctx.lineTo(contours[2][0], contours[2][1]); ctx.lineTo(contours[3][0], contours[3][1]); ctx.fillStyle = boxColor; ctx.fill(); setCoordinates(contours); setHasCoordinates(true); ctx.restore(); } } }; const cropImage = async () => { const video = videoRef; const cropImgCanvas = cropImgRef; if (fullSizeImage) { console.log("[Event] Full size image is set"); if (hasCoordinates) { setIsDocument(true); let { sx, sy, sw, sh } = getCropLimits(coordinates); const scaleWidth = fullSizeImage.width / video.videoWidth; const scaleHeight = fullSizeImage.height / video.videoHeight; sx = sx * scaleWidth; sy = sy * scaleHeight; sw = sw * scaleWidth; sh = sh * scaleHeight; cropImgCanvas.width = sw; cropImgCanvas.height = sh; const ctx = cropImgCanvas.getContext("2d"); if (ctx) { ctx.save(); ctx.drawImage(fullSizeImage, sx, sy, sw, sh, 0, 0, sw, sh); ctx.restore(); } } else { cropImgCanvas.width = video.videoWidth; cropImgCanvas.height = video.videoHeight; const ctx = cropImgCanvas.getContext("2d"); if (ctx) { ctx.save(); ctx.drawImage(fullSizeImage, 0, 0); ctx.restore(); } } } else { if (hasCoordinates) { setIsDocument(true); let { sx, sy, sw, sh } = getCropLimits(coordinates); cropImgCanvas.width = sw; cropImgCanvas.height = sh; const ctx = cropImgCanvas.getContext("2d"); if (ctx) { ctx.save(); ctx.drawImage(video, sx, sy, sw, sh, 0, 0, sw, sh); ctx.restore(); } } else { cropImgCanvas.width = video.videoWidth; cropImgCanvas.height = video.videoHeight; const ctx = cropImgCanvas.getContext("2d"); if (ctx) { ctx.save(); ctx.drawImage(video, 0, 0); ctx.restore(); } } } waitForElement("#blur-detector").then(() => { isBlurry(cropImgCanvas); }); image = cropImgCanvas; const imgString = cropImgCanvas.toDataURL("image/jpeg"); return imgString.split("data:image/jpeg;base64,")[1]; }; const getCropLimits = (coordinates) => { const sx = Math.min( coordinates[0][0], coordinates[1][0], coordinates[2][0], coordinates[3][0] ); const sy = Math.min( coordinates[0][1], coordinates[1][1], coordinates[2][1], coordinates[3][1] ); const sw = Math.max( coordinates[0][0], coordinates[1][0], coordinates[2][0], coordinates[3][0] ) - sx; const sh = Math.max( coordinates[0][1], coordinates[1][1], coordinates[2][1], coordinates[3][1] ) - sy; return { sx, sy, sw, sh }; }; const fetchSessionId = async (clientId) => { return await fetch(VALIDATE_URL, { method: "POST", headers: { "CLIENT-ID": clientId, }, }) .then((response) => { if (response.ok) { return response.json(); } throw new Error("Wrong client id"); }) .then((response) => { return response.session; }) .catch((error) => { console.log("[EVENT] " + error); }); }; const stopLens = () => { ws.close(); videoRef.srcObject.getTracks().forEach((track) => track.stop()); clearInterval(intervalRef); }; const createElement = (type, id, classes, container) => { const element = document.createElement(type); element.setAttribute("id", id); element.className += classes; container.appendChild(element); }; const getDeviceID = (uuid, fingerprint) => { return `${uuid + fingerprint}`.replace(/\-/g, ""); }; const startLens = async () => { await socketInitializer().then(() => { console.log("[EVENT] Socket Initialized"); }); getVideo(); requestAnimationFrame(displayVideo); intervalRef = setInterval(() => { requestAnimationFrame(sendFrame); }, INTERVAL); return () => { clearInterval(intervalRef); }; }; const setBlurStatus = (blur, variance) => { return { blur, variance, }; }; const isBlurry = async (image) => { console.log("[EVENT] Checking for blur"); const src = cv.imread(image); let refVariance; let whiteCanvas = new cv.Mat(490, 866, cv.CV_8UC3, [255, 255, 255, 0]); const grayscale = new cv.Mat(); const refGrayscale = new cv.Mat(); cv.cvtColor(src, grayscale, cv.COLOR_RGBA2GRAY); cv.cvtColor(whiteCanvas, refGrayscale, cv.COLOR_RGBA2GRAY); const laplacian = new cv.Mat(); const refLaplacian = new cv.Mat(); cv.Laplacian(grayscale, laplacian, cv.CV_8U); cv.Laplacian(refGrayscale, refLaplacian, cv.CV_8U); const meanStdDev = new cv.Mat(); const laplacianMean = new cv.Mat(); const refMeanStdDev = new cv.Mat(); const refLaplacianMean = new cv.Mat(); cv.meanStdDev(laplacian, laplacianMean, meanStdDev); cv.meanStdDev(refLaplacian, refLaplacianMean, refMeanStdDev); variance = meanStdDev.data64F[0] * 10; refVariance = refMeanStdDev.data64F[0] * 10; console.log("variance", variance); console.log("reference variance", refVariance); grayscale.delete(); laplacian.delete(); meanStdDev.delete(); laplacianMean.delete(); refGrayscale.delete(); refLaplacian.delete(); refMeanStdDev.delete(); refLaplacianMean.delete(); whiteCanvas.delete(); if (variance > 95) { blurStatus = false; setBlurStatus(blurStatus, variance); return; } else { blurStatus = true; setBlurStatus(blurStatus, variance); return; } }; return { init: async (session) => { const fp = await FingerprintID.load(); device_fingerprint = (await fp.get()).visitorId; userAgent = navigator.userAgent; device_uuid = new DeviceUUID(userAgent).get(); console.log( "[EVENT] Device ID", getDeviceID(device_uuid, device_fingerprint) ); if (session) lensSessionKey = session; const container = document.getElementById("veryfi-container"); const generalClasses = "absolute sm:rounded-md h-full max-w-none"; const frameClasses = "absolute invisible sm:rounded-md h-full max-w-none"; const cropImgClasses = "absolute sm:rounded-md h-full max-w-none z-30"; createElement( "canvas", "veryfi-crop-img-ref", [cropImgClasses], container ); createElement("canvas", "veryfi-frame-ref", frameClasses, container); createElement( "video", "veryfi-video-ref", `${generalClasses} z-10`, container ); createElement( "canvas", "veryfi-box-ref", `${generalClasses} z-10`, container ); videoRef = document.getElementById("veryfi-video-ref"); const video = videoRef; video.playsInline = true; video.preload = "metadata"; video.autoplay = true; frameRef = document.getElementById("veryfi-frame-ref"); boxRef = document.getElementById("veryfi-box-ref"); cropImgRef = document.getElementById("veryfi-crop-img-ref"); startLens(); }, startCamera: () => { console.log("[EVENT] startCamera"); startLens(); }, stopCamera: () => { console.log("[EVENT] stopCamera"); stopLens(); clearInterval(intervalRef); }, capture: async (setImage, setIsEditing) => { console.log("[EVENT] capture"); const finalImage = await cropImage(); setImage(finalImage); console.log("[EVENT] hasCoordinates: ", hasCoordinates); if (hasCoordinates) setIsDocument(true); stopLens(); setIsEditing(true); return finalImage; }, createNewSession: async (clientId) => { await fetchSessionId(clientId); setClientId(clientId); }, setUserAgent: (ua) => { userAgent = ua; }, getBoxColor: () => { return boxColor; }, setBoxColor: (color) => { boxColor = color; }, getCroppedImage: () => { return image; }, getCoordinates: () => { return coordinates; }, getHasCoordinates: () => { return hasCoordinates; }, getHasInit: () => { return hasInit; }, getIsDocument: () => { return isDocument; }, getLensSessionKey: () => { return lensSessionKey; }, setLensSessionKey: (key) => { lensSessionKey = key; }, getSocketStatus: () => { const value = ws?.readyState || 4; return SOCKET_STATUSES[value]; }, getSocketStatusColor: () => { const socketStatusesColors = [ "yellow", "green", "orange", "red", "purple", ]; const value = ws?.readyState || 4; return socketStatusesColors[value]; }, getBlurStatus: () => { return { blurStatus, variance, }; }, setLocation: (location) => { switch(location) { case 'US': SOCKET_URL = "wss://lens.veryfi.com/ws/crop" VALIDATE_URL = "https://lens.veryfi.com/rest/validate_partner" break; case 'Brazil': SOCKET_URL = "wss://lens-sae1.veryfi.com/ws/crop" VALIDATE_URL = "https://lens-sae1.veryfi.com/rest/validate_partner" break; default: console.log('Unsupported location, use US or Brazil'); return null; } console.log('location is set to' + location) }, cleanCanvases: () => { console.log("Cleaning"); const box = boxRef; const video = videoRef; // const frame = frameRef const crop = cropImgRef; releaseCanvas(crop); video.pause(); video.removeAttribute("src"); video.load(); releaseCanvas(box); videoRef.remove(); frameRef.remove(); boxRef.remove(); cropImgRef.remove(); setIsDocument(false); }, }; })(); export default VeryfiLens;