IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android MediaProjection截屏&录屏-适配AndroidQ以上版本 -> 正文阅读

[移动开发]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 截屏和录屏

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-09-30 01:04:20  更:2022-09-30 01:06:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 21:34:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码