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; } 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);}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
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(); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
方法中进行了编码器的参数配置与启动、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(); 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 { 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); } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
private void resetOutputFormat() { if (mMuxerStarted) { throw new IllegalStateException("output format already changed!"); } MediaFormat newFormat = mEncoder.getOutputFormat(); 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; /** * 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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
实现编码:
private void encodeToVideoTrack(int index) { ByteBuffer encodedData = mEncoder.getOutputBuffer(index); if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { 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); mMuxer.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo); Log.i(TAG, "sent " + mBufferInfo.size + " bytes to muxer..."); }}
- 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的大体分析,由于总结时间仓促,很多细节部分我也没有进行深入的发掘研究,所以请大家抱着怀疑的态度阅读,如果说明有误或是理解不到位的地方,希望大家帮忙指出,谢谢!
参考文档
在功能的开发中还参考了很多有价值的资料与文章:
- Android屏幕直播方案
- Google官方的EncodeVirtualDisplayTest
- FLV文件格式解析
- 使用librtmp进行H264与AAC直播
- 后续更新…