使用MediaRecorder+MediaProjection高效实现Android录屏

使用MediaRecorder+MediaProjection高效实现Android录屏

前言

在 Android 平台上,实现录屏主要有两种方式:MediaCodec + MediaMuxer 组合和 MediaRecorder。前者提供了高度的灵活性和自定义能力,但实现起来相对复杂,需要手动处理视频编码、音频编码和混流等过程。 而 MediaRecorder 则是一个高级的 API,它封装了底层的音视频录制和编码过程,能显著减少开发工作量,同时保持良好的性能和兼容性。

MediaProjection是Android 5.0(API 21)引入的系统级服务,它解决了屏幕录制的核心难题------如何安全、高效地获取屏幕内容,为录屏功能提供了合法的、面向应用的标准化接口。

本文介绍如何使用MediaRecorder+MediaProjection高效实现Android录屏。

MediaRecorder 的核心:生命周期状态机

使用MediaRecorder要理解 MediaRecorder 的工作原理,要掌握它的生命周期状态。MediaRecorder 的状态转换是严格的,任何不正确的操作都可能导致异常。

理解 MediaRecorder 的工作原理,首先要掌握它的生命周期状态。MediaRecorder 的状态转换是严格的,任何不正确的操作都可能导致异常。

1.初始状态(Initial):这是 MediaRecorder 刚被创建时的状态。你可以通过 new MediaRecorder() 或 reset() 方法进入此状态。

2.设置状态(Configuring):在这个状态,你可以调用一系列 set... 方法来配置录制参数,例如:

setAudioSource(MediaRecorder.AudioSource.MIC):设置音频来源。

setVideoSource(MediaRecorder.VideoSource.SURFACE):设置视频来源为 Surface。

setOutputFormat(MediaRecorder.OutputFormat.MPEG_4):设置输出格式。

setVideoEncoder(MediaRecorder.VideoEncoder.H264):设置视频编码器。

setOutputFile(filePath):设置录制文件的路径。 在所有参数都设置完毕后,调用 prepare() 方法。

3.准备状态(Prepared):调用 prepare() 后进入此状态。MediaRecorder 会进行内部检查和资源分配,确保一切都准备就绪,可以开始录制。

4.录制状态(Recording):调用 start() 方法后进入此状态。此时,MediaRecorder 会开始从音视频源捕获数据、编码并写入文件。这是录制过程的核心状态。

5.停止状态(Stopped):调用 stop() 方法后进入此状态。MediaRecorder 会停止录制,完成文件写入和封装,并释放部分资源。注意,一旦进入此状态,你就无法在同一个文件中继续录制。

6.错误状态(Error):如果在任何状态发生错误,MediaRecorder 都会进入此状态,并抛出 RuntimeException。

reset():将 MediaRecorder 恢复到初始状态,你可以重新配置并开始新的录制。

release():完全释放 MediaRecorder 占用的所有资源。一旦调用,此对象将无法再使用。

MediaRecorder 的设计思想是易用性,它将复杂的音视频编码和封装过程抽象化,提供了一个简单的接口。

MediaProjection核心特性

1.用户授权机制:必须通过系统弹窗获得用户明确授权,确保隐私安全

2.虚拟显示支持:创建VirtualDisplay将屏幕内容重定向到Surface

3.系统级集成:直接与显示系统交互,避免root权限需求

相比传统的root方案或ADB方案,MediaProjection提供了合法的、面向应用的标准化接口,让录屏功能可以上架正规应用商店。

打造你的录屏应用

权限申请与用户授权

录屏是一个耗时操作,为了防止应用被系统回收,应该使用前台服务来运行录制任务,并在通知栏显示录制状态。

我们创建一个前台服务RecordingService用于录屏,首先在 AndroidManifest.xml 中声明 RECORD_AUDIO 和 FOREGROUND_SERVICE 等权限。

xml 复制代码
<manifest ...>

    <application
    ...
        <service
            android:name=".services.RecordingService"
            android:foregroundServiceType="mediaProjection"
            android:enabled="true"
            android:exported="false"></service>

    </application>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
</manifest>

