N_Android

Android CameraX

developer android 用户指南 developer android 用户文档

Jetpack 是一个由多个库组成的套件,可帮助开发者遵循最佳实践、减少样板代码并编写可在各种 Android 版本和设备中一致运行的代码,让开发者可将精力集中于真正重要的编码工作。

CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。如果您要开发新应用,我们建议您从 CameraX 开始。它提供了一个一致且易于使用的 API,该 API 适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。

CameraX 结构

您可以使用 CameraX,借助名为“用例”的抽象概念与设备的相机进行交互。提供的用例如下:

  • 预览:接受用于显示预览的 Surface,例如 PreviewView
  • 图片分析:为分析(例如机器学习)提供 CPU 可访问的缓冲区。
  • 图片拍摄:拍摄并保存照片。
  • 视频拍摄:通过 VideoCapture 拍摄视频和音频

关键对象

提供了一致的 API 接口

CameraSelector(相机选择器)

用于选择要打开的相机设备。可以选择前置摄像头、后置摄像头或者其他可用的相机设备。

CameraSelector cameraSelector = new CameraSelector.Builder()
       .requireLensFacing(CameraSelector.LENS_FACING_BACK)
       .build();

Preview(预览)

用于在屏幕上显示相机预览。通过配置 Preview 对象,可以实现预览界面的定制和控制。

Preview preview = new Preview.Builder().build();
preview.setSurfaceProvider(viewFinder.createSurfaceProvider());
 

ImageAnalysis(图像分析)

用于对相机捕获的图像进行分析。开发者可以通过 ImageAnalysis 处理图像数据,实现例如图像识别、文字识别等功能。

ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
       .setTargetResolution(new Size(1280, 720))
       .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
       .build();
imageAnalysis.setAnalyzer(executor, image -> {
       // 处理图像数据的逻辑
       // ...
});
 

Camera(相机)

将上述预览、图像分析和图像捕获等功能整合在一起,用于控制相机的整体行为。

Camera camera = Camera.open(cameraSelector);
camera.setPreview(preview);
camera.setImageAnalysis(imageAnalysis);
camera.setImageCapture(imageCapture);
 

CameraController (相机控制)

CameraController 在单个类中提供大多数 CameraX 核心功能。它只需少量设置代码,并且可自动处理相机初始化、用例管理、目标旋转、点按对焦、双指张合缩放等操作。扩展 CameraController 的具体类为 LifecycleCameraController

val previewView: PreviewView = viewBinding.previewView
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
previewView.controller = cameraController

CameraProvider

CameraProvider 仍然易于使用,但由于应用开发者会处理更多设置,因此有更多机会自定义配置,例如在 ImageAnalysis 中启用输出图片旋转或设置输出图像格式。您还可以使用自定义 Surface 进行相机预览以提高灵活性,而对于 CameraController,您需要使用 PreviewView。如果现有的 Surface 代码已是应用的其他部分的输入,则使用该代码会非常有用。

val preview = Preview.Builder().build()  
val viewFinder: PreviewView = findViewById(R.id.previewView)  
 
// 用它来绑定生命周期
// The use case is bound to an Android Lifecycle with the following code  
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)  
  
// PreviewView creates a surface provider and is the recommended provider  
preview.setSurfaceProvider(viewFinder.getSurfaceProvider())

安卓官方就相机 API 就改了三版, 离谱!

实例 (拍照)

https://developer.android.google.cn/media/camera/camerax/take-photo?hl=de

使用 GPS, 陀螺仪 + 磁力计 实时获取当前拍向的位置

依赖

在应用或模块的 build.gradle 文件中添加所需工件的依赖项:

dependencies {
    // CameraX core library using the camera2 implementation
    val camerax_version = "1.3.0-alpha04"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation("androidx.camera:camera-core:${camerax_version}")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    // If you want to additionally use the CameraX Lifecycle library
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    // If you want to additionally use the CameraX VideoCapture library
    implementation("androidx.camera:camera-video:${camerax_version}")
    // If you want to additionally use the CameraX View class
    implementation("androidx.camera:camera-view:${camerax_version}")
    // If you want to additionally add CameraX ML Kit Vision Integration
    implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
    // If you want to additionally use the CameraX Extensions library
    implementation("androidx.camera:camera-extensions:${camerax_version}")
}

