Android實(shí)現(xiàn)一鍵錄屏功能(附源碼)
一、項(xiàng)目介紹
在 Android 5.0(API 21)及以上版本,系統(tǒng)提供了 MediaProjection API,允許應(yīng)用在用戶授權(quán)下錄制屏幕內(nèi)容并輸出到視頻文件?;诖耍覀兛梢詫?shí)現(xiàn)一個(gè) 一鍵錄屏 功能,無需 ROOT,也無需系統(tǒng)簽名。完整功能包含:
申請(qǐng)錄屏權(quán)限:使用 MediaProjectionManager 觸發(fā)系統(tǒng)授權(quán)對(duì)話框
屏幕采集與編碼:結(jié)合 MediaProjection、VirtualDisplay 與 MediaRecorder(或 MediaCodec + Muxer)進(jìn)行音視頻同步錄制
后臺(tái)服務(wù)管理:將錄屏流程封裝在 Service 或 Foreground Service 中,確保在應(yīng)用切后臺(tái)時(shí)仍可錄制
用戶交互:在應(yīng)用內(nèi)通過按鈕控制錄屏的啟動(dòng)、暫停、停止,并提供錄制時(shí)長實(shí)時(shí)顯示
文件存儲(chǔ)與管理:自動(dòng)將錄制文件保存到外部存儲(chǔ)或 MediaStore,支持列表預(yù)覽與播放
性能與兼容:兼容不同分辨率、適配屏幕方向變化,優(yōu)化視頻碼率與幀率,控制錄屏對(duì)系統(tǒng)性能的影響
二、相關(guān)技術(shù)與原理
| 技術(shù) | 功能 |
|---|---|
| MediaProjection | 提供屏幕內(nèi)容采集權(quán)限,輸出到 VirtualDisplay |
| VirtualDisplay | 將屏幕內(nèi)容鏡像到指定 Surface(例如 MediaRecorder 的輸入) |
| MediaRecorder | 原生音視頻錄制 API,簡(jiǎn)化同步編碼、封裝 AV 文件 |
| MediaCodec + Muxer | 手動(dòng)編碼與封裝,獲得更細(xì)粒度控制(可選高級(jí)方案) |
| Service | 后臺(tái)管理錄屏任務(wù),結(jié)合前臺(tái)服務(wù)確保錄制不中斷 |
| MediaStore | Android 10+ 存儲(chǔ)協(xié)議,安全寫入公共相冊(cè) |
| Notification | 前臺(tái)服務(wù)需在通知欄持續(xù)顯示錄制狀態(tài) |
API 要求:最早 API 21,建議應(yīng)用最小支持 API 23 以上,以便簡(jiǎn)化外部存儲(chǔ)和權(quán)限管理。
編解碼參數(shù):典型分辨率與幀率組合:1080×1920@30fps、720×1280@30fps;視頻碼率 4–8 Mbps;音頻碼率 128 kbps;
三、系統(tǒng)權(quán)限與用戶授權(quán)
Manifest 權(quán)限
<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"/>
運(yùn)行時(shí)授權(quán)
Android 6.0+ 需請(qǐng)求存儲(chǔ)與麥克風(fēng)權(quán)限
通過 MediaProjectionManager.createScreenCaptureIntent() 發(fā)起系統(tǒng)錄屏授權(quán)對(duì)話框
四、項(xiàng)目架構(gòu)與流程
用戶點(diǎn)擊“開始錄制”按鈕
│
▼
MainActivity 發(fā)起錄屏授權(quán) Intent
│
▼
onActivityResult 收到授權(quán) RESULT_OK & data Intent
│
▼
啟動(dòng) ScreenRecordService(前臺(tái)服務(wù)),傳入 data
│
▼
Service 中:
├─ 初始化 MediaRecorder
├─ 獲取 MediaProjection
├─ 創(chuàng)建 VirtualDisplay,Surface 指向 MediaRecorder
├─ 調(diào)用 MediaRecorder.start()
│
▼
循環(huán)錄制屏幕與麥克風(fēng)數(shù)據(jù)
│
▼
用戶點(diǎn)擊“停止錄制”
│
▼
Service 調(diào)用 MediaRecorder.stop(), reset(), release()
│
└─ 通知主進(jìn)程錄制完成,保存文件到 MediaStore
五、環(huán)境配置與依賴
// 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
// 描述:權(quán)限申請(qǐng)與 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
// 描述:應(yīng)用主題
// =======================================================
<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
// 描述:申請(qǐng)權(quán)限、發(fā)起錄屏授權(quán)、啟動(dòng)/停止 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
// 描述:前臺(tái) 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()
// 通知系統(tǒng)圖庫更新文件
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
// 描述:錄制狀態(tài)圖標(biāo)(示例可用系統(tǒng)資源)
// =======================================================
<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 發(fā)起錄屏授權(quán),使用 ActivityResultContracts.StartActivityForResult 回調(diào)獲取授權(quán) Intent 與結(jié)果碼;
- 請(qǐng)求存儲(chǔ)與錄音權(quán)限,授權(quán)后調(diào)用 startService() 啟動(dòng) ScreenRecordService;
2.ScreenRecordService
- 在 onStartCommand() 中根據(jù)授權(quán)信息獲取 MediaProjection;
- 調(diào)用 MediaRecorder 完成音視頻同步編碼配置,并通過 MediaProjection.createVirtualDisplay() 將屏幕內(nèi)容輸送到 MediaRecorder.getSurface();
- 使用 前臺(tái) Service(startForeground())提高存活優(yōu)先級(jí),在通知欄顯示錄制狀態(tài);
- 在 onDestroy() 中停止錄制并釋放資源,并發(fā)送廣播刷新系統(tǒng)圖庫。
3.MediaRecorder 配置
- setVideoSource(MediaRecorder.VideoSource.SURFACE):將虛擬屏幕 Surface 作為視頻幀輸入;
- 音頻源使用麥克風(fēng) AudioSource.MIC;
- 視頻編碼器 H.264,音頻編碼器 AAC;
- 分辨率動(dòng)態(tài)獲取當(dāng)前屏幕分辨率,幀率 30 fps,碼率 8 Mbps;
4.文件存儲(chǔ)
- 保存到應(yīng)用私有外部存儲(chǔ) getExternalFilesDir("RecordVideos"),Android 10+ 可換成 MediaStore 接口;
- 錄制完成后廣播 ACTION_MEDIA_SCANNER_SCAN_FILE,讓系統(tǒng)圖庫立即識(shí)別新文件。
5.通知與圖標(biāo)
- 使用 ic_record.xml 簡(jiǎn)單繪制錄制狀態(tài)圖標(biāo),也可替換為自己的矢量資源;
- 通知必須傳入前臺(tái)服務(wù)渠道 CHANNEL_ID,并設(shè)置 setOngoing(true) 鎖定通知。
八、性能優(yōu)化與兼容性考慮
分辨率與碼率自適應(yīng):在高端設(shè)備上可錄制 1080p 或更高分辨率;在低端或后臺(tái)時(shí)可降級(jí)到 720p 以降低 CPU 負(fù)載;
動(dòng)態(tài)暫停與恢復(fù):通過 MediaRecorder.pause() / resume()(API 24+)實(shí)現(xiàn)錄制的中斷與續(xù)錄,防止錄屏過長文件過大;
存儲(chǔ)位置選擇:根據(jù) Android 10+ Scoped Storage 模式改用 MediaStore.Video.Media 插入,可讓用戶在公共相冊(cè)中看到錄制視頻;
錄制時(shí)長限制:在 Service 中使用 Handler 配合定時(shí)邏輯自動(dòng)停止錄制,避免用戶忘記關(guān)閉;
屏幕方向與旋轉(zhuǎn)處理:使用 setOrientationHint() 傳入正確的旋轉(zhuǎn)角度,保證錄制后視頻方向正確;可動(dòng)態(tài)讀取 Display.getRotation() 值;
多路屏幕錄制:如果需要同時(shí)錄制應(yīng)用內(nèi)部視圖和全屏,可結(jié)合 SurfaceView + MediaCodec 自定義編碼合流;
九、擴(kuò)展功能
預(yù)覽與分享:在錄制完成后,跳轉(zhuǎn)到 播放頁面 預(yù)覽視頻,并提供分享到微信、QQ、抖音等鏈接;
水印與濾鏡:在 VirtualDisplay 或 MediaCodec 編碼前,使用 OpenGL ES 在錄制幀上疊加文字與圖片水印,或添加濾鏡效果;
畫中畫:在錄制主屏幕的同時(shí),疊加前置攝像頭小窗口,實(shí)現(xiàn)游戲解說或教學(xué)場(chǎng)景;
軌跡標(biāo)記:支持用戶在錄制時(shí)繪制屏幕內(nèi)容,如手繪箭頭、圈注關(guān)鍵點(diǎn),適用于教學(xué)與演示;
云端同步:錄制完成后自動(dòng)上傳到 OSS/S3/騰訊云等對(duì)象存儲(chǔ),結(jié)合短視頻處理平臺(tái)進(jìn)行后續(xù)剪輯與分發(fā)。
十、項(xiàng)目總結(jié)與落地建議
本文基于 MediaProjection + MediaRecorder 實(shí)現(xiàn)了一個(gè)完整、可擴(kuò)展的 Android 錄屏功能,涵蓋:
系統(tǒng)授權(quán)與運(yùn)行時(shí)權(quán)限管理
前臺(tái) Service 保證長期錄制不中斷
高質(zhì)量音視頻同步編碼與文件保存
錄制狀態(tài)通知與系統(tǒng)相冊(cè)更新
在實(shí)際項(xiàng)目中,可結(jié)合應(yīng)用業(yè)務(wù)場(chǎng)景,增添更多人性化功能與企業(yè)級(jí)需求,如錄制回放剪輯、云端上傳、畫中畫、手勢(shì)標(biāo)注等,打造更具競(jìng)爭(zhēng)力的錄屏產(chǎn)品。
十一、常見問題解答(FAQ)
Q1:為什么錄制內(nèi)容全黑或黑屏?
需在 MediaRecorder 配置好 setVideoSource(MediaRecorder.VideoSource.SURFACE) 后再調(diào)用 prepare()。
檢查是否正確調(diào)用 projection.createVirtualDisplay() 并傳入 recorder.getSurface()。
Q2:錄制文件巨大怎么辦?
降低視頻碼率,或增加壓縮率。使用高效編碼器(H.265)需自行集成 MediaCodec + MediaMuxer。
Q3:如何實(shí)現(xiàn)暫停和續(xù)錄?
Android N(API 24)以上,MediaRecorder.pause() 與 resume() 可控制錄制中斷;
舊版本則需停止當(dāng)前錄制并啟動(dòng)下一段分片,后期合并分片文件。
Q4:錄制靜音或無聲?
檢查麥克風(fēng)是否被 setAudioSource() 正確調(diào)用,應(yīng)用是否擁有 RECORD_AUDIO 權(quán)限;
部分設(shè)備后臺(tái)錄音需在 AudioAttributes 中指定前臺(tái)模式。
Q5:錄制過程中怎樣顯示時(shí)長?
在 Service 中啟動(dòng)定時(shí)器(Handler.postDelayed()),定期通過通知或廣播更新時(shí)長到 UI。
到此這篇關(guān)于Android實(shí)現(xiàn)簡(jiǎn)單的錄屏功能(附源碼)的文章就介紹到這了,更多相關(guān)Android錄屏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android實(shí)現(xiàn)底部狀態(tài)欄切換的兩種方式
這篇文章主要介紹了Android實(shí)現(xiàn)底部狀態(tài)欄切換功能,在文中給大家提到了兩種實(shí)現(xiàn)方式,本文分步驟給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06
Android網(wǎng)絡(luò)工具類NetworkUtils詳解
這篇文章主要為大家詳細(xì)介紹了Android網(wǎng)絡(luò)工具類NetworkUtils,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04
android之計(jì)時(shí)器(Chronometer)的使用以及常用的方法
在Android的SDK中,為我們提供了一個(gè)計(jì)時(shí)器,這個(gè)計(jì)時(shí)器稱為Chronometer,我們可以成它為Android的一個(gè)組件,同時(shí)它也具備自己獨(dú)有的方法2013-01-01
Android自定義View設(shè)定到FrameLayout布局中實(shí)現(xiàn)多組件顯示的方法 分享
Android自定義View設(shè)定到FrameLayout布局中實(shí)現(xiàn)多組件顯示的方法 分享,需要的朋友可以參考一下2013-05-05
Android檢測(cè)url地址是否可達(dá)的兩種方法
今天小編就為大家分享一篇Android檢測(cè)url地址是否可達(dá)的兩種方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-01-01
VerticalBannerView仿淘寶頭條實(shí)現(xiàn)垂直輪播廣告
這篇文章主要為大家詳細(xì)介紹了VerticalBannerView仿淘寶頭條實(shí)現(xiàn)垂直輪播廣告,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08

