react-native-frame-capture 1.0.1

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.
Files changed (77) hide show
  1. package/FrameCapture.podspec +21 -0
  2. package/LICENSE +20 -0
  3. package/README.md +158 -0
  4. package/android/build.gradle +77 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +20 -0
  7. package/android/src/main/java/com/framecapture/CaptureManager.kt +831 -0
  8. package/android/src/main/java/com/framecapture/Constants.kt +196 -0
  9. package/android/src/main/java/com/framecapture/ErrorHandler.kt +165 -0
  10. package/android/src/main/java/com/framecapture/FrameCaptureModule.kt +653 -0
  11. package/android/src/main/java/com/framecapture/FrameCapturePackage.kt +32 -0
  12. package/android/src/main/java/com/framecapture/OverlayRenderer.kt +423 -0
  13. package/android/src/main/java/com/framecapture/PermissionHandler.kt +150 -0
  14. package/android/src/main/java/com/framecapture/ScreenCaptureService.kt +366 -0
  15. package/android/src/main/java/com/framecapture/StorageManager.kt +221 -0
  16. package/android/src/main/java/com/framecapture/capture/BitmapProcessor.kt +157 -0
  17. package/android/src/main/java/com/framecapture/capture/CaptureEventEmitter.kt +120 -0
  18. package/android/src/main/java/com/framecapture/models/CaptureModels.kt +302 -0
  19. package/android/src/main/java/com/framecapture/models/EnumsAndExtensions.kt +60 -0
  20. package/android/src/main/java/com/framecapture/models/OverlayModels.kt +154 -0
  21. package/android/src/main/java/com/framecapture/service/CaptureNotificationManager.kt +286 -0
  22. package/android/src/main/java/com/framecapture/storage/StorageStrategies.kt +317 -0
  23. package/android/src/main/java/com/framecapture/utils/ValidationUtils.kt +379 -0
  24. package/app.plugin.js +1 -0
  25. package/ios/FrameCapture.h +5 -0
  26. package/ios/FrameCapture.mm +21 -0
  27. package/lib/module/NativeFrameCapture.js +24 -0
  28. package/lib/module/NativeFrameCapture.js.map +1 -0
  29. package/lib/module/api.js +146 -0
  30. package/lib/module/api.js.map +1 -0
  31. package/lib/module/constants.js +67 -0
  32. package/lib/module/constants.js.map +1 -0
  33. package/lib/module/errors.js +19 -0
  34. package/lib/module/errors.js.map +1 -0
  35. package/lib/module/events.js +58 -0
  36. package/lib/module/events.js.map +1 -0
  37. package/lib/module/index.js +24 -0
  38. package/lib/module/index.js.map +1 -0
  39. package/lib/module/normalize.js +51 -0
  40. package/lib/module/normalize.js.map +1 -0
  41. package/lib/module/package.json +1 -0
  42. package/lib/module/types.js +165 -0
  43. package/lib/module/types.js.map +1 -0
  44. package/lib/module/validation.js +190 -0
  45. package/lib/module/validation.js.map +1 -0
  46. package/lib/typescript/package.json +1 -0
  47. package/lib/typescript/plugin/src/index.d.ts +4 -0
  48. package/lib/typescript/plugin/src/index.d.ts.map +1 -0
  49. package/lib/typescript/src/NativeFrameCapture.d.ts +75 -0
  50. package/lib/typescript/src/NativeFrameCapture.d.ts.map +1 -0
  51. package/lib/typescript/src/api.d.ts +66 -0
  52. package/lib/typescript/src/api.d.ts.map +1 -0
  53. package/lib/typescript/src/constants.d.ts +41 -0
  54. package/lib/typescript/src/constants.d.ts.map +1 -0
  55. package/lib/typescript/src/errors.d.ts +14 -0
  56. package/lib/typescript/src/errors.d.ts.map +1 -0
  57. package/lib/typescript/src/events.d.ts +30 -0
  58. package/lib/typescript/src/events.d.ts.map +1 -0
  59. package/lib/typescript/src/index.d.ts +12 -0
  60. package/lib/typescript/src/index.d.ts.map +1 -0
  61. package/lib/typescript/src/normalize.d.ts +43 -0
  62. package/lib/typescript/src/normalize.d.ts.map +1 -0
  63. package/lib/typescript/src/types.d.ts +247 -0
  64. package/lib/typescript/src/types.d.ts.map +1 -0
  65. package/lib/typescript/src/validation.d.ts +15 -0
  66. package/lib/typescript/src/validation.d.ts.map +1 -0
  67. package/package.json +196 -0
  68. package/plugin/build/index.js +48 -0
  69. package/src/NativeFrameCapture.ts +86 -0
  70. package/src/api.ts +189 -0
  71. package/src/constants.ts +69 -0
  72. package/src/errors.ts +21 -0
  73. package/src/events.ts +61 -0
  74. package/src/index.tsx +31 -0
  75. package/src/normalize.ts +81 -0
  76. package/src/types.ts +327 -0
  77. package/src/validation.ts +321 -0
