百万级日活 App 的屏幕录制功能是如何实现的,flutter 小程序的 onshow
}
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) {
评论