kotlin 使用CameraX录制视频点击对焦,保存至相册中

Posted 橘海w_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kotlin 使用CameraX录制视频点击对焦,保存至相册中相关的知识,希望对你有一定的参考价值。

文章目录


前言

kotlin使用CameraX实现录制视频
实现效果:
录制视频,点击对焦,录制完成后在手机相册中即可找到录制的视频


一、添加依赖

包括camerax的依赖自身和用于权限申请的permissionx依赖
camerax自身依赖可以在官方文档中查阅最新版本

// CameraX core library using the camera2 implementation
    def camerax_version = "1.2.0-alpha02" //1.2.0-alpha02
// 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"
//权限请求 支持Androidx
    implementation 'com.guolindev.permissionx:permissionx:1.6.4'

二、权限申请

1.Manifest配置文件中

代码如下(示例):

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- 摄像头权限 -->
    <uses-permission android:name="android.permission.CAMERA" /> <!-- 具备摄像头 -->
    <uses-feature android:name="android.hardware.camera.any" /> <!-- 存储图像或者视频权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- 录制音频权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

这里若需要读取相册中的文件进行上传等操作时需要在application中添加:

android:requestLegacyExternalStorage="true"

2.Activity中动态申请权限

代码如下(示例):

//权限申请
        PermissionX.init(this).permissions(
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.RECORD_AUDIO)
            .request allGranted, deniedList, _ ->
                if(allGranted)
                    //若权限申请成功
                    //启动预览
                    setUpCamera( binding.previewView )

                    //按键监听事件
                    listener()
                
                else
                    Toast.makeText(this," You denied $deniedList", Toast.LENGTH_SHORT).show()
                
            

这里使用的viewBinding进行的布局绑定binding.previewView即为布局文件中的预览控件

3.布局文件

主要是PreviewView用以相机预览

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/toolbarvideo"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        android:background="@color/colorPrimary"
        app:layout_collapseMode="pin">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="25dp"
            android:text="录制视频">

        </TextView>
    </androidx.appcompat.widget.Toolbar>


    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/toolbarvideo" />

    <ImageView
        android:id="@+id/recording"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:src="@drawable/start"
        android:layout_centerHorizontal="true"
        android:layout_alignParentBottom="true"
        android:foreground="?android:attr/selectableItemBackground"/>


</RelativeLayout>

三、功能实现

1.预览

setUpCamera函数具体实现

private fun setUpCamera(previewView: PreviewView) 

        val camerProviderFuture : ListenableFuture<ProcessCameraProvider> =
            ProcessCameraProvider.getInstance(this)
        camerProviderFuture.addListener(
            try 
                //绑定相机生命周期
                cameraProvider = camerProviderFuture.get()
                bindPreview(cameraProvider, previewView)
            catch (e: Exception)
                e.printStackTrace()
            
        , ContextCompat.getMainExecutor(this))

    
    @SuppressLint("RestrictedApi")
    private fun bindPreview(
        cameraProvider: ProcessCameraProvider,
        previewView: PreviewView)
    
        //重新绑定前取消绑定用例
        cameraProvider.unbindAll()

        //设置预览
        preview = Preview.Builder().build()

        videoCapture = VideoCapture.Builder()
            .setVideoFrameRate(25)
            .setBitRate(3*1024*1024)
            .build()

        camera = cameraProvider.bindToLifecycle(
            this,
            CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture
        )
        preview?.setSurfaceProvider(previewView.surfaceProvider)

    

2.录制

oncreate()中添加监听事件

binding.recording.setOnClickListener 
            //开始录制

            if(!inRecording)
                startRecording()
                inRecording = true
                binding.recording.setImageResource(R.drawable.stop)
            else
                videoCapture?.stopRecording() //这里为结束录制方法
                binding.recording.setImageResource(R.drawable.start)
                inRecording = false
            
        

startRecording函数实现

@SuppressLint("RestrictedApi", "MissingPermission")
    private fun startRecording()
        val contentValues = ContentValues()
        contentValues.put(
            MediaStore.MediaColumns.DISPLAY_NAME,
            RECORDED_FILE_NAME+"_"+System.currentTimeMillis()
        )
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE,RECORDED_FILE_NAME_END)

        val outputFileOptions = VideoCapture.OutputFileOptions.Builder(
            contentResolver,
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues
        ).build()
        videoCapture?.startRecording(
            outputFileOptions,
            ContextCompat.getMainExecutor(this),
            object : VideoCapture.OnVideoSavedCallback
                override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) 
                    val proj = arrayOf(MediaStore.Images.Media.DATA)
                    val actualimagecursor = managedQuery(outputFileResults.savedUri, proj, null, null, null)
                    val actual_image_column_index = actualimagecursor.getColumnIndexOrThrow(
                        MediaStore.Images.Media.DATA);
                    actualimagecursor.moveToFirst();
                    val img_path = actualimagecursor.getString(actual_image_column_index);
                    Log.i("TAG", "视频路径: " + img_path);

                    Log.i("TAG", "视频保存成功: $outputFileResults.savedUri")
                

                override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) 
                    Log.i("TAG", "出现异常 cause:$cause")
                
            
        )
    

