当前位置: 首页 > news >正文

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

背景:

        按照需求,需要支持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);}}

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 解决TypeError: __init__() takes 1 positional argument but 2 were given
  • 使用Python Turtle绘制圣诞树和装饰
  • 使用 exe4j 转换 Java jar 程序为 Windows 平台可执行文件 (.exe)
  • WebSocket实现群聊功能、房间隔离
  • 字节抖音电商 后端开发岗位 一面
  • 图像边缘检测中Sobel算子的原理,并附OpenCV和Matlab的示例代码
  • 安全防御:智能选路
  • Study--Oracle-07-ASM自动存储管理(二)
  • vue路由的钩子函数
  • 【字幕】字幕特效入门
  • Android 使用WindowManager.LayoutParams窗口参数修改 Dialog 窗口的位置
  • Chapter 1:数据结构前言
  • 使用Python批量压缩图片
  • js获取和设置url参数
  • 7月17日学习打卡,数组
  • $translatePartialLoader加载失败及解决方式
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • Angular4 模板式表单用法以及验证
  • Facebook AccountKit 接入的坑点
  • Git同步原始仓库到Fork仓库中
  • Go 语言编译器的 //go: 详解
  • Java 23种设计模式 之单例模式 7种实现方式
  • JAVA并发编程--1.基础概念
  • Java的Interrupt与线程中断
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • Logstash 参考指南(目录)
  • OpenStack安装流程(juno版)- 添加网络服务(neutron)- controller节点
  • PAT A1050
  • 浏览器缓存机制分析
  • 腾讯优测优分享 | Android碎片化问题小结——关于闪光灯的那些事儿
  • 我看到的前端
  • 线性表及其算法(java实现)
  • 一、python与pycharm的安装
  • 怎样选择前端框架
  • media数据库操作,可以进行增删改查,实现回收站,隐私照片功能 SharedPreferences存储地址:
  • 测评:对于写作的人来说,Markdown是你最好的朋友 ...
  • ​Linux·i2c驱动架构​
  • ​ssh免密码登录设置及问题总结
  • # C++之functional库用法整理
  • # 达梦数据库知识点
  • #07【面试问题整理】嵌入式软件工程师
  • #NOIP 2014#Day.2 T3 解方程
  • (13)Hive调优——动态分区导致的小文件问题
  • (C语言)输入自定义个数的整数,打印出最大值和最小值
  • (DFS + 剪枝)【洛谷P1731】 [NOI1999] 生日蛋糕
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (二)学习JVM —— 垃圾回收机制
  • (附源码)计算机毕业设计SSM基于java的云顶博客系统
  • (三)Hyperledger Fabric 1.1安装部署-chaincode测试
  • (四)Linux Shell编程——输入输出重定向
  • (一)pytest自动化测试框架之生成测试报告(mac系统)
  • (转)PlayerPrefs在Windows下存到哪里去了?
  • (转)德国人的记事本
  • (轉貼) 2008 Altera 亞洲創新大賽 台灣學生成果傲視全球 [照片花絮] (SOC) (News)
  • .bashrc在哪里,alias妙用