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 = falsetry {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 {// saveFileif (saveFile != null) {val newFile = File(savePath, "$saveName.mp4")// 录制结束后修改后缀为 mp4saveFile!!.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}}

我们必须来看看 MediaRecorderstop() 方法的注释。

/*** 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 = nullvirtualDisplay?.release()virtualDisplay = nullmediaProjection?.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 = falsevar frameCount = 0val offset = 100val sampleSize = 1000 * 1024val 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_RATEval frameRate = if (videoFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {videoFormat.getInteger(MediaFormat.KEY_FRAME_RATE)} else {31}// 得出平均每一帧间隔多少微妙val videoSampleTime = 1000 * 1000 / frameRatewhile (!sawEOS) {videoBufferInfo.offset = offsetvideoBufferInfo.size = videoExtractor.readSampleData(videoBuf, offset)if (videoBufferInfo.size < 0) {sawEOS = truevideoBufferInfo.size = 0} else {videoBufferInfo.presentationTimeUs += videoSampleTimevideoBufferInfo.flags = videoExtractor.sampleFlagsmuxer.writeSampleData(videoTrack, videoBuf, videoBufferInfo)videoExtractor.advance()frameCount++}}var sawEOS2 = falsevar frameCount2 = 0while (!sawEOS2) {frameCount2++audioBufferInfo.offset = offsetaudioBufferInfo.size = audioExtractor.readSampleData(audioBuf, offset)if (audioBufferInfo.size < 0) {sawEOS2 = trueaudioBufferInfo.size = 0} else {audioBufferInfo.presentationTimeUs = audioExtractor.sampleTimeaudioBufferInfo.flags = audioExtractor.sampleFlagsmuxer.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 中稳定运行了两个版本,于是抽出了一个工具类库分享给大家,使用非常简单,代码注释比较全面,感兴趣的可以直接点击链接进行访问:https://github.com/nanchen2251/ScreenRecordHelper

使用就非常简单了,直接把 [README] (https://github.com/nanchen2251/ScreenRecordHelper/blob/master/README.md) 贴过来吧。

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 truescreenRecordHelper?.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。

参考文章:http://lastwarmth.win/2018/11/23/media-mix/
https://juejin.im/post/5afaee7df265da0ba2672608

转载于:https://www.cnblogs.com/liushilin/p/11086697.html

百万级日活 App 的屏幕录制功能是如何实现的相关推荐

  1. 【测试专场沙龙报名】千万级日活App的质量保证

    美团技术沙龙由美团技术团队和美团科协主办,每期沙龙邀请美团及其他互联网公司的技术专家分享来自一线的实践经验,覆盖各主要技术领域. 活动时间:2018年9月15日 下午 13:30 - 17:30 活动 ...

  2. AdScopes收益分析 | 百万日活APP,一天广告收入有多少?一文教你如何计算

    6月份,AdScope联合蝉大师举办了一场"APP流量变现技巧"线上分享交流会,私下里就有小伙伴问了:"我们DAU如果能达到百万,广告一天能有多少收入--''想必这应该是 ...

  3. 读阿里亿级日活网关通道架构演进有感

    读<阿里亿级日活网关通道架构演进>时对优化方法有些概念不理解,特意搜索了一下,拓展自己的思路. 其中的优化: 优化方法中1,2比较常见,3,4我知道的比较少,很感兴趣.就继续追踪下去: 于 ...

  4. 亿级用户体量,千万级日活用户,《王者荣耀》高并发背后的故事!

    堪称中国最火爆的手机游戏"王者荣耀",拥有亿级用户体量,千万级日活用户,如何快速.低成本地保障业务突发?本文从该问题出发,论述了问题对应的解决方案,并对其效果做出总结. 作者:黎斌 ...

  5. ios html录制视频,iPhone怎么录屏?玩转iOS14自带屏幕录制功能全攻略

    原标题:iPhone怎么录屏?玩转iOS14自带屏幕录制功能全攻略 录屏是很多小伙伴,平时用的比较多的一项功能.苹果手机内置了录屏功能,无需下载安装第三方APP即可体验.不过,有一些小伙伴对iPhon ...

  6. android 实现屏幕录制功能,Android实现屏幕录制功能

    本文实例为大家分享了Android实现屏幕录制功能的具体代码,供大家参考,具体内容如下 1.效果图: 2.添加依赖 dependencies { implementation fileTree(dir ...

  7. python 屏幕录制_Python实现屏幕录制功能的代码

    前段时间做视频时需要演示电脑端的操作,因此要用到屏幕录制,下载了个迅捷屏幕录制,但是没有vip录制的视频有水印且只能录制二分钟,于是鄙人想了下能不能通过万能的python来实现呢?经过一晚上的尝试发现 ...

  8. IOS ReplayKit RPScreenRecorder 的屏幕录制功能

    IOS ReplayKit RPScreenRecorder 的屏幕录制功能 开始录制有两个方法: (void)startRecordingWithMicrophoneEnabled:(BOOL)mi ...

  9. python屏幕录制与回放_Python实现屏幕录制功能的代码

    前段时间做视频时需要演示电脑端的操作,因此要用到屏幕录制,下载了个迅捷屏幕录制,但是没有vip录制的视频有水印且只能录制二分钟,于是鄙人想了下能不能通过万能的python来实现呢?经过一晚上的尝试发现 ...

最新文章

  1. 2021-2027年中国手机壳行业现状研究及发展趋势分析报告
  2. MySQL中对varchar类型排序问题的解决
  3. python网络爬虫之yield关键字的使用!
  4. 边缘使用 K8s 门槛太高?OpenYurt 这个功能帮你快速搭建集群!
  5. 游戏中的整容术! 《Honey Select》捏人系统剖析
  6. 【今晚七点】:对话熊谱翔——开源RTOS与多媒体
  7. 日程解读 | LiveVideoStackCon 2020北京站日程官宣了!
  8. 虚拟机VirtualBox中Ubuntu无法全屏(终极解决方法)
  9. Java-异常03 自定义异常
  10. 前端笔记-Vue框架的基本认识
  11. 修改IntelliJ IDEA包层级结构显示方式
  12. Android API 指南
  13. 全球品牌百强榜单出炉:中国品牌仅有华为上榜
  14. 【数论】蓝桥20:数列求值
  15. rest sso 和_SSO企业单点登录系统——CAS REST认证方式
  16. Matlab R2019a Win64位 迅雷下载链接
  17. 【Appium实战】如何使用mumu模拟器模拟安卓手机
  18. TCP和UPD的理解
  19. 错题集 HDLBits Exams/ece241 2013 q7 JK触发器
  20. 离散化-利用计算机求解y=x,离散信号处理(双语)-中国大学mooc-题库零氪

热门文章

  1. archlinux 蓝牙耳机没有声音
  2. 时间局部性和空间局部性(埋坑)
  3. 拼多多放心推系统必须知道的规则!千万别错过
  4. bzoj3118 Orz the MST(单纯形)
  5. 第一代第二代计算机区别,小米空气净化器第一代与第二代比赛有什么区别
  6. amd插帧技术如何开启_AMD承诺会尽快公布RX6000系列光追和超采样细节
  7. 决策树(1)——ID3算法与C4.5算法的理论基础与python实现
  8. 数组作为函数参数时,最好将数组大小也作为一个函数参数
  9. 对不起,给pandas配表情包太难了,pandas你该这么学,No.6
  10. JavaWeb一个简单的酒店项目【前台管理和用户界面和后台管理】以及微信对接