Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support tap to focus without a Preview View #3089

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.mrousavy.camera.core.types

import com.mrousavy.camera.core.InvalidTypeScriptUnionError

enum class CoordinateSystem(override val unionValue: String) : JSUnionValue {
PREVIEW_VIEW("preview-view"),
CAMERA("camera");

companion object : JSUnionValue.Companion<CoordinateSystem> {
override fun fromUnionValue(unionValue: String?): CoordinateSystem =
when (unionValue) {
"preview-view" -> PREVIEW_VIEW
"camera" -> CAMERA
else -> throw InvalidTypeScriptUnionError("coordinateSystem", unionValue)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
package com.mrousavy.camera.react

import android.content.res.Resources
import com.facebook.react.bridge.ReadableMap
import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import com.mrousavy.camera.core.FocusRequiresPreviewError
import com.mrousavy.camera.core.focus
import com.mrousavy.camera.core.utils.runOnUiThreadAndWait

suspend fun CameraView.focus(pointMap: ReadableMap) {
val x = pointMap.getDouble("x")
val y = pointMap.getDouble("y")
suspend fun CameraView.focusInPreviewViewCoordinates(x: Float, y: Float) {
val previewView = previewView ?: throw FocusRequiresPreviewError()

val point = runOnUiThreadAndWait {
val dp = Resources.getSystem().displayMetrics.density
previewView.meteringPointFactory.createPoint(x.toFloat() * dp, y.toFloat() * dp)
previewView.meteringPointFactory.createPoint(x * dp, y * dp)
}
cameraSession.focus(point)
}

suspend fun CameraView.focusInCameraCoordinates(x: Float, y: Float) {
val factory = SurfaceOrientedMeteringPointFactory(1f, 1f)
val point = factory.createPoint(x, y)
cameraSession.focus(point)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import com.facebook.react.uimanager.common.UIManagerType
import com.mrousavy.camera.BuildConfig
import com.mrousavy.camera.core.CameraError
import com.mrousavy.camera.core.CameraQueues
import com.mrousavy.camera.core.InvalidTypeScriptUnionError
import com.mrousavy.camera.core.ViewNotFoundError
import com.mrousavy.camera.core.types.CoordinateSystem
import com.mrousavy.camera.core.types.PermissionStatus
import com.mrousavy.camera.core.types.RecordVideoOptions
import com.mrousavy.camera.core.types.TakeSnapshotOptions
Expand Down Expand Up @@ -178,11 +180,18 @@ class CameraViewModule(reactContext: ReactApplicationContext) : ReactContextBase
}

@ReactMethod
fun focus(viewTag: Int, point: ReadableMap, promise: Promise) {
fun focus(viewTag: Int, focusOptions: ReadableMap, promise: Promise) {
backgroundCoroutineScope.launch {
val view = findCameraView(viewTag)
withPromise(promise) {
view.focus(point)
val coordinateSystem = CoordinateSystem.fromUnionValue(focusOptions.getString("coordinateSystem"))
val point = focusOptions.getMap("point") ?: throw InvalidTypeScriptUnionError("point", focusOptions.toString())
val x = point.getDouble("x").toFloat()
val y = point.getDouble("y").toFloat()
when (coordinateSystem) {
CoordinateSystem.PREVIEW_VIEW -> view.focusInPreviewViewCoordinates(x, y)
CoordinateSystem.CAMERA -> view.focusInCameraCoordinates(x, y)
}
return@withPromise null
}
}
Expand Down
14 changes: 7 additions & 7 deletions package/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1391,16 +1391,16 @@ PODS:
- ReactCommon/turbomodule/core
- Yoga
- SocketRocket (0.7.0)
- VisionCamera (4.5.1):
- VisionCamera/Core (= 4.5.1)
- VisionCamera/FrameProcessors (= 4.5.1)
- VisionCamera/React (= 4.5.1)
- VisionCamera/Core (4.5.1)
- VisionCamera/FrameProcessors (4.5.1):
- VisionCamera (4.5.0):
- VisionCamera/Core (= 4.5.0)
- VisionCamera/FrameProcessors (= 4.5.0)
- VisionCamera/React (= 4.5.0)
- VisionCamera/Core (4.5.0)
- VisionCamera/FrameProcessors (4.5.0):
- React
- React-callinvoker
- react-native-worklets-core
- VisionCamera/React (4.5.1):
- VisionCamera/React (4.5.0):
- React-Core
- VisionCamera/FrameProcessors
- Yoga (0.0.0)
Expand Down
26 changes: 26 additions & 0 deletions package/ios/Core/Types/CoordinateSystem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// CoordinateSystem.swift
// VisionCamera
//
// Created by Marc Rousavy on 17.07.24.
//

import Foundation

@frozen
enum CoordinateSystem: String, JSUnionValue {
case previewView = "preview-view"
case camera

init(jsValue: String) throws {
if let parsed = CoordinateSystem(rawValue: jsValue) {
self = parsed
} else {
throw CameraError.parameter(.invalid(unionName: "hardwareLevel", receivedValue: jsValue))
}
}

var jsValue: String {
return rawValue
}
}
9 changes: 8 additions & 1 deletion package/ios/React/CameraView+Focus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import AVFoundation
import Foundation

extension CameraView {
func focus(point: CGPoint, promise: Promise) {
func focus(pointInPreviewViewCoordinates point: CGPoint, promise: Promise) {
withPromise(promise) {
guard let previewView = self.previewView else {
throw CameraError.capture(.focusRequiresPreview)
Expand All @@ -20,4 +20,11 @@ extension CameraView {
return nil
}
}

func focus(pointInCameraCoordinates point: CGPoint, promise: Promise) {
withPromise(promise) {
try cameraSession.focus(point: point)
return nil
}
}
}
4 changes: 2 additions & 2 deletions package/ios/React/CameraViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject);
RCT_EXTERN_METHOD(focus
: (nonnull NSNumber*)node point
: (NSDictionary*)point resolve
: (nonnull NSNumber*)node focusOptions
: (NSDictionary*)focusOptions resolve
: (RCTPromiseResolveBlock)resolve reject
: (RCTPromiseRejectBlock)reject);

Expand Down
25 changes: 21 additions & 4 deletions package/ios/React/CameraViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,31 @@ final class CameraViewManager: RCTViewManager {
}

@objc
final func focus(_ node: NSNumber, point: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
final func focus(_ node: NSNumber, focusOptions: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
let promise = Promise(resolver: resolve, rejecter: reject)
guard let x = point["x"] as? NSNumber, let y = point["y"] as? NSNumber else {
promise.reject(error: .parameter(.invalid(unionName: "point", receivedValue: point.description)))

guard let coordinateSystemString = focusOptions["coordinateSystem"] as? String,
let pointDictionary = focusOptions["point"] as? NSDictionary else {
promise.reject(error: .parameter(.invalid(unionName: "focusOptions", receivedValue: focusOptions.description)))
return
}
guard let coordinateSystem = try? CoordinateSystem(jsValue: coordinateSystemString) else {
promise.reject(error: .parameter(.invalid(unionName: "focusOptions.coordinateSystem", receivedValue: coordinateSystemString)))
return
}
guard let x = pointDictionary["x"] as? NSNumber, let y = pointDictionary["y"] as? NSNumber else {
promise.reject(error: .parameter(.invalid(unionName: "focusOptions.point", receivedValue: pointDictionary.description)))
return
}
let component = getCameraView(withTag: node)
component.focus(point: CGPoint(x: x.doubleValue, y: y.doubleValue), promise: promise)
let point = CGPoint(x: x.doubleValue, y: y.doubleValue)

switch coordinateSystem {
case .previewView:
component.focus(pointInPreviewViewCoordinates: point, promise: promise)
case .camera:
component.focus(pointInCameraCoordinates: point, promise: promise)
}
}

@objc
Expand Down
28 changes: 26 additions & 2 deletions package/src/Camera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
} from './NativeCameraView'
import { NativeCameraView } from './NativeCameraView'
import { RotationHelper } from './RotationHelper'
import { convertPoint } from './types/CameraMatrix'

//#region Types
export type CameraPermissionStatus = 'granted' | 'not-determined' | 'denied' | 'restricted'
Expand Down Expand Up @@ -125,6 +126,11 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
return nodeHandle
}

private get enablePreviewView(): boolean {
const isRenderingWithSkia = isSkiaFrameProcessor(this.props.frameProcessor)
return isRenderingWithSkia ? false : this.props.preview ?? true
}

//#region View-specific functions (UIViewManager)
/**
* Take a single photo and write it's content to a temporary file.
Expand Down Expand Up @@ -385,7 +391,24 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
*/
public async focus(point: Point): Promise<void> {
try {
return await CameraModule.focus(this.handle, point)
if (isSkiaFrameProcessor(this.props.frameProcessor)) {
// We have a Skia Frame Processor as a Preview - use that Matrix for transformations
const matrix = this.props.frameProcessor.cameraMatrix.value
// TODO: Where do I get width/height from? Needs to happen in SkiaCameraCanvas
const converted = convertPoint(point, { width: 375, height: 667 }, matrix)
return await CameraModule.focus(this.handle, {
coordinateSystem: 'camera',
point: converted,
})
} else if ((this.props.preview ?? true) === true) {
// Use Preview coordinate system
return await CameraModule.focus(this.handle, {
coordinateSystem: 'preview-view',
point: point,
})
} else {
throw new Error('Cannot focus without a PreviewView!')
}
} catch (e) {
throw tryParseNativeCameraError(e)
}
Expand Down Expand Up @@ -669,11 +692,12 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
codeScannerOptions={codeScanner}
enableFrameProcessor={frameProcessor != null}
enableBufferCompression={props.enableBufferCompression ?? shouldEnableBufferCompression}
preview={isRenderingWithSkia ? false : props.preview ?? true}>
preview={this.enablePreviewView}>
{isRenderingWithSkia && (
<SkiaCameraCanvas
style={styles.customPreviewView}
offscreenTextures={frameProcessor.offscreenTextures}
cameraMatrix={frameProcessor.cameraMatrix}
resizeMode={props.resizeMode}
/>
)}
Expand Down
10 changes: 9 additions & 1 deletion package/src/skia/SkiaCameraCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ISharedValue } from 'react-native-worklets-core'
import type { SkImage } from '@shopify/react-native-skia'
import { ReanimatedProxy } from '../dependencies/ReanimatedProxy'
import { SkiaProxy } from '../dependencies/SkiaProxy'
import type { CameraMatrix } from '../types/CameraMatrix'

interface SkiaCameraCanvasProps extends ViewProps {
/**
Expand All @@ -13,6 +14,13 @@ interface SkiaCameraCanvasProps extends ViewProps {
* This view will always pop the latest Texture from this queue and render it.
*/
offscreenTextures: ISharedValue<SkImage[]>
/**
* The Matrix created by the Skia Frame Processor.
*
* While the Skia Frame Processor populates the Matrix's width and orientation data,
* the `<SkiaCameraCanvas>` populates it's `viewWidth` and `viewHeight` properties.
*/
cameraMatrix: ISharedValue<CameraMatrix>
/**
* The resize mode to use for displaying the feed
*/
Expand Down Expand Up @@ -49,7 +57,7 @@ function SkiaCameraCanvasImpl({ offscreenTextures, resizeMode = 'cover', childre
return (
<SkiaProxy.Canvas {...props} onLayout={onLayout} pointerEvents="none">
{children}
<SkiaProxy.Image x={0} y={0} width={width} height={height} fit={resizeMode} image={texture} />
<SkiaProxy.Image width={width} height={height} fit={resizeMode} image={texture} />
</SkiaProxy.Canvas>
)
}
Expand Down
Loading
Loading