开发者

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

目录
  • 一、项目概述
  • 二、相关知识
    • 2.1 MediaProjection API
    • 2.2 Socket 网络通信
    • 2.3 输入事件模拟
    • 2.4 数据压缩与传输优化
    • 2.5 多线程与异步处理
  • 三、实现思路
    • 3.1 架构设计
    • 3.2 协议与数据格式
    • 3.3 屏幕捕获与编码
    • 3.4 网络传输与解码
      • 主控端
      • 受控端
    • 3.5 输入事件捕获与模拟
      • 主控端
      • 受控端
  • 四、完整代码
    • 五、代码解读
      • 六、项目总结

        一、项目概述

        在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 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 管理捕获、编码、发送任javascript务

          • 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.Almkynet.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;
         
        /*
         * ScreenSharphpeService:负责
         * 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() {
               js 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手机屏幕共享和远程控制的资料请关注编程客栈(www.devze.com)其它相关文章!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