内容简介:Android 从 4.0 开始就提供了手机录屏方法,但是需要 root 权限,比较麻烦不容易实现。但是从 5.0 开始,系统提供给了 App 录制屏幕的一系列方法,不需要 root 权限,只需要用户授权即可录屏,相对来说较为简单。基本上根据官方文档 便可以写出录屏的相关代码。上面可以看到,我们可以设置一系列参数,各种参数的意思就希望大家自己去观摩官方文档了。其中有一个比较重要的一点是我们通过
Android 从 4.0 开始就提供了手机录屏方法,但是需要 root 权限,比较麻烦不容易实现。但是从 5.0 开始,系统提供给了 App 录制屏幕的一系列方法,不需要 root 权限,只需要用户授权即可录屏,相对来说较为简单。
基本上根据官方文档 便可以写出录屏的相关代码。
屏幕录制的基本实现步骤
在 Manifest 中申明权限
<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"/> 复制代码
获取 MediaProjectionManager 并申请权限
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() 复制代码
重写 onActivityResult() 对用户授权进行处理
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 文件保存。
录制完毕,调用 stop 方法保存数据
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()
方法的注释。
/** * 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...
语句块进行包裹。
比如你 initRecorder
中某些参数设置有问题,也会出现 stop()
出错,数据写不进你的文件。
完毕后,释放资源
fun clearAll() { mediaRecorder?.release() mediaRecorder = null virtualDisplay?.release() virtualDisplay = null mediaProjection?.stop() mediaProjection = null } 复制代码
无法绕过的环境声音
上面基本对 Android 屏幕录制做了简单的代码编写,当然实际上,我们需要做的地方还不止上面这些,感兴趣的可以移步到 ScreenRecordHelper 进行查看。
但这根本不是我们的重点, 我们极其容易遇到这样的情况,需要我们录制音频的时候录制系统音量,但却不允许我们把环境音量录进去。
似乎我们前面初始化 MediaRecorder
的时候有个设置音频源的地方,我们来看看这个 MediaRecorder.setAudioSource()
方法都支持设置哪些东西。
从官方文档 可知,我们可以设置以下这些音频源。由于官方注释太多,这里就简单解释一些我们支持的可以设置的音频源。
//设定录音来源于同方向的相机麦克风相同,若相机无内置相机或无法识别,则使用预设的麦克风 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 } } } 复制代码
于是成就了录屏帮助类 ScreenRecordHelper
经过各种兼容性测试,目前在 DAU 超过 100 万的 APP 中稳定运行了两个版本,于是抽出了一个 工具 类库分享给大家,使用非常简单,代码注释比较全面,感兴趣的可以直接点击链接进行访问: github.com/nanchen2251…
使用就非常简单了,直接把 [README] ( github.com/nanchen2251… ) 贴过来吧。
Step 1. Add it in your root build.gradle at the end of repositories:
allprojects { repositories { ... maven { url 'https://jitpack.io' } } } 复制代码
Step 2. Add the dependency
dependencies { implementation 'com.github.nanchen2251:ScreenRecordHelper:1.0.2' } 复制代码
Step 3. Just use it in your project
// 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() } } 复制代码
Step 4. if you want to mix the audio into your video,you just should do
// parameter1 -> The last video length you want // parameter2 -> the audio's duration // parameter2 -> assets resource stopRecord(duration, audioDuration, afdd) 复制代码
Step 5. If you still don't understand, please refer to the demo
由于个人水平有限,虽然目前抗住了公司产品的考验,但肯定还有很多地方没有支持全面,希望有知道的大佬不啬赐教,有任何兼容性问题请直接提 issues,Thx。
参考文章: lastwarmth.win/2018/11/23/…
我是南尘,只做比心的公众号,欢迎关注我。
南尘,GitHub 7k Star,各大技术 Blog 论坛常客,出身 Android,但不仅仅是 Android。写点技术,也吐点情感。做不完的开源,写不完的矫情,你就听听我吹逼,不会错~
以上所述就是小编给大家介绍的《百万级日活 App 的屏幕录制功能是如何实现的》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- ShareX 13.4.0 发布,截图与屏幕录制工具
- ShareX 13.5.0 发布,截图与屏幕录制工具
- iOS端使用replaykit录制屏幕的技术细节
- ShareX 13.1.0 发布,截图与屏幕录制工具
- ShareX 13.3 发布,开源的截图与屏幕录制工具
- ShareX 12.3.0 发布,屏幕录制添加 WebP 编码支持
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。