2.对焦功能(附加)

添加监听事件

//点击对焦
        binding.previewView.setOnTouchListener  view , event ->
            val action = FocusMeteringAction.Builder(
                binding.previewView.meteringPointFactory.createPoint(event.x, event.y)
            ).build();

            showTapView(event.x.toInt(), event.y.toInt())
            camera?.cameraControl?.startFocusAndMetering((action))

            true
        

showTapView函数实现

//显示对焦图标
    private fun showTapView( x:Int, y:Int ) 
        val popupWindow = PopupWindow(
            100,
            100

        )

        val imageView = ImageView(this)
        imageView.setImageResource(R.drawable.ic_focus_view)

        popupWindow.contentView = imageView
        popupWindow.showAsDropDown(binding.previewView, x,y)
        binding.previewView.postDelayed(popupWindow.dismiss(), 600)
        binding.previewView.playSoundEffect(SoundEffectConstants.CLICK)

    

总结

现在就可以实现点击imageview录制再次点击结束录制,并可在相册中看到录制的视频

Android 使用CameraX实现预览/拍照/录制视频/图片分析/对焦/缩放/切换摄像头等操作

1. CameraX架构

看官方文档 CameraX架构
有如下这一段话

使用CameraX,借助名为"用例"的抽象概念与设备的相机进行交互。

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

不同用例可以组合使用,也可以同时处于活跃状态。
例如,应用中可以加入预览用例,以便让用户查看进入相机视野的画面
加入图片分析用例,以确定照片里的人物是否在微笑
还可以加入图片拍摄用例,以便在人物微笑时拍摄照片

第一次看的时候,一脸懵逼,“用例”,是个什么鬼玩意。

后来,研究了一下,知道"用例"的英文原文叫做Use Case,CameraX中的每一项操作,对应着一种UseCase

  • 预览 : Preview.java
  • 图片分析 : ImageAnalysis.java
  • 图片拍摄 : ImageCapture.java
  • 视频拍摄 : VideoCapture.java

可以看到,这几个类都是继承自UseCase.java类的

public final class Preview extends UseCase 
	//...

public final class ImageAnalysis extends UseCase 
	//...

public final class ImageCapture extends UseCase 
	//...

public final class VideoCapture extends UseCase 
	//...

接下来让我们来尝试使用一下。

2. 前置操作

首先,我们需要新建一个项目,然后引入依赖

// CameraX core library using the camera2 implementation
def camerax_version = "1.2.0-alpha02" //1.2.0-alpha02
// 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"

AndroidManifest.xml里添加权限

<!--摄像头权限-->
<uses-permission android:name="android.permission.CAMERA" />
<!--具备摄像头-->
<uses-feature android:name="android.hardware.camera.any" />
<!--存储图像或者视频权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!--录制音频权限-->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

别忘了申请权限

ActivityCompat.requestPermissions(
        this, arrayOf(
            Manifest.permission.CAMERA,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.RECORD_AUDIO
        ), 123
    )

3. 预览 : Preview.java

首先修改activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/camera_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/black">

    <androidx.camera.view.PreviewView
        android:id="@+id/previewView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

修改MainActivity.kt

class MainActivity : AppCompatActivity() 
    private lateinit var binding: ActivityMainBinding
    private lateinit var cameraProvider: ProcessCameraProvider
    private var preview: Preview? = null
    private var camera: Camera? = null

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        //TODO 省略了权限申请,具体看文章中 "前置操作" 部分

        setUpCamera(binding.previewView)
    

    private fun setUpCamera(previewView: PreviewView) 
        val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
            ProcessCameraProvider.getInstance(this)
        cameraProviderFuture.addListener(
            try 
                cameraProvider = cameraProviderFuture.get()
                bindPreview(cameraProvider, previewView)
             catch (e: Exception) 
                e.printStackTrace()
            
        , ContextCompat.getMainExecutor(this))
    

    private fun bindPreview(
        cameraProvider: ProcessCameraProvider,
        previewView: PreviewView
    ) 
	    //解除所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常
        cameraProvider.unbindAll()
        preview = Preview.Builder().build()
        camera = cameraProvider.bindToLifecycle(
            this,
            CameraSelector.DEFAULT_BACK_CAMERA, preview
        )
        preview?.setSurfaceProvider(previewView.surfaceProvider)
    

