@face-detector/core
Version:
Face Detector Web SDK Core Package
1,283 lines (1,282 loc) • 46.3 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { FilesetResolver, FaceDetector as FaceDetector$1 } from "@mediapipe/tasks-vision";
var i = /* @__PURE__ */ ((n) => (n.INITIALIZING = "initializing", n.READY = "ready", n.RUNNING = "running", n.MEASURING = "measuring", n.COMPLETED = "completed", n.FAILED = "failed", n))(i || {});
class BaseManager {
constructor(config, eventEmitter) {
__publicField(this, "_config");
__publicField(this, "_eventEmitter");
this._config = config;
this._eventEmitter = eventEmitter;
}
get config() {
return this._config;
}
get eventEmitter() {
return this._eventEmitter;
}
}
function calculateBoundingBox(boundingBox, imageWidth, imageHeight) {
const scaleFactor = 0.6;
const width = boundingBox.width * imageWidth * scaleFactor;
const height = boundingBox.height * imageHeight * scaleFactor;
const left = boundingBox.xCenter * imageWidth - width / 2;
const top = boundingBox.yCenter * imageHeight - height / 2;
return { left, top, width, height };
}
function calculateROI(facePosition, frameWidth, frameHeight) {
const boundingBox = calculateBoundingBox(facePosition, frameWidth, frameHeight);
const sx = Math.max(0, Math.floor(boundingBox.left));
const sy = Math.max(0, Math.floor(boundingBox.top));
const sw = Math.max(1, Math.floor(Math.min(boundingBox.width, frameWidth - sx)));
const sh = Math.max(1, Math.floor(Math.min(boundingBox.height, frameHeight - sy)));
return { sx, sy, sw, sh };
}
const _Logger = class _Logger {
constructor() {
__publicField(this, "debugEnabled", false);
}
static getInstance() {
if (!_Logger.instance) {
_Logger.instance = new _Logger();
}
return _Logger.instance;
}
setDebugMode(enabled) {
this.debugEnabled = enabled;
}
debug(message, ...args) {
if (this.debugEnabled) {
console.log(`[FaceDetectionSDK] ${message}`, ...args);
}
}
error(message, ...args) {
console.error(`[FaceDetectionSDK ERROR] ${message}`, ...args);
}
warn(message, ...args) {
console.warn(`[FaceDetectionSDK WARN] ${message}`, ...args);
}
};
__publicField(_Logger, "instance");
let Logger = _Logger;
const logger = Logger.getInstance();
class FaceDetectionManager extends BaseManager {
constructor(config, eventEmitter) {
super(config, eventEmitter);
__publicField(this, "faceDetector");
__publicField(this, "faceDetectionManagerConfig");
__publicField(this, "minDetectionConfidence");
__publicField(this, "_isDetected");
__publicField(this, "unsubFrameCaptured");
__publicField(this, "unsubFrameWarmUp");
__publicField(this, "_facePosition", null);
__publicField(this, "_ROI", null);
__publicField(this, "imageWidth", 0);
__publicField(this, "imageHeight", 0);
/**
* MediaPipe 결과 콜백
* - 검출 여부 판단, 바운딩박스 갱신, 상태 이벤트 발행
*/
__publicField(this, "onResults", (results) => {
var _a, _b;
const isDetected = results.detections.length > 0;
const wasDetected = this.isDetected;
this.isDetected = isDetected;
if (isDetected && this.imageWidth > 0 && this.imageHeight > 0) {
const detection = results.detections[0];
if (detection.boundingBox) {
this.facePosition = this.convertBoundingBoxToFacePosition(detection.boundingBox);
this.ROI = calculateROI(this.facePosition, this.imageWidth, this.imageHeight);
if (!wasDetected) {
logger.debug("Face detected", {
confidence: (_b = (_a = detection.categories) == null ? void 0 : _a[0]) == null ? void 0 : _b.score,
position: this.facePosition
});
}
}
} else {
if (wasDetected) {
logger.debug("Face lost");
}
this.facePosition = null;
this.ROI = null;
this.eventEmitter.emit("face:lost", void 0);
this.eventEmitter.emit("system:running", void 0);
}
});
this.faceDetectionManagerConfig = config;
this.minDetectionConfidence = config.minDetectionConfidence;
this._isDetected = false;
logger.debug("FaceDetectionManager initialized", {
minDetectionConfidence: config.minDetectionConfidence,
delegate: config.delegate
});
}
get isDetected() {
return this._isDetected;
}
set isDetected(isDetected) {
this._isDetected = isDetected;
}
set facePosition(facePosition) {
if (this._facePosition === facePosition) return;
this._facePosition = facePosition;
this.eventEmitter.emit("face:position", facePosition);
}
get facePosition() {
return this._facePosition;
}
get ROI() {
return this._ROI;
}
set ROI(ROI) {
this._ROI = ROI;
}
/**
* MediaPipe Face Detection 초기화 및 결과 콜백/이벤트 구독 설정
*/
async initialize(video) {
this.imageWidth = video.videoWidth;
this.imageHeight = video.videoHeight;
this.unSubscribeEventListeners();
if (this.faceDetector) {
this.faceDetector.close();
this.faceDetector = null;
}
const vision = await FilesetResolver.forVisionTasks(
"https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm"
);
const faceDetectorOptions = {
baseOptions: {
modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite`,
delegate: this.faceDetectionManagerConfig.delegate
},
minDetectionConfidence: this.minDetectionConfidence,
runningMode: "VIDEO"
};
this.faceDetector = await FaceDetector$1.createFromOptions(vision, faceDetectorOptions);
this.setupEventListeners(video);
logger.debug("MediaPipe Face Detection initialized", {
videoSize: `${video.videoWidth}x${video.videoHeight}`,
confidence: this.minDetectionConfidence
});
}
reset() {
this.facePosition = null;
this.ROI = null;
this.isDetected = false;
logger.debug("FaceDetectionManager reset completed");
}
/**
* 현재 캔버스를 MediaPipe에 전송하여 얼굴 위치 검증 실행
*/
validateFacePosition(video, isWarmUp = false) {
const timestamp = performance.now();
const results = this.faceDetector.detectForVideo(video, timestamp);
if (isWarmUp) {
return;
}
this.onResults(results);
}
stop() {
this._isDetected = false;
}
dispose() {
this.isDetected = false;
if (this.faceDetector) {
this.faceDetector.close();
this.faceDetector = null;
}
this.unSubscribeEventListeners();
}
setupEventListeners(video) {
var _a;
(_a = this.unsubFrameWarmUp) == null ? void 0 : _a.call(this);
this.unsubFrameWarmUp = this.eventEmitter.subscribe("frame:warmUp", () => {
this.validateFacePosition(video, true);
});
}
unSubscribeEventListeners() {
var _a, _b;
(_a = this.unsubFrameCaptured) == null ? void 0 : _a.call(this);
(_b = this.unsubFrameWarmUp) == null ? void 0 : _b.call(this);
this.unsubFrameCaptured = void 0;
this.unsubFrameWarmUp = void 0;
}
convertBoundingBoxToFacePosition(bbox) {
return {
xCenter: (bbox.originX + bbox.width / 2) / this.imageWidth,
yCenter: (bbox.originY + bbox.height / 2) / this.imageHeight,
width: bbox.width / this.imageWidth,
height: bbox.height / this.imageHeight
};
}
}
class WorkerService {
constructor() {
__publicField(this, "worker");
__publicField(this, "pending", /* @__PURE__ */ new Map());
__publicField(this, "requestId", 0);
__publicField(this, "messageHandlers", /* @__PURE__ */ new Map());
}
async initialize(config) {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this.worker = new Worker(config.workerUrl, { type: "module" });
if (config.messageHandlers) {
Object.entries(config.messageHandlers).forEach(([type, handler]) => {
this.registerMessageHandler(type, handler);
});
}
this.worker.onmessage = (event) => {
const message = event.data;
if ("id" in message && this.pending.has(message.id)) {
const { resolve } = this.pending.get(message.id);
resolve(message);
this.pending.delete(message.id);
return;
}
const handler = this.messageHandlers.get(message.type) || config.onMessage;
if (handler) {
handler(message);
}
};
}
registerMessageHandler(messageType, handler) {
this.messageHandlers.set(messageType, handler);
}
sendMessage(message, transferables) {
if (!this.worker) {
throw new Error("WorkerService not initialized");
}
this.worker.postMessage(message, transferables || []);
}
sendRequest(message, transferables) {
if (!this.worker) {
return Promise.reject(new Error("WorkerService not initialized"));
}
return new Promise((resolve, reject) => {
this.pending.set(message.id, { resolve, reject });
this.worker.postMessage(message, transferables || []);
});
}
generateRequestId() {
return this.requestId++;
}
clearPendingRequests() {
this.pending.forEach(({ reject }) => {
reject(new Error("WorkerService disposed"));
});
this.pending.clear();
}
dispose() {
this.clearPendingRequests();
this.messageHandlers.clear();
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
}
class DataProcessingManager extends BaseManager {
constructor(config, eventEmitter) {
super(config, eventEmitter);
__publicField(this, "workerService");
__publicField(this, "_dataBucket");
__publicField(this, "_progressPercentage");
__publicField(this, "targetDataLength");
__publicField(this, "unsubFaceLost");
__publicField(this, "unsubCompleted");
this.targetDataLength = config.targetDataLength;
this._dataBucket = {
sigR: [],
sigG: [],
sigB: [],
timestamps: []
};
logger.debug("DataProcessingManager initialized", { targetDataLength: config.targetDataLength });
}
get dataBucket() {
return this._dataBucket;
}
get progressPercentage() {
return this._progressPercentage;
}
/**
* 워커 초기화 및 오프스크린 캔버스 연결
*/
async initialize(extractingCanvas, willReadFrequently = false) {
this.unSubscribeEventListeners();
try {
await this.initializeWorkerService();
this.clearDataBucket();
this.setUpEventListeners();
this.updateProgressPercentage(0);
await this.connectVideoCanvas(extractingCanvas, willReadFrequently);
logger.debug("DataProcessingManager Worker initialized");
} catch (error) {
logger.error("DataProcessingManager initialization failed", error);
}
}
reset() {
this.clearDataBucket();
if (this.workerService) {
this.workerService.clearPendingRequests();
}
this.updateProgressPercentage(0);
}
async dispose() {
this.stop();
if (this.workerService) {
this.workerService.dispose();
this.workerService = null;
}
this.unSubscribeEventListeners();
}
stop() {
this.clearDataBucket();
if (this.workerService) {
this.workerService.clearPendingRequests();
}
}
/**
* 오프스크린 캔버스를 워커에 전송하여 연결
*/
async connectVideoCanvas(extractingCanvas, willReadFrequently = false) {
if (extractingCanvas.dataset.transferred === "true") {
return true;
}
try {
const offscreenExtractingCanvas = extractingCanvas.transferControlToOffscreen();
extractingCanvas.dataset.transferred = "true";
const message = {
type: "connectVideoCanvas",
id: this.workerService.generateRequestId(),
extractingCanvas: offscreenExtractingCanvas,
willReadFrequently
};
const response = await this.workerService.sendRequest(message, [offscreenExtractingCanvas]);
return response.success;
} catch (error) {
logger.error("Offscreen canvas connection failed", error);
return false;
}
}
/**
* 비디오 캔버스 연결 해제 요청을 보내고 완료를 대기
*/
async releaseVideoCanvas() {
const message = {
type: "releaseVideoCanvas",
id: this.workerService.generateRequestId()
};
const response = await this.workerService.sendRequest(message);
return response.success;
}
/**
* 프레임과 얼굴 위치, 옵션을 워커에 전달하여 처리 요청
*/
processFrame(frame, isWarmUp = false) {
const message = {
type: "processFrame",
frame,
isWarmUp
};
this.workerService.sendMessage(message, [frame]);
}
/**
* 이벤트 구독 설정: 얼굴 유실 시 버킷 초기화 등
*/
setUpEventListeners() {
var _a, _b;
(_a = this.unsubFaceLost) == null ? void 0 : _a.call(this);
this.unsubFaceLost = this.eventEmitter.subscribe("face:lost", () => {
this.clearDataBucket();
this.updateProgressPercentage(this.calculateProgressPercentage());
});
(_b = this.unsubCompleted) == null ? void 0 : _b.call(this);
this.unsubCompleted = this.eventEmitter.subscribe("system:completed", () => {
});
}
async initializeWorkerService() {
this.workerService = new WorkerService();
await this.workerService.initialize({
workerUrl: new URL("data:text/javascript;base64,bGV0IGV4dHJhY3RpbmdDYW52YXMgPSBudWxsOw0KbGV0IGV4dHJhY3RpbmdDb250ZXh0ID0gbnVsbDsNCg0Kc2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbiAoZSkgew0KICBjb25zdCBtZXNzYWdlID0gZS5kYXRhOw0KDQogIHN3aXRjaCAobWVzc2FnZS50eXBlKSB7DQogICAgY2FzZSAnY29ubmVjdFZpZGVvQ2FudmFzJzoNCiAgICAgIGhhbmRsZUNvbm5lY3RWaWRlb0NhbnZhcyhtZXNzYWdlLCBlKTsNCiAgICAgIGJyZWFrOw0KICAgIGNhc2UgJ3Byb2Nlc3NGcmFtZSc6DQogICAgICBoYW5kbGVQcm9jZXNzRnJhbWUobWVzc2FnZSwgZSk7DQogICAgICBicmVhazsNCiAgICBjYXNlICdyZWxlYXNlVmlkZW9DYW52YXMnOg0KICAgICAgaGFuZGxlUmVsZWFzZVZpZGVvQ2FudmFzKG1lc3NhZ2UsIGUpOw0KICAgICAgYnJlYWs7DQogICAgZGVmYXVsdDoNCiAgICAgIGNvbnNvbGUud2Fybign7JWMIOyImCDsl4bripQg66mU7Iuc7KeAIO2DgOyehTonLCBtZXNzYWdlLnR5cGUpOw0KICB9DQp9Ow0KDQpzZWxmLm9ubWVzc2FnZWVycm9yID0gZnVuY3Rpb24gKGUpIHsNCiAgY29uc29sZS5lcnJvcign7JuM7LukIOuplOyLnOyngCDsspjrpqwg7Jik66WYOicsIGUpOw0KfTsNCg0KZnVuY3Rpb24gaGFuZGxlQ29ubmVjdFZpZGVvQ2FudmFzKG1lc3NhZ2UsIGV2ZW50KSB7DQogIHRyeSB7DQogICAgY29uc3QgcmVjZWl2ZWRFeHRyYWN0aW5nQ2FudmFzID0NCiAgICAgIChtZXNzYWdlICYmIG1lc3NhZ2UuZXh0cmFjdGluZ0NhbnZhcykgfHwgKGV2ZW50ICYmIGV2ZW50LmRhdGEgJiYgZXZlbnQuZGF0YS5leHRyYWN0aW5nQ2FudmFzKTsNCg0KICAgIGlmICghcmVjZWl2ZWRFeHRyYWN0aW5nQ2FudmFzKSB7DQogICAgICB0aHJvdyBuZXcgRXJyb3IoJ+yYpO2UhOyKpO2BrOumsCDsupTrsoTsiqTqsIAg66mU7Iuc7KeA66GcIOyghOuLrOuQmOyngCDslYrslZjsirXri4jri6QnKTsNCiAgICB9DQoNCiAgICBjb25zdCB3aWxsUmVhZEZyZXF1ZW50bHkgPSBtZXNzYWdlICYmIG1lc3NhZ2Uud2lsbFJlYWRGcmVxdWVudGx5Ow0KICAgIGV4dHJhY3RpbmdDYW52YXMgPSByZWNlaXZlZEV4dHJhY3RpbmdDYW52YXM7DQogICAgZXh0cmFjdGluZ0NvbnRleHQgPSBleHRyYWN0aW5nQ2FudmFzLmdldENvbnRleHQoJzJkJywgeyB3aWxsUmVhZEZyZXF1ZW50bHkgfSk7DQoNCiAgICBpZiAoIWV4dHJhY3RpbmdDb250ZXh0KSB7DQogICAgICB0aHJvdyBuZXcgRXJyb3IoJ+yYpO2UhOyKpO2BrOumsCDsupTrsoTsiqQgMkQg7Luo7YWN7Iqk7Yq4IOyDneyEsSDsi6TtjKgnKTsNCiAgICB9DQoNCiAgICBzZWxmLnBvc3RNZXNzYWdlKHsNCiAgICAgIHR5cGU6ICdjb25uZWN0VmlkZW9DYW52YXNSZXNwb25zZScsDQogICAgICBzdWNjZXNzOiB0cnVlLA0KICAgICAgaWQ6IG1lc3NhZ2UuaWQsDQogICAgfSk7DQogIH0gY2F0Y2ggKGVycm9yKSB7DQogICAgY29uc29sZS5lcnJvcign67mE65SU7JikIOy6lOuyhOyKpCDsl7DqsrAg7Jik66WYOicsIGVycm9yKTsNCiAgICBzZWxmLnBvc3RNZXNzYWdlKHsNCiAgICAgIHR5cGU6ICdjb25uZWN0VmlkZW9DYW52YXNSZXNwb25zZScsDQogICAgICBzdWNjZXNzOiBmYWxzZSwNCiAgICAgIGlkOiBtZXNzYWdlLmlkLA0KICAgIH0pOw0KICB9DQp9DQoNCmZ1bmN0aW9uIGhhbmRsZVJlbGVhc2VWaWRlb0NhbnZhcyhtZXNzYWdlKSB7DQogIGxldCBzdWNjZXNzID0gZmFsc2U7DQogIGlmIChleHRyYWN0aW5nQ2FudmFzKSB7DQogICAgc3VjY2VzcyA9IHRydWU7DQogIH0NCg0KICBzZWxmLnBvc3RNZXNzYWdlKHsNCiAgICB0eXBlOiAncmVsZWFzZVZpZGVvQ2FudmFzUmVzcG9uc2UnLA0KICAgIHN1Y2Nlc3M6IHN1Y2Nlc3MsDQogICAgaWQ6IG1lc3NhZ2UuaWQsDQogIH0pOw0KICBleHRyYWN0aW5nQ2FudmFzID0gbnVsbDsNCiAgZXh0cmFjdGluZ0NvbnRleHQgPSBudWxsOw0KfQ0KDQpmdW5jdGlvbiBoYW5kbGVQcm9jZXNzRnJhbWUobWVzc2FnZSwgZXZlbnQpIHsNCiAgY29uc3QgeyBpc1dhcm1VcCB9ID0gbWVzc2FnZTsNCiAgbGV0IGV4dHJhY3RlZFJHQiA9IG51bGw7DQogIGxldCBmcmFtZSA9IG51bGw7DQoNCiAgdHJ5IHsNCiAgICBmcmFtZSA9IGV4dHJhY3RGcmFtZUZyb21FdmVudChldmVudCk7DQogICAgaWYgKGZyYW1lKSB7DQogICAgICAvLyBSR0Ig7LaU7LacIOyaqSDsupTrsoTsiqTsl5DshJwg7ZSE66CI7J6E7JeQ7IScIFJHQiDstpTstpwNCiAgICAgIGV4dHJhY3RlZFJHQiA9IGV4dHJhY3RSR0JGcm9tRnJhbWUoZnJhbWUpOw0KICAgIH0NCiAgfSBjYXRjaCAoZXJyb3IpIHsNCiAgICBjb25zb2xlLmVycm9yKCftlITroIjsnoQg7LKY66asIOyYpOulmDonLCBlcnJvcik7DQogICAgZXh0cmFjdGVkUkdCID0gbnVsbDsNCiAgfSBmaW5hbGx5IHsNCiAgICBpZiAoZnJhbWUgJiYgdHlwZW9mIGZyYW1lLmNsb3NlID09PSAnZnVuY3Rpb24nKSB7DQogICAgICBmcmFtZS5jbG9zZSgpOw0KICAgIH0NCiAgICBzZW5kUHJvY2Vzc0ZyYW1lUmVzcG9uc2UoZXh0cmFjdGVkUkdCLCBpc1dhcm1VcCk7DQogIH0NCn0NCg0KZnVuY3Rpb24gc2VuZFByb2Nlc3NGcmFtZVJlc3BvbnNlKGV4dHJhY3RlZFJHQiwgaXNXYXJtVXApIHsNCiAgc2VsZi5wb3N0TWVzc2FnZSh7DQogICAgdHlwZTogJ3Byb2Nlc3NGcmFtZVJlc3BvbnNlJywNCiAgICBleHRyYWN0ZWRSR0I6IGV4dHJhY3RlZFJHQiwNCiAgICBpc1dhcm1VcDogaXNXYXJtVXAsDQogIH0pOw0KfQ0KDQpmdW5jdGlvbiBleHRyYWN0UkdCRnJvbUZyYW1lKGZyYW1lKSB7DQogIGlmICghZXh0cmFjdGluZ0NhbnZhcyB8fCAhZXh0cmFjdGluZ0NvbnRleHQpIHsNCiAgICB0aHJvdyBuZXcgRXJyb3IoJ+yYpO2UhOyKpO2BrOumsCDsupTrsoTsiqTqsIAg66mU7Iuc7KeA66GcIOyghOuLrOuQmOyngCDslYrslZjsirXri4jri6QnKTsNCiAgfQ0KICAvLyDsupTrsoTsiqQg7YGs6riwIOyhsOyglQ0KICBpZiAoZXh0cmFjdGluZ0NhbnZhcy53aWR0aCAhPT0gZnJhbWUud2lkdGggfHwgZXh0cmFjdGluZ0NhbnZhcy5oZWlnaHQgIT09IGZyYW1lLmhlaWdodCkgew0KICAgIGV4dHJhY3RpbmdDYW52YXMud2lkdGggPSBmcmFtZS53aWR0aDsNCiAgICBleHRyYWN0aW5nQ2FudmFzLmhlaWdodCA9IGZyYW1lLmhlaWdodDsNCiAgfSBlbHNlIHsNCiAgICBleHRyYWN0aW5nQ29udGV4dC5jbGVhclJlY3QoMCwgMCwgZXh0cmFjdGluZ0NhbnZhcy53aWR0aCwgZXh0cmFjdGluZ0NhbnZhcy5oZWlnaHQpOw0KICB9DQoNCiAgZXh0cmFjdGluZ0NvbnRleHQuZHJhd0ltYWdlKGZyYW1lLCAwLCAwLCBmcmFtZS53aWR0aCwgZnJhbWUuaGVpZ2h0KTsNCg0KICBjb25zdCBpbWFnZURhdGEgPSBleHRyYWN0aW5nQ29udGV4dC5nZXRJbWFnZURhdGEoMCwgMCwgZnJhbWUud2lkdGgsIGZyYW1lLmhlaWdodCk7DQogIHJldHVybiBnZXRSR0JGcm9tSW1hZ2VEYXRhKGltYWdlRGF0YSk7DQp9DQoNCmZ1bmN0aW9uIGV4dHJhY3RGcmFtZUZyb21FdmVudChldmVudCkgew0KICBjb25zdCBtZXNzYWdlID0gZXZlbnQuZGF0YTsNCiAgaWYgKG1lc3NhZ2UgJiYgbWVzc2FnZS5mcmFtZSBpbnN0YW5jZW9mIEltYWdlQml0bWFwKSB7DQogICAgcmV0dXJuIG1lc3NhZ2UuZnJhbWU7DQogIH0NCg0KICBpZiAoZXZlbnQuZGF0YSBpbnN0YW5jZW9mIEltYWdlQml0bWFwKSB7DQogICAgcmV0dXJuIGV2ZW50LmRhdGE7DQogIH0NCg0KICByZXR1cm4gbnVsbDsNCn0NCg0KZnVuY3Rpb24gZ2V0UkdCRnJvbUltYWdlRGF0YShpbWFnZURhdGEpIHsNCiAgY29uc3QgYnVmZmVyRGF0YSA9IG5ldyBVaW50MzJBcnJheShpbWFnZURhdGEuZGF0YS5idWZmZXIpOw0KICBjb25zdCBsZW4gPSBidWZmZXJEYXRhLmxlbmd0aDsNCiAgbGV0IHNpZ1IgPSAwOw0KICBsZXQgc2lnRyA9IDA7DQogIGxldCBzaWdCID0gMDsNCiAgZm9yIChsZXQgaSA9IDA7IGkgPCBsZW47IGkrKykgew0KICAgIGNvbnN0IHBpeGVsID0gYnVmZmVyRGF0YVtpXTsNCiAgICBzaWdSICs9IHBpeGVsICYgMHhmZjsNCiAgICBzaWdHICs9IChwaXhlbCA+PiA4KSAmIDB4ZmY7DQogICAgc2lnQiArPSAocGl4ZWwgPj4gMTYpICYgMHhmZjsNCiAgfQ0KICBjb25zdCBzYW1wbGVkUGl4ZWxzID0gbGVuOw0KICByZXR1cm4gew0KICAgIHNpZ1I6IHNpZ1IgLyBzYW1wbGVkUGl4ZWxzLA0KICAgIHNpZ0c6IHNpZ0cgLyBzYW1wbGVkUGl4ZWxzLA0KICAgIHNpZ0I6IHNpZ0IgLyBzYW1wbGVkUGl4ZWxzLA0KICAgIHRpbWVzdGFtcDogRGF0ZS5ub3coKSAqIDEwMDAsDQogIH07DQp9DQo=", import.meta.url).href,
messageHandlers: {
"processFrameResponse": (message) => {
this.handleProcessFrameResponse(message);
}
}
});
}
unSubscribeEventListeners() {
var _a, _b;
(_a = this.unsubFaceLost) == null ? void 0 : _a.call(this);
this.unsubFaceLost = void 0;
(_b = this.unsubCompleted) == null ? void 0 : _b.call(this);
this.unsubCompleted = void 0;
}
isDataBucketFull() {
return this._dataBucket.timestamps.length >= this.targetDataLength;
}
clearDataBucket() {
this._dataBucket = {
sigR: [],
sigG: [],
sigB: [],
timestamps: []
};
}
pushToDataBucket(rgbData) {
this._dataBucket.sigR.push(rgbData.sigR);
this._dataBucket.sigG.push(rgbData.sigG);
this._dataBucket.sigB.push(rgbData.sigB);
this._dataBucket.timestamps.push(rgbData.timestamp);
}
getReport() {
return {
rawData: this._dataBucket,
quality: {
positionError: 0,
yPositionError: 0,
dataPointCount: this._dataBucket.timestamps.length
}
};
}
calculateProgressPercentage() {
return Math.round(this._dataBucket.timestamps.length / this.targetDataLength * 100);
}
updateProgressPercentage(number) {
this._progressPercentage = number;
this.eventEmitter.emit("progress:updated", this._progressPercentage);
}
/**
* 워커 프레임 처리 응답
* - RGB 누적, 진행률 갱신, 완료 시 완료 이벤트 발행
*/
handleProcessFrameResponse(message) {
if (message.isWarmUp) {
this.eventEmitter.emit("frame:warmUp", void 0);
return;
}
if (message.extractedRGB !== null) {
this.pushToDataBucket(message.extractedRGB);
this.updateProgressPercentage(this.calculateProgressPercentage());
if (this.isDataBucketFull()) {
this.updateProgressPercentage(100);
logger.debug("Data collection completed", {
dataPoints: this._dataBucket.timestamps.length,
target: this.targetDataLength
});
this.eventEmitter.emit("system:completed", this.getReport());
}
}
}
}
const VIDEO_READY_STATE = 3;
class WebCamManager extends BaseManager {
constructor(config, eventEmitter, videoElement) {
super(config, eventEmitter);
__publicField(this, "webCamStream", null);
__publicField(this, "videoElement");
__publicField(this, "_width");
__publicField(this, "_height");
__publicField(this, "_frame", null);
this.videoElement = videoElement;
this._width = config.width;
this._height = config.height;
this.setupEventListeners();
logger.debug("WebCamManager initialized", {
size: `${config.width}x${config.height}`
});
}
get frame() {
return this._frame;
}
get width() {
return this._width;
}
get height() {
return this._height;
}
/**
* 브라우저 미디어 스트림을 초기화하고 video 요소에 연결
*/
initialize() {
return new Promise(async (resolve, reject) => {
try {
this.webCamStream = await navigator.mediaDevices.getUserMedia({
video: {
width: this.width,
height: this.height
}
});
this.handleWebcamMalfunction();
this.videoElement.srcObject = this.webCamStream;
this.videoElement.oncanplay = () => {
this.startWebcam();
logger.debug("Webcam stream connected", {
size: `${this.width}x${this.height}`
});
resolve();
};
this.videoElement.oninvalid;
this.videoElement.onerror = (err) => {
logger.error("Video stream load failed", err);
this.eventEmitter.emit("webcam:videoError", void 0);
reject(new Error(`Video stream load failed: ${err}`));
};
} catch (err) {
logger.error("Webcam initialization failed", err);
reject(err);
}
});
}
async reset() {
this.stopWebCam();
await this.initialize();
}
startWebcam() {
try {
this.videoElement.play();
} catch (err) {
logger.error("Webcam start failed", err);
}
}
stopWebCam() {
var _a;
(_a = this.webCamStream) == null ? void 0 : _a.getTracks().forEach((track) => track.stop());
this.webCamStream = null;
logger.debug("Webcam stream stopped");
}
validateVideoReadyState() {
if (this.videoElement.readyState < VIDEO_READY_STATE) {
return false;
}
return true;
}
/**
* 웹캠 프레임 캡처
* - 얼굴인식이 필요한 경우 전체 프레임 캡처
* - 얼굴인식이 필요하지 않은 경우 ROI 범위 내에서만 캡처
* @param roi ROI 범위
*/
async captureFrame(roi) {
try {
this._frame = await createImageBitmap(this.videoElement, roi.sx, roi.sy, roi.sw, roi.sh);
} catch (err) {
logger.error("Frame capture failed", err);
}
}
setupEventListeners() {
}
handleWebcamMalfunction() {
var _a, _b, _c, _d;
(_b = (_a = this.webCamStream) == null ? void 0 : _a.getTracks()[0]) == null ? void 0 : _b.addEventListener("mute", () => {
console.log("webcam muted");
this.checkWebcamMutedAfterTimeout(1e3);
});
(_d = (_c = this.webCamStream) == null ? void 0 : _c.getTracks()[0]) == null ? void 0 : _d.addEventListener("ended", () => {
console.log("webcam ended");
this.eventEmitter.emit("webcam:streamEnded", void 0);
});
}
checkWebcamMutedAfterTimeout(timeout) {
setTimeout(() => {
var _a;
const tracks = (_a = this.webCamStream) == null ? void 0 : _a.getTracks();
if (tracks) {
const track = tracks[0];
if (track.muted) {
this.eventEmitter.emit("webcam:streamMuted", void 0);
}
}
}, timeout);
}
stop() {
if (this.videoElement) {
this.videoElement.pause();
}
this._frame = null;
}
dispose() {
this.stop();
this.stopWebCam();
if (this.videoElement) {
this.videoElement.srcObject = null;
}
}
clearFrame() {
this._frame = null;
}
}
const calculateInterval = (fps) => {
return 1e3 / fps;
};
function clamp(v, lo, hi) {
if (Number.isNaN(v)) return v;
return Math.max(lo, Math.min(v, hi));
}
class MeasurementLoop {
constructor(config, eventEmitter) {
__publicField(this, "eventEmitter");
__publicField(this, "measurementConfig");
__publicField(this, "_shouldExcuteRGBExtraction");
__publicField(this, "_countdown");
__publicField(this, "isRunning", false);
__publicField(this, "processingTimer");
__publicField(this, "unsubscribeFunctions", []);
__publicField(this, "dataProcessingManager");
__publicField(this, "faceDetectionManager");
__publicField(this, "webCamManager");
__publicField(this, "loopCount");
__publicField(this, "exptected");
__publicField(this, "minSpacing");
this.eventEmitter = eventEmitter;
this.measurementConfig = config.measurementConfig;
this.dataProcessingManager = new DataProcessingManager(
config.dataProcessingManagerConfig,
eventEmitter
);
this.faceDetectionManager = new FaceDetectionManager(
config.faceDetectionManagerConfig,
eventEmitter
);
this.webCamManager = new WebCamManager(
config.webCamManagerConfig,
eventEmitter,
config.measurementConfig.videoElement
);
this._shouldExcuteRGBExtraction = true;
logger.debug("MeasurementLoop initialized", {
countdown: config.measurementConfig.countdown,
processingFps: config.measurementConfig.processingFps,
targetDataLength: config.dataProcessingManagerConfig.targetDataLength
});
this.resetProcess();
this.setUpEventListeners();
}
get shouldExcuteRGBExtraction() {
return this._shouldExcuteRGBExtraction;
}
get countdown() {
return this._countdown;
}
set countdown(countdown) {
this._countdown = countdown;
}
get progressPercentage() {
return this.dataProcessingManager.progressPercentage;
}
get facePosition() {
return this.faceDetectionManager.facePosition;
}
setUpEventListeners() {
this.unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
this.unsubscribeFunctions = [];
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:initialize", async () => {
await this.initialize();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:reset", () => {
this.resetSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:start", () => {
this.startSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:completed", () => {
this.stopSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:stop", () => {
this.stopSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:terminate", async () => {
await this.terminateSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:toggleMeasure", (shouldExcuteRGBExtraction) => {
this.enableRGBExtraction(shouldExcuteRGBExtraction);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("webcam:streamMuted", () => {
this.eventEmitter.emit("system:failed", new Error("Webcam stream muted"));
this.stopSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("webcam:videoError", () => {
this.eventEmitter.emit("system:failed", new Error("Webcam video error"));
this.stopSystem();
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("webcam:streamEnded", () => {
this.eventEmitter.emit("system:failed", new Error("Webcam stream ended"));
this.stopSystem();
})
);
}
/**
* 웹캠/데이터/검출 매니저를 병렬 초기화하고 상태를 갱신
*/
async initialize() {
this.resetProcess();
try {
await this.webCamManager.initialize(), await Promise.all([
this.dataProcessingManager.initialize(
this.measurementConfig.extractingCanvas,
this.measurementConfig.willReadFrequently
),
this.faceDetectionManager.initialize(this.measurementConfig.videoElement)
]);
await this.processWarmUpWhileInitializing();
this.eventEmitter.emit("system:ready", void 0);
} catch (err) {
logger.error("MeasurementLoop initialization failed", err);
this.eventEmitter.emit("system:failed", err);
}
}
/**
* 각 매니저를 재초기화하고 READY 상태로 복귀
*/
resetSystem() {
try {
this.dataProcessingManager.reset();
this.faceDetectionManager.reset();
this.resetProcess();
this.eventEmitter.emit("system:ready", void 0);
} catch (err) {
logger.error("MeasurementLoop system reset failed", err);
this.eventEmitter.emit("system:failed", err);
}
}
/**
* 모든 리소스 해제 및 처리 타이머 정리
*/
async terminateSystem() {
this.isRunning = false;
this.clearProcessingTimer();
this.webCamManager.dispose();
this.dataProcessingManager.dispose();
this.faceDetectionManager.dispose();
this.dispose();
}
/**
* MeasurementLoop의 이벤트 구독 해제
*/
dispose() {
this.unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
this.unsubscribeFunctions = [];
}
/**
* 루프 중지 및 각 매니저 정지, 상태를 CANCELED로 변경
*/
stopSystem() {
this.isRunning = false;
this.clearProcessingTimer();
this.webCamManager.clearFrame();
this.dataProcessingManager.stop();
this.faceDetectionManager.stop();
}
/**
* 카운트다운 후 프레임 처리 루프 시작
*/
async startSystem() {
if (this.isRunning) return;
await Promise.all([this.processCountdown(), this.processWarmUpWhileCountdown()]);
logger.debug("Measurement loop started");
this.runLoop();
}
/**
* 다음 프레임 처리까지의 간격을 계산해 루프를 구동
* - 비디오 준비상태 검증, RUNNING 상태 알림, 간격 계산
*/
runLoop() {
if (!this.webCamManager.validateVideoReadyState()) {
this.eventEmitter.emit("system:failed", new Error("Video is not ready"));
return;
}
this.isRunning = true;
const frameInterval = calculateInterval(this.measurementConfig.processingFps);
this.exptected = performance.now() + frameInterval;
this.minSpacing = clamp(frameInterval * 0.15, 6, 12);
this.processLoop(frameInterval);
}
/**
* 지연 보정 로직으로 다음 실행 시점을 계산하며 반복 실행
* - shouldContinueLoop로 종료 조건을 선확인
*/
processLoop(idleInterval) {
if (!this.shouldContinueLoop()) {
this.handleLoopTermination();
return;
}
const actualInterval = this.getCalcualteInterval();
this.processingTimer = setTimeout(() => {
if (!this.shouldContinueLoop()) {
this.handleLoopTermination();
return;
}
this.processFrame();
this.exptected += idleInterval;
if (this.shouldContinueLoop()) {
this.processLoop(idleInterval);
} else {
this.handleLoopTermination();
}
}, actualInterval);
}
/**
* 코어 사이클 : RGB 추출 및 얼굴인식 루프의 사이클
*
* case
* - shouldExtractRGB: true && shouldValidateFacePosition: true : 비디오 프레임 전체 캡처
* - shouldExtractRGB: true && shouldValidateFacePosition: false : ROI 범위 내에서만 캡처
* - shouldExtractRGB: false && shouldValidateFacePosition: true : 비디오 프레임 전체 캡처
* - shouldExtractRGB: false && shouldValidateFacePosition: false : 아무것도 하지 않음
*/
async processFrame() {
this.loopCount++;
const shouldValidateFacePosition = this.shouldValidateFacePosition();
const shouldExtractRGB = this.shouldExtractRGB();
this.emitProcessingState(shouldExtractRGB);
if (shouldValidateFacePosition) {
this.faceDetectionManager.validateFacePosition(this.measurementConfig.videoElement);
}
if (!shouldExtractRGB && !shouldValidateFacePosition) return;
if (shouldExtractRGB) {
await this.extractAndProcessRGB();
}
}
/**
* 얼굴 위치 검증 상태에 따른 이벤트 발생
*/
emitProcessingState(shouldExtractRGB) {
if (shouldExtractRGB) {
this.eventEmitter.emit("system:measuring", void 0);
}
}
/**
* RGB 데이터 추출 및 처리
*/
async extractAndProcessRGB() {
if (this.faceDetectionManager.ROI === null) return;
await this.webCamManager.captureFrame(this.faceDetectionManager.ROI);
this.dataProcessingManager.processFrame(this.webCamManager.frame);
}
/**
* 워밍업 사이클
*/
async warmUp() {
this.faceDetectionManager.validateFacePosition(this.measurementConfig.videoElement, true);
if (this.webCamManager.frame !== null) {
this.dataProcessingManager.processFrame(this.webCamManager.frame, true);
}
}
shouldContinueLoop() {
if (!this.isRunning) {
return false;
}
if (!this.webCamManager.validateVideoReadyState()) {
this.eventEmitter.emit("system:failed", new Error("Video connection lost"));
return false;
}
if (this.dataProcessingManager.isDataBucketFull()) {
return false;
}
return true;
}
handleLoopTermination() {
this.isRunning = false;
this.clearProcessingTimer();
}
/**
* 초 단위 카운트다운을 진행하며 매 초마다 이벤트 발행
*/
async processCountdown() {
this.countdown = this.measurementConfig.countdown;
while (this.countdown > 0) {
this.countdown--;
this.eventEmitter.emit("countdown:tick", this.countdown);
await new Promise((resolve) => setTimeout(resolve, 1e3));
}
}
/**
* 카운트다운 중 워밍업 처리
*/
async processWarmUpWhileCountdown() {
while (this.countdown > 0) {
await this.warmUp();
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
/**
* 초기화 중 워밍업 처리
*/
async processWarmUpWhileInitializing() {
for (let i2 = 0; i2 < 5; i2++) {
await this.warmUp();
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
clearProcessingTimer() {
if (this.processingTimer) {
clearTimeout(this.processingTimer);
this.processingTimer = void 0;
}
}
resetProcess() {
this.countdown = 0;
this.loopCount = 0;
this.exptected = 0;
this._shouldExcuteRGBExtraction = true;
this.clearProcessingTimer();
}
/**
* 지연 보정: 예상 시점 대비 뒤처짐 여부에 따라 최소 지연으로 보정
*/
getCalcualteInterval() {
const now = performance.now();
const baseDelay = this.exptected - now;
const behind = baseDelay <= 0;
const delay = behind ? this.minSpacing : Math.max(baseDelay, this.minSpacing);
return Math.max(delay, 0);
}
shouldValidateFacePosition() {
return this.loopCount % (this.measurementConfig.processingFps / this.measurementConfig.validationFps) === 0;
}
shouldExtractRGB() {
return this.faceDetectionManager.isDetected && this.shouldExcuteRGBExtraction;
}
enableRGBExtraction(shouldExcuteRGBExtraction) {
try {
if (this._shouldExcuteRGBExtraction === shouldExcuteRGBExtraction) {
return;
}
this._shouldExcuteRGBExtraction = shouldExcuteRGBExtraction;
if (this._shouldExcuteRGBExtraction) {
this.dataProcessingManager.reset();
this.eventEmitter.emit("system:measuring", void 0);
} else {
this.eventEmitter.emit("system:running", void 0);
}
} catch (err) {
logger.error("RGB extraction toggle failed", err);
this.eventEmitter.emit("system:failed", err);
}
}
}
class StateMachine {
constructor(eventEmitter) {
__publicField(this, "state");
__publicField(this, "eventEmitter");
__publicField(this, "unsubscribeFunctions", []);
__publicField(this, "validTransitions", {
[i.INITIALIZING]: [
i.READY,
i.FAILED,
i.INITIALIZING
],
[i.READY]: [
i.RUNNING,
i.MEASURING,
i.INITIALIZING,
i.FAILED,
i.READY
],
[i.RUNNING]: [
i.MEASURING,
i.READY,
i.FAILED,
i.RUNNING
],
[i.MEASURING]: [
i.RUNNING,
i.COMPLETED,
i.READY,
i.FAILED,
i.MEASURING
],
[i.COMPLETED]: [
i.INITIALIZING,
i.FAILED,
i.COMPLETED
],
[i.FAILED]: [i.INITIALIZING, i.FAILED]
});
this.state = i.INITIALIZING;
this.eventEmitter = eventEmitter;
this.setupEventListeners();
logger.debug("StateMachine initialized");
}
getState() {
return this.state;
}
setState(newState) {
const previousState = this.state;
if (!this.isValidTransition(previousState, newState)) {
logger.warn("Invalid state transition", { from: previousState, to: newState });
return;
}
if (previousState !== newState) {
this.state = newState;
logger.debug("State changed", { from: previousState, to: newState });
}
this.eventEmitter.emit("state:changed", newState);
}
/**
* 시스템 이벤트를 상태 변경으로 매핑
* - 각 이벤트 수신 시 대응되는 상태로 전이
*/
setupEventListeners() {
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:initialize", () => {
this.setState(i.INITIALIZING);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:ready", () => {
this.setState(i.READY);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:start", () => {
this.setState(i.RUNNING);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:running", () => {
this.setState(i.RUNNING);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:measuring", () => {
this.setState(i.MEASURING);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:completed", () => {
this.setState(i.COMPLETED);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:stop", () => {
this.setState(i.READY);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:failed", () => {
this.setState(i.FAILED);
})
);
this.unsubscribeFunctions.push(
this.eventEmitter.subscribe("system:reset", () => {
this.setState(i.INITIALIZING);
})
);
}
dispose() {
this.unsubscribeFunctions.forEach((unsubscribe) => unsubscribe());
this.unsubscribeFunctions = [];
}
isValidTransition(from, to) {
var _a;
return ((_a = this.validTransitions[from]) == null ? void 0 : _a.includes(to)) ?? false;
}
}
class EventEmitter {
constructor() {
__publicField(this, "events", {});
}
/**
* 동기적으로 리스너들을 호출하여 이벤트를 전달
* @param eventType 이벤트 이름
* @param data 이벤트 페이로드
*/
emit(eventType, data) {
if (this.events[eventType]) {
this.events[eventType].forEach((callback) => callback(data));
}
}
/**
* 이벤트를 구독하고 해제 함수 반환
* @param eventType 이벤트 이름
* @param callback 수신 콜백
* @returns 구독 해제 함수
*/
subscribe(eventType, callback) {
if (!this.events[eventType]) {
this.events[eventType] = [];
}
this.events[eventType].push(callback);
return () => {
if (this.events[eventType]) {
this.events[eventType] = this.events[eventType].filter((cb) => cb !== callback);
}
};
}
/**
* 특정 콜백을 이벤트에서 해제
* @param eventType 이벤트 이름
* @param callback 해제할 콜백
*/
unsubscribe(eventType, callback) {
if (this.events[eventType]) {
this.events[eventType] = this.events[eventType].filter((cb) => cb !== callback);
}
}
/**
* 특정 이벤트 또는 전체 이벤트에 등록된 리스너를 제거
* @param eventType 생략 시 전체 제거
*/
removeAllListeners(eventType) {
if (eventType) {
delete this.events[eventType];
} else {
this.events = {};
}
}
}
const defaultConfig = (videoElement, extractingCanvasElement) => {
return {
faceDetectionManagerConfig: {
minDetectionConfidence: 0.5,
delegate: "CPU"
},
webCamManagerConfig: {
width: 640,
height: 480
},
measurementConfig: {
countdown: 3,
processingFps: 30,
validationFps: 1,
retryCount: 3,
extractingCanvas: extractingCanvasElement,
videoElement,
willReadFrequently: false
},
dataProcessingManagerConfig: {
targetDataLength: 450
},
debug: true
};
};
class FaceDetector {
constructor(configOrVideoElement, extractingCanvasElement) {
__publicField(this, "stateMachine");
__publicField(this, "measurementLoop");
__publicField(this, "eventEmitter");
__publicField(this, "activeUnsubscribeCompleted");
__publicField(this, "activeUnsubscribeFailed");
let config;
if (configOrVideoElement instanceof HTMLVideoElement) {
config = defaultConfig(configOrVideoElement, extractingCanvasElement);
} else {
config = configOrVideoElement;
}
logger.setDebugMode(config.debug);
logger.debug("FaceDetector initialization started", { debug: config.debug });
this.eventEmitter = new EventEmitter();
this.stateMachine = new StateMachine(this.eventEmitter);
this.measurementLoop = new MeasurementLoop(config, this.eventEmitter);
this.eventEmitter.emit("system:initialize", void 0);
}
getState() {
return this.stateMachine.getState();
}
getProgress() {
return this.measurementLoop.progressPercentage;
}
getCountdown() {
return this.measurementLoop.countdown;
}
getFacePosition() {
return this.measurementLoop.facePosition;
}
subscribeState(callback) {
this.eventEmitter.subscribe("state:changed", callback);
return () => this.eventEmitter.unsubscribe("state:changed", callback);
}
subscribeProgress(callback) {
this.eventEmitter.subscribe("progress:updated", callback);
return () => this.eventEmitter.unsubscribe("progress:updated", callback);
}
subscribeCountdown(callback) {
this.eventEmitter.subscribe("countdown:tick", callback);
return () => this.eventEmitter.unsubscribe("countdown:tick", callback);
}
subscribeFacePosition(callback) {
this.eventEmitter.subscribe("face:position", callback);
return () => this.eventEmitter.unsubscribe("face:position", callback);
}
/**
* 측정을 시작하고 완료/실패 이벤트를 대기하여 결과를 반환
* - 완료/실패 구독을 등록하고, 먼저 발생한 이벤트로 Promise를 해결
*/
async run() {
if (this.getState() !== i.READY) {
throw new Error(`Cannot start detection. Current state: ${this.getState()}`);
}
this.cleanupActiveSubscriptions();
const start = performance.now();
this.eventEmitter.emit("system:start", void 0);
return new Promise((resolve, reject) => {
this.activeUnsubscribeCompleted = this.eventEmitter.subscribe("system:completed", (result) => {
this.cleanupActiveSubscriptions();
resolve(result);
logger.debug("Measurement completed", {
duration: `${((performance.now() - start) / 1e3).toFixed(2)}s`,
state: this.getState()
});
});
this.activeUnsubscribeFailed = this.eventEmitter.subscribe("system:failed", (error) => {
this.cleanupActiveSubscriptions();
reject(error);
});
});
}
cleanupActiveSubscriptions() {
var _a, _b;
(_a = this.activeUnsubscribeCompleted) == null ? void 0 : _a.call(this);
(_b = this.activeUnsubscribeFailed) == null ? void 0 : _b.call(this);
this.activeUnsubscribeCompleted = void 0;
this.activeUnsubscribeFailed = void 0;
}
stop() {
this.eventEmitter.emit("system:stop", void 0);
}
terminate() {
this.eventEmitter.emit("system:terminate", void 0);
}
reset() {
this.eventEmitter.emit("system:reset", void 0);
}
enableRGBExtraction(shouldExcuteRGBExtraction) {
this.eventEmitter.emit("system:toggleMeasure", shouldExcuteRGBExtraction);
}
}
export {
EventEmitter,
FaceDetector,
MeasurementLoop,
i as ProcessState,
StateMachine,
defaultConfig
};
//# sourceMappingURL=index.es.js.map