capacitor-community-multilens-camerapreview
v8.4.0
Published
fork of capacitor community camera preview with support for switchting lenses
Readme
Capacitor Camera Preview Multi-Lens
Capacitor camera preview plugin with native multi-lens support for iOS and Android.
This fork is focused on real-world camera UX rather than a generic preview only. It adds native-feeling rear camera zoom behavior, direct lens targeting, hardware shutter integration, zoom state events, and smoother multi-lens handling across devices that expose either true physical cameras or only logical cameras.
Highlights
- Native multi-lens switching for rear and front cameras.
- Use a numeric zoom value or a specific lens ID with the same
setZoom()API. zoomChangedevents with the active lens, global zoom factor, per-lens zoom factor, and alensChangedflag.- Hardware shutter integration.
- Android: volume up and dedicated camera keys can trigger native capture.
- iOS: native hardware camera controls are surfaced when available.
- Faster native capture paths and lower-latency repeated captures.
- Android logical-camera support with synthesized rear lens presets when vendors hide physical cameras behind a single logical camera.
- Pinch-to-zoom, tap-to-focus, flash modes, front/rear flip, and preview opacity.
- Angular service included out of the box.
Installation
npm install capacitor-community-multilens-camerapreview
npx cap syncExported API
The package exports:
CameraPreviewMultiLensfor direct Capacitor plugin usage.CameraPreviewServicefor Angular apps.- All TypeScript types from
src/definitions.ts.
Quick Start
Direct Capacitor Usage
import {
CameraPreviewMultiLens,
type CameraLens,
type ZoomChangedEvent,
} from 'capacitor-community-multilens-camerapreview';
let lenses: CameraLens[] = [];
await CameraPreviewMultiLens.start({
position: 'rear',
toBack: true,
enableZoom: true,
quality: 'medium',
imageScale: 'fill',
storeToFile: false,
});
const lensResponse = await CameraPreviewMultiLens.getSupportedZoomLevels();
lenses = lensResponse.result;
await CameraPreviewMultiLens.addListener('zoomChanged', (event: ZoomChangedEvent) => {
console.log('Active lens:', event.lens.name);
console.log('Global zoom:', event.zoomFactor);
console.log('Current lens zoom:', event.lensZoomFactor);
console.log('Lens changed:', event.lensChanged);
});
const photo = await CameraPreviewMultiLens.capture({ quality: 90 });
const imageSrc = `data:image/jpeg;base64,${photo.value}`;Angular Service Usage
import { Component, OnDestroy, OnInit } from '@angular/core';
import {
CameraPreviewService,
type CameraLens,
type HardwareShutterEvent,
type ZoomChangedEvent,
} from 'capacitor-community-multilens-camerapreview';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-camera',
templateUrl: './camera.component.html',
})
export class CameraComponent implements OnInit, OnDestroy {
lenses: CameraLens[] = [];
activeZoom = 1;
private subscriptions = new Subscription();
constructor(private cameraPreview: CameraPreviewService) {}
async ngOnInit(): Promise<void> {
await this.cameraPreview.start({
position: 'rear',
toBack: true,
enableZoom: true,
quality: 'medium',
imageScale: 'fill',
storeToFile: false,
});
this.lenses = await this.cameraPreview.getLenses();
this.subscriptions.add(
this.cameraPreview.zoomChanged$.subscribe((event: ZoomChangedEvent) => {
this.activeZoom = event.zoomFactor;
})
);
this.subscriptions.add(
this.cameraPreview.hardwareShutterPressed$.subscribe((event: HardwareShutterEvent) => {
if (event.error) {
console.error('Hardware shutter error:', event.error);
return;
}
if (event.value) {
const src = event.storeToFile
? event.value
: `data:image/jpeg;base64,${event.value}`;
console.log('Native hardware shutter result:', src);
}
})
);
}
async selectLens(lens: CameraLens): Promise<void> {
await this.cameraPreview.setZoom(lens.id);
}
async setNumericZoom(zoom: number): Promise<void> {
await this.cameraPreview.setZoom(zoom);
}
async takePhoto(): Promise<void> {
const value = await this.cameraPreview.capture({ quality: 90 });
console.log('Captured image:', value);
}
async ngOnDestroy(): Promise<void> {
this.subscriptions.unsubscribe();
await this.cameraPreview.stop();
}
}Lens Selection and Zoom
The plugin supports two different setZoom() modes:
1. Pass a Lens ID
Use a lens ID from getSupportedZoomLevels() when you want a deterministic lens target such as ultra-wide, main, or telephoto.
const { result: lenses } = await CameraPreviewMultiLens.getSupportedZoomLevels();
const ultraWide = lenses.find((lens) => lens.zoomFactor < 1);
if (ultraWide) {
await CameraPreviewMultiLens.setZoom({ zoomFactor: ultraWide.id });
}2. Pass a Numeric Zoom
Use a numeric zoom factor when you want continuous zoom behavior.
await CameraPreviewMultiLens.setZoom({ zoomFactor: 1.8 });
await CameraPreviewMultiLens.setZoom({ zoomFactor: 2.5 });How Native Zoom Behaves
- iOS uses native rear camera behavior so zoom transitions feel closer to the stock camera experience.
- Android switches between real lenses when available.
- On Android devices that only expose a single logical rear camera through CameraX, the plugin synthesizes reachable presets like
2xand3xso your UI can still present lens buttons. zoomChangedis emitted after pinch gestures, lens changes, and native zoom updates.
Lens Picker UI Example
const { result: lenses } = await CameraPreviewMultiLens.getSupportedZoomLevels();
for (const lens of lenses) {
console.log(lens.id, lens.name, lens.zoomFactor, lens.focalLength);
}
// Example button click handler
async function onLensSelected(lensId: string): Promise<void> {
await CameraPreviewMultiLens.setZoom({ zoomFactor: lensId });
}Tap To Focus
Tap-to-focus is handled natively by the preview layer. No extra JavaScript API call is required.
Hardware Shutter Events
The plugin emits hardwareShutterPressed when a native hardware shutter path is used.
Event Shape
interface HardwareShutterEvent {
source: 'volume-button' | 'camera-button' | 'camera-control';
value?: string;
storeToFile?: boolean;
error?: string;
}Notes
valuecontains the capture result when the native side already completed a photo capture.- If
storeToFileistrue,valueis a file URI. - Otherwise
valueis base64 JPEG data. erroris populated when the native hardware shutter capture failed.
Example
await CameraPreviewMultiLens.addListener('hardwareShutterPressed', (event) => {
if (event.error) {
console.error(event.error);
return;
}
if (!event.value) {
return;
}
const output = event.storeToFile
? event.value
: `data:image/jpeg;base64,${event.value}`;
console.log('Hardware shutter capture:', output, event.source);
});Zoom State Events
zoomChanged gives you the active lens and zoom state so you can keep your UI in sync with native pinch gestures and automatic lens transitions.
interface ZoomChangedEvent {
lens: CameraLens;
zoomFactor: number;
lensZoomFactor: number;
lensChanged: boolean;
}Example UI Sync
await CameraPreviewMultiLens.addListener('zoomChanged', (event) => {
activeLensId = event.lens.id;
displayedZoom = event.zoomFactor;
isLensTransition = event.lensChanged;
});Capture Examples
Base64 Capture
const result = await CameraPreviewMultiLens.capture({ quality: 85 });
const src = `data:image/jpeg;base64,${result.value}`;File Capture
await CameraPreviewMultiLens.start({
position: 'rear',
enableZoom: true,
storeToFile: true,
});
const result = await CameraPreviewMultiLens.capture({ quality: 90 });
console.log('File URI:', result.value);Low-Resolution Sample Capture
const sample = await CameraPreviewMultiLens.captureSample({ quality: 40 });API Reference
Types
CameraPreviewOptions
interface CameraPreviewOptions {
parent?: string;
className?: string;
width?: number;
height?: number;
x?: number;
y?: number;
toBack?: boolean;
paddingBottom?: number;
rotateWhenOrientationChanged?: boolean;
position?: 'rear' | 'front' | string;
quality?: 'low' | 'medium' | 'high' | 'ultra';
imageScale?: 'fill' | 'fit';
storeToFile?: boolean;
disableExifHeaderStripping?: boolean;
enableHighResolution?: boolean;
disableAudio?: boolean;
lockAndroidOrientation?: boolean;
enableOpacity?: boolean;
enableZoom?: boolean;
zoomFactor?: string | number;
}Important fields:
position:'rear'or'front'.quality: image quality tier (low/medium/high/ultra) that drives JPEG quality and capture behavior.imageScale:'fill'to crop and fill the container or'fit'to letterbox and show the full sensor frame.enableZoom: enables native pinch-to-zoom handling.zoomFactor: accepts either a numeric zoom or a lens ID string.storeToFile: changes the default capture result mode.toBack: shows the native preview behind your web UI on iOS and Android.
CameraPreviewPictureOptions
interface CameraPreviewPictureOptions {
height?: number;
width?: number;
quality?: number;
storeToFile?: boolean;
}CameraLens
interface CameraLens {
id: string;
name: string;
facing: 'front' | 'rear';
zoomFactor: number;
focalLength?: number;
}Methods
start(options)
Starts the native camera preview.
await CameraPreviewMultiLens.start({
position: 'rear',
toBack: true,
enableZoom: true,
});stop()
Stops the preview and releases native camera resources.
capture(options)
Captures a photo and returns:
- base64 JPEG by default.
- file URI when
storeToFileis enabled.
captureSample(options)
Captures a lower-cost sample frame for scanning or analysis.
getSupportedZoomLevels()
Returns lens descriptors for your zoom UI.
const { result } = await CameraPreviewMultiLens.getSupportedZoomLevels();setZoom({ zoomFactor })
Accepts either:
- a number such as
1.5or2.0. - a lens ID string returned by
getSupportedZoomLevels().
getSupportedFlashModes()
Returns the flash modes available for the active camera.
setFlashMode({ flashMode })
getAvailableResolutions()
Returns the pixel size and JPEG compression quality that will be used at each image quality tier on the current device.
const { result } = await CameraPreviewMultiLens.getAvailableResolutions();
console.log(result.low, result.medium, result.high, result.ultra);setImageScale({ imageScale })
Controls how the preview content is fitted into its container:
fill: default; crops to fill the view.fit: letterboxes to keep the full sensor frame visible (black bars where needed).
Sets the flash mode for the current camera.
Supported values in the shared type:
offonautored-eyetorch
Native support depends on platform and active camera.
flip()
Switches between the front and rear camera.
setOpacity({ opacity })
Adjusts preview transparency.
- Android: supported.
- Web: supported.
- iOS: do not rely on this.
resume()
Restarts or resumes the preview after returning from the background.
Angular Service Reference
CameraPreviewService wraps the Capacitor plugin and exposes RxJS streams.
Observables
isCameraActive$errors$hardwareShutterPressed$zoomChanged$
Service Methods
start(options)stop()capture(options)captureSample(options)setFlashMode(flashMode)setZoom(zoomFactor)setOpacity(opacity)flip()getLenses()getSupportedFlashModes()resume()
Additional helpers:
getAvailableResolutions()– wrapper around the plugin method, returns the per-tier resolution/quality map ornullon error.setImageScale(imageScale)– updates preview scaling (fill vs fit) at runtime.
Platform Notes
iOS
- Rear camera zoom is tuned to behave more like the native camera app.
- High-resolution capture remains supported.
- Hardware shutter and camera-control integration are surfaced when the device supports them.
Android
- Works with devices that expose true physical lenses and with devices that only expose logical rear cameras.
- If the vendor camera stack hides physical rear lenses, the plugin can still expose synthetic reachable presets so your UI is not limited to a single
1xbutton. - Pinch-to-zoom is native and optimized for continuous gesture updates.
- Volume-up and dedicated camera keys can trigger native capture.
Web
- Basic preview and capture are supported.
- Lens switching, flash control, and native zoom are not implemented on web.
UI Layering Notes
If you use toBack: true, your web layer must be transparent.
For Ionic apps, that usually means making the page background transparent:
html,
body,
ion-app,
ion-content {
--background: transparent;
background: transparent;
}Practical Recommendations
- Use
getSupportedZoomLevels()to build the lens picker UI. - Subscribe to
zoomChangedso your selected lens button stays in sync with pinch gestures. - Prefer lens IDs for discrete buttons like
.5x,1x,2x, and3x. - Prefer numeric
setZoom()for sliders and gesture-driven zoom. - Enable
storeToFileif you do not want large base64 payloads crossing the bridge.
Known Behavior
- Available lenses vary by vendor and device.
- On some Android phones, telephoto and ultra-wide entries may be synthesized from reachable camera metadata instead of coming from directly bindable camera IDs.
- Flash support depends on the currently active camera and vendor capabilities.
License
MIT
