前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于Android录屏程序在Android10下的修改

关于Android录屏程序在Android10下的修改

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

学更好的别人,

做更好的自己。

——《微卡智享》

本文长度为1769,预计阅读4分钟

前言

上一篇《Android制作带悬浮窗控制的录屏程序Demo》我自己用的虚拟机是Android8的版本,后来用自己的手机无法使用,原因是在Android 10之后录屏等功能要求在前台Service中进行,所以如果你的设备是Android 10以上的 ,上一篇中的录屏就不能用了,所以这篇是专门针对Android 10录屏做的改动。

TIPS

由于最近抽空是想做个局域网内Windows远程的Android程序,整个程序的源码最做完后再发,这里只先把解决Android 10录屏的核心代码放上。

顺便说下这个整个程序估计要花些时间了,Android录屏的图像上传上去是H264的格式,而C#端是需要做解码显示的,就要考虑还要学习FFmgep进行视频的解码,以前没接触过,所以这次也是一个新的尝试。

#

Android 10的录屏注意事项

1

创建一个Service服务,用于在Android 10后启动录幕

2

manifests中要加入前台服务的权限和<service>

3

调用录屏时判断Android的SDK如果大于Q版本启动前台服务,如果小于的话还是用原来的录屏方式即可。

代码实现

微卡智享

01

创建Service的服务

代码语言:javascript
复制
package pers.vaccae.screendevice.utils

import android.content.Intent
import android.os.IBinder
import android.media.projection.MediaProjection

import android.media.projection.MediaProjectionManager

import android.os.Build

import android.R
import android.app.*
import android.content.Context

import android.graphics.BitmapFactory

import pers.vaccae.screendevice.MainActivity
import java.util.*


/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:14:47
 * 功能模块说明:
 */
class MediaScreenService : Service() {

    private var mResultCode = 0
    private var mResultData: Intent? = null

    override fun onBind(p0: Intent?): IBinder? {
        return null
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        createNotificationChannel();
        intent?.let {
            mResultCode = it.getIntExtra("code", -1);
            mResultData = it.getParcelableExtra("data");

            MediaPronUtil.getInstance()
                .startRecording(mResultData, false)
        }
        return super.onStartCommand(intent, flags, startId)
    }

    private fun createNotificationChannel() {
        val builder = Notification.Builder(this.applicationContext) //获取一个Notification构造器
        val nfIntent = Intent(this, MainActivity::class.java) //点击后跳转的界面,可以设置跳转数据
        builder.setContentIntent(PendingIntent.getActivity(this, 0, nfIntent, 0)) // 设置PendingIntent
            .setLargeIcon(
                BitmapFactory.decodeResource(
                    this.resources,
                    R.mipmap.sym_def_app_icon
                )
            ) // 设置下拉列表中的图标(大图标)
            //.setContentTitle("屏幕录制") // 设置下拉列表里的标题
            .setSmallIcon(R.mipmap.sym_def_app_icon) // 设置状态栏内的小图标
            .setContentText("正在获取屏幕流......") // 设置上下文内容
            .setWhen(System.currentTimeMillis()) // 设置该通知发生的时间

        /*以下是对Android 8.0的适配*/
        //普通notification适配
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setChannelId("notification_id")
        }
        //前台服务notification适配
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
            val channel = NotificationChannel(
                "notification_id",
                "notification_name",
                NotificationManager.IMPORTANCE_LOW
            )
            notificationManager.createNotificationChannel(channel)
        }
        val notification: Notification = builder.build() // 获取构建好的Notification
        notification.defaults = Notification.DEFAULT_SOUND //设置为默认的声音
        startForeground(110, notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        stopForeground(true)
    }
}

02

Manifests加入权限

代码语言:javascript
复制
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ScreenDevice">

        <service android:name=".utils.MediaScreenService"
            android:enabled="true"
            android:foregroundServiceType="mediaProjection"/>

    </application>

03

调用录屏时的SDK判断

代码语言:javascript
复制
   override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        Log.i("video", "Activity:$requestCode $resultCode")
        super.onActivityResult(requestCode, resultCode, data)
        try {
            if (requestCode == MediaPronUtil.RECORD_REQUEST_CODE) {
                if (resultCode == RESULT_OK) {
                    //开启悬浮框
                    startjobservice()
                    //开始录制
                    //判断系统版本选择不同处理方法
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                        //启动前台服务
                        var service = Intent(this, MediaScreenService::class.java)
                        service.putExtra("code", resultCode)
                        service.putExtra("data", data)
                        startForegroundService(service)
                    } else {
                        MediaPronUtil.getInstance()
                            .startRecording(data, false)
                    }

                } else {
                    Toast.makeText(
                        this, "用戶拒绝录制屏幕", Toast.LENGTH_SHORT
                    ).show();
                }
            }
        } catch (e: Exception) {
            Toast.makeText(
                this, e.message.toString(), Toast.LENGTH_SHORT
            ).show();
        }
    }

04

重新贴一下录屏的封装类

代码语言:javascript
复制
package pers.vaccae.screendevice.utils

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 com.jeremyliao.liveeventbus.LiveEventBus
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
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 startRecording(data: Intent?, mprn:MediaProjection, issavefile: Boolean = true) {
        isSaveFile = issavefile
        data?.let {
//            mMediaPron = mediaProMng.getMediaProjection(RESULT_OK, it);
            mMediaPron = mprn
            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)

                if (isSaveFile) {
                    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;
                }

                //发送图像数据
                LiveEventBus.get<ByteArray>("MediaData")
                    .post(h264RawFrame)
            }
        } 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
    }
}

完成上面这四个步骤,在Android 10下的录屏问题也解决了,由于我自己的Demo程序是通过网络通讯开启录屏的,所以这里就不做视频演示了,等整个程序成型时再做视频。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档