一、项目概述
在远程协助、在线教学、技术支持等多种场景下,实时获得另一部移动设备的屏幕画面,并对其进行操作,具有极高的应用价值。本项目旨在实现两台 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() {} }
<!-- 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手机屏幕共享和远程控制的资料请关注代码网其它相关文章!
发表评论