http://blog.csdn.net/zxccxzzxz/article/details/54150396

Android实现录屏直播(一)ScreenRecorder的简单分析

Android实现录屏直播(二)需求才是硬道理之产品功能调研

Android实现录屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec实现视频编码并推流到rtmp服务器

应项目需求瞄准了Bilibili的录屏直播功能,基本就仿着做一个吧。研究后发现Bilibili是使用的MediaProjection 与 VirtualDisplay结合实现的,需要 Android 5.0 Lollipop API 21以上的系统才能使用。

其实官方提供的android-ScreenCapture这个Sample中已经有了MediaRecorder的实现与使用方式,还有使用MediaRecorder实现的录制屏幕到本地文件的Demo,从中我们都能了解这些API的使用。

而如果需要直播推流的话就需要自定义MediaCodec,再从MediaCodec进行编码后获取编码后的帧,免去了我们进行原始帧的采集的步骤省了不少事。可是问题来了,因为之前没有仔细了解H264文件的结构与FLV封装的相关技术,其中爬了不少坑,此后我会一一记录下来,希望对用到的朋友有帮助。

项目中对我参考意义最大的一个Demo是网友Yrom的GitHub项目ScreenRecorder,Demo中实现了录屏并将视频流存为本地的MP4文件(咳咳,其实Yrom就是Bilibili的员工吧?( ゜- ゜)つロ)��。在此先大致分析一下该Demo的实现,之后我会再说明我的实现方式。

ScreenRecorder

具体的原理在Demo的README中已经说得很明白了:

  • Display 可以“投影”到一个 VirtualDisplay
  • 通过 MediaProjectionManager 取得的 MediaProjection创建VirtualDisplay
  • VirtualDisplay 会将图像渲染到 Surface中,而这个Surface是由MediaCodec所创建的
  1. mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
  2. ...
  3. mSurface = mEncoder.createInputSurface();
  4. ...
  5. mVirtualDisplay = mMediaProjection.createVirtualDisplay(name, mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null);
  • 1
  • 2
  • 3
  • 4
  • 5
  • MediaMuxer 将从 MediaCodec 得到的图像元数据封装并输出到MP4文件中
  1. int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
  2. ...
  3. ByteBuffer encodedData = mEncoder.getOutputBuffer(index);
  4. ...
  5. mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

所以其实在Android 4.4上可以通过DisplayManager来创建VirtualDisplay也是可以实现录屏,但因为权限限制需要ROOT。 (see DisplayManager.createVirtualDisplay())

Demo很简单,两个Java文件:

  • MainActivity.java
  • ScreenRecorder.java

MainActivity

