UNPKG

face-detection-web-sdk

Version:

웹 기반 얼굴 인식을 통해 실시간으로 심박수, 스트레스, 혈압 등의 건강 정보를 측정하는 SDK

3 lines (2 loc) 26.2 kB
"use strict";var e=Object.defineProperty,t=(t,n,i)=>((t,n,i)=>n in t?e(t,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):t[n]=i)(t,"symbol"!=typeof n?n+"":n,i);Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const n=require("@mediapipe/face_detection");var i="undefined"!=typeof document?document.currentScript:null;const a={platform:{isIOS:!1,isAndroid:!1},measurement:{targetDataPoints:450,frameInterval:33.33,frameProcessInterval:30,readyToMeasuringDelay:3},faceDetection:{timeout:3e3,minDetectionConfidence:.5},video:{width:640,height:480,frameRate:30},ui:{containerId:"face-detection-container",customClasses:{container:"",video:"",canvas:"",progress:""}},server:{baseUrl:"",timeout:3e4},debug:{enabled:!1,enableConsoleLog:!1},dataDownload:{enabled:!1,autoDownload:!1,filename:"rgb_data.txt"},errorBounding:4};class s{constructor(e={}){t(this,"config"),this.config=this.mergeConfig(a,e)}mergeConfig(e,t){const n=JSON.parse(JSON.stringify(e));return Object.keys(t).forEach((e=>{const i=t[e];void 0!==i&&("elements"===e||"object"!=typeof i||Array.isArray(i)?n[e]=i:n[e]={...n[e],...i})})),n}getConfig(){return this.config}}var o=(e=>(e.INITIAL="initial",e.READY="ready",e.MEASURING="measuring",e.COMPLETED="completed",e))(o||{}),r=(e=>(e.FACE_NOT_DETECTED="face_not_detected",e.FACE_OUT_OF_CIRCLE="face_out_of_circle",e.WEBCAM_PERMISSION_DENIED="webcam_permission_denied",e.WEBCAM_ACCESS_FAILED="webcam_access_failed",e.INITIALIZATION_FAILED="initialization_failed",e.UNKNOWN_ERROR="unknown_error",e))(r||{});class c{constructor(e={},n){t(this,"stateChangeCallbacks",[]),t(this,"callbacks",{}),t(this,"log"),this.callbacks=e,this.log=n,e.onStateChange&&this.onStateChange(e.onStateChange)}onStateChange(e){this.stateChangeCallbacks.push(e)}removeStateChangeCallback(e){const t=this.stateChangeCallbacks.indexOf(e);t>-1&&this.stateChangeCallbacks.splice(t,1)}emitStateChange(e,t){this.stateChangeCallbacks.forEach((n=>{try{n(e,t)}catch(i){console.error("상태 변경 콜백 실행 중 오류:",i)}}))}emitError(e,t){const n=t?`${t}: ${e.message}`:e.message;this.log&&this.log(`오류 발생: ${n}`,e),this.callbacks.onError&&this.callbacks.onError({type:r.UNKNOWN_ERROR,message:n,originalError:e})}emitWebcamError(e,t){const n="NotAllowedError"===e.name||"PermissionDeniedError"===e.name,i=t&&e.message&&(e.message.includes("permission")||e.message.includes("허가")||e.message.includes("권한"));let a,s;n||i?(a=r.WEBCAM_PERMISSION_DENIED,s="웹캠 접근 권한이 거부되었습니다. 브라우저 설정에서 카메라 권한을 허용해주세요."):(a=r.WEBCAM_ACCESS_FAILED,s=`웹캠에 접근할 수 없습니다: ${e.message}`),this.callbacks.onError&&this.callbacks.onError({type:a,message:s,originalError:e})}emitFaceDetectionChange(e,t){this.callbacks.onFaceDetectionChange&&this.callbacks.onFaceDetectionChange(e,t)}emitFacePositionChange(e){this.callbacks.onFacePositionChange&&this.callbacks.onFacePositionChange(e)}emitProgress(e,t){this.callbacks.onProgress&&this.callbacks.onProgress(e,t)}emitMeasurementComplete(e){this.callbacks.onMeasurementComplete&&this.callbacks.onMeasurementComplete(e)}dispose(){this.stateChangeCallbacks=[],this.callbacks={}}}class l{constructor(){t(this,"faceDetection"),t(this,"onResultsCallback",null)}async initialize(e=.5){this.faceDetection=new n.FaceDetection({locateFile:e=>`https://cdn.jsdelivr.net/npm/@mediapipe/face_detection/${e}`});const t={model:"short",minDetectionConfidence:e,runningMode:"VIDEO"};this.faceDetection.setOptions(t)}setOnResultsCallback(e){this.onResultsCallback=e,this.faceDetection.onResults(this.onResultsCallback)}async sendImage(e){await this.faceDetection.send({image:e})}dispose(){this.faceDetection&&(this.faceDetection=null),this.onResultsCallback=null}}class h{constructor(){t(this,"currentState",o.INITIAL),t(this,"stateChangeCallback")}getCurrentState(){return this.currentState}setStateChangeCallback(e){this.stateChangeCallback=e}setState(e){const t=this.currentState;this.currentState=e,this.emitStateChange(e,t)}emitStateChange(e,t){if(this.stateChangeCallback)try{this.stateChangeCallback(e,t)}catch(n){console.error("상태 변경 콜백 실행 중 오류:",n)}}isState(e){return this.currentState===e}isAnyState(...e){return e.includes(this.currentState)}}class d{constructor(e,n){t(this,"webcamStream",null),t(this,"config"),t(this,"events"),this.config=e,this.events=n}async startWebcam(){var e,t,n;try{const i={width:(null==(e=this.config.video)?void 0:e.width)||640,height:(null==(t=this.config.video)?void 0:t.height)||480,frameRate:(null==(n=this.config.video)?void 0:n.frameRate)||30};return this.webcamStream=await navigator.mediaDevices.getUserMedia({video:i}),this.webcamStream}catch(i){throw this.handleWebcamError(i),i}}stopWebcam(){this.webcamStream&&(this.webcamStream.getTracks().forEach((e=>e.stop())),this.webcamStream=null)}handleWebcamError(e){var t;const n=(null==(t=this.config.platform)?void 0:t.isIOS)||!1;this.events.onWebcamError(e,n)}dispose(){this.stopWebcam()}}class g{constructor(e=4){t(this,"lastPosition",0),t(this,"lastYPosition",0),t(this,"positionErr",0),t(this,"yPositionErr",0),t(this,"errorBounding"),this.errorBounding=e}updateFacePosition(e,t,n){const i=e.xCenter*t.videoWidth,a=e.yCenter*t.videoHeight,{isInCircle:s}=function(e,t,n,i){const a=i.getBoundingClientRect(),s=n.getBoundingClientRect(),o=n.videoWidth/s.width,r=n.videoHeight/s.height,c={x:(a.left-s.left+a.width/2)*o,y:(a.top-s.top+a.height/2)*r},l=a.width/2*o*.6,h=Math.sqrt(Math.pow(e-c.x,2)+Math.pow(t-c.y,2));return{isInCircle:h<=l,distance:h,allowedRadius:l,progressCenter:c}}(i,a,t,n),{lastPosition:o,lastYPosition:r,positionErr:c,yPositionErr:l}=(h=i,d=a,g=this.lastPosition,m=this.lastYPosition,u=this.positionErr,p=this.yPositionErr,b=this.errorBounding,g&&Math.abs(h-g)>b&&u++,m&&Math.abs(d-m)>b&&p++,{lastPosition:h,lastYPosition:d,positionErr:u,yPositionErr:p});var h,d,g,m,u,p,b;return this.lastPosition=o,this.lastYPosition=r,this.positionErr=c,this.yPositionErr=l,{isInCircle:s}}getPositionErrors(){return{positionErr:this.positionErr,yPositionErr:this.yPositionErr}}}class m{constructor(e){t(this,"faceRegionWorker"),t(this,"lastRGB"),t(this,"events"),this.events=e}initialize(){const e=new URL("data:text/javascript;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbiAoZSkgew0KICBjb25zdCB7IGZhY2VSZWdpb25EYXRhIH0gPSBlLmRhdGE7DQogIGNvbnN0IGRhdGEgPSBuZXcgVWludDMyQXJyYXkoZmFjZVJlZ2lvbkRhdGEuZGF0YS5idWZmZXIpOyAvLyBVaW50MzJBcnJheeuhnCDrs4DtmZgNCiAgY29uc3QgbGVuID0gZGF0YS5sZW5ndGg7DQoNCiAgbGV0IHN1bVJlZCA9IDAsDQogICAgc3VtR3JlZW4gPSAwLA0KICAgIHN1bUJsdWUgPSAwOw0KDQogIGZvciAobGV0IGkgPSAwOyBpIDwgbGVuOyBpKyspIHsNCiAgICBjb25zdCBwaXhlbCA9IGRhdGFbaV07DQoNCiAgICAvLyDqsIHqsIHsnZgg7IOJ7IOBIOyxhOuEkOydhCDstpTstpwNCiAgICBzdW1SZWQgKz0gcGl4ZWwgJiAweGZmOyAvLyBSDQogICAgc3VtR3JlZW4gKz0gKHBpeGVsID4+IDgpICYgMHhmZjsgLy8gRw0KICAgIHN1bUJsdWUgKz0gKHBpeGVsID4+IDE2KSAmIDB4ZmY7IC8vIEINCiAgfQ0KDQogIGNvbnN0IHNhbXBsZWRQaXhlbHMgPSBsZW47DQoNCiAgY29uc3QgcmVzdWx0ID0gew0KICAgIG1lYW5SZWQ6IHN1bVJlZCAvIHNhbXBsZWRQaXhlbHMsDQogICAgbWVhbkdyZWVuOiBzdW1HcmVlbiAvIHNhbXBsZWRQaXhlbHMsDQogICAgbWVhbkJsdWU6IHN1bUJsdWUgLyBzYW1wbGVkUGl4ZWxzLA0KICAgIHRpbWVzdGFtcDogU3RyaW5nKERhdGUubm93KCkgKiAxMDAwKSwNCiAgfTsNCg0KICBzZWxmLnBvc3RNZXNzYWdlKHJlc3VsdCk7DQp9Ow0K","undefined"==typeof document?require("url").pathToFileURL(__filename).href:i&&"SCRIPT"===i.tagName.toUpperCase()&&i.src||new URL("index.js",document.baseURI).href);this.faceRegionWorker=new Worker(e,{type:"module"}),this.lastRGB={timestamp:0,r:null,g:null,b:null},this.setupWorker()}setupWorker(){this.faceRegionWorker.onmessage=({data:e})=>{this.lastRGB=this.events.onDataProcessed(e)}}postFaceRegionData(e){this.faceRegionWorker.postMessage({faceRegionData:e})}terminate(){this.faceRegionWorker&&this.faceRegionWorker.terminate()}getLastRGB(){return this.lastRGB}}const u=e=>new Promise((t=>setTimeout(t,1e3*e)));class p{constructor(e,n){t(this,"mean_red",[]),t(this,"mean_green",[]),t(this,"mean_blue",[]),t(this,"timingHist",[]),t(this,"config"),t(this,"events"),t(this,"isCompleted",!1),this.config=e,this.events=n}addRGBData(e){var t;if(!this.isCompleted&&null!==e.r&&null!==e.g&&null!==e.b){this.mean_red.push(e.r),this.mean_green.push(e.g),this.mean_blue.push(e.b),this.timingHist.push(e.timestamp);const n=(null==(t=this.config.measurement)?void 0:t.targetDataPoints)||450;if(this.timingHist.length>n){const e=this.timingHist.length-n;this.mean_red.splice(0,e),this.mean_green.splice(0,e),this.mean_blue.splice(0,e),this.timingHist.splice(0,e)}this.updateProgress(),this.timingHist.length===n&&this.finalizeMeasurement()}}updateProgress(){var e;if(this.timingHist.length>0){const t=(null==(e=this.config.measurement)?void 0:e.targetDataPoints)||450,n=Math.min(this.timingHist.length/t,1);this.events.onProgress(n,this.timingHist.length)}}finalizeMeasurement(){this.isCompleted=!0;const e=(t=this.mean_red,n=this.mean_green,i=this.mean_blue,a=this.timingHist,t.map(((e,s)=>`${a[s]}\t${t[s]}\t${n[s]}\t${i[s]}`)).join("\n"));var t,n,i,a;const s={rawData:{sigR:[...this.mean_red],sigG:[...this.mean_green],sigB:[...this.mean_blue],timestamp:[...this.timingHist].map(Number)},quality:{positionError:0,yPositionError:0,dataPoints:this.timingHist.length}};this.events.onMeasurementComplete(s),this.events.onDataDownload(e)}resetData(){this.mean_red=[],this.mean_green=[],this.mean_blue=[],this.timingHist=[],this.isCompleted=!1}async startReadyToMeasuringTransition(e,t,n,i){var a;try{for(let i=(null==(a=this.config.measurement)?void 0:a.readyToMeasuringDelay)||3;i>0;i--)if(this.events.onLog(`측정 시작까지 ${i}초 남았습니다...`),await u(1),!e()||!t()||!n())return;e()&&t()&&n()&&i("measuring")}catch(s){this.events.onLog("Ready to measuring 상태 전환 중 오류: "+String(s))}}}function b(e,t){const n=new Blob([e],{type:"text/plain"}),i=URL.createObjectURL(n),a=document.createElement("a");a.href=i,a.download=t,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(i)}function f(e,t,n,i){t.enabled?t.autoDownload?(!function(e,t,n){n.isAndroid?void 0!==window.Android?window.Android.downloadRgbData(e):b(e,t):n.isIOS&&void 0!==window.webkit?window.webkit.messageHandlers.downloadRgbData.postMessage(e):b(e,t)}(e,t.filename,n),null==i||i("자동 다운로드를 실행했습니다.")):(!function(e,t){const n=window.open("","_blank","width=500,height=400,scrollbars=yes,resizable=yes");if(!n)return void alert("팝업이 차단되었습니다. 팝업을 허용해주세요.");const i=(new Date).toLocaleString("ko-KR");n.document.write(`\n <!DOCTYPE html>\n <html lang="ko">\n <head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>RGB 데이터 다운로드</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n margin: 20px;\n background-color: #f5f5f5;\n }\n .container {\n background: white;\n padding: 20px;\n border-radius: 8px;\n box-shadow: 0 2px 10px rgba(0,0,0,0.1);\n max-width: 450px;\n margin: 0 auto;\n }\n h2 {\n color: #333;\n margin-bottom: 20px;\n text-align: center;\n }\n .info {\n background: #e3f2fd;\n padding: 15px;\n border-radius: 5px;\n margin-bottom: 20px;\n border-left: 4px solid #2196f3;\n }\n .info p {\n margin: 5px 0;\n color: #1976d2;\n }\n .data-preview {\n background: #f8f9fa;\n border: 1px solid #dee2e6;\n border-radius: 4px;\n padding: 10px;\n margin: 15px 0;\n max-height: 100px;\n overflow-y: auto;\n font-family: monospace;\n font-size: 12px;\n color: #495057;\n }\n .buttons {\n display: flex;\n gap: 10px;\n justify-content: center;\n margin-top: 20px;\n flex-wrap: wrap;\n }\n button {\n padding: 10px 20px;\n border: none;\n border-radius: 5px;\n cursor: pointer;\n font-size: 14px;\n transition: background-color 0.2s;\n min-width: 100px;\n }\n .download-btn {\n background-color: #4caf50;\n color: white;\n }\n .download-btn:hover {\n background-color: #45a049;\n }\n .cancel-btn {\n background-color: #f44336;\n color: white;\n }\n .cancel-btn:hover {\n background-color: #da190b;\n }\n .copy-btn {\n background-color: #2196f3;\n color: white;\n }\n .copy-btn:hover {\n background-color: #1976d2;\n }\n @media (max-width: 480px) {\n .buttons {\n flex-direction: column;\n }\n button {\n width: 100%;\n margin: 5px 0;\n }\n }\n </style>\n </head>\n <body>\n <div class="container">\n <h2>📊 RGB 데이터 다운로드</h2>\n \n <div class="info">\n <p><strong>측정 완료 시간:</strong> ${i}</p>\n <p><strong>파일명:</strong> ${t}</p>\n <p><strong>데이터 크기:</strong> ${(e.length/1024).toFixed(2)} KB</p>\n <p><strong>데이터 라인 수:</strong> ${e.split("\n").length.toLocaleString()}</p>\n </div>\n\n <div class="data-preview">\n <strong>데이터 미리보기:</strong><br>\n ${e.substring(0,200).replace(/</g,"&lt;").replace(/>/g,"&gt;")}${e.length>200?"...":""}\n </div>\n\n <div class="buttons">\n <button class="download-btn" onclick="downloadData()">\n 💾 다운로드\n </button>\n <button class="copy-btn" onclick="copyToClipboard()">\n 📋 복사\n </button>\n <button class="cancel-btn" onclick="window.close()">\n ❌ 취소\n </button>\n </div>\n </div>\n\n <script>\n const dataString = ${JSON.stringify(e)};\n const filename = ${JSON.stringify(t)};\n\n function downloadData() {\n try {\n // 사용자 정보 가져오기 (파일명에 사용)\n const userData = JSON.parse(sessionStorage.getItem('userData') || '{}');\n \n // 현재 날짜와 시간을 포맷팅 (YYYYMMDD_HHMM 형식)\n const now = new Date();\n const dateStr = now.getFullYear() + \n ('0' + (now.getMonth() + 1)).slice(-2) + \n ('0' + now.getDate()).slice(-2) + \n '_' +\n ('0' + now.getHours()).slice(-2) + \n ('0' + now.getMinutes()).slice(-2);\n \n // 고유한 파일명 생성\n const finalFilename = userData.userId ? \n \`rgb_data_\${userData.userId}_\${dateStr}.txt\` : \n \`\${filename.replace('.txt', '')}_\${dateStr}.txt\`;\n\n const blob = new Blob([dataString], { type: 'text/plain' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = finalFilename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n \n alert('다운로드가 시작되었습니다.');\n } catch (error) {\n console.error('다운로드 오류:', error);\n alert('다운로드 중 오류가 발생했습니다.');\n }\n }\n\n function copyToClipboard() {\n if (navigator.clipboard && window.isSecureContext) {\n navigator.clipboard.writeText(dataString).then(() => {\n alert('데이터가 클립보드에 복사되었습니다.');\n }).catch(err => {\n console.error('복사 실패:', err);\n fallbackCopyTextToClipboard();\n });\n } else {\n fallbackCopyTextToClipboard();\n }\n }\n\n function fallbackCopyTextToClipboard() {\n try {\n const textArea = document.createElement('textarea');\n textArea.value = dataString;\n textArea.style.position = 'fixed';\n textArea.style.left = '-999999px';\n textArea.style.top = '-999999px';\n document.body.appendChild(textArea);\n textArea.focus();\n textArea.select();\n document.execCommand('copy');\n document.body.removeChild(textArea);\n alert('데이터가 클립보드에 복사되었습니다.');\n } catch (err) {\n console.error('복사 실패:', err);\n alert('복사에 실패했습니다. 수동으로 복사해주세요.');\n }\n }\n <\/script>\n </body>\n </html>\n `),n.document.close()}(e,t.filename),null==i||i("다운로드 다이얼로그를 표시했습니다.")):null==i||i("데이터 다운로드가 비활성화되어 있습니다.")}const C="0.1.14",v=class e{constructor(n={},i={}){t(this,"configManager"),t(this,"eventManager"),t(this,"mediapipeManager"),t(this,"stateManager"),t(this,"webcamManager"),t(this,"facePositionManager"),t(this,"workerManager"),t(this,"measurementManager"),t(this,"isFaceDetectiveActive",!1),t(this,"isFaceInCircle",!1),t(this,"isReadyTransitionStarted",!1),t(this,"isInitialized",!1),t(this,"video"),t(this,"canvasElement"),t(this,"videoCanvas"),t(this,"videoCtx"),t(this,"container"),t(this,"ctx"),t(this,"lastBoundingBox",null),t(this,"faceDetectionTimer",null),t(this,"isFaceDetected",!1),t(this,"isFirstFrame",!0),this.configManager=new s(n),this.eventManager=new c(i),this.stateManager=new h,this.mediapipeManager=new l,this.webcamManager=new d(this.configManager.getConfig(),{onWebcamError:this.handleWebcamError.bind(this)}),this.facePositionManager=new g(this.configManager.getConfig().errorBounding||4),this.workerManager=new m({onDataProcessed:this.handleWorkerData.bind(this)}),this.measurementManager=new p(this.configManager.getConfig(),{onProgress:this.eventManager.emitProgress.bind(this.eventManager),onMeasurementComplete:this.handleMeasurementComplete.bind(this),onDataDownload:this.createDownloadFunction(),onLog:e=>this.log(e)}),this.stateManager.setStateChangeCallback(((e,t)=>{this.eventManager.emitStateChange(e,t)})),this.log(`SDK 인스턴스가 생성되었습니다. (v${e.VERSION})`)}async initializeAndStart(){try{this.log("SDK 완전 초기화를 시작합니다..."),await this.initializeElements(),this.isInitialized||(await this.initializeMediaPipe(),this.workerManager.initialize(),this.isInitialized=!0,this.log("SDK 초기화가 완료되었습니다.")),await this.handleClickStart(),this.log("SDK 초기화 및 측정 시작이 완료되었습니다.")}catch(e){throw this.eventManager.emitError(e,"SDK 완전 초기화 중 오류"),e}}dispose(){this.stopDetection(),this.workerManager.terminate(),this.webcamManager.dispose(),this.mediapipeManager.dispose(),this.eventManager.dispose(),this.isInitialized=!1,this.log("SDK가 정리되었습니다.")}createDownloadFunction(){return e=>{var t,n,i,a,s;const o=this.configManager.getConfig();f(e,{enabled:(null==(t=o.dataDownload)?void 0:t.enabled)||!1,autoDownload:(null==(n=o.dataDownload)?void 0:n.autoDownload)||!1,filename:(null==(i=o.dataDownload)?void 0:i.filename)||"rgb_data.txt"},{isAndroid:(null==(a=o.platform)?void 0:a.isAndroid)||!1,isIOS:(null==(s=o.platform)?void 0:s.isIOS)||!1},this.log.bind(this))}}handleMeasurementComplete(e){var t;const{positionErr:n,yPositionErr:i}=this.facePositionManager.getPositionErrors();this.stateManager.setState(o.COMPLETED),this.isFaceDetectiveActive=!1,this.eventManager.emitMeasurementComplete({...e,quality:{...e.quality,positionError:n,yPositionError:i,dataPoints:(null==(t=e.quality)?void 0:t.dataPoints)||0}})}handleWorkerData(e){if(!this.stateManager.isState(o.MEASURING))return this.workerManager.getLastRGB();const t=function(e,t,n,i,a,s){const{timestamp:o}=e;let{meanRed:r,meanGreen:c,meanBlue:l}=e;return o===s.timestamp||0===r||0===c||0===l?s:(s.r===r&&s.g===c&&s.b===l&&(r+=.01*(Math.random()-.5),c+=.01*(Math.random()-.5),l+=.01*(Math.random()-.5)),t.push(r),n.push(c),i.push(l),a.push(o),{timestamp:o,r:r,g:c,b:l})}(e,[],[],[],[],this.workerManager.getLastRGB());return this.measurementManager.addRGBData(t),t}setupFaceDetection(){this.mediapipeManager.setOnResultsCallback((e=>{var t;if(!e.detections||0===e.detections.length)return this.eventManager.emitFaceDetectionChange(!1,null),this.isFaceInCircle=!1,this.eventManager.emitFacePositionChange(!1),this.measurementManager.resetData(),void this.eventManager.emitError(new Error("얼굴을 인식할 수 없습니다. 조명이 충분한 곳에서 다시 시도해주세요."),"FACE_NOT_DETECTED");const n=function(e,{isFirstFrame:t,isFaceDetected:n,faceDetectionTimer:i,FACE_DETECTION_TIMEOUT:a,handleFaceDetection:s,handleNoDetection:o,mean_red:r}){if(t&&(t=!1,i=setTimeout((()=>{}),a)),e.detections&&e.detections.length>0){const a=e.detections[0],o=function({boundingBox:e},t,n){const i=e.width*t*.6,a=e.height*n*.6;return{left:e.xCenter*t-i/2,top:e.yCenter*n-a/2,width:i,height:a}}(a,e.image.width,e.image.height);return s(a),n||(n=!0,i&&(clearTimeout(i),i=null)),{isFirstFrame:t,isFaceDetected:n,faceDetectionTimer:i,lastBoundingBox:o}}return{isFirstFrame:t,isFaceDetected:n,faceDetectionTimer:i,lastBoundingBox:null}}(e,{isFirstFrame:this.isFirstFrame,isFaceDetected:this.isFaceDetected,faceDetectionTimer:this.faceDetectionTimer,FACE_DETECTION_TIMEOUT:(null==(t=this.configManager.getConfig().faceDetection)?void 0:t.timeout)||3e3,handleFaceDetection:this.handleFaceDetection.bind(this),handleNoDetection:()=>{},mean_red:[]});this.isFirstFrame=n.isFirstFrame,this.isFaceDetected=n.isFaceDetected,this.faceDetectionTimer=n.faceDetectionTimer,this.lastBoundingBox=n.lastBoundingBox}))}handleFaceDetection(e){this.eventManager.emitFaceDetectionChange(!0,this.lastBoundingBox);const{isInCircle:t}=this.facePositionManager.updateFacePosition(e.boundingBox,this.video,this.container);if(this.isFaceInCircle!==t&&(this.isFaceInCircle=t,this.eventManager.emitFacePositionChange(t)),this.stateManager.isState(o.INITIAL)&&!this.isReadyTransitionStarted&&t&&(this.isReadyTransitionStarted=!0,this.stateManager.setState(o.READY),this.startReadyToMeasuringTransition()),!t)return this.stateManager.isState(o.READY)&&(this.stateManager.setState(o.INITIAL),this.isReadyTransitionStarted=!1),this.measurementManager.resetData(),void this.eventManager.emitError(new Error("원 안에 얼굴을 위치해주세요."),"FACE_OUT_OF_CIRCLE");if(this.stateManager.isState(o.MEASURING)){const e=this.canvasElement.width,t=this.canvasElement.height,n=this.ctx.getImageData(0,0,e,t);this.workerManager.postFaceRegionData(n)}}async handleClickStart(){try{this.isFaceDetectiveActive=!0,this.isFaceDetected=!1,this.isFirstFrame=!0,this.isFaceInCircle=!1,this.isReadyTransitionStarted=!1;const e=await this.webcamManager.startWebcam();this.video.srcObject=e,this.video.play(),this.video.addEventListener("loadeddata",(()=>{let e=0,t=0;const n=async()=>{var i,a;if(!this.isFaceDetectiveActive||this.video.readyState<3)return;const s=performance.now(),o=s-e,r=(null==(i=this.configManager.getConfig().measurement)?void 0:i.frameInterval)||33.33;if(o>r){e=s-o%r,t++,this.videoCtx.drawImage(this.video,0,0,this.videoCanvas.width,this.videoCanvas.height);const n=(null==(a=this.configManager.getConfig().measurement)?void 0:a.frameProcessInterval)||30;if(t%n===0)await this.mediapipeManager.sendImage(this.video);else if(this.lastBoundingBox&&this.isFaceInCircle){const{left:e,top:t,width:n,height:i}=this.lastBoundingBox,a=this.videoCtx.getImageData(e,t,n,i);this.workerManager.postFaceRegionData(a)}}requestAnimationFrame(n)};requestAnimationFrame(n)}))}catch(e){this.handleWebcamError(e)}}handleWebcamError(e){var t;const n=(null==(t=this.configManager.getConfig().platform)?void 0:t.isIOS)||!1;this.eventManager.emitWebcamError(e,n)}async startReadyToMeasuringTransition(){await this.measurementManager.startReadyToMeasuringTransition((()=>this.stateManager.isState(o.READY)),(()=>this.isFaceDetectiveActive),(()=>this.isFaceInCircle),(e=>this.stateManager.setState(e)))}log(e,...t){var n;(null==(n=this.configManager.getConfig().debug)?void 0:n.enableConsoleLog)&&console.log(`[FaceDetectionSDK] ${e}`,...t)}stopDetection(){this.isFaceDetectiveActive&&(this.isFaceDetectiveActive=!1,this.webcamManager.stopWebcam(),this.workerManager.terminate(),this.faceDetectionTimer&&(clearTimeout(this.faceDetectionTimer),this.faceDetectionTimer=null))}getCurrentState(){return this.stateManager.getCurrentState()}isState(e){return this.stateManager.isState(e)}isAnyState(...e){return this.stateManager.isAnyState(...e)}isFaceInsideCircle(){return this.isFaceInCircle}async initializeElements(){const e=this.configManager.getConfig();if(!e.elements)throw new Error("HTML 요소들이 config에 제공되지 않았습니다. config.elements를 설정해주세요.");this.video=e.elements.video,this.canvasElement=e.elements.canvasElement,this.videoCanvas=e.elements.videoCanvas,this.container=e.elements.container;const t=this.videoCanvas.getContext("2d",{willReadFrequently:!0});if(!t)throw new Error("Video canvas context를 가져올 수 없습니다.");this.videoCtx=t;const n=this.canvasElement.getContext("2d",{willReadFrequently:!0});if(!n)throw new Error("Canvas context를 가져올 수 없습니다.");this.ctx=n}async initializeMediaPipe(){var e;const t=this.configManager.getConfig();await this.mediapipeManager.initialize((null==(e=t.faceDetection)?void 0:e.minDetectionConfidence)||.5),this.setupFaceDetection()}};t(v,"VERSION",C);let D=v;const w=D.VERSION;exports.FaceDetectionSDK=D,exports.SDK_VERSION=w; //# sourceMappingURL=index.js.map