Android 從 4.0 開始就提供了手機錄屏方法,可是須要 root 權限,比較麻煩不容易實現。可是從 5.0 開始,系統提供給了 App 錄製屏幕的一系列方法,不須要 root 權限,只須要用戶受權便可錄屏,相對來講較爲簡單。html
基本上根據 官方文檔 即可以寫出錄屏的相關代碼。java
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
private val mediaProjectionManager by lazy { activity.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as? MediaProjectionManager } private var mediaProjection: MediaProjection? = null if (mediaProjectionManager == null) { Log.d(TAG, "mediaProjectionManager == null,當前手機暫不支持錄屏") showToast(R.string.phone_not_support_screen_record) return } // 申請相關權限 PermissionUtils.permission(PermissionConstants.STORAGE, PermissionConstants.MICROPHONE) .callback(object : PermissionUtils.SimpleCallback { override fun onGranted() { Log.d(TAG, "start record") mediaProjectionManager?.apply { // 申請相關權限成功後,要向用戶申請錄屏對話框 val intent = this.createScreenCaptureIntent() if (activity.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { activity.startActivityForResult(intent, REQUEST_CODE) } else { showToast(R.string.phone_not_support_screen_record) } } } override fun onDenied() { showToast(R.string.permission_denied) } }) .request()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { if (requestCode == REQUEST_CODE) { if (resultCode == Activity.RESULT_OK) { mediaProjection = mediaProjectionManager!!.getMediaProjection(resultCode, data) // 實測,部分手機上錄製視頻的時候會有彈窗的出現,因此咱們須要作一個 150ms 的延遲 Handler().postDelayed({ if (initRecorder()) { mediaRecorder?.start() } else { showToast(R.string.phone_not_support_screen_record) } }, 150) } else { showToast(R.string.phone_not_support_screen_record) } } } private fun initRecorder(): Boolean { Log.d(TAG, "initRecorder") var result = true // 建立文件夾 val f = File(savePath) if (!f.exists()) { f.mkdirs() } // 錄屏保存的文件 saveFile = File(savePath, "$saveName.tmp") saveFile?.apply { if (exists()) { delete() } } mediaRecorder = MediaRecorder() val width = Math.min(displayMetrics.widthPixels, 1080) val height = Math.min(displayMetrics.heightPixels, 1920) mediaRecorder?.apply { // 能夠設置是否錄製音頻 if (recordAudio) { setAudioSource(MediaRecorder.AudioSource.MIC) } setVideoSource(MediaRecorder.VideoSource.SURFACE) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setVideoEncoder(MediaRecorder.VideoEncoder.H264) if (recordAudio){ setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) } setOutputFile(saveFile!!.absolutePath) setVideoSize(width, height) setVideoEncodingBitRate(8388608) setVideoFrameRate(VIDEO_FRAME_RATE) try { prepare() virtualDisplay = mediaProjection?.createVirtualDisplay("MainScreen", width, height, displayMetrics.densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, surface, null, null) Log.d(TAG, "initRecorder 成功") } catch (e: Exception) { Log.e(TAG, "IllegalStateException preparing MediaRecorder: ${e.message}") e.printStackTrace() result = false } } return result }
上面能夠看到,咱們能夠設置一系列參數,各類參數的意思就但願你們本身去觀摩官方文檔了。其中有一個比較重要的一點是咱們經過 MediaProjectionManager
建立了一個 VirtualDisplay
,這個 VirtualDisplay
能夠理解爲虛擬的呈現器,它能夠捕獲屏幕上的內容,並將其捕獲的內容渲染到 Surface
上,MediaRecorder
再進一步把其封裝爲 mp4 文件保存。android
private fun stop() { if (isRecording) { isRecording = false try { mediaRecorder?.apply { setOnErrorListener(null) setOnInfoListener(null) setPreviewDisplay(null) stop() Log.d(TAG, "stop success") } } catch (e: Exception) { Log.e(TAG, "stopRecorder() error!${e.message}") } finally { mediaRecorder?.reset() virtualDisplay?.release() mediaProjection?.stop() listener?.onEndRecord() } } } /** * if you has parameters, the recordAudio will be invalid */ fun stopRecord(videoDuration: Long = 0, audioDuration: Long = 0, afdd: AssetFileDescriptor? = null) { stop() if (audioDuration != 0L && afdd != null) { syntheticAudio(videoDuration, audioDuration, afdd) } else { // saveFile if (saveFile != null) { val newFile = File(savePath, "$saveName.mp4") // 錄製結束後修改後綴爲 mp4 saveFile!!.renameTo(newFile) // 刷新到相冊 val intent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) intent.data = Uri.fromFile(newFile) activity.sendBroadcast(intent) showToast(R.string.save_to_album_success) } saveFile = null } }
咱們必須來看看 MediaRecorder
對 stop()
方法的註釋。git
/** * Stops recording. Call this after start(). Once recording is stopped, * you will have to configure it again as if it has just been constructed. * Note that a RuntimeException is intentionally thrown to the * application, if no valid audio/video data has been received when stop() * is called. This happens if stop() is called immediately after * start(). The failure lets the application take action accordingly to * clean up the output file (delete the output file, for instance), since * the output file is not properly constructed when this happens. * * @throws IllegalStateException if it is called before start() */ public native void stop() throws IllegalStateException;
根據官方文檔,stop()
若是在 prepare()
後當即調用會崩潰,但對其餘狀況下發生的錯誤卻沒有作過多說起,實際上,當你真正地使用 MediaRecorder
作屏幕錄製的時候,你會發現即便你沒有在 prepare()
後當即調用 stop()
,也可能拋出 IllegalStateException
異常。因此,保險起見,咱們最好是直接使用 try...catch...
語句塊進行包裹。github
好比你
initRecorder
中某些參數設置有問題,也會出現stop()
出錯,數據寫不進你的文件。app
fun clearAll() { mediaRecorder?.release() mediaRecorder = null virtualDisplay?.release() virtualDisplay = null mediaProjection?.stop() mediaProjection = null }
上面基本對 Android 屏幕錄製作了簡單的代碼編寫,固然實際上,咱們須要作的地方還不止上面這些,感興趣的能夠移步到 ScreenRecordHelper 進行查看。maven
但這根本不是咱們的重點,咱們極其容易遇到這樣的狀況,須要咱們錄製音頻的時候錄製系統音量,但卻不容許咱們把環境音量錄進去。ide
彷佛咱們前面初始化 MediaRecorder
的時候有個設置音頻源的地方,咱們來看看這個 MediaRecorder.setAudioSource()
方法都支持設置哪些東西。工具
從官方文檔 可知,咱們能夠設置如下這些音頻源。因爲官方註釋太多,這裏就簡單解釋一些咱們支持的能夠設置的音頻源。post
//設定錄音來源於同方向的相機麥克風相同,若相機無內置相機或沒法識別,則使用預設的麥克風 MediaRecorder.AudioSource.CAMCORDER //默認音頻源 MediaRecorder.AudioSource.DEFAULT //設定錄音來源爲主麥克風 MediaRecorder.AudioSource.MIC //設定錄音來源爲語音撥出的語音與對方說話的聲音 MediaRecorder.AudioSource.VOICE_CALL // 攝像頭旁邊的麥克風 MediaRecorder.AudioSource.VOICE_COMMUNICATION //下行聲音 MediaRecorder.AudioSource.VOICE_DOWNLINK //語音識別 MediaRecorder.AudioSource.VOICE_RECOGNITION //上行聲音 MediaRecorder.AudioSource.VOICE_UPLINK
咋一看沒有咱們想要的選項,實際上你逐個進行測試,你也會發現,確實如此。咱們想要媒體播放的音樂,老是沒法擺脫環境聲音的限制。
奇怪的是,咱們使用華爲部分手機的系統錄屏的時候,卻能夠作到,這就感嘆於 ROM 的定製性更改的神奇,固然,千奇百怪的第三方 ROM 也一直讓咱們 Android 適配困難重重。
既然咱們經過調用系統的 API 始終沒法實現咱們的需求:錄製屏幕,並同時播放背景音樂,錄製好保存的視頻須要只有背景音樂而沒有環境音量,咱們只好另闢蹊徑。
不難想到,咱們徹底能夠在錄製視頻的時候不設置音頻源,這樣獲得的視頻就是一個沒有任何聲音的視頻,若是此時咱們再把音樂強行剪輯進去,這樣就能夠完美解決用戶的須要了。
對於音視頻的混合編輯,想必大多數人都能想到的是大名鼎鼎的 FFmpeg ,但若是要本身去編譯優化獲得一個穩定可以使用的 FFmpge 庫的話,須要花上很多時間。更重要的是,咱們爲一個如此簡單的功能大大的增大咱們 APK 的體積,那是萬萬不可的。因此咱們須要把目光轉移到官方的 MediaExtractor
上。
從 官方文檔 來看,可以支持到 m4a 和 aac 格式的音頻文件合成到視頻文件中,根據相關文檔咱們就不難寫出這樣的代碼。
/** * https://stackoverflow.com/questions/31572067/android-how-to-mux-audio-file-and-video-file */ private fun syntheticAudio(audioDuration: Long, videoDuration: Long, afdd: AssetFileDescriptor) { Log.d(TAG, "start syntheticAudio") val newFile = File(savePath, "$saveName.mp4") if (newFile.exists()) { newFile.delete() } try { newFile.createNewFile() val videoExtractor = MediaExtractor() videoExtractor.setDataSource(saveFile!!.absolutePath) val audioExtractor = MediaExtractor() afdd.apply { audioExtractor.setDataSource(fileDescriptor, startOffset, length * videoDuration / audioDuration) } val muxer = MediaMuxer(newFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4) videoExtractor.selectTrack(0) val videoFormat = videoExtractor.getTrackFormat(0) val videoTrack = muxer.addTrack(videoFormat) audioExtractor.selectTrack(0) val audioFormat = audioExtractor.getTrackFormat(0) val audioTrack = muxer.addTrack(audioFormat) var sawEOS = false var frameCount = 0 val offset = 100 val sampleSize = 1000 * 1024 val videoBuf = ByteBuffer.allocate(sampleSize) val audioBuf = ByteBuffer.allocate(sampleSize) val videoBufferInfo = MediaCodec.BufferInfo() val audioBufferInfo = MediaCodec.BufferInfo() videoExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) audioExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC) muxer.start() // 每秒多少幀 // 實測 OPPO R9em 垃圾手機,拿出來的沒有 MediaFormat.KEY_FRAME_RATE val frameRate = if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) { videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE) } else { 31 } // 得出平均每一幀間隔多少微妙 val videoSampleTime = 1000 * 1000 / frameRate while (!sawEOS) { videoBufferInfo.offset = offset videoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset) if (videoBufferInfo.size < 0) { sawEOS = true videoBufferInfo.size = 0 } else { videoBufferInfo.presentationTimeUs += videoSampleTime videoBufferInfo.flags = videoExtractor.sampleFlags muxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo) videoExtractor.advance() frameCount++ } } var sawEOS2 = false var frameCount2 = 0 while (!sawEOS2) { frameCount2++ audioBufferInfo.offset = offset audioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset) if (audioBufferInfo.size < 0) { sawEOS2 = true audioBufferInfo.size = 0 } else { audioBufferInfo.presentationTimeUs = audioExtractor.sampleTime audioBufferInfo.flags = audioExtractor.sampleFlags muxer.writeSampleData(audioTrack, audioBuf, audioBufferInfo) audioExtractor.advance() } } muxer.stop() muxer.release() videoExtractor.release() audioExtractor.release() // 刪除無聲視頻文件 saveFile?.delete() } catch (e: Exception) { Log.e(TAG, "Mixer Error:${e.message}") // 視頻添加音頻合成失敗,直接保存視頻 saveFile?.renameTo(newFile) } finally { afdd.close() Handler().post { refreshVideo(newFile) saveFile = null } } }
通過各類兼容性測試,目前在 DAU 超過 100 萬的 APP 中穩定運行了兩個版本,因而抽出了一個工具類庫分享給你們,使用很是簡單,代碼註釋比較全面,感興趣的能夠直接點擊連接進行訪問:https://github.com/nanchen2251/ScreenRecordHelper
使用就很是簡單了,直接把 [README] (https://github.com/nanchen2251/ScreenRecordHelper/blob/master/README.md) 貼過來吧。
allprojects { repositories { ... maven { url 'https://jitpack.io' } } }
dependencies { implementation 'com.github.nanchen2251:ScreenRecordHelper:1.0.2' }
// start screen record if (screenRecordHelper == null) { screenRecordHelper = ScreenRecordHelper(this, null, PathUtils.getExternalStoragePath() + "/nanchen") } screenRecordHelper?.apply { if (!isRecording) { // if you want to record the audio,you can set the recordAudio as true screenRecordHelper?.startRecord() } } // You must rewrite the onActivityResult override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && data != null) { screenRecordHelper?.onActivityResult(requestCode, resultCode, data) } } // just stop screen record screenRecordHelper?.apply { if (isRecording) { stopRecord() } }
// parameter1 -> The last video length you want // parameter2 -> the audio's duration // parameter2 -> assets resource stopRecord(duration, audioDuration, afdd)
因爲我的水平有限,雖然目前抗住了公司產品的考驗,但確定還有不少地方沒有支持全面,但願有知道的大佬不嗇賜教,有任何兼容性問題請直接提 issues,Thx。
參考文章:http://lastwarmth.win/2018/11/23/media-mix/
http://www.javashuo.com/article/p-hxttuvrm-dz.html