记录一次Android推流、录像踩坑过程

avatar
作者
猴君
阅读量:2

背景:

        按照需求,需要支持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 = cameraId          this.previewFormat = imageFormat         surfaceHolder = holder                  mSurfaceTexture = 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 = 30000             val maxFps: Int = 30000             setCameraPreviewFpsRange(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 = 640                 previewHeight = 480                  GBApp.instance!!.previewWidth = 640                 GBApp.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);         }     }

如果合成的视频在后续还会卡在某一帧,可以把之前的视频数据队列清空,这样避免因为切换相机之前的垃圾数据导致问题,然后执行上面的步骤

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!