一、项目介绍
在 android 5.0(api 21)及以上版本,系统提供了 mediaprojection api,允许应用在用户授权下录制屏幕内容并输出到视频文件。基于此,我们可以实现一个 一键录屏 功能,无需 root,也无需系统签名。完整功能包含:
申请录屏权限:使用 mediaprojectionmanager 触发系统授权对话框
屏幕采集与编码:结合 mediaprojection、virtualdisplay 与 mediarecorder(或 mediacodec + muxer)进行音视频同步录制
后台服务管理:将录屏流程封装在 service 或 foreground service 中,确保在应用切后台时仍可录制
用户交互:在应用内通过按钮控制录屏的启动、暂停、停止,并提供录制时长实时显示
文件存储与管理:自动将录制文件保存到外部存储或 mediastore,支持列表预览与播放
性能与兼容:兼容不同分辨率、适配屏幕方向变化,优化视频码率与帧率,控制录屏对系统性能的影响
二、相关技术与原理
| 技术 | 功能 |
|---|---|
| mediaprojection | 提供屏幕内容采集权限,输出到 virtualdisplay |
| virtualdisplay | 将屏幕内容镜像到指定 surface(例如 mediarecorder 的输入) |
| mediarecorder | 原生音视频录制 api,简化同步编码、封装 av 文件 |
| mediacodec + muxer | 手动编码与封装,获得更细粒度控制(可选高级方案) |
| service | 后台管理录屏任务,结合前台服务确保录制不中断 |
| mediastore | android 10+ 存储协议,安全写入公共相册 |
| notification | 前台服务需在通知栏持续显示录制状态 |
api 要求:最早 api 21,建议应用最小支持 api 23 以上,以便简化外部存储和权限管理。
编解码参数:典型分辨率与帧率组合:1080×1920@30fps、720×1280@30fps;视频码率 4–8 mbps;音频码率 128 kbps;
三、系统权限与用户授权
manifest 权限
<uses-permission android:name="android.permission.foreground_service"/> <uses-permission android:name="android.permission.write_external_storage"/> <uses-permission android:name="android.permission.record_audio"/>
运行时授权
android 6.0+ 需请求存储与麦克风权限
通过 mediaprojectionmanager.createscreencaptureintent() 发起系统录屏授权对话框
四、项目架构与流程
用户点击“开始录制”按钮
│
▼
mainactivity 发起录屏授权 intent
│
▼
onactivityresult 收到授权 result_ok & data intent
│
▼
启动 screenrecordservice(前台服务),传入 data
│
▼
service 中:
├─ 初始化 mediarecorder
├─ 获取 mediaprojection
├─ 创建 virtualdisplay,surface 指向 mediarecorder
├─ 调用 mediarecorder.start()
│
▼
循环录制屏幕与麦克风数据
│
▼
用户点击“停止录制”
│
▼
service 调用 mediarecorder.stop(), reset(), release()
│
└─ 通知主进程录制完成,保存文件到 mediastore
五、环境配置与依赖
// app/build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
}
android {
compilesdk 34
defaultconfig {
applicationid "com.example.screenrecorder"
minsdk 23
targetsdk 34
}
buildfeatures { viewbinding true }
}
dependencies {
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.core:core-ktx:1.10.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1"
}六、完整代码整合
// =======================================================
// 文件:androidmanifest.xml
// 描述:权限申请与 service 声明
// =======================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.screenrecorder">
<uses-permission android:name="android.permission.foreground_service"/>
<uses-permission android:name="android.permission.write_external_storage"/>
<uses-permission android:name="android.permission.record_audio"/>
<application
android:name=".app"
android:theme="@style/theme.screenrecorder">
<activity android:name=".mainactivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.main"/>
<category android:name="android.intent.category.launcher"/>
</intent-filter>
</activity>
<service android:name=".screenrecordservice"
android:exported="false"/>
</application>
</manifest>
// =======================================================
// 文件:app.kt
// 描述:application,用于初始化通知渠道
// =======================================================
package com.example.screenrecorder
import android.app.application
import android.app.notificationchannel
import android.app.notificationmanager
import android.os.build
class app : application() {
companion object { const val channel_id = "screen_recorder_channel" }
override fun oncreate() {
super.oncreate()
if (build.version.sdk_int >= build.version_codes.o) {
notificationmanager::class.java
.getsystemservice(notificationmanager::class.java)
.createnotificationchannel(
notificationchannel(
channel_id,
"screen recorder service",
notificationmanager.importance_low
)
)
}
}
}
// =======================================================
// 文件:res/values/themes.xml
// 描述:应用主题
// =======================================================
<resources>
<style name="theme.screenrecorder" parent="theme.materialcomponents.daynight.noactionbar"/>
</resources>
// =======================================================
// 文件:res/layout/activity_main.xml
// 描述:主界面,控制录制与播放
// =======================================================
<?xml version="1.0" encoding="utf-8"?>
<framelayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="match_parent"
android:padding="24dp">
<linearlayout
android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="center">
<button
android:id="@+id/btnstart"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="开始录制"/>
<button
android:id="@+id/btnstop"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:text="停止录制"
android:layout_margintop="16dp"
android:enabled="false"/>
<textview
android:id="@+id/tvpath"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_margintop="16dp"
android:text="录制路径:"/>
</linearlayout>
</framelayout>
// =======================================================
// 文件:mainactivity.kt
// 描述:申请权限、发起录屏授权、启动/停止 service
// =======================================================
package com.example.screenrecorder
import android.app.activity
import android.content.intent
import android.media.projection.mediaprojectionmanager
import android.os.bundle
import androidx.activity.result.contract.activityresultcontracts
import androidx.appcompat.app.appcompatactivity
import androidx.core.app.activitycompat
import com.example.screenrecorder.databinding.activitymainbinding
class mainactivity : appcompatactivity() {
private lateinit var b: activitymainbinding
private lateinit var projectionmanager: mediaprojectionmanager
private var resultcode = activity.result_canceled
private var resultdata: intent? = null
private val reqstorage = registerforactivityresult(
activityresultcontracts.requestpermission()
) { granted ->
if (!granted) finish()
}
private val reqscreen = registerforactivityresult(
activityresultcontracts.startactivityforresult()
) { res ->
if (res.resultcode == activity.result_ok) {
resultcode = res.resultcode
resultdata = res.data
startservice()
}
}
override fun oncreate(s: bundle?) {
super.oncreate(s)
b = activitymainbinding.inflate(layoutinflater)
setcontentview(b.root)
projectionmanager = getsystemservice(media_projection_service)
as mediaprojectionmanager
reqstorage.launch(android.manifest.permission.write_external_storage)
b.btnstart.setonclicklistener {
reqscreen.launch(projectionmanager.createscreencaptureintent())
}
b.btnstop.setonclicklistener {
stopservice(intent(this, screenrecordservice::class.java))
}
}
private fun startservice() {
val intent = intent(this, screenrecordservice::class.java).apply {
putextra("code", resultcode)
putextra("data", resultdata)
}
startservice(intent)
b.btnstart.isenabled = false
b.btnstop.isenabled = true
}
}
// =======================================================
// 文件:screenrecordservice.kt
// 描述:前台 service,管理 mediarecorder 与 mediaprojection
// =======================================================
package com.example.screenrecorder
import android.app.notification
import android.app.pendingintent
import android.app.service
import android.content.intent
import android.hardware.display.displaymanager
import android.hardware.display.mediaprojection
import android.hardware.display.mediaprojectionmanager
import android.hardware.display.virtualdisplay
import android.media.mediarecorder
import android.os.*
import android.provider.mediastore
import androidx.core.app.notificationcompat
import java.io.file
import java.text.simpledateformat
import java.util.*
class screenrecordservice : service() {
private lateinit var recorder: mediarecorder
private var projection: mediaprojection? = null
private var virtualdisplay: virtualdisplay? = null
private lateinit var projectionmanager: mediaprojectionmanager
private lateinit var outputfile: string
override fun oncreate() {
super.oncreate()
projectionmanager = getsystemservice(media_projection_service)
as mediaprojectionmanager
recorder = mediarecorder()
}
override fun onstartcommand(intent: intent?, flags: int, startid: int): int {
val code = intent?.getintextra("code", -1) ?: -1
val data = intent?.getparcelableextra<intent>("data")
if (code != -1 && data != null) {
initrecorder()
projection = projectionmanager.getmediaprojection(code, data)
virtualdisplay = projection?.createvirtualdisplay(
"screenrecorder",
recorder.videowidth, recorder.videoheight, resources.displaymetrics.densitydpi,
displaymanager.virtual_display_flag_auto_mirror,
recorder.surface, null, null
)
recorder.start()
startforeground(1, buildnotification())
}
return start_not_sticky
}
private fun initrecorder() {
val metrics = resources.displaymetrics
val width = metrics.widthpixels
val height = metrics.heightpixels
recorder.apply {
setaudiosource(mediarecorder.audiosource.mic)
setvideosource(mediarecorder.videosource.surface)
setoutputformat(mediarecorder.outputformat.mpeg_4)
outputfile = createoutputfile()
setoutputfile(outputfile)
setvideoencoder(mediarecorder.videoencoder.h264)
setaudioencoder(mediarecorder.audioencoder.aac)
setvideosize(width, height)
setvideoframerate(30)
setvideoencodingbitrate(8_000_000)
setorientationhint(0)
prepare()
}
}
private fun createoutputfile(): string {
val sd = file(getexternalfilesdir(null), "recordvideos")
if (!sd.exists()) sd.mkdirs()
val name = "scr_${simpledateformat("yyyymmdd_hhmmss",
locale.us).format(date())}.mp4"
return file(sd, name).absolutepath
}
private fun buildnotification(): notification {
val pi = pendingintent.getactivity(
this,0,
intent(this, mainactivity::class.java), pendingintent.flag_immutable
)
return notificationcompat.builder(this, app.channel_id)
.setcontenttitle("正在录屏")
.setsmallicon(r.drawable.ic_record)
.setcontentintent(pi)
.setongoing(true)
.build()
}
override fun ondestroy() {
super.ondestroy()
recorder.stop()
recorder.reset()
virtualdisplay?.release()
projection?.stop()
// 通知系统图库更新文件
sendbroadcast(intent(intent.action_media_scanner_scan_file).apply {
data = android.net.uri.fromfile(file(outputfile))
})
}
override fun onbind(intent: intent?) = null
}
// =======================================================
// 文件:res/drawable/ic_record.xml
// 描述:录制状态图标(示例可用系统资源)
// =======================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp" android:viewportwidth="24"
android:viewportheight="24">
<path
android:fillcolor="#f44336"
android:pathdata="m12,3c7.03,3 3,7.03 3,12s4.03,9 9,9 9,-4.03 9,-9s16.97,3 12,3zm12,19c-3.86,0 -7,-3.14 -7,-7s3.14,-7 7,-7 7,3.14 7,7 -3.14,7 -7,7z"/>
<circle
android:fillcolor="#f44336"
android:cx="12" android:cy="12" android:r="5"/>
</vector>七、代码解读
1.mainactivity
- 通过 mediaprojectionmanager 发起录屏授权,使用 activityresultcontracts.startactivityforresult 回调获取授权 intent 与结果码;
- 请求存储与录音权限,授权后调用 startservice() 启动 screenrecordservice;
2.screenrecordservice
- 在 onstartcommand() 中根据授权信息获取 mediaprojection;
- 调用 mediarecorder 完成音视频同步编码配置,并通过 mediaprojection.createvirtualdisplay() 将屏幕内容输送到 mediarecorder.getsurface();
- 使用 前台 service(startforeground())提高存活优先级,在通知栏显示录制状态;
- 在 ondestroy() 中停止录制并释放资源,并发送广播刷新系统图库。
3.mediarecorder 配置
- setvideosource(mediarecorder.videosource.surface):将虚拟屏幕 surface 作为视频帧输入;
- 音频源使用麦克风 audiosource.mic;
- 视频编码器 h.264,音频编码器 aac;
- 分辨率动态获取当前屏幕分辨率,帧率 30 fps,码率 8 mbps;
4.文件存储
- 保存到应用私有外部存储 getexternalfilesdir("recordvideos"),android 10+ 可换成 mediastore 接口;
- 录制完成后广播 action_media_scanner_scan_file,让系统图库立即识别新文件。
5.通知与图标
- 使用 ic_record.xml 简单绘制录制状态图标,也可替换为自己的矢量资源;
- 通知必须传入前台服务渠道 channel_id,并设置 setongoing(true) 锁定通知。
八、性能优化与兼容性考虑
分辨率与码率自适应:在高端设备上可录制 1080p 或更高分辨率;在低端或后台时可降级到 720p 以降低 cpu 负载;
动态暂停与恢复:通过 mediarecorder.pause() / resume()(api 24+)实现录制的中断与续录,防止录屏过长文件过大;
存储位置选择:根据 android 10+ scoped storage 模式改用 mediastore.video.media 插入,可让用户在公共相册中看到录制视频;
录制时长限制:在 service 中使用 handler 配合定时逻辑自动停止录制,避免用户忘记关闭;
屏幕方向与旋转处理:使用 setorientationhint() 传入正确的旋转角度,保证录制后视频方向正确;可动态读取 display.getrotation() 值;
多路屏幕录制:如果需要同时录制应用内部视图和全屏,可结合 surfaceview + mediacodec 自定义编码合流;
九、扩展功能
预览与分享:在录制完成后,跳转到 播放页面 预览视频,并提供分享到微信、qq、抖音等链接;
水印与滤镜:在 virtualdisplay 或 mediacodec 编码前,使用 opengl es 在录制帧上叠加文字与图片水印,或添加滤镜效果;
画中画:在录制主屏幕的同时,叠加前置摄像头小窗口,实现游戏解说或教学场景;
轨迹标记:支持用户在录制时绘制屏幕内容,如手绘箭头、圈注关键点,适用于教学与演示;
云端同步:录制完成后自动上传到 oss/s3/腾讯云等对象存储,结合短视频处理平台进行后续剪辑与分发。
十、项目总结与落地建议
本文基于 mediaprojection + mediarecorder 实现了一个完整、可扩展的 android 录屏功能,涵盖:
系统授权与运行时权限管理
前台 service 保证长期录制不中断
高质量音视频同步编码与文件保存
录制状态通知与系统相册更新
在实际项目中,可结合应用业务场景,增添更多人性化功能与企业级需求,如录制回放剪辑、云端上传、画中画、手势标注等,打造更具竞争力的录屏产品。
十一、常见问题解答(faq)
q1:为什么录制内容全黑或黑屏?
需在 mediarecorder 配置好 setvideosource(mediarecorder.videosource.surface) 后再调用 prepare()。
检查是否正确调用 projection.createvirtualdisplay() 并传入 recorder.getsurface()。
q2:录制文件巨大怎么办?
降低视频码率,或增加压缩率。使用高效编码器(h.265)需自行集成 mediacodec + mediamuxer。
q3:如何实现暂停和续录?
android n(api 24)以上,mediarecorder.pause() 与 resume() 可控制录制中断;
旧版本则需停止当前录制并启动下一段分片,后期合并分片文件。
q4:录制静音或无声?
检查麦克风是否被 setaudiosource() 正确调用,应用是否拥有 record_audio 权限;
部分设备后台录音需在 audioattributes 中指定前台模式。
q5:录制过程中怎样显示时长?
在 service 中启动定时器(handler.postdelayed()),定期通过通知或广播更新时长到 ui。
到此这篇关于android实现简单的录屏功能(附源码)的文章就介绍到这了,更多相关android录屏内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论