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

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

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

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

学更好的别人,

做更好的自己。

——《微卡智享》

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

前言

上一篇《Android制作带悬浮窗控制的录屏程序Demo

TIPS

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

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

#Android 10的录屏注意事项
1创建一个Service服务,用于在Android 10后启动录幕
2manifests中要加入前台服务的权限和
3调用录屏时判断Android的SDK如果大于Q版本启动前台服务,如果小于的话还是用原来的录屏方式即可。

代码实现

微卡智享

01

创建Service的服务

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.*





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()) // 设置该通知发生的时间


        
        //普通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加入权限


    
    


        


    

03

调用录屏时的SDK判断

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

重新贴一下录屏的封装类

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



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()
                        
                        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("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程序是通过网络通讯开启录屏的,所以这里就不做视频演示了,等整个程序成型时再做视频。

扫描二维码

获取更多精彩

微卡智享

「 往期文章 」

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

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

Android通讯库VNanoMsg的1.0.4发布

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

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

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