@octane-xplat/camera 0.13.0 → 0.15.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 (58) hide show
  1. package/README.md +89 -3
  2. package/package.json +35 -11
  3. package/platforms/android/AndroidManifest.xml +2 -0
  4. package/platforms/android/include.gradle +1 -0
  5. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +13 -0
  6. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
  7. package/platforms/ios/Info.plist +2 -0
  8. package/platforms/macos/src/CameraOwnership.swift +22 -0
  9. package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
  10. package/platforms/macos/src/XplatCameraHost.swift +639 -0
  11. package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
  12. package/src/CameraView.android.tsrx +27 -3
  13. package/src/CameraView.ios.tsrx +32 -4
  14. package/src/CameraView.macos.tsrx +95 -0
  15. package/src/CameraView.web.tsrx +31 -2
  16. package/src/CameraView.windows.tsrx +126 -0
  17. package/src/camera-permission.mobile.ts +18 -4
  18. package/src/camera-permission.windows.ts +76 -0
  19. package/src/camera-session.macos.ts +75 -0
  20. package/src/camera-session.windows.ts +126 -0
  21. package/src/host-movie.web.ts +295 -0
  22. package/src/index.ts +32 -0
  23. package/src/index.web.ts +32 -0
  24. package/src/layout-child.windows.ts +79 -0
  25. package/src/movie-metadata.web.ts +84 -0
  26. package/src/movie-storage.web.ts +164 -0
  27. package/src/preview-tap.ts +34 -0
  28. package/src/props.ts +14 -1
  29. package/src/session-backend.android.ts +439 -0
  30. package/src/session-backend.ios.ts +1258 -0
  31. package/src/session-backend.linux.ts +887 -0
  32. package/src/session-backend.macos.ts +707 -0
  33. package/src/session-backend.ts +59 -0
  34. package/src/session-backend.web.ts +891 -0
  35. package/src/session-backend.windows.ts +1599 -0
  36. package/src/session-core.ts +723 -0
  37. package/src/session.ts +37 -0
  38. package/src/synthetic-session.ts +377 -0
  39. package/src/types.ts +357 -0
  40. package/types/index.d.ts +43 -0
  41. package/types/props.d.ts +14 -1
  42. package/types/session-core.d.ts +139 -0
  43. package/types/synthetic-session.d.ts +51 -0
  44. package/types/types.d.ts +290 -0
  45. package/dist/native/CameraView.android.js +0 -166
  46. package/dist/native/CameraView.ios.js +0 -131
  47. package/dist/native/camera-permission.mobile.js +0 -45
  48. package/dist/native/camera-session.ios.js +0 -45
  49. package/dist/native/cx.mobile.js +0 -9
  50. package/dist/native/index.js +0 -2
  51. package/dist/native/layout-child.js +0 -53
  52. package/dist/native/octane-client-build.json +0 -8
  53. package/dist/web/CameraView.web.tsrx.js +0 -317
  54. package/dist/web/index.js +0 -2
  55. package/dist/web/layout-child.web.js +0 -29
  56. package/dist/web/octane-client-build.json +0 -8
  57. package/src/CameraView.windows.ts +0 -5
  58. package/src/cx.mobile.ts +0 -6
