前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android制作带悬浮窗控制的录屏程序Demo

Android制作带悬浮窗控制的录屏程序Demo

作者头像
Vaccae
发布2021-10-12 15:47:23
1.7K0
发布2021-10-12 15:47:23
举报
文章被收录于专栏:微卡智享微卡智享

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为3729,预计阅读6分钟

前言

最近开发的新版程序初版基本差不多了,所以抽空需要研究一下针对运维方便的辅助工具,其中就有需要做一个WIndows服务器可以远程控制Android客户端的工具,实现的原理大概已经有了个思路了,拆解后每个细节就需要去做技术验证,远程控制首先就需要做到看到对面的图像,预览图像就要使用录屏的功能,所以就有了这个小Demo,当然最终要做的东西是不需要保存本地视频的,这里是为了验证一下是否成功。

实现效果

代码实现

微卡智享

采用的组件

MediaProjectionManager

MediaProjection

MediaCodec

MediaMuxer

Android 5.0后Google终于开放了屏幕采集的接口,也就是 MediaProjection 和 MediaProjectionManager,然后再用MediaCodec输出AAC、MediaMuxer合成音频视频并输出mp4,这样就可以完成屏幕录制成视频的方式了。

核心代码

上面用几个组件可以实现屏幕录制,所以我把整个录制都写进了一个MediaPronUtil的类里。

MediaPronUtil类代码:

代码语言:javascript
复制
package dem.vaccae.mediaprojection

import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.MediaMuxer
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.util.Log
import android.view.Surface
import android.view.WindowManager
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat.startActivityForResult
import androidx.core.content.ContextCompat.getSystemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:21:52
 * 功能模块说明:
 */
class MediaPronUtil {
    companion object {
        val RECORD_REQUEST_CODE = 999;

        private var mMediaPronUtil: MediaPronUtil? = null

        fun getInstance(): MediaPronUtil {
            mMediaPronUtil ?: run {
                synchronized(MediaPronUtil::class.java) {
                    mMediaPronUtil = MediaPronUtil()
                }
            }
            return mMediaPronUtil!!
        }
    }

    private var mActivity: Activity? = null
    private lateinit var mediaProMng: MediaProjectionManager
    private var mVirtualDisplay: VirtualDisplay? = null
    private var mMediaPron: MediaProjection? = null
    private var mSurface: Surface? = null
    private var mMediaCodec: MediaCodec? = null
    private var mMuxer: MediaMuxer? = null
    private var mVideoTrackIndex = -1;

    //是否保存录制文件
    private var isSaveFile = true

    //是否开始录制
    private var isRecord = false
    private var frameSPSFPS: ByteArray = ByteArray(0)

    private var mBufferInfo: MediaCodec.BufferInfo = MediaCodec.BufferInfo()


    /**
     * 请求录屏
     */
    fun requestRecording(activity: Activity) {
        mActivity = activity;
        mActivity?.let {
            mediaProMng =
                it.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

            var captureIntent: Intent? = null
            if (mediaProMng != null) {
                captureIntent = mediaProMng.createScreenCaptureIntent()
            }
            it.startActivityForResult(captureIntent, RECORD_REQUEST_CODE)
        }
    }

    /**
     * 开始录屏
     */
    fun startRecording(data: Intent?, issavefile: Boolean = true) {
        isSaveFile = issavefile
        data?.let {
            mMediaPron = mediaProMng.getMediaProjection(RESULT_OK, it);
            setconfigMedia()
        }
    }