看下效果

4. 图像分析 : ImageAnalysis.java

图像分析用例ImageAnalysis为应用提供可实时分析的图像数据,我们可以对这些图像执行图像处理、计算机视觉或机器学习推断。

val imageAnalysis = ImageAnalysis.Builder()
    // enable the following line if RGBA output is needed.
    // .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
    .setTargetResolution(Size(1280, 720))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
    .build()
imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer  imageProxy ->
    val rotationDegrees = imageProxy.imageInfo.rotationDegrees
    // insert your code here.
    // 在这里处理图片的解析,比如解析成二维码之类的
    ...
    // after done, release the ImageProxy object
    imageProxy.close()
)

在调用cameraProvider.bindToLifecycle()时,进行传入

cameraProvider.bindToLifecycle(
	this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalysis
)

5. 拍照 : ImageCapture.java

5.1 仅拍照

这里,我们需要先创建一个imageCapture

imageCapture = ImageCapture.Builder()
	.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
	//.setTargetAspectRatio(screenAspectRatio)
	//.setTargetRotation(binding.previewView.display.rotation)
	.build()

然后,在调用cameraProvider.bindToLifecycle()时,进行传入

camera = cameraProvider.bindToLifecycle(
    this,CameraSelector.DEFAULT_BACK_CAMERA, preview, imageCapture
)

增加takePicture()方法进行拍照

//进行拍照
private fun takePicture() 
    imageCapture?.let  imageCapture ->
        val mainExecutor = ContextCompat.getMainExecutor(this)
        imageCapture.takePicture(mainExecutor, object : ImageCapture.OnImageCapturedCallback() 
            override fun onCaptureSuccess(image: ImageProxy) 
                super.onCaptureSuccess(image)
            

            override fun onError(exception: ImageCaptureException) 
                super.onError(exception)
            
        )

        // 让画面闪一下,营造拍照的感觉
        // We can only change the foreground Drawable using API level 23+ API
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 
            // Display flash animation to indicate that photo was captured
            binding.root.postDelayed(
                binding.root.foreground = ColorDrawable(Color.WHITE)
                binding.root.postDelayed(
                     binding.root.foreground = null , 50L
                )
            , 100L)
        
    

5.2 拍照并保存到本地存储

我们也可以拍照后,保存到本地存储中

/** Helper function used to create a timestamped file */
private fun createFile(baseFolder: File, format: String, extension: String) =
    File(
        baseFolder, SimpleDateFormat(format, Locale.US)
            .format(System.currentTimeMillis()) + extension
    )

/** Use external media if it is available, our app's file directory otherwise */
fun getOutputDirectory(context: Context): File 
    val appContext = context.applicationContext
    val mediaDir = context.externalMediaDirs.firstOrNull()?.let 
        File(it, appContext.resources.getString(R.string.app_name)).apply  mkdirs() 
    
    return if (mediaDir != null && mediaDir.exists())
        mediaDir else appContext.filesDir


companion object 
    private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
    private const val PHOTO_EXTENSION = ".jpg"


//进行拍照并保存到本地
private fun takePictureSaveToDisk() 
    imageCapture?.let  imageCapture ->

        // Create output file to hold the image
        val photoFile = createFile(getOutputDirectory(this), FILENAME, PHOTO_EXTENSION)
        Log.i(TAG, "photoFile:$photoFile")

        // Setup image capture metadata
        val metadata = ImageCapture.Metadata().apply 

            // Mirror image when using the front camera
            isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
        

        // Create output options object which contains file + metadata
        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
            .setMetadata(metadata)
            .build()

        // Setup image capture listener which is triggered after photo has been taken
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback 
                override fun onError(exc: ImageCaptureException) 
                    Log.e(TAG, "Photo capture failed: $exc.message", exc)
                

                override fun onImageSaved(output: ImageCapture.OutputFileResults) 
                    val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                    Log.d(TAG, "Photo capture succeeded: $savedUri")

                    // Implicit broadcasts will be ignored for devices running API level >= 24
                    // so if you only target API level 24+ you can remove this statement
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) 
                        application.sendBroadcast(
                            Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
                        )
                    

                    // If the folder selected is an external media directory, this is
                    // unnecessary but otherwise other apps will not be able to access our
                    // images unless we scan them using [MediaScannerConnection]
                    val mimeType = MimeTypeMap.getSingleton()
                        .getMimeTypeFromExtension(savedUri.toFile().extension)
                    MediaScannerConnection.scanFile(
                        application,
                        arrayOf(savedUri.toFile().absolutePath),
                        arrayOf(mimeType)
                    )  _, uri ->
                        Log.d(TAG, "Image capture scanned into media store: $uri")
                    
                
            )

 		// 让画面闪一下,营造拍照的感觉
        // We can only change the foreground Drawable using API level 23+ API
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 
            // Display flash animation to indicate that photo was captured
            binding.root.postDelayed(
                binding.root.foreground = ColorDrawable(Color.WHITE)
                binding.root.postDelayed(
                     binding.root.foreground = null , 50L
                )
            , 100L)
        
    