@@ -0,0 +1,76 @@
1
+ /** Windows camera-permission probe for preview-only `CameraView` callers.
2
+ * Uses `AppCapability` when projected and falls back to
3
+ * `DeviceAccessInformation`; both are non-prompting checks — only
4
+ * `RequestAccessAsync` may show the system prompt. */
5
+
6
+ const windows = () => globalThis as any
7
+
8
+ const toPromise = (op: any): Promise<any> => {
9
+ const { NSWinRT } = windows()
10
+ if (NSWinRT?.toPromise && op != null) {
11
+ return NSWinRT.toPromise(op)
12
+ }
13
+
14
+ if (op?.done) {
15
+ return new Promise((resolve, reject) => {
16
+ try {
17
+ op.done(resolve, reject)
18
+ } catch (cause) {
19
+ reject(cause)
20
+ }
21
+ })
22
+ }
23
+
24
+ return Promise.resolve(op)
25
+ }
26
+
27
+ export function cameraHardwarePresent(): boolean {
28
+ const { Windows, Microsoft } = windows()
29
+ return !!(
30
+ Windows?.Media?.Capture?.MediaCapture &&
31
+ Windows?.Devices?.Enumeration?.DeviceInformation &&
32
+ Microsoft?.UI?.Xaml?.Controls?.CaptureElement
33
+ )
34
+ }
35
+
36
+ export async function ensureCameraPermission(): Promise<boolean> {
37
+ try {
38
+ const { Windows } = windows()
39
+ const capability =
40
+ Windows?.Security?.Authorization?.AppCapabilityAccess?.AppCapability?.Create?.('webcam')
41
+
42
+ if (capability) {
43
+ const status = capability.CheckAccess()
44
+ // AppCapabilityAccessStatus: DeniedBySystem=0, NotDeclaredByApp=1,
45
+ // DeniedByUser=2, UserPromptRequired=3, Allowed=4.
46
+ if (status === 4) {
47
+ return true
48
+ }
49
+
50
+ if (status === 3) {
51
+ return (await toPromise(capability.RequestAccessAsync())) === 4
52
+ }
53
+
54
+ return false
55
+ }
56
+
57
+ const enumeration = Windows?.Devices?.Enumeration
58
+ const deviceClass = enumeration?.DeviceClass?.VideoCapture
59
+ const info =
60
+ deviceClass === undefined
61
+ ? undefined
62
+ : enumeration?.DeviceAccessInformation?.CreateFromDeviceClass(deviceClass)
63
+
64
+ if (info) {
65
+ // DeviceAccessStatus: Unspecified=0, Allowed=1, DeniedByUser=2,
66
+ // DeniedBySystem=3. The enumeration API cannot prompt — an
67
+ // Unspecified outcome is resolved by the preview's own
68
+ // InitializeAsync, which runs the OS consent flow.
69
+ return info.CurrentStatus !== 2 && info.CurrentStatus !== 3
70
+ }
71
+ } catch {
72
+ return false
73
+ }
74
+
75
+ return false
76
+ }
@@ -0,0 +1,75 @@
1
+ import '@nativescript/macos-node-api'
2
+
3
+ interface CameraHost {
4
+ attachPreview(view: object): string | null
5
+ detachPreview(): void
6
+ installObserver(emit: (message: string) => void): void
7
+ permissionStatus(kind: string): string
8
+ requestPermissionReply(kind: string, reply: (status: string) => void): void
9
+ devices(): string
10
+ audioDeviceAvailable(): boolean
11
+ configure(options: { cameraId?: string; audio?: boolean; rotationAngle?: number }): string | null
12
+ dispose(): void
13
+ }
14
+
15
+ declare const XplatCameraHost: {
16
+ alloc(): { init(): CameraHost }
17
+ }
18
+
19
+ let helper: CameraHost | undefined
20
+ const helperHost = () => (helper ??= XplatCameraHost.alloc().init())
21
+
22
+ export function cameraHardwarePresent(): boolean {
23
+ try {
24
+ return JSON.parse(helperHost().devices()).length > 0
25
+ } catch {
26
+ return false
27
+ }
28
+ }
29
+
30
+ /** Preview-only permission entry point: prompts once from mount, mirrors the
31
+ * iOS legacy path. Session code should use `session.requestPermission` —
32
+ * explicit actions only. */
33
+ export function ensureCameraPermission(): Promise<boolean> {
34
+ const status = helperHost().permissionStatus('camera')
35
+ if (status === 'granted') {
36
+ return Promise.resolve(true)
37
+ }
38
+
39
+ if (status === 'undeclared') {
40
+ return Promise.resolve(false)
41
+ }
42
+
43
+ return new Promise((resolve) =>
44
+ helperHost().requestPermissionReply('camera', (next) => resolve(next === 'granted')),
45
+ )
46
+ }
47
+
48
+ /** Legacy preview-only path: a dedicated XplatCameraHost owns the preview
49
+ * session — shared with nothing, disposed with the view. The session-backed
50
+ * path goes through `attachSessionPreview` instead. */
51
+ export function startCameraPreview(
52
+ view: object,
53
+ _facing: 'front' | 'back',
54
+ onReady?: () => void,
55
+ ): () => void {
56
+ const host = XplatCameraHost.alloc().init()
57
+ host.installObserver(() => {})
58
+ const configured = host.configure({ audio: false })
59
+ if (configured) {
60
+ host.dispose()
61
+ throw new Error(`camera unavailable: ${configured}`)
62
+ }
63
+
64
+ const failure = host.attachPreview(view)
65
+ if (failure) {
66
+ host.dispose()
67
+ throw new Error(`camera unavailable: ${failure}`)
68
+ }
69
+
70
+ onReady?.()
71
+ return () => {
72
+ host.detachPreview()
73
+ host.dispose()
74
+ }
75
+ }
@@ -0,0 +1,126 @@
1
+ /** Standalone (non-session) WinUI preview for `CameraView` without a
2
+ * `session` prop. Owns a private `MediaCapture` bound to a
3
+ * `CaptureElement`; the returned function stops preview and closes the
4
+ * device. Session-backed previews share their capture through
5
+ * `session-backend.windows.ts` instead. */
6
+
7
+ const windows = () => globalThis as any
8
+
9
+ const toPromise = (op: any): Promise<any> => {
10
+ const { NSWinRT } = windows()
11
+ if (NSWinRT?.toPromise && op != null) {
12
+ return NSWinRT.toPromise(op)
13
+ }
14
+
15
+ if (op?.done) {
16
+ return new Promise((resolve, reject) => {
17
+ try {
18
+ op.done(resolve, reject)
19
+ } catch (cause) {
20
+ reject(cause)
21
+ }
22
+ })
23
+ }
24
+
25
+ return Promise.resolve(op)
26
+ }
27
+
28
+ const vectorToArray = (items: any): any[] => {
29
+ const result: any[] = []
30
+ const size = Number(items?.Size ?? items?.Count ?? 0)
31
+ for (let index = 0; index < size; index += 1) {
32
+ result.push(items.GetAt(index))
33
+ }
34
+
35
+ return result
36
+ }
37
+
38
+ /** Attach a MediaCapture preview to the view's `CaptureElement`.
39
+ * `facing` maps to enclosure panels; desktop cameras without a panel fall
40
+ * back to the OS default — front/back is a preference, never a fabrication. */
41
+ export async function startCameraPreview(
42
+ view: any,
43
+ facing: 'front' | 'back',
44
+ onReady?: () => void,
45
+ ): Promise<() => Promise<void>> {
46
+ const { Windows } = windows()
47
+ const enumeration = Windows?.Devices?.Enumeration
48
+ const captureApi = Windows?.Media?.Capture
49
+ if (!enumeration || !captureApi) {
50
+ throw new Error('camera preview unsupported')
51
+ }
52
+
53
+ const found = await toPromise(
54
+ enumeration.DeviceInformation.FindAllAsync(enumeration.DeviceClass.VideoCapture),
55
+ )
56
+
57
+ const devices = vectorToArray(found)
58
+ if (!devices.length) {
59
+ throw new Error('camera unavailable')
60
+ }
61
+
62
+ const wantedPanel = facing === 'front' ? 1 : 2
63
+ const device =
64
+ devices.find((entry: any) => Number(entry?.EnclosureLocation?.Panel) === wantedPanel) ??
65
+ devices[0]
66
+
67
+ const settings = new captureApi.MediaCaptureInitializationSettings()
68
+ settings.VideoDeviceId = String(device?.Id ?? device?.id ?? '')
69
+ settings.StreamingCaptureMode = 0 // Video
70
+ const capture = new captureApi.MediaCapture()
71
+ try {
72
+ await toPromise(capture.InitializeAsync(settings))
73
+ } catch (cause) {
74
+ try {
75
+ capture.Close()
76
+ } catch {
77
+ try {
78
+ capture.Dispose()
79
+ } catch {
80
+ // GC reclaims the device handle.
81
+ }
82
+ }
83
+
84
+ throw cause instanceof Error ? cause : new Error('camera unavailable')
85
+ }
86
+
87
+ const element = view?.nativeViewProtected ?? view
88
+ element.Source = capture
89
+ try {
90
+ if (
91
+ Number(device?.EnclosureLocation?.Panel) === 1 &&
92
+ typeof capture.SetPreviewMirroring === 'function'
93
+ ) {
94
+ capture.SetPreviewMirroring(true)
95
+ }
96
+ } catch {
97
+ // Mirrored preview is cosmetic.
98
+ }
99
+
100
+ await toPromise(capture.StartPreviewAsync())
101
+ onReady?.()
102
+
103
+ return async () => {
104
+ try {
105
+ element.Source = null
106
+ } catch {
107
+ // The element may already be gone.
108
+ }
109
+
110
+ try {
111
+ await toPromise(capture.StopPreviewAsync())
112
+ } catch {
113
+ // The pipeline may already be down.
114
+ }
115
+
116
+ try {
117
+ capture.Close()
118
+ } catch {
119
+ try {
120
+ capture.Dispose()
121
+ } catch {
122
+ // GC reclaims the device handle.
123
+ }
124
+ }
125
+ }
126
+ }
@@ -0,0 +1,295 @@
1
+ import { createHostClient, type HostClient } from '@octane-xplat/platform/host'
2
+ import { createWebKitTransport } from '@octane-xplat/platform/host/web'
3
+ import { CameraCaptureError } from './types'
4
+ import type { MovieOutput, MovieOutputHandle } from './types'
5
+
6
+ // Inside a desktop native webview the typed host channel may expose a
7
+ // `camera` service: truthful macOS privacy status plus app-private durable
8
+ // file storage. Outside a host — or when the service is absent — every probe
9
+ // resolves absent and the adapter keeps its browser storage behavior.
10
+
11
+ export interface CameraHostServices {
12
+ camera: {
13
+ permissionStatus(options: { kind: string }): Promise<{ status: string }>
14
+ requestPermission(options: { kind: string }): Promise<{ status: string }>
15
+ movieDirectory(): Promise<{ fileUrl: string | null }>
16
+ reserveMoviePath(options: {
17
+ fileUrl?: string
18
+ container?: string
19
+ }): Promise<{ reservation: { fileUrl: string } | null }>
20
+ writeMovieFile(options: {
21
+ fileUrl: string
22
+ base64: string
23
+ }): Promise<{ file: { name: string; uri: string } | null }>
24
+ movieFileInfo(options: {
25
+ fileUrl: string
26
+ }): Promise<{ info: { exists: boolean; fileUrl?: string; size?: number } | null }>
27
+ readMovieFile(options: { fileUrl: string }): Promise<{ base64: string | null }>
28
+ deleteMovieFile(options: { fileUrl: string }): Promise<{ removed: boolean }>
29
+ /** Optional chunked variants — a multi-hundred-MB movie must not
30
+ * round-trip through the bridge as one base64 payload. Absent on
31
+ * hosts built before the chunked contract; detected per client. */
32
+ writeMovieFileChunk?(options: {
33
+ fileUrl: string
34
+ base64: string
35
+ final?: boolean
36
+ }): Promise<{ file: { name: string; uri: string } | { ok: boolean } | null }>
37
+ readMovieFileChunk?(options: {
38
+ fileUrl: string
39
+ offset: number
40
+ length: number
41
+ }): Promise<{ base64: string | null }>
42
+ }
43
+ }
44
+
45
+ const requiredMethods = [
46
+ 'permissionStatus',
47
+ 'requestPermission',
48
+ 'movieDirectory',
49
+ 'reserveMoviePath',
50
+ 'writeMovieFile',
51
+ 'movieFileInfo',
52
+ 'readMovieFile',
53
+ 'deleteMovieFile',
54
+ ]
55
+
56
+ export type CameraHost = HostClient<CameraHostServices>
57
+
58
+ let hostProbe: Promise<CameraHost | null> | undefined
59
+
60
+ const probeHost = async (): Promise<CameraHost | null> => {
61
+ try {
62
+ const transport = createWebKitTransport()
63
+ if (!transport) {
64
+ return null
65
+ }
66
+
67
+ const client = createHostClient<CameraHostServices>(transport)
68
+ const capabilities = (await client.capabilities().catch(() => null)) as {
69
+ camera?: string[]
70
+ } | null
71
+
72
+ const methods = capabilities?.camera
73
+ if (Array.isArray(methods) && requiredMethods.every((name) => methods.includes(name))) {
74
+ return client
75
+ }
76
+
77
+ client.dispose()
78
+ return null
79
+ } catch {
80
+ return null
81
+ }
82
+ }
83
+
84
+ /** The camera-capable desktop host client, or null outside a qualified host.
85
+ * Only a confirmed client stays cached — a null result may be a transient
86
+ * transport or capabilities failure, so the next call probes again rather
87
+ * than sticking 'absent' for the page lifetime. */
88
+ export async function cameraHost(): Promise<CameraHost | null> {
89
+ hostProbe ??= probeHost()
90
+ const host = await hostProbe
91
+ if (!host) {
92
+ hostProbe = probeHost()
93
+ return hostProbe
94
+ }
95
+
96
+ return host
97
+ }
98
+
99
+ const error = (kind: 'destinationUnavailable' | 'storageFailed' | 'unavailable', message: string) =>
100
+ new CameraCaptureError({ kind, operation: 'storage', message })
101
+
102
+ const dataToBase64 = (data: Uint8Array): string => {
103
+ let binary = ''
104
+ const chunk = 0x8000
105
+ for (let offset = 0; offset < data.length; offset += chunk) {
106
+ binary += String.fromCharCode(...data.subarray(offset, offset + chunk))
107
+ }
108
+
109
+ return btoa(binary)
110
+ }
111
+
112
+ const blobToBase64 = async (bytes: Blob): Promise<string> =>
113
+ dataToBase64(new Uint8Array(await bytes.arrayBuffer()))
114
+
115
+ const base64ToBlob = (base64: string, type: string): Blob => {
116
+ const binary = atob(base64)
117
+ const data = new Uint8Array(binary.length)
118
+ for (let index = 0; index < binary.length; index++) {
119
+ data[index] = binary.charCodeAt(index)
120
+ }
121
+
122
+ return new Blob([data], { type })
123
+ }
124
+
125
+ const mimeForFileUrl = (fileUrl: string): string =>
126
+ fileUrl.endsWith('.webm')
127
+ ? 'video/webm'
128
+ : fileUrl.endsWith('.mov')
129
+ ? 'video/quicktime'
130
+ : 'video/mp4'
131
+
132
+ /** Reserve a host-side movie file. `destinationFileUrl` is validated
133
+ * host-side: it must be a new file inside the app-private storage root. */
134
+ export const reserveHostMovie = async (
135
+ host: CameraHost,
136
+ options: { destinationFileUrl?: string; container?: string },
137
+ ): Promise<{ fileUrl: string }> => {
138
+ const { reservation } = await host.call('camera', 'reserveMoviePath', {
139
+ fileUrl: options.destinationFileUrl,
140
+ container: options.container,
141
+ })
142
+
143
+ if (!reservation?.fileUrl) {
144
+ throw error(
145
+ 'destinationUnavailable',
146
+ options.destinationFileUrl
147
+ ? 'destinationFileUrl must be a new file inside the app-private recordings directory'
148
+ : 'The host could not allocate a movie recording file',
149
+ )
150
+ }
151
+
152
+ return reservation
153
+ }
154
+
155
+ /** Bytes bridged per call when the host exposes the chunked movie contract —
156
+ * large enough to amortize IPC, small enough to keep peak copies bounded. */
157
+ const MOVIE_CHUNK_BYTES = 4 * 1024 * 1024
158
+
159
+ const chunkedSupport = new WeakMap<CameraHost, Promise<boolean>>()
160
+
161
+ /** A host built before the chunked contract lacks the methods entirely —
162
+ * the capabilities probe answers that without a probe call. */
163
+ const supportsChunkedMovies = (host: CameraHost): Promise<boolean> => {
164
+ let probe = chunkedSupport.get(host)
165
+ if (!probe) {
166
+ probe = host
167
+ .capabilities()
168
+ .then((capabilities) => {
169
+ const methods = (capabilities as { camera?: string[] }).camera ?? []
170
+ return methods.includes('writeMovieFileChunk') && methods.includes('readMovieFileChunk')
171
+ })
172
+ .catch(() => false)
173
+
174
+ chunkedSupport.set(host, probe)
175
+ }
176
+
177
+ return probe
178
+ }
179
+
180
+ /** Commit finalized movie bytes to host-side app-private storage. Hosts
181
+ * with the chunked contract stream 4MB slices — a multi-hundred-MB
182
+ * recording no longer materializes ~3-4x its size across the bridge. */
183
+ export const commitHostMovie = async (
184
+ host: CameraHost,
185
+ fileUrl: string,
186
+ bytes: Blob,
187
+ ): Promise<MovieOutput> => {
188
+ if (await supportsChunkedMovies(host)) {
189
+ for (let offset = 0; ; offset += MOVIE_CHUNK_BYTES) {
190
+ const slice = bytes.slice(offset, offset + MOVIE_CHUNK_BYTES)
191
+ const final = offset + MOVIE_CHUNK_BYTES >= bytes.size
192
+ const { file } = await host.call('camera', 'writeMovieFileChunk', {
193
+ fileUrl,
194
+ base64: dataToBase64(new Uint8Array(await slice.arrayBuffer())),
195
+ final,
196
+ })
197
+
198
+ if (final) {
199
+ const committed = file && 'uri' in file ? file : null
200
+ if (!committed?.uri) {
201
+ throw error('storageFailed', 'The host could not write the recorded movie')
202
+ }
203
+
204
+ return { kind: 'nativeFile', resourceId: committed.uri, fileUrl: committed.uri }
205
+ }
206
+
207
+ if (!file || !('ok' in file) || file.ok !== true) {
208
+ throw error('storageFailed', 'The host could not write a recorded movie chunk')
209
+ }
210
+ }
211
+ }
212
+
213
+ const base64 = await blobToBase64(bytes)
214
+ const { file } = await host.call('camera', 'writeMovieFile', { fileUrl, base64 })
215
+ if (!file?.uri) {
216
+ throw error('storageFailed', 'The host could not write the recorded movie')
217
+ }
218
+
219
+ return { kind: 'nativeFile', resourceId: file.uri, fileUrl: file.uri }
220
+ }
221
+
222
+ /** Drop an uncommitted host reservation after a failed attempt. */
223
+ export const removeHostReservation = async (host: CameraHost, fileUrl: string): Promise<void> => {
224
+ await host.call('camera', 'deleteMovieFile', { fileUrl }).catch(() => {})
225
+ }
226
+
227
+ /** Reopen a host-stored movie as a temporary playback URL. The durable
228
+ * reference remains `fileUrl`; the returned url lives until `release()`. */
229
+ export const openHostMovie = async (
230
+ host: CameraHost,
231
+ output: MovieOutput,
232
+ ): Promise<MovieOutputHandle> => {
233
+ if (output.kind !== 'nativeFile' || !output.fileUrl) {
234
+ throw new CameraCaptureError({
235
+ kind: 'invalidArgument',
236
+ operation: 'openOutput',
237
+ message: 'Only file-backed output can be opened through the desktop host',
238
+ })
239
+ }
240
+
241
+ const { info } = await host.call('camera', 'movieFileInfo', { fileUrl: output.fileUrl })
242
+ if (!info?.exists) {
243
+ throw error('unavailable', `Movie file is missing at ${output.fileUrl}`)
244
+ }
245
+
246
+ const size = Number(info.size ?? -1)
247
+ if (size > 0 && (await supportsChunkedMovies(host))) {
248
+ const data = new Uint8Array(size)
249
+ for (let offset = 0; offset < size; offset += MOVIE_CHUNK_BYTES) {
250
+ const { base64: chunk } = await host.call('camera', 'readMovieFileChunk', {
251
+ fileUrl: output.fileUrl,
252
+ offset,
253
+ length: Math.min(MOVIE_CHUNK_BYTES, size - offset),
254
+ })
255
+
256
+ if (chunk == null) {
257
+ throw error('unavailable', `Movie file could not be read at ${output.fileUrl}`)
258
+ }
259
+
260
+ let decoded: string
261
+ try {
262
+ decoded = atob(chunk)
263
+ } catch {
264
+ throw error('unavailable', 'The host returned invalid movie bytes')
265
+ }
266
+
267
+ if (decoded.length !== Math.min(MOVIE_CHUNK_BYTES, size - offset)) {
268
+ throw error('unavailable', 'The stored movie changed or could not be read completely')
269
+ }
270
+
271
+ for (let index = 0; index < decoded.length; index += 1) {
272
+ data[offset + index] = decoded.charCodeAt(index)
273
+ }
274
+ }
275
+
276
+ const url = URL.createObjectURL(new Blob([data], { type: mimeForFileUrl(output.fileUrl) }))
277
+ return {
278
+ url,
279
+ fileUrl: output.fileUrl,
280
+ release: () => URL.revokeObjectURL(url),
281
+ }
282
+ }
283
+
284
+ const { base64 } = await host.call('camera', 'readMovieFile', { fileUrl: output.fileUrl })
285
+ if (!base64) {
286
+ throw error('unavailable', `Movie file could not be read at ${output.fileUrl}`)
287
+ }
288
+
289
+ const url = URL.createObjectURL(base64ToBlob(base64, mimeForFileUrl(output.fileUrl)))
290
+ return {
291
+ url,
292
+ fileUrl: output.fileUrl,
293
+ release: () => URL.revokeObjectURL(url),
294
+ }
295
+ }
package/src/index.ts CHANGED
@@ -2,3 +2,35 @@
2
2
  // the iOS or Android component for the current build target.
