栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Android 循环录制最近一段时间的视频

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Android 循环录制最近一段时间的视频

Android 循环录制最近一段时间的视频

在日常开发测试中,往往发生问题了再去想办法复现录屏、抓取日志的工作,往往会出现偶现问题很难复现,导致问题很难定位。在这里给出一个能抓取历史操作视频的解决方案:

  1. 将录屏的视频帧数据一帧帧的缓存到一块固定大小的内存中(空间循环利用)
  2. 发现问题时,触发混合器(MediaMuxer)将指定时间范围的视频帧数据取出存储为指定的mp4文件
数据缓存

数据缓存用来解决历史数据保存,需要合理的分配内存大小,根据自己的实际情况(手机屏幕分辨率、多长时间的视频记录等等)选择合适的大小。

提供四个JNI函数:

object frameDataCacheUtils {
    
    external fun initCache(cacheSize: Int, isDebug: Boolean)

    
    external fun addframeData(
        timestamp: Long,
        isKeyframe: Boolean,
        frameData: ByteArray,
        length: Int
    )

    
    external fun getFirstframeData(
        timestamp: Long,
        curTimestamp: LongArray,
        frameData: ByteArray,
        length: IntArray
    ): Int

    
    external fun getNextframeData(
        preTimestamp: Long,
        curTimestamp: LongArray,
        frameData: ByteArray,
        length: IntArray,
        isKeyframe: BooleanArray
    ): Int

    init {
        System.loadLibrary("framedatacachejni")
    }
}

缓存框架源码:https://download.csdn.net/download/lkl22/73404181

开启屏幕录屏 一、申请权限

二、创建service
    
        
    
    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        createNotificationChannel()
        val resultCode = intent.getIntExtra(ScreenCapture.KEY_RESULT_CODE, -1)
        val cacheSize = intent.getIntExtra(ScreenCapture.KEY_CACHE_SIZE, ScreenCapture.DEFAULT_CACHE_SIZE)
        val resultData = intent.getParcelableExtra(ScreenCapture.KEY_DATA)
        resultData?.apply {
            ScreenCaptureManager.instance.startRecord(resultCode, this, cacheSize)
            LogUtils.e(TAG, "startRecord.")
        }
        return super.onStartCommand(intent, flags, startId)
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            if (notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) == null) {
                val channel = NotificationChannel(
                    NOTIFICATION_CHANNEL_ID,
                    NOTIFICATION_CHANNEL_NAME,
                    NotificationManager.importANCE_DEFAULT
                )
                notificationManager.createNotificationChannel(channel)
            }
        }

        val builder =
            NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) //获取一个Notification构造器
                .setContentTitle("ScreenCapture") // 设置下拉列表里的标题
                .setSmallIcon(R.mipmap.ic_launcher) // 设置状态栏内的小图标
                .setContentText("is running......") // 设置上下文内容
                .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .setWhen(System.currentTimeMillis()) // 设置该通知发生的时间
        LogUtils.d(TAG, "startForeground")
        startForeground(NOTIFICATION_ID, builder.build())
    }
三、开启录屏 1、创建MediaProjectionManager对象
    private val mProjectionManager: MediaProjectionManager = baseApplication.context
        .getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
2、创建createScreenCaptureIntent
    fun createScreenCaptureIntent(): Intent {
        return mProjectionManager.createScreenCaptureIntent()
    }
3、跳转ScreenCapture
            startActivityForResult(
                ScreenCaptureManager.instance.createScreenCaptureIntent(),
                SCREEN_CAPTURE_REQUEST_CODE
            )
4、回调处理
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == SCREEN_CAPTURE_REQUEST_CODE) {
            data?.apply {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                    val service = Intent(this@MainActivity, ScreenCaptureService::class.java)
                    service.putExtra(ScreenCapture.KEY_RESULT_CODE, resultCode)
                    service.putExtra(ScreenCapture.KEY_DATA, data)
                    service.putExtra(ScreenCapture.KEY_CACHE_SIZE, cacheSize)
                    startForegroundService(service)
                } else {
                    ScreenCaptureManager.instance.startRecord(resultCode, this, cacheSize)
                }
            }
        }
    }
处理录屏数据 开启录屏线程记录视频帧数据
    fun startRecord(resultCode: Int, data: Intent, cacheSize: Int) {
        mScreenCaptureThread = ScreenCaptureThread(
            MediaFormatParams(
                mDisplayMetrics.widthPixels,
                mDisplayMetrics.heightPixels,
                colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
            ),
            mDisplayMetrics.densityDpi,
            mProjectionManager.getMediaProjection(resultCode, data),
            object : ScreenCaptureThread.Callback {
                override fun prePrepare(mediaFormatParams: MediaFormatParams) {
                    frameDataCacheUtils.initCache(
                        cacheSize, BuildConfig.DEBUG
                    )
                    isEnvReady.set(true)
                }

                override fun putframeData(frameData: frameData) {
                    // 将编码好的H264数据存储到缓冲中
                    frameDataCacheUtils.addframeData(
                        frameData.timestamp,
                        frameData.isKeyframe,
                        frameData.data,
                        frameData.length
                    )
                }
            }
        )
        mScreenCaptureThread?.start()
    }

