Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 98 additions & 9 deletions ios/Sources/CameraPreviewPlugin/CameraController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ class CameraController: NSObject {
var audioInput: AVCaptureDeviceInput?

var zoomFactor: CGFloat = 1.0

/// All available rear cameras for manual switching (iOS 13+)
var availableRearCameras: [AVCaptureDevice] = []

/// Index of currently active rear camera in availableRearCameras array
var currentRearCameraIndex: Int = 0
}

extension CameraController {
Expand All @@ -45,25 +51,46 @@ extension CameraController {
}

func configureCaptureDevices() throws {
// Discover all physical camera types (not virtual multi-cam devices)
// This prevents iOS from auto-switching cameras based on lighting conditions
var deviceTypes: [AVCaptureDevice.DeviceType] = [.builtInWideAngleCamera]
if #available(iOS 13.0, *) {
deviceTypes.append(.builtInUltraWideCamera)
deviceTypes.append(.builtInTelephotoCamera)
}

let session = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified)
let session = AVCaptureDevice.DiscoverySession(
deviceTypes: deviceTypes,
mediaType: AVMediaType.video,
position: .unspecified
)

let cameras = session.devices.compactMap { $0 }
guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }

var rearCameras: [AVCaptureDevice] = []
for camera in cameras {
if camera.position == .front {
self.frontCamera = camera
}

if camera.position == .back {
self.rearCamera = camera

try camera.lockForConfiguration()
camera.focusMode = .continuousAutoFocus
camera.unlockForConfiguration()
rearCameras.append(camera)
}
}

// Store all rear cameras for manual switching
self.availableRearCameras = rearCameras

// Default to first rear camera
if let cam = rearCameras.first {
self.rearCamera = cam
self.currentRearCameraIndex = 0

try cam.lockForConfiguration()
cam.focusMode = .continuousAutoFocus
cam.unlockForConfiguration()
}

if disableAudio == false {
self.audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
}
Expand Down Expand Up @@ -366,9 +393,9 @@ extension CameraController {
var currentCamera: AVCaptureDevice?
switch currentCameraPosition {
case .front:
currentCamera = self.frontCamera!
currentCamera = self.frontCamera
case .rear:
currentCamera = self.rearCamera!
currentCamera = self.rearCamera
default: break
}

Expand All @@ -393,7 +420,69 @@ extension CameraController {
} catch {
throw CameraControllerError.invalidOperation
}
}

/// Switch to a specific rear camera by index
/// - Parameter index: Index in availableRearCameras array (0-based)
/// - Throws: CameraControllerError if session missing or index invalid
func switchToCamera(index: Int) throws {
guard let captureSession = self.captureSession else {
throw CameraControllerError.captureSessionIsMissing
}
guard index >= 0 && index < availableRearCameras.count else {
throw CameraControllerError.invalidOperation
}

let newCamera = availableRearCameras[index]

captureSession.beginConfiguration()

// Remove old input
if let oldInput = self.rearCameraInput {
captureSession.removeInput(oldInput)
}

// Add new input
let newInput = try AVCaptureDeviceInput(device: newCamera)
if captureSession.canAddInput(newInput) {
captureSession.addInput(newInput)
self.rearCameraInput = newInput
self.rearCamera = newCamera
self.currentRearCameraIndex = index
}

captureSession.commitConfiguration()

// Re-enable torch after camera switch
try? self.setTorchMode()
}

/// Get the number of available rear cameras
func getAvailableRearCameraCount() -> Int {
return availableRearCameras.count
}

/// Set zoom level on current camera
/// - Parameter zoomFactor: Zoom factor (1.0 = no zoom)
func setZoom(zoomFactor: CGFloat) throws {
var currentCamera: AVCaptureDevice?
switch currentCameraPosition {
case .front:
currentCamera = self.frontCamera
case .rear:
currentCamera = self.rearCamera
default: break
}

guard let device = currentCamera else {
throw CameraControllerError.noCamerasAvailable
}

try device.lockForConfiguration()
let clampedZoom = min(max(1.0, zoomFactor), device.activeFormat.videoMaxZoomFactor)
device.videoZoomFactor = clampedZoom
self.zoomFactor = clampedZoom
device.unlockForConfiguration()
}

func captureVideo(completion: @escaping (URL?, Error?) -> Void) {
Expand Down
38 changes: 38 additions & 0 deletions ios/Sources/CameraPreviewPlugin/CameraPreviewPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "flip", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getSupportedFlashModes", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setZoom", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "switchCamera", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getAvailableCameras", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "startRecordVideo", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "stopRecordVideo", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "isCameraStarted", returnType: CAPPluginReturnPromise)
Expand Down Expand Up @@ -335,4 +338,39 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin {
}
}

@objc func setZoom(_ call: CAPPluginCall) {
guard let zoomFactor = call.getDouble("zoom") else {
call.reject("zoom parameter is required")
return
}
do {
try self.cameraController.setZoom(zoomFactor: CGFloat(zoomFactor))
call.resolve()
} catch {
call.reject("failed to set zoom")
}
}

@objc func switchCamera(_ call: CAPPluginCall) {
guard let index = call.getInt("index") else {
call.reject("index parameter is required")
return
}
do {
try self.cameraController.switchToCamera(index: index)
call.resolve()
} catch {
call.reject("failed to switch camera")
}
}

@objc func getAvailableCameras(_ call: CAPPluginCall) {
let count = self.cameraController.getAvailableRearCameraCount()
let currentIndex = self.cameraController.currentRearCameraIndex
call.resolve([
"count": count,
"currentIndex": currentIndex
])
}

}
38 changes: 38 additions & 0 deletions src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ export interface CameraOpacityOptions {
opacity?: number;
}

export interface CameraZoomOptions {
/** The zoom factor (1.0 = no zoom, higher = more zoom) */
zoom: number;
}

export interface CameraSwitchOptions {
/** Index of rear camera to switch to (0-based). Use getAvailableCameras() to get count. */
index: number;
}

export interface AvailableCamerasResult {
/** Number of available rear cameras */
count: number;
/** Index of currently active camera */
currentIndex: number;
}

export interface CameraPreviewPlugin {
start(options: CameraPreviewOptions): Promise<void>;
startRecordVideo(options: CameraPreviewOptions): Promise<void>;
Expand All @@ -72,4 +89,25 @@ export interface CameraPreviewPlugin {
flip(): Promise<void>;
setOpacity(options: CameraOpacityOptions): Promise<void>;
isCameraStarted(): Promise<{ value: boolean }>;
/**
* Set the zoom level of the current camera
* @param options Zoom options with zoom factor
* @since 6.1.0
*/
setZoom(options: CameraZoomOptions): Promise<void>;
/**
* Switch to a specific rear camera by index.
* Useful for multi-camera devices where you need to select a specific physical camera.
* Only available on iOS 13+ devices with multiple rear cameras.
* @param options Switch options with camera index
* @since 6.1.0
*/
switchCamera(options: CameraSwitchOptions): Promise<void>;
/**
* Get information about available rear cameras.
* Returns the count of cameras and current camera index.
* Only available on iOS.
* @since 6.1.0
*/
getAvailableCameras(): Promise<AvailableCamerasResult>;
}