@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
package/src/index.web.ts
CHANGED
|
@@ -1,2 +1,33 @@
|
|
|
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
|
+
CameraProfileCapability,
|
|
14
|
+
CameraRecorderState,
|
|
15
|
+
CameraSelection,
|
|
16
|
+
CameraSession,
|
|
17
|
+
CameraSessionCapabilities,
|
|
18
|
+
CameraSessionConfig,
|
|
19
|
+
CameraSessionEvent,
|
|
20
|
+
CameraSessionSnapshot,
|
|
21
|
+
CaptureIssue,
|
|
22
|
+
EffectiveCaptureConfig,
|
|
23
|
+
MovieClip,
|
|
24
|
+
MovieEndReason,
|
|
25
|
+
MovieOutcome,
|
|
26
|
+
MovieOutput,
|
|
27
|
+
MovieOutputHandle,
|
|
28
|
+
MovieTake,
|
|
29
|
+
StartRecordingOptions,
|
|
30
|
+
} from './types'
|
|
31
|
+
|
|
32
|
+
export { createMemoryCameraStorage, createSyntheticCameraSession } from './synthetic-session'
|
|
33
|
+
export type { SyntheticCameraScript, SyntheticCameraStorage } from './synthetic-session'
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CameraViewProps } from './props'
|
|
2
|
+
|
|
3
|
+
/** Windows twins of the layout-child helpers — the unsuffixed module
|
|
4
|
+
* resolves the platform escape bag through `isIOS`, which has no Windows
|
|
5
|
+
* branch. Pure helpers are re-declared here to keep divergence at the file
|
|
6
|
+
* boundary. */
|
|
7
|
+
|
|
8
|
+
export function layoutChildProps(props: CameraViewProps): Record<string, any> {
|
|
9
|
+
const result: Record<string, any> = {}
|
|
10
|
+
for (const key of [
|
|
11
|
+
'row',
|
|
12
|
+
'col',
|
|
13
|
+
'rowSpan',
|
|
14
|
+
'colSpan',
|
|
15
|
+
'dock',
|
|
16
|
+
'left',
|
|
17
|
+
'top',
|
|
18
|
+
'flexGrow',
|
|
19
|
+
'flexShrink',
|
|
20
|
+
'alignSelf',
|
|
21
|
+
'order',
|
|
22
|
+
] as const) {
|
|
23
|
+
if (props[key] !== undefined) {
|
|
24
|
+
result[key] = props[key]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function applyEscapeProps(view: any, props: { windows?: any }): void {
|
|
32
|
+
if (view && props.windows) {
|
|
33
|
+
Object.assign(view, props.windows)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const nativeRoles: Record<string, string> = {
|
|
38
|
+
button: 'button',
|
|
39
|
+
link: 'link',
|
|
40
|
+
search: 'search',
|
|
41
|
+
image: 'image',
|
|
42
|
+
heading: 'header',
|
|
43
|
+
adjustable: 'adjustable',
|
|
44
|
+
summary: 'summary',
|
|
45
|
+
text: 'text',
|
|
46
|
+
none: 'none',
|
|
47
|
+
progressbar: 'progressBar',
|
|
48
|
+
checkbox: 'checkbox',
|
|
49
|
+
switch: 'switch',
|
|
50
|
+
radio: 'radioButton',
|
|
51
|
+
spinbutton: 'spinButton',
|
|
52
|
+
tab: 'button',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function nativeAccessibilityRole(role?: string): string | undefined {
|
|
56
|
+
return role ? nativeRoles[role] : undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function nativeAccessibilityState(
|
|
60
|
+
state?: CameraViewProps['accessibilityState'],
|
|
61
|
+
): string | undefined {
|
|
62
|
+
if (state?.disabled) {
|
|
63
|
+
return 'disabled'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (state?.selected) {
|
|
67
|
+
return 'selected'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (state?.checked === true) {
|
|
71
|
+
return 'checked'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (state?.checked === false) {
|
|
75
|
+
return 'unchecked'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { BlobSource, Input, MP4, WEBM } from 'mediabunny'
|
|
2
|
+
import { CameraCaptureError } from './types'
|
|
3
|
+
|
|
4
|
+
/** Read packet end timestamps and track metadata, never the wall-clock stop time. */
|
|
5
|
+
export async function inspectMovie(bytes: Blob, audioRequested: boolean) {
|
|
6
|
+
if (!bytes.size) {
|
|
7
|
+
throw new CameraCaptureError({
|
|
8
|
+
kind: 'noMedia',
|
|
9
|
+
operation: 'finalize',
|
|
10
|
+
message: 'The recorder produced no movie bytes',
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const input = new Input({ source: new BlobSource(bytes), formats: [WEBM, MP4] })
|
|
15
|
+
const read = async () => {
|
|
16
|
+
const video = await input.getPrimaryVideoTrack()
|
|
17
|
+
const audio = await input.getPrimaryAudioTrack()
|
|
18
|
+
if (!video) {
|
|
19
|
+
throw new CameraCaptureError({
|
|
20
|
+
kind: 'noMedia',
|
|
21
|
+
operation: 'finalize',
|
|
22
|
+
message: 'The movie contains no video track',
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const videoDuration = await video.computeDuration()
|
|
27
|
+
const hasAudio = audio !== null && (await audio.computeDuration()) > 0
|
|
28
|
+
const durationMs = (await input.computeDuration()) * 1000
|
|
29
|
+
const width = await video.getDisplayWidth()
|
|
30
|
+
const height = await video.getDisplayHeight()
|
|
31
|
+
if (
|
|
32
|
+
!Number.isFinite(durationMs) ||
|
|
33
|
+
durationMs <= 0 ||
|
|
34
|
+
videoDuration <= 0 ||
|
|
35
|
+
width <= 0 ||
|
|
36
|
+
height <= 0
|
|
37
|
+
) {
|
|
38
|
+
throw new CameraCaptureError({
|
|
39
|
+
kind: 'noMedia',
|
|
40
|
+
operation: 'finalize',
|
|
41
|
+
message: 'The movie has no usable video media',
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (audioRequested && !hasAudio) {
|
|
46
|
+
throw new CameraCaptureError({
|
|
47
|
+
kind: 'captureFailed',
|
|
48
|
+
operation: 'finalize',
|
|
49
|
+
message: 'Requested audio is absent from the finalized movie',
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
durationMs,
|
|
55
|
+
width,
|
|
56
|
+
height,
|
|
57
|
+
hasAudio,
|
|
58
|
+
mimeType: await input.getMimeType(),
|
|
59
|
+
orientation: 'unspecified' as const,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
64
|
+
try {
|
|
65
|
+
return await Promise.race([
|
|
66
|
+
read(),
|
|
67
|
+
new Promise<never>((_resolve, reject) => {
|
|
68
|
+
timer = setTimeout(() => {
|
|
69
|
+
input.dispose()
|
|
70
|
+
reject(
|
|
71
|
+
new CameraCaptureError({
|
|
72
|
+
kind: 'finalizationFailed',
|
|
73
|
+
operation: 'metadata',
|
|
74
|
+
message: 'Movie metadata verification timed out',
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
}, 30_000)
|
|
78
|
+
}),
|
|
79
|
+
])
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer)
|
|
82
|
+
input.dispose()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { CameraCaptureError } from './types'
|
|
2
|
+
import type { MovieClip, MovieOutput, MovieOutputHandle } from './types'
|
|
3
|
+
|
|
4
|
+
const databaseName = 'octane-xplat-camera'
|
|
5
|
+
const storeName = 'movies'
|
|
6
|
+
|
|
7
|
+
type StoredMovie = { id: string; clip?: MovieClip; bytes?: Blob }
|
|
8
|
+
|
|
9
|
+
const storageError = (cause: unknown) =>
|
|
10
|
+
new CameraCaptureError({
|
|
11
|
+
kind:
|
|
12
|
+
cause instanceof DOMException && cause.name === 'QuotaExceededError'
|
|
13
|
+
? 'insufficientStorage'
|
|
14
|
+
: 'storageFailed',
|
|
15
|
+
operation: 'storage',
|
|
16
|
+
message: 'The movie could not be committed to origin-local storage',
|
|
17
|
+
cause,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
/** Each operation closes its database; playback survives session and page lifetimes. */
|
|
21
|
+
async function transact<T>(
|
|
22
|
+
mode: IDBTransactionMode,
|
|
23
|
+
operation: (store: IDBObjectStore, result: (value: T) => void) => void,
|
|
24
|
+
): Promise<T> {
|
|
25
|
+
return new Promise<T>((resolve, reject) => {
|
|
26
|
+
let database: IDBDatabase | undefined
|
|
27
|
+
let transaction: IDBTransaction | undefined
|
|
28
|
+
let result: T
|
|
29
|
+
let done = false
|
|
30
|
+
const finish = (cause?: unknown) => {
|
|
31
|
+
if (done) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
done = true
|
|
36
|
+
clearTimeout(timer)
|
|
37
|
+
database?.close()
|
|
38
|
+
if (cause) {
|
|
39
|
+
reject(storageError(cause))
|
|
40
|
+
} else {
|
|
41
|
+
resolve(result)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const timer = setTimeout(() => {
|
|
46
|
+
transaction?.abort()
|
|
47
|
+
finish(new Error('Storage transaction timed out'))
|
|
48
|
+
}, 15_000)
|
|
49
|
+
|
|
50
|
+
const request = indexedDB.open(databaseName, 1)
|
|
51
|
+
request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: 'id' })
|
|
52
|
+
request.onerror = () => finish(request.error)
|
|
53
|
+
request.onblocked = () => finish(new Error('Storage upgrade is blocked by another page'))
|
|
54
|
+
request.onsuccess = () => {
|
|
55
|
+
database = request.result
|
|
56
|
+
if (done) {
|
|
57
|
+
database.close()
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
transaction = database.transaction(storeName, mode, { durability: 'strict' })
|
|
63
|
+
transaction.oncomplete = () => finish()
|
|
64
|
+
transaction.onabort = () =>
|
|
65
|
+
finish(transaction?.error ?? new Error('Storage transaction aborted'))
|
|
66
|
+
|
|
67
|
+
transaction.onerror = () =>
|
|
68
|
+
finish(transaction?.error ?? new Error('Storage transaction failed'))
|
|
69
|
+
|
|
70
|
+
operation(transaction.objectStore(storeName), (value) => {
|
|
71
|
+
result = value
|
|
72
|
+
})
|
|
73
|
+
} catch (cause) {
|
|
74
|
+
finish(cause)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function reserveMovie(): Promise<string> {
|
|
81
|
+
const estimate = await navigator.storage.estimate()
|
|
82
|
+
if (
|
|
83
|
+
estimate.quota !== undefined &&
|
|
84
|
+
estimate.usage !== undefined &&
|
|
85
|
+
estimate.quota - estimate.usage < 1_048_576
|
|
86
|
+
) {
|
|
87
|
+
throw new CameraCaptureError({
|
|
88
|
+
kind: 'insufficientStorage',
|
|
89
|
+
operation: 'reserve',
|
|
90
|
+
message: 'Origin storage has less than 1 MiB free',
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const id = crypto.randomUUID()
|
|
95
|
+
await transact<void>('readwrite', (store) => {
|
|
96
|
+
store.add({ id })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return id
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function commitMovie(id: string, bytes: Blob, clip: MovieClip): Promise<void> {
|
|
103
|
+
if (!(await navigator.storage.persisted())) {
|
|
104
|
+
throw new CameraCaptureError({
|
|
105
|
+
kind: 'destinationUnavailable',
|
|
106
|
+
operation: 'storage',
|
|
107
|
+
message: 'Persistent origin storage is no longer granted',
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await transact<void>('readwrite', (store) => {
|
|
112
|
+
store.put({ id, bytes, clip } satisfies StoredMovie)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Reopen the committed entry before handing ownership to the app.
|
|
116
|
+
const stored = await readMovie(id)
|
|
117
|
+
if (!stored.bytes || stored.bytes.size !== bytes.size || !stored.clip) {
|
|
118
|
+
throw storageError(new Error('Committed movie cannot be reopened'))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function removeReservation(id: string): Promise<void> {
|
|
123
|
+
await transact<void>('readwrite', (store) => {
|
|
124
|
+
store.delete(id)
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function readMovie(id: string): Promise<StoredMovie> {
|
|
129
|
+
return transact<StoredMovie>('readonly', (store, result) => {
|
|
130
|
+
const request = store.get(id)
|
|
131
|
+
request.onsuccess = () => result(request.result ?? { id })
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function openStoredMovie(output: MovieOutput): Promise<MovieOutputHandle> {
|
|
136
|
+
if (output.kind !== 'browserStorage' || output.synthetic) {
|
|
137
|
+
throw new CameraCaptureError({
|
|
138
|
+
kind: 'invalidArgument',
|
|
139
|
+
operation: 'openOutput',
|
|
140
|
+
message: 'Expected an origin-local movie reference',
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const stored = await readMovie(output.resourceId)
|
|
145
|
+
if (!stored.bytes || !stored.clip) {
|
|
146
|
+
throw new CameraCaptureError({
|
|
147
|
+
kind: 'destinationUnavailable',
|
|
148
|
+
operation: 'openOutput',
|
|
149
|
+
message: 'Movie is missing or site data was cleared',
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const url = URL.createObjectURL(stored.bytes)
|
|
154
|
+
let released = false
|
|
155
|
+
return {
|
|
156
|
+
url,
|
|
157
|
+
release: () => {
|
|
158
|
+
if (!released) {
|
|
159
|
+
released = true
|
|
160
|
+
URL.revokeObjectURL(url)
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
}
|
package/src/props.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Octane } from 'octane/jsx-runtime'
|
|
2
|
+
import type { CameraSession } from './types'
|
|
2
3
|
|
|
3
4
|
export type CameraAccessibilityRole =
|
|
4
5
|
| 'button'
|
|
@@ -48,7 +49,11 @@ export interface CameraViewProps {
|
|
|
48
49
|
checked?: boolean
|
|
49
50
|
}
|
|
50
51
|
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive'
|
|
51
|
-
/**
|
|
52
|
+
/** Shared camera owner enabling movie recording. When set, the preview
|
|
53
|
+
* attaches to the session's acquisition instead of owning one, and
|
|
54
|
+
* `facing` must be omitted — camera selection lives on the session. */
|
|
55
|
+
session?: CameraSession
|
|
56
|
+
/** Lens — defaults to the rear camera. Invalid together with `session`. */
|
|
52
57
|
facing?: 'front' | 'back'
|
|
53
58
|
/** Default true. False releases the preview session. */
|
|
54
59
|
active?: boolean
|
|
@@ -61,4 +66,5 @@ export interface CameraViewProps {
|
|
|
61
66
|
ios?: Record<string, any>
|
|
62
67
|
android?: Record<string, any>
|
|
63
68
|
web?: Record<string, any>
|
|
69
|
+
windows?: Record<string, any>
|
|
64
70
|
}
|