Android截屏录屏MediaProjection分享

发布时间:2025-06-24 17:35:53  作者:北方职教升学中心  阅读量:846


Android截图方案使用MediaProjection截图

文章目录

  • Android截图方案使用MediaProjection截图
    • 1.简介
    • 2.具体使用
      • 2.1 声明Service
      • 2.2 发起屏幕捕获申请
      • 2.3 在onActivityResult中处理结果
      • 2.4 提前启动Service
        • 方案一:
        • 方案二:
        • 方案三:
      • 2.4 开始捕获屏幕
    • 3.注意事项
    • 3.附:录屏

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,并传递foregroundServiceTypeServiceInfo#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代码

classTinderService :Service(){overridefunonBind(intent:Intent?):IBinder?{returnnull}overridefunonCreate(){super.onCreate()Log.i(TAG,"onCreate: ")//前台化通知if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.O){valchannelId ="001"valchannelName ="screenshot"valchannel =NotificationChannel(channelId,channelName,NotificationManager.IMPORTANCE_NONE)channel.lightColor =Color.BLUE            channel.lockscreenVisibility =Notification.VISIBILITY_PRIVATE            valmanager =getSystemService(Context.NOTIFICATION_SERVICE)asNotificationManager?if(manager !=null){valactivityIntent:Intent =Intent(this,MainActivity::class.java                )activityIntent.setAction("stop")valcontentIntent =PendingIntent.getActivity(this,0,activityIntent,PendingIntent.FLAG_MUTABLE                )manager.createNotificationChannel(channel)valnotification: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_PROJECTIONstartForeground(1,notification,ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION                    )}else{startForeground(1,notification)}}}else{startForeground(1,Notification())}}companionobject{privateconstvalTAG ="TinderService"}}

不必多说,服务前台化是必要的;

Manifest声明:

<manifestxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><!-- 前台服务--><uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE"/><!-- 文件权限--><uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE"/><uses-permissionandroid:name="android.permission.READ_EXTERNAL_STORAGE"/><uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE"/><!-- 截屏如果不想变成系统应用,增加这个权限--><uses-permissionandroid:name="android.permission.READ_FRAME_BUFFER"tools:ignore="ProtectedPermissions"/><application><serviceandroid: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 开始捕获屏幕

privatefungetScreenshot(resultCode:Int,data:Intent){Log.i(TAG,"getScreenshot: ")mMediaProjection?.let{valimageReader =ImageReader.newInstance(width,height,PixelFormat.RGBA_8888,3)imageReader.setOnImageAvailableListener({Log.i(TAG,"onImageAvailable: ")//                val image: Image? = imageReader.acquireNextImage()valimage:Image?=imageReader.acquireLatestImage()image?.let{valwidth =image.width                    valheight =image.height                    valplanes =image.planes                    valbuffer =planes[0].buffer                    valpixelStride =planes[0].pixelStride                    valrowStride =planes[0].rowStride                    valrowPadding =rowStride -pixelStride *width                    valbitmap =Bitmap.createBitmap(width +rowPadding /pixelStride,height,Bitmap.Config.ARGB_8888                    )bitmap.copyPixelsFromBuffer(buffer)valfilePath =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)}}privatefunsaveBitmap(bitmap:Bitmap,path:String){try{valfop =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.附:录屏

classMediaProjectionScreenshotImpl(valcontext:Context){privatevalmProjectionManager:MediaProjectionManager =context.getSystemService(Context.MEDIA_PROJECTION_SERVICE)asMediaProjectionManager    //录屏privatevalmMediaRecorder:MediaRecorder bylazy {initRecorder()}privatevarmMediaProjection:MediaProjection?=nullprivatefuninitRecorder():MediaRecorder {returnif(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)//存储路径valpath =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()}}privatefunstartRecorder(){mMediaProjection?.let{it.createVirtualDisplay("tinder_screen_recorder",width,height,dpi,DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,mMediaRecorder.surface,null,null)mMediaRecorder.start()}}overridefunstopRecord(){Log.i(TAG,"stopRecord: ")try{mMediaRecorder.stop()mMediaRecorder.reset()}catch(e:Exception){e.printStackTrace()}release()}}

和截图一样,同样需要服务前台化,在onActivityResult后进行操作