前段时间在Andorid平台实现了屏幕直播,现将其整理一下,用到的知识点主要为:MediaProjection和MediaCodec。
一.MediaProjection获取
MediaProjection是Android5.0后提出的一套用于录制屏幕的API,无需root权限。与 MediaProjection协同的类有 MediaProjectionManager, MediaCodec。使用MediaProjection需要在AndroidManifest.xml中加入以下权限:
a.获取MediaProjectionManager
获取MediaProjection,需要用到MediaProjectionManager,它是一个系统级的服务,类似WindowManager,ActivityManager等,可以通过getSystemService方法来获取它的实例:
MediaProjectionManager mediaProjectionManager =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
public static final String MEDIA_PROJECTION_SERVICE = "media_projection";
b.请求
获取到MediaProjectionManager之后,再来进一步获取MediaProjection,获取方式如下:
private void requestPermission() {
MediaProjectionManager mediaProjectionManager =
(MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
startActivityForResult(mediaProjectionManager.createScreenCaptureIntent(),
REQUEST_MEDIA_PROJECTION);
}
获取方式是通过startActivityForResult()来获取,createScreenCaptureIntent()是获取请求的Intent,如下:
public Intent createScreenCaptureIntent() {
Intent i = new Intent();
i.setClassName("com.android.systemui","com.android.systemui.media.MediaProjectionPermissionActivity");
return i;
}
从请求的Intent可以看到,是去启动systemui里面的一个叫MediaProjectionPermissionActivity的Activity。
c.请求处理
由于屏幕录制会涉及到个人隐私,需要弹窗确认,一起看一下MediaProjectionPermissionActivity的逻辑处理:
public class MediaProjectionPermissionActivity extends Activity
implements DialogInterface.OnClickListener, CheckBox.OnCheckedChangeListener,
DialogInterface.OnCancelListener {
......
......
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
mPackageName = getCallingPackage();
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
mService = IMediaProjectionManager.Stub.asInterface(b);
if (mPackageName == null) {
finish();
return;
}
PackageManager packageManager = getPackageManager();
ApplicationInfo aInfo;
try {
aInfo = packageManager.getApplicationInfo(mPackageName, 0);
mUid = aInfo.uid;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "unable to look up package name", e);
finish();
return;
}
try {
if (mService.hasProjectionPermission(mUid, mPackageName)) {
setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName,
false /*permanentGrant*/));
finish();
return;
}
} catch (RemoteException e) {
Log.e(TAG, "Error checking projection permissions", e);
finish();
return;
}
......
//弹窗来确认是否赋予权限
......
}
......
......
}
通过以上可以看到,在MediaProjectionPermissionActivity创建后,主要做了以下几件事:
1.通过ServiceManager获取到MediaProjectionManager引用对象;
2.获取调用者的包名、uid等信息,进行检测判断,如果该package已经请求过且同意过,直接调用setResult()返回;否则的话,会弹窗进行确认;
接下来看一下允许后,执行setResult()的逻辑:
setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName, mPermanentGrant));
private Intent getMediaProjectionIntent(int uid, String packageName, boolean permanentGrant)
throws RemoteException {
IMediaProjection projection = mService.createProjection(uid, packageName,
MediaProjectionManager.TYPE_SCREEN_CAPTURE, permanentGrant);
Intent intent = new Intent();
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());
return intent;
}
可以看到,通过getMediaProjectionIntent()来获取Intent,通过前面获取的mService来获取IMediaProjection实例,然后通过asBinder()获取到IMediaProjection实例对应的binder传入Intent,最后返回Intent。
d.请求返回
处理端MediaProjectionPermissionActivity执行setResult()后,申请端通过onActivityResult来获取结果,data为Intent,通过getMediaProjection来获取MediaProjection。
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != RESULT_OK) {
Toast.makeText(this,
"User denied screen recorder permission", Toast.LENGTH_SHORT).show();
return;
}
mMediaProjection = mProjectionManager.getMediaProjection(resultCode, data);
}
至此MediaProjection已经获取完毕。
Rom开发获取MediaProjection
如果应用是基于平台开发,不希望录屏时给用户弹窗提示,可以不通过startActivityForResult,从而跳过去MediaProjectionPermissionActivity申请弹窗权限,逻辑如下:
a.应用系统具有系统权限
android:sharedUserId="android.uid.system
b.直接去获取onActivityResult返回的data Intent
public Intent getMediaProjectionIntent(Context context, String packageName,
boolean permanentGrant) throws RemoteException {
IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
sMediaProjectionManager = IMediaProjectionManager.Stub.asInterface(b);
PackageManager packageManager = context.getPackageManager();
ApplicationInfo aInfo;
int uid;
try {
aInfo = packageManager.getApplicationInfo(packageName, 0);
uid = aInfo.uid;
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "unable to look up package name", e);
return null;
}
IMediaProjection projection = sMediaProjectionManager.createProjection(uid, packageName,
MediaProjectionManager.TYPE_SCREEN_CAPTURE, permanentGrant);
Intent intent = new Intent();
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());
return intent;
}
c.获取MediaProjection
Intent data = getMediaProjectionIntent(c, PKG_NAME, true);
mMediaProjection = mProjectionManager.getMediaProjection(Activity.RESULT_OK, data);
二.屏幕录制
在获取到MediaProjection之后,录屏的权限已经获得,接下来就可以进行屏幕录制了,需要创建一virtualDisplay来进行录屏,创建方式如下:
mVirtualDisplay = mMediaProjection.createVirtualDisplay("-display", width, height,
1,DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
在创建VirtualDisplay时,注意如下:
a.width、height分别代表录制display对应的宽和高像素大小;
b.surface传值不能为null,为null时,没有屏幕数据产出;
c.当surface为surfaceView.getHolder().getSurface()时,录屏会直接在surfaceView上显示,在加载surfaceview时,需要执行surfaceView.getHolder().setFixedSize(VIDEO_WIDTH, VIDEO_HEIGHT),VIDEO_WIDTH和VIDEO_HEIGHT需要跟createVirtualDisplay时传入的width和height保持一致,否则的话,surfaceview内的视频会有拉伸或位移;
d.当surface = vencoder.createInputSurface()时,获取MediaCodec的surface,这个surface其实就是一个入口,屏幕作为输入源就会进入这个入口,然后交给MediaCodec编码,可以将数据通过网络传输给其他设备显示。
在上述都准备好后,需要MediaCodec登场了,MediaCodec可以访问底层的媒体编解码器,可以对媒体进行编/解码,编码是录屏的过程,解码是显示的过程。
三.MediaCodec编解码
编码是录屏的过程,实时获取屏幕的数据,接下来看一下通过Mediacodec来创建编码器。
a.Encoder
Encoder负责实时获取屏幕数据,将数据储存,供后续通过网络发送屏幕数据。
1.Encoder配置及创建
public static final String MIMETYPE_VIDEO_AVC = "video/avc";
private void startVideoEncoder() {
MediaCodec vencoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
vencoder.configure(format[属性配置], null, null, CONFIGURE_FLAG_ENCODE);
Surface surface = vencoder.createInputSurface();
mVirtualDisplay = mMediaProjection.createVirtualDisplay("-display", width, height,
1,DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null);
vencoder.start();
}
调用MediaCodec的createEncoderByType()来创建Encoder,对应的type为"video/avc",代表屏幕视频数据是H264编码;获取到Encoder对象后,调用Encoder的createInputSurface()来创建surface作为屏幕数据的入口,用来储存后进行发送;配置好Format后,调用start()来启动进行屏幕录制。
2.获取屏幕数据并发送到指定端去渲染显示
MediaCodec.BufferInfo vBufferInfo = new MediaCodec.BufferInfo();
while (isRunning) {
int outputBufferId = vencoder.dequeueOutputBuffer(vBufferInfo, 0);//dequeue有效的Output buffer索引,为了发送传输。
ByteBuffer bb;
if (outputBufferId >= 0) {
if (Build.VERSION.SDK_INT < 21) {
ByteBuffer[] outputBuffers = vencoder.getOutputBuffers();//获取录屏数据存储的Output buffer数组
bb = outputBuffers[outputBufferId];
} else {
bb = vencoder.getOutputBuffer(outputBufferId);
}
}
}
在获取输出缓存时,首先创建一个BufferInfo对象,然后不断循环通过dequeueOutputBuffer(BufferInfo info, long timeoutUs)来请求输出缓存索引outputBufferId,再通过getOutputBuffer()和outputBufferId来获取输出缓存,在获取索引的时候需要传入刚创建的BufferInfo对象,用于存储ByteBuffer的信息,比如:当前是配置帧还是关键帧,使用方式如下:
//读取索引下的有效数据,进行转换后发送到指定端
private void onEncodedAvcFrame(ByteBuffer buffer, MediaCodec.BufferInfo info) {
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
/*
* 特定格式信息等配置数据,不是媒体数据
*/
} else if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
/* delimiter: 00 00 00 01 */
/* I-frame:buf[5]==0x65; SPS:buf[5]==0x67; PPS:buf[5]==0x68; */
}
}
//发送数据
发送数据完成后,释放申请的output buffer,释放方式如下:
vencoder.releaseOutputBuffer(outputBufferId, false[不渲染到surface]);//释放申请的Output buffer
3.Encoder工作流程图
b.Decoder
Decoder负责渲染屏幕数据,将从网络接收的屏幕数据进行入列处理后出列再进行渲染。
1.Decoder配置及创建
private void startVideoDecoder() {
MediaCodec decoder = MediaCodec.createDecoderByType(MIME_TYPE);
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, VIDEO_WIDTH, VIDEO_HEIGHT);
format.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_WIDTH * VIDEO_HEIGHT);
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
//横屏
byte[] header_sps = {0, 0, 0, 1, 103, 66, -128, 31, -38, 1, 64, 22, -24, 6, -48, -95, 53};
byte[] header_pps = {0, 0 ,0, 1, 104, -50, 6, -30};
//竖屏
byte[] header_sps = {0, 0, 0, 1, 103, 66, -128, 31, -38, 2, -48, 40, 104, 6, -48, -95, 53};
byte[] header_pps = {0, 0 ,0, 1, 104, -50, 6, -30};
format.setByteBuffer("csd-0", ByteBuffer.wrap(header_sps));
format.setByteBuffer("csd-1", ByteBuffer.wrap(header_pps));
decoder.configure(format, mSurface, null, 0);//mSurface对应需要展示surfaceview的surface
decoder.start();
}
调用MediaCodec的createDecoderByType()来创建Decoder,对应的type为"video/avc",代表屏幕视频数据是H264编码;配置好Format后,调用start()来启动。
2.将远端传输过来的屏幕数据渲染显示
a.远端屏幕数据过来后,将数据存入到Input Buffer中;
// Get input buffer index
int inputBufferIndex = decoder.dequeueInputBuffer(100);//dequeue可以存储的有效索引
ByteBuffer inputBuffer;
if (inputBufferIndex >= 0) {
if (Build.VERSION.SDK_INT < 21) {
ByteBuffer[] inputBuffers = decoder.getInputBuffers();//获取可以存储的input buffer数组
inputBuffer = inputBuffers[inputBufferIndex];
} else {
inputBuffer = decoder.getInputBuffer(inputBufferIndex);
}
inputBuffer.clear();
inputBuffer.put(buf, offset, length);//将传过来的buf放入有效的buffer索引中
decoder.queueInputBuffer(inputBufferIndex, 0, length, System.currentTimeMillis(), 0);//将数据queue到需要渲染的input buffer中
}
通过getInputBuffer(inputBufferIndex)得到当前请求的输入缓存,在使用之前要进行clear(),避免之前的缓存数据影响当前数据,然后把网络接收的数据添加到输入缓存中,并调用queueInputBuffer(…)把缓存数据入队;
b.不断去获取存入input buffer中的数据,渲染到surfaceview上显示
while (mIsRunning) {
// Get output buffer index
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
int outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 100);//dequeue一块已经存好数据[a步queueInputBuffer的数据]的 输出buffer索引
while (outputBufferIndex >= 0) {
decoder.releaseOutputBuffer(outputBufferIndex, true[渲染到surface]);//将数据在surface上渲染[surfaceview上显示]
outputBufferIndex = decoder.dequeueOutputBuffer(bufferInfo, 0);//不断dequeue,以备渲染
}
}
通过以上可以看到,首先请求一个空的输入缓存(input buffer),向其中填充满数据并将它传递给编解码器处理,编解码器处理完这些数据并将处理结果输出至一个空的输出缓存(output buffer)中。最终请求到一个填充了结果数据的输出缓存(output buffer),使用完其中的数据,并将其释放给编解码器再次使用。
3.具体流程
a. Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer];
b. Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer];
c. MediaCodec 模块从 input 缓冲区队列取一帧数据进行编解码处理;
d. 编解码处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列;
e. Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer];
f. Client 对编解码后的 buffer 进行渲染/播放;
g. 渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 ;[releaseOutputBuffer]
原文链接:https://blog.csdn.net/gb702250823/java/article/details/81627503
4.Decoder工作流程图
四.总结
针对以上的分析,最后总结一下屏幕录制及分享的工作流程:
1.屏幕分享端先获取MediaProjection;
2.屏幕分享端通过MediaCodec的createEncoderByType创建编码器,进行配置后start();
3.屏幕观看端通过MediaCodec的createDecoderByType创建解码器,进行配置后start();
4.屏幕分享端循环执行dequeueOutputBuffer(),getOutputBuffer(),sendData(),releaseOutputBuffer(,false);
5.屏幕观看端循环执行dequeueInputBuffer(),getInputBuffer(),queueInputBuffer(),dequeueOutputBuffer(),releaseOutputBuffer(,true);
6.分享及观看结束时,执行stop()、release();
配置帧
cfgFrame:配置帧,解码器在收到该帧后,才能开始解码,否则的话,会出现绿屏等现象,格式如下:
byte [] cfgFrame1 = {0, 0, 0, 1, 103, 66, -128, 31, -38, 1, 64, 22, -23, 72, 40, 48, 48, 54, -123, 9, -88, 0, 0, 0, 1, 104, -50, 6, -30};
byte [] cfgFrame2 = {0, 0, 0, 1, 103, 66, -128, 31, -38, 1, 64, 61, -91, 32, -96, -64, -64, -38, 20, 38, -96, 0, 0, 0, 1, 104, -50, 6, -30};
一张图总结一下分享流程
至此在Android平台上屏幕直播流程已经完成了。