How one million Nikkatsu App screen recording function is implemented

Android 4.0 from the start to provide a mobile phone to record screen method, but requires root privileges, more trouble is not easy to achieve. But from the beginning of 5.0, the system provides a series of methods to screen recording App does not require root privileges, only authorized users can record screen, is relatively simple.

Basically according to the official documentation will be able to write the relevant code recording screen.

Screen recording basic implementation steps

Affirming the rights 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"/>
And apply for permission to get 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()
Rewrite onActivityResult () for user authorization processing
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
}   

Can be seen above, we can set a series of parameters, the meaning of various parameters on the hope we all go to watch the official documents. There is a more important point is that we are through MediaProjectionManagerto create a VirtualDisplay, this VirtualDisplaycan be understood as a virtual presentation, it can capture content on the screen and render its contents to capture Surfaceon, MediaRecorderfurther to its package for the mp4 file is saved .

After recording, call the stop method to save data

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
    }

}

We must look MediaRecorderto stop()annotate methods.

/**
 * 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; 

According to official documents, stop()if the prepare()call immediately after the crash, but an error occurred in other cases did not do too much mention, in fact, when you actually use MediaRecorderto make screen recording, you will find that even if you are not in prepare()the call immediately stop(), it may throw IllegalStateExceptionan exception. So, sure, we'd better be used directly try...catch...statement block wrapped.

For example, you initRecorderare certain parameters there is a problem, there will be stop()mistakes, do not write the data into your document.

After completion, the release of resources
fun clearAll() {
    mediaRecorder?.release()
    mediaRecorder = null
    virtualDisplay?.release()
    virtualDisplay = null
    mediaProjection?.stop()
    mediaProjection = null
}

You can not bypass ambient sound

The above basic screen recording for Android made simple code to write, of course, in fact, where we also need to do more than those above, may be of interest to the venue to ScreenRecordHelper view.

But this is not our focus, we are extremely vulnerable to such a situation, when we need to record audio recording system volume, but it does not allow us to go in record volume environment.

It seems we've initialized MediaRecorderwhen there is a setting where the audio source, we have to look at the MediaRecorder.setAudioSource()methods support what things set up.

From the official documents it shows that we can set the following audio sources. As the official comment too much, here is a simple explanation of some of the audio source can set our support.

//设定录音来源于同方向的相机麦克风相同,若相机无内置相机或无法识别,则使用预设的麦克风
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

At first sight there is no option we want to actually test you one by one, you will find, indeed. We want to play music media, you can not always get rid of restrictions ambient sound.

The strange thing is, we use part of Huawei's mobile phone screen recording system when they can do, which is customizable to change the ROM sigh of magic, of course, strange third-party Android ROM also kept us fit difficult.

Quxianjiuguo stripped ambient sound

Since we can not always achieve our demands through the system API calls: Record screen, and while playing background music, recorded video requires a good save and not only the background music volume environment, we had a different approach.

Not difficult to think, we can not set the audio source while recording video, so that the resulting video is a video without any sound, then if this time we music clips into force, so that you can perfectly address the needs of the user.

For mixed audio and video editing, presumably most people can think of is the famous FFmpeg , but if you want to compile your own optimization to get a stable FFmpge library that can be used, then, need to spend a lot of time. More importantly, we significantly increased our volume APK for such a simple function, it is absolutely essential. So we need to shift attention to the official MediaExtractoron.

From the official documents of view, to be able to support m4a and aac format audio file to a video file synthesis, according to the relevant documents we can easily write code.

/**
 * 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
        }
    }
}

So the achievements recorded screen helper ScreenRecordHelper

After all kinds of compatibility testing, currently more than 1 million DAU stable operation of APP in two versions, then pulled out a tools library to share to you, very simple to use, more comprehensive code comments, interested can click the link visit: https://github.com/nanchen2251/ScreenRecordHelper

Use is very simple, directly to the [README] (https://github.com/nanchen2251/ScreenRecordHelper/blob/master/README.md) paste over it.

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

Due to limited personal level, although withstood the test of the company's products, but certainly there are many places do not fully support, hope to have a big brother does not know the stingy wing, there are any compatibility issues, please direct reference to issues, Thx.

Reference article: http://lastwarmth.win/2018/11/23/media-mix/
https://juejin.im/post/5afaee7df265da0ba2672608

Guess you like

Origin www.cnblogs.com/liushilin/p/11086697.html