代码

package com.example.myapplication
 
import android.Manifest
import android.content.pm.PackageManager
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture
import java.util.concurrent.ExecutionException
import java.util.logging.Logger
 
 
class CamActivity : AppCompatActivity(),LocationListener, SensorEventListener {
 
    val log = Logger.getLogger("CamActivity");
 
    //当前状态描述, 实时更新UI 及 定时器 
    private var realtimeLocation = "";
    private var realtimeAngle = "";
    private val mHandler: Handler = Handler(Looper.getMainLooper())
    private val mRunnable = object : Runnable {
        override fun run() {
            val textView = findViewById<TextView>(R.id.textView)
            textView.setText("经纬度: "+realtimeLocation+", 方向: "+realtimeAngle)
            mHandler.postDelayed(this, 500)
        }
    }
    //
    private var previewView: PreviewView? = null
    private var imageCapture: ImageCapture? = null
    //
    private var sensorManager: SensorManager? = null
    //GPS
    private var locationManager: LocationManager? = null
    // 陀螺仪 & 磁力计
    private var accelerometer: Sensor? = null
    private var magnetometer: Sensor? = null
    private val accelerometerReading = FloatArray(3)
    private val magnetometerReading = FloatArray(3)
    private val rotationMatrix = FloatArray(9)
    private val orientationAngles = FloatArray(3)
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setTheme(android.R.style.Theme_Light_NoTitleBar_Fullscreen)
        setContentView(R.layout.activity_cam)
        previewView = findViewById<PreviewView>(R.id.previewView)// PreviewView 是 CameraX 摄像头画面 预览组件
        if (allPermissionsGranted()) {// 请求所有授权
            initOnPermissionsGranted()
        } else {
            ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
        }
    }
 
    private fun allPermissionsGranted(): Boolean {
        for (permission in REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(
                    this,
                    permission
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                return false
            }
        }
        return true
    }
    private fun initOnPermissionsGranted(){
        startCamera()
        initializeSensors()
        initializeLocation()
    }
 
    private fun startCamera() {
        val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener({//初始化完成的回调
            try {
                val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
                // set preview
                val rotation = previewView?.display?.rotation
                val preview = rotation?.let {
                    Preview
                        .Builder()
                        .setTargetRotation(it)// 设置为横屏
                        .build().also {
                            it.setSurfaceProvider(previewView?.surfaceProvider)
                        }
                }
                val cameraSelector = CameraSelector.Builder()// 选择摄像头, 现在手机 动不动N个摄像头
                    .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                    .build()
 
                imageCapture = ImageCapture.Builder().build()
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
            } catch (e: ExecutionException) {
                e.printStackTrace()
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
        }, ContextCompat.getMainExecutor(this))
    }
    private fun initializeSensors() {
	    // 初始化传感器监听
        sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
        if (sensorManager != null) {
            accelerometer = sensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)//陀螺仪
            magnetometer = sensorManager!!.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)//磁力计
            log.info("initializeSensors")
        }
    }
    private fun initializeLocation() {
        locationManager = getSystemService(LOCATION_SERVICE) as LocationManager
        if (locationManager != null) {
            if (ActivityCompat.checkSelfPermission(
                    this,
                    Manifest.permission.ACCESS_FINE_LOCATION
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                return
            }
            log.info("initializeLocation")
            // 请监听位置改变
            locationManager!!.requestLocationUpdates(LocationManager.GPS_PROVIDER, 500L, 1.0f, this)//GPS
            val lastKnownLocation = locationManager!!.getLastKnownLocation(LocationManager.GPS_PROVIDER)//最近一次的位置
            lastKnownLocation?.let {
                onLocationChanged(it)
            }
 
 
        }
    }
    override fun onLocationChanged(location: Location) {
        locationUpdate(location.longitude, location.latitude)
    }
    private fun locationUpdate(lng : Double, lat: Double) {
        log.warning("位置更新: $lng, $lat")
        realtimeLocation = "$lng, $lat"
    }
 
    override fun onResume() {
        super.onResume()
        sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
        //注册 陀螺仪 传感器监听
        sensorManager!!.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
	    //注册 磁力计 传感器监听    
        sensorManager!!.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL)
        mHandler.postDelayed(mRunnable, 500)//for UI
    }
 
    override fun onPause() {
        super.onPause()
        sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
        sensorManager?.unregisterListener(this)
        mHandler.removeCallbacks(mRunnable)
    }
 
    override fun onSensorChanged(event: SensorEvent) {
        // Update sensor readings based on sensor type
        if (event.sensor === accelerometer) {
            System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.size)
        } else if (event.sensor === magnetometer) {
            System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.size)
        }
        SensorManager.getRotationMatrix(
            rotationMatrix,
            null,
            accelerometerReading,
            magnetometerReading
        )
        SensorManager.getOrientation(rotationMatrix, orientationAngles)
        SensorManager.getRotationMatrix(
            rotationMatrix,
            null,
            accelerometerReading,
            magnetometerReading
        )
        SensorManager.getOrientation(rotationMatrix, orientationAngles)
        val azimuthInRadians = orientationAngles[0]
        var azimuthInDegrees = Math.toDegrees(azimuthInRadians.toDouble()).toFloat()
        azimuthInDegrees+=90//校准横屏, 正北是 0度
        azimuthInDegrees = (azimuthInDegrees + 360) % 360
        angleUpdate(azimuthInDegrees);
    }
    private fun angleUpdate(azimuthInDegrees :Float){
        val direction = getDirectDesc(azimuthInDegrees)
//        log.warning("角度更新: $azimuthInDegrees")
        this.realtimeAngle = direction+"( "+azimuthInDegrees.toInt().toString()
    }
 
    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
 
 
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                initOnPermissionsGranted();
            } else {
                Toast.makeText(this, "未获得权限", Toast.LENGTH_SHORT).show()
                finish();
            }
        }
    }
 
    private fun getDirectDesc(azimuthInDegrees: Float): String{
        val direction: String = if (azimuthInDegrees >= 22.5 && azimuthInDegrees < 67.5) {
            "东北"
        } else if (azimuthInDegrees >= 67.5 && azimuthInDegrees < 112.5) {
            "东"
        } else if (azimuthInDegrees >= 112.5 && azimuthInDegrees < 157.5) {
            "东南"
        } else if (azimuthInDegrees >= 157.5 && azimuthInDegrees < 202.5) {
            "南"
        } else if (azimuthInDegrees >= 202.5 && azimuthInDegrees < 247.5) {
            "西南"
        } else if (azimuthInDegrees >= 247.5 && azimuthInDegrees < 292.5) {
            "西"
        } else if (azimuthInDegrees >= 292.5 && azimuthInDegrees < 337.5) {
            "西北"
        } else {
            "北"
        }
        return direction
    }
    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 101
        private val REQUIRED_PERMISSIONS =
            arrayOf(Manifest.permission.CAMERA, Manifest.permission.ACCESS_FINE_LOCATION)
    }
 
 
}
 

