Android投屏,设备远程协助,被远程服务浏览器上面操控屏幕如何实现?

1,766 阅读6分钟

14b5fee4-67b0-4c3d-a092-b1e02cbcb098.jpg

Android 设备开发中,远程协助是一项重要的工具
相关系列文章:
(一)如何拦截其他Android应用程序播放器的原始音频数据自定义保存下来?
(二)Android拦截其它播放声音:内录音,外录音,录屏,剪辑,混音,一键制作大片全搞定 (三)Android投屏,设备远程协助,被远程服务浏览器上面操控屏幕如何实现?

一、前言

在Android 终端设备开发中,比如广告大屏,收银机,收银秤,或者自助设备机器,如医院,地铁,酒店,机场等设备,往往都内置了一个系统级别的远程协助的App,该App主要功能作用是:可以方便远程操作看业务问题,或者方便技术人员排查系统问题,毕竟很大一块设备放在外面,并不像手机那样容易搬运。

今天来给大家分享一下,Android 远程协助App开发实现是怎么样的?

首先必须考虑的是四个问题:

  1. 设备上面App是怎么录屏的?
  2. 设备端录屏数据和远程服务端通信,数据怎么传输的?
  3. 远程服务端接收到录屏数据怎么显示的?
  4. 远程服务端操作显示的界面怎么同步操作设备上面的屏幕的?

二、设备上面App是怎么录屏的?

  • 我前面文章讲过录屏,是直接通过 MediaRecorderMediaProjection 联合使用直接录制成MP4格式,这种录屏方式,简单,MediaRecorder 内部直接拿到 录屏的YUV数据然后处理加工保存成MP4了。
  • 但是想要把录屏的每一帧画面的YUV数据拿到并传输出去,在服务端实时显示,得使用: MediaCodec - +MediaProjection。如下:
  1. 准备好 MediaProjection(详见我前面文章如何拦截其他Android应用程序播放器的原始音频数据自定义保存下来?) 配置录制视频相关参数,如下代码采用h265,也可以采用h264编码:
public H265Encoder(MediaProjection mMediaProjection) {
    this.mMediaProjection = mMediaProjection;
    this.width = 640;
    this.height = 1920;
    try {
        MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_HEVC, width, height);
        mediaCodec = MediaCodec.createEncoderByType("video/hevc");
        //帧率 1秒 20帧
        mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 20);
        //30帧    一个I帧
        mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 30);
        // 码率     width heigth  帧  码率
        mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height);
        //编码来源
        mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        // 后面第4个参数   编码标志位(CONFIGURE_FLAG_ENCODE为编码   , 0为解码)
        // 第2个参数  编码时候不需要传入,解码时候需要 第3个参数先不管??
        mediaCodec.configure(mediaFormat, null, null, CONFIGURE_FLAG_ENCODE);
        // 录屏和编码提供一个场地 上进行
        Surface surface = mediaCodec.createInputSurface();
        //name:绑定关系名字,要唯一, mMediaProjection 和 Surface 绑定
        //width:编码宽
        //height:编码高
        //dpi:2  一个dpi输出2个像素 越大越清晰
        //flag: DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC 绑定关系是公开的
        //surface
        mMediaProjection.createVirtualDisplay("wgllss_dipan", width, height, 2,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                surface, null, null
        );
    } catch (Exception e) {

    }
}

2. 开启MediaCodec后,获取到录屏的原始YUV数据后,直接发送出去。如下面代码,可以将outData直接发送出去,投屏,直播,都可以这么干。只是采用的通信协议不一样。

mediaCodec.start();
        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        while (isRuning) {
            //直接拿到输出,不用管输入,输入:mMediaProjection 已经被实现了
            int outIndex = mediaCodec.dequeueOutputBuffer(info, 1000);
            if (outIndex >= 0) {
                //编码的数据
                ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outIndex);
                //byteBuffer.remaining() 和 info。size 大小一样的
                byte[] outData = new byte[byteBuffer.remaining()];
                //将容器的byteBuffer  内部的数据 转移到 byte[]  outData中
                byteBuffer.get(outData);
                //outData 录屏数据可以进行推送给外部:如直播后台地址,或者投屏                                       mediaCodec.releaseOutputBuffer(outIndex, false);
            }
        }

3. 以上录屏投屏传屏,直播,原始逻辑都是这么干的,需要注意的是
1)申请录屏权限
2)将上述逻辑全部放在 服务(Service) 里面
3)Android 8以后服务需要开启前台服务结合通知栏使用

三、数据怎么传输通信的?

数据怎么传输通信?即是上面录屏后的数据outData怎么和服务端进行通信

  • 如果是投屏,或者视频会议,大多数情况下 采用WebRtc 就可以了。WebRTC(Web Real-Time Communication)是一个允许网页浏览器进行实时语音通话、视频聊天和P2P数据共享的开源项目 使用
