@octane-xplat/camera 0.12.0 → 0.14.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.
Files changed (83) hide show
  1. package/README.md +63 -3
  2. package/dist/linux/CameraView.web.tsrx.js +337 -0
  3. package/dist/linux/host-movie.web.js +100 -0
  4. package/dist/linux/index.js +5 -0
  5. package/dist/linux/layout-child.web.js +29 -0
  6. package/dist/linux/movie-metadata.web.js +65 -0
  7. package/dist/linux/octane-client-build.json +8 -0
  8. package/dist/linux/session-backend.linux.js +479 -0
  9. package/dist/linux/session-core.js +368 -0
  10. package/dist/linux/session.js +29 -0
  11. package/dist/linux/synthetic-session.js +280 -0
  12. package/dist/linux/types.js +19 -0
  13. package/dist/native/CameraView.android.js +25 -1
  14. package/dist/native/CameraView.ios.js +24 -1
  15. package/dist/native/CameraView.windows.js +173 -0
  16. package/dist/native/camera-permission.windows.js +43 -0
  17. package/dist/native/camera-session.windows.js +78 -0
  18. package/dist/native/cx.mobile.js +3 -2
  19. package/dist/native/cx.windows.js +9 -0
  20. package/dist/native/index.js +4 -1
  21. package/dist/native/layout-child.windows.js +53 -0
  22. package/dist/native/octane-client-build.json +1 -1
  23. package/dist/native/session-backend.android.js +234 -0
  24. package/dist/native/session-backend.ios.js +682 -0
  25. package/dist/native/session-backend.js +55 -0
  26. package/dist/native/session-backend.windows.js +905 -0
  27. package/dist/native/session-core.js +368 -0
  28. package/dist/native/session.js +29 -0
  29. package/dist/native/synthetic-session.js +280 -0
  30. package/dist/native/types.js +19 -0
  31. package/dist/web/CameraView.web.tsrx.js +28 -8
  32. package/dist/web/host-movie.web.js +100 -0
  33. package/dist/web/index.js +4 -1
  34. package/dist/web/movie-metadata.web.js +65 -0
  35. package/dist/web/movie-storage.web.js +119 -0
  36. package/dist/web/octane-client-build.json +1 -1
  37. package/dist/web/session-backend.web.js +490 -0
  38. package/dist/web/session-core.js +368 -0
  39. package/dist/web/session.js +29 -0
  40. package/dist/web/synthetic-session.js +280 -0
  41. package/dist/web/types.js +19 -0
  42. package/package.json +23 -5
  43. package/platforms/android/AndroidManifest.xml +2 -0
  44. package/platforms/android/include.gradle +1 -0
  45. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +4 -0
  46. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +363 -0
  47. package/platforms/ios/Info.plist +2 -0
  48. package/platforms/macos/src/XplatCameraHost.swift +513 -0
  49. package/src/CameraView.android.tsrx +21 -1
  50. package/src/CameraView.ios.tsrx +28 -2
  51. package/src/CameraView.macos.tsrx +93 -0
  52. package/src/CameraView.web.tsrx +20 -2
  53. package/src/CameraView.windows.tsrx +124 -0
  54. package/src/camera-permission.windows.ts +76 -0
  55. package/src/camera-session.macos.ts +75 -0
  56. package/src/camera-session.windows.ts +123 -0
  57. package/src/cx.macos.ts +5 -0
  58. package/src/cx.mobile.ts +3 -2
  59. package/src/cx.windows.ts +6 -0
  60. package/src/host-movie.web.ts +180 -0
  61. package/src/index.ts +31 -0
  62. package/src/index.web.ts +31 -0
  63. package/src/layout-child.windows.ts +79 -0
  64. package/src/movie-metadata.web.ts +84 -0
  65. package/src/movie-storage.web.ts +164 -0
  66. package/src/props.ts +7 -1
  67. package/src/session-backend.android.ts +397 -0
  68. package/src/session-backend.ios.ts +1092 -0
  69. package/src/session-backend.linux.ts +869 -0
  70. package/src/session-backend.macos.ts +624 -0
  71. package/src/session-backend.ts +57 -0
  72. package/src/session-backend.web.ts +874 -0
  73. package/src/session-backend.windows.ts +1517 -0
  74. package/src/session-core.ts +659 -0
  75. package/src/session.ts +37 -0
  76. package/src/synthetic-session.ts +367 -0
  77. package/src/types.ts +329 -0
  78. package/types/index.d.ts +43 -0
  79. package/types/props.d.ts +7 -1
  80. package/types/session-core.d.ts +129 -0
  81. package/types/synthetic-session.d.ts +48 -0
  82. package/types/types.d.ts +263 -0
  83. package/src/CameraView.windows.ts +0 -5