布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <!-- camerax 预览组件-->
        <androidx.camera.view.PreviewView
            android:id="@+id/previewView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
 
        <LinearLayout
            android:id="@+id/buttonsLayout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            android:padding="16dp">
 
            <Button
                android:id="@+id/captureButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Capture" />
 
            <TextView
                android:id="@+id/textView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="TextView"
                android:textColor="#FFFFFF" />
 
        </LinearLayout>
 
    </LinearLayout>
</RelativeLayout>

实例 (视频拍摄)

https://developer.android.google.cn/media/camera/camerax/preview?hl=de https://developer.android.google.cn/media/camera/camerax/video-capture?hl=de

视频捕获架构

https://developer.android.google.cn/media/camera/camerax/video-capture?hl=de

捕获系统通常会录制视频流和音频流,对其进行压缩,对这两个流进行多路复用,然后将生成的流写入磁盘 在 CameraX 中,用于视频捕获的解决方案是 VideoCapture 用例:

CameraX 视频捕获包括几个高级架构组件:

SurfaceProvider,表示视频来源。 AudioSource,表示音频来源。 用于对视频/音频进行编码和压缩的两个编码器。 用于对两个流进行多路复用的媒体复用器。 用于写出结果的文件保存器。 VideoCapture API 会对复杂的捕获引擎进行抽象化处理,为应用提供更加简单且直观的 API。

