ray-note/博客/Android/相机/Android Camera2 在预览时,录制视频.md

356 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

>写在前面:
> 1. 本文中的方法只是实现在 `Android` 中使用 `Camera2` 的情况下实现一个在预览时实现时评录制。本文中的代码只是一个demo相机相关的流程需要再细化
> 2. 另外,本文中的代码会存在一个问题,预览过程中会出现拉伸,原因是,本例中使用的是全局预览,需要再做适配,后面再补上.
> 3. 本文中的代码,并不规范,只是实现效果,许多地方的异常可能发生的地方,被直接忽略了
### 因为代码比较简单,所以直接给出代码:
```kotlin
class RecordVideoWhilePreviewClient(
val context: Context,
val mTextureView: AutoFitTextureView,
val filePath: String,
val stateListener: CameraStateListener
) {
companion object {
const val TAG = "RecordVideoWhilePreviewClient"
}
private var state: State = State.NEW
val SENSOR_ORIENTATION_DEFAULT_DEGREES = 90
private val SENSOR_ORIENTATION_INVERSE_DEGREES = 270
private val DEAULT_ORIENTATIONS = SparseIntArray().apply {
append(Surface.ROTATION_0, 90)
append(Surface.ROTATION_90, 0)
append(Surface.ROTATION_180, 270)
append(Surface.ROTATION_270, 180)
}
private val INVERSE_ORIENTATIONS = SparseIntArray().apply {
append(Surface.ROTATION_0, 270)
append(Surface.ROTATION_90, 180)
append(Surface.ROTATION_180, 90)
append(Surface.ROTATION_270, 0)
}
private var mBackgroundThread: HandlerThread = HandlerThread(TAG)
private lateinit var mBackHandler: Handler
@Volatile
private var mCameraDevice: CameraDevice? = null
@Volatile
private var mCameraCaptureSession: CameraCaptureSession? = null
@Volatile
private var mCaptureRequest: CaptureRequest.Builder? = null
@Volatile
private var mPreviewSurface: Surface? = null
@Volatile
private var mRecordSurface: Surface? = null
@Volatile
private var mMediaRecorder: MediaRecorder? = null
@Volatile
private var mSensorOrientation: Int = 0
@Volatile
private var mCameraOpenCloseLock = Semaphore(1)
private lateinit var mPreviewSize: Size
private lateinit var mVideoSize: Size
private val mSurfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
openCamera(width, height)
}
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
configureTransform(width, height)
}
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) = true
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {
} }
private val mCameraStateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
mCameraOpenCloseLock.release()
mCameraDevice = device
if (!mTextureView.isAvailable) return
try {
val texture = mTextureView.surfaceTexture!!
// texture.setDefaultBufferSize(, 720)
val previewSurface = Surface(texture)
mPreviewSurface = previewSurface
setupMediaRecorder(1280, 720)
val recordSurface = mRecordSurface!!
mCaptureRequest = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
addTarget(previewSurface)
addTarget(recordSurface)
}
mCameraDevice?.createCaptureSession(
listOf(previewSurface, recordSurface),
mCaptureSessionStateCallback,
mBackHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "onOpened: ")
}
}
override fun onDisconnected(device: CameraDevice) {
mCameraOpenCloseLock.release()
device.close()
mCameraDevice = null
}
override fun onError(device: CameraDevice, error: Int) {
mCameraOpenCloseLock.release()
device.close()
mCameraDevice = null
}
}
private val mCaptureSessionStateCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
mCameraCaptureSession = session
val camera = mCameraDevice ?: return
val previewSurface = mPreviewSurface ?: return
val recordSurface = mRecordSurface ?: return
val captureRequest = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
addTarget(previewSurface)
addTarget(recordSurface)
}
mCaptureRequest = captureRequest
session.setRepeatingRequest(captureRequest.build(), mCaptureCallback, mBackHandler)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "CameraCaptureSession.StateCallback -- onConfigureFailed: ")
}
}
private val mCaptureCallback = object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
session: CameraCaptureSession,
request: CaptureRequest,
result: TotalCaptureResult
) {
super.onCaptureCompleted(session, request, result)
if (state == State.INITIALIZING) {
onStateChange(State.READY)
}
}
}
init {
mBackgroundThread.start()
mBackHandler = Handler(mBackgroundThread.looper)
}
@SuppressLint("MissingPermission")
fun openCamera(width: Int, height: Int) {
val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
try {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw RuntimeException("Time out waiting to lock camera opening")
}
val cameraId = manager.cameraIdList[0]
val characteristics = manager.getCameraCharacteristics(cameraId)
val range21 = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
val map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
?: throw RuntimeException("Cannot get available preview/video sizes")
mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
mVideoSize = Size(1280, 720)
mPreviewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture::class.java), width, height, Size(2400, 1080));
mTextureView.setAspectRatio(mPreviewSize.width, mPreviewSize.height)
configureTransform(width, height)
mMediaRecorder = MediaRecorder()
manager.openCamera(cameraId, mCameraStateCallback, null)
} catch (e: CameraAccessException) {
Log.e(RecordVideoWhilePreview.TAG, "openCamera: Cannot access the camera. \n$e")
} catch (e: NullPointerException) {
Log.e(RecordVideoWhilePreview.TAG, "onRequestPermissionsResult: 没得相机硬件")
} catch (e: InterruptedException) {
throw RuntimeException("Interrupted while trying to lock camera opening.")
}
}
private fun setupMediaRecorder(width: Int, height: Int) {
var mediaRecorder = mMediaRecorder
if (mMediaRecorder == null) {
mediaRecorder = MediaRecorder()
} else {
mediaRecorder?.reset()
}
val rotation = (context as Activity).windowManager.defaultDisplay.rotation
when (mSensorOrientation) {
SENSOR_ORIENTATION_DEFAULT_DEGREES -> {
mediaRecorder?.setOrientationHint(DEAULT_ORIENTATIONS.get(rotation))
}
SENSOR_ORIENTATION_INVERSE_DEGREES -> {
mediaRecorder?.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation))
}
}
var recordSurface = mRecordSurface
if (recordSurface == null) {
recordSurface = MediaCodec.createPersistentInputSurface()
mRecordSurface = recordSurface
}
mediaRecorder?.apply {
setInputSurface(recordSurface!!)
setVideoSource(MediaRecorder.VideoSource.SURFACE)
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
setOutputFile(filePath)
setVideoEncodingBitRate(1280*720)
setVideoSize(width, height)
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
setCaptureRate(30.0)
setVideoFrameRate(30)
prepare()
}
mMediaRecorder = mediaRecorder
}
private fun chooseOptimalSize(
choices: Array<Size>,
width: Int,
height: Int,
aspectRatio: Size
): Size {
val w = aspectRatio.width
val h = aspectRatio.height
val bigEnough = choices.filter {
it.height == it.width * h / w && it.width >= width && it.height >= height
}
return if (bigEnough.isNotEmpty()) {
Collections.min(bigEnough, CompareSizeByArea())
} else {
choices[0]
}
}
private fun configureTransform(viewWidth: Int, viewHeight: Int) {
val rotation = (context as Activity).windowManager.defaultDisplay.rotation
val matrix = Matrix()
val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
val bufferRect = RectF(0f, 0f, mPreviewSize.height.toFloat(), mPreviewSize.width.toFloat())
val centerY = viewRect.centerY()
val centerX = viewRect.centerX()
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY())
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL)
val scale = Math.max(
viewHeight.toFloat() / mPreviewSize.height,
viewWidth.toFloat() / mPreviewSize.width
)
with(matrix) {
postScale(scale, scale, centerX, centerY)
postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY)
}
}
mTextureView.setTransform(matrix)
}
private fun onStateChange(newState: State) {
state = newState
stateListener.onStateChange(state)
val state = when (state) {
State.NEW -> "STATE.NEW"
State.INITIALIZING -> "STATE.INITIALIZING"
State.READY -> "STATE.READY"
State.RECORDING -> "STATE.RECORDING"
State.COMPETE -> "STATE.COMPLETE"
}
context.showToast(state, Toast.LENGTH_SHORT)
}
fun initialize() {
if (mTextureView.isAvailable) {
openCamera(mTextureView.width, mTextureView.height)
} else {
mTextureView.surfaceTextureListener = mSurfaceTextureListener
}
onStateChange(State.INITIALIZING)
}
fun close() {
mBackgroundThread?.quitSafely()
try {
mBackgroundThread?.join()
} catch (e: InterruptedException) {
Log.e(TAG, "close: ")
}
mPreviewSurface?.release()
mRecordSurface?.release()
mMediaRecorder?.release()
mMediaRecorder = null
}
fun startRecord() {
if (state != State.READY && state != State.COMPETE) return
context.showToast("开始录像", Toast.LENGTH_SHORT)
setupMediaRecorder(1280, 720)
mMediaRecorder?.let {
it.start()
}
onStateChange(State.RECORDING)
}
fun stopRecord() {
if (state != State.RECORDING) return
context.showToast("结束录像", Toast.LENGTH_SHORT)
mMediaRecorder?.let {
it.stop()
it.reset()
}
onStateChange(State.COMPETE)
}
interface CameraStateListener {
fun onStateChange(newState: State)
}
enum class State {
NEW,
INITIALIZING,
READY,
RECORDING,
COMPETE,
}
}
```
### 需要注意的问题:
- 在创建 `MediaRecorder` 的时候,需要设置文件路径,这个时候,会在文件系统中生成一个空文件,等到录制的视频数据写入。
- 但是我们上面的代码中,在 `CameraDevice.StateCallback``onOpened()` 中床创建 `CameraCaptureSession.StateCallback` 后面的过程中需要使用到 用来在 `MediaRecorder``Surface`, 需要提前 `设置好MediaRecorder` 并调用 `prepare` 不然预览会失败
- 所以在第一次会创建一个文件,建议处理好文件的路径,本例目前只有一个文件路径
### 直接总结:
- 本文主要参考的是 [google官方的demo](https://github.com/android/camera-samples/tree/main/Camera2Basic), 官方的预览和录制视频是分开的,导致我一开始以为需要重新创建 `CameraCaptureSession`, 所以一开始,是在 *预览**视频录制* 之间进行切换,但是这个方法在 **切换** 的时候,会有明显的卡顿。
- 最后选择了在预览的 `CameraCaptureSession` 中添加 `MediaRecorder` 用来录制的 `Surface`. 最后实现的效果还是相对理想的,但是会有一个瑕疵,预览的显示和最后录制得到的视频不一致,但是我需要的是最后的结果,屏幕预览的效果,后面再适配一下,是我可以接受的效果