dependencies {
    implementation 'org.webrtc:google-webrtc:1.0.+'
}
  • 直播一般采用 Rtmp, RTMP(Real Time Messaging Protocol)是一种协议,广泛用于视频和音频流的实时传输
  • 在远程协助上面,可以采用长连接或者WebSocket等传输数据,通俗来讲是因为它并不像视频会议或者直播那样同时存在多个客户端在使用。如下:
private WebSocketClient webSocketClient;

// 初始化WebSocket连接 
webSocketClient = new WebSocketClient(new URI("ws://yourserver.com/stream")) { 
     @Override public void onMessage(String message) {
          handleControlCommand(message); // 处理控制指令 
     } 
}; 
webSocketClient.connect();


//发送录屏帧数据YUV
private void sendVideoFrame(byte[] outData) {
   webSocketClient.send(outData);
}

四、远程服务端接收到录屏数据怎么显示的?

  1. 服务端代码通过Node.js,接收到设备端传输过来的视频数据帧,然后转发给浏览器。同时处理浏览器端输入的触控指令并回传至Android设备
// server.js
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

let androidSocket = null;
let browserSockets = new Set();

wss.on('connection', (ws) => {
    ws.on('message', (message) => {
        if (message.toString() === 'ANDROID_DEVICE') {
            androidSocket = ws; // 标识Android设备连接
        } else {
            // 转发控制指令到Android设备
            if (androidSocket && androidSocket.readyState === WebSocket.OPEN) {
                androidSocket.send(message);
            }
        }
    });

    // 转发视频流到浏览器
    if (ws !== androidSocket) {
        browserSockets.add(ws);
        ws.on('close', () => browserSockets.delete(ws));
    }
});

// 广播视频流
function broadcast(data) {
    browserSockets.forEach(client => {
        if (client.readyState === WebSocket.OPEN) {
            client.send(data);
        }
    });
}

2. 服务端浏览器核心代码,渲染设备传输过来的视频帧,显示并控制:基于WebAssembly实现H264软解码,通过Canvas渲染视频流,并监听用户鼠标/触控事件生成操作指令。

<!-- index.html -->
<canvas id="screen" width="1280" height="720"></canvas>

<script>
const canvas = document.getElementById('screen');
const ws = new WebSocket('ws://yourserver.com:8080');

// 初始化H264解码器
const decoder = new BroadwayJS.Decoder();
decoder.onPictureDecoded = (buffer) => {
    renderToCanvas(buffer); // 使用WebGL渲染
};

// 接收视频流
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
    if (event.data instanceof ArrayBuffer) {
        decoder.decode(new Uint8Array(event.data));
    }
};

// 发送控制指令
canvas.addEventListener('mousedown', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (1280 / rect.width);
    const y = (e.clientY - rect.top) * (720 / rect.height);

    ws.send(JSON.stringify({
        type: 'tap',
        x: x.toFixed(2),
        y: y.toFixed(2)
    }));
});

// WebGL渲染函数
function renderToCanvas(yuvData) {
    const gl = canvas.getContext('webgl');
    // ... 具体YUV转RGB渲染逻辑
}
</script>

3. 浏览器需要依赖js

<script src="https://unpkg.com/broadwayjs/Decoder.js"></script>

五、 远程服务端操作显示的界面怎么同步操作设备上面的?

  • 浏览器上面监听Touch事件,拿到界面的x,y值,通过webSocket 发送给设备端,设备端接收到x,y值,模拟成操作事件
  1. 浏览器上面代码如下,监听事件并发送:
// 发送控制指令
canvas.addEventListener('mousedown', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = (e.clientX - rect.left) * (1280 / rect.width);
    const y = (e.clientY - rect.top) * (720 / rect.height);

    ws.send(JSON.stringify({
        type: 'tap',
        x: x.toFixed(2),
        y: y.toFixed(2)
    }));
});

2. 设备端上面:接收传入过来的x,y值模拟成Android设备上面操作事件

private void handleControlCommand(String json) { 
    // 解析JSON指令并执行 
    JSONObject cmd = new JSONObject(json);
    float x = cmd.getFloat("x"); 
    float y = cmd.getFloat("y"); 
    simulateTap(x, y); 
} 

private void simulateTap(float x, float y) { 
    // 使用AccessibilityService执行点击 
    GestureDescription gesture = new GestureDescription.Builder()
         .addStroke(new GestureDescription.StrokeDescription( 
            new Path().moveTo(x, y), 0, 50))         
         .build(); 
    accessibilityService.dispatchGesture(gesture, null, null); 
}

六、总结

本文重点介绍了:Android 投屏,设备远程协助的思路步骤:

  1. 如何录屏拿到录屏原始yuv帧数据
  2. 设备端和服务端如何通信?yuv数据如何传到服务端,服务端操作如何同步到设备端
  3. 服务端接收到yuv数据如何解码并显示到浏览器上面
  4. 服务端对浏览器上面操作如何同步到设备端,并在设备端模拟成相应的操作

感谢阅读:

欢迎用你发财的小手 关注,点赞、收藏

这里你会学到不一样的东西