使用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 平台上实现录屏功能的利器。它们通过封装复杂的底层实现,让开发者能够以更少的代码完成高质量的录制任务。完整的示例代码可以在音视频编辑器的录屏功能部分找到(代码写得比较潦草)。