Android 音视频开发 -- Android Mediaprojection 截屏和录屏
Posted 夏至的稻穗
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 音视频开发 -- Android Mediaprojection 截屏和录屏相关的知识,希望对你有一定的参考价值。
Android 音视频开发(一) – 使用AudioRecord 录制PCM(录音);AudioTrack播放音频
Android 音视频开发(二) – Camera1 实现预览、拍照功能
Android 音视频开发(三) – Camera2 实现预览、拍照功能
Android 音视频开发(四) – CameraX 实现预览、拍照功能
Android 音视频开发(五) – 使用 MediaExtractor 分离音视频,并使用 MediaMuxer合成新视频(音视频同步)
Android 音视频开发(六) – Android Mediaprojection 截屏和录屏
这章学习android录屏,效果如下:
截屏 | 录屏 |
---|---|
从这一章,我们将看到
- MediaProjection 的基本使用
- ImageReader 与 MediaProjection 实现截屏
- 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, //获取内容的 surface
null, //回调
null //回调执行的handler
二. 截屏
实际上,此时你的程序已经在获取屏幕的数据的,如果你的surface 是 SurfaceView,还会看到一帧一帧的数据。
截屏实际上就是获取当前屏幕的画面,这里可以使用 ImageReader ,Imageereader 类允许应用程序直接访问渲染到 Surface 中的图像数据,在使用 Camrea2 获取拍照数据也是使用了它。Android 音视频开发(三) – Camera2 实现预览、拍照功能
然而需要注意的是,Camera 获取的是 YUV 数据,而MediaProjection 获取的则是 RGBA 的数据,所以它的初始化为:
private fun configImageReader()
val dm = resources.displayMetrics
imageReader = ImageReader.newInstance(dm.widthPixels, dm.heightPixels,
PixelFormat.RGBA_8888, 1).apply
//监听图片生成
setOnImageAvailableListener(
savePicTask(it)
, null)
//把内容投射到ImageReader 的surface
mediaProjection?.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.width
val height = image.height
//拿到所有的 Plane 数组
val planes = image.planes
val 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? = null
try
//获取捕获的照片数据
image = reader.acquireLatestImage()
val width = image.width
val height = image.height
//拿到所有的 Plane 数组
val planes = image.planes
val 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
//记得关闭 image
try
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, //获取内容的 surface
null, //回调
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 截屏和录屏的主要内容,如果未能解决你的问题,请参考以下文章
字节大佬写给Android中高级开发的《Android 音视频开发进阶指南》,限时开源分享!!!