类中仅仅是实现的入口,最重要的方法是onActivityResult,因为MediaProjection就需要从该方法开启。但是别忘了先进行MediaProjectionManager的初始化

  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, Intent data) {
  3. MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
  4. if (mediaProjection == null) {
  5. Log.e("@@", "media projection is null");
  6. return;
  7. }
  8. // video size
  9. final int width = 1280;
  10. final int height = 720;
  11. File file = new File(Environment.getExternalStorageDirectory(),
  12. "record-" + width + "x" + height + "-" + System.currentTimeMillis() + ".mp4");
  13. final int bitrate = 6000000;
  14. mRecorder = new ScreenRecorder(width, height, bitrate, 1, mediaProjection, file.getAbsolutePath());
  15. mRecorder.start();
  16. mButton.setText("Stop Recorder");
  17. Toast.makeText(this, "Screen recorder is running...", Toast.LENGTH_SHORT).show();
  18. moveTaskToBack(true);
  19. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

ScreenRecorder

这是一个线程,结构很清晰,run()方法中完成了MediaCodec的初始化,VirtualDisplay的创建,以及循环进行编码的全部实现。

线程主体

  1. @Override
  2. public void run() {
  3. try {
  4. try {
  5. prepareEncoder();
  6. mMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
  7. } catch (IOException e) {
  8. throw new RuntimeException(e);
  9. }
  10. mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG + "-display",
  11. mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
  12. mSurface, null, null);
  13. Log.d(TAG, "created virtual display: " + mVirtualDisplay);
  14. recordVirtualDisplay();
  15. } finally {
  16. release();
  17. }
  18. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

MediaCodec的初始化

方法中进行了编码器的参数配置与启动、Surface的创建两个关键的步骤

  1. private void prepareEncoder() throws IOException {
  2. MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight);
  3. format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
  4. MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // 录屏必须配置的参数
  5. format.setInteger(MediaFormat.KEY_BIT_RATE, mBitRate);
  6. format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
  7. format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
  8. Log.d(TAG, "created video format: " + format);
  9. mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
  10. mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
  11. mSurface = mEncoder.createInputSurface(); // 需要在createEncoderByType之后和start()之前才能创建,源码注释写的很清楚
  12. Log.d(TAG, "created input surface: " + mSurface);
  13. mEncoder.start();
  14. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

编码器实现循环编码

下面的代码就是编码过程,由于作者使用的是Muxer来进行视频的采集,所以在resetOutputFormat方法中实际意义是将编码后的视频参数信息传递给Muxer并启动Muxer。

  1. private void recordVirtualDisplay() {
  2. while (!mQuit.get()) {
  3. int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
  4. Log.i(TAG, "dequeue output buffer index=" + index);
  5. if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
  6. resetOutputFormat();
  7. } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) {
  8. Log.d(TAG, "retrieving buffers time out!");
  9. try {
  10. // wait 10ms
  11. Thread.sleep(10);
  12. } catch (InterruptedException e) {
  13. }
  14. } else if (index >= 0) {
  15. if (!mMuxerStarted) {
  16. throw new IllegalStateException("MediaMuxer dose not call addTrack(format) ");
  17. }
  18. encodeToVideoTrack(index);
  19. mEncoder.releaseOutputBuffer(index, false);
  20. }
  21. }
  22. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. private void resetOutputFormat() {
  2. // should happen before receiving buffers, and should only happen once
  3. if (mMuxerStarted) {
  4. throw new IllegalStateException("output format already changed!");
  5. }
  6. MediaFormat newFormat = mEncoder.getOutputFormat();
  7. // 在此也可以进行sps与pps的获取,获取方式参见方法getSpsPpsByteBuffer()
  8. Log.i(TAG, "output format changed.\n new format: " + newFormat.toString());
  9. mVideoTrackIndex = mMuxer.addTrack(newFormat);
  10. mMuxer.start();
  11. mMuxerStarted = true;
  12. Log.i(TAG, "started media muxer, videoIndex=" + mVideoTrackIndex);
  13. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

获取sps pps的ByteBuffer,注意此处的sps pps都是read-only只读状态

  1. private void getSpsPpsByteBuffer(MediaFormat newFormat) {
  2. ByteBuffer rawSps = newFormat.getByteBuffer("csd-0");
  3. ByteBuffer rawPps = newFormat.getByteBuffer("csd-1");
  4. }
  • 1
  • 2
  • 3
  • 4

录屏视频帧的编码过程

BufferInfo.flags表示当前编码的信息,如源码注释:

  1. /**
  2. * This indicates that the (encoded) buffer marked as such contains
  3. * the data for a key frame.
  4. */
  5. public static final int BUFFER_FLAG_KEY_FRAME = 1; // 关键帧
  6. /**
  7. * This indicated that the buffer marked as such contains codec
  8. * initialization / codec specific data instead of media data.
  9. */
  10. public static final int BUFFER_FLAG_CODEC_CONFIG = 2; // 该状态表示当前数据是avcc,可以在此获取sps pps
  11. /**
  12. * This signals the end of stream, i.e. no buffers will be available
  13. * after this, unless of course, {@link #flush} follows.
  14. */
  15. public static final int BUFFER_FLAG_END_OF_STREAM = 4;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

实现编码:

  1. private void encodeToVideoTrack(int index) {
  2. ByteBuffer encodedData = mEncoder.getOutputBuffer(index);
  3. if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
  4. // The codec config data was pulled out and fed to the muxer when we got
  5. // the INFO_OUTPUT_FORMAT_CHANGED status.
  6. // Ignore it.
  7. // 大致意思就是配置信息(avcc)已经在之前的resetOutputFormat()中喂给了Muxer,此处已经用不到了,然而在我的项目中这一步却是十分重要的一步,因为我需要手动提前实现sps, pps的合成发送给流媒体服务器
  8. Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG");
  9. mBufferInfo.size = 0;
  10. }
  11. if (mBufferInfo.size == 0) {
  12. Log.d(TAG, "info.size == 0, drop it.");
  13. encodedData = null;
  14. } else {
  15. Log.d(TAG, "got buffer, info: size=" + mBufferInfo.size
  16. + ", presentationTimeUs=" + mBufferInfo.presentationTimeUs
  17. + ", offset=" + mBufferInfo.offset);
  18. }
  19. if (encodedData != null) {
  20. encodedData.position(mBufferInfo.offset);
  21. encodedData.limit(mBufferInfo.offset + mBufferInfo.size); // encodedData是编码后的视频帧,但注意作者在此并没有进行关键帧与普通视频帧的区别,统一将数据写入Muxer
  22. mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
  23. Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer...");
  24. }
  25. }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

以上就是对ScreenRecorder这个Demo的大体分析,由于总结时间仓促,很多细节部分我也没有进行深入的发掘研究,所以请大家抱着怀疑的态度阅读,如果说明有误或是理解不到位的地方,希望大家帮忙指出,谢谢!

参考文档

在功能的开发中还参考了很多有价值的资料与文章:

  1. Android屏幕直播方案
  2. Google官方的EncodeVirtualDisplayTest
  3. FLV文件格式解析
  4. 使用librtmp进行H264与AAC直播
  5. 后续更新…
 
 

