@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,397 @@
1
+ import { Application } from '@nativescript/core'
2
+ import { CameraCaptureError } from './types'
3
+ import type {
4
+ BackendCaptureSink,
5
+ BackendObserver,
6
+ BackendPreviewListener,
7
+ CameraSessionBackend,
8
+ } from './session-core'
9
+
10
+ import type {
11
+ CameraPermissionKind,
12
+ CameraPermissionStatus,
13
+ CameraSessionConfig,
14
+ CaptureIssue,
15
+ } from './types'
16
+
17
+ let requestCode = 7820
18
+ let permissionChain: Promise<unknown> = Promise.resolve()
19
+ let owner: object | undefined
20
+
21
+ const nativeBridge = () => (globalThis as any).com?.octanexplat?.camera?.CameraSessionBridge
22
+ const permissionName = (kind: CameraPermissionKind) =>
23
+ kind === 'camera' ? 'android.permission.CAMERA' : 'android.permission.RECORD_AUDIO'
24
+
25
+ const error = (kind: CaptureIssue['kind'], operation: string, message: string, cause?: unknown) =>
26
+ new CameraCaptureError({ kind, operation, message, cause })
27
+
28
+ const context = () => Application.android.context
29
+ const preferences = () => context().getSharedPreferences('octane-camera-permissions', 0)
30
+
31
+ async function checkPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus> {
32
+ const activity = Application.android.foregroundActivity ?? Application.android.startActivity
33
+ if (!activity) {
34
+ return 'unavailable'
35
+ }
36
+
37
+ const permission = permissionName(kind)
38
+ if (activity.checkSelfPermission(permission) === 0) {
39
+ return 'granted'
40
+ }
41
+
42
+ return preferences().getBoolean(kind, false)
43
+ ? activity.shouldShowRequestPermissionRationale(permission)
44
+ ? 'denied'
45
+ : 'blocked'
46
+ : 'notDetermined'
47
+ }
48
+
49
+ function requestPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus> {
50
+ const result = permissionChain.then(async () => {
51
+ const status = await checkPermission(kind)
52
+ if (status !== 'notDetermined') {
53
+ return status
54
+ }
55
+
56
+ const activity = Application.android.foregroundActivity ?? Application.android.startActivity
57
+ if (!activity) {
58
+ return 'unavailable' as const
59
+ }
60
+
61
+ return new Promise<CameraPermissionStatus>((resolve) => {
62
+ const code = requestCode++
63
+ const finish = (value: CameraPermissionStatus) => {
64
+ clearTimeout(timer)
65
+ Application.android.off(Application.android.activityRequestPermissionsEvent, onResult)
66
+ resolve(value)
67
+ }
68
+
69
+ const onResult = (args: any) => {
70
+ if (args.requestCode !== code) {
71
+ return
72
+ }
73
+
74
+ if (!args.grantResults?.length) {
75
+ finish('unknown')
76
+ return
77
+ }
78
+
79
+ preferences().edit().putBoolean(kind, true).apply()
80
+ finish(
81
+ args.grantResults[0] === 0
82
+ ? 'granted'
83
+ : activity.shouldShowRequestPermissionRationale(permissionName(kind))
84
+ ? 'denied'
85
+ : 'blocked',
86
+ )
87
+ }
88
+
89
+ const timer = setTimeout(() => finish('unknown'), 120_000)
90
+ Application.android.on(Application.android.activityRequestPermissionsEvent, onResult)
91
+ try {
92
+ activity.requestPermissions([permissionName(kind)], code)
93
+ } catch {
94
+ finish('unavailable')
95
+ }
96
+ })
97
+ })
98
+
99
+ permissionChain = result.catch(() => {})
100
+ return result
101
+ }
102
+
103
+ export function createSessionBackend(config: CameraSessionConfig): CameraSessionBackend {
104
+ let currentConfig = { ...config }
105
+ let observer: BackendObserver | undefined
106
+ let bridge: any
107
+ let host: any
108
+ let listener: BackendPreviewListener | undefined
109
+ let sink: BackendCaptureSink | undefined
110
+ let released = false
111
+ const lease = {}
112
+ const selected = () =>
113
+ typeof currentConfig.camera === 'object'
114
+ ? `id:${currentConfig.camera.deviceId}`
115
+ : (currentConfig.camera ?? 'default')
116
+
117
+ const releaseLease = () => {
118
+ if (owner === lease) {
119
+ owner = undefined
120
+ }
121
+ }
122
+
123
+ const onSuspend = () => bridge?.background()
124
+ const onResume = () => bridge?.foreground()
125
+ const ensureBridge = () => {
126
+ if (bridge) {
127
+ return
128
+ }
129
+
130
+ const Bridge = nativeBridge()
131
+ if (!Bridge) {
132
+ throw error(
133
+ 'unsupportedPlatform',
134
+ 'preview',
135
+ 'CameraX video bridge is missing from the native build',
136
+ )
137
+ }
138
+
139
+ bridge = new Bridge(
140
+ context(),
141
+ new Bridge.Listener({
142
+ event(type: string, payload: string) {
143
+ if (type === 'ready') {
144
+ observer?.previewReady(payload === 'true')
145
+ if (payload === 'true') {
146
+ listener?.onReady?.()
147
+ }
148
+ } else if (type === 'availability') {
149
+ observer?.availability(payload === 'true')
150
+ } else if (type === 'interruption') {
151
+ observer?.interruption('began', payload as any)
152
+ } else if (type === 'recovered') {
153
+ observer?.interruption('ended', payload as any)
154
+ } else if (type === 'previewError') {
155
+ observer?.availability(false)
156
+ listener?.onError?.(error('unavailable', 'preview', payload))
157
+ if (!host) {
158
+ releaseLease()
159
+ }
160
+ } else if (type === 'finishing') {
161
+ sink?.finishing(payload ? (payload as any) : undefined)
162
+ } else if (type === 'started') {
163
+ sink?.started(JSON.parse(payload))
164
+ } else if (type === 'finished' || type === 'failed') {
165
+ const target = sink
166
+ sink = undefined
167
+ if (!host) {
168
+ releaseLease()
169
+ }
170
+
171
+ const result = JSON.parse(payload)
172
+ target?.settled(
173
+ type === 'finished'
174
+ ? result
175
+ : {
176
+ kind: 'failed',
177
+ stage: result.stage,
178
+ issue: error(result.kind, result.stage, result.message),
179
+ },
180
+ )
181
+ }
182
+ },
183
+ }),
184
+ )
185
+
186
+ Application.on(Application.suspendEvent, onSuspend)
187
+ Application.on(Application.resumeEvent, onResume)
188
+ }
189
+
190
+ return {
191
+ platform: 'android',
192
+ get supported() {
193
+ return !!nativeBridge()
194
+ },
195
+ checkPermission,
196
+ requestPermission: async (kind) => {
197
+ const status = await requestPermission(kind)
198
+ if (
199
+ kind === 'camera' &&
200
+ status === 'granted' &&
201
+ host &&
202
+ !sink &&
203
+ !released &&
204
+ !bridge?.isReady()
205
+ ) {
206
+ try {
207
+ ensureBridge()
208
+ bridge.attach(host, selected())
209
+ } catch (cause) {
210
+ observer?.availability(false)
211
+ listener?.onError?.(
212
+ error(
213
+ 'unavailable',
214
+ 'preview',
215
+ 'Could not activate CameraX after permission approval',
216
+ cause,
217
+ ),
218
+ )
219
+ }
220
+ }
221
+
222
+ return status
223
+ },
224
+ getCapabilities: async () => {
225
+ const value = bridge ? JSON.parse(bridge.capabilities()) : { cameras: [], profiles: [] }
226
+ const supported = !!nativeBridge()
227
+ return {
228
+ supported,
229
+ available: !!bridge?.isReady(),
230
+ reason: !supported
231
+ ? 'CameraX video bridge is missing from the native build'
232
+ : !bridge?.isReady()
233
+ ? 'An active permitted preview is required'
234
+ : undefined,
235
+ cameras: value.cameras,
236
+ profiles: value.profiles,
237
+ audio: context().getPackageManager().hasSystemFeature('android.hardware.microphone'),
238
+ orientations: [],
239
+ durationLimit: 'native',
240
+ elapsedTime: 'nativeMedia',
241
+ output: {
242
+ mimeType: 'video/mp4',
243
+ container: 'mp4',
244
+ storage: 'appPrivateFile',
245
+ destinationFileUrl: true,
246
+ },
247
+ }
248
+ },
249
+ previewReady: () => !!bridge?.isReady(),
250
+ validateStart: (options) => {
251
+ if (
252
+ options.maximumDurationMs !== undefined &&
253
+ options.maximumDurationMs > 9_223_372_036_854
254
+ ) {
255
+ throw error(
256
+ 'unsupportedConfiguration',
257
+ 'startRecording',
258
+ 'The duration exceeds CameraX media-time range',
259
+ )
260
+ }
261
+
262
+ if (options.orientation && options.orientation !== 'unspecified') {
263
+ throw error(
264
+ 'unsupportedConfiguration',
265
+ 'startRecording',
266
+ 'Fixed cardinal orientation has not been qualified on this Android source',
267
+ )
268
+ }
269
+ },
270
+ attachPreview: (target, callbacks) => {
271
+ if (released || sink) {
272
+ throw error('invalidState', 'attachPreview', 'The camera is disposed or finalizing')
273
+ }
274
+
275
+ if (owner && owner !== lease) {
276
+ throw error('busy', 'attachPreview', 'Another camera session is still finalizing')
277
+ }
278
+
279
+ owner = lease
280
+ host = target
281
+ listener = callbacks
282
+ try {
283
+ ensureBridge()
284
+ } catch (cause) {
285
+ host = undefined
286
+ listener = undefined
287
+ releaseLease()
288
+ throw error(
289
+ 'unavailable',
290
+ 'attachPreview',
291
+ 'Could not construct the CameraX native bridge',
292
+ cause,
293
+ )
294
+ }
295
+
296
+ void checkPermission('camera').then((status) => {
297
+ if (released || !host) {
298
+ return
299
+ }
300
+
301
+ if (status !== 'granted') {
302
+ listener?.onError?.(
303
+ error(
304
+ 'permissionDenied',
305
+ 'preview',
306
+ 'Request camera permission before activating this preview',
307
+ ),
308
+ )
309
+
310
+ return
311
+ }
312
+
313
+ try {
314
+ bridge.attach(host, selected())
315
+ } catch (cause) {
316
+ listener?.onError?.(
317
+ error('unavailable', 'preview', 'Could not bind shared CameraX preview', cause),
318
+ )
319
+ }
320
+ })
321
+
322
+ return () => {
323
+ host = undefined
324
+ listener = undefined
325
+ bridge.detach()
326
+ if (!sink) {
327
+ releaseLease()
328
+ }
329
+ }
330
+ },
331
+ configure: (next) => {
332
+ const sourceChanged =
333
+ JSON.stringify(next.camera) !== JSON.stringify(currentConfig.camera) ||
334
+ next.profile !== currentConfig.profile
335
+
336
+ currentConfig = { ...next }
337
+ if (host && sourceChanged) {
338
+ bridge.detach()
339
+ bridge.attach(host, selected())
340
+ }
341
+ },
342
+ startCapture: (request, target) => {
343
+ sink = target
344
+ try {
345
+ bridge.start(
346
+ request.audio,
347
+ request.destinationPath ?? '',
348
+ request.maximumDurationMs === undefined
349
+ ? 0
350
+ : Math.max(1, Math.ceil(request.maximumDurationMs)),
351
+ )
352
+ } catch (cause) {
353
+ sink = undefined
354
+ target.settled({
355
+ kind: 'failed',
356
+ stage: 'preparation',
357
+ issue: error(
358
+ 'captureFailed',
359
+ 'startRecording',
360
+ 'CameraX could not begin recording',
361
+ cause,
362
+ ),
363
+ })
364
+ }
365
+ },
366
+ stopCapture: () => bridge?.stop(),
367
+ elapsedMs: () => Number(bridge?.elapsedMs() ?? 0),
368
+ openOutput: async (output) => {
369
+ if (output.kind !== 'nativeFile' || output.synthetic) {
370
+ throw error('invalidArgument', 'openOutput', 'Expected a native Android movie reference')
371
+ }
372
+
373
+ try {
374
+ const url = nativeBridge().openOutput(output.resourceId, output.fileUrl)
375
+ return { url, fileUrl: url, release: () => {} }
376
+ } catch (cause) {
377
+ throw error(
378
+ 'destinationUnavailable',
379
+ 'openOutput',
380
+ 'The stored Android movie cannot be reopened',
381
+ cause,
382
+ )
383
+ }
384
+ },
385
+ release: async () => {
386
+ released = true
387
+ Application.off(Application.suspendEvent, onSuspend)
388
+ Application.off(Application.resumeEvent, onResume)
389
+ bridge?.release()
390
+ host = undefined
391
+ releaseLease()
392
+ },
393
+ setObserver: (value) => {
394
+ observer = value
395
+ },
396
+ }
397
+ }