3
3
  export { CameraView } from './CameraView'
4
4
  export type { CameraAccessibilityRole, CameraViewHandle, CameraViewProps } from './props'
5
+ export { createCameraSession } from './session'
6
+ export { CameraCaptureError } from './types'
7
+ export type {
8
+ CameraCaptureErrorKind,
9
+ CameraDeviceInfo,
10
+ CameraFacing,
11
+ CameraInterruptionReason,
12
+ CameraOrientation,
13
+ CameraPermissionKind,
14
+ CameraPermissionStatus,
15
+ CameraPreviewPoint,
16
+ CameraProfileCapability,
17
+ CameraRecorderState,
18
+ CameraSelection,
19
+ CameraSession,
20
+ CameraSessionCapabilities,
21
+ CameraSessionConfig,
22
+ CameraSessionEvent,
23
+ CameraSessionSnapshot,
24
+ CaptureIssue,
25
+ EffectiveCaptureConfig,
26
+ MovieClip,
27
+ MovieEndReason,
28
+ MovieOutcome,
29
+ MovieOutput,
30
+ MovieOutputHandle,
31
+ MovieTake,
32
+ StartRecordingOptions,
33
+ } from './types'
34
+
35
+ export { createMemoryCameraStorage, createSyntheticCameraSession } from './synthetic-session'
36
+ export type { SyntheticCameraScript, SyntheticCameraStorage } from './synthetic-session'
package/src/index.web.ts CHANGED
@@ -1,2 +1,34 @@
1
1
  export { CameraView } from './CameraView.web.tsrx'
