当前位置: 代码网 > it编程>App开发>Android > Android实现两台手机屏幕共享和远程控制功能

Android实现两台手机屏幕共享和远程控制功能

2025年04月22日 Android 我要评论
一、项目概述在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 android 手机之间的屏幕共享与远程控制,其核心功

一、项目概述

在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 android 手机之间的屏幕共享与远程控制,其核心功能包括:

  • 主控端(controller):捕获自身屏幕并将实时画面编码后通过网络发送;同时监听用户在主控端的触摸、滑动和按键等输入操作,并将操作事件发送至受控端。

  • 受控端(receiver):接收屏幕画面数据并实时解码、渲染到本地界面;接收并解析主控端的输入操作事件,通过系统接口模拟触摸和按键,实现被控设备的操作。

通过这一方案,用户可以实时“看到”受控端的屏幕,并在主控端进行点触、滑动等交互,达到“远程操控”他机的效果。本项目的核心难点在于如何保证图像数据的实时性与清晰度,以及如何准确、及时地模拟输入事件。

二、相关知识

2.1 mediaprojection api

  • 概述:android 5.0(api 21)引入的屏幕录制和投影接口。通过 mediaprojectionmanager 获取用户授权后,可创建 virtualdisplay,将屏幕内容输送至 surface 或 imagereader

  • 关键类

    • mediaprojectionmanager:请求屏幕捕获权限

    • mediaprojection:执行屏幕捕获

    • virtualdisplay:虚拟显示、输出到 surface

    • imagereader:以 image 帧的方式获取屏幕图像

2.2 socket 网络通信

  • 概述:基于 tcp 协议的双向流式通信,适合大块数据的稳定传输。

  • 关键类

    • serversocket / socket:服务端监听与客户端连接

    • inputstream / outputstream:数据读写

  • 注意:需要设计简单高效的协议,在发送每帧图像前加上帧头(如长度信息),以便接收端正确分包、组帧。

2.3 输入事件模拟

  • 概述:在非系统应用中无法直接使用 inputmanager 注入事件,需要借助无障碍服务(accessibilityservice)或系统签名权限。

  • 关键技术

    • 无障碍服务(accessibilityservice)注入触摸事件

    • 使用 gesturedescription 构造手势并通过 dispatchgesture 触发

2.4 数据压缩与传输优化

  • 图像编码:将 image 帧转为 jpeg 或 h.264,以减小带宽占用。

  • 数据分片:对大帧进行分片发送,防止单次写入阻塞或触发 outofmemoryerror

  • 网络缓冲与重传:tcp 本身提供重传,但需控制合适的发送速率,防止拥塞。

2.5 多线程与异步处理

  • 概述:屏幕捕获与网络传输耗时,需放在独立线程或 handlerthread 中,否则 ui 会卡顿。

  • 框架

    • threadpoolexecutor 管理捕获、编码、发送任务

    • handlerthread 配合 handler 处理 io 回调

三、实现思路

3.1 架构设计

+--------------+                                +--------------+
|              |--(请求授权)------------------->|              |
| mainactivity |                                | remoteactivity|
|              |<-(启动服务、连接成功)-----------|              |
+------+-------+                                +------+-------+
       |                                                |
       | 捕获屏幕 -> mediaprojection -> imagereader      | 接收画面 -> 解码 -> surfaceview
       | 编码(jpeg/h.264)                               | 
       | 发送 -> socket outputstream                     | 
       |                                                | 接收事件 -> 无障碍 service -> dispatchgesture
       |<--触摸事件包------------------------------------|
       | 模拟触摸 => accessibilityservice                |
+------+-------+                                +------+-------+
| screenshare  |                                | remotecontrol|
|   service    |                                |   service    |
+--------------+                                +--------------+

3.2 协议与数据格式

  • 帧头结构(12 字节)

    • 4 字节:帧类型(0x01 表示图像,0x02 表示触摸事件)

    • 4 字节:数据长度 n(网络字节序)

    • 4 字节:时间戳(毫秒)

  • 图像帧数据[帧头][jpeg 数据]

  • 触摸事件数据

    • 1 字节:事件类型(0:down,1:move,2:up)

    • 4 字节:x 坐标(float)

    • 4 字节:y 坐标(float)

    • 8 字节:时间戳

3.3 屏幕捕获与编码

  1. 主控端调用 mediaprojectionmanager.createscreencaptureintent(),请求授权。

  2. 授权通过后,获取 mediaprojection,创建 virtualdisplay 并绑定 imagereader.getsurface()

  3. 在独立线程中,通过 imagereader.acquirelatestimage() 不断获取原始 image

  4. 将 image 转为 bitmap,然后使用 bitmap.compress(bitmap.compressformat.jpeg, 50, outputstream) 编码。

  5. 将 jpeg 字节根据协议拼接帧头,发送至受控端。