Android实现录屏直播(一)ScreenRecorder的简单分析的更多相关文章

  1. Android实现录屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec实现视频编码并推流到rtmp服务器

    请尊重分享成果,转载请注明出处,本文来自Coder包子哥,原文链接:http://blog.csdn.net/zxccxzzxz/article/details/55230272 Android实现录 ...

  2. Android实现录屏直播(二)需求才是硬道理之产品功能调研

    请尊重分享成果,转载请注明出处,本文来自Coder包子哥,原文链接:http://blog.csdn.net/zxccxzzxz/article/details/54254244 前面的Android ...

  3. Android设备一对多录屏直播--(UDP组播连接,Tcp传输)

    原文:https://blog.csdn.net/sunmmer123/article/details/82734245 近期需要学习流媒体知识,做一个Android设备相互投屏Demo,因此找到了这 ...

  4. 手游录屏直播技术详解 | 直播 SDK 性能优化实践

    在上期<直播推流端弱网优化策略 >中,我们介绍了直播推流端是如何优化的.本期,将介绍手游直播中录屏的实现方式. 直播经过一年左右的快速发展,衍生出越来越丰富的业务形式,也覆盖越来越广的应用 ...

  5. Android gif 录屏

    /********************************************************************************** * Android gif 录屏 ...

  6. Windows 11实现录屏直播,搭建Nginx的rtmp服务

    先!下载几个工具呗 官方下载FFmpeg:http://www.ffmpeg.org 官方下载nginx-rtmp-module:https://github.com/arut/nginx-rtmp- ...

  7. Windows11实现录屏直播,H5页面直播 HLS ,不依赖Flash

    这两天的一个小需求,需要实现桌面实时直播,前面讲了两种方式: 1.Windows 11实现录屏直播,搭建Nginx的rtmp服务 的方式需要依赖与Flash插件,使用场景有限 2.Windows 11 ...

  8. EasyIPCamera实现Windows PC桌面、安卓Android桌面同屏直播,助力无纸化会议系统

    最近在EasyDarwin开源群里,有不少用户私信需求,要做一种能够多端同屏的系统,细分下来有屏幕采集端和同屏端,屏幕采集端细想也就是一个低延时的流媒体音视频服务器,同屏端也就是一个低延时的播放器,负 ...

  9. Android 和 iOS 实现录屏推流的方案整理

    一.录屏推流实现的步骤 1. 采集数据 主要是采集屏幕获得视频数据,采集麦克风获得音频数据,如果可以实现的话,我们还可以采集一些应用内置的音频数据. 2. 数据格式转换 主要是将获取到的视频和音频转换 ...

随机推荐

  1. php检测文件只读、可写、可执行权限

    例子:检测文件是否可读.可写.可执行. 复制代码代码示例: <?php  $myfile = "./test.txt"; if (is_readable ($myfile)) ...

  2. 浅谈MVC和MVVM模式

    MVC I’m dating with a model… and a view, and a controller. 众所周知,MVC 是开发客户端最经典的设计模式,iOS 开发也不例外,但是 MVC ...

  3. ExtJs 6.0+快速入门,ext-bootstrap.js文件的分析,各版本API下载

    ExtJS6.0+快速入门+API下载地址 ExtAPI 下载地址如下,包含各个版本 http://docs.sencha.com/misc/guides/offline_docs.html 1.使用 ...

  4. c++11 处理时间和日期

    c++11提供了日期时间相关的库 chrono,通过chrono库可以很方便的处理日期和时间. 1. 记录时间长度的duration template<class Rep, class Peri ...

  5. $.when()方法监控ajax请求获取到的数据与普通ajax请求回调获取到的数据的不同

    1.$.when(ajax).done(function(data)}); 2.$.ajax().done(function(data){}); 1中的data被封装进一个对象[data, " ...

  6. SPF难以解决邮件伪造的现状以及方案

    邮件伪造的现状 仿冒域名 私搭邮服仿冒域名: 例如某公司企业的域名是example.com,那么攻击者可以搭建一个邮服,也把自己的域名配置为example.com,然后发邮件给真实的企业员工xxx@e ...

  7. linux动态查看某组进程状态的办法

    这里记录一下我监控某组进程的解决办法. 1.首先要获取要监控的进程的进程id,如果你要勇ps grep 那你就out了,强大的linux系统有一个pidof命令,用来查找相关进程的进程id,其实还有一 ...

  8. HUB、SPAN、TAP比较

    在获取数据包进行网络分析时,常用的方法有三种:HUB.SPAN和TAP. 一 HUB    HUB 很“弱智”,但这种方法却是最早的数据包获取方法.HUB是半双工的以太网设备,在广播数据包时,无法同时 ...

  9. 三维凸包求其表面积(POJ3528)

    Ultimate Weapon Time Limit: 2000MS   Memory Limit: 131072K Total Submissions: 2074   Accepted: 989 D ...

  10. 国外DNS服务器总结

    国外12个免费的DNS DNS(即Domain Name System,域名系统),是因特网上作为域名和IP地址相互映射的一个分布式数据库,能够让用户更方便的访问互联网,而不用去记住能够被机器直接读取 ...