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

      前段时间在Andorid平台实现了屏幕直播,现将其整理一下,用到的知识点主要为:MediaProjection和MediaCodec。

一.MediaProjection获取

      MediaProjection是Android5.0后提出的一套用于录制屏幕的API,无需root权限。与 MediaProjection协同的类有 MediaProjectionManager, MediaCodec。使用MediaProjection需要在AndroidManifest.xml中加入以下权限:

<uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" />
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平台上屏幕直播流程已经完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270