背景:
? ? ? ? 按照需求,需要支持APP在手機息屏時進行推流、錄像。
技術要點:
????????1、手機在息屏時能夠打開camera獲取預覽數據
????????2、獲取預覽數據時進行編碼以及合成視頻
一、息屏時獲取camera預覽數據:
? ? ? ? ①Camera.setPreviewDisplay(SurfaceHolder holder):
一般常規的打開camera后(Camera.open(int cameraId)),給相機設置預覽setPreviewDisplay(SurfaceHolder holder),holder通過surfaceview獲取。但是者在surfaceDestroyed(xxxxxx)后無法獲取預覽數據,所以setPreviewDisplay(SurfaceHolder holder)此方法無法滿足息屏的需求。
????????②Camera.setPreviewTexture(SurfaceTexture surfaceTexture):
此方法通過創建一個new?SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)傳入就可以實現息屏獲取相機的預覽數據。這樣就可以避免直接使用TextureView帶來的onSurfaceTextureDestroyed(xxxx)導致息屏后無法獲取預覽數據。
二、預覽camera預覽數據:
? ? ? ? ①Camera.setPreviewTexture(SurfaceTexture surfaceTexture):
獲取到yuv數據進行轉換成bitmap,然后用Imageview或者Surfaceview直接顯示。
此方法帶來的弊端:
? ? ? ? 1、每一幀數據都要生成bitmap,短時間頻繁的創建對象會導致STW,從而導致ANR
? ? ? ? 2、預覽數據不流暢,是用Imageview或者Surfaceview手動方式展示的
? ? ? ? ②Camera.setPreviewDisplay(SurfaceHolder holder):
此方法是Android自帶的,沒有上述的弊端:ANR、畫面卡頓,但是在息屏時無法獲取預覽數據
? ? ? ? ③Camera.setPreviewTexture(SurfaceTexture ????????????????surfaceTexture)+Camera.setPreviewDisplay(SurfaceHolder holder):
此方法既解決了預覽問題也解決了息屏獲取預覽數據問題,但是此方法在MediaMuxer兩種模式轉換合成音視頻時無法合成連續的音視頻,只能亮屏時合成一段,息屏時合成一段。不過也嘗試在轉換模式時,MediaMuxer繼續寫入數據,雖然視頻可以播放但是會導致寫入失敗,視頻畫面卡頓在轉換的那一幀畫面。因為在轉換模式時,編碼的數據出問題了,大小比之前的要小很多,此問題待研究。
三、解決方案:
采用上述的第三種方法:
????????Camera.setPreviewTexture(SurfaceTexture ????????????????surfaceTexture)+Camera.setPreviewDisplay(SurfaceHolder holder);
息屏、切換前后置攝像頭時先釋放相機releaseCamera(),代碼如下:
override fun releaseCamera() {try {stopBackgroundThread()mCamera?.stopPreview()mCamera?.setPreviewCallbackWithBuffer(null)mCamera?.release()mCamera = null} catch (runError: RuntimeException) {KLog.e(TAG, "releaseCamera happened error: " + runError.message)} catch (e: Exception) {KLog.e(TAG, "releaseCamera error: $e")}}
然后再重新打開相機openCamera,代碼如下:
override fun openCamera(cameraId: Int,imageFormat: Int,holder: SurfaceHolder?) {mCameraId = cameraIdthis.previewFormat = imageFormatsurfaceHolder = holdermSurfaceTexture = SurfaceTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES)openCamera(surfaceHolder, mSurfaceTexture!!, cameraId)}private fun openCamera(surfaceHolder: SurfaceHolder?,surfaceTexture: SurfaceTexture,cameraId: Int) {if (cameraId < 0 /*|| cameraId > Camera.getNumberOfCameras() - 1*/) {Log.w(TAG,"openCamera failed, cameraId=" + cameraId + ", Camera.getNumberOfCameras()=" + Camera.getNumberOfCameras())return}startBackgroundThread()try {
// Log.i(TAG,"surfaceCreated open camera cameraId=$cameraId start")mCamera = Camera.open(cameraId)mCamera?.setDisplayOrientation(90)if (surfaceHolder == null) {mCamera?.setPreviewTexture(surfaceTexture)} else {mCamera?.setPreviewDisplay(surfaceHolder)}// set preview format @{this.previewFormat = setCameraPreviewFormat(mCamera!!, this.previewFormat)// @}// 設置fps@{val minFps: Int = 30000val maxFps: Int = 30000setCameraPreviewFpsRange(mCamera!!, minFps, maxFps)// @}// 設置預覽尺寸 @{val hasSetPreviewSize = setCameraPreviewSize(mCamera!!)if (hasSetPreviewSize.size > 1) {/* previewWidth = hasSetPreviewSize[0]previewHeight = hasSetPreviewSize[1]GBApp.getInstance().previewWidth = hasSetPreviewSize[0]GBApp.getInstance().previewHeight = hasSetPreviewSize[1]*/previewWidth = 640previewHeight = 480GBApp.instance!!.previewWidth = 640GBApp.instance!!.previewHeight = 480}// @}// 設置照片尺寸 @{setCameraPictureSize(mCamera!!)// @}// 設置預覽回調函數@{mCamera?.setPreviewCallbackWithBuffer(mCameraCallbacks)Log.i(TAG,"ImageFormat: $previewFormat bits per pixel=" + ImageFormat.getBitsPerPixel(previewFormat))// 初始化數組for (index in 0 until previewDataSize) {val previewData = if (previewFormat != ImageFormat.YV12) {ByteArray(previewWidth * previewHeight * ImageFormat.getBitsPerPixel(previewFormat) / 8)} else {val size = ImageUtils.getYV12ImagePixelSize(previewWidth, previewHeight)ByteArray(size)}previewDataArray.add(previewData)}//addAllPreviewCallbackData()mCamera?.addCallbackBuffer(ByteArray(previewWidth * previewHeight * 3 / 2))// @}//autoRatioTextureView()mCamera?.startPreview()} catch (localIOException: IOException) {Log.e(TAG,"surfaceCreated open camera localIOException cameraId=" + cameraId + ", error=" + localIOException.message,localIOException)} catch (run: RuntimeException) {Log.e(TAG,"open camera RuntimeException error=" + run.message)} catch (e: Exception) {Log.e(TAG,"surfaceCreated open camera cameraId=" + cameraId + ", error=" + e.message,e)}}
此情況依舊會導致在切換相機時,出現錄制的視頻卡在某一幀,解決方案如下:
依舊使用SurfaceView預覽相機
1、相機停止寫入數據pauseRecord()
// 根據 status 狀態是否寫入數據
public void pauseRecord() {if (status == Status.RECORDING) {pauseMoment = System.nanoTime() / 1000;status = Status.PAUSED;if (listener != null) listener.onStatusChange(status);}}
2、釋放相機
fun releaseCamera() {try {stopBackgroundThread()mCamera?.stopPreview()mCamera?.setPreviewCallbackWithBuffer(null)mCamera?.release()mCamera = null} catch (runError: RuntimeException) {KLog.e(TAG, "releaseCamera happened error: " + runError.message)} catch (e: Exception) {KLog.e(TAG, "releaseCamera error: $e")}}
3、繼續錄制視頻
fun doResumeRecord(eventData: ResumeRecordEvent) {// 打開相機GBApp.instance?.service?.doOpenCamera(OpenCameraEvent(eventData.holder,VideoTaskUtil.instance.mCameraId,ImageFormat.NV21,eventData.eventType))// 請求關鍵幀camera2Base?.videoEncoder?.requestKeyframe()// 繼續寫入音視頻數據camera2Base?.resumeRecord()}public void resumeRecord() {if (status == Status.PAUSED) {pauseTime += System.nanoTime() / 1000 - pauseMoment;status = Status.RESUMED;if (listener != null) listener.onStatusChange(status);}}
如果合成的視頻在后續還會卡在某一幀,可以把之前的視頻數據隊列清空,這樣避免因為切換相機之前的垃圾數據導致問題,然后執行上面的步驟