3.4 网络传输与解码

主控端

  • 使用单例 socketclient 管理连接。

  • 将编码后的帧数据写入 bufferedoutputstream,并在必要时调用 flush()

受控端

  • 启动 screenreceiverservice,监听端口,接受连接。

  • 使用 bufferedinputstream,先读取 12 字节帧头,再根据长度读完数据。

  • 将 jpeg 数据用 bitmapfactory.decodebytearray() 解码,更新到 surfaceview

3.5 输入事件捕获与模拟

主控端

  • 在 mainactivity 上监听触摸事件 ontouchevent(motionevent),提取事件类型与坐标。

  • 按协议封装成事件帧,发送至受控端。

受控端

  • remotecontrolservice 接收事件帧后,通过无障碍接口构造 gesturedescription

path path = new path();
path.moveto(x, y);
gesturedescription.strokedescription stroke = new gesturedescription.strokedescription(path, 0, 1);
  • 调用 dispatchgesture(stroke, callback, handler) 注入触摸。

四、完整代码

/************************** mainactivity.java **************************/
package com.example.screencast;
 
import android.app.activity;
import android.app.alertdialog;
import android.content.context;
import android.content.intent;
import android.graphics.pixelformat;
import android.media.image;
import android.media.imagereader;
import android.media.projection.mediaprojection;
import android.media.projection.mediaprojectionmanager;
import android.os.bundle;
import android.util.displaymetrics;
import android.view.motionevent;
import android.view.surfaceview;
import android.view.view;
import android.widget.button;
 
import java.io.bufferedoutputstream;
import java.io.bytearrayoutputstream;
import java.io.outputstream;
import java.net.socket;
 
/*
 * mainactivity:负责
 * 1. 请求屏幕捕获权限
 * 2. 启动 screenshareservice
 * 3. 捕获触摸事件并发送
 */
public class mainactivity extends activity {
    private static final int request_code_capture = 100;
    private mediaprojectionmanager mprojectionmanager;
    private mediaprojection mmediaprojection;
    private imagereader mimagereader;
    private virtualdisplay mvirtualdisplay;
    private screenshareservice mshareservice;
    private button mstartbtn, mstopbtn;
    private socket msocket;
    private bufferedoutputstream mout;
 
    @override
    protected void oncreate(bundle savedinstancestate) {
        super.oncreate(savedinstancestate);
        setcontentview(r.layout.activity_main);
        mstartbtn = findviewbyid(r.id.btn_start);
        mstopbtn = findviewbyid(r.id.btn_stop);
 
        // 点击开始:请求授权并启动服务
        mstartbtn.setonclicklistener(v -> startcapture());
        // 点击停止:停止服务并断开连接
        mstopbtn.setonclicklistener(v -> {
            mshareservice.stop();
        });
    }
 
    /** 请求屏幕捕获授权 */
    private void startcapture() {
        mprojectionmanager = (mediaprojectionmanager) getsystemservice(context.media_projection_service);
        startactivityforresult(mprojectionmanager.createscreencaptureintent(), request_code_capture);
    }
 
    @override
    protected void onactivityresult(int requestcode, int resultcode, intent data) {
        if (requestcode == request_code_capture && resultcode == result_ok) {
            mmediaprojection = mprojectionmanager.getmediaprojection(resultcode, data);
            // 初始化 imagereader 和 virtualdisplay
            setupvirtualdisplay();
            // 启动服务
            mshareservice = new screenshareservice(mmediaprojection, mimagereader);
            mshareservice.start();
        }
    }
 
    /** 初始化虚拟显示器用于屏幕捕获 */
    private void setupvirtualdisplay() {
        displaymetrics metrics = getresources().getdisplaymetrics();
        mimagereader = imagereader.newinstance(metrics.widthpixels, metrics.heightpixels,
                                               pixelformat.rgba_8888, 2);
        mvirtualdisplay = mmediaprojection.createvirtualdisplay("screencast",
                metrics.widthpixels, metrics.heightpixels, metrics.densitydpi,
                displaymanager.virtual_display_flag_auto_mirror,
                mimagereader.getsurface(), null, null);
    }
 
    /** 捕获触摸事件并发送至受控端 */
    @override
    public boolean ontouchevent(motionevent event) {
        if (mshareservice != null && mshareservice.isrunning()) {
            mshareservice.sendtouchevent(event);
        }
        return super.ontouchevent(event);
    }
}
 
/************************** screenshareservice.java **************************/
package com.example.screencast;
 
