一、项目概述
在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 android 手机之间的屏幕共享与远程控制,其核心功能包括:
主控端(controller):捕获自身屏幕并将实时画面编码后通过网络发送;同时监听用户在主控端的触摸、滑动和按键等输入操作,并将操作事件发送至受控端。
受控端(receiver):接收屏幕画面数据并实时解码、渲染到本地界面;接收并解析主控端的输入操作事件,通过系统接口模拟触摸和按键,实现被控设备的操作。
通过这一方案,用户可以实时“看到”受控端的屏幕,并在主控端进行点触、滑动等交互,达到“远程操控”他机的效果。本项目的核心难点在于如何保证图像数据的实时性与清晰度,以及如何准确、及时地模拟输入事件。
二、相关知识
2.1 mediaprojection api
概述:android 5.0(api 21)引入的屏幕录制和投影接口。通过
mediaprojectionmanager获取用户授权后,可创建virtualdisplay,将屏幕内容输送至surface或imagereader。关键类:
mediaprojectionmanager:请求屏幕捕获权限mediaprojection:执行屏幕捕获virtualdisplay:虚拟显示、输出到surfaceimagereader:以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() {}
}<!-- 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手机屏幕共享和远程控制的资料请关注代码网其它相关文章!
发表评论