2
2
  export type { CameraAccessibilityRole, CameraViewHandle, CameraViewProps } from './props'
3
+ export { createCameraSession } from './session'
4
+ export { CameraCaptureError } from './types'
5
+ export type {
6
+ CameraCaptureErrorKind,
7
+ CameraDeviceInfo,
8
+ CameraFacing,
9
+ CameraInterruptionReason,
10
+ CameraOrientation,
11
+ CameraPermissionKind,
12
+ CameraPermissionStatus,
13
+ CameraPreviewPoint,
14
+ CameraProfileCapability,
15
+ CameraRecorderState,
16
+ CameraSelection,
17
+ CameraSession,
18
+ CameraSessionCapabilities,
19
+ CameraSessionConfig,
20
+ CameraSessionEvent,
21
+ CameraSessionSnapshot,
22
+ CaptureIssue,
23
+ EffectiveCaptureConfig,
24
+ MovieClip,
25
+ MovieEndReason,
26
+ MovieOutcome,
27
+ MovieOutput,
28
+ MovieOutputHandle,
29
+ MovieTake,
30
+ StartRecordingOptions,
31
+ } from './types'
32
+
33
+ export { createMemoryCameraStorage, createSyntheticCameraSession } from './synthetic-session'
34
+ export type { SyntheticCameraScript, SyntheticCameraStorage } from './synthetic-session'