一、概述
視頻錄制,在一般開(kāi)發(fā)中很少遇到,大部分開(kāi)發(fā)工作都是寫(xiě)寫(xiě)頁(yè)面,請(qǐng)求接口,展示數(shù)據(jù)等等。真要遇到,可能采用第三方庫(kù)實(shí)現(xiàn),一來(lái)實(shí)現(xiàn)快速,二來(lái)可能覺(jué)得別人實(shí)現(xiàn)的比較好。特別是在開(kāi)發(fā)周期很緊的情況下,一般都不會(huì)自己花時(shí)間實(shí)現(xiàn)。
其實(shí)最好是使用手機(jī)系統(tǒng)的錄制視頻,功能完善,穩(wěn)定。實(shí)現(xiàn)起來(lái)最簡(jiǎn)單,簡(jiǎn)簡(jiǎn)單單幾句代碼:
//跳轉(zhuǎn)系統(tǒng)的錄制視頻頁(yè)面
val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY,1)
intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT,30)//錄制時(shí)長(zhǎng)
startActivityForResult(intent, 666)
//打開(kāi)手機(jī)的選擇視頻頁(yè)
// val intent = Intent()
// intent.action = Intent.ACTION_PICK
// intent.data = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
// startActivityForResult(intent,666)
然后在onActiityResult方法中接收錄制好的視頻路徑處理即可。
但如果需求是像微信那樣app內(nèi)的錄制視頻,就不能使用系統(tǒng)自帶錄制功能,需要自己實(shí)現(xiàn)。
下面將我自己實(shí)現(xiàn)的,記錄下來(lái)。這里也只是實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的錄制功能,甚至還有問(wèn)題:前置攝像頭錄制視頻是鏡像的。
另外下面的實(shí)現(xiàn)不支持在Android6.0以下的手機(jī)上使用,因?yàn)槭褂玫搅薃PI23的方法:MediaCodec.createPersistentInputSurface(),主要是為了能支持橫屏錄制的視頻方向?yàn)闄M屏。
先看看演示效果:
二、實(shí)現(xiàn)方案和細(xì)節(jié)
使用的Camera2 和 MediaRecorder。
如果使用Camera1的話,可能會(huì)更簡(jiǎn)單一些,Camera2用起來(lái)確實(shí)相對(duì)麻煩一點(diǎn)。不過(guò)Camera1畢竟已經(jīng)被棄用了,且使用Camera1打開(kāi)相機(jī)比Camera2要耗時(shí)一些。
Camera2使用
- 用CameraManager獲取相機(jī)Id列表cameraIdList,然后openCamera指定的相機(jī)id,打開(kāi)相機(jī)
- 打開(kāi)成功后,使用 CameraDevice.createCaptureSession 創(chuàng)建CameraCaptureSession
- 創(chuàng)建成功后,使用CameraCaptureSession.setRepeatingRequest 發(fā)起預(yù)覽請(qǐng)求,它需要傳入CaptureRequest,通過(guò)CameraDevice.captureRequest創(chuàng)建,CaptureRequest可以設(shè)置一些參數(shù),對(duì)焦、曝光、閃光燈等等
第2步 createCaptureSession 時(shí)需要傳入Surface列表。
這里傳入了兩個(gè)Surface,一個(gè)是預(yù)覽使用,由SurfaceView提供。
另一個(gè)是錄制使用,通過(guò)MediaCodec.createPersistentInputSurface() 創(chuàng)建,設(shè)置給MediaRecorder。
如果預(yù)覽時(shí)不創(chuàng)建MediaRecorder,只傳入預(yù)覽Surface,等到點(diǎn)擊錄制時(shí),才創(chuàng)建MediaRecorder,需要重新創(chuàng)建createCaptureSession,傳入新的Surface,這樣雖然可以,但是點(diǎn)擊錄制時(shí)會(huì)很慢,預(yù)覽畫(huà)面會(huì)斷一下。
第2步傳入的Surface列表,還需要在第3步中使用CaptureRequest.addTarget 添加,兩個(gè)地方必須對(duì)應(yīng),不然發(fā)起預(yù)覽請(qǐng)求時(shí)會(huì)報(bào)錯(cuò)。
MediaRecorder配置
因?yàn)槭褂玫氖荂amera2,所以不能使用MediaRecorder.setCamera()。
替代方法是使用MediaRecorder.surface,前提是需要設(shè)置數(shù)據(jù)源為Surface
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
且需要在mediaRecorder.prepare之后,mediaRecorder.surface 才可用。
后面因?yàn)橐С謾M屏錄制,沒(méi)有采用 mediaRecorder.surface 。
如果進(jìn)入頁(yè)面開(kāi)啟相機(jī)預(yù)覽時(shí)手機(jī)豎屏,點(diǎn)擊錄制時(shí)手機(jī)橫屏,因?yàn)樵陬A(yù)覽時(shí)就創(chuàng)建了mediaRecorder,并且setOrientationHint確定了視頻方向,無(wú)法再改變(只能prepare之前設(shè)置),這時(shí)錄制的視頻方向肯定就不對(duì)。
要改變視頻方向,只能重新創(chuàng)建 mediaRecorder ,但是重新創(chuàng)建mediaRecorder,同時(shí)也重新創(chuàng)建了一個(gè)新的Sueface,需要重新createCaptureSession傳入新的Sueface。(改成點(diǎn)擊錄制時(shí),創(chuàng)建mediaRecorder,然后重新createCaptureSession,測(cè)試中也發(fā)現(xiàn)畫(huà)面會(huì)斷一下,效果不好)。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-516232.html
正因如此,最終改為使用 MediaCodec.createPersistentInputSurface() 創(chuàng)建 Surface,然后 setInputSurface 給 mediaRecorder。MediaCodec.createPersistentInputSurface()創(chuàng)建的Surface只有在mediaRecorder.prepare之后才可用。 在點(diǎn)擊錄制時(shí),重新配置mediaRecorder,設(shè)置新的方向。這樣雖然mediaRecorder重新配置了,但是Surface還是同一個(gè)。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-516232.html
var mediaRecorder = MediaRecorder()
recordSurface = MediaCodec.createPersistentInputSurface()
var recordSurface = mRecordSurface
if (recordSurface == null) {
recordSurface = MediaCodec.createPersistentInputSurface()
mRecordSurface = recordSurface
}
mediaRecorder.setInputSurface(recordSurface)
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
//數(shù)據(jù)源來(lái)之surface
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
//設(shè)置QUALITY_HIGH,可以提高視頻的錄制質(zhì)量(文件也會(huì)變大),但是不能設(shè)置編碼格式和幀率等參數(shù),否則報(bào)錯(cuò)
val profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P)
mediaRecorder.setProfile(profile)
mediaRecorder.setMaxFileSize(0)
mediaRecorder.setVideoSize(width, height)
//視頻方向
mediaRecorder.setOrientationHint(mRecordVideoOrientation)
val parentFile = externalCacheDir ?: cacheDir
val fileName = "${System.currentTimeMillis()}.mp4"
//不設(shè)置setOutputFile prepare時(shí)會(huì)報(bào)錯(cuò)
mediaRecorder.setOutputFile(parentFile.absolutePath + File.separator + fileName)
//prepare之后recordSurface才能用
mediaRecorder.prepare()
三、全部代碼
/**
* 錄制視頻
* 支持后置、前置攝像頭切換(但前置攝像頭錄制視頻是鏡像的,需要翻轉(zhuǎn)) TODO
* Android 6.0以下不支持
* 支持橫屏錄制
* 2023/03/17
*/
@RequiresApi(Build.VERSION_CODES.M)
class VideoRecord23Activity : AppCompatActivity(), SurfaceHolder.Callback,
View.OnClickListener {
companion object {
const val DURATION = "duration"
const val FILE_NAME = "name"
const val FILE_PATH = "path"
}
private val requestPermissionCode = 52
private val requestActivityCode = 10
/**
* mCountDownMsg 錄制進(jìn)度倒計(jì)時(shí)msg,一秒鐘發(fā)送一次
* mStartRecordMsg 開(kāi)始錄制
* mCameraOpenFailMsg 相機(jī)打開(kāi)失敗
* mCameraPreviewFailMsg 相機(jī)預(yù)覽失敗
* mRecordErrorMsg 錄制出現(xiàn)錯(cuò)誤
* 在 mCountDownHandler(主線程的Handler)中處理
*/
private val mCountDownMsg = 19
private val mStartRecordMsg = 20
private val mCameraOpenFailMsg = 21
private val mCameraPreviewFailMsg = 22
private val mRecordErrorMsg = 23
private lateinit var mSurfaceView :SurfaceView
private lateinit var mRecordProgressBar: ProgressBar
private lateinit var mRecordStateIv: ImageView
private lateinit var mFlashlightIv: ImageView
private lateinit var mSwitchCameraIv: ImageView
/**
* 錄制視頻文件路徑
*/
@Volatile
private var mFilePath: String? = null
/**
* 錄制的視頻文件名
*/
@Volatile
private var mFileName: String? = null
/**
* 預(yù)覽畫(huà)面尺寸,和視頻錄制尺寸
*/
@Volatile
private var mRecordSize: Size? = null
/**
* 相機(jī)方向
*/
private var mCameraOrientation: Int = 0
/**
* 錄制視頻的方向,隨著手機(jī)方向的改變而改變
*/
@Volatile
private var mRecordVideoOrientation: Int = 0
/**
* 默認(rèn)打開(kāi)后置相機(jī) LENS_FACING_BACK
* 可以切換為前置相機(jī) LENS_FACING_FRONT
*/
private var mFensFacing = CameraCharacteristics.LENS_FACING_BACK
/**
* 預(yù)覽Surface
*/
@Volatile
private var mPreviewSurface: Surface? = null
/**
* 錄制Surface
*/
@Volatile
private var mRecordSurface: Surface? = null
@Volatile
private var mCameraDevice: CameraDevice? = null
@Volatile
private var mCameraCaptureSession: CameraCaptureSession? = null
@Volatile
private var mCaptureRequest: CaptureRequest.Builder? = null
private var mOrientationEventListener: OrientationEventListener? = null
@Volatile
private var mMediaRecorder: MediaRecorder? = null
/**
* 是否是錄制中的狀態(tài)
* true:錄制中
*/
@Volatile
private var mRecordingState = false
/**
* 是否錄制完成。從手動(dòng)點(diǎn)擊開(kāi)始錄制到手動(dòng)點(diǎn)擊停止錄制(或者錄制時(shí)長(zhǎng)倒計(jì)時(shí)到了),為錄制完成,值為true。其他情況為false
*/
private var mRecordComplete = false
/**
* 閃光燈狀態(tài)
* true 開(kāi)啟
* false 關(guān)閉
*/
private var mFlashlightState = false
/**
* 是否可以錄制
* 錄制完成,跳轉(zhuǎn)播放頁(yè)面后,返回時(shí)點(diǎn)擊的完成,不能錄制
* 其他情況都為可以錄制
*/
private var mRecordable = true
/**
* 錄制最大時(shí)長(zhǎng),時(shí)間到了之后錄制完成
* 單位:秒
*/
private var mMaxRecordDuration = 30
/**
* 已錄制的時(shí)長(zhǎng)
*/
private var mCurrentRecordDuration = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_video_record)
mSurfaceView = findViewById(R.id.surfaceView)
mSurfaceView.holder.addCallback(this)
mRecordStateIv = findViewById(R.id.recordStateIv)
mRecordProgressBar = findViewById(R.id.recordProgressBar)
mFlashlightIv = findViewById(R.id.flashlightIv)
mSwitchCameraIv = findViewById(R.id.switchIv)
mRecordStateIv.setOnClickListener(this)
mFlashlightIv.setOnClickListener(this)
mSwitchCameraIv.setOnClickListener(this)
initOrientationEventListener()
mMaxRecordDuration = intent.getIntExtra(DURATION, 30)
mSurfaceView.scaleX = -1f
}
override fun surfaceCreated(holder: SurfaceHolder?) {
if (mRecordable) {
mRecordComplete = false
mFlashlightState = false
mFlashlightIv.setImageResource(R.drawable.flashlight_off)
checkPermissionAndOpenCamera()
mOrientationEventListener?.enable()
}
}
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
@SuppressLint("MissingPermission")
override fun onClick(v: View) {
when (v.id) {
R.id.recordStateIv -> {
if (mRecordingState) {
//停止錄制
stopRecord()
mRecordComplete = true
//跳轉(zhuǎn)預(yù)覽頁(yè)面
openPlayActivity()
} else {
startRecord()
mOrientationEventListener?.disable()
}
}
R.id.flashlightIv -> {
val captureRequest = mCaptureRequest ?: return
val cameraCaptureSession = mCameraCaptureSession ?: return
if (mFlashlightState) {
//閃光燈開(kāi)啟,點(diǎn)擊關(guān)閉
captureRequest[CaptureRequest.FLASH_MODE] = CaptureRequest.FLASH_MODE_OFF
mFlashlightIv.setImageResource(R.drawable.flashlight_off)
} else {
//閃關(guān)燈關(guān)閉,點(diǎn)擊開(kāi)啟
captureRequest[CaptureRequest.FLASH_MODE] = CaptureRequest.FLASH_MODE_TORCH
mFlashlightIv.setImageResource(R.drawable.flashlight_on)
}
mFlashlightState = !mFlashlightState
cameraCaptureSession.stopRepeating()
mCameraCaptureSession?.setRepeatingRequest(captureRequest.build(), mCameraCaptureSessionCaptureCallback, mRecordThreadHandler)
}
R.id.switchIv -> {
if (mRecordingState) {
//正在錄制中
Toast.makeText(this, "正在錄制", Toast.LENGTH_SHORT).show()
return
}
if (mFensFacing == CameraCharacteristics.LENS_FACING_BACK) {
//當(dāng)前打開(kāi)的是后置攝像頭,切換到前置攝像頭
mFensFacing = CameraCharacteristics.LENS_FACING_FRONT
close()
openCamera()
} else if (mFensFacing == CameraCharacteristics.LENS_FACING_FRONT) {
//當(dāng)前打開(kāi)的是前置攝像頭,切換到后置攝像頭
mFensFacing = CameraCharacteristics.LENS_FACING_BACK
close()
openCamera()
}
}
}
}
private fun startRecord() {
mRecordThreadHandler.sendEmptyMessage(mStartRecordMsg)
mRecordingState = true
mRecordStateIv.setImageResource(R.drawable.record_start_state_bg)
mCurrentRecordDuration = 0
//開(kāi)始錄制倒計(jì)時(shí)
mUiHandler.sendEmptyMessageDelayed(mCountDownMsg, 1000)
}
private fun stopRecord() {
//視圖變?yōu)?停止錄制狀態(tài)
mRecordingState = false
mRecordStateIv.setImageResource(R.drawable.record_stop_state_bg)
mRecordProgressBar.progress = 0
mUiHandler.removeMessages(mCountDownMsg)
val mediaRecorder = mMediaRecorder ?: return
try {
mediaRecorder.stop()
} catch (t: Throwable) {
t.printStackTrace()
}
}
/**
* 檢查相機(jī)和錄音與權(quán)限,并打開(kāi)相機(jī)
*/
private fun checkPermissionAndOpenCamera(){
//錄制音頻權(quán)限ok
val audioOk = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
//相機(jī)權(quán)限ok
val cameraOk = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)== PackageManager.PERMISSION_GRANTED
if (audioOk && cameraOk) {
openCamera()
} else if (!audioOk && !cameraOk) {
val array = arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(this, array, requestPermissionCode)
} else if (!audioOk && cameraOk) {
val array = arrayOf(Manifest.permission.RECORD_AUDIO)
ActivityCompat.requestPermissions(this, array, requestPermissionCode)
} else if (audioOk && !cameraOk) {
val array = arrayOf(Manifest.permission.CAMERA)
ActivityCompat.requestPermissions(this, array, requestPermissionCode)
}
}
@SuppressLint("MissingPermission")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == requestPermissionCode) {
for (i in grantResults) {
if (i != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "請(qǐng)開(kāi)啟相機(jī)和錄音權(quán)限", Toast.LENGTH_SHORT).show()
finish()
return
}
}
openCamera()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == requestActivityCode) {
when (resultCode) {
//確認(rèn)
Activity.RESULT_OK -> {
val fileName = mFileName ?: return
val filePath = mFilePath ?: return
//不能再錄制
mRecordable = false
val intent = Intent()
//把錄制的視頻文件名、文件路徑傳給外面調(diào)用的頁(yè)面
intent.putExtra(FILE_NAME, fileName)
intent.putExtra(FILE_PATH, filePath)
setResult(Activity.RESULT_OK, intent)
finish()
}
//重新錄制
Activity.RESULT_CANCELED -> {
//刪除文件,重新錄制
deleteRecordFile()
}
}
}
}
/**
* 頁(yè)面暫停時(shí),關(guān)閉相機(jī),停止錄制
*/
override fun onStop() {
super.onStop()
close()
mOrientationEventListener?.disable()
mRecordThreadHandler.removeMessages(mStartRecordMsg)
}
override fun onDestroy() {
super.onDestroy()
mRecordThreadHandler.looper.quit()
mPreviewSurface?.release()
mRecordSurface?.release()
mMediaRecorder?.release()
mMediaRecorder = null
}
/**
* 準(zhǔn)備錄制相關(guān)處理
*/
@RequiresPermission(Manifest.permission.CAMERA)
fun openCamera() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//使用context可能會(huì)出現(xiàn)內(nèi)存泄漏(紅米手機(jī)上),CameraManager會(huì)一直持有context
val cameraManager = applicationContext.getSystemService(CAMERA_SERVICE) as? CameraManager
val cameraIdList = cameraManager?.cameraIdList
if (cameraManager == null || cameraIdList == null || cameraIdList.isEmpty()) {
Toast.makeText(this, "無(wú)法使用設(shè)備相機(jī)", Toast.LENGTH_SHORT).show()
finish()
return
}
for (id in cameraIdList) {
var cameraCharacteristics: CameraCharacteristics? = null
try {
cameraCharacteristics = cameraManager.getCameraCharacteristics(id)
} catch (t: Throwable) {
t.printStackTrace()
}
if (cameraCharacteristics == null) {
continue
}
val fensFacing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
if (fensFacing != mFensFacing) {
continue
}
val level = cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)
val capabilities = cameraCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
mCameraOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
val map = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) ?: continue
// //獲取預(yù)覽支持的分辨率
// val outputSizes = map.getOutputSizes(SurfaceHolder::class.java)
// Log.i("TAG", "prepareRecord: outputSizes ${Arrays.toString(outputSizes)}")
//獲取錄制支持的分辨率
val recorderSizes = map.getOutputSizes(MediaRecorder::class.java)
// Log.i("TAG", "prepareRecord: recorderSizes ${Arrays.toString(recorderSizes)}")
val recordSize = getRecordSize(recorderSizes)
mRecordSize = recordSize
resizeSurfaceSize(recordSize.width, recordSize.height)
mSurfaceView.holder.setFixedSize(recordSize.width, recordSize.height)
try {
cameraManager.openCamera(id, mCameraDeviceStateCallback, mRecordThreadHandler)
} catch (t: Throwable) {
t.printStackTrace()
Toast.makeText(this, "相機(jī)打開(kāi)失敗,請(qǐng)關(guān)閉重試", Toast.LENGTH_SHORT).show()
}
break
}
} else {
//6.0以下
Toast.makeText(this, "Android系統(tǒng)版本太低不支持", Toast.LENGTH_SHORT).show()
finish()
}
}
private val mCameraDeviceStateCallback: CameraDevice.StateCallback by lazy(LazyThreadSafetyMode.NONE) {
object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
mCameraDevice = camera
val recordSize = mRecordSize ?: return
//預(yù)覽surface
val previewSurface = mSurfaceView.holder.surface
mPreviewSurface = previewSurface
setupMediaRecorder(recordSize.width, recordSize.height, false)
val recordSurface = mRecordSurface
mRecordSurface = recordSurface
val surfaceList = listOf(previewSurface, recordSurface)
camera.createCaptureSession(surfaceList, mCameraCaptureSessionStateCallback, mRecordThreadHandler)
}
override fun onDisconnected(camera: CameraDevice) {
//相機(jī)連接斷開(kāi)
if (mCameraDevice != null) {
close()
} else {
camera.close()
}
}
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
mUiHandler.sendEmptyMessage(mCameraOpenFailMsg)
}
}
}
private val mCameraCaptureSessionStateCallback: CameraCaptureSession.StateCallback by lazy {
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)
mCaptureRequest = captureRequest
captureRequest.addTarget(previewSurface)
captureRequest.addTarget(recordSurface)
//對(duì)焦
captureRequest.set(CaptureRequest.CONTROL_AF_MODE,CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
//自動(dòng)曝光
captureRequest.set(CaptureRequest.CONTROL_AE_MODE,CaptureRequest.CONTROL_AE_MODE_ON)
//進(jìn)行重復(fù)請(qǐng)求錄制預(yù)覽
session.setRepeatingRequest(captureRequest.build(), mCameraCaptureSessionCaptureCallback, mRecordThreadHandler)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
session.close()
mUiHandler.sendEmptyMessage(mCameraPreviewFailMsg)
}
}
}
private val mCameraCaptureSessionCaptureCallback: CameraCaptureSession.CaptureCallback by lazy(LazyThreadSafetyMode.NONE){
object :CameraCaptureSession.CaptureCallback(){
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
//這個(gè)方法在預(yù)覽過(guò)長(zhǎng)中,會(huì)一直被回調(diào)
// Log.i("TAG", "onCaptureCompleted thread ${Thread.currentThread().name}")
}
override fun onCaptureFailed(session: CameraCaptureSession, request: CaptureRequest, failure: CaptureFailure) {
super.onCaptureFailed(session, request, failure)
}
}
}
/**
* 錄制線程的Handler
*/
private val mRecordThreadHandler: Handler by lazy(LazyThreadSafetyMode.NONE) {
val recordThread = HandlerThread("RecordVideoThread")
recordThread.start()
object : Handler(recordThread.looper) {
override fun handleMessage(msg: Message) {
if (isFinishing || isDestroyed) return
when (msg.what) {
mStartRecordMsg -> {//開(kāi)始錄制
if (!mRecordingState) {//不是開(kāi)始錄制狀態(tài),return
return
}
val recordSize = mRecordSize ?: return
try {
//重新配置MediaRecorder,因?yàn)橛脩?hù)剛打開(kāi)頁(yè)面時(shí)的手機(jī)方向,和點(diǎn)擊錄制時(shí)的手機(jī)方向可能不一樣,所以重新配置。注意是為了支持橫屏錄制的視頻為橫屏視頻,不然都是豎屏視頻
setupMediaRecorder(recordSize.width, recordSize.height, true)
//視圖變?yōu)殇浿茽顟B(tài)
mMediaRecorder?.start()
} catch (t: Throwable) {
t.printStackTrace()
//錄制出現(xiàn)錯(cuò)誤
mUiHandler.sendEmptyMessage(mRecordErrorMsg)
}
}
}
}
}
}
/**
* ui線程Handler 處理錄制倒計(jì)時(shí),相機(jī)打開(kāi)失敗相關(guān)消息
*/
private val mUiHandler: Handler by lazy(LazyThreadSafetyMode.NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message?) {
if (isFinishing || isDestroyed) {
return
}
when(msg?.what){
mCountDownMsg -> {
mCurrentRecordDuration += 1
val progress = (mCurrentRecordDuration * 1f / mMaxRecordDuration * 100 + 0.5f).toInt()
mRecordProgressBar.progress = progress
if (mCurrentRecordDuration >= mMaxRecordDuration) {
//錄制時(shí)間到了,停止錄制
stopRecord()
mRecordComplete = true
//跳轉(zhuǎn)預(yù)覽頁(yè)面
openPlayActivity()
} else {
sendEmptyMessageDelayed(mCountDownMsg, 1000)
}
}
mCameraOpenFailMsg -> {
Toast.makeText(this@VideoRecord23Activity, "相機(jī)打開(kāi)失敗,請(qǐng)關(guān)閉重試", Toast.LENGTH_SHORT).show()
}
mCameraPreviewFailMsg -> {
Toast.makeText(this@VideoRecord23Activity, "相機(jī)預(yù)覽失敗,請(qǐng)關(guān)閉重試", Toast.LENGTH_SHORT).show()
}
}
}
}
}
/**
* 創(chuàng)建并配置 MediaRecorder
* @param width 視頻寬度
* @param height 視頻高度
* @param outputFileCreated 輸出文件是否已經(jīng)創(chuàng)建;第一次prepare時(shí),文件已經(jīng)創(chuàng)建了,開(kāi)始錄制時(shí),不用再次創(chuàng)建文件
*/
private fun setupMediaRecorder(width: Int, height: Int, outputFileCreated: Boolean): MediaRecorder {
var mediaRecorder = mMediaRecorder
if (mediaRecorder == null) {
mediaRecorder = MediaRecorder()
mMediaRecorder = mediaRecorder
} else {
mediaRecorder.reset()
}
var recordSurface = mRecordSurface
if (recordSurface == null) {
recordSurface = MediaCodec.createPersistentInputSurface()
mRecordSurface = recordSurface
}
mediaRecorder.setInputSurface(recordSurface)
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC)
//數(shù)據(jù)源來(lái)之surface
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
//設(shè)置QUALITY_HIGH,可以提高視頻的錄制質(zhì)量(文件也會(huì)變大),但是不能設(shè)置編碼格式和幀率等參數(shù),否則報(bào)錯(cuò)
val profile = CamcorderProfile.get(CamcorderProfile.QUALITY_480P)
mediaRecorder.setProfile(profile)
mediaRecorder.setMaxFileSize(0)
mediaRecorder.setVideoSize(width, height)
//視頻方向
mediaRecorder.setOrientationHint(mRecordVideoOrientation)
//錄制文件沒(méi)有創(chuàng)建,創(chuàng)建文件
if (!outputFileCreated) {
val parentFile = externalCacheDir ?: cacheDir
val fileName = "${System.currentTimeMillis()}.mp4"
mFileName = fileName
mFilePath = parentFile.absolutePath + File.separator + fileName
}
//不設(shè)置setOutputFile prepare時(shí)會(huì)報(bào)錯(cuò)
mediaRecorder.setOutputFile(mFilePath)
//prepare之后recordSurface才能用
mediaRecorder.prepare()
return mediaRecorder
}
/**
* 頁(yè)面關(guān)閉或不在前臺(tái)時(shí),停止錄制、釋放相機(jī)
*/
private fun close() {
if (mRecordingState) {
//停止錄制
stopRecord()
}
if (!mRecordComplete) {
//沒(méi)有錄制完成,或者沒(méi)有開(kāi)始錄制過(guò)(MediaRecorder prepare時(shí)會(huì)創(chuàng)建文件),刪除錄制的文件
deleteRecordFile()
}
//釋放相機(jī)
val previewSurface = mPreviewSurface
if (previewSurface != null) {
mCaptureRequest?.removeTarget(previewSurface)
}
val recordSurface = mRecordSurface
if (recordSurface != null) {
mCaptureRequest?.removeTarget(recordSurface)
}
mCameraCaptureSession?.close()
mCameraDevice?.close()
mCaptureRequest = null
mCameraCaptureSession = null
mCameraDevice = null
}
/**
* 刪除錄制的文件
*/
private fun deleteRecordFile() {
val filePath = mFilePath ?: return
try {
val file = File(filePath)
if (file.exists()) {
file.delete()
}
mFilePath = null
} catch (t: Throwable) {
t.printStackTrace()
}
}
/**
* 獲取錄制的視頻尺寸
* @param sizes 支持的尺寸列表
*/
private fun getRecordSize(sizes: Array<Size>): Size {
//參考尺寸 1280*720
val compareWidth = 1280
val compareHeight = 720
var resultSize = sizes[0]
var minDiffW = Int.MAX_VALUE
var minDiffH = Int.MAX_VALUE
for (size in sizes) {
if (size.width == compareWidth && size.height == compareHeight) {
resultSize = size
break
}
//找到最接近 1280*720的size
val diffW = abs(size.width - compareWidth)
val diffH = abs(size.height - compareHeight)
if (diffW < minDiffW && diffH < minDiffH) {
minDiffW = diffW
minDiffH = diffH
resultSize = size
}
}
return resultSize
}
/**
* 根據(jù)視頻寬高,修改surfaceView的寬高,來(lái)適應(yīng)預(yù)覽尺寸
*
* @param width 預(yù)覽寬度
* @param height 預(yù)覽高度
*/
private fun resizeSurfaceSize(height: Int, width: Int) {
val displayW: Int = mSurfaceView.width
val displayH: Int = mSurfaceView.height
if (displayW == 0 || displayH == 0) return
var ratioW = 1f
var ratioH = 1f
if (width != displayW) {
ratioW = width * 1f / displayW
}
if (height != displayH) {
ratioH = height * 1f / displayH
}
var finalH = displayH
var finalW = displayW
if (ratioW >= ratioH) {
finalH = (height / ratioW).toInt()
} else {
finalW = (width / ratioH).toInt()
}
val layoutParams = mSurfaceView.layoutParams
if (layoutParams.width == finalW && layoutParams.height == finalH) {
return
}
layoutParams.width = finalW
layoutParams.height = finalH
mSurfaceView.layoutParams = layoutParams
}
/**
* 監(jiān)聽(tīng)手機(jī)方向改變,計(jì)算錄制時(shí)的視頻方向。橫屏錄制時(shí),視頻橫屏。豎屏錄制時(shí),視頻豎屏
*/
private fun initOrientationEventListener() {
val orientationEventListener = object : OrientationEventListener(this) {
override fun onOrientationChanged(orientation: Int) {
if (orientation == ORIENTATION_UNKNOWN) return
val rotation = (orientation + 45) / 90 * 90
if (mFensFacing == CameraCharacteristics.LENS_FACING_BACK) {
//后置攝像頭
mRecordVideoOrientation = (mCameraOrientation + rotation) % 360
} else if (mFensFacing == CameraCharacteristics.LENS_FACING_FRONT) {
//前置攝像頭
mRecordVideoOrientation = mCameraOrientation - rotation
}
}
}
mOrientationEventListener = orientationEventListener
}
/**
* 跳轉(zhuǎn)錄制視頻預(yù)覽頁(yè)面
*/
private fun openPlayActivity() {
//val intent = Intent(this, VideoPlayActivity::class.java)
//intent.putExtra(VideoPlayActivity.FILE_PATH, mFilePath)
//startActivity(intent)
}
}
到了這里,關(guān)于基于Camera2和MediaRecorder實(shí)現(xiàn)視頻錄制的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!