Android 屏幕录制与本地保存完整实现指南

本文将详细介绍如何在 Android 应用中实现屏幕录制功能,并将录制的视频保存到本地存储。我们将涵盖从权限获取到最终视频保存的完整流程,包括关键代码实现。

一、实现原理概述

Android 屏幕录制主要依赖以下几个核心组件:

  1. MediaProjection:获取屏幕内容的入口,出于安全和隐私的考虑,每次录制前,系统都会弹出一个对话框,明确请求用户的授权。
  2. MediaProjectionManager: 管理MediaProjection实例
  3. VirtualDisplay:虚拟显示设备,将屏幕内容投射到编码器
  4. MediaRecorder: 负责录制和编码

由于屏幕录制通常是持续性任务,即使用户切换到其他应用或返回桌面,录制也应继续。因此,我们必须将录制逻辑放置在前台服务 (Foreground Service) 中。 这不仅能防止我们的应用在后台被系统终止,还能通过一个持续的通知告知用户,屏幕正在被录制,保证了操作的透明性。

环境准备

1.配置 Manifest 文件

js 复制代码
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 运行前台服务的必要权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- 声明用于屏幕录制的 Service --> 
<service android:name=".ScreenCaptureService"
    android:exported="false"
    android:foregroundServiceType="mediaProjection"/>

2.请求用户授权

我们无法直接请求屏幕捕获权限。相反,我们必须通过 MediaProjectionManager 创建一个 Intent,然后启动这个 Intent 来显示一个系统对话框。

js 复制代码
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

// 使用 ActivityResultLauncher 来处理返回结果
val screenCaptureLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val serviceIntent = Intent(this, ScreenCaptureService::class.java).apply {
            action = "START"
            putExtra("resultCode", result.resultCode)
            putExtra("data", result.data)
        }
        startForegroundService(serviceIntent)
    } else {
        // 用户拒绝了授权
        Toast.makeText(this, "需要屏幕捕获权限才能录制", Toast.LENGTH_SHORT).show()
    }
}

// 点击录屏按钮调用
fun startScreenCapture() {
    screenCaptureLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
}

3.创建并实现前台服务

kotlin 复制代码
class ScreenCaptureService : Service() {

    private lateinit var mediaProjection: MediaProjection
    private lateinit var virtualDisplay: VirtualDisplay
    private lateinit var mediaRecorder: MediaRecorder
    private lateinit var callBack:MediaProjection.Callback

    private var currentVideoUri: Uri? = null

    companion object {
        const val RESULT_CODE = "resultCode"
        const val RESULT_DATA = "resultData"
        const val NOTIFICATION_ID = 1001
        const val CHANNEL_ID = "screen_record_channel"
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val resultCode = intent?.getIntExtra(RESULT_CODE, 0) ?: 0
        val resultData = intent?.getParcelableExtra<Intent>(RESULT_DATA)

        if (resultCode != 0 && resultData != null) {
            startRecording(resultCode, resultData)
        }

        return START_STICKY
    }

    private fun startRecording(resultCode: Int, resultData: Intent) {
        //创建通知并启动前台服务
        startForeground(NOTIFICATION_ID, createNotification())

        // 获取mediaProjection实例
        val projectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        mediaProjection = projectionManager.getMediaProjection(resultCode, resultData)
        val fileName = "ScreenRecord_${System.currentTimeMillis()}.mp4"
        // 配置 MediaRecorder,设置视频源、输出格式、编码器、文件路径等。
        mediaRecorder = MediaRecorder().apply {
            setVideoSource(MediaRecorder.VideoSource.SURFACE)
            setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
            setOutputFile(getOutputFileDescriptor(applicationContext,fileName))
            setVideoEncoder(MediaRecorder.VideoEncoder.H264)
            setVideoSize(1080, 1920) // 根据实际需求调整
            setVideoFrameRate(30)
            prepare()
        }

        callBack = object : MediaProjection.Callback() {
            override fun onStop() {

            }
        }

        // 注册回调
        mediaProjection.registerCallback(callBack, null)

        // 创建一个虚拟显示 (VirtualDisplay),并将其渲染的画面输出到 MediaRecorder 的 Surface 上
        virtualDisplay = mediaProjection.createVirtualDisplay(
            "ScreenRecorder",
            1080, 1920, resources.displayMetrics.densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
            mediaRecorder.surface, null, null
        )

        // 开始录制
        mediaRecorder.start()
    }

    private fun createNotification(): Notification {
        createNotificationChannel()

        return NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("屏幕录制中")
            .setContentText("正在录制您的屏幕操作")
            .setSmallIcon(R.mipmap.ic_launcher)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .build()
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "屏幕录制",
                NotificationManager.IMPORTANCE_LOW
            ).apply {
                description = "屏幕录制服务正在运行"
            }

            (getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
                .createNotificationChannel(channel)
        }
    }

    // 设置视频保存路径
    private fun getOutputFileDescriptor(context: Context, fileName: String): FileDescriptor? {
        val contentValues = ContentValues().apply {
            put(MediaStore.Video.Media.DISPLAY_NAME, fileName)
            put(MediaStore.Video.Media.MIME_TYPE, "video/mp4")
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/")
                put(MediaStore.Video.Media.IS_PENDING, 1)
            }
        }

        val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
        val itemUri = context.contentResolver.insert(collection, contentValues)

        currentVideoUri = itemUri

        return if (itemUri != null) {
            context.contentResolver.openFileDescriptor(itemUri, "w")?.fileDescriptor
        } else {
            null
        }
    }

    override fun onDestroy() {
        mediaProjection.unregisterCallback(callBack)
        super.onDestroy()
        stopRecording()
    }

     // 停止录制并释放资源
    private fun stopRecording() {
        mediaRecorder.apply {
            stop()
            reset()
            release()
        }

        virtualDisplay.release()

        if (::mediaProjection.isInitialized) {
            mediaProjection.stop()
        }
        // 将录制的视频保存到本地
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && currentVideoUri != null) {
            val contentValues = ContentValues().apply {
                put(MediaStore.Video.Media.IS_PENDING, 0)
            }
            contentResolver.update(currentVideoUri!!, contentValues, null, null)
        }
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

总结

本文利用Android屏幕录制API完成了基本的屏幕录制功能,后续还可以结合音视频编码将屏幕录制的数据利用RTMP推流到服务端实现录屏直播功能。

相关推荐
YF02111 天前
Protobuf与 gRPC 的关系:从理论到 Android + Go 实战通信全解析
android·后端·grpc
YF02111 天前
Android 卡顿性能优化专项治理:从 ANR 根源到系统性重构实践
android·app
蒙奇·D·路飞-1 天前
Kotlin安卓app版本自动升级设计实现
android
博客zhu虎康1 天前
小程序按钮实现先表单校验再走手机号获取功能
android·javascript·小程序
码途漫谈1 天前
Easy-Vibe高级开发篇阅读笔记(十三)——多平台开发之Android App 原生开发
android·人工智能·笔记·ai·开源·ai编程
街灯L1 天前
【ADB】使用ADB工具箱卸载安卓系统软件
android·adb
赏金术士1 天前
Kotlin 从入门到进阶 之泛型 模块(七)
android·开发语言·kotlin
恋猫de小郭1 天前
经典,Flutter iOS 又修复了一个构建问题,还是很抽象
android·前端·flutter
Kapaseker1 天前
不吹牛逼!精通 Compose 绘制(一)
android·kotlin
黄林晴1 天前
Android 终于做桌面了,而三星 DeX 早已把路趟完
android