@incodetech/core 2.0.0-alpha.9 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,995 +0,0 @@
1
- import { n as requestPermission, r as getDeviceClass, t as checkPermission } from "./permissionServices-I6vX6DBy.esm.js";
2
- import { c as FaceDetectionProvider, d as StreamCanvasProcessingSession, f as StreamCanvasCapture, g as createManager, h as stopCameraStream, m as requestCameraAccess, n as api, s as WasmUtilProvider, t as endpoints, u as OpenViduRecordingProvider } from "./endpoints-BSTFaHYo.esm.js";
3
- import { a as createActor, i as fromPromise, n as assign, r as fromCallback, t as setup } from "./xstate.esm-B_rda9yU.esm.js";
4
- import { t as addEvent } from "./addEvent-W0ORK0jT.esm.js";
5
-
6
- //#region src/modules/selfie/types.ts
7
- const FACE_ERROR_CODES = {
8
- FACE_OCCLUDED: "FACE_OCCLUDED",
9
- LIVENESS: "LIVENESS_ERROR",
10
- BRIGHTNESS: "BRIGHTNESS_ERROR",
11
- LENSES: "LENSES_ERROR",
12
- MASK: "MASK_ERROR",
13
- CLOSED_EYES: "CLOSED_EYES_ERROR",
14
- HEAD_COVER: "HEAD_COVER_ERROR",
15
- SERVER: "SERVER_ERROR",
16
- FACE_NOT_FOUND: "FACE_NOT_FOUND",
17
- MULTIPLE_FACES: "MULTIPLE_FACES",
18
- TOO_BLURRY: "TOO_BLURRY_ERROR",
19
- TOO_DARK: "TOO_DARK_ERROR",
20
- USER_IS_NOT_RECOGNIZED: "USER_IS_NOT_RECOGNIZED",
21
- SPOOF_ATTEMPT_DETECTED: "SPOOF_ATTEMPT_DETECTED",
22
- FACE_TOO_DARK: "FACE_TOO_DARK",
23
- LENSES_DETECTED: "LENSES_DETECTED",
24
- FACE_MASK_DETECTED: "FACE_MASK_DETECTED",
25
- CLOSED_EYES_DETECTED: "CLOSED_EYES_DETECTED",
26
- HEAD_COVER_DETECTED: "HEAD_COVER_DETECTED",
27
- FACE_CROPPING_FAILED: "FACE_CROPPING_FAILED",
28
- FACE_TOO_SMALL: "FACE_TOO_SMALL",
29
- FACE_TOO_BLURRY: "FACE_TOO_BLURRY",
30
- BAD_PHOTO_QUALITY: "BAD_PHOTO_QUALITY",
31
- PROCESSING_ERROR: "PROCESSING_ERROR",
32
- BAD_REQUEST: "BAD_REQUEST",
33
- NONEXISTENT_CUSTOMER: "NONEXISTENT_CUSTOMER",
34
- HINT_NOT_PROVIDED: "HINT_NOT_PROVIDED",
35
- SELFIE_IMAGE_LOW_QUALITY: "SELFIE_IMAGE_LOW_QUALITY"
36
- };
37
-
38
- //#endregion
39
- //#region src/modules/selfie/selfieErrorUtils.ts
40
- const FACE_ERROR_CODE_VALUES = Object.values(FACE_ERROR_CODES);
41
- const isFaceErrorCode = (value) => {
42
- return FACE_ERROR_CODE_VALUES.includes(value);
43
- };
44
- const getFaceErrorCodeFromUnknown = (value) => {
45
- if (value instanceof Error && typeof value.message === "string") return isFaceErrorCode(value.message) ? value.message : void 0;
46
- if (typeof value === "string") return isFaceErrorCode(value) ? value : void 0;
47
- };
48
- const validateUploadResponse = ({ config, response }) => {
49
- if (!response) return FACE_ERROR_CODES.SERVER;
50
- if (response.confidence !== 0) return FACE_ERROR_CODES.LIVENESS;
51
- if (config.validateBrightness && !response.isBright) return FACE_ERROR_CODES.BRIGHTNESS;
52
- if (config.validateLenses && response.hasLenses) return FACE_ERROR_CODES.LENSES;
53
- if (config.validateFaceMask && response.hasFaceMask) return FACE_ERROR_CODES.MASK;
54
- if (config.validateClosedEyes && response.hasClosedEyes) return FACE_ERROR_CODES.CLOSED_EYES;
55
- if (config.validateHeadCover && response.hasHeadCover) return FACE_ERROR_CODES.HEAD_COVER;
56
- };
57
-
58
- //#endregion
59
- //#region src/internal/recordings/recordingsRepository.ts
60
- async function createRecordingSession(type) {
61
- return (await api.post(endpoints.recordingCreateSessionV2, { type })).data;
62
- }
63
- async function startRecording(params) {
64
- return (await api.post(endpoints.recordingStartV2, {
65
- videoRecordingId: params.videoRecordingId,
66
- frameRate: 30,
67
- outputMode: "COMPOSED",
68
- resolution: params.resolution,
69
- type: params.type,
70
- hasAudio: params.hasAudio ?? false
71
- })).data;
72
- }
73
- async function stopRecording$1(videoRecordingId) {
74
- return (await api.post(endpoints.recordingStopV2, { videoRecordingId })).data;
75
- }
76
-
77
- //#endregion
78
- //#region src/internal/recordings/streamingEvents.ts
79
- const streamingEvents = {
80
- strSessionDidConnect: "strSessionDidConnect",
81
- strSessionDidDisconnect: "strSessionDidDisconnect",
82
- strSessionDidFailWithError: "strSessionDidFailWithError",
83
- strStreamVideoCaptureStart: "strStreamVideoCaptureStart",
84
- strStreamVideoCaptureStop: "strStreamVideoCaptureStop",
85
- strStreamPublisherCreated: "strStreamPublisherCreated",
86
- strStreamPublisherDestroyed: "strStreamPublisherDestroyed",
87
- strStreamPublisherDidFailWithError: "strStreamPublisherDidFailWithError"
88
- };
89
-
90
- //#endregion
91
- //#region src/modules/selfie/selfieUploadService.ts
92
- /**
93
- * Uploads a selfie image to the backend.
94
- */
95
- async function uploadSelfie({ encryptedBase64Image, faceCoordinates, signal }) {
96
- try {
97
- const res = await api.post(endpoints.selfie, {
98
- base64Image: encryptedBase64Image,
99
- faceCoordinates: faceCoordinates ?? void 0,
100
- encrypted: true,
101
- clientInfo: { deviceClass: getDeviceClass() }
102
- }, {
103
- signal,
104
- query: { imageType: "selfie" }
105
- });
106
- if (!res.ok) throw new Error(`POST ${endpoints.selfie} failed: ${res.status} ${res.statusText}`);
107
- return res.data;
108
- } catch (error) {
109
- const errorCode = getFaceErrorCodeFromHttpError(error);
110
- if (errorCode) throw new Error(errorCode);
111
- throw new Error(FACE_ERROR_CODES.SERVER);
112
- }
113
- }
114
- const getFaceErrorCodeFromHttpError = (error) => {
115
- const err = error;
116
- if (err.ok !== false || typeof err.status !== "number") return;
117
- if (err.status !== 400) return FACE_ERROR_CODES.SERVER;
118
- if (typeof err.data?.status !== "number") return FACE_ERROR_CODES.BAD_REQUEST;
119
- return {
120
- 3004: FACE_ERROR_CODES.FACE_NOT_FOUND,
121
- 3005: FACE_ERROR_CODES.FACE_NOT_FOUND,
122
- 3006: FACE_ERROR_CODES.TOO_BLURRY,
123
- 3007: FACE_ERROR_CODES.TOO_DARK,
124
- 4010: FACE_ERROR_CODES.MULTIPLE_FACES,
125
- 4019: FACE_ERROR_CODES.FACE_NOT_FOUND,
126
- 4077: FACE_ERROR_CODES.BAD_PHOTO_QUALITY,
127
- 4078: FACE_ERROR_CODES.FACE_OCCLUDED
128
- }[err.data.status] ?? FACE_ERROR_CODES.BAD_REQUEST;
129
- };
130
-
131
- //#endregion
132
- //#region src/modules/selfie/selfieServices.ts
133
- const CAMERA_CONSTRAINTS = {
134
- video: {
135
- facingMode: "user",
136
- height: { ideal: 480 },
137
- width: { ideal: 640 }
138
- },
139
- audio: false
140
- };
141
- function stopStream(stream) {
142
- stopCameraStream(stream);
143
- }
144
- async function initializeCamera(config) {
145
- const provider = new FaceDetectionProvider();
146
- await provider.initialize({ autocaptureInterval: config.autoCaptureTimeout * 1e3 });
147
- provider.setChecksEnabled({
148
- lenses: config.validateLenses,
149
- mask: config.validateFaceMask,
150
- closedEyes: config.validateClosedEyes,
151
- headWear: config.validateHeadCover,
152
- occlusion: false
153
- });
154
- return {
155
- stream: await requestCameraAccess({
156
- video: CAMERA_CONSTRAINTS.video,
157
- audio: false
158
- }),
159
- provider
160
- };
161
- }
162
- /**
163
- * Encrypts the provided selfie image using WASM.
164
- */
165
- async function encryptSelfieImage({ canvas }) {
166
- const base64Image = canvas.getBase64Image();
167
- if (!base64Image) throw new Error("Canvas image is empty or null");
168
- return (await WasmUtilProvider.getInstance()).encryptImage(base64Image);
169
- }
170
- function startDetection(params) {
171
- let lastStatus;
172
- let session;
173
- const { provider } = params;
174
- const setStatus = (status) => {
175
- if (session?.isDisposed() === true) return;
176
- if (lastStatus === status) return;
177
- lastStatus = status;
178
- params.onUpdate(status);
179
- };
180
- const stopDetectionLoop = () => {
181
- session?.dispose();
182
- };
183
- const reset = () => {
184
- provider.reset();
185
- };
186
- const cleanup = () => {
187
- stopDetectionLoop();
188
- };
189
- (async () => {
190
- try {
191
- provider.setCallbacks({
192
- onFarAway: () => setStatus("tooFar"),
193
- onTooClose: () => setStatus("tooClose"),
194
- onTooManyFaces: () => setStatus("tooManyFaces"),
195
- onNoFace: () => setStatus("idle"),
196
- onCenterFace: () => setStatus("centerFace"),
197
- onGetReady: () => setStatus("getReady"),
198
- onGetReadyFinished: () => setStatus("getReadyFinished"),
199
- onDark: () => {
200
- if (params.config.validateBrightness) setStatus("dark");
201
- },
202
- onBlur: () => setStatus("blur"),
203
- onFaceAngle: () => setStatus("faceAngle"),
204
- onLenses: () => {
205
- if (params.config.validateLenses) setStatus("lenses");
206
- },
207
- onMask: () => {
208
- if (params.config.validateFaceMask) setStatus("faceMask");
209
- },
210
- onEyesClosed: () => {
211
- if (params.config.validateClosedEyes) setStatus("eyesClosed");
212
- },
213
- onHeadWear: () => {
214
- if (params.config.validateHeadCover) setStatus("headWear");
215
- },
216
- onFaceOccluded: () => {},
217
- onSwitchToManualCapture: () => {
218
- setStatus("manualCapture");
219
- stopDetectionLoop();
220
- },
221
- onCapture: (canvas, faceCoordinates) => {
222
- setStatus("success");
223
- params.onSuccess(canvas, faceCoordinates);
224
- cleanup();
225
- }
226
- });
227
- setStatus("detecting");
228
- session = new StreamCanvasProcessingSession({
229
- capturer: params.capturer,
230
- provider,
231
- onFrame: params.onFrame
232
- });
233
- } catch {
234
- setStatus("error");
235
- cleanup();
236
- }
237
- })();
238
- return {
239
- cleanup,
240
- reset
241
- };
242
- }
243
- function buildResolutionFromStream(stream) {
244
- const track = stream.getVideoTracks()[0];
245
- if (!track) return;
246
- const settings = track.getSettings();
247
- const width = settings.width;
248
- const height = settings.height;
249
- if (typeof width === "number" && typeof height === "number") return `${width}x${height}`;
250
- }
251
- async function startRecordingSession(params) {
252
- if (params.config.enableFaceRecording !== true) return;
253
- if (params.existing) return params.existing;
254
- const provider = params.config.recording?.capability ?? new OpenViduRecordingProvider();
255
- const clonedStream = params.clonedStream;
256
- const hasAudio = clonedStream.getAudioTracks().length > 0;
257
- const resolution = buildResolutionFromStream(clonedStream);
258
- const session = await createRecordingSession("selfie");
259
- const connection = await provider.connect({
260
- sessionToken: session.token,
261
- stream: clonedStream,
262
- events: {
263
- onSessionConnected: (sessionId) => {
264
- addEvent({
265
- code: streamingEvents.strSessionDidConnect,
266
- payload: {
267
- message: "Recording session connected",
268
- sessionId
269
- }
270
- });
271
- },
272
- onSessionDisconnected: (sessionId) => {
273
- addEvent({
274
- code: streamingEvents.strSessionDidDisconnect,
275
- payload: {
276
- message: "Recording session disconnected",
277
- sessionId
278
- }
279
- });
280
- },
281
- onSessionException: (event) => {
282
- addEvent({
283
- code: streamingEvents.strSessionDidFailWithError,
284
- payload: {
285
- message: "Recording session failed due to an error",
286
- eventName: event.name,
287
- type: "OpenViduException",
288
- errorMessage: event.message,
289
- sessionId: event.sessionId
290
- }
291
- });
292
- },
293
- onPublisherCreated: (p) => {
294
- addEvent({
295
- code: streamingEvents.strStreamPublisherCreated,
296
- payload: {
297
- message: "Recording publisher created",
298
- sessionId: p.sessionId,
299
- streamId: p.streamId
300
- }
301
- });
302
- },
303
- onPublisherError: (p) => {
304
- addEvent({
305
- code: streamingEvents.strStreamPublisherDidFailWithError,
306
- payload: {
307
- message: "Recording publisher failed due to an error",
308
- sessionId: p.sessionId,
309
- streamId: p.streamId,
310
- error: { message: p.message ?? "Unknown error" }
311
- }
312
- });
313
- }
314
- }
315
- });
316
- await startRecording({
317
- videoRecordingId: session.videoRecordingId,
318
- type: "selfie",
319
- resolution,
320
- hasAudio
321
- });
322
- addEvent({
323
- code: streamingEvents.strStreamVideoCaptureStart,
324
- payload: {
325
- message: "Recording capture started",
326
- resolution,
327
- videoRecordingId: session.videoRecordingId,
328
- sessionId: session.sessionId,
329
- streamId: connection.publisher.getStreamId()
330
- }
331
- });
332
- return {
333
- token: session.token,
334
- sessionId: session.sessionId,
335
- videoRecordingId: session.videoRecordingId,
336
- connection,
337
- resolution,
338
- hasAudio
339
- };
340
- }
341
- function stopRecording(session) {
342
- (async () => {
343
- try {
344
- addEvent({
345
- code: streamingEvents.strStreamVideoCaptureStop,
346
- payload: {
347
- message: "Recording capture stopped",
348
- videoRecordingId: session.videoRecordingId,
349
- sessionId: session.sessionId,
350
- streamId: session.connection.publisher.getStreamId()
351
- }
352
- });
353
- await stopRecording$1(session.videoRecordingId);
354
- addEvent({
355
- code: streamingEvents.strStreamPublisherDestroyed,
356
- payload: {
357
- message: "Recording publisher destroyed",
358
- sessionId: session.sessionId,
359
- streamId: session.connection.publisher.getStreamId()
360
- }
361
- });
362
- } finally {
363
- await session.connection.disconnect();
364
- addEvent({
365
- code: streamingEvents.strSessionDidDisconnect,
366
- payload: {
367
- message: "Recording session disconnected",
368
- sessionId: session.sessionId
369
- }
370
- });
371
- }
372
- })();
373
- }
374
-
375
- //#endregion
376
- //#region src/modules/selfie/selfieStateMachine.ts
377
- const _selfieMachine = setup({
378
- types: {
379
- context: {},
380
- events: {},
381
- input: {}
382
- },
383
- actors: {
384
- checkPermission: fromPromise(async () => {
385
- return checkPermission();
386
- }),
387
- requestPermission: fromPromise(async () => {
388
- return requestPermission();
389
- }),
390
- initializeCamera: fromPromise(async ({ input }) => {
391
- return initializeCamera(input);
392
- }),
393
- runDetection: fromCallback(({ input, sendBack }) => {
394
- if (!input.frameCapturer || !input.provider) {
395
- sendBack({
396
- type: "DETECTION_UPDATE",
397
- status: "error"
398
- });
399
- return () => {};
400
- }
401
- const { cleanup, reset } = startDetection({
402
- config: input.config,
403
- capturer: input.frameCapturer,
404
- onUpdate: (status) => sendBack({
405
- type: "DETECTION_UPDATE",
406
- status
407
- }),
408
- onFrame: (frame) => sendBack({
409
- type: "DETECTION_FRAME",
410
- frame
411
- }),
412
- onSuccess: (canvas, faceCoordinates) => sendBack({
413
- type: "DETECTION_SUCCESS",
414
- canvas,
415
- faceCoordinates
416
- }),
417
- provider: input.provider
418
- });
419
- sendBack({
420
- type: "DETECTION_RESET_READY",
421
- reset
422
- });
423
- return cleanup;
424
- }),
425
- uploadSelfie: fromPromise(async ({ input, signal }) => {
426
- return uploadSelfie({
427
- encryptedBase64Image: await encryptSelfieImage({ canvas: input.canvas }),
428
- faceCoordinates: input.faceCoordinates,
429
- signal
430
- });
431
- }),
432
- startRecording: fromPromise(async ({ input }) => {
433
- if (!input.clonedStream) return;
434
- return startRecordingSession({
435
- config: input.config,
436
- clonedStream: input.clonedStream,
437
- existing: input.existing
438
- });
439
- })
440
- },
441
- actions: {
442
- stopMediaStream: assign(({ context }) => {
443
- context.frameCapturer?.dispose();
444
- if (context.stream) stopStream(context.stream);
445
- context.provider?.dispose();
446
- return {
447
- stream: void 0,
448
- provider: void 0,
449
- frameCapturer: void 0
450
- };
451
- }),
452
- setStreamAndCapturer: assign({
453
- stream: ({ event }) => {
454
- if ("output" in event) return event.output.stream;
455
- },
456
- provider: ({ event }) => {
457
- if ("output" in event) return event.output.provider;
458
- },
459
- frameCapturer: ({ event }) => {
460
- if ("output" in event) return new StreamCanvasCapture(event.output.stream);
461
- }
462
- }),
463
- trackTutorialSelfie: () => {
464
- addEvent({
465
- code: "tutorialSelfie",
466
- payload: { tutorialSelfie: true }
467
- });
468
- },
469
- trackContinue: () => {
470
- addEvent({ code: "continue" });
471
- },
472
- resetContext: assign(({ context }) => ({
473
- stream: void 0,
474
- provider: void 0,
475
- frameCapturer: void 0,
476
- error: void 0,
477
- detectionStatus: "idle",
478
- debugFrame: void 0,
479
- capturedImage: void 0,
480
- faceCoordinates: void 0,
481
- uploadResponse: void 0,
482
- recordingSession: void 0,
483
- recordingStream: void 0,
484
- attemptsRemaining: context.config.captureAttempts,
485
- uploadError: void 0,
486
- permissionResult: void 0,
487
- resetDetection: void 0
488
- })),
489
- resetDetection: ({ context }) => {
490
- context.resetDetection?.();
491
- },
492
- captureImage: assign({ capturedImage: ({ context }) => {
493
- if (context.capturedImage) return context.capturedImage;
494
- return context.frameCapturer?.getLatestCanvas() ?? void 0;
495
- } }),
496
- captureLatestFrame: assign({ capturedImage: ({ context }) => {
497
- return context.frameCapturer?.getLatestCanvas() ?? void 0;
498
- } }),
499
- clearUploadFailure: assign({
500
- uploadError: () => void 0,
501
- detectionStatus: () => "idle",
502
- capturedImage: () => void 0
503
- }),
504
- decrementAttemptsRemaining: assign(({ context }) => ({ attemptsRemaining: context.attemptsRemaining - 1 })),
505
- setUploadErrorFromUploadValidation: assign({ uploadError: ({ context }) => validateUploadResponse({
506
- config: context.config,
507
- response: context.uploadResponse
508
- }) ?? FACE_ERROR_CODES.SERVER }),
509
- stopMediaRecording: ({ context }) => {
510
- if (context.recordingSession) stopRecording(context.recordingSession);
511
- },
512
- clearRecordingSession: assign({
513
- recordingSession: () => void 0,
514
- recordingStream: () => void 0
515
- }),
516
- createRecordingStream: assign({ recordingStream: ({ context }) => {
517
- if (!context.config.enableFaceRecording) return;
518
- if (context.recordingStream) return context.recordingStream;
519
- return context.stream?.clone();
520
- } }),
521
- stopRecordingStream: ({ context }) => {
522
- if (context.recordingStream) stopStream(context.recordingStream);
523
- }
524
- },
525
- guards: {
526
- hasShowTutorial: ({ context }) => context.config.showTutorial,
527
- isPermissionGranted: ({ event }) => {
528
- if ("output" in event) return event.output === "granted";
529
- return false;
530
- },
531
- isPermissionDeniedError: ({ event }) => {
532
- if ("error" in event) {
533
- const error = event.error;
534
- return error?.name === "NotAllowedError" || error?.name === "PermissionDeniedError";
535
- }
536
- return false;
537
- },
538
- hasStream: ({ context }) => context.stream !== void 0,
539
- hasAttemptsRemaining: ({ context }) => context.attemptsRemaining > 0,
540
- hasCapturedImage: ({ context }) => context.capturedImage !== void 0,
541
- hasUploadValidationError: ({ context }) => validateUploadResponse({
542
- config: context.config,
543
- response: context.uploadResponse
544
- }) !== void 0
545
- }
546
- }).createMachine({
547
- id: "selfie",
548
- initial: "idle",
549
- context: ({ input }) => ({
550
- config: input.config,
551
- stream: void 0,
552
- provider: void 0,
553
- frameCapturer: void 0,
554
- error: void 0,
555
- detectionStatus: "idle",
556
- debugFrame: void 0,
557
- capturedImage: void 0,
558
- faceCoordinates: void 0,
559
- uploadResponse: void 0,
560
- recordingSession: void 0,
561
- recordingStream: void 0,
562
- attemptsRemaining: input.config.captureAttempts,
563
- uploadError: void 0,
564
- permissionResult: void 0,
565
- resetDetection: void 0
566
- }),
567
- on: { QUIT: { target: "#selfie.closed" } },
568
- states: {
569
- idle: { on: { LOAD: [{
570
- target: "tutorial",
571
- guard: "hasShowTutorial"
572
- }, { target: "loading" }] } },
573
- loading: { invoke: {
574
- id: "checkPermissionLoading",
575
- src: "checkPermission",
576
- onDone: [{
577
- target: "capture",
578
- guard: "isPermissionGranted",
579
- actions: assign({ permissionResult: ({ event }) => event.output })
580
- }, {
581
- target: "permissions",
582
- actions: assign({ permissionResult: ({ event }) => event.output })
583
- }]
584
- } },
585
- tutorial: {
586
- initial: "checkingPermission",
587
- entry: "trackTutorialSelfie",
588
- states: {
589
- checkingPermission: { invoke: {
590
- id: "checkPermissionTutorial",
591
- src: "checkPermission",
592
- onDone: [{
593
- target: "initializingCamera",
594
- guard: "isPermissionGranted",
595
- actions: assign({ permissionResult: ({ event }) => event.output })
596
- }, {
597
- target: "ready",
598
- actions: assign({ permissionResult: ({ event }) => event.output })
599
- }]
600
- } },
601
- initializingCamera: {
602
- initial: "booting",
603
- invoke: {
604
- id: "tutorialInitCamera",
605
- src: "initializeCamera",
606
- input: ({ context }) => context.config,
607
- onDone: { actions: "setStreamAndCapturer" },
608
- onError: [{
609
- target: "ready",
610
- guard: "isPermissionDeniedError",
611
- actions: assign({ permissionResult: () => "denied" })
612
- }, {
613
- target: "ready",
614
- actions: assign({ error: ({ event }) => String(event.error) })
615
- }]
616
- },
617
- states: {
618
- booting: {
619
- always: [{
620
- target: "#tutorialCameraReady",
621
- guard: "hasStream"
622
- }],
623
- on: { NEXT_STEP: {
624
- target: "waitingForStream",
625
- actions: "trackContinue"
626
- } }
627
- },
628
- waitingForStream: { always: [{
629
- target: "#selfie.capture",
630
- guard: "hasStream"
631
- }] }
632
- }
633
- },
634
- cameraReady: {
635
- id: "tutorialCameraReady",
636
- on: { NEXT_STEP: {
637
- target: "#selfie.capture",
638
- actions: "trackContinue"
639
- } }
640
- },
641
- ready: { on: { NEXT_STEP: {
642
- target: "waitingForPermission",
643
- actions: "trackContinue"
644
- } } },
645
- waitingForPermission: { invoke: {
646
- id: "checkPermissionWaiting",
647
- src: "checkPermission",
648
- onDone: [{
649
- target: "#selfie.capture",
650
- guard: "isPermissionGranted",
651
- actions: assign({ permissionResult: ({ event }) => event.output })
652
- }, {
653
- target: "#selfie.permissions",
654
- actions: assign({ permissionResult: ({ event }) => event.output })
655
- }]
656
- } }
657
- }
658
- },
659
- permissions: {
660
- initial: "idle",
661
- states: {
662
- idle: {
663
- invoke: {
664
- id: "checkPermissionIdle",
665
- src: "checkPermission",
666
- onDone: [{
667
- target: "#selfie.capture",
668
- guard: "isPermissionGranted",
669
- actions: assign({ permissionResult: ({ event }) => event.output })
670
- }, {
671
- target: "denied",
672
- guard: ({ event }) => event.output === "denied",
673
- actions: assign({ permissionResult: ({ event }) => event.output })
674
- }]
675
- },
676
- on: {
677
- REQUEST_PERMISSION: "requesting",
678
- GO_TO_LEARN_MORE: "learnMore"
679
- }
680
- },
681
- learnMore: { on: {
682
- BACK: "idle",
683
- REQUEST_PERMISSION: "requesting"
684
- } },
685
- requesting: { invoke: {
686
- id: "requestPermission",
687
- src: "requestPermission",
688
- onDone: [
689
- {
690
- target: "#selfie.capture",
691
- guard: "isPermissionGranted",
692
- actions: assign({ permissionResult: ({ event }) => event.output })
693
- },
694
- {
695
- target: "denied",
696
- guard: ({ event }) => event.output === "denied",
697
- actions: assign({ permissionResult: ({ event }) => event.output })
698
- },
699
- {
700
- target: "idle",
701
- actions: assign({ permissionResult: ({ event }) => event.output })
702
- }
703
- ],
704
- onError: { target: "denied" }
705
- } },
706
- denied: { entry: assign({ permissionResult: () => "refresh" }) }
707
- }
708
- },
709
- capture: {
710
- initial: "checkingStream",
711
- exit: [
712
- "stopMediaRecording",
713
- "stopRecordingStream",
714
- "clearRecordingSession",
715
- "stopMediaStream"
716
- ],
717
- states: {
718
- checkingStream: { always: [{
719
- target: "detecting",
720
- guard: "hasStream"
721
- }, { target: "initializing" }] },
722
- initializing: { invoke: {
723
- id: "initializeCamera",
724
- src: "initializeCamera",
725
- input: ({ context }) => context.config,
726
- onDone: {
727
- target: "detecting",
728
- actions: "setStreamAndCapturer"
729
- },
730
- onError: [{
731
- target: "#selfie.permissions",
732
- guard: "isPermissionDeniedError",
733
- actions: assign({ permissionResult: () => "denied" })
734
- }, {
735
- target: "#selfie.error",
736
- actions: assign({ error: ({ event }) => String(event.error) })
737
- }]
738
- } },
739
- detecting: {
740
- entry: [assign({ detectionStatus: () => "detecting" }), "createRecordingStream"],
741
- invoke: [{
742
- id: "startRecording",
743
- src: "startRecording",
744
- input: ({ context }) => ({
745
- config: context.config,
746
- clonedStream: context.recordingStream,
747
- existing: context.recordingSession
748
- }),
749
- onDone: { actions: assign({ recordingSession: ({ context, event }) => {
750
- return event.output ?? context.recordingSession;
751
- } }) },
752
- onError: { actions: () => void 0 }
753
- }, {
754
- id: "runDetection",
755
- src: "runDetection",
756
- input: ({ context }) => ({
757
- frameCapturer: context.frameCapturer,
758
- provider: context.provider,
759
- config: context.config
760
- })
761
- }],
762
- on: {
763
- DETECTION_UPDATE: { actions: assign({ detectionStatus: ({ event }) => event.status }) },
764
- DETECTION_FRAME: { actions: assign({ debugFrame: ({ event }) => event.frame }) },
765
- DETECTION_RESET_READY: { actions: assign({ resetDetection: ({ event }) => event.reset }) },
766
- DETECTION_SUCCESS: {
767
- target: "capturing",
768
- actions: assign({
769
- capturedImage: ({ event }) => event.canvas,
770
- faceCoordinates: ({ event }) => event.faceCoordinates
771
- })
772
- },
773
- MANUAL_CAPTURE: { target: "capturingManual" }
774
- }
775
- },
776
- capturing: {
777
- entry: ["captureImage"],
778
- always: [{
779
- target: "uploading",
780
- guard: "hasCapturedImage"
781
- }, {
782
- target: "uploadError",
783
- actions: assign(({ context }) => ({
784
- uploadError: FACE_ERROR_CODES.SERVER,
785
- attemptsRemaining: context.attemptsRemaining - 1
786
- }))
787
- }]
788
- },
789
- capturingManual: {
790
- entry: ["captureLatestFrame"],
791
- always: [{
792
- target: "uploading",
793
- guard: "hasCapturedImage"
794
- }, {
795
- target: "uploadError",
796
- actions: assign(({ context }) => ({
797
- uploadError: FACE_ERROR_CODES.SERVER,
798
- attemptsRemaining: context.attemptsRemaining - 1
799
- }))
800
- }]
801
- },
802
- uploading: { invoke: {
803
- id: "uploadSelfie",
804
- src: "uploadSelfie",
805
- input: ({ context }) => {
806
- const canvas = context.capturedImage;
807
- if (!canvas) throw new Error(FACE_ERROR_CODES.SERVER);
808
- return {
809
- canvas,
810
- faceCoordinates: context.faceCoordinates
811
- };
812
- },
813
- onDone: {
814
- target: "validatingUpload",
815
- actions: assign({ uploadResponse: ({ event }) => event.output })
816
- },
817
- onError: {
818
- target: "uploadError",
819
- actions: assign(({ context, event }) => ({
820
- uploadError: getFaceErrorCodeFromUnknown(event.error) ?? FACE_ERROR_CODES.SERVER,
821
- attemptsRemaining: context.attemptsRemaining - 1
822
- }))
823
- }
824
- } },
825
- validatingUpload: { always: [{
826
- target: "uploadError",
827
- guard: "hasUploadValidationError",
828
- actions: ["setUploadErrorFromUploadValidation", "decrementAttemptsRemaining"]
829
- }, { target: "success" }] },
830
- uploadError: { on: { RETRY_CAPTURE: {
831
- target: "detecting",
832
- guard: "hasAttemptsRemaining",
833
- actions: ["resetDetection", "clearUploadFailure"]
834
- } } },
835
- success: {
836
- entry: "stopMediaRecording",
837
- after: { 3e3: { target: "#selfie.finished" } }
838
- }
839
- }
840
- },
841
- finished: {
842
- entry: "stopMediaStream",
843
- on: { RESET: {
844
- target: "idle",
845
- actions: "resetContext"
846
- } }
847
- },
848
- closed: {
849
- entry: "stopMediaStream",
850
- type: "final"
851
- },
852
- error: {
853
- entry: "stopMediaStream",
854
- on: { RESET: {
855
- target: "idle",
856
- actions: "resetContext"
857
- } }
858
- }
859
- }
860
- });
861
- /**
862
- * The selfie capture state machine.
863
- *
864
- * Note: Uses AnyStateMachine type for declaration file portability.
865
- * Type safety is ensured via the machine configuration.
866
- */
867
- const selfieMachine = _selfieMachine;
868
-
869
- //#endregion
870
- //#region src/modules/selfie/selfieActor.ts
871
- function createSelfieActor(options) {
872
- return createActor(selfieMachine, { input: { config: options.config } }).start();
873
- }
874
-
875
- //#endregion
876
- //#region src/modules/selfie/selfieManager.ts
877
- function getPermissionStatus(snapshot) {
878
- if (!snapshot.matches("permissions")) return;
879
- if (snapshot.matches({ permissions: "idle" })) return "idle";
880
- if (snapshot.matches({ permissions: "learnMore" })) return "learnMore";
881
- if (snapshot.matches({ permissions: "requesting" })) return "requesting";
882
- if (snapshot.matches({ permissions: "denied" })) return "denied";
883
- }
884
- function getCaptureStatus(snapshot) {
885
- if (snapshot.matches({ capture: "initializing" })) return "initializing";
886
- if (snapshot.matches({ capture: "detecting" })) return "detecting";
887
- if (snapshot.matches({ capture: "capturing" })) return "capturing";
888
- if (snapshot.matches({ capture: "capturingManual" })) return "capturing";
889
- if (snapshot.matches({ capture: "uploading" })) return "uploading";
890
- if (snapshot.matches({ capture: "uploadError" })) return "uploadError";
891
- if (snapshot.matches({ capture: "success" })) return "success";
892
- }
893
- function mapState(snapshot) {
894
- const { context } = snapshot;
895
- if (snapshot.matches("idle")) return { status: "idle" };
896
- if (snapshot.matches("loading")) return { status: "loading" };
897
- if (snapshot.matches("tutorial")) return { status: "tutorial" };
898
- if (snapshot.matches("closed")) return { status: "closed" };
899
- if (snapshot.matches("permissions")) {
900
- const permissionStatus = getPermissionStatus(snapshot);
901
- if (permissionStatus === void 0) return {
902
- status: "permissions",
903
- permissionStatus: "idle"
904
- };
905
- return {
906
- status: "permissions",
907
- permissionStatus
908
- };
909
- }
910
- if (snapshot.matches("capture")) return {
911
- status: "capture",
912
- captureStatus: getCaptureStatus(snapshot) ?? "initializing",
913
- stream: context.stream,
914
- detectionStatus: context.detectionStatus,
915
- debugFrame: context.debugFrame,
916
- attemptsRemaining: context.attemptsRemaining,
917
- uploadError: context.uploadError
918
- };
919
- if (snapshot.matches("finished")) return { status: "finished" };
920
- if (snapshot.matches("error")) return {
921
- status: "error",
922
- error: context.error ?? "Unknown error"
923
- };
924
- return { status: "idle" };
925
- }
926
- function createApi({ actor }) {
927
- return {
928
- load() {
929
- actor.send({ type: "LOAD" });
930
- },
931
- nextStep() {
932
- actor.send({ type: "NEXT_STEP" });
933
- },
934
- requestPermission() {
935
- actor.send({ type: "REQUEST_PERMISSION" });
936
- },
937
- goToLearnMore() {
938
- actor.send({ type: "GO_TO_LEARN_MORE" });
939
- },
940
- back() {
941
- actor.send({ type: "BACK" });
942
- },
943
- close() {
944
- actor.send({ type: "QUIT" });
945
- },
946
- reset() {
947
- actor.send({ type: "RESET" });
948
- },
949
- retryCapture() {
950
- actor.send({ type: "RETRY_CAPTURE" });
951
- },
952
- capture() {
953
- actor.send({ type: "MANUAL_CAPTURE" });
954
- }
955
- };
956
- }
957
- /**
958
- * Creates a selfie manager instance for handling selfie capture flow.
959
- *
960
- * The selfie manager provides:
961
- * - State management with statuses: `idle`, `loading`, `tutorial`, `permissions`, `capture`, `finished`, `closed`, `error`
962
- * - Permission handling with nested states: `idle`, `requesting`, `denied`, `learnMore`
963
- * - Capture handling with nested states: `initializing`, `startingRecorder`, `recordingActive`, `detecting`, `capturing`, `uploading`, `uploadError`, `success`
964
- * - Camera stream access when in `capture` state
965
- * - Detection status feedback during face detection
966
- * - Attempt tracking with `attemptsRemaining`
967
- *
968
- * @param options - Configuration for the selfie actor
969
- * @param options.config - The selfie module configuration from the flow
970
- * @returns A manager instance with state subscription, API methods, and lifecycle controls
971
- *
972
- * @example
973
- * ```ts
974
- * const selfieManager = createSelfieManager({ config: selfieConfig });
975
- *
976
- * selfieManager.subscribe((state) => {
977
- * if (state.status === 'capture') {
978
- * console.log('Camera ready:', state.stream);
979
- * console.log('Detection status:', state.detectionStatus);
980
- * }
981
- * });
982
- *
983
- * selfieManager.load();
984
- * ```
985
- */
986
- function createSelfieManager(options) {
987
- return createManager({
988
- actor: createSelfieActor(options),
989
- mapState,
990
- createApi
991
- });
992
- }
993
-
994
- //#endregion
995
- export { createSelfieManager, selfieMachine };