import android.graphics.bitmap;
import android.graphics.imageformat;
import android.media.image;
import android.media.imagereader;
import android.media.projection.mediaprojection;
import android.os.handler;
import android.os.handlerthread;
import android.util.log;
 
import java.io.bufferedoutputstream;
import java.io.bytearrayoutputstream;
import java.net.socket;
 
/*
 * screenshareservice:负责
 * 1. 建立 socket 连接
 * 2. 从 imagereader 获取屏幕帧
 * 3. 编码后发送
 * 4. 接收触摸事件发送
 */
public class screenshareservice {
    private mediaprojection mprojection;
    private imagereader mimagereader;
    private socket msocket;
    private bufferedoutputstream mout;
    private volatile boolean mrunning;
    private handlerthread mencodethread;
    private handler mencodehandler;
 
    public screenshareservice(mediaprojection projection, imagereader reader) {
        mprojection = projection;
        mimagereader = reader;
        // 创建后台线程处理编码与网络
        mencodethread = new handlerthread("encodethread");
        mencodethread.start();
        mencodehandler = new handler(mencodethread.getlooper());
    }
 
    /** 启动服务:连接服务器并开始捕获发送 */
    public void start() {
        mrunning = true;
        mencodehandler.post(this::connectandshare);
    }
 
    /** 停止服务 */
    public void stop() {
        mrunning = false;
        try {
            if (msocket != null) msocket.close();
            mencodethread.quitsafely();
        } catch (exception ignored) {}
    }
 
    /** 建立 socket 连接并循环捕获发送 */
    private void connectandshare() {
        try {
            msocket = new socket("192.168.1.100", 8888);
            mout = new bufferedoutputstream(msocket.getoutputstream());
            while (mrunning) {
                image image = mimagereader.acquirelatestimage();
                if (image != null) {
                    sendimageframe(image);
                    image.close();
                }
            }
        } catch (exception e) {
            log.e("screenshare", "连接或发送失败", e);
        }
    }
 
    /** 发送图像帧 */
    private void sendimageframe(image image) throws exception {
        // 将 image 转 bitmap、压缩为 jpeg
        image.plane plane = image.getplanes()[0];
        bytebuffer buffer = plane.getbuffer();
        int width = image.getwidth(), height = image.getheight();
        bitmap bmp = bitmap.createbitmap(width, height, bitmap.config.argb_8888);
        bmp.copypixelsfrombuffer(buffer);
 
        bytearrayoutputstream baos = new bytearrayoutputstream();
        bmp.compress(bitmap.compressformat.jpeg, 40, baos);
        byte[] jpegdata = baos.tobytearray();
 
        // 写帧头:类型=1, 长度, 时间戳
        mout.write(inttobytes(1));
        mout.write(inttobytes(jpegdata.length));
        mout.write(longtobytes(system.currenttimemillis()));
        // 写图像数据
        mout.write(jpegdata);
        mout.flush();
    }
 
    /** 发送触摸事件 */
    public void sendtouchevent(motionevent ev) {
        try {
            bytearrayoutputstream baos = new bytearrayoutputstream();
            baos.write((byte) ev.getaction());
            baos.write(floattobytes(ev.getx()));
            baos.write(floattobytes(ev.gety()));
            baos.write(longtobytes(ev.geteventtime()));
            byte[] data = baos.tobytearray();
 
            mout.write(inttobytes(2));
            mout.write(inttobytes(data.length));
            mout.write(longtobytes(system.currenttimemillis()));
            mout.write(data);
            mout.flush();
        } catch (exception ignored) {}
    }
 
    // …(byte/int/long/float 与 bytes 相互转换方法,略)
}
 
/************************** remotecontrolservice.java **************************/
package com.example.screencast;
 
import android.accessibilityservice.accessibilityservice;
import android.graphics.path;
import android.view.accessibility.gesturedescription;
 
import java.io.bufferedinputstream;
import java.io.inputstream;
import java.net.serversocket;
import java.net.socket;
 
/*
 * remotecontrolservice(继承 accessibilityservice)
 * 1. 启动 serversocket,接收主控端连接
 * 2. 循环读取帧头与数据
 * 3. 区分图像帧与事件帧并处理
 */
public class remotecontrolservice extends accessibilityservice {
    private serversocket mserversocket;
    private socket mclient;
    private bufferedinputstream min;
    private volatile boolean mrunning;
 
    @override
    public void onserviceconnected() {
        super.onserviceconnected();
        new thread(this::startserver).start();
    }
 
    /** 启动服务端 socket */
    private void startserver() {
        try {
            mserversocket = new serversocket(8888);
            mclient = mserversocket.accept();
            min = new bufferedinputstream(mclient.getinputstream());
            mrunning = true;
            while (mrunning) {
                handleframe();
            }
        } catch (exception e) {
            e.printstacktrace();
        }
    }
 
