Skip to content

Commit

Permalink
feat: Add onOutputOrientationChanged event (#2945)
Browse files Browse the repository at this point in the history
* feat: Add `onOutputOrientationChanged` event

* fix: Fix imports

* fix: Also debounce on iOS
  • Loading branch information
mrousavy committed Jun 6, 2024
1 parent e6e04c5 commit 6daea19
Show file tree
Hide file tree
Showing 18 changed files with 92 additions and 12 deletions.
2 changes: 2 additions & 0 deletions docs/docs/guides/ORIENTATION.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ The orientation in which photos and videos are captured can be adjusted via the
<Camera {...props} outputOrientation="device" />
```

Whenever the output orientation changes, the [`onOutputOrientationChanged`](/docs/api/interfaces/CameraProps#onoutputorientationchanged) event will be called with the new output orientation.

#### `"device"`

With the output orientation set to `device` (the default), photos and videos will be captured in the phone's physical orientation, even if the screen-rotation is locked.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,9 @@ class CameraSession(private val context: Context, private val callback: Callback
videoOutput?.targetRotation = outputOrientation.toSurfaceRotation()
frameProcessorOutput?.targetRotation = outputOrientation.toSurfaceRotation()
codeScannerOutput?.targetRotation = outputOrientation.toSurfaceRotation()

// onOutputOrientationChanged(..) event
callback.onOutputOrientationChanged(outputOrientation)
}

suspend fun takePhoto(flash: Flash, enableShutterSound: Boolean): Photo {
Expand Down Expand Up @@ -680,6 +683,7 @@ class CameraSession(private val context: Context, private val callback: Callback
fun onStarted()
fun onStopped()
fun onShutter(type: ShutterType)
fun onOutputOrientationChanged(outputOrientation: Orientation)
fun onCodeScanned(codes: List<Barcode>, scannerFrame: CodeScannerFrame)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ class OrientationManager(private val context: Context, private val callback: Cal
fun setTargetOutputOrientation(targetOrientation: OutputOrientation) {
Log.i(TAG, "Target Orientation changed $targetOutputOrientation -> $targetOrientation!")
targetOutputOrientation = targetOrientation
lastOrientation = null

// remove previous listeners if attached
displayManager.unregisterDisplayListener(displayListener)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.mrousavy.camera.core.CameraError
import com.mrousavy.camera.core.CodeScannerFrame
import com.mrousavy.camera.core.UnknownCameraError
import com.mrousavy.camera.core.types.CodeType
import com.mrousavy.camera.core.types.Orientation
import com.mrousavy.camera.core.types.ShutterType

fun CameraView.invokeOnInitialized() {
Expand Down Expand Up @@ -48,6 +49,17 @@ fun CameraView.invokeOnShutter(type: ShutterType) {
this.sendEvent(event)
}

fun CameraView.invokeOnOutputOrientationChanged(outputOrientation: Orientation) {
Log.i(CameraView.TAG, "invokeOnOutputOrientationChanged($outputOrientation)")

val surfaceId = UIManagerHelper.getSurfaceId(this)
val data = Arguments.createMap()
data.putString("outputOrientation", outputOrientation.unionValue)

val event = CameraOrientationChangedEvent(surfaceId, id, data)
this.sendEvent(event)
}

fun CameraView.invokeOnError(error: Throwable) {
Log.e(CameraView.TAG, "invokeOnError(...):")
error.printStackTrace()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.mrousavy.camera.core.CameraSession
import com.mrousavy.camera.core.CodeScannerFrame
import com.mrousavy.camera.core.types.CameraDeviceFormat
import com.mrousavy.camera.core.types.CodeScannerOptions
import com.mrousavy.camera.core.types.Orientation
import com.mrousavy.camera.core.types.OutputOrientation
import com.mrousavy.camera.core.types.PixelFormat
import com.mrousavy.camera.core.types.PreviewViewType
Expand Down Expand Up @@ -318,6 +319,10 @@ class CameraView(context: Context) :
invokeOnShutter(type)
}

override fun onOutputOrientationChanged(outputOrientation: Orientation) {
invokeOnOutputOrientationChanged(outputOrientation)
}

override fun onCodeScanned(codes: List<Barcode>, scannerFrame: CodeScannerFrame) {
invokeOnCodeScanned(codes, scannerFrame)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class CameraViewManager : ViewGroupManager<CameraView>() {
.put("cameraStarted", MapBuilder.of("registrationName", "onStarted"))
.put("cameraStopped", MapBuilder.of("registrationName", "onStopped"))
.put("cameraShutter", MapBuilder.of("registrationName", "onShutter"))
.put("cameraOrientationChanged", MapBuilder.of("registrationName", "onOutputOrientationChanged"))
.put("averageFpsChanged", MapBuilder.of("registrationName", "onAverageFpsChanged"))
.put("cameraError", MapBuilder.of("registrationName", "onError"))
.put("cameraCodeScanned", MapBuilder.of("registrationName", "onCodeScanned"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ class CameraShutterEvent(surfaceId: Int, viewId: Int, private val data: Writable
override fun getEventData() = data
}

class CameraOrientationChangedEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) :
Event<CameraOrientationChangedEvent>(surfaceId, viewId) {
override fun getEventName() = "cameraOrientationChanged"
override fun getEventData() = data
}

class AverageFpsChangedEvent(surfaceId: Int, viewId: Int, private val data: WritableMap) : Event<CameraShutterEvent>(surfaceId, viewId) {
override fun getEventName() = "averageFpsChanged"
override fun getEventData() = data
Expand Down
2 changes: 1 addition & 1 deletion package/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -713,4 +713,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 66976ac26c778d788a06e6c1bab624e6a1233cdd

COCOAPODS: 1.11.3
COCOAPODS: 1.14.3
5 changes: 3 additions & 2 deletions package/example/src/CameraPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,9 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
ref={camera}
onInitialized={onInitialized}
onError={onError}
onStarted={() => 'Camera started!'}
onStopped={() => 'Camera stopped!'}
onStarted={() => console.log('Camera started!')}
onStopped={() => console.log('Camera stopped!')}
onOutputOrientationChanged={(o) => console.log(`Orientation changed to ${o}!`)}
format={format}
fps={fps}
photoHdr={photoHdr}
Expand Down
3 changes: 3 additions & 0 deletions package/ios/Core/CameraSession+Orientation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,8 @@ extension CameraSession: OrientationManagerDelegate {
connection.orientation = outputOrientation
}
}

// onOrientationChanged(..) event
delegate?.onOrientationChanged(outputOrientation: outputOrientation)
}
}
4 changes: 4 additions & 0 deletions package/ios/Core/CameraSessionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ protocol CameraSessionDelegate: AnyObject {
Called just before a photo or snapshot is captured.
*/
func onCaptureShutter(shutterType: ShutterType)
/**
Called whenever the output orientation of the [CameraSession] changes.
*/
func onOrientationChanged(outputOrientation: Orientation)
/**
Called for every frame (if video or frameProcessor is enabled)
*/
Expand Down
18 changes: 13 additions & 5 deletions package/ios/Core/OrientationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ protocol OrientationManagerDelegate: AnyObject {
class OrientationManager: CameraOrientationCoordinatorDelegate {
private var orientationCoordinator: CameraOrientationCoordinator?
private var targetOutputOrientation = OutputOrientation.device
private var lastPreviewOrientation: Orientation?
private var lastOutputOrientation: Orientation?
private weak var previewLayer: CALayer?
private weak var device: AVCaptureDevice?

Expand Down Expand Up @@ -94,13 +96,19 @@ class OrientationManager: CameraOrientationCoordinatorDelegate {
VisionLogger.log(level: .info, message: "Setting target output orientation from \(targetOutputOrientation) to \(targetOrientation)...")
targetOutputOrientation = targetOrientation
// update delegate listener
delegate?.onOutputOrientationChanged(outputOrientation: outputOrientation)
delegate?.onPreviewOrientationChanged(previewOrientation: previewOrientation)
onOrientationChanged()
}

func onOrientationChanged() {
// Notify both delegate listeners about the new orientation change
delegate?.onPreviewOrientationChanged(previewOrientation: previewOrientation)
delegate?.onOutputOrientationChanged(outputOrientation: outputOrientation)
if lastPreviewOrientation != previewOrientation {
// Preview orientation changed
delegate?.onPreviewOrientationChanged(previewOrientation: previewOrientation)
lastPreviewOrientation = previewOrientation
}
if lastOutputOrientation != outputOrientation {
// Output orientation changed
delegate?.onOutputOrientationChanged(outputOrientation: outputOrientation)
lastOutputOrientation = outputOrientation
}
}
}
2 changes: 1 addition & 1 deletion package/ios/Core/PhotoCaptureDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class PhotoCaptureDelegate: GlobalReferenceHolder, AVCapturePhotoCaptureDelegate
AudioServicesDisposeSystemSoundID(1108)
}

// onShutter() event
// onShutter(..) event
cameraSessionDelegate?.onCaptureShutter(shutterType: .photo)
}

Expand Down
10 changes: 10 additions & 0 deletions package/ios/React/CameraView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public final class CameraView: UIView, CameraSessionDelegate, FpsSampleCollector
@objc var onStarted: RCTDirectEventBlock?
@objc var onStopped: RCTDirectEventBlock?
@objc var onShutter: RCTDirectEventBlock?
@objc var onOutputOrientationChanged: RCTDirectEventBlock?
@objc var onViewReady: RCTDirectEventBlock?
@objc var onAverageFpsChanged: RCTDirectEventBlock?
@objc var onCodeScanned: RCTDirectEventBlock?
Expand Down Expand Up @@ -346,6 +347,15 @@ public final class CameraView: UIView, CameraSessionDelegate, FpsSampleCollector
])
}

func onOrientationChanged(outputOrientation: Orientation) {
guard let onOutputOrientationChanged else {
return
}
onOutputOrientationChanged([
"outputOrientation": outputOrientation.jsValue,
])
}

func onFrame(sampleBuffer: CMSampleBuffer, orientation: Orientation) {
fpsSampleCollector.onTick()

Expand Down
1 change: 1 addition & 0 deletions package/ios/React/CameraViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ @interface RCT_EXTERN_REMAP_MODULE (CameraView, CameraViewManager, RCTViewManage
RCT_EXPORT_VIEW_PROPERTY(onStarted, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onStopped, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onShutter, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onOutputOrientationChanged, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onViewReady, RCTDirectEventBlock);
RCT_EXPORT_VIEW_PROPERTY(onAverageFpsChanged, RCTDirectEventBlock);
// Code Scanner
Expand Down
14 changes: 13 additions & 1 deletion package/src/Camera.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import type { TakeSnapshotOptions } from './types/Snapshot'
import { SkiaCameraCanvas } from './skia/SkiaCameraCanvas'
import type { Frame } from './types/Frame'
import { FpsGraph, MAX_BARS } from './FpsGraph'
import type { AverageFpsChangedEvent, NativeCameraViewProps, OnCodeScannedEvent, OnErrorEvent } from './NativeCameraView'
import type {
AverageFpsChangedEvent,
NativeCameraViewProps,
OnCodeScannedEvent,
OnErrorEvent,
OutputOrientationChangedEvent,
} from './NativeCameraView'
import { NativeCameraView } from './NativeCameraView'

//#region Types
Expand Down Expand Up @@ -85,6 +91,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
this.onStarted = this.onStarted.bind(this)
this.onStopped = this.onStopped.bind(this)
this.onShutter = this.onShutter.bind(this)
this.onOutputOrientationChanged = this.onOutputOrientationChanged.bind(this)
this.onError = this.onError.bind(this)
this.onCodeScanned = this.onCodeScanned.bind(this)
this.ref = React.createRef<RefType>()
Expand Down Expand Up @@ -516,6 +523,10 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
private onShutter(event: NativeSyntheticEvent<OnShutterEvent>): void {
this.props.onShutter?.(event.nativeEvent)
}

private onOutputOrientationChanged(event: NativeSyntheticEvent<OutputOrientationChangedEvent>): void {
this.props.onOutputOrientationChanged?.(event.nativeEvent.outputOrientation)
}
//#endregion

private onCodeScanned(event: NativeSyntheticEvent<OnCodeScannedEvent>): void {
Expand Down Expand Up @@ -602,6 +613,7 @@ export class Camera extends React.PureComponent<CameraProps, CameraState> {
onStarted={this.onStarted}
onStopped={this.onStopped}
onShutter={this.onShutter}
onOutputOrientationChanged={this.onOutputOrientationChanged}
onError={this.onError}
codeScannerOptions={codeScanner}
enableFrameProcessor={frameProcessor != null}
Expand Down
7 changes: 6 additions & 1 deletion package/src/NativeCameraView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { requireNativeComponent } from 'react-native'
import type { ErrorWithCause } from './CameraError'
import type { CameraProps, OnShutterEvent } from './types/CameraProps'
import type { Code, CodeScanner, CodeScannerFrame } from './types/CodeScanner'
import type { Orientation } from './types/Orientation'

export interface OnCodeScannedEvent {
codes: Code[]
Expand All @@ -16,9 +17,12 @@ export interface OnErrorEvent {
export interface AverageFpsChangedEvent {
averageFps: number
}
export interface OutputOrientationChangedEvent {
outputOrientation: Orientation
}
export type NativeCameraViewProps = Omit<
CameraProps,
'device' | 'onInitialized' | 'onError' | 'onShutter' | 'frameProcessor' | 'codeScanner'
'device' | 'onInitialized' | 'onError' | 'onShutter' | 'onOutputOrientationChanged' | 'frameProcessor' | 'codeScanner'
> & {
// private intermediate props
cameraId: string
Expand All @@ -34,6 +38,7 @@ export type NativeCameraViewProps = Omit<
onStarted?: (event: NativeSyntheticEvent<void>) => void
onStopped?: (event: NativeSyntheticEvent<void>) => void
onShutter?: (event: NativeSyntheticEvent<OnShutterEvent>) => void
onOutputOrientationChanged?: (event: NativeSyntheticEvent<OutputOrientationChangedEvent>) => void
}

// requireNativeComponent automatically resolves 'CameraView' to 'CameraViewManager'
Expand Down
7 changes: 7 additions & 0 deletions package/src/types/CameraProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Frame } from './Frame'
import type { ISharedValue } from 'react-native-worklets-core'
import type { SkImage } from '@shopify/react-native-skia'
import type { OutputOrientation } from './OutputOrientation'
import type { Orientation } from './Orientation'

export interface ReadonlyFrameProcessor {
frameProcessor: (frame: Frame) => void
Expand Down Expand Up @@ -315,6 +316,12 @@ export interface CameraProps extends ViewProps {
* Inside this callback you can play a custom shutter sound or show visual feedback to the user.
*/
onShutter?: (event: OnShutterEvent) => void
/**
* Called whenever the output orientation changed.
*
* @see See ["Orientation"](https://react-native-vision-camera.com/docs/guides/orientation)
*/
onOutputOrientationChanged?: (outputOrientation: Orientation) => void
/**
* A worklet which will be called for every frame the Camera "sees".
*
Expand Down

0 comments on commit 6daea19

Please sign in to comment.