package/src/types.ts ADDED
@@ -0,0 +1,327 @@
1
+ /**
2
+ * Type definitions for React Native Frame Capture
3
+ * @module types
4
+ */
5
+
6
+ // ============================================================================
7
+ // Enums
8
+ // ============================================================================
9
+
10
+ /**
11
+ * Capture state values
12
+ */
13
+ export enum CaptureState {
14
+ IDLE = 'idle',
15
+ CAPTURING = 'capturing',
16
+ PAUSED = 'paused',
17
+ STOPPING = 'stopping',
18
+ }
19
+
20
+ /**
21
+ * Permission status values
22
+ */
23
+ export enum PermissionStatus {
24
+ GRANTED = 'granted',
25
+ DENIED = 'denied',
26
+ NOT_DETERMINED = 'not_determined',
27
+ }
28
+
29
+ /**
30
+ * Error codes for capture operations
31
+ */
32
+ export enum CaptureErrorCode {
33
+ PERMISSION_DENIED = 'PERMISSION_DENIED',
34
+ ALREADY_CAPTURING = 'ALREADY_CAPTURING',
35
+ INVALID_OPTIONS = 'INVALID_OPTIONS',
36
+ STORAGE_ERROR = 'STORAGE_ERROR',
37
+ SYSTEM_ERROR = 'SYSTEM_ERROR',
38
+ NOT_SUPPORTED = 'NOT_SUPPORTED',
39
+ }
40
+
41
+ /**
42
+ * Event types emitted by the capture system
43
+ */
44
+ export enum CaptureEventType {
45
+ FRAME_CAPTURED = 'onFrameCaptured',
46
+ CAPTURE_ERROR = 'onCaptureError',
47
+ CAPTURE_STOP = 'onCaptureStop',
48
+ CAPTURE_START = 'onCaptureStart',
49
+ STORAGE_WARNING = 'onStorageWarning',
50
+ CAPTURE_PAUSE = 'onCapturePause',
51
+ CAPTURE_RESUME = 'onCaptureResume',
52
+ OVERLAY_ERROR = 'onOverlayError',
53
+ }
54
+
55
+ // ============================================================================
56
+ // Basic Types
57
+ // ============================================================================
58
+
59
+ /**
60
+ * Unit type for capture region coordinates
61
+ */
62
+ export type CaptureRegionUnit = 'pixels' | 'percentage';
63
+
64
+ /**
65
+ * Position preset for overlay placement
66
+ */
67
+ export type PositionPreset =
68
+ | 'top-left'
69
+ | 'top-right'
70
+ | 'bottom-left'
71
+ | 'bottom-right'
72
+ | 'center';
73
+
74
+ // ============================================================================
75
+ // Capture Configuration
76
+ // ============================================================================
77
+
78
+ /**
79
+ * Custom capture region specification
80
+ */
81
+ export interface CaptureRegion {
82
+ /** Left position (pixels or 0-1 for percentage) */
83
+ x: number;
84
+ /** Top position (pixels or 0-1 for percentage) */
85
+ y: number;
86
+ /** Width (pixels or 0-1 for percentage) */
87
+ width: number;
88
+ /** Height (pixels or 0-1 for percentage) */
89
+ height: number;
90
+ /** Unit type for coordinates (default: 'percentage') */
91
+ unit?: CaptureRegionUnit;
92
+ }
93
+
94
+ /**
95
+ * File naming configuration
96
+ */
97
+ export interface FileNamingConfig {
98
+ /** Filename prefix (default: "capture_") */
99
+ prefix?: string;
100
+ /** Date format pattern (default: "yyyyMMdd_HHmmss_SSS") */
101
+ dateFormat?: string;
102
+ /** Frame number padding digits (default: 5) */
103
+ framePadding?: number;
104
+ }
105
+
106
+ /**
107
+ * Notification customization options for the foreground service
108
+ */
109
+ export interface NotificationOptions {
110
+ title?: string;
111
+ description?: string;
112
+ icon?: string;
113
+ smallIcon?: string;
114
+ color?: string;
115
+ channelName?: string;
116
+ channelDescription?: string;
117
+ priority?: 'low' | 'default' | 'high';
118
+ showFrameCount?: boolean;
119
+ updateInterval?: number;
120
+ pausedTitle?: string;
121
+ pausedDescription?: string;
122
+ showStopAction?: boolean;
123
+ showPauseAction?: boolean;
124
+ showResumeAction?: boolean;
125
+ }
126
+
127
+ /**
128
+ * Capture behavior configuration
129
+ */
130
+ export interface CaptureConfig {
131
+ /** Milliseconds between captures (100-60000) */
132
+ interval: number;
133
+ }
134
+
135
+ /**
136
+ * Image processing configuration
137
+ */
138
+ export interface ImageConfig {
139
+ /** Image quality 0-100 */
140
+ quality: number;
141
+ /** Output format */
142
+ format: 'png' | 'jpeg';
143
+ /** Resolution scale factor 0.1-1.0 */
144
+ scaleResolution?: number;
145
+ /** Custom capture region */
146
+ region?: CaptureRegion;
147
+ /** Exclude status bar from capture */
148
+ excludeStatusBar?: boolean;
149
+ }
150
+
151
+ /**
152
+ * Storage configuration
153
+ */
154
+ export interface StorageOptions {
155
+ /** Whether to save captured frames to device storage */
156
+ saveFrames?: boolean;
157
+ /** Storage location for captured frames */
158
+ location?: 'private' | 'public';
159
+ /** Custom output directory */
160
+ outputDirectory?: string;
161
+ /** Storage warning threshold in bytes (default: 100MB, set to 0 to disable) */
162
+ warningThreshold?: number;
163
+ /** File naming configuration */
164
+ fileNaming?: FileNamingConfig;
165
+ }
166
+
167
+ /**
168
+ * Performance tuning configuration
169
+ */
170
+ export interface PerformanceOptions {
171
+ /** Overlay image cache size in bytes (default: 10MB) */
172
+ overlayCacheSize?: number;
173
+ /** ImageReader buffer count (default: 2) */
174
+ imageReaderBuffers?: number;
175
+ /** Executor shutdown timeout in milliseconds (default: 5000ms) */
176
+ executorShutdownTimeout?: number;
177
+ /** Forced executor shutdown timeout in milliseconds (default: 1000ms) */
178
+ executorForcedShutdownTimeout?: number;
179
+ }
180
+
181
+ /**
182
+ * Configuration options for screen capture
183
+ */
184
+ export interface CaptureOptions {
185
+ /** Capture behavior configuration */
186
+ capture: CaptureConfig;
187
+ /** Image processing configuration */
188
+ image: ImageConfig;
189
+ /** Storage configuration */
190
+ storage?: StorageOptions;
191
+ /** Performance tuning configuration */
192
+ performance?: PerformanceOptions;
193
+ /** Notification customization options */
194
+ notification?: NotificationOptions;
195
+ /** Array of overlays to render on captured frames */
196
+ overlays?: OverlayConfig[];
197
+ }
198
+
199
+ // ============================================================================
200
+ // Overlay Configuration
201
+ // ============================================================================
202
+
203
+ /**
204
+ * Custom position coordinates for overlay placement
205
+ */
206
+ export interface PositionCoordinates {
207
+ x: number;
208
+ y: number;
209
+ unit?: 'pixels' | 'percentage';
210
+ }
211
+
212
+ /**
213
+ * Overlay position - either a preset string or custom coordinates
214
+ */
215
+ export type OverlayPosition = PositionPreset | PositionCoordinates;
216
+
217
+ /**
218
+ * Text overlay style configuration
219
+ */
220
+ export interface TextStyle {
221
+ fontSize?: number;
222
+ color?: string;
223
+ backgroundColor?: string;
224
+ padding?: number;
225
+ fontWeight?: 'normal' | 'bold';
226
+ textAlign?: 'left' | 'center' | 'right';
227
+ }
228
+
229
+ /**
230
+ * Text overlay configuration
231
+ */
232
+ export interface TextOverlay {
233
+ type: 'text';
234
+ content: string;
235
+ position: OverlayPosition;
236
+ style?: TextStyle;
237
+ }
238
+
239
+ /**
240
+ * Image size configuration
241
+ */
242
+ export interface ImageSize {
243
+ width: number;
244
+ height: number;
245
+ }
246
+
247
+ /**
248
+ * Image overlay configuration
249
+ */
250
+ export interface ImageOverlay {
251
+ type: 'image';
252
+ source: string;
253
+ position: OverlayPosition;
254
+ size?: ImageSize;
255
+ opacity?: number;
256
+ }
257
+
258
+ /**
259
+ * Union type for all overlay configurations
260
+ */
261
+ export type OverlayConfig = TextOverlay | ImageOverlay;
262
+
263
+ // ============================================================================
264
+ // Session and Status
265
+ // ============================================================================
266
+
267
+ /**
268
+ * Information about an active capture session
269
+ */
270
+ export interface CaptureSession {
271
+ id: string;
272
+ startTime: number;
273
+ frameCount: number;
274
+ options: CaptureOptions;
275
+ }
276
+
277
+ /**
278
+ * Current capture status
279
+ */
280
+ export interface CaptureStatus {
281
+ state: CaptureState;
282
+ session: CaptureSession | null;
283
+ isPaused: boolean;
284
+ }
285
+
286
+ // ============================================================================
287
+ // Event Interfaces
288
+ // ============================================================================
289
+
290
+ /**
291
+ * Re-export event types from NativeFrameCapture for consistency
292
+ * These are defined in NativeFrameCapture.ts for codegen compatibility
293
+ */
294
+ import type {
295
+ FrameCapturedEvent,
296
+ CaptureErrorEvent,
297
+ CaptureStopEvent,
298
+ CaptureStartEvent,
299
+ StorageWarningEvent,
300
+ CapturePauseEvent,
301
+ CaptureResumeEvent,
302
+ OverlayErrorEvent,
303
+ } from './NativeFrameCapture';
304
+
305
+ export type {
306
+ FrameCapturedEvent,
307
+ CaptureErrorEvent,
308
+ CaptureStopEvent,
309
+ CaptureStartEvent,
310
+ StorageWarningEvent,
311
+ CapturePauseEvent,
312
+ CaptureResumeEvent,
313
+ OverlayErrorEvent,
314
+ };
315
+
316
+ /**
317
+ * Union type for all event callbacks
318
+ */
319
+ export type CaptureEventCallback =
320
+ | ((event: FrameCapturedEvent) => void)
321
+ | ((event: CaptureErrorEvent) => void)
322
+ | ((event: CaptureStopEvent) => void)
323
+ | ((event: CaptureStartEvent) => void)
324
+ | ((event: StorageWarningEvent) => void)
325
+ | ((event: CapturePauseEvent) => void)
326
+ | ((event: CaptureResumeEvent) => void)
327
+ | ((event: OverlayErrorEvent) => void);
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Input validation utilities
3
+ * @module validation
4
+ */
5
+
6
+ import { CaptureErrorCode } from './types';
7
+ import { CaptureError } from './errors';
8
+ import {
9
+ MIN_INTERVAL,
10
+ MAX_INTERVAL,
11
+ MIN_QUALITY,
12
+ MAX_QUALITY,
13
+ } from './constants';
14
+ import type { CaptureOptions } from './types';
15
+
16
+ /**
17
+ * Validates capture options
18
+ * @throws {CaptureError} if validation fails
19
+ */
20
+ export function validateOptions(options: Partial<CaptureOptions>): void {
21
+ // Validate capture config
22
+ if (options.capture) {
23
+ const { interval } = options.capture;
24
+
25
+ if (interval !== undefined) {
26
+ if (
27
+ typeof interval !== 'number' ||
28
+ interval < MIN_INTERVAL ||
29
+ interval > MAX_INTERVAL
30
+ ) {
31
+ throw new CaptureError(
32
+ CaptureErrorCode.INVALID_OPTIONS,
33
+ `capture.interval must be a number between ${MIN_INTERVAL} and ${MAX_INTERVAL} milliseconds`
34
+ );
35
+ }
36
+ }
37
+ }
38
+
39
+ // Validate image config
40
+ if (options.image) {
41
+ const { quality, format, scaleResolution, region, excludeStatusBar } =
42
+ options.image;
43
+
44
+ if (quality !== undefined) {
45
+ if (
46
+ typeof quality !== 'number' ||
47
+ quality < MIN_QUALITY ||
48
+ quality > MAX_QUALITY
49
+ ) {
50
+ throw new CaptureError(
51
+ CaptureErrorCode.INVALID_OPTIONS,
52
+ `image.quality must be a number between ${MIN_QUALITY} and ${MAX_QUALITY}`
53
+ );
54
+ }
55
+ }
56
+
57
+ if (format !== undefined) {
58
+ if (format !== 'png' && format !== 'jpeg') {
59
+ throw new CaptureError(
60
+ CaptureErrorCode.INVALID_OPTIONS,
61
+ `image.format must be either 'png' or 'jpeg'`
62
+ );
63
+ }
64
+ }
65
+
66
+ if (scaleResolution !== undefined) {
67
+ if (
68
+ typeof scaleResolution !== 'number' ||
69
+ scaleResolution < 0.1 ||
70
+ scaleResolution > 1.0
71
+ ) {
72
+ throw new CaptureError(
73
+ CaptureErrorCode.INVALID_OPTIONS,
74
+ `image.scaleResolution must be a number between 0.1 and 1.0`
75
+ );
76
+ }
77
+ }
78
+
79
+ if (region !== undefined) {
80
+ validateCaptureRegion(region);
81
+ }
82
+
83
+ if (
84
+ excludeStatusBar !== undefined &&
85
+ typeof excludeStatusBar !== 'boolean'
86
+ ) {
87
+ throw new CaptureError(
88
+ CaptureErrorCode.INVALID_OPTIONS,
89
+ `image.excludeStatusBar must be a boolean`
90
+ );
91
+ }
92
+ }
93
+
94
+ // Validate storage config
95
+ if (options.storage) {
96
+ const {
97
+ saveFrames,
98
+ location,
99
+ outputDirectory,
100
+ warningThreshold,
101
+ fileNaming,
102
+ } = options.storage;
103
+
104
+ if (saveFrames !== undefined && typeof saveFrames !== 'boolean') {
105
+ throw new CaptureError(
106
+ CaptureErrorCode.INVALID_OPTIONS,
107
+ `storage.saveFrames must be a boolean`
108
+ );
109
+ }
110
+
111
+ if (location !== undefined) {
112
+ if (location !== 'private' && location !== 'public') {
113
+ throw new CaptureError(
114
+ CaptureErrorCode.INVALID_OPTIONS,
115
+ `storage.location must be either 'private' or 'public'`
116
+ );
117
+ }
118
+ }
119
+
120
+ if (outputDirectory !== undefined) {
121
+ if (
122
+ typeof outputDirectory !== 'string' ||
123
+ outputDirectory.trim() === ''
124
+ ) {
125
+ throw new CaptureError(
126
+ CaptureErrorCode.INVALID_OPTIONS,
127
+ `storage.outputDirectory must be a non-empty string`
128
+ );
129
+ }
130
+ }
131
+
132
+ if (warningThreshold !== undefined) {
133
+ if (typeof warningThreshold !== 'number' || warningThreshold < 0) {
134
+ throw new CaptureError(
135
+ CaptureErrorCode.INVALID_OPTIONS,
136
+ `storage.warningThreshold must be a non-negative number (bytes)`
137
+ );
138
+ }
139
+ }
140
+
141
+ if (fileNaming) {
142
+ validateFileNaming(fileNaming);
143
+ }
144
+ }
145
+
146
+ // Validate performance config
147
+ if (options.performance) {
148
+ validatePerformance(options.performance);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Validates file naming configuration
154
+ */
155
+ function validateFileNaming(fn: any): void {
156
+ if (fn.prefix !== undefined) {
157
+ if (typeof fn.prefix !== 'string' || fn.prefix.trim() === '') {
158
+ throw new CaptureError(
159
+ CaptureErrorCode.INVALID_OPTIONS,
160
+ `storage.fileNaming.prefix must be a non-empty string`
161
+ );
162
+ }
163
+ if (/[<>:"/\\|?*]/.test(fn.prefix)) {
164
+ throw new CaptureError(
165
+ CaptureErrorCode.INVALID_OPTIONS,
166
+ `storage.fileNaming.prefix contains invalid filename characters`
167
+ );
168
+ }
169
+ }
170
+
171
+ if (fn.dateFormat !== undefined) {
172
+ if (typeof fn.dateFormat !== 'string' || fn.dateFormat.trim() === '') {
173
+ throw new CaptureError(
174
+ CaptureErrorCode.INVALID_OPTIONS,
175
+ `storage.fileNaming.dateFormat must be a non-empty string`
176
+ );
177
+ }
178
+ }
179
+
180
+ if (fn.framePadding !== undefined) {
181
+ if (
182
+ typeof fn.framePadding !== 'number' ||
183
+ fn.framePadding < 1 ||
184
+ fn.framePadding > 10
185
+ ) {
186
+ throw new CaptureError(
187
+ CaptureErrorCode.INVALID_OPTIONS,
188
+ `storage.fileNaming.framePadding must be between 1 and 10`
189
+ );
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Validates performance configuration
196
+ */
197
+ function validatePerformance(perf: any): void {
198
+ if (perf.overlayCacheSize !== undefined) {
199
+ if (
200
+ typeof perf.overlayCacheSize !== 'number' ||
201
+ perf.overlayCacheSize < 0
202
+ ) {
203
+ throw new CaptureError(
204
+ CaptureErrorCode.INVALID_OPTIONS,
205
+ `performance.overlayCacheSize must be a non-negative number (bytes)`
206
+ );
207
+ }
208
+ }
209
+
210
+ if (perf.imageReaderBuffers !== undefined) {
211
+ if (
212
+ typeof perf.imageReaderBuffers !== 'number' ||
213
+ perf.imageReaderBuffers < 1 ||
214
+ perf.imageReaderBuffers > 10
215
+ ) {
216
+ throw new CaptureError(
217
+ CaptureErrorCode.INVALID_OPTIONS,
218
+ `performance.imageReaderBuffers must be between 1 and 10`
219
+ );
220
+ }
221
+ }
222
+
223
+ if (perf.executorShutdownTimeout !== undefined) {
224
+ if (
225
+ typeof perf.executorShutdownTimeout !== 'number' ||
226
+ perf.executorShutdownTimeout < 100 ||
227
+ perf.executorShutdownTimeout > 30000
228
+ ) {
229
+ throw new CaptureError(
230
+ CaptureErrorCode.INVALID_OPTIONS,
231
+ `performance.executorShutdownTimeout must be between 100 and 30000ms`
232
+ );
233
+ }
234
+ }
235
+
236
+ if (perf.executorForcedShutdownTimeout !== undefined) {
237
+ if (
238
+ typeof perf.executorForcedShutdownTimeout !== 'number' ||
239
+ perf.executorForcedShutdownTimeout < 100 ||
240
+ perf.executorForcedShutdownTimeout > 10000
241
+ ) {
242
+ throw new CaptureError(
243
+ CaptureErrorCode.INVALID_OPTIONS,
244
+ `performance.executorForcedShutdownTimeout must be between 100 and 10000ms`
245
+ );
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Validates capture region configuration
252
+ */
253
+ function validateCaptureRegion(region: any): void {
254
+ const unit = region.unit || 'percentage';
255
+
256
+ if (
257
+ typeof region.x !== 'number' ||
258
+ typeof region.y !== 'number' ||
259
+ typeof region.width !== 'number' ||
260
+ typeof region.height !== 'number'
261
+ ) {
262
+ throw new CaptureError(
263
+ CaptureErrorCode.INVALID_OPTIONS,
264
+ `image.region coordinates must be numbers`
265
+ );
266
+ }
267
+
268
+ if (unit === 'percentage') {
269
+ if (
270
+ region.x < 0 ||
271
+ region.x > 1 ||
272
+ region.y < 0 ||
273
+ region.y > 1 ||
274
+ region.width < 0 ||
275
+ region.width > 1 ||
276
+ region.height < 0 ||
277
+ region.height > 1
278
+ ) {
279
+ throw new CaptureError(
280
+ CaptureErrorCode.INVALID_OPTIONS,
281
+ `image.region percentage values must be between 0 and 1`
282
+ );
283
+ }
284
+ if (region.x + region.width > 1 || region.y + region.height > 1) {
285
+ throw new CaptureError(
286
+ CaptureErrorCode.INVALID_OPTIONS,
287
+ `image.region extends beyond screen bounds (x + width or y + height > 1)`
288
+ );
289
+ }
290
+ } else if (unit === 'pixels') {
291
+ if (region.width <= 0 || region.height <= 0) {
292
+ throw new CaptureError(
293
+ CaptureErrorCode.INVALID_OPTIONS,
294
+ `image.region width and height must be positive`
295
+ );
296
+ }
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Merges user options with defaults
302
+ */
303
+ export function mergeOptions(
304
+ options?: Partial<CaptureOptions>
305
+ ): CaptureOptions {
306
+ return {
307
+ capture: {
308
+ interval: 1000,
309
+ ...options?.capture,
310
+ },
311
+ image: {
312
+ quality: 80,
313
+ format: 'jpeg',
314
+ ...options?.image,
315
+ },
316
+ storage: options?.storage,
317
+ performance: options?.performance,
318
+ notification: options?.notification,
319
+ overlays: options?.overlays,
320
+ };
321
+ }