共计 5306 个字符,预计需要花费 14 分钟才能阅读完成。
越来越多的 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()
几行代码就能够实现屏幕采集编码传输~十分的不便、
关注我,每天分享常识干货~