Using Camera
Open the system camera to take a photo or record a video, and get the captured file back — on both iOS and Android.
Basic usage
The whole surface lives on a single Camera object. On Android, capture auto-requests the CAMERA permission, so you can call takePicture directly and catch a denial:
import { Camera } from '@sigx/lynx-camera';
async function snap() {
try {
const photo = await Camera.takePicture({ facing: 'back', quality: 0.8 });
if (!photo.uri) return; // user cancelled
console.log(photo.uri, photo.width, photo.height);
} catch (err) {
console.warn('Camera unavailable or permission denied', err);
}
}
takePicture follows a three-outcome contract: it resolves with a PhotoResult (always carrying a uri) on success, resolves with { cancelled: true } (no uri) if the user dismisses the camera, and throws on a real failure (permission denied, no camera, …). Narrow on uri for the success path and try / catch failures.
The uri is normalized to a scheme: file:// on iOS, content:// on Android (backed by a JPEG in the app cache directory). Move or upload it promptly, since the OS may reclaim cached files.
Recording video
recordVideo opens the camera in video mode and follows the exact same three-outcome contract as takePicture. The returned uri is loadable directly by @sigx/lynx-video's VideoPlayer:
import { component, signal } from '@sigx/lynx';
import { Camera } from '@sigx/lynx-camera';
import { VideoPlayer } from '@sigx/lynx-video';
const Recorder = component(() => {
const clip = signal<string | null>(null);
const record = async () => {
try {
const result = await Camera.recordVideo({ maxDurationMs: 30_000 });
if (result.uri) clip.set(result.uri); // else: user cancelled
} catch (err) {
console.warn('Recording failed', err);
}
};
return () => (
<view>
<text bindtap={record}>Record</text>
{clip() ? (
<VideoPlayer src={clip()!} controls style={{ width: '100%', aspectRatio: 16 / 9 }} />
) : null}
</view>
);
});
maxDurationMs caps the recording length on iOS (videoMaximumDuration); Android's ACTION_VIDEO_CAPTURE intent exposes no duration cap. On Android, recordVideo auto-requests the CAMERA permission, and also microphone when the app declares RECORD_AUDIO (e.g. by installing @sigx/lynx-audio). A single in-camera photo/video toggle is iOS-only at the system level — present a "photo or video" choice in your own UI and call the matching method.
Installing and linking
Install the package, then let the CLI wire up the native module:
pnpm add @sigx/lynx-camera
sigx prebuildsigx prebuild auto-discovers the package, links the native module, injects android.permission.CAMERA, and adds the iOS usage descriptions (NSCameraUsageDescription, NSMicrophoneUsageDescription, NSPhotoLibraryAddUsageDescription). The companion @sigx/lynx-permissions package is auto-linked too — there is nothing extra to install.
To customize the iOS permission prompt copy, set it in signalx.config.ts:
import { defineLynxConfig } from '@sigx/lynx-cli/config';
export default defineLynxConfig({
ios: {
usageDescriptions: {
NSCameraUsageDescription: 'Acme uses the camera to scan QR codes.',
},
},
});
Handling permissions properly
Calling requestPermission before capture is optional — takePicture / recordVideo auto-request the CAMERA permission on Android (iOS auto-requests at the capture layer), and a denial surfaces as a thrown error you can catch. Request explicitly when you want to gate your own UI ahead of time or inspect canAskAgain. Use getPermissionStatus for a silent check (it never prompts) and requestPermission to show the OS dialog:
import { Camera } from '@sigx/lynx-camera';
async function ensureCamera(): Promise<boolean> {
let { status, canAskAgain } = await Camera.getPermissionStatus();
if (status === 'undetermined') {
({ status, canAskAgain } = await Camera.requestPermission());
}
if (status === 'granted') return true;
// status is 'denied' or 'blocked'. If canAskAgain is false the user must
// re-enable the permission from system Settings — prompting again is a no-op.
return false;
}
canAskAgain is false once the user picks "Don't ask again" on Android, or after the first denial on iOS (where the only path back is the Settings app). Surface a "open Settings" message in that case rather than re-requesting.
Capturing and using a photo
A complete capture flow. A user cancel resolves with { cancelled: true } (no uri); a real failure — permission denied, no camera, or a missing native module — throws. So narrow on uri for the success path and wrap the call in try / catch:
import { Camera } from '@sigx/lynx-camera';
import type { PhotoResult } from '@sigx/lynx-camera';
async function capturePortrait(): Promise<PhotoResult | null> {
if (!Camera.isAvailable()) {
console.warn('Camera module not present in this build');
return null;
}
try {
const result = await Camera.takePicture({ facing: 'front', quality: 0.9 });
// Cancel resolves with { cancelled: true } and no uri.
if (!result.uri) {
console.warn('Capture cancelled', result);
return null;
}
return result;
} catch (err) {
console.warn('Capture failed or permission denied', err);
return null;
}
}
Notes
- Options are mostly iOS-only. iOS honors
facingand JPEGquality(default0.8) for photos andfacing/maxDurationMsfor video; Android's capture intents ignore options (the user can still switch cameras in the system UI).CameraOptions.maxWidth/maxHeightare reserved and not yet applied on either platform. - Three outcomes, not one shape. Only a successful capture returns a
PhotoResult/VideoResultwith auri. A cancel resolves with{ cancelled: true }(nouri); a real failure throws. Narrow onurifor success andtry/catchfailures. Metadata beyonduri(iOSfileSize, dimensions) is best-effort;base64is declared but not populated by either native implementation today. isAvailable()is JS-only — it reports whether the native module is registered in the build; it does not check hardware. iOS additionally gates real capture onUIImagePickerControllersource availability.- Test on a physical device. The iOS Simulator has no camera, so
UIImagePickerController's camera source is unavailable there and capture fails —isAvailable()is a JS-only module check and won't catch this. Run capture on a real device. - If you customize
AndroidManifest.xml, keep the auto-injectedFileProvider<provider>(authority${applicationId}.fileprovider) intact — the capture intent needs it as a write target.
See also
- Overview — what the package is and how it fits the family.
- API reference — every export with signatures.
