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

【Android技术分享| 一行代码实现安卓屏幕采集编码】青春须早为,岂能长少年。这篇文章主要讲述Android技术分享| 一行代码实现安卓屏幕采集编码相关的知识,希望能为你提供帮助。
越来越多的App需要共享手机屏幕给他人观看,特别是在线教育行业。android 从5.0开始支持了MediaProjection,利用MediaProjection ,可以实现截屏录屏功能。
本库对屏幕采集编码进行了封装,简单的调用即可实现MediaProjection权限申请,H264硬编码,错误处理等功能。

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

文章图片

特点
  • 适配安卓高版本
  • 使用 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 andMediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0 if (keyFrame){ configData = https://www.songbingjia.com/android/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()

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

    推荐阅读