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

学更好的别人,

做更好的自己。

——《微卡智享》

9c8244cb0140985a8f903efd8bbe6d6b.png

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

前言

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

264fc884e0515f603f8acf5d44276f9f.png

实现效果

2df0f30e87b3570a33d04506b24f6a91.gif

代码实现

d35caead54d54334600ddf374dd394c7.png

微卡智享

采用的组件

MediaProjectionManager

MediaProjection

MediaCodec

MediaMuxer

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

核心代码

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

MediaPronUtil类代码:

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里面判断是否允许录制,并开启录制。

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()
            }
        }
    }

动态申请权限的方式

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实现可移动的悬浮窗

5116054bd9d247c373569dd492bb0f13.png

FloatWindowJobService

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后还需要开启悬浮窗的上层权限 ,所以在动态申请权限那里还要加上这个开启。

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

a771e9c3d15d0b58eb01106dcc028980.png

实现画面

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

源码地址

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

92ccab692d80d90d670bfbd473110146.png

扫描二维码

获取更多精彩

微卡智享

85b96832399186b14e37bc20f14f304f.png

「 往期文章 」

实现Android本地Sqlite数据库网络传输到PC端

Android通讯库VNanoMsg的1.0.4发布

Android使用BaseSectionQuickAdapter动态生成不规则宫格

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是一个简单的示例代码,可以实现Android 5.0及以上版本中使用MediaProjection API进行屏幕录制: ``` public class ScreenRecorder { private static final String TAG = "ScreenRecorder"; private MediaProjection mMediaProjection; private VirtualDisplay mVirtualDisplay; private MediaRecorder mMediaRecorder; private int mScreenWidth; private int mScreenHeight; private int mScreenDensity; private boolean mIsRecording; public ScreenRecorder(MediaProjection mediaProjection, int screenWidth, int screenHeight, int screenDensity) { mMediaProjection = mediaProjection; mScreenWidth = screenWidth; mScreenHeight = screenHeight; mScreenDensity = screenDensity; } public boolean isRecording() { return mIsRecording; } public void startRecording(String filePath) { mMediaRecorder = new MediaRecorder(); try { mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mMediaRecorder.setVideoEncodingBitRate(512 * 1000); mMediaRecorder.setVideoFrameRate(30); mMediaRecorder.setVideoSize(mScreenWidth, mScreenHeight); mMediaRecorder.setOutputFile(filePath); mMediaRecorder.prepare(); } catch (IOException e) { e.printStackTrace(); } mVirtualDisplay = mMediaProjection.createVirtualDisplay(TAG, mScreenWidth, mScreenHeight, mScreenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null); mMediaRecorder.start(); mIsRecording = true; } public void stopRecording() { if (mMediaRecorder != null) { mMediaRecorder.stop(); mMediaRecorder.release(); mMediaRecorder = null; } if (mVirtualDisplay != null) { mVirtualDisplay.release(); mVirtualDisplay = null; } if (mMediaProjection != null) { mMediaProjection.stop(); mMediaProjection = null; } mIsRecording = false; } } ``` 使用该类进行屏幕录制的示例代码如下: ``` private MediaProjectionManager mMediaProjectionManager; private ScreenRecorder mScreenRecorder; private void startScreenRecord() { if (mMediaProjectionManager == null) { mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); } if (mScreenRecorder == null) { int screenWidth = getResources().getDisplayMetrics().widthPixels; int screenHeight = getResources().getDisplayMetrics().heightPixels; int screenDensity = getResources().getDisplayMetrics().densityDpi; mScreenRecorder = new ScreenRecorder(mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, mResultData), screenWidth, screenHeight, screenDensity); } if (!mScreenRecorder.isRecording()) { mScreenRecorder.startRecording(Environment.getExternalStorageDirectory() + "/test.mp4"); } } private void stopScreenRecord() { if (mScreenRecorder != null && mScreenRecorder.isRecording()) { mScreenRecorder.stopRecording(); } } ``` 需要注意的是,使用该示例代码进行屏幕录制需要先获取悬浮窗权限和录音权限。同时,还需要在AndroidManifest.xml文件中添加以下权限: ``` <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vaccae

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值