越來越多的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 one rror(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()
幾行代碼就可以實作螢屏采集編碼傳輸~非常的方便
參考
參考
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/302587.html
標籤:其他