ScreenCaptureThread核心代码

package com.lkl.medialib.core

import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.MediaCodec
import android.media.MediaFormat
import android.media.projection.MediaProjection
import android.util.Log
import android.view.Surface
import com.lkl.commonlib.util.LogUtils
import com.lkl.medialib.bean.frameData
import com.lkl.medialib.bean.MediaFormatParams
import com.lkl.medialib.constant.ScreenCapture
import com.lkl.medialib.util.MediaUtils
import java.io.IOException


class ScreenCaptureThread(
    private val mediaFormatParams: MediaFormatParams,
    private val dpi: Int,
    private val mediaProjection: MediaProjection,
    private val callback: Callback,
    threadName: String = TAG
) : baseMediaThread(threadName) {
    companion object {
        private const val TAG = "ScreenRecordService"

        private const val TIMEOUT_US = 10000L
    }

    private var mEncoder: MediaCodec? = null
    private var mSurface: Surface? = null
    private val mBufferInfo = MediaCodec.BufferInfo()
    private var mVirtualDisplay: VirtualDisplay? = null

    private var mMediaFormat: MediaFormat? = null

    @Throws(IOException::class)
    override fun prepare() {
        callback.prePrepare(mediaFormatParams)

        val format = MediaUtils.createVideoFormat(mediaFormatParams)
        Log.d(TAG, "created video format: $format")
        mEncoder = MediaCodec.createEncoderByType(mediaFormatParams.mimeType)
        mEncoder?.apply {
            configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
            mSurface = createInputSurface()
            Log.d(TAG, "created input surface: $mSurface")
            start()

            mVirtualDisplay = mediaProjection.createVirtualDisplay(
                "$TAG-display", mediaFormatParams.width, mediaFormatParams.height, dpi,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, mSurface, null, null
            )
            Log.d(TAG, "created virtual display: $mVirtualDisplay")
        }

    }

    override fun drain() {
        val index = mEncoder!!.dequeueOutputBuffer(mBufferInfo, TIMEOUT_US)
        when {
            index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
                // 后续输出格式变化
                mMediaFormat = mEncoder?.outputFormat
            }
            index == MediaCodec.INFO_TRY_AGAIN_LATER -> {
                // 请求超时
                waitTime(10)
            }
            index >= 0 -> {
                // 有效输出
                encodeDataToCallback(index)
                mEncoder?.releaseOutputBuffer(index, false)
            }
        }
    }

    private fun encodeDataToCallback(index: Int) {
        // 获取到的实时帧视频数据
        var encodedData = mEncoder!!.getOutputBuffer(index)
        if (mBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_ConFIG != 0) {
            // The codec config data was pulled out and fed to the muxer
            // when we got the INFO_OUTPUT_FORMAT_CHANGED status. Ignore it.
            Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG")
            mBufferInfo.size = 0
        }
        if (mBufferInfo.size == 0) {
            Log.d(TAG, "info.size == 0, drop it.")
            encodedData = null
        }

        if (encodedData != null) {
            // adjust the ByteBuffer values to match BufferInfo (not needed?)
            encodedData.position(mBufferInfo.offset)
            encodedData.limit(mBufferInfo.offset + mBufferInfo.size)

            // 取出编码好的H264数据
            val data = ByteArray(mBufferInfo.size)
            encodedData[data]

            callback.putframeData(
                frameData(
                    data, mBufferInfo.size, System.currentTimeMillis(),
                    mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_frame
                )
            )

            if (ScreenCapture.PRINT_DEBUG_LOG) {
                LogUtils.d(
                    TAG,
                    "sent ${mBufferInfo.size} bytes callback, ts=${mBufferInfo.presentationTimeUs}"
                )
            }
        }
    }

    fun getMediaFormat(): MediaFormat? {
        return mMediaFormat
    }

    override fun release() {
        LogUtils.d(TAG, "release")
        mEncoder?.apply {
            stop()
            release()
            mEncoder = null
        }
        mVirtualDisplay?.apply {
            release()
            mVirtualDisplay = null
        }
        mediaProjection.stop()
    }

    interface Callback {
        
        fun prePrepare(mediaFormatParams: MediaFormatParams)

        
        fun putframeData(frameData: frameData)
    }
}
提取视频帧存储到MP4文件
   fun startMuxer(fileName: String, startTime: Long, endTime: Long) {
        val mediaFormat = getMediaFormat()
        if (mediaFormat == null) {
            LogUtils.e(TAG, "")
            return
        }
        mVideoMuxerThread =
            VideoMuxerThread(mediaFormat!!, fileName, object : VideoMuxerThread.Callback {
                override fun getFirstIframeData(): frameData? {
                    val res = frameDataCacheUtils.getFirstframeData(
                        startTime,
                        mCurTimeStamp,
                        mframeBuffer,
                        mLength
                    )
                    if (res == DataCacheCode.RES_SUCCESS) {
                        return frameData(mframeBuffer, mLength[0], mCurTimeStamp[0], true)
                    }
                    return null
                }

                override fun getNextframeData(): frameData? {
                    val res = frameDataCacheUtils.getNextframeData(
                        mCurTimeStamp[0],
                        mCurTimeStamp,
                        mframeBuffer,
                        mLength,
                        mIsKeyframe
                    )
                    if (res == DataCacheCode.RES_SUCCESS) {
                        if (mCurTimeStamp[0] > endTime) {
                            mVideoMuxerThread?.quit()
                        }
                        return frameData(mframeBuffer, mLength[0], mCurTimeStamp[0], mIsKeyframe[0])
                    } else if (res == DataCacheCode.RES_FAILED) {
                        mVideoMuxerThread?.quit()
                    }
                    return null
                }

                override fun finished(fileName: String) {
                    ThreadUtils.runOnMainThread{
                        ToastUtils.showLong("视频录制完成。")
                    }
                }
            })

        mVideoMuxerThread?.start()
    }

