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