Android-Camera X录制视频

472 阅读6分钟

Camera X拍照 的基础上实现Camera X录制视

视频的录制

主要用到了 VideoCapture和Recording这两个类来完成。

  • 获取到ProcessCameraProvider 对象;
  • 创建 preview,设置宽高,并和显示的PreviewView绑定到一起;
  • 创建一个Recoder,设置图片质量;
  • 创建出 videoCapture 对象;
  • 绑定 videoCapture;

绑定主要代码如下:

//前面遵循cmaerx拍照相关预览配置
//1.获得Recorder
val recorder = Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST)).build()
//2、获得videoCapture
videoCapture = VideoCapture.withOutput(recorder)
//3、 通过 cameraProvider 绑定生命周期得到Camera对象
try {
			camera = cameraProvider.bindToLifecycle(
				this, cameraSelector, preview, imageCapture, videoCapture,imageAnalyzer
			)
			// 设置 surfaceProvider,这样数据就会源源不断流到 viewFinder(PreviewView)控件上了,这样就完成了预览了
			preview?.setSurfaceProvider(viewFinder.surfaceProvider)   // 观察相机状态
			observeCameraState(camera?.cameraInfo!!)
		} catch (exc : Exception) {
			Log.e(TAG, "Use case binding failed", exc)
		}

将 VideoCapture 与其他用例结合使用

clip-20240625105741.png转存失败,建议直接上传图片文件

camerax视频的录制与关闭

通过Recording对象,操作暂停、继续、停止操作完成视频录制。 然后可以通过监听VideoRecordEvent知道录制的状态

/**视频的录制与关闭*/
	private fun captureVideo() {
//		val videoCapture = this.videoCapture ?: return
//		viewBinding.videoCaptureButton.isEnabled = false
		var curRecording = recorder
		if (curRecording != null) {
			// Stop the current recording session.
			curRecording.stop()
			recorder = null
			return
		}
		val name = SimpleDateFormat("yyyy-MM-dd HH:mm:ss",  Locale.US).format(System.currentTimeMillis())

		val contentValues = ContentValues().apply {
			put(MediaStore.MediaColumns.DISPLAY_NAME, name)
			put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
			if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
				put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
			}
		}

		val mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
			contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI
		).setContentValues(contentValues).build()
		// 创建一个Recoder
		recorder = videoCapture?.output?.prepareRecording(this, mediaStoreOutputOptions).apply {
			if (PermissionChecker.checkSelfPermission(
					this@MainActivity, Manifest.permission.RECORD_AUDIO
				) == PermissionChecker.PERMISSION_GRANTED
			) {
//					withAudioEnabled()
			}
		}?.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
			//lambda event listener
			when (recordEvent) {
				is VideoRecordEvent.Start -> {
					cameraRecordButton.text = "停止录制"
					isEnabled = true
				}
				is VideoRecordEvent.Finalize -> {
					if (!recordEvent.hasError()) {
						val msg =
							"Video capture succeeded: " + "${recordEvent.outputResults.outputUri}"
						Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
						Log.d(TAG, msg)
					} else {
						curRecording?.close()
						curRecording = null
						Log.e(
							TAG, "Video capture ends with error: " + "${recordEvent.error}"
						)
					}
					cameraRecordButton.text = "开始录制"
					isEnabled = true
				}
			}
		}
	}

监听录制状态

private val captureListener = Consumer { event ->

}

停止录制

curRecording.stop()

继续录制

curRecording. resume()

暂停录制

curRecording. pause()

图像亮度分析实例

private class LuminosityAnalyzer(listener : LumaListener? = null) : ImageAnalysis.Analyzer {
		private val frameRateWindow = 8
		private val frameTimestamps = ArrayDeque<Long>(5)
		private val listeners = ArrayList<LumaListener>().apply { listener?.let { add(it) } }
		private var lastAnalyzedTimestamp = 0L
		var framesPerSecond : Double = -1.0
			private set

		/** 用于从图像平面缓冲区提取字节数组的Helper扩展函*/
		private fun ByteBuffer.toByteArray() : ByteArray {
			rewind()    // 将缓冲区倒回零
			val data = ByteArray(remaining())
			get(data)   // 将缓冲区复制到字节数组中
			return data // 返回字节数组
		}