    /** 处理每个数据帧 */
    private void handleframe() throws exception {
        byte[] header = new byte[12];
        min.read(header);
        int type = bytestoint(header, 0);
        int len = bytestoint(header, 4);
        // long ts = bytestolong(header, 8);
 
        byte[] payload = new byte[len];
        int read = 0;
        while (read < len) {
            read += min.read(payload, read, len - read);
        }
 
        if (type == 1) {
            // 图像帧:解码并渲染到 surfaceview
            handleimageframe(payload);
        } else if (type == 2) {
            // 触摸事件:模拟
            handletouchevent(payload);
        }
    }
 
    /** 解码 jpeg 并更新 ui(通过 broadcast 或 handler 通信) */
    private void handleimageframe(byte[] data) {
        // …(略,解码 bitmap 并 post 到 surfaceview)
    }
 
    /** 根据协议解析并 dispatchgesture */
    private void handletouchevent(byte[] data) {
        int action = data[0];
        float x = bytestofloat(data, 1);
        float y = bytestofloat(data, 5);
        // long t = bytestolong(data, 9);
 
        path path = new path();
        path.moveto(x, y);
        gesturedescription.strokedescription sd =
                new gesturedescription.strokedescription(path, 0, 1);
        dispatchgesture(new gesturedescription.builder().addstroke(sd).build(),
                        null, null);
    }
 
    @override
    public void oninterrupt() {}
}
<!-- androidmanifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.screencast">
    <uses-permission android:name="android.permission.foreground_service"/>
    <uses-permission android:name="android.permission.system_alert_window"/>
    <uses-permission android:name="android.permission.write_external_storage"/>
    <application
        android:allowbackup="true"
        android:label="screencast">
        <activity android:name=".mainactivity">
            <intent-filter>
                <action android:name="android.intent.action.main"/>
                <category android:name="android.intent.category.launcher"/>
            </intent-filter>
        </activity>
        <service android:name=".remotecontrolservice"
                 android:permission="android.permission.bind_accessibility_service">
            <intent-filter>
                <action android:name="android.accessibilityservice.accessibilityservice"/>
            </intent-filter>
            <meta-data
                android:name="android.accessibilityservice"
                android:resource="@xml/accessibility_service_config"/>
        </service>
    </application>
</manifest>
<!-- activity_main.xml -->
<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent" android:gravity="center">
    <button
        android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始屏幕共享"/>
    <button
        android:id="@+id/btn_stop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="停止服务"/>
    <surfaceview
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</linearlayout>

五、代码解读

  1. mainactivity

    • 请求并处理用户授权,创建并绑定 virtualdisplay

    • 启动 screenshareservice 负责捕获与发送;

    • 重写 ontouchevent,将触摸事件传给服务。

  2. screenshareservice

    • 在后台线程中建立 tcp 连接;

    • 循环从 imagereader 获取帧,将其转为 bitmap 并压缩后通过 socket 发送;

    • 监听主控端触摸事件,封装并发送事件帧。

  3. remotecontrolservice

    • 作为无障碍服务启动,监听端口接收数据;

    • 读取帧头与载荷,根据类型分发到图像处理或触摸处理;

    • 触摸处理时使用 dispatchgesture 注入轨迹,实现远程控制。

  4. 布局与权限

    • 在 androidmanifest.xml 中声明必要权限与无障碍服务;

    • activity_main.xml 简单布局包含按钮与 surfaceview 用于渲染。

六、项目总结

通过本项目,我们完整地实现了 android 平台上两台设备的屏幕共享与远程控制功能,掌握并综合运用了以下关键技术:

  • mediaprojection api:原生屏幕捕获与虚拟显示创建;

  • socket 编程:设计帧协议,实现高效、可靠的图像与事件双向传输;

  • 图像编码/解码:将屏幕帧压缩为 jpeg,平衡清晰度与带宽;

  • 无障碍服务:通过 dispatchgesture 注入触摸事件,完成远程控制;

  • 多线程处理:使用 handlerthread 保证捕获、编码、传输等实时性,避免 ui 阻塞。

这套方案具备以下扩展方向:

  1. 音频同步:在屏幕共享同时传输麦克风或系统音频。

  2. 视频编解码优化:引入硬件 h.264 编码,以更低延迟和更高压缩率。

  3. 跨平台支持:在 ios、windows 等平台实现对应客户端。

  4. 安全性增强:加入 tls/ssl 加密,防止中间人攻击;验证设备身份。

以上就是android实现两台手机屏幕共享和远程控制功能的详细内容,更多关于android手机屏幕共享和远程控制的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com