然后,我们可以在相册里找到这张图片了,图片的真实位置位于/storage/emulated/0/Android/media/你的包名/项目名/中。

6. 视频录制 : VideoCapture.java

视频录制用的是VideoCapture

videoCapture = VideoCapture.Builder()
	//.setTargetRotation(previewView.getDisplay().getRotation())
	.setVideoFrameRate(25)
	.setBitRate(3 * 1024 * 1024)
	.build()

在调用cameraProvider.bindToLifecycle()时,进行传入。

camera = cameraProvider.bindToLifecycle(
    this,CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture 
)

需要注意的是,videoCapture无法和imageAnalysisimageCapture一起使用。
如果同一个页面中这几个功能融合在一起,则需要通过标志位来进行判断。

if (isVideo) 
    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,
            preview, videoCapture);
 else 
    mCamera = cameraProvider.bindToLifecycle(this, cameraSelector,
            preview, imageCapture, imageAnalysis);

开始录制

private val RECORDED_FILE_NAME = "recorded_video"
private val RECORDED_FILE_NAME_END = "video/mp4"

@SuppressLint("RestrictedApi")
private fun startRecording() 
	//TODO 这里省略了RECORD_AUDIO、PERMISSION_GRANTED权限的判断
	
    val contentValues = ContentValues()
    contentValues.put(
        MediaStore.MediaColumns.DISPLAY_NAME,
        RECORDED_FILE_NAME + "_" + System.currentTimeMillis()
    )
    contentValues.put(MediaStore.MediaColumns.MIME_TYPE, RECORDED_FILE_NAME_END)

    val outputFileOptions = VideoCapture.OutputFileOptions.Builder(
        getContentResolver(),
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues
    ).build()
    videoCapture.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        object : VideoCapture.OnVideoSavedCallback 
            override fun onVideoSaved(outputFileResults: VideoCapture.OutputFileResults) 
                Log.i(TAG, "视频保存成功:$outputFileResults.savedUri")
            

            override fun onError(
                videoCaptureError: Int,
                message: String,
                cause: Throwable?
            ) 
                Log.i(TAG, "当出现异常 cause:$cause")
            
        
    )

停止视频录制

videoCapture.stopRecording()

当我们执行停止视频录制之后,就可以在相册里看到多了一个录制的视频了。

介绍了CameraX里一些常用的UseCase,我们接下来来看下CameraX中的其他一些功能。

7. 切换前后摄像头

我们之前使用cameraProvider.bindToLifecycle()的时候,有一个参数是CameraSelector
CameraX默认给我们提供了前置摄像头和后置摄像头的CameraSelector

public final class CameraSelector 
    @NonNull
    public static final CameraSelector DEFAULT_FRONT_CAMERA =
            new CameraSelector.Builder().requireLensFacing(LENS_FACING_FRONT).build();

    @NonNull
    public static final CameraSelector DEFAULT_BACK_CAMERA =
            new CameraSelector.Builder().requireLensFacing(LENS_FACING_BACK).build();

	//...

我们去切换摄像头的时候,就是重新调用一下bindPreview方法,传入新的cameraSelector值就好了

private fun bindPreview(
        cameraProvider: ProcessCameraProvider,
        previewView: PreviewView,
        cameraSelector : CameraSelector
    ) 
        // 解除所有绑定,防止CameraProvider重复绑定到Lifecycle发生异常
        cameraProvider.unbindAll()
        preview = Preview.Builder().build()
        camera = cameraProvider.bindToLifecycle(
            this,
            cameraSelector, preview
        )
        preview?.setSurfaceProvider(previewView.surfaceProvider)
    

CameraX还为我们提供了判断前置/后置摄像头是否存在的方法

/** Returns true if the device has an available back camera. False otherwise */
private fun hasBackCamera(): Boolean 
    return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERAAndroid CameraX - 录制视频时的面部检测

Android CameraX实现摄像头预览拍照录制视频

cameraX视频录制 拷贝直接用

cameraX视频录制 拷贝直接用

cameraX视频录制 拷贝直接用

为啥CameraX预览在真机上模糊不清,怎么对焦?