写点什么

百万级日活 App 的屏幕录制功能是如何实现的,flutter 小程序的 onshow

用户头像
Android架构
关注
发布于: 3 小时前

}


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) {

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
百万级日活 App 的屏幕录制功能是如何实现的,flutter小程序的onshow