Android实现录屏直播(一)ScreenRecorder的简单分析
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
所创建的mEncoder = MediaCodec.createEncoderByType(MIME_TYPE);
...
mSurface = mEncoder.createInputSurface();
...
mVirtualDisplay = mMediaProjection.createVirtualDisplay(name, mWidth, mHeight, mDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null);
- 1
- 2
- 3
- 4
- 5
MediaMuxer
将从MediaCodec
得到的图像元数据封装并输出到MP4文件中int index = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US);
...
ByteBuffer encodedData = mEncoder.getOutputBuffer(index);
...
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的初始化
@Override
protected 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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
ScreenRecorder
这是一个线程,结构很清晰,run()
方法中完成了MediaCodec的初始化,VirtualDisplay的创建,以及循环进行编码的全部实现。
线程主体
@Override
public 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
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();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
编码器实现循环编码
下面的代码就是编码过程,由于作者使用的是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);
}
}
}
- 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() {
// 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);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
获取sps pps的ByteBuffer,注意此处的sps pps都是read-only只读状态
private void getSpsPpsByteBuffer(MediaFormat newFormat) {
ByteBuffer rawSps = newFormat.getByteBuffer("csd-0");
ByteBuffer rawPps = newFormat.getByteBuffer("csd-1");
}
- 1
- 2
- 3
- 4
录屏视频帧的编码过程
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;
- 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) {
// 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...");
}
}
- 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实现录屏直播(一)ScreenRecorder的简单分析的更多相关文章
- Android实现录屏直播(三)MediaProjection + VirtualDisplay + librtmp + MediaCodec实现视频编码并推流到rtmp服务器
请尊重分享成果,转载请注明出处,本文来自Coder包子哥,原文链接:http://blog.csdn.net/zxccxzzxz/article/details/55230272 Android实现录 ...
- Android实现录屏直播(二)需求才是硬道理之产品功能调研
请尊重分享成果,转载请注明出处,本文来自Coder包子哥,原文链接:http://blog.csdn.net/zxccxzzxz/article/details/54254244 前面的Android ...
- Android设备一对多录屏直播--(UDP组播连接,Tcp传输)
原文:https://blog.csdn.net/sunmmer123/article/details/82734245 近期需要学习流媒体知识,做一个Android设备相互投屏Demo,因此找到了这 ...
- 手游录屏直播技术详解 | 直播 SDK 性能优化实践
在上期<直播推流端弱网优化策略 >中,我们介绍了直播推流端是如何优化的.本期,将介绍手游直播中录屏的实现方式. 直播经过一年左右的快速发展,衍生出越来越丰富的业务形式,也覆盖越来越广的应用 ...
- Android gif 录屏
/********************************************************************************** * Android gif 录屏 ...
- Windows 11实现录屏直播,搭建Nginx的rtmp服务
先!下载几个工具呗 官方下载FFmpeg:http://www.ffmpeg.org 官方下载nginx-rtmp-module:https://github.com/arut/nginx-rtmp- ...
- Windows11实现录屏直播,H5页面直播 HLS ,不依赖Flash
这两天的一个小需求,需要实现桌面实时直播,前面讲了两种方式: 1.Windows 11实现录屏直播,搭建Nginx的rtmp服务 的方式需要依赖与Flash插件,使用场景有限 2.Windows 11 ...
- EasyIPCamera实现Windows PC桌面、安卓Android桌面同屏直播,助力无纸化会议系统
最近在EasyDarwin开源群里,有不少用户私信需求,要做一种能够多端同屏的系统,细分下来有屏幕采集端和同屏端,屏幕采集端细想也就是一个低延时的流媒体音视频服务器,同屏端也就是一个低延时的播放器,负 ...
- Android 和 iOS 实现录屏推流的方案整理
一.录屏推流实现的步骤 1. 采集数据 主要是采集屏幕获得视频数据,采集麦克风获得音频数据,如果可以实现的话,我们还可以采集一些应用内置的音频数据. 2. 数据格式转换 主要是将获取到的视频和音频转换 ...
随机推荐
- ResourcesCompat和ContextCompat
getResources().getDrawable() 过时的解决方法 当你这个Drawable不受主题影响时 ResourcesCompat.getDrawable(getResources(), ...
- 《C++ Primer Plus》14.3 多重继承 学习笔记
多重继承(MI)描述的是有多个直接基类的类.与单继承一样,共有MI表示的也是is-a关系.例如,可以从Awiter类和Singer类派生出SingingWaiter类:class SingingWai ...
- Android SharedPreferences保存第一次的信息
private void setHomeTimeZone() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPrefere ...
- VScode之JavaScript Snippet Pack
一个片段包 使用例如: cl 回车或者tab键,就可以完整的打出console.log("") 还有很多快捷功能: 参考: https://marketplace.visualst ...
- centos6.8上安装paramiko
author : headsen chen date: 2018-10-07 17:06:07 # 安装gcc,python-devel yum -y install python-devel ...
- 【Android】Android--Dialog
前言 对话框对于应用也是必不可少的一个组件,在Android中也不例外,对话框对于一些提示重要信息,或者一些需要用户额外交互的一些内容很有帮助.本篇博客就讲解一下Android下对话框的使用,在本篇博 ...
- flex弹性布局属性详解!
详细看下flex弹性布局具体属性: flex容器属性详解:flex-direction:row/column:(横排/竖排) 决定元素的排列方向:flex-wrap:nowrap/wrap/wrap- ...
- 微信小程序 --- 设置页面的标题
第一种方式:修改 page.json文件 { "navigationBarTitleText": "豆瓣 - 电影" } 第二种方式:使用 JS 修改: wx. ...
- 【HTTP header】【Access-Control-Allow-Credentials】跨域Ajax请求时是否带Cookie的设置
1. 无关Cookie跨域Ajax请求 客户端 以 Jquery 的 ajax 为例: $.ajax({ url : 'http://remote.domain.com/corsrequest', d ...
- yii2设置发送邮件的一些配置
错误提示: Warning: stream_socket_enable_crypto(): this stream does not support SSL/crypto in C:\xampp\ht ...