实时预览

https://developer.android.google.cn/media/camera/camerax/preview?hl=de

官方-示例仓库 视频录制-参考

将 PreviewView 添加到布局

   <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
 
        <androidx.camera.view.PreviewView
            android:id="@+id/videoPreview"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>

使用 PreviewView 存在一些限制。使用 PreviewView 时,您无法执行以下任何操作:

  • 创建 SurfaceTexture,以在 TextureView 和 Preview.SurfaceProvider 上进行设置。
  • 从 TextureView 检索 SurfaceTexture,并在 Preview.SurfaceProvider 上对其进行设置。
  • 从 SurfaceView 获取 Surface,并在 Preview.SurfaceProvider 上对其进行设置。

如果出现上述任何一种情况,Preview 就会停止将帧流式传输到 PreviewView

获取 CameraProvider

import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.common.util.concurrent.ListenableFuture
 
private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>  
private lateinit var previewView : PreviewView;
 
class MainActivity : AppCompatActivity() {
    private lateinit var cameraProviderFuture : ListenableFuture<ProcessCameraProvider>
    override fun onCreate(savedInstanceState: Bundle?) {
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)//cameraProviderFuture
        previewView = findViewById<PreviewView>(R.id.videoPreview)//预览组件
    }
}

绑定预览, 检查 CameraProvider

请求 CameraProvider 后,请验证它能否在视图创建后成功初始化。以下代码展示了如何执行此操作:

// 请求 CameraProvider 来初始化 CameraX
cameraProviderFuture.addListener(Runnable {// 监听初始化完成
    val cameraProvider = cameraProviderFuture.get()
    
    bindPreview(cameraProvider)//绑定预览 生命周期
}, ContextCompat.getMainExecutor(this))
fun bindPreview(cameraProvider : ProcessCameraProvider) {
	var preview : Preview = Preview.Builder()
		.build()
	var cameraSelector : CameraSelector = CameraSelector.Builder()
		.requireLensFacing(CameraSelector.LENS_FACING_BACK)
		.build()
	// `SurfaceProvider`, 表示视频来源;
	preview.setSurfaceProvider(previewView.getSurfaceProvider())
	// 视频质量选择
	val qualitySelector = QualitySelector.fromOrderedList(
		listOf(Quality.FHD, Quality.HD, Quality.SD)
		,FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
	)
	// Recorder 对象
	val recorder = Recorder.Builder()
		.setQualitySelector(qualitySelector).build()
	//  VideoCapture 对象
	videoCapture = VideoCapture.withOutput(recorder)
	try {
		cameraProvider.unbindAll()
		cameraProvider.bindToLifecycle(
			this as LifecycleOwner,
			cameraSelector,
			videoCapture,
			preview
		)
	} catch (exc: Exception) {
		Log.e(TAG, "Use case binding failed", exc)
	}
}

bindToLifecycle() 会返回一个 Camera 对象。如需详细了解如何控制相机输出(例如变焦和曝光),请参阅相机输出

VideoCapture API

VideoCapture 既可以单独使用,也可以与其他用例搭配使用。受支持的具体组合取决于相机硬件功能,不过 Preview 和 VideoCapture 这一用例组合适用于所有设备。