		/**
		 * 分析图像产生的结果
		 *
		 * <p>调用方负责确保此分析方法能够快速执行
		 *足以防止图像采集管线中的停滞。否则,新可用
		 *将不会获取和分析图像。
		 *
		 *<p>此方法返回后,传递给此方法的图像将无效。呼叫者
		 *不应存储对此图像的外部引用,因为这些引用将变成无效的
		 *
		 * @正在分析的param图像非常重要:Analyzer方法实现必须
		 *在使用完收到的图像后,对其调用image.close()。否则,新图像
		 *可能无法接收到或相机可能失速。
		 */
		override fun analyze(image : ImageProxy) {
			// 如果没有附加侦听器,我们就不需要执行分析
			if (listeners.isEmpty()) {
				image.close()
				return
			}
			// 跟踪分析的帧
			val currentTime = System.currentTimeMillis()
			frameTimestamps.addFirst(currentTime)
			// 使用移动平均值计算FPS
			while (frameTimestamps.size >= frameRateWindow) frameTimestamps.removeLast()
			val timestampFirst = frameTimestamps.first() ?: currentTime
			val timestampLast = frameTimestamps.last() ?: currentTime
			framesPerSecond =
				1.0 / ((timestampFirst - timestampLast) / frameTimestamps.size.coerceAtLeast(1)
					.toDouble()) * 1000.0

			// 分析可能需要任意长的时间
			// 由于我们在不同的线程中运行,它不会拖延其他用例
			lastAnalyzedTimestamp = frameTimestamps.first()
			// 由于ImageAnalysis中的格式是YUV,因此image.planes[0]包含亮度平面
			val buffer = image.planes[0].buffer
			// 从回调对象中提取图像数据
			val data = buffer.toByteArray()
			// 将数据转换为0-255范围内的像素值数组
			val pixels = data.map { it.toInt() and 0xFF }
			// 计算图像的平均亮度
			val luma = pixels.average()
			// 用新值呼叫所有侦听器setOutputFile
			listeners.forEach { it(luma) }
			image.close()
		}
	}

一些用到的API

VideoCapture

在 Android 中,VideoCapture 是一种用于录制视频的类,它是 CameraX 相机库中的一部分。 VideoCapture 类提供了一种简单且方便的方式来录制视频。可以使用 VideoCapture 类创建一个实例,并配置一些属性和参数来控制视频录制的行为。 以下是 VideoCapture 类的一些功能和常用方法:

1.开始和停止录制:使用 startRecording() 方法开始录制视频,并使用 stopRecording() 方法停止录制。这些方法会自动处理音频和视频数据的捕获、编码和保存。

2.设置输出文件:使用 setOutputFile() 方法设置保存录制视频的输出文件路径。

3.设置视频质量和参数:VideoCapture 支持设置视频帧率、分辨率、比特率等参数,以及选择前置或后置摄像头进行录制。

4.监听录制状态:可以注册一个 VideoCapture.OnVideoSavedCallback 回调接口,以监听录制完成的回调和错误处理。

5.自定义录制配置:VideoCapture 还支持其他一些配置选项,如设置录制时使用的摄像头、音频源、视频编码器等。

VideoCapture 通过封装底层的 CameraX 和 MediaRecorder 功能,为开发者提供了更简单的录制视频的接口。使用 VideoCapture 类,可以快速实现视频录制的功能,并根据需要进行定制和扩展。

QualitySelector

QualitySelector(图像质量选择器)并不是一个内置的类。可以通过使用相机 API 或图像处理库来实现图像质量的选择和控制。 使用相机 API 进行图像捕获,可以通过设置参数来控制图像的质量。以下是一些可用于控制图像质量的常见参数:

1.图像格式(Image Format):您可以选择不同的图像格式,如 JPEG、PNG 等。JPEG 是最常用的图像格式,可以通过设置图像质量参数来控制压缩比例和图像质量级别。

2.图像质量(Image Quality):对于 JPEG 格式的图像,您可以设置图像质量参数来控制压缩比例和图像清晰度。通常,图像质量值的范围为 0 到 100,其中 0 表示最低质量、最高压缩,而 100 表示最高质量、最低压缩。

3.图像分辨率(Image Resolution):您可以选择不同的图像分辨率来控制图像的细节水平和文件大小。较高的分辨率通常会产生更清晰的图像,但也会增加文件大小。

4.其他参数:还有其他一些参数,例如亮度、对比度、饱和度等,可以通过设置来影响图像的质量和效果。

如果您使用图像处理库(如 Glide、Picasso 等)来显示和处理图像,这些库通常提供了一些方法来控制图像的质量。您可以根据库的文档和示例代码来查找有关图像质量控制的具体方法和用法。 需要根据您的具体需求选择相应的方法和参数来控制图像的质量。请注意,图像质量的选择会影响文件大小、加载速度、显示效果等因素,因此需要在图像质量和性能之间进行权衡。

遇到的一些问题

imageAnalyzer 设置的分辨率和 MediaCodec 分辨率设置不一致,编码出来的视频 会是绿屏或是别的花屏,这里要特别注意。 在一些时候,会在MediaCodec 的输入缓冲区会报错 BufferOverflowException,尝试找了原因,但都没有找到,试过设置 MediaFormat.KEY_MAX_INPUT_SIZE 和码率还是没有办法解决。但是我想着还是和分辨率有关,接下来拿 CameraX 支持的分辨率再试一试,看看是不是我设置的分辨率问题。

总结

今天简单的介绍了CameraX 录制视频和编码,总体使用起来还是很简单的,都是通过 cameraProvider.bindToLifecycle 方法,只要往里面添加 对应的UseCase就可以; 录制使用添加的 UseCase是VideoCapture; 编码添加的是 UseCase 是ImageAnalysis; 到这里用Camera 采集视频数据就简单的介绍完了.