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 中稳定运行了两个版本,因而抽出了一个工具类库分享给你们,使用很是简单,代码注释比较全面,感兴趣的能够直接点击连接进行访问:github.com/nanchen2251…
使用就很是简单了,直接把 [README] (github.com/nanchen2251…) 贴过来吧。
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。
参考文章:lastwarmth.win/2018/11/23/… juejin.im/post/5afaee…
我是南尘,只作比心的公众号,欢迎关注我。
南尘,GitHub 7k Star,各大技术 Blog 论坛常客,出身 Android,但不只仅是 Android。写点技术,也吐点情感。作不完的开源,写不完的矫情,你就听听我吹逼,不会错~