@@ -0,0 +1,682 @@
1
+ import { CameraCaptureError } from "./types.js";
2
+ import { File, knownFolders, path } from "@nativescript/core";
3
+ //#region src/session-backend.ios.ts
4
+ var authorizationNotDetermined = 0;
5
+ var authorizationRestricted = 1;
6
+ var authorizationDenied = 2;
7
+ var authorizationAuthorized = 3;
8
+ var captureDevicePositionUnspecified = 0;
9
+ var captureDevicePositionBack = 1;
10
+ var captureDevicePositionFront = 2;
11
+ var videoOrientationPortrait = 1;
12
+ var videoOrientationPortraitUpsideDown = 2;
13
+ var videoOrientationLandscapeRight = 3;
14
+ var videoOrientationLandscapeLeft = 4;
15
+ var interruptionVideoDeviceNotAvailableInBackground = 1;
16
+ var interruptionAudioDeviceInUseByAnotherClient = 2;
17
+ var interruptionVideoDeviceInUseByAnotherClient = 3;
18
+ var interruptionVideoDeviceNotAvailableWithMultipleForegroundApps = 4;
19
+ var interruptionVideoDeviceNotAvailableDueToSystemPressure = 5;
20
+ var avErrorMaximumDurationReached = -11810;
21
+ var mediaTypeFor = (kind) => kind === "camera" ? AVMediaTypeVideo : AVMediaTypeAudio;
22
+ var usageKeyFor = (kind) => kind === "camera" ? "NSCameraUsageDescription" : "NSMicrophoneUsageDescription";
23
+ var readPermission = (kind) => {
24
+ switch (AVCaptureDevice.authorizationStatusForMediaType(mediaTypeFor(kind))) {
25
+ case authorizationAuthorized: return "granted";
26
+ case authorizationDenied: return "denied";
27
+ case authorizationRestricted: return "restricted";
28
+ case authorizationNotDetermined: return "notDetermined";
29
+ default: return "unknown";
30
+ }
31
+ };
32
+ var requestAccess = (kind) => {
33
+ if (!NSBundle.mainBundle.objectForInfoDictionaryKey(usageKeyFor(kind))) return Promise.reject(new CameraCaptureError({
34
+ kind: "configurationMissing",
35
+ operation: "requestPermission",
36
+ permission: kind,
37
+ message: `Missing ${usageKeyFor(kind)} in the host app's Info.plist`
38
+ }));
39
+ const status = readPermission(kind);
40
+ if (status !== "notDetermined") return Promise.resolve(status);
41
+ return new Promise((resolve) => {
42
+ AVCaptureDevice.requestAccessForMediaTypeCompletionHandler(mediaTypeFor(kind), () => resolve(readPermission(kind)));
43
+ });
44
+ };
45
+ var facingForPosition = (position) => position === captureDevicePositionFront ? "front" : position === captureDevicePositionBack ? "back" : void 0;
46
+ var listCameras = () => {
47
+ const types = [
48
+ AVCaptureDeviceTypeBuiltInWideAngleCamera,
49
+ AVCaptureDeviceTypeBuiltInUltraWideCamera,
50
+ AVCaptureDeviceTypeBuiltInTelephotoCamera,
51
+ AVCaptureDeviceTypeBuiltInDualCamera,
52
+ AVCaptureDeviceTypeBuiltInDualWideCamera,
53
+ AVCaptureDeviceTypeBuiltInTripleCamera,
54
+ AVCaptureDeviceTypeBuiltInTrueDepthCamera
55
+ ].filter((entry) => typeof entry === "string");
56
+ const discovery = AVCaptureDeviceDiscoverySession.discoverySessionWithDeviceTypesMediaTypePosition(types, AVMediaTypeVideo, captureDevicePositionUnspecified);
57
+ const devices = [];
58
+ const found = discovery?.devices;
59
+ const count = found?.count ?? 0;
60
+ for (let index = 0; index < count; index++) devices.push(found.objectAtIndex(index));
61
+ if (!devices.length) {
62
+ const legacy = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo);
63
+ for (let index = 0; index < (legacy?.count ?? 0); index++) devices.push(legacy.objectAtIndex(index));
64
+ }
65
+ return devices;
66
+ };
67
+ var selectDevice = (camera, devices) => {
68
+ if (camera && typeof camera === "object" && "deviceId" in camera) return devices.find((device) => device.uniqueID === camera.deviceId);
69
+ if (camera === "front" || camera === "back") {
70
+ const position = camera === "front" ? captureDevicePositionFront : captureDevicePositionBack;
71
+ return devices.find((device) => device.position === position);
72
+ }
73
+ return devices.find((device) => device.position === captureDevicePositionBack) ?? devices[0];
74
+ };
75
+ var orientationToRotationAngle = (orientation) => orientation === "portrait" ? 90 : orientation === "portraitUpsideDown" ? 270 : orientation === "landscapeLeft" ? 180 : orientation === "landscapeRight" ? 0 : void 0;
76
+ var orientationToLegacyConstant = (orientation) => orientation === "portrait" ? videoOrientationPortrait : orientation === "portraitUpsideDown" ? videoOrientationPortraitUpsideDown : orientation === "landscapeLeft" ? videoOrientationLandscapeLeft : orientation === "landscapeRight" ? videoOrientationLandscapeRight : void 0;
77
+ /** Derive the cardinal capture orientation from a video track's
78
+ * preferred transform; `undefined` when it cannot be told honestly. */
79
+ var orientationFromTransform = (transform) => {
80
+ const { a, b, c, d } = transform;
81
+ if (a === 0 && b === 1 && c === -1 && d === 0) return "portrait";
82
+ if (a === 0 && b === -1 && c === 1 && d === 0) return "portraitUpsideDown";
83
+ if (a === 1 && b === 0 && c === 0 && d === 1) return "landscapeRight";
84
+ if (a === -1 && b === 0 && c === 0 && d === -1) return "landscapeLeft";
85
+ return "unspecified";
86
+ };
87
+ var mapInterruptionReason = (raw) => {
88
+ switch (raw) {
89
+ case interruptionVideoDeviceNotAvailableInBackground: return "background";
90
+ case interruptionAudioDeviceInUseByAnotherClient: return "audioInUse";
91
+ case interruptionVideoDeviceInUseByAnotherClient: return "cameraInUse";
92
+ case interruptionVideoDeviceNotAvailableWithMultipleForegroundApps: return "multitasking";
93
+ case interruptionVideoDeviceNotAvailableDueToSystemPressure: return "systemPressure";
94
+ default: return "unknown";
95
+ }
96
+ };
97
+ var recordingsDirectory = () => {
98
+ const directory = path.join(knownFolders.documents().path, "octane-camera");
99
+ if (!File.exists(directory)) NSFileManager.defaultManager.createDirectoryAtPathWithIntermediateDirectoriesAttributesError(directory, true, null, void 0);
100
+ return directory;
101
+ };
102
+ var hasUsageDeclaration = (kind) => Boolean(NSBundle.mainBundle.objectForInfoDictionaryKey(usageKeyFor(kind)));
103
+ var minFreeDiskBytes = 67108864;
104
+ var freeDiskBytes = () => {
105
+ try {
106
+ const free = NSFileManager.defaultManager.attributesOfFileSystemForPathError(knownFolders.documents().path, void 0)?.objectForKey(NSFileSystemFreeSize);
107
+ return free ? Number(free.longLongValue ?? free.doubleValue ?? 0) : Number.MAX_SAFE_INTEGER;
108
+ } catch {
109
+ return Number.MAX_SAFE_INTEGER;
110
+ }
111
+ };
112
+ /** iOS adapter: one AVCaptureSession shared between the attached preview
113
+ * and a single AVCaptureMovieFileOutput capture attempt. */
114
+ function createSessionBackend(config) {
115
+ let currentConfig = { ...config };
116
+ let session;
117
+ let videoInput;
118
+ let audioInput;
119
+ let movieOutput;
120
+ let previewLayer;
121
+ let previewHost;
122
+ let detachFrame;
123
+ let previewReadyFlag = false;
124
+ let device;
125
+ let observer;
126
+ let observers = [];
127
+ let captureSink;
128
+ let captureRequest;
129
+ let limitRequested = false;
130
+ let noDurationLimit;
131
+ let interrupted = false;
132
+ const recordingDelegate = NSObject.extend({
133
+ captureOutputDidStartRecordingToOutputFileAtURLFromConnections(_output, _url, _connections) {
134
+ onCaptureStarted();
135
+ },
136
+ captureOutputDidFinishRecordingToOutputFileAtURLFromConnectionsError(_output, url, _connections, error) {
137
+ onCaptureFinished(url, error);
138
+ }
139
+ }, { protocols: [AVCaptureFileOutputRecordingDelegate] }).new();
140
+ const emitIssue = (kind, operation, message, cause) => ({
141
+ kind,
142
+ operation,
143
+ message,
144
+ cause
145
+ });
146
+ const notify = (phase, reason) => {
147
+ interrupted = phase === "began";
148
+ observer?.interruption(phase, reason);
149
+ observer?.previewReady(previewReadyFlag && !interrupted);
150
+ };
151
+ const observeSession = (target) => {
152
+ const center = NSNotificationCenter.defaultCenter;
153
+ const add = (name, object, handler) => {
154
+ observers.push(center.addObserverForNameObjectQueueUsingBlock(name, object, NSOperationQueue.mainQueue, handler));
155
+ };
156
+ add(AVCaptureSessionWasInterruptedNotification, target, (note) => {
157
+ const raw = note.userInfo?.objectForKey(AVCaptureSessionInterruptionReasonKey);
158
+ notify("began", mapInterruptionReason(raw ? Number(raw.integerValue) : void 0));
159
+ });
160
+ add(AVCaptureSessionInterruptionEndedNotification, target, () => {
161
+ notify("ended");
162
+ });
163
+ add(AVCaptureSessionRuntimeErrorNotification, target, () => {
164
+ observer?.availability(false);
165
+ });
166
+ add(AVCaptureDeviceWasDisconnectedNotification, device, () => {
167
+ observer?.availability(false);
168
+ });
169
+ add(AVCaptureDeviceWasConnectedNotification, null, () => {
170
+ observer?.availability(true);
171
+ });
172
+ add(UIApplicationDidEnterBackgroundNotification, null, () => {
173
+ notify("began", "background");
174
+ });
175
+ add(UIApplicationWillEnterForegroundNotification, null, () => {
176
+ notify("ended", "background");
177
+ });
178
+ };
179
+ const clearObservers = () => {
180
+ const center = NSNotificationCenter.defaultCenter;
181
+ for (const entry of observers) center.removeObserver(entry);
182
+ observers = [];
183
+ };
184
+ const supportedPreset = (target, captureDevice) => target.canSetSessionPreset(AVCaptureSessionPreset1280x720) && captureDevice.supportsAVCaptureSessionPreset(AVCaptureSessionPreset1280x720);
185
+ /** Resolve the effective standard profile for the current camera:
186
+ * 720p/30 when the device supports it, otherwise the device's own
187
+ * active format disclosed honestly. */
188
+ const standardProfile = (captureDevice) => {
189
+ if (captureDevice && session && supportedPreset(session, captureDevice)) return {
190
+ width: 1280,
191
+ height: 720,
192
+ frameRate: 30
193
+ };
194
+ const format = captureDevice?.activeFormat;
195
+ const dimensions = format ? CMVideoFormatDescriptionGetDimensions(format.formatDescription) : void 0;
196
+ const range = captureDevice?.activeVideoMinFrameDuration;
197
+ const frameRate = range && range.timescale > 0 && range.value > 0 ? Math.round(range.timescale / range.value) : 30;
198
+ return {
199
+ width: dimensions?.width ?? 1280,
200
+ height: dimensions?.height ?? 720,
201
+ frameRate
202
+ };
203
+ };
204
+ const ensureAudioInput = (target) => {
205
+ if (audioInput) return true;
206
+ const mic = AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio);
207
+ if (!mic) return false;
208
+ const input = AVCaptureDeviceInput.deviceInputWithDeviceError(mic, void 0);
209
+ if (!input || !target.canAddInput(input)) return false;
210
+ target.addInput(input);
211
+ audioInput = input;
212
+ return true;
213
+ };
214
+ const ensureSession = () => {
215
+ if (session) return session;
216
+ const target = AVCaptureSession.new();
217
+ target.beginConfiguration();
218
+ device = selectDevice(currentConfig.camera, listCameras());
219
+ if (!device) {
220
+ target.commitConfiguration();
221
+ throw new CameraCaptureError({
222
+ kind: "unavailable",
223
+ operation: "startPreview",
224
+ message: "No camera device is available"
225
+ });
226
+ }
227
+ if (supportedPreset(target, device)) target.sessionPreset = AVCaptureSessionPreset1280x720;
228
+ const input = AVCaptureDeviceInput.deviceInputWithDeviceError(device, void 0);
229
+ if (!input || !target.canAddInput(input)) {
230
+ target.commitConfiguration();
231
+ throw new CameraCaptureError({
232
+ kind: "unavailable",
233
+ operation: "startPreview",
234
+ message: "Camera input is unavailable"
235
+ });
236
+ }
237
+ target.addInput(input);
238
+ videoInput = input;
239
+ const output = AVCaptureMovieFileOutput.new();
240
+ if (target.canAddOutput(output)) {
241
+ target.addOutput(output);
242
+ movieOutput = output;
243
+ noDurationLimit = output.maxRecordedDuration;
244
+ } else {
245
+ target.commitConfiguration();
246
+ throw new CameraCaptureError({
247
+ kind: "unsupportedPlatform",
248
+ operation: "startPreview",
249
+ message: "Movie file output is unavailable on this device"
250
+ });
251
+ }
252
+ if (currentConfig.audio && readPermission("microphone") === "granted") ensureAudioInput(target);
253
+ target.commitConfiguration();
254
+ session = target;
255
+ observeSession(target);
256
+ return target;
257
+ };
258
+ const stopSessionIfIdle = () => {
259
+ if (!session || captureSink || previewHost) return;
260
+ session.stopRunning();
261
+ clearObservers();
262
+ session = void 0;
263
+ videoInput = void 0;
264
+ audioInput = void 0;
265
+ movieOutput = void 0;
266
+ previewLayer = void 0;
267
+ device = void 0;
268
+ };
269
+ const onCaptureStarted = () => {
270
+ const request = captureRequest;
271
+ const sink = captureSink;
272
+ if (!request || !sink || !device) return;
273
+ const profile = standardProfile(device);
274
+ const configuration = {
275
+ cameraId: device.uniqueID,
276
+ facing: facingForPosition(device.position),
277
+ audio: request.audio && audioInput !== void 0,
278
+ width: profile.width,
279
+ height: profile.height,
280
+ frameRate: profile.frameRate,
281
+ orientation: request.orientation ?? "unspecified",
282
+ mimeType: "video/quicktime",
283
+ container: "mov"
284
+ };
285
+ sink.started(configuration);
286
+ };
287
+ const onCaptureFinished = (url, error) => {
288
+ const request = captureRequest;
289
+ const sink = captureSink;
290
+ captureRequest = void 0;
291
+ captureSink = void 0;
292
+ if (!request || !sink) return;
293
+ const filePath = url?.path ? String(url.path) : "";
294
+ if (!(!error || error.userInfo?.objectForKey(AVErrorRecordingSuccessfullyFinishedKey)?.boolValue === true)) {
295
+ try {
296
+ if (filePath && File.exists(filePath)) NSFileManager.defaultManager.removeItemAtPathError(filePath, void 0);
297
+ } catch {}
298
+ sink.settled({
299
+ kind: "failed",
300
+ stage: "capture",
301
+ issue: emitIssue("captureFailed", "finalize", error?.localizedDescription ?? "Movie recording did not complete", error)
302
+ });
303
+ stopSessionIfIdle();
304
+ return;
305
+ }
306
+ const limitReached = limitRequested && error?.code === avErrorMaximumDurationReached;
307
+ try {
308
+ const asset = AVURLAsset.assetWithURL(url);
309
+ const durationSeconds = asset ? CMTimeGetSeconds(asset.duration) : 0;
310
+ const videoTracks = asset?.tracksWithMediaType(AVMediaTypeVideo);
311
+ const audioTracks = asset?.tracksWithMediaType(AVMediaTypeAudio);
312
+ const track = videoTracks && videoTracks.count > 0 ? videoTracks.objectAtIndex(0) : null;
313
+ if (!track || !(durationSeconds > 0)) {
314
+ try {
315
+ if (filePath && File.exists(filePath)) NSFileManager.defaultManager.removeItemAtPathError(filePath, void 0);
316
+ } catch {}
317
+ sink.settled({
318
+ kind: "failed",
319
+ stage: "capture",
320
+ issue: emitIssue("noMedia", "finalize", "The finalized movie contains no playable media")
321
+ });
322
+ stopSessionIfIdle();
323
+ return;
324
+ }
325
+ const naturalSize = track.naturalSize;
326
+ const transform = track.preferredTransform;
327
+ const orientation = request.orientation && request.orientation !== "unspecified" ? request.orientation : orientationFromTransform(transform);
328
+ const rotated = Math.abs(transform.b) === 1 && Math.abs(transform.c) === 1;
329
+ const width = rotated ? naturalSize.height : naturalSize.width;
330
+ const height = rotated ? naturalSize.width : naturalSize.height;
331
+ if (request.audio && !(audioTracks && audioTracks.count > 0)) {
332
+ try {
333
+ if (filePath && File.exists(filePath)) NSFileManager.defaultManager.removeItemAtPathError(filePath, void 0);
334
+ } catch {}
335
+ sink.settled({
336
+ kind: "failed",
337
+ stage: "capture",
338
+ issue: emitIssue("captureFailed", "finalize", "Requested audio track is missing from the finalized movie")
339
+ });
340
+ stopSessionIfIdle();
341
+ return;
342
+ }
343
+ sink.settled({
344
+ kind: "clip",
345
+ output: {
346
+ kind: "nativeFile",
347
+ resourceId: `file:${filePath}`,
348
+ fileUrl: `file://${filePath}`
349
+ },
350
+ mimeType: "video/quicktime",
351
+ durationMs: Math.round(durationSeconds * 1e3),
352
+ width,
353
+ height,
354
+ orientation,
355
+ hasAudio: request.audio && (audioTracks?.count ?? 0) > 0,
356
+ limitReached
357
+ });
358
+ } catch (cause) {
359
+ sink.settled({
360
+ kind: "failed",
361
+ stage: "finalization",
362
+ issue: emitIssue("finalizationFailed", "finalize", cause instanceof Error ? cause.message : String(cause), cause)
363
+ });
364
+ }
365
+ stopSessionIfIdle();
366
+ };
367
+ return {
368
+ platform: "ios",
369
+ supported: typeof AVCaptureMovieFileOutput !== "undefined",
370
+ checkPermission: async (kind) => {
371
+ if (!hasUsageDeclaration(kind)) return "unknown";
372
+ return readPermission(kind);
373
+ },
374
+ requestPermission: (kind) => requestAccess(kind),
375
+ getCapabilities: async () => {
376
+ const devices = listCameras();
377
+ const cameras = devices.map((entry) => ({
378
+ id: entry.uniqueID,
379
+ label: entry.localizedName ?? entry.uniqueID,
380
+ facing: facingForPosition(entry.position)
381
+ }));
382
+ const selected = device ?? selectDevice(currentConfig.camera, devices);
383
+ const profile = selected ? (() => {
384
+ const probe = AVCaptureSession.new();
385
+ probe.beginConfiguration();
386
+ const supported = supportedPreset(probe, selected);
387
+ probe.commitConfiguration();
388
+ return supported ? {
389
+ width: 1280,
390
+ height: 720,
391
+ frameRate: 30
392
+ } : standardProfile(selected);
393
+ })() : {
394
+ width: 1280,
395
+ height: 720,
396
+ frameRate: 30
397
+ };
398
+ const available = cameras.length > 0;
399
+ return {
400
+ supported: typeof AVCaptureMovieFileOutput !== "undefined" && available,
401
+ reason: !available ? "No camera device is available on this host" : typeof AVCaptureMovieFileOutput === "undefined" ? "Movie file output is unsupported by this runtime" : void 0,
402
+ available,
403
+ cameras,
404
+ profiles: [{
405
+ profile: "standard",
406
+ ...profile
407
+ }],
408
+ audio: typeof AVCaptureDevice.defaultDeviceWithMediaType === "function",
409
+ orientations: [
410
+ "portrait",
411
+ "portraitUpsideDown",
412
+ "landscapeLeft",
413
+ "landscapeRight"
414
+ ],
415
+ durationLimit: "native",
416
+ elapsedTime: "nativeMedia",
417
+ output: {
418
+ mimeType: "video/quicktime",
419
+ container: "mov",
420
+ storage: "appPrivateFile",
421
+ destinationFileUrl: true
422
+ }
423
+ };
424
+ },
425
+ previewReady: () => previewReadyFlag && !interrupted,
426
+ attachPreview: (host, listener) => {
427
+ let target;
428
+ try {
429
+ target = ensureSession();
430
+ } catch (error) {
431
+ throw error instanceof CameraCaptureError ? error : new CameraCaptureError({
432
+ kind: "unavailable",
433
+ operation: "startPreview",
434
+ message: error instanceof Error ? error.message : String(error),
435
+ cause: error
436
+ });
437
+ }
438
+ const view = host;
439
+ const nativeView = view?.ios ?? view?.nativeViewProtected;
440
+ if (!nativeView?.layer) throw new CameraCaptureError({
441
+ kind: "invalidArgument",
442
+ operation: "attachPreview",
443
+ message: "Camera preview host is unavailable"
444
+ });
445
+ const layer = AVCaptureVideoPreviewLayer.layerWithSession(target);
446
+ layer.videoGravity = AVLayerVideoGravityResizeAspectFill;
447
+ const connection = layer.connection;
448
+ if (connection) {
449
+ connection.automaticallyAdjustsVideoMirroring = false;
450
+ if (connection.supportsVideoMirroring) connection.videoMirrored = device?.position === captureDevicePositionFront;
451
+ }
452
+ nativeView.layer.addSublayer(layer);
453
+ previewLayer = layer;
454
+ previewHost = view;
455
+ const updateFrame = () => {
456
+ layer.frame = nativeView.bounds;
457
+ };
458
+ detachFrame = () => view.off?.("layoutChanged", updateFrame);
459
+ view.on?.("layoutChanged", updateFrame);
460
+ updateFrame();
461
+ target.startRunning();
462
+ previewReadyFlag = true;
463
+ observer?.previewReady(true);
464
+ listener?.onReady?.();
465
+ return () => {
466
+ previewReadyFlag = false;
467
+ view.off?.("layoutChanged", updateFrame);
468
+ layer.removeFromSuperlayer();
469
+ previewLayer = void 0;
470
+ previewHost = void 0;
471
+ detachFrame = void 0;
472
+ observer?.previewReady(false);
473
+ stopSessionIfIdle();
474
+ };
475
+ },
476
+ configure: (next) => {
477
+ const previous = currentConfig;
478
+ currentConfig = { ...next };
479
+ const target = session;
480
+ if (!target || !device) return;
481
+ target.beginConfiguration();
482
+ const wanted = selectDevice(next.camera ?? previous.camera, listCameras());
483
+ if (wanted && wanted !== device && videoInput) {
484
+ const input = AVCaptureDeviceInput.deviceInputWithDeviceError(wanted, void 0);
485
+ if (input && target.canAddInput(input)) {
486
+ target.removeInput(videoInput);
487
+ target.addInput(input);
488
+ videoInput = input;
489
+ device = wanted;
490
+ }
491
+ }
492
+ if (next.audio === true && readPermission("microphone") === "granted") ensureAudioInput(target);
493
+ else if (next.audio === false && audioInput) {
494
+ target.removeInput(audioInput);
495
+ audioInput = void 0;
496
+ }
497
+ target.commitConfiguration();
498
+ },
499
+ startCapture: (request, sink) => {
500
+ const target = session;
501
+ if (!target || !movieOutput || !device) {
502
+ sink.settled({
503
+ kind: "failed",
504
+ stage: "preparation",
505
+ issue: emitIssue("unavailable", "startRecording", "The camera session is not running")
506
+ });
507
+ return;
508
+ }
509
+ if (!hasUsageDeclaration("camera")) {
510
+ sink.settled({
511
+ kind: "failed",
512
+ stage: "preparation",
513
+ issue: emitIssue("configurationMissing", "startRecording", "Missing NSCameraUsageDescription in the host app's Info.plist")
514
+ });
515
+ return;
516
+ }
517
+ if (request.audio) {
518
+ if (!hasUsageDeclaration("microphone")) {
519
+ sink.settled({
520
+ kind: "failed",
521
+ stage: "preparation",
522
+ issue: emitIssue("configurationMissing", "startRecording", "Missing NSMicrophoneUsageDescription in the host app's Info.plist")
523
+ });
524
+ return;
525
+ }
526
+ const micStatus = readPermission("microphone");
527
+ if (micStatus !== "granted") {
528
+ sink.settled({
529
+ kind: "failed",
530
+ stage: "preparation",
531
+ issue: emitIssue(micStatus === "restricted" ? "permissionRestricted" : "permissionDenied", "startRecording", `Microphone permission is ${micStatus}`, { permission: "microphone" })
532
+ });
533
+ return;
534
+ }
535
+ if (!audioInput) {
536
+ target.beginConfiguration();
537
+ const added = ensureAudioInput(target);
538
+ target.commitConfiguration();
539
+ if (!added) {
540
+ sink.settled({
541
+ kind: "failed",
542
+ stage: "preparation",
543
+ issue: emitIssue("unavailable", "startRecording", "No audio input device is available")
544
+ });
545
+ return;
546
+ }
547
+ }
548
+ }
549
+ const connection = movieOutput.connectionWithMediaType(AVMediaTypeVideo);
550
+ if (request.orientation) {
551
+ const angle = orientationToRotationAngle(request.orientation);
552
+ const legacy = orientationToLegacyConstant(request.orientation);
553
+ let applied = false;
554
+ if (connection && angle !== void 0 && typeof connection.isVideoRotationAngleSupported === "function" && connection.isVideoRotationAngleSupported(angle)) {
555
+ connection.videoRotationAngle = angle;
556
+ applied = true;
557
+ } else if (connection && legacy !== void 0 && connection.supportsVideoOrientation) {
558
+ connection.videoOrientation = legacy;
559
+ applied = true;
560
+ }
561
+ if (!applied) {
562
+ sink.settled({
563
+ kind: "failed",
564
+ stage: "preparation",
565
+ issue: emitIssue("unsupportedConfiguration", "startRecording", `Orientation '${request.orientation}' is not supported by this connection`)
566
+ });
567
+ return;
568
+ }
569
+ }
570
+ if (connection) {
571
+ connection.automaticallyAdjustsVideoMirroring = false;
572
+ if (connection.supportsVideoMirroring) connection.videoMirrored = false;
573
+ }
574
+ let filePath;
575
+ if (request.destinationPath) {
576
+ const directory = request.destinationPath.slice(0, request.destinationPath.lastIndexOf("/"));
577
+ if (!File.exists(directory) || File.exists(request.destinationPath)) {
578
+ sink.settled({
579
+ kind: "failed",
580
+ stage: "preparation",
581
+ issue: emitIssue("destinationUnavailable", "startRecording", File.exists(request.destinationPath) ? "destinationFileUrl already exists" : "destinationFileUrl directory does not exist")
582
+ });
583
+ return;
584
+ }
585
+ filePath = request.destinationPath;
586
+ } else try {
587
+ filePath = path.join(recordingsDirectory(), `clip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mov`);
588
+ } catch (cause) {
589
+ sink.settled({
590
+ kind: "failed",
591
+ stage: "preparation",
592
+ issue: emitIssue("destinationUnavailable", "startRecording", cause instanceof Error ? cause.message : String(cause), cause)
593
+ });
594
+ return;
595
+ }
596
+ if (freeDiskBytes() < minFreeDiskBytes) {
597
+ sink.settled({
598
+ kind: "failed",
599
+ stage: "preparation",
600
+ issue: emitIssue("insufficientStorage", "startRecording", "Not enough local storage for a movie recording")
601
+ });
602
+ return;
603
+ }
604
+ limitRequested = request.maximumDurationMs !== void 0;
605
+ movieOutput.maxRecordedDuration = request.maximumDurationMs !== void 0 ? CMTimeMakeWithSeconds(request.maximumDurationMs / 1e3, 600) : noDurationLimit ?? movieOutput.maxRecordedDuration;
606
+ captureRequest = request;
607
+ captureSink = sink;
608
+ try {
609
+ movieOutput.startRecordingToOutputFileURLRecordingDelegate(NSURL.fileURLWithPath(filePath), recordingDelegate);
610
+ } catch (cause) {
611
+ captureRequest = void 0;
612
+ captureSink = void 0;
613
+ sink.settled({
614
+ kind: "failed",
615
+ stage: "preparation",
616
+ issue: emitIssue("captureFailed", "startRecording", cause instanceof Error ? cause.message : String(cause), cause)
617
+ });
618
+ }
619
+ },
620
+ stopCapture: () => {
621
+ try {
622
+ if (movieOutput?.recording) movieOutput.stopRecording();
623
+ } catch {}
624
+ },
625
+ elapsedMs: () => {
626
+ try {
627
+ const duration = movieOutput?.recordedDuration;
628
+ if (!duration) return 0;
629
+ return Math.max(0, Math.round(CMTimeGetSeconds(duration) * 1e3));
630
+ } catch {
631
+ return 0;
632
+ }
633
+ },
634
+ openOutput: async (output) => {
635
+ if (output.kind !== "nativeFile") throw new CameraCaptureError({
636
+ kind: "invalidArgument",
637
+ operation: "openOutput",
638
+ message: "Only file-backed output can be opened on iOS"
639
+ });
640
+ const filePath = output.fileUrl.slice(7);
641
+ if (!File.exists(filePath)) throw new CameraCaptureError({
642
+ kind: "unavailable",
643
+ operation: "openOutput",
644
+ message: `Movie file is missing at ${output.fileUrl}`
645
+ });
646
+ return {
647
+ url: output.fileUrl,
648
+ fileUrl: output.fileUrl,
649
+ release: () => {}
650
+ };
651
+ },
652
+ release: async () => {
653
+ previewReadyFlag = false;
654
+ detachFrame?.();
655
+ detachFrame = void 0;
656
+ if (previewHost) try {
657
+ previewLayer?.removeFromSuperlayer();
658
+ } catch {}
659
+ previewHost = void 0;
660
+ previewLayer = void 0;
661
+ captureRequest = void 0;
662
+ captureSink = void 0;
663
+ clearObservers();
664
+ try {
665
+ session?.stopRunning();
666
+ } catch {}
667
+ session = void 0;
668
+ videoInput = void 0;
669
+ audioInput = void 0;
670
+ movieOutput = void 0;
671
+ device = void 0;
672
+ },
673
+ onPreviewDetached: () => {
674
+ stopSessionIfIdle();
675
+ },
676
+ setObserver: (next) => {
677
+ observer = next;
678
+ }
679
+ };
680
+ }
681
+ //#endregion
682
+ export { createSessionBackend };