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

2025-04-22 16:50

本文主要是介绍Android实现两台手机屏幕共享和远程控制功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《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 管理捕获、编码、发送任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);
        }
China编程 
        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. 跨平台支持:在 IOSWindows 等平台实现对应客户端。

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

以上就是Android实现两台手机屏幕共享和远程控制功能的详细内容,更多关于Android手机屏幕共享和远程控制的资料请关注China编程(www.chinasem.cn)其它相关文章!

这篇关于Android实现两台手机屏幕共享和远程控制功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154328

相关文章

Java实现优雅日期处理的方案详解

《Java实现优雅日期处理的方案详解》在我们的日常工作中,需要经常处理各种格式,各种类似的的日期或者时间,下面我们就来看看如何使用java处理这样的日期问题吧,感兴趣的小伙伴可以跟随小编一起学习一下... 目录前言一、日期的坑1.1 日期格式化陷阱1.2 时区转换二、优雅方案的进阶之路2.1 线程安全重构2

使用Python实现图像LBP特征提取的操作方法

《使用Python实现图像LBP特征提取的操作方法》LBP特征叫做局部二值模式,常用于纹理特征提取,并在纹理分类中具有较强的区分能力,本文给大家介绍了如何使用Python实现图像LBP特征提取的操作方... 目录一、LBP特征介绍二、LBP特征描述三、一些改进版本的LBP1.圆形LBP算子2.旋转不变的LB

Redis消息队列实现异步秒杀功能

《Redis消息队列实现异步秒杀功能》在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给Redis处理,并通过异步方式执行,Redis提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Re... 目录1 Redis消息队列1.1 List 结构1.2 Pub/Sub 模式1.3 Stream 结

C# Where 泛型约束的实现

《C#Where泛型约束的实现》本文主要介绍了C#Where泛型约束的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录使用的对象约束分类where T : structwhere T : classwhere T : ne

将Java程序打包成EXE文件的实现方式

《将Java程序打包成EXE文件的实现方式》:本文主要介绍将Java程序打包成EXE文件的实现方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录如何将Java程序编程打包成EXE文件1.准备Java程序2.生成JAR包3.选择并安装打包工具4.配置Launch4

MySQL索引的优化之LIKE模糊查询功能实现

《MySQL索引的优化之LIKE模糊查询功能实现》:本文主要介绍MySQL索引的优化之LIKE模糊查询功能实现,本文通过示例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录一、前缀匹配优化二、后缀匹配优化三、中间匹配优化四、覆盖索引优化五、减少查询范围六、避免通配符开头七、使用外部搜索引擎八、分

Python实现特殊字符判断并去掉非字母和数字的特殊字符

《Python实现特殊字符判断并去掉非字母和数字的特殊字符》在Python中,可以通过多种方法来判断字符串中是否包含非字母、数字的特殊字符,并将这些特殊字符去掉,本文为大家整理了一些常用的,希望对大家... 目录1. 使用正则表达式判断字符串中是否包含特殊字符去掉字符串中的特殊字符2. 使用 str.isa

Spring Boot 集成 Quartz并使用Cron 表达式实现定时任务

《SpringBoot集成Quartz并使用Cron表达式实现定时任务》本篇文章介绍了如何在SpringBoot中集成Quartz进行定时任务调度,并通过Cron表达式控制任务... 目录前言1. 添加 Quartz 依赖2. 创建 Quartz 任务3. 配置 Quartz 任务调度4. 启动 Sprin

Android实现悬浮按钮功能

《Android实现悬浮按钮功能》在很多场景中,我们希望在应用或系统任意界面上都能看到一个小的“悬浮按钮”(FloatingButton),用来快速启动工具、展示未读信息或快捷操作,所以本文给大家介绍... 目录一、项目概述二、相关技术知识三、实现思路四、整合代码4.1 Java 代码(MainActivi

使用Python实现一个优雅的异步定时器

《使用Python实现一个优雅的异步定时器》在Python中实现定时器功能是一个常见需求,尤其是在需要周期性执行任务的场景下,本文给大家介绍了基于asyncio和threading模块,可扩展的异步定... 目录需求背景代码1. 单例事件循环的实现2. 事件循环的运行与关闭3. 定时器核心逻辑4. 启动与停