写在前面:
- 本文中的方法只是实现在
Android中使用Camera2的情况下,实现一个在预览时,实现时评录制。本文中的代码只是一个demo,相机相关的流程需要再细化,- 另外,本文中的代码会存在一个问题,预览过程中会出现拉伸,原因是,本例中使用的是全局预览,需要再做适配,后面再补上.
- 本文中的代码,并不规范,只是实现效果,许多地方的异常可能发生的地方,被直接忽略了
因为代码比较简单,所以直接给出代码:
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, 官方的预览和录制视频是分开的,导致我一开始以为需要重新创建
CameraCaptureSession, 所以一开始,是在 预览 和 视频录制 之间进行切换,但是这个方法在 切换 的时候,会有明显的卡顿。 - 最后选择了在预览的
CameraCaptureSession中添加MediaRecorder用来录制的Surface. 最后实现的效果还是相对理想的,但是会有一个瑕疵,预览的显示和最后录制得到的视频不一致,但是我需要的是最后的结果,屏幕预览的效果,后面再适配一下,是我可以接受的效果