feat: Support rotation (#301)

* feat: Android: Listen to rotation changes

* Only change rotation on configuration change

* feat: iOS: Support Rotation

* Swift lint
This commit is contained in:
Marc Rousavy 2021-07-26 11:32:58 +02:00 committed by GitHub
parent 159ff44de3
commit ef455df865
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 36 deletions

View File

@ -4,6 +4,7 @@ import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.hardware.camera2.*
import android.util.Log
import android.util.Range
@ -99,6 +100,7 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
internal var imageCapture: ImageCapture? = null
internal var videoCapture: VideoCapture? = null
private var imageAnalysis: ImageAnalysis? = null
private var preview: Preview? = null
private var lastFrameProcessorCall = System.currentTimeMillis()
@ -107,10 +109,13 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
private val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener
private val scaleGestureDetector: ScaleGestureDetector
private val touchEventListener: OnTouchListener
private val orientationEventListener: OrientationEventListener
private val lifecycleRegistry: LifecycleRegistry
private var hostLifecycleState: Lifecycle.State
private var rotation: Int = Surface.ROTATION_0
private var minZoom: Float = 1f
private var maxZoom: Float = 1f
@ -164,6 +169,17 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
}
scaleGestureDetector = ScaleGestureDetector(context, scaleGestureListener)
touchEventListener = OnTouchListener { _, event -> return@OnTouchListener scaleGestureDetector.onTouchEvent(event) }
orientationEventListener = object : OrientationEventListener(context) {
override fun onOrientationChanged(orientation : Int) {
rotation = when (orientation) {
in 45..134 -> Surface.ROTATION_270
in 135..224 -> Surface.ROTATION_180
in 225..314 -> Surface.ROTATION_90
else -> Surface.ROTATION_0
}
}
}
orientationEventListener.enable()
hostLifecycleState = Lifecycle.State.INITIALIZED
lifecycleRegistry = LifecycleRegistry(this)
@ -186,8 +202,21 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
})
}
@SuppressLint("RestrictedApi")
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
if (preview?.targetRotation != rotation) {
preview?.targetRotation = rotation
imageCapture?.targetRotation = rotation
imageAnalysis?.targetRotation = rotation
videoCapture?.setTargetRotation(rotation)
}
}
fun finalize() {
mHybridData.resetNative()
orientationEventListener.disable()
}
private external fun initHybrid(): HybridData
@ -304,8 +333,6 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
}
}
val rotation = previewView.display.rotation
val previewBuilder = Preview.Builder()
.setTargetRotation(rotation)
val imageCaptureBuilder = ImageCapture.Builder()
@ -314,8 +341,8 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
val videoCaptureBuilder = VideoCapture.Builder()
.setTargetRotation(rotation)
val imageAnalysisBuilder = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setBackgroundExecutor(CameraViewModule.FrameProcessorThread)
if (format == null) {
@ -395,10 +422,10 @@ class CameraView(context: Context) : FrameLayout(context), LifecycleOwner {
useCases.add(imageAnalysis!!)
}
val preview = previewBuilder.build()
preview = previewBuilder.build()
Log.i(TAG, "Attaching ${useCases.size} use-cases...")
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, *useCases.toTypedArray())
preview.setSurfaceProvider(previewView.surfaceProvider)
preview!!.setSurfaceProvider(previewView.surfaceProvider)
minZoom = camera!!.cameraInfo.zoomState.value?.minZoomRatio ?: 1f
maxZoom = camera!!.cameraInfo.zoomState.value?.maxZoomRatio ?: 1f

View File

@ -322,7 +322,7 @@ PODS:
- React
- RNVectorIcons (8.1.0):
- React-Core
- VisionCamera (2.4.2-beta.7):
- VisionCamera (2.4.2-beta.10):
- React
- React-callinvoker
- React-Core
@ -490,7 +490,7 @@ SPEC CHECKSUMS:
RNReanimated: 9c13c86454bfd54dab7505c1a054470bfecd2563
RNStaticSafeAreaInsets: 6103cf09647fa427186d30f67b0f5163c1ae8252
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
VisionCamera: d65c038e7a538fbd719f3ff3e7511a2c71111e3a
VisionCamera: 1b59ab003dc1d96da223e04e460b702bf85fb866
Yoga: 575c581c63e0d35c9a83f4b46d01d63abc1100ac
PODFILE CHECKSUM: 4b093c1d474775c2eac3268011e4b0b80929d3a2

View File

@ -59,6 +59,9 @@
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>

View File

@ -135,9 +135,10 @@ extension CameraView {
videoOutput!.setSampleBufferDelegate(self, queue: videoQueue)
videoOutput!.alwaysDiscardsLateVideoFrames = false
captureSession.addOutput(videoOutput!)
videoOutput!.setOrientation(forCameraPosition: videoDeviceInput!.device.position)
}
onOrientationChanged()
invokeOnInitialized()
isReady = true
ReactLogger.log(level: .info, message: "Session successfully configured!")

View File