VideoMuxerThread核心代码

package com.lkl.medialib.core

import android.media.MediaCodec
import android.media.MediaFormat
import android.media.MediaMuxer
import android.text.TextUtils
import com.lkl.commonlib.util.BitmapUtils
import com.lkl.commonlib.util.DateUtils
import com.lkl.commonlib.util.FileUtils
import com.lkl.commonlib.util.LogUtils
import com.lkl.medialib.bean.frameData
import com.lkl.medialib.constant.ScreenCapture
import java.nio.ByteBuffer
import java.util.*


class VideoMuxerThread(
    private val mediaFormat: MediaFormat,
    private val saveFilePath: String? = null,
    private val callback: Callback,
    threadName: String = TAG
) : baseMediaThread(threadName) {
    companion object {
        private const val TAG = "VideoMuxerCore"
    }

    private var mMuxer: MediaMuxer? = null
    private val mBufferInfo = MediaCodec.BufferInfo()
    private var mMuxerStarted = false

    // 时间戳(ms),通过该时间抽帧
    private var mTimeStamp: Long = -1

    // video视频第一帧时间戳 (ms),抽帧时作为起始点
    private var mFirstTimeStamp: Long = -1
    private var mOutputFileName = ""
    private var mTrackIndex = -1

    override fun prepare() {
        // Create a MediaMuxer.  We can't add the video track and start() the muxer here,
        // because our MediaFormat doesn't have the Magic Goodies.  These can only be
        // obtained from the encoder after it has started processing data.
        //
        // We're not actually interested in multiplexing audio.  We just want to convert
        // the raw H.264 elementary stream we get from MediaCodec into a .mp4 file.
        mOutputFileName = if (TextUtils.isEmpty(saveFilePath)) {
            FileUtils.videoDir + DateUtils.nowTime.replace(" ", "_") + BitmapUtils.VIDEO_FILE_EXT
        } else {
            saveFilePath!!
        }
        mMuxer = MediaMuxer(mOutputFileName, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

        // now that we have the Magic Goodies, start the muxer
        LogUtils.d(TAG, "Muxer init mediaFormat -> $mediaFormat")
        mMuxer?.apply {
            mTrackIndex = addTrack(mediaFormat)
            start()
            mMuxerStarted = true

            firstframeHandler()
        }
    }

    private fun firstframeHandler() {
        val frameData = callback.getFirstIframeData()
        if (frameData == null) {
            LogUtils.e(TAG, "get first Iframe data failed.")
            quit()
            return
        }
        writeSampleData(frameData)
    }

    override fun drain() {
        val frameData = callback.getNextframeData()
        if (frameData == null) {
            waitTime(10)
        } else {
            writeSampleData(frameData)
        }
    }

    private fun writeSampleData(frameData: frameData) {
        mMuxer?.apply {
            val sampleData = ByteBuffer.wrap(frameData.data, 0, frameData.length)
            setBufferInfo(
                if (frameData.isKeyframe) MediaCodec.BUFFER_FLAG_KEY_frame else 0,
                frameData.timestamp,
                frameData.length
            )
            writeSampleData(mTrackIndex, sampleData, mBufferInfo)
            if (ScreenCapture.PRINT_DEBUG_LOG) {
                LogUtils.d(
                    TAG, "get frame data: size -> ${frameData.length}  timestamp -> " +
                            DateUtils.convertDateToString(
                                DateUtils.DATE_TIME,
                                Date(frameData.timestamp)
                            ) + " isKeyframe -> ${frameData.isKeyframe}"
                )
            }
        }
    }

    private fun setBufferInfo(flags: Int, presentationTimeMs: Long, size: Int) {
        mBufferInfo.flags = flags
        mBufferInfo.offset = 0
        mBufferInfo.presentationTimeUs = presentationTimeMs * 1000
        mBufferInfo.size = size
    }

    override fun release() {
        LogUtils.d(TAG, "release")
        callback.finished(mOutputFileName)
        mMuxer?.release()
    }

    interface Callback {

        fun getFirstIframeData(): frameData?

        fun getNextframeData(): frameData?

        fun finished(fileName: String)
    }
}
参考文献

高效的两段式循环缓冲区──BipBuffer

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/694713.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号