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. Android中设置动画循环旋转的方法
  3. 向模拟器发短信打电话的方法
  4. android手机屏幕适配方法
  5. Android 简单视频播放器(破烂版,后续更新)
  6. 更新android studio gradle 不成功解决方法
  7. Android VideoView播放视频
  8. android视频播放器源码分析

随机推荐

  1. ch09 Android ListView
  2. android firstslide
  3. android:layout_weight
  4. [置顶] Android获取本地图片缩略图终极解
  5. android之相对布局示例
  6. Android中GridView区中且行间距设置
  7. android 官方SDK文档
  8. Android(Java):边框
  9. android解决方案链接
  10. Android(安卓)用户界面---Android如何描