本文是Mob开发者平台技术副总监余勋杰基于MediaProjection实现Android全系统录屏功能的原理解析,包括了结合MediaRecorder和MediaCodec两套方案。

文 / 余勋杰

前言

自安卓4.4开始,系统提供了内置的录屏功能,用户可以在adb下执行screenrecord命令,以指定码率、帧率、分辨率和时长来录制屏幕。但这个方案有缺点,普通用户无法直接执行adb命令,只能要么求助于adb终端,比如pc端的android-sdk,又或者在安卓设备上获取root权限,再执行录屏命令。幸而从5.1开始,系统又提供了MediaProjection API,通过再组合MediaRecorder或者MediaCodec API,开发者可以十分轻松地实现一个免root的全系统录屏工具,而ShareREC的全系统录屏功能,正是基于这种组合。

基于MediaProjection来实现录屏有两种方案,如果结合MediaRecorder,则前者为输入,后者为输出,原理清晰,实现简单,代码也很少。但如果结合的是MediaCodec,则由于后者仅仅只是一个编码器,我们要仔细考虑采用什么样子的数据作为编码输入,编码后要将数据输出到什么工具上压制为视频文件等等,原理复杂,实现困难,代码也很多。但相比较而言,第二个方案自由度很高,站在ShareREC的立场,我们除了全系统录屏,还有别的应用内录屏工具,这些工具已经实现了基于MediaCodec的方案;加之我们还要考虑输出的媒体流可能不是存为文件,而是作为流媒体传输,MediaRecorder是很难满足要求的。故而ShareREC使用的是第二套方案。

但本文会将这两套方案都介绍一遍,因此让我们由浅及深一步步来吧。

方案一:使用MediaRecorder作为媒体输出

让我们先来看一下MediaProjection API是个什么东西。顾名思义,它是一套“屏幕镜像”工具,核心类包括:MediaProjectionManager、MediaProjection和VirtualDisplay。

其中MediaProjectionManager用于向用户显示一个弹窗,请求获取屏幕镜像的权限(如下图)。此弹窗的操作结果会通过Activity的onActivityResult返回,RESULT_OK表示用户已经给了权限。

private MediaProjectionManager mpm;

private void showDialog() {

mpm = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);

Intent captureIntent = mpm.createScreenCaptureIntent();

startActivityForResult(captureIntent, REQUEST_CODE);

}

public void onActivityResult(int requestCode, int resultCode, Intent data) {

if (requestCode == REQUEST_CODE) {

// 从此处开始抓屏操作

CreateMediaRecorder();

createVirtualDisplay(data);

}

}

得到权限后,可以调用MediaProjectionManager的getMediaProjection方法获取MediaProjection实例,并用此实例创建一个VirtualDisplay,这个就是我们的屏幕镜像。

创建VirtualDisplay时需要一个surface做出输出缓存,即存放即将显示在屏幕上的数据。另一方面,自安卓5.1以后,系统为MediaRecorder提供多了一种新的图形输入方式,我们可以通过其实例方法getSurface得到一个surface作为输入缓存。如此结合起来,在录屏的场景中,我们可以先从MediaRecorder中得到一个输入缓存,并将这个缓存当做VirtualDisplay的输出缓存,形成I/O流通、内存共享。

private MediaRecorder mr;

private MediaProjection mp;

private VirtualDisplay vd;

private Callback cb;

private void CreateMediaRecorder() {

try {

mr = new MediaRecorder();

mr.setAudioSource(MediaRecorder.AudioSource.MIC);

mr.setVideoSource(MediaRecorder.VideoSource.SURFACE);

mr.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);

mr.setVideoEncoder(MediaRecorder.VideoEncoder.H264);

mr.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

mr.setVideoEncodingBitRate(bitRate);

mr.setVideoFrameRate(30);

mr.setVideoSize(1280, 720);

mr.setOutputFile(“/sdcard/test.mp4”);

mr.prepare();

} catch (Throwable t) {

t.printStackTrace();

}

}

private void createVirtualDisplay(Intent data) {

MediaProjection mp = mpm.getMediaProjection(RESULT_OK, data);

cb = new Callback() {

public void onStop() {

if (mr != null) {

mr.stop();

mr.release();

mr = null;

}

if (vd != null) {

vd.release();

vd = null;

}

}

};

mp.registerCallback(cb, null);

int densityDpi = (int) (getResources().getDisplayMetrics().densityDpi + 0.5f);

vd = mp.createVirtualDisplay("ShareREC",

1280, 720, densityDpi,

DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,

mr.getSurface(), null, null);

mr.start();

}

经过上面的代码,程序已经进入录屏模式。MediaRecorder将以h264/aac为编码格式,将录制的结果以mp4格式存储在sd卡的test.mp4中。

