face-detection-web-sdk
Version:
웹 기반 얼굴 인식을 통해 실시간으로 심박수, 스트레스, 혈압 등의 건강 정보를 측정하는 SDK
3 lines (2 loc) • 26.7 kB
JavaScript
;var t=Object.defineProperty,e=(e,n,i)=>((e,n,i)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:i}):e[n]=i)(e,"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(t={}){e(this,"config"),this.config=this.mergeConfig(a,t)}mergeConfig(t,e){const n=structuredClone(t);return Object.entries(e).reduce(((t,[e,n])=>(void 0===n||(t[e]="elements"===e||"object"!=typeof n||Array.isArray(n)?n:{...t[e],...n}),t)),n)}getConfig(){return this.config}}var o=(t=>(t.INITIAL="initial",t.READY="ready",t.MEASURING="measuring",t.COMPLETED="completed",t))(o||{}),r=(t=>(t.FACE_NOT_DETECTED="face_not_detected",t.FACE_OUT_OF_CIRCLE="face_out_of_circle",t.WEBCAM_PERMISSION_DENIED="webcam_permission_denied",t.WEBCAM_ACCESS_FAILED="webcam_access_failed",t.INITIALIZATION_FAILED="initialization_failed",t.UNKNOWN_ERROR="unknown_error",t))(r||{});class c{constructor(t={},n){e(this,"stateChangeCallbacks",[]),e(this,"callbacks",{}),e(this,"log"),this.callbacks=t,this.log=n,t.onStateChange&&this.onStateChange(t.onStateChange)}onStateChange(t){this.stateChangeCallbacks.push(t)}removeStateChangeCallback(t){const e=this.stateChangeCallbacks.indexOf(t);e>-1&&this.stateChangeCallbacks.splice(e,1)}emitStateChange(t,e){this.stateChangeCallbacks.forEach((n=>{try{n(t,e)}catch(i){console.error("상태 변경 콜백 실행 중 오류:",i)}}))}emitError(t,e,n){var i,a,s;const o=n?`${n}: ${t.message}`:t.message,c=e??r.UNKNOWN_ERROR;null==(i=this.log)||i.call(this,`오류 발생: ${o}`),null==(s=(a=this.callbacks).onError)||s.call(a,{type:c,message:o})}emitWebcamError(t,e){var n,i,a;const s="NotAllowedError"===t.name||"PermissionDeniedError"===t.name,o=e&&/permission|허가|권한/.test(t.message??""),c=s||o,l=c?r.WEBCAM_PERMISSION_DENIED:r.WEBCAM_ACCESS_FAILED,h=c?"웹캠 접근 권한이 거부되었습니다. 브라우저 설정에서 카메라 권한을 허용해주세요.":`웹캠에 접근할 수 없습니다: ${t.message}`;null==(n=this.log)||n.call(this,`웹캠 오류 발생: ${h}`),null==(a=(i=this.callbacks).onError)||a.call(i,{type:l,message:h})}emitFaceDetectionChange(t,e){var n,i;null==(i=(n=this.callbacks).onFaceDetectionChange)||i.call(n,t,e)}emitFacePositionChange(t){var e,n;null==(n=(e=this.callbacks).onFacePositionChange)||n.call(e,t)}emitProgress(t,e){var n,i;null==(i=(n=this.callbacks).onProgress)||i.call(n,t,e)}emitMeasurementComplete(t){var e,n;null==(n=(e=this.callbacks).onMeasurementComplete)||n.call(e,t)}emitCountdown(t,e){var n,i;null==(i=(n=this.callbacks).onCountdown)||i.call(n,t,e)}dispose(){this.stateChangeCallbacks=[],this.callbacks={}}}class l{constructor(){e(this,"faceDetection"),e(this,"onResultsCallback",null)}async initialize(t=.5){this.faceDetection=new n.FaceDetection({locateFile:t=>`https://cdn.jsdelivr.net/npm/@mediapipe/face_detection/${t}`});const e={model:"short",minDetectionConfidence:t,runningMode:"VIDEO"};this.faceDetection.setOptions(e)}setOnResultsCallback(t){this.onResultsCallback=t,this.faceDetection.onResults(this.onResultsCallback)}async sendImage(t){await this.faceDetection.send({image:t})}dispose(){this.faceDetection=null,this.onResultsCallback=null}}class h{constructor(){e(this,"currentState",o.INITIAL),e(this,"stateChangeCallback")}getCurrentState(){return this.currentState}setStateChangeCallback(t){this.stateChangeCallback=t}setState(t){const e=this.currentState;this.currentState=t,this.emitStateChange(t,e)}emitStateChange(t,e){var n;try{null==(n=this.stateChangeCallback)||n.call(this,t,e)}catch(i){console.error("상태 변경 콜백 실행 중 오류:",i)}}isState(t){return this.currentState===t}isAnyState(...t){return t.includes(this.currentState)}}class d{constructor(t,n){e(this,"webcamStream",null),e(this,"config"),e(this,"events"),this.config=t,this.events=n}async startWebcam(){var t,e,n;try{const i={width:(null==(t=this.config.video)?void 0:t.width)||640,height:(null==(e=this.config.video)?void 0:e.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(){var t;null==(t=this.webcamStream)||t.getTracks().forEach((t=>t.stop())),this.webcamStream=null}handleWebcamError(t){var e;const n=(null==(e=this.config.platform)?void 0:e.isIOS)||!1;this.events.onWebcamError(t,n)}dispose(){this.stopWebcam()}}class g{constructor(t=4){e(this,"lastPosition",0),e(this,"lastYPosition",0),e(this,"positionErr",0),e(this,"yPositionErr",0),this.errorBounding=t}updateFacePosition(t,e,n){const i=t.xCenter*e.videoWidth,a=t.yCenter*e.videoHeight,{isInCircle:s}=function(t,e,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(t-c.x,2)+Math.pow(e-c.y,2));return{isInCircle:h<=l,distance:h,allowedRadius:l,progressCenter:c}}(i,a,e,n);var o,r,c,l,h,d,g;return({lastPosition:this.lastPosition,lastYPosition:this.lastYPosition,positionErr:this.positionErr,yPositionErr:this.yPositionErr}=(o=i,r=a,c=this.lastPosition,l=this.lastYPosition,h=this.positionErr,d=this.yPositionErr,g=this.errorBounding,c&&Math.abs(o-c)>g&&h++,l&&Math.abs(r-l)>g&&d++,{lastPosition:o,lastYPosition:r,positionErr:h,yPositionErr:d})),{isInCircle:s}}getPositionErrors(){return{positionErr:this.positionErr,yPositionErr:this.yPositionErr}}}class m{constructor(t){e(this,"faceRegionWorker"),e(this,"lastRGB"),e(this,"events"),this.events=t}initialize(){const t=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(t,{type:"module"}),this.lastRGB={timestamp:0,r:null,g:null,b:null},this.setupWorker()}setupWorker(){this.faceRegionWorker.onmessage=({data:t})=>{this.lastRGB=this.events.onDataProcessed(t)}}postFaceRegionData(t){this.faceRegionWorker.postMessage({faceRegionData:t})}terminate(){var t;null==(t=this.faceRegionWorker)||t.terminate()}getLastRGB(){return this.lastRGB}}const u=t=>new Promise((e=>setTimeout(e,1e3*t)));class p{constructor(t,n){e(this,"red",[]),e(this,"green",[]),e(this,"blue",[]),e(this,"timestamps",[]),e(this,"isCompleted",!1),e(this,"isCountdownActive",!1),this.config=t,this.events=n}addRGBData({r:t,g:e,b:n,timestamp:i}){var a;if(this.isCompleted||null==t||null==e||null==n)return;this.red.push(t),this.green.push(e),this.blue.push(n),this.timestamps.push(i);const s=(null==(a=this.config.measurement)?void 0:a.targetDataPoints)??450;if(this.timestamps.length>s){const t=this.timestamps.length-s;this.red.splice(0,t),this.green.splice(0,t),this.blue.splice(0,t),this.timestamps.splice(0,t)}this.events.onProgress(Math.min(this.timestamps.length/s,1),this.timestamps.length),this.timestamps.length===s&&this.finalize()}finalize(){this.isCompleted=!0;const t=(e=this.red,n=this.green,i=this.blue,a=this.timestamps,e.map(((t,s)=>`${a[s]}\t${e[s]}\t${n[s]}\t${i[s]}`)).join("\n"));var e,n,i,a;const s={rawData:{sigR:this.red,sigG:this.green,sigB:this.blue,timestamp:this.timestamps},quality:{positionError:0,yPositionError:0,dataPoints:this.timestamps.length}};this.events.onMeasurementComplete(s),this.events.onDataDownload(t)}resetData(){this.red=[],this.green=[],this.blue=[],this.timestamps=[],this.isCompleted=this.isCountdownActive=!1}stopCountdown(){this.isCountdownActive=!1,this.events.onLog("카운트다운이 사용자에 의해 중단되었습니다.")}isCountdownRunning(){return this.isCountdownActive}async startReadyToMeasuringTransition(t,e,n,i){var a;const s=(null==(a=this.config.measurement)?void 0:a.readyToMeasuringDelay)??3;this.isCountdownActive=!0,this.events.onCountdown(s,s),this.events.onLog(`측정 시작까지 ${s}초 남았습니다...`);try{for(let t=s-1;t>0;t--){if(await u(1),!this.isCountdownActive)return;this.events.onCountdown(t,s),this.events.onLog(`측정 시작까지 ${t}초 남았습니다...`)}this.isCountdownActive=!1,t()&&e()&&n()&&i("measuring")}catch(o){this.isCountdownActive=!1,this.events.onLog("Ready to measuring 상태 전환 중 오류: "+o)}}}function b(t,e){const n=new Blob([t],{type:"text/plain"}),i=URL.createObjectURL(n),a=document.createElement("a");a.href=i,a.download=e,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(i)}function C(t,e,n,i){e.enabled?e.autoDownload?(!function(t,e,n){n.isAndroid?void 0!==window.Android?window.Android.downloadRgbData(t):b(t,e):n.isIOS&&void 0!==window.webkit?window.webkit.messageHandlers.downloadRgbData.postMessage(t):b(t,e)}(t,e.filename,n),null==i||i("자동 다운로드를 실행했습니다.")):(!function(t,e){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> ${e}</p>\n <p><strong>데이터 크기:</strong> ${(t.length/1024).toFixed(2)} KB</p>\n <p><strong>데이터 라인 수:</strong> ${t.split("\n").length.toLocaleString()}</p>\n </div>\n\n <div class="data-preview">\n <strong>데이터 미리보기:</strong><br>\n ${t.substring(0,200).replace(/</g,"<").replace(/>/g,">")}${t.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(t)};\n const filename = ${JSON.stringify(e)};\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()}(t,e.filename),null==i||i("다운로드 다이얼로그를 표시했습니다.")):null==i||i("데이터 다운로드가 비활성화되어 있습니다.")}const v="0.2.0",f=class t{constructor(n={},i={}){e(this,"configManager"),e(this,"eventManager"),e(this,"mediapipeManager"),e(this,"stateManager"),e(this,"webcamManager"),e(this,"facePositionManager"),e(this,"workerManager"),e(this,"measurementManager"),e(this,"isFaceDetectiveActive",!1),e(this,"isFaceInCircle",!1),e(this,"isReadyTransitionStarted",!1),e(this,"isInitialized",!1),e(this,"video"),e(this,"canvasElement"),e(this,"videoCanvas"),e(this,"videoCtx"),e(this,"container"),e(this,"ctx"),e(this,"lastBoundingBox",null),e(this,"faceDetectionTimer",null),e(this,"isFaceDetected",!1),e(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:t=>this.log(t),onCountdown:(t,e)=>{this.eventManager.emitCountdown(t,e)}}),this.stateManager.setStateChangeCallback(((t,e)=>{this.eventManager.emitStateChange(t,e)})),this.log(`SDK 인스턴스가 생성되었습니다. (v${t.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(t){throw this.eventManager.emitError(t,r.INITIALIZATION_FAILED,"SDK 완전 초기화 중 오류"),t}}dispose(){this.stopDetection(),this.workerManager.terminate(),this.webcamManager.dispose(),this.mediapipeManager.dispose(),this.eventManager.dispose(),this.isInitialized=!1,this.log("SDK가 정리되었습니다.")}createDownloadFunction(){return t=>{var e,n,i,a,s;const o=this.configManager.getConfig();C(t,{enabled:(null==(e=o.dataDownload)?void 0:e.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(t){var e;const{positionErr:n,yPositionErr:i}=this.facePositionManager.getPositionErrors();this.stateManager.setState(o.COMPLETED),this.isFaceDetectiveActive=!1,this.eventManager.emitMeasurementComplete({...t,quality:{...t.quality,positionError:n,yPositionError:i,dataPoints:(null==(e=t.quality)?void 0:e.dataPoints)||0}})}handleWorkerData(t){if(!this.stateManager.isState(o.MEASURING))return this.workerManager.getLastRGB();const e=function(t,e,n,i,a,s){const{timestamp:o}=t;let{meanRed:r,meanGreen:c,meanBlue:l}=t;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)),e.push(r),n.push(c),i.push(l),a.push(o),{timestamp:o,r:r,g:c,b:l})}(t,[],[],[],[],this.workerManager.getLastRGB());return this.measurementManager.addRGBData(e),e}setupFaceDetection(){this.mediapipeManager.setOnResultsCallback((t=>{var e;if(!t.detections||0===t.detections.length)return void this.handleNoFaceDetected();const n=this.configManager.getConfig(),i=function(t,{isFirstFrame:e,isFaceDetected:n,faceDetectionTimer:i,FACE_DETECTION_TIMEOUT:a,handleFaceDetection:s,handleNoDetection:o,mean_red:r}){if(e&&(e=!1,i=setTimeout((()=>{}),a)),t.detections&&t.detections.length>0){const a=t.detections[0],o=function({boundingBox:t},e,n){const i=t.width*e*.6,a=t.height*n*.6;return{left:t.xCenter*e-i/2,top:t.yCenter*n-a/2,width:i,height:a}}(a,t.image.width,t.image.height);return s(a),n||(n=!0,i&&(clearTimeout(i),i=null)),{isFirstFrame:e,isFaceDetected:n,faceDetectionTimer:i,lastBoundingBox:o}}return{isFirstFrame:e,isFaceDetected:n,faceDetectionTimer:i,lastBoundingBox:null}}(t,{isFirstFrame:this.isFirstFrame,isFaceDetected:this.isFaceDetected,faceDetectionTimer:this.faceDetectionTimer,FACE_DETECTION_TIMEOUT:(null==(e=n.faceDetection)?void 0:e.timeout)??3e3,handleFaceDetection:this.handleFaceDetection.bind(this),handleNoDetection:()=>{},mean_red:[]});this.isFirstFrame=i.isFirstFrame,this.isFaceDetected=i.isFaceDetected,this.faceDetectionTimer=i.faceDetectionTimer,this.lastBoundingBox=i.lastBoundingBox}))}handleNoFaceDetected(){this.eventManager.emitFaceDetectionChange(!1,null),this.isFaceInCircle=!1,this.eventManager.emitFacePositionChange(!1),this.measurementManager.resetData(),this.eventManager.emitError(new Error("얼굴을 인식할 수 없습니다. 조명이 충분한 곳에서 다시 시도해주세요."),r.FACE_NOT_DETECTED)}handleFaceDetection(t){this.eventManager.emitFaceDetectionChange(!0,this.lastBoundingBox);const{isInCircle:e}=this.facePositionManager.updateFacePosition(t.boundingBox,this.video,this.container);if(this.isFaceInCircle!==e&&(this.isFaceInCircle=e,this.eventManager.emitFacePositionChange(e)),this.stateManager.isState(o.INITIAL)&&!this.isReadyTransitionStarted&&e&&(this.isReadyTransitionStarted=!0,this.stateManager.setState(o.READY),this.startReadyToMeasuringTransition()),e){if(this.stateManager.isState(o.MEASURING)){const t=this.ctx.getImageData(0,0,this.canvasElement.width,this.canvasElement.height);this.workerManager.postFaceRegionData(t)}}else this.handleFaceOutOfCircle()}handleFaceOutOfCircle(){this.stateManager.isState(o.READY)&&(this.stateManager.setState(o.INITIAL),this.isReadyTransitionStarted=!1),this.measurementManager.resetData(),this.eventManager.emitError(new Error("원 안에 얼굴을 위치해주세요."),r.FACE_OUT_OF_CIRCLE)}async handleClickStart(){try{await this.initializeDetectionState();const t=await this.webcamManager.startWebcam();await this.setupVideoStream(t),this.startVideoProcessing()}catch(t){this.handleWebcamError(t)}}async initializeDetectionState(){this.isFaceDetectiveActive=!0,this.isFaceDetected=!1,this.isFirstFrame=!0,this.isFaceInCircle=!1,this.isReadyTransitionStarted=!1}async setupVideoStream(t){this.video.srcObject=t,this.video.play()}startVideoProcessing(){this.video.addEventListener("loadeddata",(()=>{this.initializeVideoProcessor()}))}initializeVideoProcessor(){let t=0,e=0;const n=async()=>{var i,a;if(!this.isFaceDetectiveActive||this.video.readyState<3)return;const s=performance.now(),o=s-t,r=(null==(i=this.configManager.getConfig().measurement)?void 0:i.frameInterval)||33.33;if(o>r){t=s-o%r,e++,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(e%n===0)await this.mediapipeManager.sendImage(this.video);else if(null!==this.lastBoundingBox&&this.isFaceInCircle){const{left:t,top:e,width:n,height:i}=this.lastBoundingBox,a=this.videoCtx.getImageData(t,e,n,i);this.workerManager.postFaceRegionData(a)}}requestAnimationFrame(n)};requestAnimationFrame(n)}handleWebcamError(t){var e;const n=(null==(e=this.configManager.getConfig().platform)?void 0:e.isIOS)||!1;this.eventManager.emitWebcamError(t,n)}async startReadyToMeasuringTransition(){await this.measurementManager.startReadyToMeasuringTransition((()=>this.stateManager.isState(o.READY)),(()=>this.isFaceDetectiveActive),(()=>this.isFaceInCircle),(t=>this.stateManager.setState(t)))}log(t,...e){var n;(null==(n=this.configManager.getConfig().debug)?void 0:n.enableConsoleLog)&&console.log(`[FaceDetectionSDK] ${t}`,...e)}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(t){return this.stateManager.isState(t)}isAnyState(...t){return this.stateManager.isAnyState(...t)}isFaceInsideCircle(){return this.isFaceInCircle}async initializeElements(){const t=this.configManager.getConfig();if(!t.elements)throw new Error("HTML 요소들이 config에 제공되지 않았습니다. config.elements를 설정해주세요.");this.video=t.elements.video,this.canvasElement=t.elements.canvasElement,this.videoCanvas=t.elements.videoCanvas,this.container=t.elements.container;const e=this.videoCanvas.getContext("2d",{willReadFrequently:!0});if(!e)throw new Error("Video canvas context를 가져올 수 없습니다.");this.videoCtx=e;const n=this.canvasElement.getContext("2d",{willReadFrequently:!0});if(!n)throw new Error("Canvas context를 가져올 수 없습니다.");this.ctx=n}async initializeMediaPipe(){var t;const e=this.configManager.getConfig();await this.mediapipeManager.initialize((null==(t=e.faceDetection)?void 0:t.minDetectionConfidence)||.5),this.setupFaceDetection()}};e(f,"VERSION",v);let w=f;const D=w.VERSION;exports.FaceDetectionSDK=w,exports.SDK_VERSION=D;
//# sourceMappingURL=index.js.map