@octane-xplat/camera 0.13.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.
- package/README.md +63 -3
- package/dist/linux/CameraView.web.tsrx.js +337 -0
- package/dist/linux/host-movie.web.js +100 -0
- package/dist/linux/index.js +5 -0
- package/dist/linux/layout-child.web.js +29 -0
- package/dist/linux/movie-metadata.web.js +65 -0
- package/dist/linux/octane-client-build.json +8 -0
- package/dist/linux/session-backend.linux.js +479 -0
- package/dist/linux/session-core.js +368 -0
- package/dist/linux/session.js +29 -0
- package/dist/linux/synthetic-session.js +280 -0
- package/dist/linux/types.js +19 -0
- package/dist/native/CameraView.android.js +25 -1
- package/dist/native/CameraView.ios.js +24 -1
- package/dist/native/CameraView.windows.js +173 -0
- package/dist/native/camera-permission.windows.js +43 -0
- package/dist/native/camera-session.windows.js +78 -0
- package/dist/native/cx.windows.js +9 -0
- package/dist/native/index.js +4 -1
- package/dist/native/layout-child.windows.js +53 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/session-backend.android.js +234 -0
- package/dist/native/session-backend.ios.js +682 -0
- package/dist/native/session-backend.js +55 -0
- package/dist/native/session-backend.windows.js +905 -0
- package/dist/native/session-core.js +368 -0
- package/dist/native/session.js +29 -0
- package/dist/native/synthetic-session.js +280 -0
- package/dist/native/types.js +19 -0
- package/dist/web/CameraView.web.tsrx.js +27 -7
- package/dist/web/host-movie.web.js +100 -0
- package/dist/web/index.js +4 -1
- package/dist/web/movie-metadata.web.js +65 -0
- package/dist/web/movie-storage.web.js +119 -0
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/session-backend.web.js +490 -0
- package/dist/web/session-core.js +368 -0
- package/dist/web/session.js +29 -0
- package/dist/web/synthetic-session.js +280 -0
- package/dist/web/types.js +19 -0
- package/package.json +22 -4
- package/platforms/android/AndroidManifest.xml +2 -0
- package/platforms/android/include.gradle +1 -0
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +4 -0
- package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +363 -0
- package/platforms/ios/Info.plist +2 -0
- package/platforms/macos/src/XplatCameraHost.swift +513 -0
- package/src/CameraView.android.tsrx +21 -1
- package/src/CameraView.ios.tsrx +28 -2
- package/src/CameraView.macos.tsrx +93 -0
- package/src/CameraView.web.tsrx +20 -2
- package/src/CameraView.windows.tsrx +124 -0
- package/src/camera-permission.windows.ts +76 -0
- package/src/camera-session.macos.ts +75 -0
- package/src/camera-session.windows.ts +123 -0
- package/src/cx.macos.ts +5 -0
- package/src/cx.windows.ts +6 -0
- package/src/host-movie.web.ts +180 -0
- package/src/index.ts +31 -0
- package/src/index.web.ts +31 -0
- package/src/layout-child.windows.ts +79 -0
- package/src/movie-metadata.web.ts +84 -0
- package/src/movie-storage.web.ts +164 -0
- package/src/props.ts +7 -1
- package/src/session-backend.android.ts +397 -0
- package/src/session-backend.ios.ts +1092 -0
- package/src/session-backend.linux.ts +869 -0
- package/src/session-backend.macos.ts +624 -0
- package/src/session-backend.ts +57 -0
- package/src/session-backend.web.ts +874 -0
- package/src/session-backend.windows.ts +1517 -0
- package/src/session-core.ts +659 -0
- package/src/session.ts +37 -0
- package/src/synthetic-session.ts +367 -0
- package/src/types.ts +329 -0
- package/types/index.d.ts +43 -0
- package/types/props.d.ts +7 -1
- package/types/session-core.d.ts +129 -0
- package/types/synthetic-session.d.ts +48 -0
- package/types/types.d.ts +263 -0
- 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
|
+
}
|