当录制完毕时,需要关闭MediaRecorder,并释放VirtualDisplay和MediaProjection,上面代码中的MediaProjection.Callback实例正是为了这个而定义的。下面的代码演示了如何停止录制操作:

private void stop() {

if (mp != null) {

mp.stop();

if (cb != null) {

mp.unregisterCallback(cb);

}

mp = null;

}

}

方案二:自行实现媒体编码和输出

看完简单的方案,现在来看一下复杂的方案。ShareREC在这个方案上的实现流程如下图:

ShareREC将全系统录屏功能拆分为抓图、编码和输出3部分。在用户授权抓屏之后,抓图模块率先启动,创建虚拟屏幕、创建图形缓存、创建回调等等。这里面的图形缓存是自安卓4.4以后提供的ImageReader。和MediaRecorder一样,它也提供了getSurface方法,返回用于更新缓存的surface实例。并且在缓存发生变更时,通过acquireLatestImage方法来获取最新的图片数据。不过由于我们并不知道什么时候缓存会发生变更,因此需要再调用setOnImageAvailableListener方法设置一个OnImageAvailableListener实例,并通过它的onImageAvailable方法实时得到缓存更新的通知:

private MediaProjectionManager mpm;

private ImageReader ir;

private MediaProjection mp;

private VirtualDisplay vd;

/**

* @param screenSize 屏幕的实际分辨率

* @param videoSize 抓取图片的分辨率

*/

public void startCapturer(final int[] screenSize, final int[] videoSize, final Intent data) {

try {

float densityDpi = getResources().getDisplayMetrics().densityDpi;

int densityDpi = (int) (densityDpi * screenSize[0] / videoSize[0] + 0.5f);

ir = ImageReader.newInstance(videoSize[0], videoSize[1], PixelFormat.RGBA_8888, 4);

ir.setOnImageAvailableListener(this, null);

mp = mpm.getMediaProjection(Activity.RESULT_OK, data);

vd = mp.createVirtualDisplay("ShareREC",

videoSize[0], videoSize[1], (int) densityDpi,

DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,

ir.getSurface(), null, null);

} catch (Throwable t) {

t.printStackTrace();

}

}

public void onImageAvailable(ImageReader reader) {

Image image = reader.acquireLatestImage();

if (image != null) {

Image.Plane[] planes = image.getPlanes();

if (planes != null && planes.length > 0) {

int rowStride = planes[0].getRowStride();

ByteBuffer rgba = planes[0].getBuffer();

if (rgba != null) {

// 将rgba数据输送给编码器

offerFrame(rgba, rowStride);

}

}

image.close();

}

}

上面的代码演示了如何通过组合VirtualDisplay和ImageReader来实现连续抓图。需要注意的一点是,根据surface内部的实现原理(超越本文的范畴),我们得到的rgba数据,多数时候不仅包含屏幕上的像素数据,还在图片的右侧包含一条黑边,因此我们在将像素数据发送给编码器之前,还需要告知编码器,每一行有效像素的个数(本例子中用了字节数)。

然后说一下编码器MediaCodec。这东西从安卓4.1开始就有,一般是用来实现音视频编解码的。在它之前,市面上早已经有ffmpeg之类的工具,但MediaCodec的优势在于它还能调起硬件编解码模块,性能更高、效果更好。但它的早期版本功能很弱,只能支持像素数据作为输入源,并且多数是YUV格式数据,故而输入前还需要做一次RGB转YUV的操作。自安卓4.3开始,它支持surface作为输入源,因此这里面临一个看似理所应当的问题:既然我们的全系统抓屏是基于安卓5.1的,而从安卓4.3开始,MediaCodec就支持以surface作为输入,那为什么不直接组合VirtualDisplay和MediaCodec就好,要中间插入一个ImageReader?这个问题怎么说呢,这是由于ShareREC不仅支持全系统录屏,还支持其它的应用内的录屏方式,如基于Cocos2d-x,Unity3D、libGDX等等引擎来做的录屏功能。而这些应用内的录屏方式,其抓取模块只能抓取到像素数据,考虑到编码模块在ShareREC内是一个通用的模块,故而全系统录屏也将抓图输出处理为像素数据输出。

private BufferInfo bufferInfo;

private MediaCodec encoder;

public void startEncoder() throws Throwable {

// 获取硬件编码器支持的颜色格式,一般是I420或者NV12

int pixelFormat = getHWColorFormat();

MediaFormat format = MediaFormat.createVideoFormat(MIME, 1280, 720);

format.setInteger(MediaFormat.KEY_BIT_RATE, 1 * 1024 * 1024);

format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);

