写点什么

Android 技术分享| 一行代码实现安卓屏幕采集编码

发布于: 23 小时前

越来越多的 App 需要共享手机屏幕给他人观看,特别是在线教育行业。Android 从 5.0 开始支持了 MediaProjection,利用 MediaProjection ,可以实现截屏录屏功能。


本库对屏幕采集编码进行了封装,简单的调用即可实现 MediaProjection 权限申请,H264 硬编码,错误处理等功能。


特点

  • 适配安卓高版本

  • 使用 MediaCodec 异步硬编码

  • 编码信息可配置

  • 通知栏显示

  • 链式调用

使用

ScreenShareKit.init(this)          .onH264{ buffer, isKeyFrame, ts ->                       }.start()
复制代码

Github

源码地址

实现

1 请求用户授权屏幕采集

@TargetApi(Build.VERSION_CODES.M)    fun requestMediaProjection(encodeBuilder: EncodeBuilder){        this.encodeBuilder = encodeBuilder;        mediaProjectionManager  = activity?.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager        startActivityForResult(mediaProjectionManager?.createScreenCaptureIntent(), 90000)    }
复制代码


startActivityForResult 是需要在 Activity 或者 Fragment 中使用的,授权结果会在 onActivityResult 中回调。所以我们需要对这一步进行一个封装,使其能以回调到方式拿到结果。这里我们采用一个无界面的 Fragment,有很多库都是使用这种形式。


private val invisibleFragment : InvisibleFragment        get() {            val existedFragment = fragmentManager.findFragmentByTag(FRAGMENT_TAG)            return if (existedFragment != null) {                existedFragment as InvisibleFragment            } else {                val invisibleFragment = InvisibleFragment()                fragmentManager.beginTransaction()                    .add(invisibleFragment, FRAGMENT_TAG)                    .commitNowAllowingStateLoss()                invisibleFragment            }        }

fun start(){ invisibleFragment.requestMediaProjection(this)}
复制代码


这样我们就可以在一个无界面的 Fragment 中拿到 onActivityResult 中的授权结果和 MediaProjection 对象。

2.适配安卓 10

如果 targetSdkVersion 设置的 29 及以上,在获取到 MediaProjection 后调用 createVirtualDisplay ,将会收到一条异常


java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
复制代码


意思是说,这个操作需要在前台服务中进行。


那我们就写一个服务,并把 onActivityResult 获取到的结果全传过去。


override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {        intent?.let {            if(isStartCommand(it)){                val notification = NotificationUtils.getNotification(this)                startForeground(notification.first, notification.second) //通知栏显示                startProjection(                    it.getIntExtra(RESULT_CODE, RESULT_CANCELED), it.getParcelableExtra(                        DATA                    )!!                )            }else if (isStopCommand(it)){                stopProjection()                stopSelf()            }        }        return super.onStartCommand(intent, flags, startId)    }
复制代码


在 startProjection 方法中,我们需要获取 MediaProjectionManager,再获取 MediaProjection,接着创建一个虚拟显示屏。


private fun startProjection(resultCode: Int, data: Intent) {        val mpManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager        if (mMediaProjection == null) {            mMediaProjection = mpManager.getMediaProjection(resultCode, data)            if (mMediaProjection != null) {                mDensity = Resources.getSystem().displayMetrics.densityDpi                val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager                mDisplay = windowManager.defaultDisplay                createVirtualDisplay()                mMediaProjection?.registerCallback(MediaProjectionStopCallback(), mHandler)            }        }    }        private fun createVirtualDisplay() {        mVirtualDisplay = mMediaProjection!!.createVirtualDisplay(            SCREENCAP_NAME,            encodeBuilder.encodeConfig.width,            encodeBuilder.encodeConfig.height,            mDensity,            DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY or DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,            surface,            null,            mHandler        )    }
复制代码


在 createVirtualDisplay 方法中,有一个 Surface 参数,屏幕上的所有动作,都会映射到这个 Surface 中,这里我们使用 MediaCodec 创建一个输入 Surface 用来接收屏幕的输出并编码。

3.MediaCodec 编码

 private fun initMediaCodec() {        val format = MediaFormat.createVideoFormat(MIME, encodeBuilder.encodeConfig.width, encodeBuilder.encodeConfig.height)        format.apply {            setInteger(MediaFormat.KEY_COLOR_FORMAT,MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) //颜色格式            setInteger(MediaFormat.KEY_BIT_RATE, encodeBuilder.encodeConfig.bitrate) //码流            setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR)            setInteger(MediaFormat.KEY_FRAME_RATE, encodeBuilder.encodeConfig.frameRate) //帧数            setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)        }        codec = MediaCodec.createEncoderByType(MIME)        codec.apply {            setCallback(object : MediaCodec.Callback() {                override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {                }                override fun onOutputBufferAvailable(                    codec: MediaCodec,                    index: Int,                    info: MediaCodec.BufferInfo                ) {                    val outputBuffer:ByteBuffer?                    try {                        outputBuffer = codec.getOutputBuffer(index)                        if (outputBuffer == null){                            return                        }                    }catch (e:IllegalStateException){                        return                    }                    val keyFrame = (info.flags and  MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0                    if (keyFrame){                        configData = ByteBuffer.allocate(info.size)                        configData.put(outputBuffer)                    }else{                        val data = createOutputBufferInfo(info,index,outputBuffer!!)                        encodeBuilder.h264CallBack?.onH264(data.buffer,data.isKeyFrame,data.presentationTimestampUs)                    }                    codec.releaseOutputBuffer(index, false)
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { encodeBuilder.errorCallBack?.onError(ErrorInfo(-1,e.message.toString())) }
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) { }
}) configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) surface = createInputSurface() codec.start() } }
复制代码


以上进行了一些常规的配置,MediaFormat 可以为编码器设置一些参数,比如码率,帧率,关键帧 间隔等。


MediaCodec 编码提供同步异步两种方式,这里采用异步设置回调的方式(异步 API 21 以上可用)

4.封装作用

在 onOutputBufferAvailable 回调中,我已经将编码后的数据回调出去,并且判断了是关键帧还是普通帧。那封装这个库有什么用呢😂其实,可以结合一些第三方的音视频 SDK,直接将编码后的屏幕流数据通过第三方 SDK 推流,就能实现屏幕共享功能。


这里以 anyRTC 音视频 SDK 的 pushExternalVideoFrame 方法为例


        val rtcEngine = RtcEngine.create(this,"",RtcEvent())        rtcEngine.enableVideo()        rtcEngine.setExternalVideoSource(true,false,true)        rtcEngine.joinChannel("","111","","")        ScreenShareKit.init(this)            .onH264 {buffer, isKeyFrame, ts ->                rtcEngine.pushExternalVideoFrame(ARVideoFrame().apply {                    val array = ByteArray(buffer.remaining())                    buffer.get(array)                    bufType = ARVideoFrame.BUFFER_TYPE_H264_EXTRA                    timeStamp = ts                    buf = array                    height = Resources.getSystem().displayMetrics.heightPixels                    stride = Resources.getSystem().displayMetrics.widthPixels                })            }.start()
复制代码


几行代码就可以实现屏幕采集编码传输~非常的方便


参考


参考

发布于: 23 小时前阅读数: 4
用户头像

实时交互,万物互联! 2020.08.10 加入

实时交互,万物互联,全球实时互动云服务商领跑者!

评论

发布
暂无评论
Android技术分享| 一行代码实现安卓屏幕采集编码