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 屏幕捕获与编码

主控端调用 MediaProjectionManager.createScreenCaptureIntent(),请求授权。

授权通过后,获取 MediaProjection,创建 VirtualDisplay 并绑定 ImageReader.getSurface()。

在独立线程中,通过 ImageReader.acquireLatestImage() 不断获取原始 Image。

将 Image 转为 Bitmap,然后使用 Bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream) 编码。

将 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() {}

}

package="com.example.screencast">

android:allowBackup="true"

android:label="ScreenCast">

android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

android:name="android.accessibilityservice"

android:resource="@xml/accessibility_service_config"/>

android:orientation="vertical" android:layout_width="match_parent"

android:layout_height="match_parent" android:gravity="center">

android:id="@+id/btn_start"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="开始屏幕共享"/>

android:id="@+id/btn_stop"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="停止服务"/>

android:id="@+id/surface_view"

android:layout_width="match_parent"

android:layout_height="match_parent"/>

五、代码解读

MainActivity

请求并处理用户授权,创建并绑定 VirtualDisplay;

启动 ScreenShareService 负责捕获与发送;

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

ScreenShareService

在后台线程中建立 TCP 连接;

循环从 ImageReader 获取帧,将其转为 Bitmap 并压缩后通过 Socket 发送;

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

RemoteControlService

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

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

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

布局与权限

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

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

六、项目总结

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

MediaProjection API:原生屏幕捕获与虚拟显示创建;

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

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

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

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

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

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

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

跨平台支持:在 iOS、Windows 等平台实现对应客户端。

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

手机网盘移动硬盘怎么用,高效便捷的数据存储与传输指南
三星910S3L K04笔记本怎么样? 三星910S3L笔记本详细测评