本文将详细介绍如何在 Android 应用中实现屏幕录制功能,并将录制的视频保存到本地存储。我们将涵盖从权限获取到最终视频保存的完整流程,包括关键代码实现。
一、实现原理概述
Android 屏幕录制主要依赖以下几个核心组件:
- MediaProjection:获取屏幕内容的入口,出于安全和隐私的考虑,每次录制前,系统都会弹出一个对话框,明确请求用户的授权。
- MediaProjectionManager: 管理MediaProjection实例
- VirtualDisplay:虚拟显示设备,将屏幕内容投射到编码器
- 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推流到服务端实现录屏直播功能。