356 lines
14 KiB
Markdown
356 lines
14 KiB
Markdown
|
||
>写在前面:
|
||
> 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`. 最后实现的效果还是相对理想的,但是会有一个瑕疵,预览的显示和最后录制得到的视频不一致,但是我需要的是最后的结果,屏幕预览的效果,后面再适配一下,是我可以接受的效果
|