@ayasofyazilim/ui 0.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.
- package/__mocks__/canvas.ts +8 -0
- package/components.json +21 -0
- package/eslint.config.js +4 -0
- package/jest-environment.js +37 -0
- package/jest.config.ts +47 -0
- package/jest.setup.ts +69 -0
- package/package.json +124 -0
- package/postcss.config.mjs +6 -0
- package/src/aria/index.tsx +1 -0
- package/src/aria/number-field.tsx +41 -0
- package/src/components/.gitkeep +0 -0
- package/src/components/accordion.tsx +66 -0
- package/src/components/alert-dialog.tsx +157 -0
- package/src/components/alert.tsx +70 -0
- package/src/components/aspect-ratio.tsx +11 -0
- package/src/components/avatar.tsx +53 -0
- package/src/components/badge.tsx +67 -0
- package/src/components/breadcrumb.tsx +109 -0
- package/src/components/button-group.tsx +83 -0
- package/src/components/button.tsx +68 -0
- package/src/components/calendar.tsx +219 -0
- package/src/components/card.tsx +92 -0
- package/src/components/carousel.tsx +241 -0
- package/src/components/chart.tsx +363 -0
- package/src/components/checkbox.tsx +32 -0
- package/src/components/collapsible.tsx +33 -0
- package/src/components/command.tsx +184 -0
- package/src/components/context-menu.tsx +252 -0
- package/src/components/dialog.tsx +144 -0
- package/src/components/drawer.tsx +135 -0
- package/src/components/dropdown-menu.tsx +258 -0
- package/src/components/empty.tsx +100 -0
- package/src/components/field.tsx +248 -0
- package/src/components/form.tsx +169 -0
- package/src/components/hover-card.tsx +44 -0
- package/src/components/input-group.tsx +170 -0
- package/src/components/input-otp.tsx +77 -0
- package/src/components/input.tsx +21 -0
- package/src/components/item.tsx +193 -0
- package/src/components/kbd.tsx +28 -0
- package/src/components/label.tsx +24 -0
- package/src/components/menubar.tsx +276 -0
- package/src/components/navigation-menu.tsx +168 -0
- package/src/components/pagination.tsx +130 -0
- package/src/components/popover.tsx +88 -0
- package/src/components/progress.tsx +31 -0
- package/src/components/radio-group.tsx +45 -0
- package/src/components/resizable.tsx +56 -0
- package/src/components/scroll-area.tsx +58 -0
- package/src/components/select.tsx +189 -0
- package/src/components/separator.tsx +28 -0
- package/src/components/sheet.tsx +140 -0
- package/src/components/sidebar.tsx +862 -0
- package/src/components/skeleton.tsx +13 -0
- package/src/components/slider.tsx +63 -0
- package/src/components/sonner.tsx +40 -0
- package/src/components/spinner.tsx +16 -0
- package/src/components/stepper.tsx +291 -0
- package/src/components/switch.tsx +31 -0
- package/src/components/table.tsx +133 -0
- package/src/components/tabs.tsx +66 -0
- package/src/components/textarea.tsx +18 -0
- package/src/components/toggle-group.tsx +83 -0
- package/src/components/toggle.tsx +47 -0
- package/src/components/tooltip.tsx +66 -0
- package/src/custom/action-button.tsx +48 -0
- package/src/custom/async-select.tsx +287 -0
- package/src/custom/awesome-not-found.tsx +116 -0
- package/src/custom/charts/area-chart.tsx +147 -0
- package/src/custom/charts/bar-chart.tsx +233 -0
- package/src/custom/charts/chart-card.tsx +103 -0
- package/src/custom/charts/index.tsx +16 -0
- package/src/custom/charts/pie-chart.tsx +168 -0
- package/src/custom/charts/radar-chart.tsx +126 -0
- package/src/custom/checkbox-tree.tsx +100 -0
- package/src/custom/combobox.tsx +296 -0
- package/src/custom/confirm-dialog.tsx +102 -0
- package/src/custom/country-selector.tsx +204 -0
- package/src/custom/date-picker/calendar-rac.tsx +109 -0
- package/src/custom/date-picker/datefield-rac.tsx +84 -0
- package/src/custom/date-picker/index.tsx +273 -0
- package/src/custom/date-picker/types/index.ts +4 -0
- package/src/custom/date-picker/utils/index.ts +42 -0
- package/src/custom/date-picker-old.tsx +50 -0
- package/src/custom/date-tooltip.tsx +98 -0
- package/src/custom/document-scanner/consts.ts +5 -0
- package/src/custom/document-scanner/corner-adjustment/action-buttons.tsx +33 -0
- package/src/custom/document-scanner/corner-adjustment/corner-handle.tsx +43 -0
- package/src/custom/document-scanner/corner-adjustment/hooks/use-corner-drag.ts +85 -0
- package/src/custom/document-scanner/corner-adjustment/index.tsx +125 -0
- package/src/custom/document-scanner/corner-adjustment/types.ts +53 -0
- package/src/custom/document-scanner/corner-adjustment/utils/clip-path.ts +22 -0
- package/src/custom/document-scanner/corner-adjustment/zoom-magnifier.tsx +115 -0
- package/src/custom/document-scanner/hooks/use-document-capture.ts +81 -0
- package/src/custom/document-scanner/hooks/use-document-scanner.ts +80 -0
- package/src/custom/document-scanner/hooks/use-perspective-crop.ts +38 -0
- package/src/custom/document-scanner/index.tsx +255 -0
- package/src/custom/document-scanner/lib.ts +407 -0
- package/src/custom/document-scanner/types.ts +205 -0
- package/src/custom/document-scanner/utils/perspective-correction.ts +139 -0
- package/src/custom/document-viewer/controllers.tsx +98 -0
- package/src/custom/document-viewer/index.tsx +43 -0
- package/src/custom/document-viewer/renderers/image.tsx +37 -0
- package/src/custom/document-viewer/renderers/index.tsx +2 -0
- package/src/custom/document-viewer/renderers/pdf.tsx +105 -0
- package/src/custom/email-input/domains.json +159 -0
- package/src/custom/email-input/email.tsx +229 -0
- package/src/custom/email-input/index.tsx +4 -0
- package/src/custom/email-input/types.ts +104 -0
- package/src/custom/file-uploader.tsx +541 -0
- package/src/custom/filter-component/fields/async-select.tsx +33 -0
- package/src/custom/filter-component/fields/date.tsx +60 -0
- package/src/custom/filter-component/fields/multi-select.tsx +30 -0
- package/src/custom/filter-component/index.tsx +217 -0
- package/src/custom/image-canvas.tsx +260 -0
- package/src/custom/json-editor.tsx +22 -0
- package/src/custom/master-data-grid/components/dialogs/column-settings-dialog.tsx +100 -0
- package/src/custom/master-data-grid/components/dialogs/index.ts +1 -0
- package/src/custom/master-data-grid/components/filters/client-filter.tsx +368 -0
- package/src/custom/master-data-grid/components/filters/filter-input.tsx +256 -0
- package/src/custom/master-data-grid/components/filters/index.ts +3 -0
- package/src/custom/master-data-grid/components/filters/inline-column-filter.tsx +233 -0
- package/src/custom/master-data-grid/components/filters/multi-filter-dialog.tsx +90 -0
- package/src/custom/master-data-grid/components/filters/server-filter.tsx +255 -0
- package/src/custom/master-data-grid/components/master-data-grid.tsx +472 -0
- package/src/custom/master-data-grid/components/pagination/index.ts +1 -0
- package/src/custom/master-data-grid/components/pagination/pagination.tsx +178 -0
- package/src/custom/master-data-grid/components/table/cell-renderer.tsx +634 -0
- package/src/custom/master-data-grid/components/table/header-cell.tsx +162 -0
- package/src/custom/master-data-grid/components/table/index.ts +4 -0
- package/src/custom/master-data-grid/components/table/table-body-renderer.tsx +113 -0
- package/src/custom/master-data-grid/components/table/virtual-body.tsx +138 -0
- package/src/custom/master-data-grid/components/toolbar/index.ts +1 -0
- package/src/custom/master-data-grid/components/toolbar/toolbar.tsx +314 -0
- package/src/custom/master-data-grid/hooks/index.ts +3 -0
- package/src/custom/master-data-grid/hooks/use-columns.tsx +332 -0
- package/src/custom/master-data-grid/hooks/use-editing.ts +106 -0
- package/src/custom/master-data-grid/hooks/use-table-state-reducer.ts +157 -0
- package/src/custom/master-data-grid/hooks/use-table-state.ts +31 -0
- package/src/custom/master-data-grid/index.ts +16 -0
- package/src/custom/master-data-grid/types.ts +466 -0
- package/src/custom/master-data-grid/utils/column-generator.tsx +306 -0
- package/src/custom/master-data-grid/utils/export-utils.ts +67 -0
- package/src/custom/master-data-grid/utils/filter-fns.ts +290 -0
- package/src/custom/master-data-grid/utils/index.ts +8 -0
- package/src/custom/master-data-grid/utils/pinning-utils.ts +88 -0
- package/src/custom/master-data-grid/utils/translation-utils.ts +42 -0
- package/src/custom/multi-select.tsx +432 -0
- package/src/custom/password-input.tsx +194 -0
- package/src/custom/phone-input.tsx +172 -0
- package/src/custom/schema-form/custom/index.tsx +1 -0
- package/src/custom/schema-form/custom/label.tsx +53 -0
- package/src/custom/schema-form/fields/base-input-field.tsx +82 -0
- package/src/custom/schema-form/fields/field.tsx +67 -0
- package/src/custom/schema-form/fields/index.tsx +5 -0
- package/src/custom/schema-form/fields/object.tsx +12 -0
- package/src/custom/schema-form/fields/table-array/array-field-item.tsx +90 -0
- package/src/custom/schema-form/fields/table-array/array-field-template.tsx +115 -0
- package/src/custom/schema-form/index.tsx +259 -0
- package/src/custom/schema-form/templates/description.tsx +20 -0
- package/src/custom/schema-form/templates/index.tsx +2 -0
- package/src/custom/schema-form/templates/submit.tsx +32 -0
- package/src/custom/schema-form/types.ts +64 -0
- package/src/custom/schema-form/utils/index.ts +4 -0
- package/src/custom/schema-form/utils/schema-dependency.ts +655 -0
- package/src/custom/schema-form/utils/schemas.ts +289 -0
- package/src/custom/schema-form/utils/validation.ts +23 -0
- package/src/custom/schema-form/widgets/boolean.tsx +77 -0
- package/src/custom/schema-form/widgets/combobox.tsx +274 -0
- package/src/custom/schema-form/widgets/date.tsx +59 -0
- package/src/custom/schema-form/widgets/email.tsx +34 -0
- package/src/custom/schema-form/widgets/index.tsx +10 -0
- package/src/custom/schema-form/widgets/password.tsx +40 -0
- package/src/custom/schema-form/widgets/phone.tsx +40 -0
- package/src/custom/schema-form/widgets/select.tsx +105 -0
- package/src/custom/schema-form/widgets/selectable.tsx +25 -0
- package/src/custom/schema-form/widgets/string-array.tsx +296 -0
- package/src/custom/schema-form/widgets/url.tsx +56 -0
- package/src/custom/section-layout-v2.tsx +212 -0
- package/src/custom/select-tabs.tsx +109 -0
- package/src/custom/selectable.tsx +316 -0
- package/src/custom/stepper.tsx +236 -0
- package/src/custom/tab-layout.tsx +213 -0
- package/src/custom/tanstack-table/fields/index.tsx +12 -0
- package/src/custom/tanstack-table/fields/tanstack-table-action-dialogs.tsx +89 -0
- package/src/custom/tanstack-table/fields/tanstack-table-column-header.tsx +66 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-date.tsx +180 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-faceted.tsx +158 -0
- package/src/custom/tanstack-table/fields/tanstack-table-filter-text.tsx +76 -0
- package/src/custom/tanstack-table/fields/tanstack-table-pagination.tsx +136 -0
- package/src/custom/tanstack-table/fields/tanstack-table-plain-table.tsx +142 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-confirmation.tsx +77 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions-custom-dialog.tsx +87 -0
- package/src/custom/tanstack-table/fields/tanstack-table-row-actions.tsx +151 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-custom-dialog.tsx +88 -0
- package/src/custom/tanstack-table/fields/tanstack-table-table-actions-schemaform-dialog.tsx +47 -0
- package/src/custom/tanstack-table/fields/tanstack-table-toolbar.tsx +143 -0
- package/src/custom/tanstack-table/fields/tanstack-table-view-options.tsx +171 -0
- package/src/custom/tanstack-table/index.tsx +244 -0
- package/src/custom/tanstack-table/types/index.ts +328 -0
- package/src/custom/tanstack-table/utils/cell-with-actions.tsx +21 -0
- package/src/custom/tanstack-table/utils/column-names.ts +26 -0
- package/src/custom/tanstack-table/utils/columns-by-row-data.tsx +312 -0
- package/src/custom/tanstack-table/utils/editable-columns-by-row-data.tsx +219 -0
- package/src/custom/tanstack-table/utils/faceted-boolean-options.tsx +22 -0
- package/src/custom/tanstack-table/utils/index.tsx +10 -0
- package/src/custom/tanstack-table/utils/pinning-styles.ts +57 -0
- package/src/custom/tanstack-table/utils/table.tsx +83 -0
- package/src/custom/tanstack-table/utils/test-conditions.ts +17 -0
- package/src/custom/timeline.tsx +208 -0
- package/src/custom/tree.tsx +200 -0
- package/src/custom/tscanify/browser.ts +66 -0
- package/src/custom/tscanify/index.ts +51 -0
- package/src/custom/tscanify/tscanify-browser.ts +522 -0
- package/src/custom/tscanify/tscanify.ts +262 -0
- package/src/custom/tscanify/types.ts +22 -0
- package/src/custom/webcam.tsx +737 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-callback-ref.ts +27 -0
- package/src/hooks/use-controllable-state.ts +67 -0
- package/src/hooks/use-debounce.ts +19 -0
- package/src/hooks/use-is-visible.ts +23 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/hooks/use-on-window-resize.ts +15 -0
- package/src/hooks/use-scroll.tsx +22 -0
- package/src/lib/utils.ts +61 -0
- package/src/lib/zod.ts +2 -0
- package/src/styles/core.css +57 -0
- package/src/styles/globals.css +130 -0
- package/src/test/email-input.test.tsx +217 -0
- package/src/test/password-input.test.tsx +92 -0
- package/src/test/select-tabs.test.tsx +302 -0
- package/src/test/selectable.test.tsx +1093 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lint.json +8 -0
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webcam Component - Core Features
|
|
3
|
+
*
|
|
4
|
+
* Three main functionalities:
|
|
5
|
+
* 1) Record Video - with interface controls, auto-start, max duration
|
|
6
|
+
* 2) Capture Photo - with interface controls, quality settings
|
|
7
|
+
* 3) Auto-capture Photo - with interface controls, intervals, quality
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Customizable placeholder overlay
|
|
11
|
+
* - Comprehensive feedback functions
|
|
12
|
+
* - Camera switching support
|
|
13
|
+
* - Internationalization support
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use client";
|
|
17
|
+
|
|
18
|
+
import { Camera, Pause, Play, RefreshCw, Square, Video } from "lucide-react";
|
|
19
|
+
import * as React from "react";
|
|
20
|
+
import { useCallback, useEffect, useRef, useState, useTransition } from "react";
|
|
21
|
+
import WebcamCore from "react-webcam";
|
|
22
|
+
import { cn } from "@repo/ayasofyazilim-ui/lib/utils";
|
|
23
|
+
import { Button } from "@repo/ayasofyazilim-ui/components/button";
|
|
24
|
+
|
|
25
|
+
// Types
|
|
26
|
+
export interface VideoDimensions {
|
|
27
|
+
width: number;
|
|
28
|
+
height: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Core Feature Interfaces
|
|
32
|
+
export interface VideoRecordingConfig {
|
|
33
|
+
hasInterface: boolean; // Show video recording UI controls
|
|
34
|
+
autoStart?: boolean; // Auto-start recording when camera ready
|
|
35
|
+
maxVideoDuration?: number; // Max duration in seconds (default: 60)
|
|
36
|
+
quality?: "low" | "medium" | "high"; // Video quality
|
|
37
|
+
mimeType?: string; // Video MIME type (default: 'video/webm')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PhotoCaptureConfig {
|
|
41
|
+
hasInterface: boolean; // Show photo capture UI controls
|
|
42
|
+
quality?: number; // JPEG quality 0-1 (default: 0.95)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AutoCaptureConfig {
|
|
46
|
+
hasInterface: boolean; // Show auto-capture UI controls
|
|
47
|
+
captureInterval: number; // Interval in seconds
|
|
48
|
+
startOnReady?: boolean; // Auto-start when camera ready (only on initial load)
|
|
49
|
+
quality?: number; // JPEG quality 0-1 (default: 0.95)
|
|
50
|
+
stopOnCapture?: boolean; // Stop auto capture after first successful capture
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Feedback Functions
|
|
54
|
+
export interface WebcamCallbacks {
|
|
55
|
+
onWebcamReady?: (dimensions: VideoDimensions) => void;
|
|
56
|
+
onVideoStart?: () => void;
|
|
57
|
+
onVideoEnd?: (videoBlob: Blob, duration: number) => void;
|
|
58
|
+
onPhotoCaptured?: (imageData: string) => void;
|
|
59
|
+
onAutoPhotoCaptured?: (imageData: string) => void;
|
|
60
|
+
onAutoCaptureStart?: () => void;
|
|
61
|
+
beforeStartAutoCapture?: () => void;
|
|
62
|
+
onAutoCaptureStop?: () => void;
|
|
63
|
+
onError?: (error: string) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Main Props Interface
|
|
67
|
+
export interface WebcamProps {
|
|
68
|
+
// Core Features (at least one must be enabled)
|
|
69
|
+
videoRecording?: VideoRecordingConfig;
|
|
70
|
+
photoCapture?: PhotoCaptureConfig;
|
|
71
|
+
autoCapture?: AutoCaptureConfig;
|
|
72
|
+
|
|
73
|
+
// Camera Settings
|
|
74
|
+
defaultCamera?: "front" | "back"; // Default camera (front='user', back='environment')
|
|
75
|
+
allowCameraSwitch?: boolean; // Allow camera switching
|
|
76
|
+
|
|
77
|
+
// UI Customization
|
|
78
|
+
placeholder?: React.ReactElement; // Overlay that covers camera area
|
|
79
|
+
showCapturedImage?: boolean; // Show last captured image preview
|
|
80
|
+
capturedImage?: string | null; // Current captured image for preview
|
|
81
|
+
forceHideInterface?: boolean; // Force hide interface controls (useful for manual capture mode)
|
|
82
|
+
// Feedback Functions
|
|
83
|
+
callbacks?: WebcamCallbacks;
|
|
84
|
+
|
|
85
|
+
// Labels for Translation
|
|
86
|
+
labels?: {
|
|
87
|
+
// Video Recording
|
|
88
|
+
startRecording?: string;
|
|
89
|
+
stopRecording?: string;
|
|
90
|
+
recording?: string;
|
|
91
|
+
|
|
92
|
+
// Photo Capture
|
|
93
|
+
capturePhoto?: string;
|
|
94
|
+
|
|
95
|
+
// Auto Capture
|
|
96
|
+
startAutoCapture?: string;
|
|
97
|
+
stopAutoCapture?: string;
|
|
98
|
+
autoCapturing?: string;
|
|
99
|
+
|
|
100
|
+
// General
|
|
101
|
+
switchCamera?: string;
|
|
102
|
+
cameraReady?: string;
|
|
103
|
+
cameraError?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// Styling
|
|
107
|
+
classNames?: {
|
|
108
|
+
container?: string;
|
|
109
|
+
webcam?: string;
|
|
110
|
+
placeholder?: string;
|
|
111
|
+
controls?: string;
|
|
112
|
+
previewImage?: string;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Technical Settings
|
|
116
|
+
webcamRef?: React.RefObject<WebcamCore>;
|
|
117
|
+
videoCheckInterval?: number; // default: 500ms
|
|
118
|
+
maxRetryCount?: number; // default: 10
|
|
119
|
+
interfaceLocation?: "static" | "absolute"; // Location of controls (default: 'bottom')
|
|
120
|
+
showBorder?: boolean; // Show border around webcam area (default: true)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type WebcamCaptureProps = WebcamProps;
|
|
124
|
+
|
|
125
|
+
// Legacy aliases for backward compatibility
|
|
126
|
+
export type WebcamAutoCapture = WebcamProps;
|
|
127
|
+
export type WebcamManualCapture = WebcamProps;
|
|
128
|
+
|
|
129
|
+
export function Webcam(props: WebcamProps) {
|
|
130
|
+
const {
|
|
131
|
+
videoRecording,
|
|
132
|
+
photoCapture,
|
|
133
|
+
autoCapture,
|
|
134
|
+
defaultCamera = "front",
|
|
135
|
+
allowCameraSwitch = false,
|
|
136
|
+
placeholder,
|
|
137
|
+
showCapturedImage = false,
|
|
138
|
+
capturedImage,
|
|
139
|
+
callbacks,
|
|
140
|
+
labels = {},
|
|
141
|
+
classNames,
|
|
142
|
+
webcamRef: externalWebcamRef,
|
|
143
|
+
videoCheckInterval = 500,
|
|
144
|
+
maxRetryCount = 10,
|
|
145
|
+
showBorder = false,
|
|
146
|
+
forceHideInterface = false,
|
|
147
|
+
interfaceLocation = "static",
|
|
148
|
+
} = props;
|
|
149
|
+
|
|
150
|
+
// Validation: At least one feature must be enabled
|
|
151
|
+
if (!videoRecording && !photoCapture && !autoCapture) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"WebcamComponent: At least one core feature (videoRecording, photoCapture, or autoCapture) must be enabled"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Default labels
|
|
158
|
+
const defaultLabels = {
|
|
159
|
+
startRecording: "Start Recording",
|
|
160
|
+
stopRecording: "Stop Recording",
|
|
161
|
+
recording: "Recording...",
|
|
162
|
+
capturePhoto: "Capture Photo",
|
|
163
|
+
startAutoCapture: "Start Auto Capture",
|
|
164
|
+
stopAutoCapture: "Stop Auto Capture",
|
|
165
|
+
autoCapturing: "Auto Capturing...",
|
|
166
|
+
switchCamera: "Switch Camera",
|
|
167
|
+
cameraReady: "Camera Ready",
|
|
168
|
+
cameraError: "Camera Error",
|
|
169
|
+
...labels,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// State
|
|
173
|
+
const [isPending, startTransition] = useTransition();
|
|
174
|
+
const [facingMode, setFacingMode] = useState<"user" | "environment">(
|
|
175
|
+
defaultCamera === "front" ? "user" : "environment"
|
|
176
|
+
);
|
|
177
|
+
const [isWebcamReady, setIsWebcamReady] = useState(false);
|
|
178
|
+
// Video Recording State
|
|
179
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
180
|
+
const [recordingDuration, setRecordingDuration] = useState(0);
|
|
181
|
+
|
|
182
|
+
// Auto Capture State
|
|
183
|
+
const [isAutoCapturing, setIsAutoCapturing] = useState(false);
|
|
184
|
+
|
|
185
|
+
// Auto capture state to track if it has been initially started
|
|
186
|
+
const [hasAutoStarted, setHasAutoStarted] = useState(false);
|
|
187
|
+
|
|
188
|
+
// Refs
|
|
189
|
+
const internalWebcamRef = useRef<WebcamCore>(null);
|
|
190
|
+
const webcamRef = externalWebcamRef || internalWebcamRef;
|
|
191
|
+
const videoCheckIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
192
|
+
const autoCapturIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
193
|
+
const recordingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
194
|
+
const retryCountRef = useRef(0);
|
|
195
|
+
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
|
196
|
+
const recordedChunksRef = useRef<Blob[]>([]);
|
|
197
|
+
|
|
198
|
+
// Cleanup utilities
|
|
199
|
+
const clearVideoCheckInterval = useCallback(() => {
|
|
200
|
+
if (videoCheckIntervalRef.current) {
|
|
201
|
+
clearInterval(videoCheckIntervalRef.current);
|
|
202
|
+
videoCheckIntervalRef.current = null;
|
|
203
|
+
}
|
|
204
|
+
}, []);
|
|
205
|
+
|
|
206
|
+
const clearAutoCapture = useCallback(() => {
|
|
207
|
+
if (autoCapturIntervalRef.current) {
|
|
208
|
+
clearInterval(autoCapturIntervalRef.current);
|
|
209
|
+
autoCapturIntervalRef.current = null;
|
|
210
|
+
}
|
|
211
|
+
}, []);
|
|
212
|
+
|
|
213
|
+
const clearRecordingInterval = useCallback(() => {
|
|
214
|
+
if (recordingIntervalRef.current) {
|
|
215
|
+
clearInterval(recordingIntervalRef.current);
|
|
216
|
+
recordingIntervalRef.current = null;
|
|
217
|
+
}
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
// Video Recording Functions
|
|
221
|
+
const startVideoRecording = useCallback(() => {
|
|
222
|
+
if (!webcamRef.current?.stream || isRecording || !videoRecording) return;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const { stream } = webcamRef.current;
|
|
226
|
+
const mimeType = videoRecording.mimeType || "video/webm";
|
|
227
|
+
|
|
228
|
+
const mediaRecorder = new MediaRecorder(stream, { mimeType });
|
|
229
|
+
recordedChunksRef.current = [];
|
|
230
|
+
|
|
231
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
232
|
+
if (event.data.size > 0) {
|
|
233
|
+
recordedChunksRef.current.push(event.data);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
mediaRecorder.onstop = () => {
|
|
238
|
+
const blob = new Blob(recordedChunksRef.current, { type: mimeType });
|
|
239
|
+
const duration = recordingDuration;
|
|
240
|
+
|
|
241
|
+
callbacks?.onVideoEnd?.(blob, duration);
|
|
242
|
+
setIsRecording(false);
|
|
243
|
+
setRecordingDuration(0);
|
|
244
|
+
clearRecordingInterval();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
248
|
+
mediaRecorder.start();
|
|
249
|
+
|
|
250
|
+
const startTime = Date.now();
|
|
251
|
+
setIsRecording(true);
|
|
252
|
+
setRecordingDuration(0);
|
|
253
|
+
|
|
254
|
+
// Start duration tracking
|
|
255
|
+
recordingIntervalRef.current = setInterval(() => {
|
|
256
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
257
|
+
setRecordingDuration(elapsed);
|
|
258
|
+
|
|
259
|
+
// Auto-stop if max duration reached
|
|
260
|
+
if (
|
|
261
|
+
videoRecording.maxVideoDuration &&
|
|
262
|
+
elapsed >= videoRecording.maxVideoDuration
|
|
263
|
+
) {
|
|
264
|
+
stopVideoRecording();
|
|
265
|
+
}
|
|
266
|
+
}, 1000);
|
|
267
|
+
|
|
268
|
+
callbacks?.onVideoStart?.();
|
|
269
|
+
} catch (error) {
|
|
270
|
+
callbacks?.onError?.(`Failed to start video recording: ${error}`);
|
|
271
|
+
}
|
|
272
|
+
}, [
|
|
273
|
+
webcamRef,
|
|
274
|
+
isRecording,
|
|
275
|
+
videoRecording,
|
|
276
|
+
callbacks,
|
|
277
|
+
recordingDuration,
|
|
278
|
+
clearRecordingInterval,
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
const stopVideoRecording = useCallback(() => {
|
|
282
|
+
if (mediaRecorderRef.current && isRecording) {
|
|
283
|
+
mediaRecorderRef.current.stop();
|
|
284
|
+
mediaRecorderRef.current = null;
|
|
285
|
+
}
|
|
286
|
+
}, [isRecording]);
|
|
287
|
+
|
|
288
|
+
// Photo Capture Functions
|
|
289
|
+
const captureHighQualityPhoto = useCallback((): string | null => {
|
|
290
|
+
const video = webcamRef.current?.video;
|
|
291
|
+
if (!video) return null;
|
|
292
|
+
|
|
293
|
+
const canvas = document.createElement("canvas");
|
|
294
|
+
const ctx = canvas.getContext("2d");
|
|
295
|
+
if (!ctx) return null;
|
|
296
|
+
|
|
297
|
+
canvas.width = video.videoWidth;
|
|
298
|
+
canvas.height = video.videoHeight;
|
|
299
|
+
|
|
300
|
+
ctx.imageSmoothingEnabled = true;
|
|
301
|
+
ctx.imageSmoothingQuality = "high";
|
|
302
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
303
|
+
|
|
304
|
+
const quality = photoCapture?.quality || 0.95;
|
|
305
|
+
return canvas.toDataURL("image/jpeg", quality);
|
|
306
|
+
}, [webcamRef, photoCapture]);
|
|
307
|
+
|
|
308
|
+
const capturePhoto = useCallback(() => {
|
|
309
|
+
if (!photoCapture) return;
|
|
310
|
+
|
|
311
|
+
const imageData = captureHighQualityPhoto();
|
|
312
|
+
if (imageData) {
|
|
313
|
+
callbacks?.onPhotoCaptured?.(imageData);
|
|
314
|
+
}
|
|
315
|
+
}, [photoCapture, captureHighQualityPhoto, callbacks]);
|
|
316
|
+
|
|
317
|
+
const stopAutoCapture = useCallback(() => {
|
|
318
|
+
setIsAutoCapturing(false);
|
|
319
|
+
callbacks?.onAutoCaptureStop?.();
|
|
320
|
+
clearAutoCapture();
|
|
321
|
+
}, [clearAutoCapture, callbacks]);
|
|
322
|
+
|
|
323
|
+
// Auto Capture Functions
|
|
324
|
+
const captureAutoPhoto = useCallback(() => {
|
|
325
|
+
if (!autoCapture) return;
|
|
326
|
+
|
|
327
|
+
const video = webcamRef.current?.video;
|
|
328
|
+
if (!video) return;
|
|
329
|
+
|
|
330
|
+
const canvas = document.createElement("canvas");
|
|
331
|
+
const ctx = canvas.getContext("2d");
|
|
332
|
+
if (!ctx) return;
|
|
333
|
+
|
|
334
|
+
canvas.width = video.videoWidth;
|
|
335
|
+
canvas.height = video.videoHeight;
|
|
336
|
+
|
|
337
|
+
ctx.imageSmoothingEnabled = true;
|
|
338
|
+
ctx.imageSmoothingQuality = "high";
|
|
339
|
+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
340
|
+
|
|
341
|
+
const quality = autoCapture.quality || 0.95;
|
|
342
|
+
const imageData = canvas.toDataURL("image/jpeg", quality);
|
|
343
|
+
|
|
344
|
+
if (imageData) {
|
|
345
|
+
callbacks?.onAutoPhotoCaptured?.(imageData);
|
|
346
|
+
|
|
347
|
+
// Stop auto capture if stopOnCapture is enabled
|
|
348
|
+
if (autoCapture.stopOnCapture) {
|
|
349
|
+
stopAutoCapture();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}, [webcamRef, autoCapture, callbacks, stopAutoCapture]);
|
|
353
|
+
|
|
354
|
+
const startAutoCapture = useCallback(
|
|
355
|
+
(isManual = false) => {
|
|
356
|
+
if (!autoCapture || !isWebcamReady || isAutoCapturing) return;
|
|
357
|
+
|
|
358
|
+
// If it's automatic start (startOnReady) and already started, don't start again
|
|
359
|
+
if (!isManual && autoCapture.startOnReady && hasAutoStarted) return;
|
|
360
|
+
|
|
361
|
+
setIsAutoCapturing(true);
|
|
362
|
+
callbacks?.onAutoCaptureStart?.();
|
|
363
|
+
|
|
364
|
+
// Set hasAutoStarted flag only for automatic starts
|
|
365
|
+
if (!isManual && autoCapture.startOnReady) {
|
|
366
|
+
setHasAutoStarted(true);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
autoCapturIntervalRef.current = setInterval(() => {
|
|
370
|
+
if (webcamRef.current && isWebcamReady) {
|
|
371
|
+
captureAutoPhoto();
|
|
372
|
+
}
|
|
373
|
+
}, autoCapture.captureInterval * 1000);
|
|
374
|
+
},
|
|
375
|
+
[
|
|
376
|
+
autoCapture,
|
|
377
|
+
isWebcamReady,
|
|
378
|
+
isAutoCapturing,
|
|
379
|
+
captureAutoPhoto,
|
|
380
|
+
webcamRef,
|
|
381
|
+
callbacks,
|
|
382
|
+
hasAutoStarted,
|
|
383
|
+
]
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// Camera Management
|
|
387
|
+
const switchCamera = useCallback(() => {
|
|
388
|
+
const newFacingMode = facingMode === "user" ? "environment" : "user";
|
|
389
|
+
setFacingMode(newFacingMode);
|
|
390
|
+
setIsWebcamReady(false);
|
|
391
|
+
setHasAutoStarted(false); // Reset auto-start flag for new camera
|
|
392
|
+
stopAutoCapture();
|
|
393
|
+
}, [facingMode, stopAutoCapture]);
|
|
394
|
+
|
|
395
|
+
// Add device detection for better browser compatibility
|
|
396
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
397
|
+
|
|
398
|
+
// Detect mobile device on component mount
|
|
399
|
+
useEffect(() => {
|
|
400
|
+
const checkIsMobile = () =>
|
|
401
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
402
|
+
navigator.userAgent
|
|
403
|
+
);
|
|
404
|
+
setIsMobile(checkIsMobile());
|
|
405
|
+
}, []);
|
|
406
|
+
|
|
407
|
+
// Video Readiness Check
|
|
408
|
+
const checkVideoReady = useCallback(() => {
|
|
409
|
+
const video = webcamRef.current?.video;
|
|
410
|
+
if (!video) return false;
|
|
411
|
+
|
|
412
|
+
const isReady =
|
|
413
|
+
video.readyState >= 2 && video.videoWidth > 0 && video.videoHeight > 0;
|
|
414
|
+
|
|
415
|
+
if (isReady) {
|
|
416
|
+
const newDimensions: VideoDimensions = {
|
|
417
|
+
width: video.videoWidth,
|
|
418
|
+
height: video.videoHeight,
|
|
419
|
+
};
|
|
420
|
+
callbacks?.onWebcamReady?.(newDimensions);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return isReady;
|
|
424
|
+
}, [webcamRef, callbacks]);
|
|
425
|
+
|
|
426
|
+
// Video Initialization
|
|
427
|
+
const handleUserMedia = useCallback(() => {
|
|
428
|
+
clearVideoCheckInterval();
|
|
429
|
+
retryCountRef.current = 0;
|
|
430
|
+
|
|
431
|
+
videoCheckIntervalRef.current = setInterval(() => {
|
|
432
|
+
if (checkVideoReady()) {
|
|
433
|
+
setIsWebcamReady(true);
|
|
434
|
+
clearVideoCheckInterval();
|
|
435
|
+
|
|
436
|
+
// Auto-start features if enabled
|
|
437
|
+
if (videoRecording?.autoStart) {
|
|
438
|
+
startVideoRecording();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (autoCapture?.startOnReady && !hasAutoStarted) {
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
startAutoCapture(false); // Automatic start
|
|
444
|
+
}, 100);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
retryCountRef.current++;
|
|
448
|
+
|
|
449
|
+
if (retryCountRef.current > maxRetryCount) {
|
|
450
|
+
clearVideoCheckInterval();
|
|
451
|
+
callbacks?.onError?.(
|
|
452
|
+
"Failed to initialize camera after maximum retries"
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Try with default constraints as a fallback
|
|
456
|
+
const currentFacingMode = facingMode;
|
|
457
|
+
setFacingMode("user");
|
|
458
|
+
setTimeout(() => setFacingMode(currentFacingMode), 100);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}, videoCheckInterval);
|
|
462
|
+
|
|
463
|
+
return clearVideoCheckInterval;
|
|
464
|
+
}, [
|
|
465
|
+
clearVideoCheckInterval,
|
|
466
|
+
checkVideoReady,
|
|
467
|
+
videoRecording,
|
|
468
|
+
autoCapture,
|
|
469
|
+
startVideoRecording,
|
|
470
|
+
startAutoCapture,
|
|
471
|
+
maxRetryCount,
|
|
472
|
+
videoCheckInterval,
|
|
473
|
+
facingMode,
|
|
474
|
+
callbacks,
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
// Handle camera initialization errors
|
|
478
|
+
const handleUserMediaError = useCallback(
|
|
479
|
+
(error: string | DOMException) => {
|
|
480
|
+
callbacks?.onError?.(
|
|
481
|
+
typeof error === "string" ? error : `Camera error: ${error.message}`
|
|
482
|
+
);
|
|
483
|
+
setIsWebcamReady(false);
|
|
484
|
+
},
|
|
485
|
+
[callbacks]
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Manual capture function
|
|
489
|
+
const handleCapturePhoto = useCallback(() => {
|
|
490
|
+
startTransition(() => {
|
|
491
|
+
capturePhoto();
|
|
492
|
+
});
|
|
493
|
+
}, [capturePhoto, startTransition]);
|
|
494
|
+
|
|
495
|
+
// Manual auto capture start function for button
|
|
496
|
+
const handleStartAutoCapture = useCallback(() => {
|
|
497
|
+
if (callbacks?.beforeStartAutoCapture) callbacks.beforeStartAutoCapture();
|
|
498
|
+
startAutoCapture(true); // Manual start
|
|
499
|
+
}, [startAutoCapture, callbacks]);
|
|
500
|
+
|
|
501
|
+
// Effects
|
|
502
|
+
useEffect(
|
|
503
|
+
() => () => {
|
|
504
|
+
clearVideoCheckInterval();
|
|
505
|
+
stopAutoCapture();
|
|
506
|
+
clearAutoCapture();
|
|
507
|
+
clearRecordingInterval();
|
|
508
|
+
},
|
|
509
|
+
[
|
|
510
|
+
clearVideoCheckInterval,
|
|
511
|
+
stopAutoCapture,
|
|
512
|
+
clearAutoCapture,
|
|
513
|
+
clearRecordingInterval,
|
|
514
|
+
]
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
if (
|
|
519
|
+
autoCapture?.startOnReady &&
|
|
520
|
+
isWebcamReady &&
|
|
521
|
+
!isAutoCapturing &&
|
|
522
|
+
!hasAutoStarted
|
|
523
|
+
) {
|
|
524
|
+
startAutoCapture(false); // Automatic start
|
|
525
|
+
}
|
|
526
|
+
}, [
|
|
527
|
+
autoCapture,
|
|
528
|
+
isWebcamReady,
|
|
529
|
+
isAutoCapturing,
|
|
530
|
+
hasAutoStarted,
|
|
531
|
+
startAutoCapture,
|
|
532
|
+
]);
|
|
533
|
+
|
|
534
|
+
// Render functions
|
|
535
|
+
const renderVideoRecordingControls = () => {
|
|
536
|
+
if (!videoRecording?.hasInterface || forceHideInterface) return null;
|
|
537
|
+
|
|
538
|
+
return (
|
|
539
|
+
<div
|
|
540
|
+
className="flex flex-col items-center space-y-2"
|
|
541
|
+
key="video-recording-controls"
|
|
542
|
+
>
|
|
543
|
+
{isRecording ? (
|
|
544
|
+
<>
|
|
545
|
+
<Button
|
|
546
|
+
className="size-12 rounded-full border-2 border-red-500 bg-red-500 p-0 text-white"
|
|
547
|
+
onClick={stopVideoRecording}
|
|
548
|
+
>
|
|
549
|
+
<Square className="size-4" />
|
|
550
|
+
</Button>
|
|
551
|
+
<div className="flex items-center space-x-2 text-white">
|
|
552
|
+
<div className="h-2 w-2 animate-pulse rounded-full bg-red-500" />
|
|
553
|
+
<span className="text-xs">
|
|
554
|
+
{defaultLabels.recording} {Math.floor(recordingDuration / 60)}:
|
|
555
|
+
{String(recordingDuration % 60).padStart(2, "0")}
|
|
556
|
+
</span>
|
|
557
|
+
</div>
|
|
558
|
+
</>
|
|
559
|
+
) : (
|
|
560
|
+
<Button
|
|
561
|
+
className="size-12 rounded-full border-2 border-white bg-white/10 p-0 text-white"
|
|
562
|
+
disabled={!isWebcamReady}
|
|
563
|
+
onClick={startVideoRecording}
|
|
564
|
+
>
|
|
565
|
+
<Video className="size-4" />
|
|
566
|
+
</Button>
|
|
567
|
+
)}
|
|
568
|
+
</div>
|
|
569
|
+
);
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const renderPhotoCaptureControls = () => {
|
|
573
|
+
if (!photoCapture?.hasInterface || forceHideInterface) return null;
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<Button
|
|
577
|
+
key="photo-capture-controls"
|
|
578
|
+
className="size-12 rounded-full border-2 border-white bg-white/10 p-0 text-white transition-all hover:bg-white hover:ring-4"
|
|
579
|
+
disabled={isPending || !isWebcamReady}
|
|
580
|
+
onClick={handleCapturePhoto}
|
|
581
|
+
>
|
|
582
|
+
<Camera className="size-4" />
|
|
583
|
+
<span className="sr-only">{defaultLabels.capturePhoto}</span>
|
|
584
|
+
</Button>
|
|
585
|
+
);
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const renderAutoCaptureControls = () => {
|
|
589
|
+
if (!autoCapture?.hasInterface || forceHideInterface) return null;
|
|
590
|
+
|
|
591
|
+
return (
|
|
592
|
+
<div
|
|
593
|
+
className="flex flex-col items-center space-y-2"
|
|
594
|
+
key="auto-capture-controls"
|
|
595
|
+
>
|
|
596
|
+
{isAutoCapturing ? (
|
|
597
|
+
<>
|
|
598
|
+
<Button
|
|
599
|
+
className="size-12 rounded-full border-2 p-0 text-white"
|
|
600
|
+
variant="ghost"
|
|
601
|
+
onClick={stopAutoCapture}
|
|
602
|
+
>
|
|
603
|
+
<Pause className="size-4" />
|
|
604
|
+
</Button>
|
|
605
|
+
<div className="flex items-center space-x-2 text-white">
|
|
606
|
+
<div className="h-2 w-2 animate-pulse rounded-full bg-green-500" />
|
|
607
|
+
<span className="text-xs">{defaultLabels.autoCapturing}</span>
|
|
608
|
+
</div>
|
|
609
|
+
</>
|
|
610
|
+
) : (
|
|
611
|
+
<Button
|
|
612
|
+
className="size-12 rounded-full border-2 p-0 text-white"
|
|
613
|
+
variant="ghost"
|
|
614
|
+
disabled={!isWebcamReady}
|
|
615
|
+
onClick={handleStartAutoCapture}
|
|
616
|
+
>
|
|
617
|
+
<Play className="size-4" />
|
|
618
|
+
<span className="sr-only">{defaultLabels.startAutoCapture}</span>
|
|
619
|
+
</Button>
|
|
620
|
+
)}
|
|
621
|
+
</div>
|
|
622
|
+
);
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const renderCameraSwitchButton = () => {
|
|
626
|
+
if (!allowCameraSwitch || forceHideInterface) return null;
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<Button
|
|
630
|
+
key="camera-switch-button"
|
|
631
|
+
className="size-8 rounded-full bg-white/10 p-0 text-white"
|
|
632
|
+
onClick={switchCamera}
|
|
633
|
+
variant="ghost"
|
|
634
|
+
>
|
|
635
|
+
<RefreshCw className="size-4" />
|
|
636
|
+
<span className="sr-only">{defaultLabels.switchCamera}</span>
|
|
637
|
+
</Button>
|
|
638
|
+
);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
const renderCapturedImagePreview = () => {
|
|
642
|
+
if (!showCapturedImage || !capturedImage) return null;
|
|
643
|
+
|
|
644
|
+
return (
|
|
645
|
+
<div className={cn("captured size-10", classNames?.previewImage)}>
|
|
646
|
+
<img
|
|
647
|
+
src={capturedImage}
|
|
648
|
+
alt="Captured"
|
|
649
|
+
className="size-10 rounded-md object-cover"
|
|
650
|
+
/>
|
|
651
|
+
</div>
|
|
652
|
+
);
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
const renderControls = () => {
|
|
656
|
+
const controls = [];
|
|
657
|
+
|
|
658
|
+
if (videoRecording?.hasInterface || forceHideInterface) {
|
|
659
|
+
controls.push(renderVideoRecordingControls());
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (photoCapture?.hasInterface || forceHideInterface) {
|
|
663
|
+
controls.push(renderPhotoCaptureControls());
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (autoCapture?.hasInterface || forceHideInterface) {
|
|
667
|
+
controls.push(renderAutoCaptureControls());
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return controls.filter(Boolean);
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
return (
|
|
674
|
+
<div
|
|
675
|
+
className={cn(
|
|
676
|
+
"webcam-container overflow-hidden rounded-md bg-black relative",
|
|
677
|
+
classNames?.container
|
|
678
|
+
)}
|
|
679
|
+
>
|
|
680
|
+
<div
|
|
681
|
+
className={cn(
|
|
682
|
+
"webcam relative",
|
|
683
|
+
showBorder && "p-2",
|
|
684
|
+
classNames?.webcam
|
|
685
|
+
)}
|
|
686
|
+
>
|
|
687
|
+
<div className="relative">
|
|
688
|
+
<WebcamCore
|
|
689
|
+
audio={false}
|
|
690
|
+
className="background-transparent h-auto w-full rounded-md"
|
|
691
|
+
mirrored={facingMode === "user"}
|
|
692
|
+
onUserMedia={handleUserMedia}
|
|
693
|
+
onUserMediaError={handleUserMediaError}
|
|
694
|
+
ref={webcamRef}
|
|
695
|
+
screenshotFormat="image/jpeg"
|
|
696
|
+
screenshotQuality={1}
|
|
697
|
+
videoConstraints={{
|
|
698
|
+
facingMode,
|
|
699
|
+
width: { ideal: 1920, min: 640 },
|
|
700
|
+
height: { ideal: 1920, min: 640 },
|
|
701
|
+
frameRate: { ideal: 30, min: 15 },
|
|
702
|
+
aspectRatio: isMobile ? 1.0 : 4 / 3,
|
|
703
|
+
}}
|
|
704
|
+
/>
|
|
705
|
+
|
|
706
|
+
{placeholder && isWebcamReady && (
|
|
707
|
+
<div
|
|
708
|
+
className={cn(
|
|
709
|
+
"absolute w-full h-full inset-0 z-3",
|
|
710
|
+
classNames?.placeholder
|
|
711
|
+
)}
|
|
712
|
+
>
|
|
713
|
+
{placeholder}
|
|
714
|
+
</div>
|
|
715
|
+
)}
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<div
|
|
720
|
+
className={cn(
|
|
721
|
+
"actions flex items-center justify-between p-2 pt-0",
|
|
722
|
+
classNames?.controls,
|
|
723
|
+
interfaceLocation === "absolute"
|
|
724
|
+
? "absolute bottom-0 left-0 p-4 z-10"
|
|
725
|
+
: ""
|
|
726
|
+
)}
|
|
727
|
+
>
|
|
728
|
+
{renderCapturedImagePreview()}
|
|
729
|
+
|
|
730
|
+
<div className="flex items-center space-x-4">
|
|
731
|
+
{renderControls()}
|
|
732
|
+
{renderCameraSwitchButton()}
|
|
733
|
+
</div>
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
);
|
|
737
|
+
}
|