远程控制平台二之高效率录屏

内容优化

前面说的受控端推流,这个“流”的数据就来源于录屏,那么我们是否只是简单录屏呢?显然不是的,我们的要求只是看到远程设备的屏幕以及操控远程设备,所以,图像是必须的,但声音是多余的,至少在我们目前这个项目是这样,所以,我们在录屏的时候,只需使用ImageReader截取受控端屏幕画面传输即可。

ImageReader.newInstance(ScreenManager.displayWidth, ScreenManager.displayHeight, PixelFormat.RGBA_8888, 1)

画质优化

考虑到个人的服务器一般性能较低,带宽较小,我们的定位是“能看清受控端屏幕并远程操控”即可,所以肯定不能高保真无损地传输受控端画面,所以我们在获取到屏幕的image数据后,进行压缩时,使用的质量参数传1,尽可能减少要传递的数据量。

bitmap = Bitmap.createBitmap(displayWidth + rowPadding / pixelStride, displayHeight, Bitmap.Config.ARGB_8888)
bitmap.copyPixelsFromBuffer(buffer)

// 裁剪多余的黑边
val croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, displayWidth, displayHeight)
// 质量参数传1
croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 1, stream)
croppedBitmap.recycle()

这里我们注意到是引入多了一个croppedBitmap,是因为在实际运行过程中,部分手机出现大黑边,所以要进行裁剪,详细实现如下:

val stream = ByteArrayOutputStream()
var bitmap : Bitmap? = null
try {
    val buffer = image.planes[0].buffer
    val pixelStride = image.planes[0].pixelStride
    val rowStride = image.planes[0].rowStride
    val rowPadding = rowStride - pixelStride * displayWidth

    // create bitmap
    bitmap = Bitmap.createBitmap(displayWidth + rowPadding / pixelStride, displayHeight, Bitmap.Config.ARGB_8888)
    bitmap.copyPixelsFromBuffer(buffer)

    // 裁剪多余的黑边
    val croppedBitmap = Bitmap.createBitmap(bitmap, 0, 0, displayWidth, displayHeight)
    croppedBitmap.compress(Bitmap.CompressFormat.JPEG, 1, stream)
    croppedBitmap.recycle()

    // 推送屏幕画面数据
    val screenData = stream.toByteArray()
    publishScreenData(screenData)
} catch (e: Exception) {
    e.printStackTrace()
}

深度压缩

这样就已经是极限了吗?并不是,我们得到的图像数据最终会转换成一个byte数组,可以在受控端对这个byte数组进一步压缩,这里使用了jzlib这个压缩库。最终,我们一帧数据甚至只有几KB(6KB-40KB之间浮动,和设备分辨率和具体画面有关)

录屏时机

在录屏ImageReader.newInstance()的时候,我们传递的是1这个参数,表示只截屏一次,而不是传一个非常大的数值(因为Google文档已经说明了,这个参数会对性能有影响)。只截屏一次如何实现连续截屏?其实很简单,我们起一个单独的线程,用一个while循环去截屏,每次只截一张,只要受控端没停止服务,就不会跳出这个循环,这样就实现了连续录屏。但并不是暴力地循环截屏,如果主控端没有在线,那么这个时候去录屏和推流是没有意义的,徒耗性能且增加了流量费用,所以我们在没有主控端连接的情况下(1分钟内没收到主控端的操控message即视为主控端下线),让线程休眠一小段时间(50毫秒?100毫秒?可以自己定),因为16帧/秒看起来就没有卡顿了,超过这个频率没有意义,而我们的场景,为了能降低延迟,稍稍卡顿也是可接受的,所以我这里就简单地让两次截屏时间间隔不少于100毫秒,如果间隔小于100毫秒,就让线程先休眠50毫秒,也就是说,一秒的视频画面不能多于10帧。

imageExecutorService.submit {
    while (publishStreamSocket != null) {
        if (System.currentTimeMillis() - lastImageMillis < IMAGE_IDLE_MILLIS) {
            try {
                Thread.sleep(50)
            } catch (e: Exception) {
                L.e(e.message)
            }
            continue
        }

        lastImageMillis = System.currentTimeMillis()
        // 没打开无障碍权限或者1分钟内有控制指令(主控端没有离开)都要推流
        if (!accessibilityEnabled || !MessageReceiver.isControlLeave()) {
            createVirtualDisplay()
        } else {
            // 不需要当前设备推流的时候停止录屏,提高性能
            mediaProjection?.stop()
            mediaProjection = null
            try {
                Thread.sleep(2000)
            } catch (e: Exception) {
                L.e(e.message)
            }
        }
    }
}

高效渲染

接收到受控端经服务器推过来的图片数据后,除了解压缩就是渲染了,难道我们用ImageView直接显示吗?当然不是,对于需要频繁刷新画面的场景,当然是要祭出我们的SurfaceView了。

private fun startPreviewVideo() {
    mZContext = ZContext()
    mSocket = mZContext!!.createSocket(SocketType.SUB)
    // 身份认证
    mSocket.enableCommonAuth()
    // 延迟控制
    mSocket.enableCommonStream()
    mSocket.connect(serverUrl)
    mSocket.subscribe("".toByteArray())
    val paint = Paint()
    var widthScale = -1.0f
    var heightScale = -1.0f
    while (mSurfaceHolder != null && mZContext != null && !mZContext!!.isClosed && !Thread.currentThread().isInterrupted) {
        try {
            val receivedData = mSocket.recv()
            if(receivedData == null || receivedData.isEmpty() || mSurfaceHolder?.surface?.isValid == false) {
                continue
            }

            val imageData = ZipUtils.unJzlib(receivedData)
            val bitmap = BitmapFactory.decodeByteArray(imageData, 0, imageData.size, options)
            if (widthScale < 0) {
                widthScale = mSurfaceView.width * 1.0f / bitmap.width
                heightScale = mSurfaceView.height * 1.0f / bitmap.height
                matrix.setScale(widthScale, heightScale)
            }

            val canvas: Canvas? = mSurfaceHolder?.lockCanvas()
            canvas?.drawBitmap(
                Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true), 
                0f, 
                0f, 
                paint
            )
            if(mSurfaceHolder?.surface?.isValid == true) {
                mSurfaceHolder?.unlockCanvasAndPost(canvas)
            }

            bitmap.recycle()
        } catch (e:Exception) {
            e.printStackTrace()
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ithouse

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

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

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

打赏作者

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

抵扣说明:

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

余额充值