Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
Android 音视频开发(六) – Android Mediaprojection 截屏和录屏

音视频工程

这章学习Android录屏,效果如下:

截屏 录屏

从这一章,我们将看到

  1. MediaProjection 的基本使用
  2. ImageReader 与 MediaProjection 实现截屏
  3. MediaRecorder 与 MediaProjection 实现录屏

一. MediaProjection 的基本使用

MediaProjection 的使用非常简单,调用的是MediaProjectionManager对象,它会创建申请录屏的 Intent:

mediaManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mediaManager.createScreenCaptureIntent().apply {startActivityForResult(this, 2)
}

此时会弹出一个申请录屏的弹窗,点击确定就开始录屏了,如果点击确定,就可以在 onActivityResult 中拿到 MediaProjection 对象

 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {super.onActivityResult(requestCode, resultCode, data)if (requestCode == 2 && resultCode == Activity.RESULT_OK) {data?.let {//获取到操作对象mediaProjection = mediaManager.getMediaProjection(resultCode, it)}}}

1.1. 获取数据

拿到 MediaProjection 对象,就可以去拿数据了, 它会通过创建 VirtualDisplay 去获取屏幕的内容,VirtualDisplay 是一个虚拟显示,它会根据应用提供的 Surface ,把内容渲染到 Surface上,这里的 Surface 可以是 ImageReader 的,也可以是MediaRecorder 或 MediaCodec的。
它的调用为:

virtualDisplay = mediaProjection?.createVirtualDisplay(TAG,  //virtualDisplay 的名字,随意写 dm.widthPixels, //virtualDisplay 的宽dm.heightPixels, //virtualDisplay 的高dm.densityDpi, // virtualDisplay 的 dpi 值,这里都跟应用保持一致即可 // 显示的标志位,不同的标志位,截取不同的内容,具体看源码解释DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,surface, //获取内容的 surfacenull, //回调null  //回调执行的handler

二. 截屏

实际上,此时你的程序已经在获取屏幕的数据的,如果你的surface 是 SurfaceView,还会看到一帧一帧的数据。
截屏实际上就是获取当前屏幕的画面,这里可以使用 ImageReader ,Imageereader 类允许应用程序直接访问渲染到 Surface 中的图像数据,在使用 Camrea2 获取拍照数据也是使用了它。Android 音视频开发(三) – Camera2 实现预览、拍照功能
然而需要注意的是,Camera 获取的是 YUV 数据,而MediaProjection 获取的则是 RGBA 的数据,所以它的初始化为:

    private fun configImageReader() {val dm = resources.displayMetricsimageReader = ImageReader.newInstance(dm.widthPixels, dm.heightPixels,PixelFormat.RGBA_8888, 1).apply {//监听图片生成setOnImageAvailableListener({savePicTask(it)}, null)//把内容投射到ImageReader 的surfacemediaProjection?.createVirtualDisplay(TAG, dm.widthPixels, dm.heightPixels, dm.densityDpi,DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null)}}

需要注意的是初始化 ImageReader 的第三个参数,改为 PixelFormat.RGBA_8888,获取线性的像素RGBA,后面生成图片需要,大小则设置为1,这里后面解释。
然后把 ImageReader 的 surface 给VirtualDisplay就可以了,此时 ImageReader 就能拿到 VirtualDisplay 的内容了,而我们设置了 setOnImageAvailableListener 图片监听,但有数据时,就会回调,我们就可以保存图片了。

2.1 保存图片

首先调用 ImageReader 的acquireLatestImage() ,从 ImageReader 的队列中获取最新的 Image,删除旧图像。如果没有新图像可用,返回 null。所以从原理看,ImageReader 初始化最大个数设置为2,则能避免 null 的情况,但是如果都能获取到新图片,则会显示两张,这里为了美观,就设置成1,大家可以试试。

拿到 Image 后,就可以通过 getPlanes 拿到buffer了,只要不是 yuv,只需要拿 planes[0] 的数据即可:

 //获取捕获的照片数据image = reader.acquireLatestImage()val width = image.widthval height = image.height//拿到所有的 Plane 数组val planes = image.planesval plane = planes[0]val buffer: ByteBuffer = plane.buffer

如果转换成图片?
从上面的 ByteBuffer 可知,其实可以使用 bitmap.copyPixelsFromBuffer(buffer) 直接转换,但这里存在一个问题,因为线性内存对称问题,有些手机是不对称的,导致宽高比不一致,就会出现花屏碎屏的问题。所以需要做一个转换:

val buffer: ByteBuffer = plane.buffer//相邻像素样本之间的距离,因为RGBA,所以间距是4个字节val pixelStride = plane.pixelStride//每行的宽度val rowStride = plane.rowStride//因为内存对齐问题,每个buffer 宽度不同,所以通过pixelStride * width 得到大概的宽度,//然后通过 rowStride 去减,得到大概的内存偏移量,不过一般都是对齐的。val rowPadding = rowStride - pixelStride * width// 创建具体的bitmap大小,由于rowPadding是RGBA 4个通道的,所以也要除以pixelStride,得到实际的宽val bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride,height, Bitmap.Config.ARGB_8888)bitmap.copyPixelsFromBuffer(buffer)

注释已经说清楚了,就不多赘述,详细代码如下:

    /*** 保存图片*/private fun savePicTask(reader: ImageReader) {scopeIo {var image: Image? = nulltry {//获取捕获的照片数据image = reader.acquireLatestImage()val width = image.widthval height = image.height//拿到所有的 Plane 数组val planes = image.planesval plane = planes[0]val buffer: ByteBuffer = plane.buffer//相邻像素样本之间的距离,因为RGBA,所以间距是4个字节val pixelStride = plane.pixelStride//每行的宽度val rowStride = plane.rowStride//因为内存对齐问题,每个buffer 宽度不同,所以通过pixelStride * width 得到大概的宽度,//然后通过 rowStride 去减,得到大概的内存偏移量,不过一般都是对齐的。val rowPadding = rowStride - pixelStride * width// 创建具体的bitmap大小,由于rowPadding是RGBA 4个通道的,所以也要除以pixelStride,得到实际的宽val bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride,height, Bitmap.Config.ARGB_8888)bitmap.copyPixelsFromBuffer(buffer)withMain {val canvas = surfaceview.holder.lockCanvas()with(canvas) {drawBitmap(bitmap, 0f, 0f, null)surfaceview.holder.unlockCanvasAndPost(this)}Toast.makeText(this@MediaProjectionActivity, "保存成功", Toast.LENGTH_SHORT).show()mediaProjection?.stop()}} catch (e: java.lang.Exception) {Log.d(TAG, "zsr doInBackground: $e")} finally {//记得关闭 imagetry {image?.close()} catch (e: Exception) {}}}}

三. 录屏

MediaProjection 能获取屏幕的数据,这个就有很多操作空间,如常用的录屏到文件再播放,游戏录屏功能,也可以把数据用 MediaCodec 编码之后发送给其他接收端,如Maxhub、录播,eshare 这些投屏软件。
这里使用的 MediaRecorder 去保存录屏数据到文件,再播放。

3.1 初始化 MediaRecorder

上面说到,MediaProjection 的 VirtualDisplay 会把数据放到 surface 上,所以使用 MediaRecorder 的 surface 就能拿到数据了,再把它放到文件即可。

val dm = resources.displayMetrics
recorder = MediaRecorder()recorder?.apply {//setAudioSource(MediaRecorder.AudioSource.MIC) //音频载体setVideoSource(MediaRecorder.VideoSource.SURFACE) //视频载体setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) //输出格式setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) //音频格式setVideoEncoder(MediaRecorder.VideoEncoder.H264) //视频格式setVideoSize(dm.widthPixels, dm.heightPixels) //视频大小//帧率,30是比较舒服的帧率setVideoFrameRate(30)//比特率,不需要太高的比特率,3m就很清晰了setVideoEncodingBitRate(3 * 1024 * 1024) //设置文件位置setOutputFile(file.absolutePath)}

MediaRecoder 的配置也比较简单,只要设置视频格式,编码格式,和帧率这些常规的操作即可。其实 MediaRecorder 也可以录制音频,可以录制环绕音等,也就是传屏软件的音视频同步功能,但你得自己计算 pts ,算出偏差值,不然就会出现视频音频对不上。虽然 Android 10 后也提供了接口,但是也得第三方应用支持才行。好吧,跑题了,这里只需要视频数据即可。

接着调用 prepare() 准备,然后把 surface 给 MediaProjection 即可:

try {prepare()virtualDisplay = mediaProjection?.createVirtualDisplay(TAG,  //virtualDisplay 的名字,随意写dm.widthPixels, //virtualDisplay 的宽dm.heightPixels, //virtualDisplay 的高dm.densityDpi, // virtualDisplay 的 dpi 值,这里都跟应用保持一致即可// 显示的标志位,不同的标志位,截取不同的内容,具体看源码解释DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,surface, //获取内容的 surfacenull, //回调null  //回调执行的handler)} catch (e: Exception) {Log.e(TAG, "MediaRecord prepare fail : $e")e.printStackTrace()return false}recorder?.start()

这里就可以保存数据到文件了,但你想暂停时,可以使用

 recorder?.stop()
mediaProjection?.stop()

然后再使用 MediaPlayer 或者其他播放视频的软件播放即可。

参考:
https://developer.android.google.cn/reference/android/media/ImageReader?hl=en
https://developer.android.google.cn/reference/android/media/Image.Plane?hl=en

Android 音视频开发(六) -- Android Mediaprojection 截屏和录屏相关推荐

  1. Android 音视频开发学习思路

    Android 音视频开发这块目前的确没有比较系统的教程或者书籍,网上的博客文章也都是比较零散的.只能通过一点点的学习和积累把这块的知识串联积累起来. 初级入门篇: Android 音视频开发(一) ...

  2. Android音视频开发基础(六):学习MediaCodec API,完成视频H.264的解码

    前言 在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了<Android 音视频从入门到提高 - 任务列表>.本文是Android音视 ...

  3. Android音视频开发,详说PCM音频重采样、PCM编码

    直播伴音,两种数据能否合在一起?不能叠加在一起 会有噪音 合并以后 再去编码推流 直播的例子 客户端播放器,可以开启多个播放器 对于我们重采样 很多时候就是为了统一格式,就是为了要合并这个流,去推送, ...

  4. android音频开发6,Android 音视频开发(一) : 通过三种方式绘制图片

    想要逐步入门音视频开发,就需要一步步的去学习整理,并积累.本文是音视频开发积累的第一篇. 对应的要学习的内容是:在 Android 平台绘制一张图片,使用至少 3 种不同的 API,ImageView ...

  5. Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)

    关于 AudioRecord Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风 ...

  6. 直播平台源码搭建教程之Android音视频开发

    直播平台源码搭建教程之Android音视频开发 音频 将声音保存成音频的过程,其实就是将模拟音频数字化的过程,为了实现这个过程,就需要对模拟音频进行采样.量化和编码.接下来我们详细讲解这一过程. 采样 ...

  7. Android 音视频开发之基础篇 使用 SurfaceView绘制一张图片

    Android 音视频开发 上一篇文章:使用 imageview绘制一张图片 任务一 SurfaceView绘制一张图片 文章目录 Android 音视频开发 前言 一.surfaceview是什么? ...

  8. Android 音视频开发之基础篇 使用 imageview绘制一张图片

    Android 音视频开发 任务一 ImageView 绘制图片 文章目录 Android 音视频开发 任务一 ImageView 绘制图片 前言 一.配置activity_main.xml 二.添加 ...

  9. Android音视频开发基础(七):视频采集-系统API基础

    前言 在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了<Android 音视频从入门到提高 - 任务列表>.本文是Android音视 ...

最新文章

  1. Python初学者零碎基础笔记(一)
  2. GridView总结二:GridView自带编辑删除更新
  3. easypoi导出word表格_java如何导出word和wps文档
  4. [转] 使用模板自定义 WPF 控件
  5. Android USB转串口编程
  6. 单例 (Singleton)设计模式
  7. 途观l怎么使用_官宣!中型SUV质量最新排名出炉:汉兰达失前三,大众途观L上榜!...
  8. 如何显示内存中的 HTML 网页
  9. Python少打字小技巧
  10. 第二讲 JavaScript基本数据结构
  11. 小学计算机基础知识总结,小学信息技术课的基本知识点
  12. 常见电子元器件的极性识别方法
  13. VSS无法访问 (0x80072EFD) 转载
  14. ipad+pdfexpert+webdav 双向同步文件
  15. Android蓝牙自动配对Demo,亲测好使!!!
  16. godaddy 服务器位置,GoDaddy DNS服务器地址 | Godaddy美国主机中文指南
  17. sigset 与 signal的区别?
  18. ibm量子计算机蓝图,IBM 量子技术扩展蓝图
  19. SVN检出报错,SVN更新时忽略指定文件或文件夹
  20. 大文件上传组件webupload插件

热门文章

  1. centos8的yum 源方案解决
  2. 华为mate升级鸿蒙系统,华为mate40升级鸿蒙系统步骤_华为mate40升级鸿蒙系统教程...
  3. ubuntu下把微信的amr音频格式转换为 mp3格式
  4. 好故事抵得上1000张照片:为什么你要成为一个更棒的故事讲述者?
  5. 白盒测试:语句/条件/判定/判定条件/条件组合/路径覆盖
  6. Lua脚本基础,简单案例
  7. 操作系统实战之操作系统不是一天造成的
  8. 一问一答知晓三方协议
  9. Tx 360 我无语了
  10. html5页面微信分享,微信 js-sdk 完成 H5自定义页面分享