format.setInteger(MediaFormat.KEY_COLOR_FORMAT, pixelFormat);

format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);

format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 0);

encoder = MediaCodec.createEncoderByType("video/avc");

encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

encoder.start();

bufferInfo = new BufferInfo();

}

上面的代码演示了如何初始化一个MediaCodec实例。需要注意的一点是,虽然我们设置了MediaCodec的帧率,但由于抓图时,图片数据不是匀速输入的,因此这个字段在此处形同虚设,可是又不能不填。上面的例子并不演示如何获取硬件编码器支持的颜色格式类型,具体的实现方式可以搜索一下,不难找。

然后我们来实现上面抓图模块中遗留的offerFrame方法:

public void offerFrame(ByteBuffer frame, int rowStride) throws Throwable {

long framePreTimeUs = System.nanoTime() / 1000;

ByteBuffer[] inputBuffers = encoder.getInputBuffers();

int inputBufferIndex = encoder.dequeueInputBuffer(-1);

if (inputBufferIndex >= 0) {

ByteBuffer ibb = inputBuffers[inputBufferIndex];

ibb.position(0);

YUVConverter.rgbaToI420(frame, ibb, 1280, 720, rowStride);

encoder.queueInputBuffer(inputBufferIndex, 0, ibb.limit(), framePreTimeUs, 0);

}

ByteBuffer[] outputBuffers = encoder.getOutputBuffers();

int outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);

while (outputBufferIndex >= 0) {

ByteBuffer obb = outputBuffers[outputBufferIndex];

if (obb != null) {

int frameType = 0;

if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_SYNC_FRAME) == 1) {

frameType = 1;

} else if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 2) {

frameType = 2;

}

// 将编码好的H264帧输出给mp4合并模块

offerVideoTrack(obb, bufferInfo.size, bufferInfo.presentationTimeUs, frameType);

}

encoder.releaseOutputBuffer(outputBufferIndex, false);

outputBufferIndex = encoder.dequeueOutputBuffer(bufferInfo, 0);

}

}

MediaCodec的输入输出都有缓存队列,我们要给它输入数据,需要先获取其输入缓存队列,然后在空闲的位置复制像素数据。由于我们抓取到的数据是RGBA格式,必须转为YUV格式才能别正确编码,这里ShareREC使用了libYUV,将RGBA转为I420。此外,并不是一输入图片就立刻会有输出h264帧,MediaCodec一般会缓存3-7张图片。

最后是视频合并模块,ShareREC使用了mp4v2来实现。其实在安卓平台同样自4.3以后系统自带了视频合并工具MediaMuxer。但这个东西似乎必须与MediaCodec一同使用,由于的用户要求ShareREC至少支持4.0以上的系统,故除了MediaCodec,其实我们还具备优化过的软件编码器。为了同时兼容两种编码器,我们放弃了MediaMuxer而采用兼容性更好的mp4v2。

本文不介绍mp4v2的使用,因为这超过java代码的范畴(libYUV也是)。但它的工作原理很简单,无非就是打开文件;在内存中保存视频轨道和音频轨道的信息;接着一帧帧写入视频或者音频数据,不用在意写入顺序,可以混在一起;在完成合并时,将内存里面的音视频信息组合为mp4描述信息,追加到文件尾部,之后关闭文件。这个流程网上的文档很多,随便搜索就有了。但使用时有一些可能需要注意的,包括多线程同步和图片呈现时间的问题。

关于多线程同步,是指因为我们在实际录屏时,音频和视频是分开两条线程来编码的,但最后往mp4v2写入时,是写入同一个文件的,但由于mp4v2没有做好同步,因此如果写入音视频帧的时候,不对mp4v2自己做好同步锁,会出现音视频写乱了的问题,导致最后视频无法播放。

至于图片呈现的问题,请回顾一下上面代码例子中的framePreTimeUs,这个是这一张图片被送入编码器的时候,合并视频时,需要将这个字段带给mp4v2。由于mp4v2默认是认为图片匀速输入的,所以它不理会我们这个字段,只在意一开始设置的帧率。但由于抓图不是匀速的,因此如果只依照固定的帧率来显示,将来视频就会时快时慢,甚至声音图片不同步。因此在添加视频帧时,务必要设置呈现的时间偏移。ShareREC以TimeScale为基准,会将framePreTimeUs根据TimeScale做一次转换,然后在MP4WriteSample的时候,renderingOffset参数传递进去。

