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 屏幕捕获与编码
主控端调用
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.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>
五、代码解读
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 加密,防止中间人攻击;验证设备身份。
以上就是Android实现两台手机屏幕共享和远程控制功能的详细内容,更多关于Android手机屏幕共享和远程控制的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论