我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android MediaProjection截屏&录屏-适配AndroidQ以上版本

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android MediaProjection截屏&录屏-适配AndroidQ以上版本

        工作中遇到截屏需求,首先想到的肯定是截图所在区域的控件,通过Canvas类将View绘制成一个Bitmap,之后是要显示还是保存都可以了。但是事实上还是有一些问题存在,已知有两个问题:①不能截取到状态栏的内容吧;② 如果页面存在视频播放器,那么无法获取到播放器视频画面吧。

        使用系统MediaProjection就可以解决上述两个问题。

Demo地址:https://download.csdn.net/download/bigfc/86711553

一、截屏

首先,看下最后的实现效果:

device-2022-09-24-182332

具体的实现步骤:

申请权限&注册前台服务

                                    ....    

在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)    }}

点击截屏的时候通过MediaProjectionManager创建截屏Intent并启动

//截屏点击事件    fun capture(view: View) {        mScreenShortService?.let {            //开始截屏            mediaManager =                getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager            mediaManager.createScreenCaptureIntent().apply {                startActivityForResult(this, CAPTURE_CODE)            }        }    }

监听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 -> {                   ...                }            }        }    }

这里必须先申请成为前台服务,否则会报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)    }

成功获取到令牌后,就可以通过监听获取有效的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            )        }    }

最后读取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

具体实现步骤如下:

除了截屏需要前台服务权限和Service,录屏还需要存储和录音权限

                                        ...    

服务绑定同截屏

点击录屏此时需要动态申请录音、存储权限

获得权限后,同样需要通过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)                    }                }            }        }    }

申请前台服务、申请令牌、配置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)    }

配置MediaRecorder

    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)    }

此时已经配置的文件路径中就可以找到录屏的文件了,如下图:


最后 无论是录屏还是截屏都需要释放资源,这里不再列举了


异常情况

java.lang.SecurityException: Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION

此问题是因为Android Q开始,使用中MediaProjection时必须申请一个前台服务,并且开启一个通知,用于提醒用户应用正在捕获屏幕信息,无论是服务开启的顺序错误还是未开启通知都会报这个异常。

RuntimeException: setAudioSource failed异常

录屏是需要动态申请录音权限,权限未申请通过会出现这个错误

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 截屏和录屏

来源地址:https://blog.csdn.net/bigfc/article/details/127028698

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android MediaProjection截屏&录屏-适配AndroidQ以上版本

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

【Android】app应用内版本更新升级(DownloadManager下载,适配Android6.0以上所有版本)

目录 前言一、实现思路二、服务端接口三、UI页面三、工具类实现1.检查版本号2.下载apk3.安装apk4.实时更新下载进度5.完整代码 三、外部使用总结 前言 版本的升级和更新是一个线上App所必备的功能,App的升级安
2023-08-19

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录