    /**
     * 关闭录屏
     */
    fun stopRecording() {
        release()
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun getCurrentTime(): String {
        val current = LocalDateTime.now()
        val formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
        val formatted = current.format(formatter)
        return formatted.toString()
    }


    private fun setconfigMedia() {
        mActivity?.let {
            val resScope = CoroutineScope(Job())
            resScope.launch {
                try {
                    //隐藏本Activity
                    it.moveTaskToBack(true)
                    //获取windowManager
                    val windowManager =
                        it.getSystemService(AppCompatActivity.WINDOW_SERVICE) as WindowManager
                    //获取屏幕对象
                    val defaultDisplay = windowManager.defaultDisplay
                    //获取屏幕的宽、高,单位是像素
                    val width = defaultDisplay.width
                    val height = defaultDisplay.height

                    //录屏存放目录
                    val fname = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        getCurrentTime() + ".mp4"
                    } else {
                        "screen.mp4"
                    }
                    val filename = it.externalMediaDirs[0].absolutePath + "/" + fname
                    Log.i("video", filename)
                    if (isSaveFile) {
                        mMuxer = MediaMuxer(filename, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
                    } else {
                        mMuxer = null
                    }


                    mMediaCodec = getVideoMediaCodec(width, height)
                    mMediaCodec?.let { mit ->
                        mSurface = mit.createInputSurface()
                        /**
                         * 创建投影
                         * name 本次虚拟显示的名称
                         * width 录制后视频的宽
                         * height 录制后视频的高
                         * dpi 显示屏像素
                         * flags VIRTUAL_DISPLAY_FLAG_PUBLIC 通用显示屏
                         * Surface 输出的Surface
                         */
                        mVirtualDisplay = mMediaPron?.createVirtualDisplay(
                            "ScreenRecord", width, height, 1,
                            DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mSurface, null, null
                        )

                        isRecord = true;
                        mit.start();

                        recordVirtualDisplay()
                    }
                } catch (e: Exception) {
                    Log.e("video", e.message.toString())
                }
            }
        }
    }

    private fun getVideoMediaCodec(width: Int, height: Int): MediaCodec? {
        val format = MediaFormat.createVideoFormat("video/avc", width, height)
        //设置颜色格式
        format.setInteger(
            MediaFormat.KEY_COLOR_FORMAT,
            MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
        )
        //设置比特率(设置码率,通常码率越高,视频越清晰)
        format.setInteger(MediaFormat.KEY_BIT_RATE, 1000 * 1024)
        //设置帧率
        format.setInteger(MediaFormat.KEY_FRAME_RATE, 20)
        //关键帧间隔时间,通常情况下,你设置成多少问题都不大。
        format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
        // 当画面静止时,重复最后一帧,不影响界面显示
        format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, (1000000 / 45).toLong())
        format.setInteger(
            MediaFormat.KEY_BITRATE_MODE,
            MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR
        )
        //设置复用模式
        format.setInteger(
            MediaFormat.KEY_COMPLEXITY,
            MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR
        )
        var mediaCodec: MediaCodec? = null
        try {
//            MediaRecorder mediaRecorder = new MediaRecorder();
            mediaCodec = MediaCodec.createEncoderByType("video/avc")
            mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        } catch (e: Exception) {
            e.printStackTrace()
            if (mediaCodec != null) {
                mediaCodec.reset()
                mediaCodec.stop()
                mediaCodec.release()
                mediaCodec = null
            }
        }
        return mediaCodec
    }


    private fun recordVirtualDisplay() {
        while (isRecord) {
            val index = mMediaCodec!!.dequeueOutputBuffer(mBufferInfo, 10000)
            if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { //后续输出格式变化
                resetOutputFormat()
            } else if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { //请求超时
                try {
                    // wait 10ms
                    Thread.sleep(10)
                } catch (e: InterruptedException) {
                }
            } else if (index >= 0) { //有效输出
                encodeToVideoTrack(index)
                mMediaCodec!!.releaseOutputBuffer(index, false)
            }
        }
    }

    private fun resetOutputFormat() {
        Log.i("video", "Reqoutputformat")
        val newFormat: MediaFormat = mMediaCodec!!.getOutputFormat()
        mMuxer?.let {
            mVideoTrackIndex = it.addTrack(newFormat)
            it.start()
        }
    }

    private fun encodeToVideoTrack(index: Int) {
        try {
            var encodedData = mMediaCodec!!.getOutputBuffer(index)
            //是编码需要的特定数据,不是媒体数据
            if (mBufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
                mBufferInfo.size = 0
            }
            if (mBufferInfo.size == 0) {
                Log.d("video", "info.size == 0, drop it.")
                encodedData = null
            } else {
                Log.d(
                    "video", "got buffer, info: size=" + mBufferInfo.size
                            + ", presentationTimeUs=" + mBufferInfo.presentationTimeUs
                            + ", offset=" + mBufferInfo.offset
                )
            }
            if (encodedData != null) {

                Log.d("video", "outdata size:" + mBufferInfo.size)
                encodedData.position(mBufferInfo.offset)
                encodedData.limit(mBufferInfo.offset + mBufferInfo.size)

                mMuxer?.let {
                    it.writeSampleData(mVideoTrackIndex, encodedData, mBufferInfo);
                }
                val outData: ByteArray = ByteArray(mBufferInfo.size)
                encodedData.get(outData);
    //
    //            var h264RawFrame: ByteArray? = null
    //            if (mBufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
    //                //h264RawFrame 每一帧的视频数据
    //                h264RawFrame = ByteArray(frameSPSFPS.size + outData.size);
    //                System.arraycopy(frameSPSFPS, 0, h264RawFrame, 0, frameSPSFPS.size);
    //                System.arraycopy(outData, 0, h264RawFrame, frameSPSFPS.size, outData.size);
    //            } else {
    //                h264RawFrame = outData;
    //            }
            }
        } catch (e: Exception) {
            Log.e("video", e.message.toString())
        }
    }