@ -105,10 +105,20 @@ public final class CameraView: UIView {
/// Specifies whether the frameProcessor() function is currently executing. used to drop late frames.
internal var isRunningFrameProcessor = false
/// Returns whether the AVCaptureSession is currently running (reflected by isActive)
var isRunning: Bool {
return captureSession.isRunning
}
/// Returns the current _interface_ orientation of the main window
private var windowInterfaceOrientation: UIInterfaceOrientation {
if #available(iOS 13.0, *) {
return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .unknown
} else {
return UIApplication.shared.statusBarOrientation
}
}
/// Convenience wrapper to get layer as its statically known type.
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
// swiftlint:disable force_cast
@ -138,6 +148,10 @@ public final class CameraView: UIView {
selector: #selector(audioSessionInterrupted),
name: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance)
NotificationCenter.default.addObserver(self,
selector: #selector(onOrientationChanged),
name: UIDevice.orientationDidChangeNotification,
object: nil)
}
@available(*, unavailable)
@ -155,6 +169,9 @@ public final class CameraView: UIView {
NotificationCenter.default.removeObserver(self,
name: AVAudioSession.interruptionNotification,
object: AVAudioSession.sharedInstance)
NotificationCenter.default.removeObserver(self,
name: UIDevice.orientationDidChangeNotification,
object: nil)
}
// pragma MARK: Props updating
@ -269,6 +286,31 @@ public final class CameraView: UIView {
}
}
@objc
func onOrientationChanged() {
// Updates the Orientation for all rotable connections (outputs) as well as for the preview layer
DispatchQueue.main.async {
// `windowInterfaceOrientation` and `videoPreviewLayer` should only be accessed from UI thread
let isMirrored = self.videoDeviceInput?.device.position == .front
let orientation = self.windowInterfaceOrientation
self.videoPreviewLayer.connection?.setInterfaceOrientation(orientation)
self.cameraQueue.async {
// Run those updates on cameraQueue since they can be blocking.
self.captureSession.outputs.forEach { output in
output.connections.forEach { connection in
if connection.isVideoMirroringSupported {
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = isMirrored
}
connection.setInterfaceOrientation(orientation)
}
}
}
}
}
// pragma MARK: Event Invokers
internal final func invokeOnError(_ error: CameraError, cause: NSError? = nil) {
ReactLogger.log(level: .error, message: "Invoking onError(): \(error.message)")

View File

@ -0,0 +1,34 @@
//
// AVCaptureConnection+setInterfaceOrientation.swift
// VisionCamera
//
// Created by Marc Rousavy on 26.07.21.
// Copyright © 2021 mrousavy. All rights reserved.
//
import AVFoundation
import Foundation
extension AVCaptureConnection {
/**
Sets the `videoOrientation` to the given `orientation` if video orientation setting is supported.
*/
func setInterfaceOrientation(_ orientation: UIInterfaceOrientation) {
if isVideoOrientationSupported {
switch orientation {
case .portrait:
videoOrientation = .portrait
case .portraitUpsideDown:
videoOrientation = .portraitUpsideDown
case .landscapeLeft:
videoOrientation = .landscapeLeft
case .landscapeRight:
videoOrientation = .landscapeRight
case .unknown:
fallthrough
@unknown default:
videoOrientation = .portrait
}
}
}
}

View File

@ -1,24 +0,0 @@
//
// AVCaptureVideoDataOutput+setOrientation.swift
// mrousavy
//
// Created by Marc Rousavy on 18.01.21.
// Copyright © 2021 mrousavy. All rights reserved.
//
import AVFoundation
extension AVCaptureVideoDataOutput {
func setOrientation(forCameraPosition position: AVCaptureDevice.Position) {
let isMirrored = position == .front
connections.forEach { connection in
if connection.isVideoMirroringSupported {
connection.automaticallyAdjustsVideoMirroring = false
connection.isVideoMirrored = isMirrored
}
if connection.isVideoOrientationSupported {
connection.videoOrientation = .portrait
}
}
}
}

View File

@ -17,6 +17,7 @@
B86DC974260E310600FB17B2 /* CameraView+AVAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC973260E310600FB17B2 /* CameraView+AVAudioSession.swift */; };
B86DC977260E315100FB17B2 /* CameraView+AVCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */; };
B8805067266798B600EAD7F2 /* JSConsoleHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = B8805066266798B600EAD7F2 /* JSConsoleHelper.mm */; };
B882721026AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */; };
B887518525E0102000DB86D6 /* PhotoCaptureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */; };
B887518625E0102000DB86D6 /* CameraView+RecordVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */; };
B887518725E0102000DB86D6 /* CameraViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B887515F25E0102000DB86D6 /* CameraViewManager.m */; };
@ -29,7 +30,6 @@
B887518F25E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516825E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift */; };
B887519025E0102000DB86D6 /* AVCaptureDevice.Format+matchesFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516925E0102000DB86D6 /* AVCaptureDevice.Format+matchesFilter.swift */; };
B887519125E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */; };
B887519225E0102000DB86D6 /* AVCaptureVideoDataOutput+setOrientation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516B25E0102000DB86D6 /* AVCaptureVideoDataOutput+setOrientation.swift */; };
B887519425E0102000DB86D6 /* MakeReactError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516E25E0102000DB86D6 /* MakeReactError.swift */; };
B887519525E0102000DB86D6 /* ReactLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887516F25E0102000DB86D6 /* ReactLogger.swift */; };
B887519625E0102000DB86D6 /* Promise.swift in Sources */ = {isa = PBXBuildFile; fileRef = B887517025E0102000DB86D6 /* Promise.swift */; };
@ -94,6 +94,7 @@
B86DC976260E315100FB17B2 /* CameraView+AVCaptureSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CameraView+AVCaptureSession.swift"; sourceTree = "<group>"; };
B8805065266798AB00EAD7F2 /* JSConsoleHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSConsoleHelper.h; sourceTree = "<group>"; };
B8805066266798B600EAD7F2 /* JSConsoleHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JSConsoleHelper.mm; sourceTree = "<group>"; };
B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVCaptureConnection+setInterfaceOrientation.swift"; sourceTree = "<group>"; };
B887515C25E0102000DB86D6 /* PhotoCaptureDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureDelegate.swift; sourceTree = "<group>"; };
B887515D25E0102000DB86D6 /* CameraView+RecordVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraView+RecordVideo.swift"; sourceTree = "<group>"; };
B887515E25E0102000DB86D6 /* CameraBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CameraBridge.h; sourceTree = "<group>"; };
@ -107,7 +108,6 @@
B887516825E0102000DB86D6 /* AVCapturePhotoOutput+mirror.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCapturePhotoOutput+mirror.swift"; sourceTree = "<group>"; };
B887516925E0102000DB86D6 /* AVCaptureDevice.Format+matchesFilter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+matchesFilter.swift"; sourceTree = "<group>"; };
B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureDevice.Format+toDictionary.swift"; sourceTree = "<group>"; };
B887516B25E0102000DB86D6 /* AVCaptureVideoDataOutput+setOrientation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVCaptureVideoDataOutput+setOrientation.swift"; sourceTree = "<group>"; };
B887516E25E0102000DB86D6 /* MakeReactError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MakeReactError.swift; sourceTree = "<group>"; };
B887516F25E0102000DB86D6 /* ReactLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactLogger.swift; sourceTree = "<group>"; };
B887517025E0102000DB86D6 /* Promise.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Promise.swift; sourceTree = "<group>"; };
@ -192,6 +192,7 @@
B8D22CDB2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift */,
B86DC970260E2D5200FB17B2 /* AVAudioSession+trySetAllowHaptics.swift */,
B80E069F266632F000728644 /* AVAudioSession+updateCategory.swift */,
B882720F26AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift */,
B887516325E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift */,
B887516425E0102000DB86D6 /* AVCaptureDevice.Format+isBetterThan.swift */,
B887516525E0102000DB86D6 /* AVCaptureDevice+isMultiCam.swift */,
@ -201,7 +202,6 @@
B887516925E0102000DB86D6 /* AVCaptureDevice.Format+matchesFilter.swift */,
B887516A25E0102000DB86D6 /* AVCaptureDevice.Format+toDictionary.swift */,
B88B47462667C8E00091F538 /* AVCaptureSession+setVideoStabilizationMode.swift */,
B887516B25E0102000DB86D6 /* AVCaptureVideoDataOutput+setOrientation.swift */,
B887516225E0102000DB86D6 /* Collection+safe.swift */,
);
path = Extensions;
@ -390,13 +390,13 @@
B8103E1C25FF553B007A1684 /* FrameProcessorUtils.mm in Sources */,
B887518E25E0102000DB86D6 /* AVFrameRateRange+includes.swift in Sources */,
B88751A125E0102000DB86D6 /* AVCaptureDevice.Position+descriptor.swift in Sources */,
B882721026AEB1A100B14107 /* AVCaptureConnection+setInterfaceOrientation.swift in Sources */,
B86DC977260E315100FB17B2 /* CameraView+AVCaptureSession.swift in Sources */,
B887518A25E0102000DB86D6 /* AVCaptureDevice+neutralZoom.swift in Sources */,
B88751A325E0102000DB86D6 /* AVCaptureDevice.FlashMode+descriptor.swift in Sources */,
B8A751D82609E4B30011C623 /* FrameProcessorRuntimeManager.mm in Sources */,
B887519A25E0102000DB86D6 /* AVVideoCodecType+descriptor.swift in Sources */,
B88751A825E0102000DB86D6 /* CameraError.swift in Sources */,
B887519225E0102000DB86D6 /* AVCaptureVideoDataOutput+setOrientation.swift in Sources */,
B88751A625E0102000DB86D6 /* CameraViewManager.swift in Sources */,
B887519F25E0102000DB86D6 /* AVCaptureDevice.DeviceType+descriptor.swift in Sources */,
B8D22CDC2642DB4D00234472 /* AVAssetWriterInputPixelBufferAdaptor+initWithVideoSettings.swift in Sources */,