使用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 概览

相关推荐
且白2 小时前
uniapp接入安卓端极光推送离线打包
android·uni-app
YoungP2 小时前
让人头疼的AndroidStudio、Gradle、AGP..
android
我命由我123453 小时前
Android WebView - loadUrl 方法的长度限制
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Coffeeee3 小时前
面试被问到Compose的副作用不会,只怪我没好好学
android·kotlin·android jetpack
Greenland_123 小时前
Android Gralde补全计划 productFlavors多渠道打包(变体/多客户)
android
Just_Paranoid3 小时前
【TaskStackListener】Android 中用于监听和响应任务栈
android·ams·task·taskstack
权泽谦3 小时前
从零搭建一个 PHP 登录注册系统(含完整源码)
android·开发语言·php
aaajj4 小时前
android contentprovider及其查看
android
fundroid10 小时前
Android Studio + Gemini:重塑安卓 AI 开发新范式
android·android studio·ai编程