    private fun release() {
        mMuxer?.let {
            if (isRecord) {
                it.stop()
                it.release()
            }
        }
        mMediaCodec?.let {
            if (isRecord) {
                try {
                    it.stop()
                    it.release()
                } catch (e: Exception) {
                    mMediaCodec = null;
                    mMediaCodec = MediaCodec.createByCodecName("")
                    mMediaCodec?.stop();
                    mMediaCodec?.release();
                }
            }
            null
        }
        mVirtualDisplay?.let {
            if (isRecord) {
                it.release()
                null
            }
        }
        isRecord = false
    }
}

调用屏幕录制

在Activity的OnCreate中直接调用请求录制,然后在onActivityResult里面判断是否允许录制,并开启录制。

代码语言:javascript
复制
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //申请权限
        allPermissionsGranted()

        //请求录屏
        MediaPronUtil.getInstance().requestRecording(this)

    }
       
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                //请求录屏
                MediaPronUtil.getInstance()
                    .requestRecording(this)
            } else {
                Toast.makeText(this, "未开启权限.", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
    

动态申请权限的方式

代码语言:javascript
复制
    companion object {
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS = arrayOf(
            Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.RECORD_AUDIO,
            Manifest.permission.MODIFY_AUDIO_SETTINGS
        )
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

悬浮控制窗

悬浮窗的文章可以看以前《Android实现可移动的悬浮窗》这篇文章,这次直接把当时的代码复制过来了,用Java写的,所以也不再重新造轮子了,只不过这次的Sevice我改为使用JobService了。

FloatWindowJobService

代码语言:javascript
复制
package dem.vaccae.mediaprojection.floatwindow

import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.app.job.JobService
import android.content.ComponentName
import android.content.Context
import android.util.Log
import kotlinx.coroutines.*
import java.lang.Exception

/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:00:32
 * 功能模块说明:
 */
class FloatWindowJobService : JobService() {


    override fun onStartJob(p0: JobParameters?): Boolean {
        Log.i("job", "StartJob")
        val resScope = CoroutineScope(Job())
        resScope.launch {
            try {
                Log.i("job", "StartJob1")
                // 当前界面没有悬浮窗显示,则创建悬浮窗。
                if (!MyWindowManager.isWindowShowing()) {
                    Log.i("job", "StartJob2")
                    withContext(Dispatchers.Main) {
                        MyWindowManager.createSmallWindow(mContext)
                    }
                }
                startScheduler(mContext)
            } catch (e: Exception) {
                e.printStackTrace()
                Log.e(TAG, e.message.toString())
            }
        }
        return false
    }


    override fun onStopJob(p0: JobParameters?): Boolean = false

    companion object {
        private lateinit var mContext:Context
        private var TAG: String = "floatjob"
        private var JOBID: Int = 999
        private var InterValTime: Long = 1000
        private var jobScheduler: JobScheduler? = null
        private var jobInfo: JobInfo? = null

        fun setJOBID(id: Int) {
            JOBID = id
        }

        fun setInterValTime(time: Long) {
            InterValTime = time
        }

        fun setTag(tag: String) {
            TAG = tag
        }


        fun startScheduler(context: Context) {
            mContext = context
            jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
            cancelScheduler()
            if (jobInfo == null) {
                jobInfo =
                    JobInfo.Builder(
                        JOBID,
                        ComponentName(context, FloatWindowJobService::class.java)
                    )
                        .setMinimumLatency(InterValTime)
                        .build()
            }
            val result = jobScheduler?.schedule(jobInfo!!)
        }

        fun cancelScheduler() {
            //jobScheduler?.cancelAll()
            jobScheduler?.cancel(JOBID)
        }
    }
}

Android8.0后还需要开启悬浮窗的上层权限 ,所以在动态申请权限那里还要加上这个开启。

代码语言:javascript
复制
        //请求上层悬浮框权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
            && !Settings.canDrawOverlays(this)
        ) {
            val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION);
            startActivity(intent);
            finish();
        }

实现画面

以上核心的代码基本都说完了,想要整个Demo的可以从下方链接中直接下载。

源码地址

https://github.com/Vaccae/AndroidScreenRecord.git

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微卡智享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 采用的组件
  • 核心代码
  • 调用屏幕录制
  • 悬浮控制窗
  • 源码地址
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档