Android 屏幕直播分享之MediaProjection和MediaCodec分析

      前段时间在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工作流程图

image.png

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工作流程图

image.png

四.总结

针对以上的分析,最后总结一下屏幕录制及分享的工作流程:

      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};
一张图总结一下分享流程
image.png

      至此在Android平台上屏幕直播流程已经完成了。

你可能感兴趣的:(Android 屏幕直播分享之MediaProjection和MediaCodec分析)