然后我们在Compose函数中使用 MediaProjectionManager 和rememberLauncherForActivityResult获取屏幕捕获权限。这一步会向用户弹出一个系统对话框,请求用户授权。

kotlin 复制代码
    // 1. 定义 ActivityResultLauncher
    //    它接收一个 Intent 作为输入,并返回一个 ActivityResult 对象
    val screenCaptureLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult()
    ) { result ->
        val activity=context.findActivity()
        if (result.resultCode == Activity.RESULT_OK && result.data != null) {
            // 用户同意录屏,启动录屏服务
            viewModel.startRecordingService(activity,result.resultCode, result.data!!)
            Toast.makeText(activity, activity.getString(R.string.start), Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(activity, activity.getString(R.string.rejected), Toast.LENGTH_SHORT).show()
        }
    }

    // 2. 启动录屏请求的函数
    val startScreenCaptureRequest = remember<(Context) -> Unit> {
        { ctx ->
            // 获取 MediaProjectionManager
            val projectionManager = ctx.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

            // 创建屏幕捕获意图
            val intent = projectionManager.createScreenCaptureIntent()
            // 使用 Compose 启动器启动意图
            screenCaptureLauncher.launch(intent)
        }
    }

    ...
    //请求录屏
    startScreenCaptureRequest(context) 
    ...

viewModel中的startRecordingService负责启动一个前台录屏服务。

kotlin 复制代码
    fun startRecordingService(context: Context, resultCode: Int, data: Intent) {
        val serviceIntent = Intent(context, RecordingService::class.java).apply {
            action = "ACTION_START"
            putExtra("resultCode", resultCode)
            putExtra("data", data)
        }
        ContextCompat.startForegroundService(context, serviceIntent)
        context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE) // 绑定Service
        binder_flag=true
    }

录屏的核心:前台服务RecordingService

录屏的核心逻辑在RecordingService中。

我们在RecordingService中的onStartCommand处理录屏请求、创建通知、注册录屏结束后的回调函数,最后才启动录屏任务。这个顺序很重要,否则无法顺利使用MediaProjection获取屏幕数据。

kotlin 复制代码
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (intent != null) {
            when (intent.action) {
                "ACTION_START" -> {
                    if (isRecording) return START_NOT_STICKY
                    val resultCode = intent.getIntExtra("resultCode", Activity.RESULT_CANCELED)
                    // 使用新的、类型安全的 getParcelableExtra 方法
                    val data = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                        intent.getParcelableExtra("data", Intent::class.java)
                    } else {
                        @Suppress("DEPRECATION")
                        intent.getParcelableExtra("data")
                    }

                    if (resultCode != Activity.RESULT_OK || data == null) {
                        Toast.makeText(this, "录屏权限被拒绝,无法启动服务", Toast.LENGTH_SHORT).show()
                        stopSelf()
                        return START_NOT_STICKY
                    }
                    // 确保通知渠道在创建通知前已存在
                    createNotificationChannel()
                    val notification = createRecordingNotification(this)//createNotification()
                    startForeground(notificationId, notification)
                    // 2. 注册匿名回调对象 (更简洁!)
                    val recordingCallback = object : MediaProjection.Callback() {
                        override fun onStop() {
                            super.onStop()
                            // 在用户/系统撤销权限时,执行清理逻辑
                            // 注意:我们必须手动注销它,尽管 stop() 会释放资源,
                            // 但最好在 onStop() 被调用后,立即执行清理。
                            mediaProjection?.unregisterCallback(this)
                        }
                    }
                    mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data)
                    // 注册回调,使用当前线程的 Handler (null)
                    mediaProjection?.registerCallback(recordingCallback, null)
                    startRecording()
                }
                "ACTION_STOP" -> {
                    stopRecording()
                }

            }
        }
        return START_NOT_STICKY
    }

我们在startRecording()中配置MediaRecorder,设置视频源、音频源、输出格式、编码器、分辨率等参数,并将 MediaProjection 捕获到的屏幕数据,通过 Surface 传递给 MediaRecorder。

