Android MediaProjection截屏&录屏-适配AndroidQ以上版本
Posted 大脸猫6_6
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android MediaProjection截屏&录屏-适配AndroidQ以上版本相关的知识,希望对你有一定的参考价值。
工作中遇到截屏需求,首先想到的肯定是截图所在区域的控件,通过Canvas类将View绘制成一个Bitmap,之后是要显示还是保存都可以了。但是事实上还是有一些问题存在,已知有两个问题:①不能截取到状态栏的内容吧;② 如果页面存在视频播放器,那么无法获取到播放器视频画面吧。
使用系统MediaProjection就可以解决上述两个问题。
Demo地址:https://download.csdn.net/download/bigfc/86711553
一、截屏
首先,看下最后的实现效果:
device-2022-09-24-182332
具体的实现步骤:
1、申请权限&注册前台服务
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhn.learn_android">
<!--前台服务-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
...>
<!--注册截屏需要用到的前台服务-->
<service
android:name=".service.ScreenShortRecordService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="mediaProjection"/>
....
</application>
</manifest>
2、在Activity生命周期中绑定和解绑定Service
这里以绑定的形式开启服务方便Service和Activity之间的交互,而且考虑可能一个页面中可能多次触发截屏,service和activity绑定到一起,不用反复启动服务,而且可以跟页面生命周期保持一致。
class MediaProjectionActivity : AppCompatActivity()
//截屏、录屏服务
private var mScreenShortService: ScreenShortRecordService? = null
...
private val connection = object : ServiceConnection
override fun onServiceConnected(name: ComponentName?, iBinder: IBinder?)
if (iBinder is ScreenShortRecordService.ScreenShortBinder)
//截屏
mScreenShortService = iBinder.getService()
override fun onServiceDisconnected(name: ComponentName?)
//no-op
override fun onStart()
super.onStart()
// 绑定服务
Intent(this, ScreenShortRecordService::class.java)
.also intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
override fun onStop()
super.onStop()
//解绑服务
unbindService(connection)
3、点击截屏的时候通过MediaProjectionManager创建截屏Intent并启动
//截屏点击事件
fun capture(view: View)
mScreenShortService?.let
//开始截屏
mediaManager =
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mediaManager.createScreenCaptureIntent().apply
startActivityForResult(this, CAPTURE_CODE)
4、监听onActivityResult,获取到返回的intent后,调用服务的开始截屏
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK)
when (requestCode)
//截屏
CAPTURE_CODE ->
data?.let
mScreenShortService?.startShort(it, object : ScreenshotListener
override suspend fun onScreenSuc(bitmap: Bitmap)
//显示截图
showScreenshort(bitmap)
)
//录屏
MIRROR_CODE ->
...
5、 这里必须先申请成为前台服务,否则会报SecurityException异常
fun startShort(intent: Intent, callback: ScreenshotListener)
//开启通知,并申请成为前台服务
startNotification()
//标记
this.isGot = false
//回调
this.callback = callback
mMediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
//获取令牌
mMediaProjection = mMediaProjectionManager?.getMediaProjection(Activity.RESULT_OK, intent)
//这里延迟一会再取
Handler(Looper.myLooper()!!).postDelayed(object : Runnable
override fun run()
//配置ImageReader
configImageReader()
, 400)
6、成功获取到令牌后,就可以通过监听获取有效的ImageReader对象
@SuppressLint("WrongConstant")
fun configImageReader()
val dm = resources.displayMetrics
imageReader = ImageReader.newInstance(
dm.widthPixels, dm.heightPixels,
PixelFormat.RGBA_8888, 1
).apply
setOnImageAvailableListener(
//这里页面帧发生变化时就会回调一次,我们只需要获取一张图片,加个标记位,避免重复
if (!isGot)
isGot = true
//这里就可以保存图片了
savePicTask(it)
, null)
//把内容投射到ImageReader 的surface
mMediaProjection?.createVirtualDisplay(
TAG, dm.widthPixels, dm.heightPixels, dm.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, surface, null, null
)
7、最后读取ImageReader生成Bitmap,此处就已经可以退出前台服务了,但是服务并没有解绑,下次只需要让服务重新申请前台,就可以继续下次截屏。
/**
* 保存图片
*/
private fun savePicTask(reader: ImageReader)
scopeIo
var image: Image? = null
try
//获取捕获的照片数据
image = reader.acquireLatestImage()
val width = image.width
val height = image.height
//拿到所有的 Plane 数组
val planes = image.planes
val plane = planes[0]
val buffer: ByteBuffer = plane.buffer
//相邻像素样本之间的距离,因为RGBA,所以间距是4个字节
val pixelStride = plane.pixelStride
//每行的宽度
val rowStride = plane.rowStride
//因为内存对齐问题,每个buffer 宽度不同,所以通过pixelStride * width 得到大概的宽度,
//然后通过 rowStride 去减,得到大概的内存偏移量,不过一般都是对齐的。
val rowPadding = rowStride - pixelStride * width
// 创建具体的bitmap大小,由于rowPadding是RGBA 4个通道的,所以也要除以pixelStride,得到实际的宽
val bitmap = Bitmap.createBitmap(
width + rowPadding / pixelStride,
height, Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
callback?.onScreenSuc(bitmap)
//服务退出前台
stopForeground(true)
mMediaProjection?.stop()
catch (e: java.lang.Exception)
e.printStackTrace()
finally
//记得关闭 image
try
image?.close()
catch (e: Exception)
二、录屏
device-2022-09-24-184104
具体实现步骤如下:
1、除了截屏需要前台服务权限和Service,录屏还需要存储和录音权限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhn.learn_android">
<!--外部存储读写权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!--前台服务-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
...>
<service
android:name=".service.ScreenShortRecordService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="mediaProjection"/>
...
</application>
</manifest>
2、服务绑定同截屏
3、点击录屏此时需要动态申请录音、存储权限
4、获得权限后,同样需要通过MediaProjectionManager创建录屏的Intent,并启动
//开始录屏
private fun startRecordScreen()
if (!isRecord)
//释放播放器
MediaPlayerHelper.release()
isRecord = true
mediaManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mediaManager.createScreenCaptureIntent().apply
startActivityForResult(this, MIRROR_CODE)
btnRecord?.text = "正在录制,可随意切换界面,点击结束并播放"
else
try
isRecord = false
btnRecord?.text = "点击开始屏幕录制"
//停止录制
mScreenShortService?.stopRecorder()
Toast.makeText(this, "开始播放", Toast.LENGTH_SHORT).show()
surfaceview?.holder?.let
val file = File(path, fileName)
MediaPlayerHelper.prepare(
file.absolutePath,
it,
MediaPlayer.OnPreparedListener
Log.d(TAG, "onPrepared: $it.isPlaying")
MediaPlayerHelper.play()
)
catch (e: Exception)
Log.d(TAG, "mediaProjecing: $e")
5、监听到返回结果是就可以调用服务,开始录制了
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK)
when (requestCode)
//截屏
CAPTURE_CODE ->
...
//录屏
MIRROR_CODE ->
//开始录制
data?.let
mScreenShortService?.startRecorder(path, fileName, it)
6、申请前台服务、申请令牌、配置MediaRecorder,开始录屏。
这里为了存储视频文件,需要传递一个文件路径和文件名,配置MediaRecorder的时候使用
//开始录屏
fun startRecorder(path: String, fileName: String, intent: Intent)
//开启通知,并申请成为前台服务
startNotification()
this.isGot = false
this.callback = callback
mMediaProjectionManager =
getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
//获得令牌
mMediaProjection = mMediaProjectionManager?.getMediaProjection(Activity.RESULT_OK, intent)
//这里延迟一会再取
Handler(Looper.myLooper()!!).postDelayed(object : Runnable
override fun run()
//配置MediaRecorder
if (configMediaRecorder(path, fileName))
try
//开始录屏
recorder?.start()
catch (e: Exception)
e.printStackTrace()
, 400)
7、配置MediaRecorder
/**
* 配置MediaProjection
*/
private fun configMediaRecorder(path: String, fileName: String): Boolean
//创建文件夹
val dir = File(path)
if (!dir.exists())
dir.mkdirs()
val file = File(path, fileName)
if (file.exists())
file.delete()
val dm = resources.displayMetrics
recorder = MediaRecorder()
recorder?.apply
setAudiosource(MediaRecorder.AudioSource.MIC) //音频载体
setVideoSource(MediaRecorder.VideoSource.SURFACE) //视频载体
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) //输出格式
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) //音频格式
setVideoEncoder(MediaRecorder.VideoEncoder.H264) //视频格式
setVideoSize(dm.widthPixels, dm.heightPixels) //size
setVideoFrameRate(30) //帧率
setVideoEncodingBitRate(3 * 1024 * 1024) //比特率
//设置文件位置
setOutputFile(file.absolutePath)
try
prepare()
virtualDisplay = mMediaProjection?.createVirtualDisplay(
TAG,
dm.widthPixels,
dm.heightPixels,
dm.densityDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface,
null,
null
)
catch (e: Exception)
e.printStackTrace()
return false
return true
8、经过上述步骤,手机就开始录屏了,当点击录制结束时,不要忘记关闭MediaRecorder。此时可以退出前台服务了。
//停止录制
fun stopRecorder()
recorder?.stop()
recorder?.release()
recorder = null
mMediaProjection?.stop()
//退出前台服务
stopForeground(true)
此时已经配置的文件路径中就可以找到录屏的文件了,如下图:
最后 无论是录屏还是截屏都需要释放资源,这里不再列举了
异常情况
1、java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
此问题是因为Android Q开始,使用中MediaProjection时必须申请一个前台服务,并且开启一个通知,用于提醒用户应用正在捕获屏幕信息,无论是服务开启的顺序错误还是未开启通知都会报这个异常。
2、RuntimeException: setAudioSource failed异常
录屏是需要动态申请录音权限,权限未申请通过会出现这个错误
3、Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified
创建PendingIntent是在Android 版本31以上时,需要指定Flag为FLAG_IMMUTABLE 或者 FLAG_MUTABLE,否则会报这个错误,具体代码可以参考:
val pendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
);
else
PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_CANCEL_CURRENT
);
参考链接:
Capture video and audio playback | Android Developers
绑定服务概览 | Android 开发者 | Android Developers
Foreground services | Android Developers
Android 音视频开发(六) -- Android Mediaprojection 截屏和录屏
Android技术分享| 一行代码实现安卓屏幕采集编码
越来越多的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()
几行代码就可以实现屏幕采集编码传输~非常的方便
以上是关于Android MediaProjection截屏&录屏-适配AndroidQ以上版本的主要内容,如果未能解决你的问题,请参考以下文章