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所创建的
mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);...mSurface = mEncoder.createInputSurface();...mVirtualDisplay = mMediaProjection.createVirtualDisplay(name, mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null);
  • MediaMuxer 将从 MediaCodec 得到的图像元数据封装并输出到MP4文件中
int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);...ByteBuffer encodedData = mEncoder.getOutputBuffer(index);...mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);

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

Demo很简单,两个Java文件:

  • MainActivity.java
  • ScreenRecorder.java

MainActivity

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

@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {    MediaProjection mediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);    if (mediaProjection == null) {        Log.e("@@", "media projection is null");        return;    }    // video size    final int width = 1280;    final int height = 720;    File file = new File(Environment.getExternalStorageDirectory(),            "record-" + width + "x" + height + "-" + System.currentTimeMillis() + ".mp4");    final int bitrate = 6000000;    mRecorder = new ScreenRecorder(width, height, bitrate, 1, mediaProjection, file.getAbsolutePath());    mRecorder.start();    mButton.setText("Stop Recorder");    Toast.makeText(this, "Screen recorder is running...", Toast.LENGTH_SHORT).show();    moveTaskToBack(true);}

ScreenRecorder

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

线程主体

 @Overridepublic void run() {    try {        try {            prepareEncoder();            mMuxer = new MediaMuxer(mDstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);        } catch (IOException e) {            throw new RuntimeException(e);        }        mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG + "-display",                mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,                mSurface, null, null);        Log.d(TAG, "created virtual display: " + mVirtualDisplay);        recordVirtualDisplay();    } finally {        release();    }}

MediaCodec的初始化

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

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

编码器实现循环编码

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

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

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

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

录屏视频帧的编码过程

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

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

实现编码:

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

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

参考文档

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

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

更多相关文章

  1. Android实现书籍翻页效果--扩展版
  2. unity与android的无缝连接
  3. Android—Android中监听EditText文本输入,实现修改用户名,清除密码
  4. Android实现文件上传功能
  5. 一个简单的Android(安卓)自定义view,实现手指划线
  6. 第一番 Android(安卓)不能进行debug调试
  7. Android开发实现HttpClient工具类
  8. Android(安卓)_Android登录对话框实现(1)
  9. 直播源码Android实现 曲线路径动画

随机推荐

  1. Android线程
  2. 丢失Android系统库或者Conversion to Dal
  3. Android防止内存溢出浅析
  4. 技术总结--android篇(二)--布局的优化
  5. Android开发之旅:活动与任务
  6. 【Android(安卓)1.6】View和ViewGroup的t
  7. android中的照相机机拍照程序(含连续拍照)
  8. (一)Android数据结构学习之链表
  9. 技术总结--android篇(二)--布局的优化
  10. Android的线程和线程池