kotlin 复制代码
    fun startRecording() {
        try {

            // 配置 MediaRecorder
            mediaRecorder =
                if(Build.VERSION.SDK_INT >=Build.VERSION_CODES.S) {
                    MediaRecorder(this)
                }
                else{
                    MediaRecorder()
                }
                .apply {
                if(recordAudioType==1){
                    setAudioSource(MediaRecorder.AudioSource.MIC)
                    setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                }

                setVideoSource(MediaRecorder.VideoSource.SURFACE)
                setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)

                if(mediaUri!=null){
                    pfd = contentResolver.openFileDescriptor(mediaUri!!, "w")
                    if (pfd != null) {
                        setOutputFile(pfd!!.fileDescriptor)
                    }
                }
                else{
                    setOutputFile(filePath)
                }

                // 视频配置
                setVideoSize(VIDEO_WIDTH, VIDEO_HEIGHT) // 示例分辨率
                setVideoEncoder(MediaRecorder.VideoEncoder.H264)
                setVideoEncodingBitRate(VIDEO_BIT_RATE) // 5 Mbps
                setVideoFrameRate(VIDEO_FRAME_RATE)
                prepare()
            }

            // 创建虚拟显示器
            val displayMetrics = resources.displayMetrics
            val densityDpi = displayMetrics.densityDpi
            virtualDisplay = mediaProjection?.createVirtualDisplay(
                "RecordingDisplay",
                VIDEO_WIDTH, VIDEO_HEIGHT, // 与 MediaRecorder 视频尺寸一致
                densityDpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                mediaRecorder?.surface,
                null,
                null
            )

            mediaRecorder?.start()
            Log.d("RecordingService", "Recording started!")
            isRecording=true
        } catch (e: IOException) {
            Log.e("RecordingService", "startRecording failed", e)
            stopRecording()
        }
    }

最后我们不要忘记释放资源的操作。

kotlin 复制代码
    private fun stopRecording() {
        isRecording=false
        mediaRecorder?.apply {
            stop()
            reset()
            release()
        }
        pfd?.close()
        pfd=null
        mediaRecorder = null

        virtualDisplay?.release()
        virtualDisplay = null

        mediaProjection?.stop()
        mediaProjection = null

        // 1. 解除前台状态
        // 使用 STOP_FOREGROUND_REMOVE 标志,表示移除前台状态的同时,也移除状态栏通知。
        // 这是最常见且完整的解除前台状态操作。
        ServiceCompat.stopForeground(
            /* service = */ this,
            /* flags = */ ServiceCompat.STOP_FOREGROUND_REMOVE
        )
        stopSelf()
        Log.d("RecordingService", "Recording stopped.")
    }

    override fun onDestroy() {
        super.onDestroy()
        stopRecording()
    }

通过以上操作,我们就完成了一个完整的录屏功能。

总结

MediaRecorder+MediaProjection这套组合 是 Android 平台上实现录屏功能的利器。它们通过封装复杂的底层实现,让开发者能够以更少的代码完成高质量的录制任务。完整的示例代码可以在音视频编辑器的录屏功能部分找到(代码写得比较潦草)。

参考

1.Media projection谷歌官方教程

2.MediaRecorder 概览

相关推荐
叶羽西1 小时前
Android15系统中(娱乐框架和车机框架)中对摄像头的朝向是怎么定义的
android
Java小白中的菜鸟1 小时前
安卓studio链接夜神模拟器的一些问题
android
莫比乌斯环1 小时前
【Android技能点】深入解析 Android 中 Handler、Looper 和 Message 的关系及全局监听方案
android·消息队列
编程之路从0到11 小时前
React Native新架构之Android端初始化源码分析
android·react native·源码阅读
行稳方能走远1 小时前
Android java 学习笔记2
android·java
编程之路从0到12 小时前
React Native 之Android端 Bolts库
android·前端·react native
爬山算法2 小时前
Hibernate(38)如何在Hibernate中配置乐观锁?
android·java·hibernate
行稳方能走远2 小时前
Android java 学习笔记 1
android·java
zhimingwen2 小时前
【開發筆記】修復 macOS 上 JADX 啟動崩潰並實現快速啟動
android·macos·反編譯