VideoCapture API 包含可与应用通信的以下对象:

  • VideoCapture 是顶级类()。VideoCapture 通过 CameraSelector 和其他 CameraX 用例绑定到 LifecycleOwner。如需详细了解这些概念和用法,请参阅 CameraX 架构
  • Recorder 是与 VideoCapture 紧密耦合的 VideoOutput 实现。 Recorder 用于执行视频和音频捕获操作。应用通过 Recorder 创建录制对象。in short 可以理解为创建记录的对象
  • PendingRecording 会配置录制对象,同时提供启用音频和设置事件监听器等选项。您必须使用 Recorder 来创建 PendingRecordingPendingRecording 不会录制任何内容。
  • Recording 会执行实际录制操作。您必须使用 PendingRecording 来创建 Recordingin short 可以理解为每一次记录, 一个对象

配置和创建录制对象

流程大概是

  1. 使用 QualitySelector 创建 Recorder
  2. 使用其中一个 OutputOptions 配置 Recorder
  3. 如果需要,使用 withAudioEnabled() 启用音频。
  4. 使用 VideoRecordEvent 监听器调用 start() 以开始录制。
  5. 针对 Recording 使用 pause()/resume()/stop() 来控制录制操作。
  6. 在事件监听器内响应 VideoRecordEvents

貌似 VideoCapture 参与的不多, 它仅作为一个顶层类

 
 fun bindPreview(cameraProvider : ProcessCameraProvider) {
        var preview : Preview = Preview.Builder()
            .build()
        var cameraSelector : CameraSelector = CameraSelector.Builder()
            .requireLensFacing(CameraSelector.LENS_FACING_BACK)
            .build()
        // `SurfaceProvider`, 表示视频来源;
        preview.setSurfaceProvider(previewView.getSurfaceProvider())
		// 优先画质选择
        val qualitySelector = QualitySelector.fromOrderedList(
            listOf(Quality.FHD, Quality.HD, Quality.SD)
            ,FallbackStrategy.lowerQualityOrHigherThan(Quality.HD)
        )
        // 1. 使用 QualitySelector 创建 Recorder。
        val recorder = Recorder.Builder()
            .setQualitySelector(qualitySelector).build()
        // 创建 VideoCapture 对象
        videoCapture = VideoCapture.withOutput(recorder)
        try {
            cameraProvider.unbindAll()
            // 绑定生命周期
            cameraProvider.bindToLifecycle(
                this as LifecycleOwner,
                cameraSelector,
                videoCapture,
                preview
            )
        } catch (exc: Exception) {
            Log.e(TAG, "Use case binding failed", exc)
        }
    }
    
 private fun startRecording() {
	//输出配置
	val fileOpt = FileOutputOptions.Builder(createVideoFile()).build()
	
	// 2.使用其中一个 OutputOptions 配置 Recorder。
	var prepareRecording = videoCapture.output.prepareRecording(this, fileOpt)
	
	// 3.如果需要,使用 withAudioEnabled() 启用音频。
	// prepareRecording.withAudioEnabled()
	
	// 4. 使用 VideoRecordEvent 监听器调用 start() 以开始录制。
	val recording = prepareRecording.start(
		ContextCompat.getMainExecutor(this),
		{
		//6. 在事件监听器内响应 VideoRecordEvents。
			if(it is VideoRecordEvent.Finalize){
			// 完成录制
			}
			Log.i(TAG, "录制事件, 当前状态: $it")
		}
	)
	// 5. 针对 Recording 使用 pause()/resume()/stop() 来控制录制操作。
	//recording?.pause()
	//recording?.stop()
}
 

您可以使用以下方法暂停、恢复和停止正在进行的 Recording

  • pause,用于暂停当前的活跃录制。
  • resume(),用于恢复已暂停的活跃录制。
  • stop(),用于完成录制并清空所有关联的录制对象。
  • mute(),用于将当前录音静音或取消静音。

请注意,无论录制处于暂停状态还是活跃状态,您都可以调用 stop() 来终止 Recording。 如果您已使用 PendingRecording.start() 注册了 EventListenerRecording 会使用 VideoRecordEvent 进行通信。