Android截图方案使用MediaProjection截图
文章目录
1.简介
MediaProjection 是google官方的屏幕截屏/录屏方式,
通过MediaProjectionManager管理有具体类型的MediaProjection建立屏幕捕获对话来实现截屏或者录屏。也就是说其实是靠MediaProjection来实现截屏或录屏的。
所有代码都在Github-Tinder
/**
* Manages the retrieval of certain types of {@link MediaProjection} tokens.
*
* <p><ol>An example flow of starting a media projection will be:
* <li>Declare a foreground service with the type {@code mediaProjection} in
* the {@code AndroidManifest.xml}.
* </li>
* <li>Create an intent by calling {@link MediaProjectionManager#createScreenCaptureIntent()}
* and pass this intent to {@link Activity#startActivityForResult(Intent, int)}.
* </li>
* <li>On getting {@link Activity#onActivityResult(int, int, Intent)},
* start the foreground service with the type
* {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION}.
* </li>
* <li>Retrieve the media projection token by calling
* {@link MediaProjectionManager#getMediaProjection(int, Intent)} with the result code and
* intent from the {@link Activity#onActivityResult(int, int, Intent)} above.
* </li>
* <li>Start the screen capture session for media projection by calling
* {@link MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,
* android.hardware.display.VirtualDisplay.Callback, Handler)}.
* </li>
* </ol>
*/
类注解就说的很明白,使用MediaProjectionManager
实现截屏仅需5步:
- 声明一个
foregroundServiceType
包含mediaProjection
类型的service - 通过 MediaProjectionManager.createScreenCaptureIntent() 获取intent,然后调用
Activity.tartActivityForResult(Intent, int)
- 在收到
Activity.onActivityResult(int, int, Intent)
回调后,启动Service,并传递foregroundServiceType
为ServiceInfo#FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
- 在
Activity.onActivityResult(int, int, Intent)
中调用MediaProjectionManager.getMediaProjection(int, Intent)
获取MediaProjection
- 调用
MediaProjection#createVirtualDisplay(String, int, int, int, int, Surface,android.hardware.display.VirtualDisplay.Callback, Handler)
开始捕获屏幕
2.具体使用
接下来,正式按照上述步骤介绍如何实现截屏
2.1 声明Service
Service代码
class TinderService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
super.onCreate()
Log.i(TAG, "onCreate: ")
//前台化通知
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channelId = "001"
val channelName = "screenshot"
val channel =
NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE)
channel.lightColor = Color.BLUE
channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
if (manager != null) {
val activityIntent: Intent = Intent(
this,
MainActivity::class.java
)
activityIntent.setAction("stop")
val contentIntent = PendingIntent.getActivity(
this,
0,
activityIntent,
PendingIntent.FLAG_MUTABLE
)
manager.createNotificationChannel(channel)
val notification: Notification =
Notification.Builder(getApplicationContext(), channelId)
.setOngoing(true)
.setSmallIcon(R.mipmap.ic_launcher)
.setCategory(Notification.CATEGORY_SERVICE)
.setContentTitle("screenshot")
.setContentIntent(contentIntent)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
//录屏要这个类型ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
startForeground(
1,
notification,ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
)
} else {
startForeground(1, notification)
}
}
} else {
startForeground(1, Notification())
}
}
companion object {
private const val TAG = "TinderService"
}
}
不必多说,服务前台化是必要的;
Manifest声明:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 前台服务-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- 文件权限-->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 截屏如果不想变成系统应用,增加这个权限-->
<uses-permission
android:name="android.permission.READ_FRAME_BUFFER"
tools:ignore="ProtectedPermissions" />
<application>
<service
android:name=".TinderService"
android:foregroundServiceType="mediaPlayback|mediaProjection"
tools:ignore="ForegroundServicePermission" />
</application>
</manifest>
切记:记得增加必要的权限;service要增加mediaProjection类型
2.2 发起屏幕捕获申请
private val mProjectionManager: MediaProjectionManager =
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val captureIntent = mProjectionManager.createScreenCaptureIntent()
s
startActivityForResult(cpatureIntent,0)
调用这个方法后,系统会弹窗询问是否允许捕获屏幕提问,不论同意还是拒绝,都会回调onActivityResult
2.3 在onActivityResult中处理结果
private val mOnActivityResult: IOnActivityResult by lazy {
object : IOnActivityResult {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.i(TAG, "onActivityResult:requestCode:$requestCode ,resultCode:$resultCode")
mMediaProjection =
mProjectionManager.getMediaProjection(resultCode, data)
data?.let {
Handler().post({
//要先启动service,否则/java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
getScreenshot(resultCode, data)
})
}
}
}
}
2.4 提前启动Service
这里注意,在调用MediaProjectionManager.getMediaProjection(int, Intent)
获取MediaProjection
之前,一定要先启动service,并保证service已经被前台化了,否则,就会包SecuretyException;
关于Service的启动,有以下3中方案:
方案一:
可以把这个intent传递到service中,在Service.onStartCommand
中在继续后续任务;
startActivity(Intent(this,TinderService::class.java).putPercelableExtra(data))
然后在Service.onStartCommand
在继续后续的工作
我个人猜想,这种方案才是符合官方方案的一种方式,因为捕获屏幕是一个后台性的任务,很适合放到service中去做。
方案二:
Handler(Looper.getMainLooper()).postDelayed({startActivity(Intent(this,TinderService::class.java)},1_000L)
这种方案我并不太喜欢,使用延时以达到任务同步无论何时都不是高明有效的方案;
方案三:
前两种方案我更推崇方案一,安全,但是我并不喜欢把一个独立的功能拆分放到不同的模块中。所以我在截屏模块启动之前,就提前启动service(比如模块初始化时),
这样就能保证安全性,功能完整性;
Service启动后,一定要将服务前台化,在前面已经贴过Service的完整代码,但一定要记得这行:
startForeground(1,notification,ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
foregroundServiceType
要传递ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
,否则会报错
2.4 开始捕获屏幕
private fun getScreenshot(resultCode: Int, data: Intent) {
Log.i(TAG, "getScreenshot: ")
mMediaProjection?.let {
val imageReader =
ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 3)
imageReader.setOnImageAvailableListener({
Log.i(TAG, "onImageAvailable: ")
// val image: Image? = imageReader.acquireNextImage()
val image: Image? = imageReader.acquireLatestImage()
image?.let {
val width = image.width
val height = image.height
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * width
val bitmap = Bitmap.createBitmap(
width + rowPadding / pixelStride,
height,
Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
val filePath =
Environment.getExternalStorageDirectory().path + File.separator + Calendar.getInstance().timeInMillis + ".jpg"
//bitmap保存为图片
saveBitmap(bitmap, filePath)
mOnScreenshotListener?.onScreenshot(bitmap)
//--释放资源
mVirtualDisplay?.release()
release()
image.close()
//使用完一定要回收
// bitmap.recycle()
cancelNotification();
}
}, null)
mVirtualDisplay = it.createVirtualDisplay(
"screenCapture",
width,
height,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader.surface,
null,
null
)
}
}
private fun saveBitmap(bitmap: Bitmap, path: String) {
try {
val fop = FileOutputStream(path)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fop)
fop.flush()
fop.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
注意:
- 如果弹窗申请捕获屏幕拒绝,
mProjectionManager.getMediaProjection(resultCode, data)
获取到的mediaprojection
就为null;
同时,如果Service还未启动或前台化,这里也会报错;
3.注意事项
网上说,如果不想服务前台化,或申请相关的权限,那么就把应用变成系统应用即manifes里面添加android:sharedUserId="android.uid.system"
,但是这对于正常开发不适用,所以我并未尝试。
3.附:录屏
class MediaProjectionScreenshotImpl(val context: Context) {
private val mProjectionManager: MediaProjectionManager =
context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
//录屏
private val mMediaRecorder: MediaRecorder by lazy {
initRecorder()
}
private var mMediaProjection: MediaProjection? = null
private fun initRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(context)
} else {
MediaRecorder()
}.apply {
//音频源
setAudioSource(MediaRecorder.AudioSource.MIC)
//视频源
setVideoSource(MediaRecorder.VideoSource.SURFACE)
//视频输出格式
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
//存储路径
val path =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).path + File.separator + "Video" + System.currentTimeMillis() + ".mp4"
setOutputFile(path)
setVideoSize(width, height)
//视频格式
setVideoEncoder(MediaRecorder.VideoEncoder.H264)
//音频格式
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
//帧率
setVideoFrameRate(24)
//视频清晰度
setVideoEncodingBitRate(5242880)
prepare()
}
}
private fun startRecorder() {
mMediaProjection?.let {
it.createVirtualDisplay(
"tinder_screen_recorder",
width,
height,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mMediaRecorder.surface,
null,
null
)
mMediaRecorder.start()
}
}
override fun stopRecord() {
Log.i(TAG, "stopRecord: ")
try {
mMediaRecorder.stop()
mMediaRecorder.reset()
} catch (e: Exception) {
e.printStackTrace()
}
release()
}
}
和截图一样,同样需要服务前台化,在onActivityResult后进行操作