android 屏幕录制方案,ShareREC for Android全系统录屏原理解析相关推荐

  1. android 屏幕录制方案,Android录屏的三种解决方案

    本文总结三种用于安卓录屏的解决方案: adb shell命令screenrecord MediaRecorder, MediaProjection MediaProjection , MediaCod ...

  2. android 屏幕录制方案,Android录制屏幕的实现方法

    原文:Paul Kinlan 翻译:Agora.io 长久以来,我一直希望能够直接从Android屏幕上进行录制并将其编码为多种格式,以便将录制内容嵌入在任意位置,而不需要安装任何软件. 如今,我们已 ...

  3. android 屏幕录制方案,Android录屏的三种方案

    本文总结三种用于安卓录屏的解决方案: adb shell命令screenrecord MediaRecorder, MediaProjection MediaProjection , MediaCod ...

  4. 1-3 /电脑屏幕录制神器!- Bandicam 满足您对录屏功能的所有幻想!

    Bandicam(班迪录屏)电脑高清录屏软件(上篇) -简单易用的专业化录屏软件 导读 说到录屏软件,我相信现在几乎每个人在电脑上或多或少都有安装一些.例如大名鼎鼎的 OBS(Open Broadca ...

  5. Win11自带屏幕录制怎么打开?Win11自带录屏的使用方法

    Win11自带屏幕录制怎么打开?有用户想要录制下自己玩游戏的精彩时刻,那么应该如何操作呢?我们不一定都是需要使用到录屏软件的,一般我们的电脑系统都自带有录制功能.下面小编将为大家介绍Win11自带录屏 ...

  6. ShareREC for iOS录屏原理解析

    众所周知,由于iOS系统的封闭性,也出于保护用户隐私的角度,苹果并没有公开的API供开发者调用,来录制屏幕内容.导致许多游戏或者应用没有办法直接通过调用系统API的方式提供录制功能,用户也无法将自己一 ...

  7. 2021年最详细的Android屏幕适配方案汇总

    1 Android屏幕适配的度量单位和相关概念 建议在阅读本文章之前,可以先阅读快乐李同学写的文章<Android屏幕适配的度量单位和相关概念>,这篇文章包含了阅读本文的一些基础知识,推荐 ...

  8. Android屏幕适配方案

    一. 手机适配的应用和使用场景 使android应用程序适用于不同的国家语言.型号.尺寸和SDK版本等手机环境中,其主要功能和界面风格保持不变. 手机适配主要包括三个方面:语言适配.屏幕适配.SDK平 ...

  9. Android 屏幕适配方案(七)

    原文地址为: Android 屏幕适配方案(七) 一. 手机适配的应用和使用场景 使android应用程序适用于不同的国家语言.型号.尺寸和SDK版本等手机环境中,其主要功能和界面风格保持不变. 手机 ...

最新文章

  1. 李沐团队新作Gluon,复现CV经典模型到BERT,简单好用 | 强烈推荐
  2. 64位Linux下JVM内存调设遇到GC问题的备忘
  3. 给用户增加SAP_ALL权限
  4. navicat中文版安装
  5. 数字孪生体技术白皮书_基于Flownex的数字孪生体解决方案 系列介绍之二:数据中心应用实例...
  6. 聚类算法——Birch详解
  7. es6 数组合并_JavaScript学习笔记(十九)-- ES6
  8. python 螺旋数组_人工智能首选语言是什么 究竟Python有多强大
  9. IDEA打造快捷属性 摆脱鼠标 高效操作
  10. 黒猩猩盗猎越来越严重!新科技「猩脸辨识」技术诞生
  11. libiconv交叉移植
  12. 【30集iCore3_ADP出厂源代码(ARM部分)讲解视频】30-11层驱动之FSMC
  13. 优酷进度条不能拖动_PerfDog测试腾讯视频、优酷、爱奇艺视频类小程序性能
  14. 查看系统信息msinfo32工具的使用
  15. 远程通讯测试软件,USR-TCP232-304和虚拟串口软件通讯测试
  16. 使用Java中面向对象的思想来实现两个人的一场战斗
  17. python源码剖析-笔记2
  18. 如何DIY一台属于你自己的电脑?
  19. 批量修改文件夹名称的一部分
  20. Pyhton语音播放

热门文章

  1. Unity3d自学记录 利用TextMesh制作飘血数字
  2. 双线性插值法(Bilinear Interpolation)
  3. ofo如果倒了,摩拜并没有胜利,真正胜利的是阿里!
  4. ArcGIS导入CAD文件转换失败,检查CAD图层名
  5. 立体画板--Plot3D
  6. 【经验分享】我的数据挖掘竞赛之路及秋招总结
  7. 颤抖吧!00后已经开始爆肝写游戏赚钱了!
  8. 7ZIP命令行极限压缩
  9. G盘无法访问此卷不包含可识别的文件系统,里面的资料如何恢复
  10. 【数论】[luoguP2431]正妹吃月饼