Android实现两台手机屏幕共享和远程控制(附带源码)

一、项目概述

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

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

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

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


二、相关知识

2.1 MediaProjection API

  • 概述:Android 5.0(API 21)引入的屏幕录制和投影接口。通过 MediaProjectionManager 获取用户授权后,可创建 VirtualDisplay,将屏幕内容输送至 SurfaceImageReader

  • 关键类

    • 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 加密,防止中间人攻击;验证设备身份。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值