做 App 测试或者远程设备调试的时候,经常会遇到一个尴尬情况:手边只有一两台手机,型号不全,系统版本不够,同事还排着队要用。市面上的云真机平台体验不错,但按小时计费,长期用成本并不低。于是很多人开始考虑:能不能自己搭一套云真机平台,把闲置的 Android 手机变成“可投屏、可操作、可出租”的资源池?本文就围绕这个目标,分享一套 99 元成本级别的云真机出租平台搭建方案。方案中的投屏模块,我称之为“穿云投屏”,核心是把手机画面实时传到浏览器,并且支持远程触控和租用计费。无论是想自建内部测试平台,还是做小规模设备出租服务,都能参考这套思路。
1. 云真机与穿云投屏:先搞清楚这套东西是什么
1.1 云真机解决什么问题
云真机,简单理解就是“通过网络远程使用真实手机”。你能在电脑或手机浏览器里看到一台真实 Android 设备的画面,点击屏幕就能操作它,就像设备放在手边一样。
它最常见的用途有几类:
- App 测试:装机、启动、兼容性验证、崩溃复现。
- 自动化脚本执行:配合 ADB 或 Appium 跑用例。
- 远程调试:帮外地的同事、客户排查手机上的问题。
- 多设备管理:同时操作多台手机,提高测试效率。
- 设备共享:一台设备多人轮流用,避免频繁借还。
云真机平台最大的价值不是“远程控制”本身,而是让设备的利用率变高。一台测试手机空闲时不占用人工,有需求的人随时接入使用,按时间计费也顺理成章。
1.2 穿云投屏在平台中的位置
一个完整的云真机出租平台,可以拆成几个模块:
- 设备接入层:把 Android 手机接入服务器,保持在线。
- 投屏服务:把手机屏幕实时编码、推流到用户端。
- 控制通道:把用户的点击、滑动操作的坐标传给手机。
- 业务层:设备列表、租用订单、计时计费、用户权限。
- 前端:PC 端和移动端的使用页面。
穿云投屏指的就是其中“投屏服务 + 控制通道”这一整套能力。你不能只把画面传出来,还得让用户能操作,否则就只是“监控”,不是“云真机”。穿云投屏这个名字,可以理解为对“远程屏幕实时传输方案”的工程化封装:设备端负责屏幕采集和编码,服务端负责转发和权限控制,浏览器负责解码展示和坐标回传。
1.3 先认识常见的投屏协议
讨论方案选型之前,有必要把常见的投屏方式放在一起对比,因为在很多项目里,方案之所以选错,就是对协议和能力边界不够清楚。
| 方案 | 传输方式 | 延迟 | 是否支持反向控制 | 适用场景 |
|---|---|---|---|---|
| Miracast | WiFi Direct 点对点 | 100-300ms | 部分支持 | 会议投屏、家庭影音 |
| AirPlay | Apple 私有协议 | 100-300ms | 不支持 | iPhone/Mac 生态 |
| Google Cast | 局域网投屏 | 100-300ms | 不支持 | 视频推送、大屏应用 |
| DLNA | 媒体文件推送 | 较高 | 不支持 | 照片、视频分享 |
| ADB+scrcpy | USB/WiFi ADB 传输 | 30-100ms | 支持 | Android 调试、远程控制 |
| 采集卡+RTMP | 物理视频采集+网络推流 | 200-500ms | 需额外通道 | 非 Android 设备、直播场景 |
这几种方案里,Miracast 是最常见的“真投屏”,手机画面直接送到电视或显示器。但做云真机平台时,Miracast 有天然短板:它是局域网直连协议,跨网段传输困难,而且控制能力很弱。所以云真机平台的主流通路通常是两条:要么走 ADB+scrcpy,要么走采集卡+流媒体推流。后面两种方案的具体做法,就是本文的实战重点。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 99 元成本怎么落地
2.1 成本拆解思路
标题里说“99 元”,并不是说一台手机加一台服务器只要 99 元,而是指在“已有闲置手机、已有电脑或服务器”的前提下,额外添置的硬件成本可以控制在 99 元以内。这套方案的关键思路是:能软件解决的不用硬件,能复用旧设备的不买新设备。
先给一个成本参考:
| 项目 | 方案 A:纯软件 | 方案 B:采集卡方案 |
|---|---|---|
| Android 手机 | 闲置旧机约 0 元 | 闲置旧机约 0 元 |
| 电脑/服务器 | 已有旧电脑或云主机 | 已有旧电脑或云主机 |
| USB 采集卡 | 不需要 | 30-60 元 |
| HDMI 线 | 不需要 | 10-20 元 |
| 其他转接头 | 不需要 | 约 10 元 |
| 合计 | 约 0 元 | 约 50-90 元 |
怎么做到这么省?因为核心依赖都是开源免费的:ADB 是 Android SDK 自带的调试工具,scrcpy 是开源投屏控制软件,SRS 是开源流媒体服务器,前端播放用浏览器内置能力加开源播放器。整条链路的软件成本为零。
2.2 方案 A:纯软件投屏(ADB+scrcpy)
方案 A 适合“手机可以连上服务器”的场景。把手机用 USB 线接到服务器,或者开启 ADB 无线调试,然后服务器端运行 scrcpy 进程来采集画面和控制设备。
它的优势非常明显:
- 延迟低:走 ADB 通道,画面和控制共用一条链路。
- 部署简单:安装 scrcpy 后,一条命令就能启动投屏。
- 不需要额外视频采集硬件。
缺点也有:每台手机要占用一个 USB 口或一条网络 ADB 连接,对服务器的 USB 接口数量和 USB Hub 稳定性要求较高。
bash复制# 安装 scrcpy 环境后,查看已连接的设备
adb devices
设备列表里能看到类似 device 状态,就可以继续操作了。
2.3 方案 B:硬件采集卡+RTMP 推流
方案 B 适合手机无法直接连接服务器,或者需要把画面分发给多个用户查看的场景。手机通过 Type-C 转 HDMI 输出画面,HDMI 信号接入 USB 采集卡,采集卡插到服务器上,服务器用 FFmpeg 把采集到的画面推流到 SRS。
这条路线的特点是:
- 不依赖手机厂商的调试协议,兼容性更广。
- 画面质量相对稳定,适合收费观看场景。
- 但反向控制需要额外走 ADB 或蓝牙通道,链路复杂一些。
方案 B 里 99 元预算主要花在采集卡和线材上,对画质要求不高的话,几十元的 USB 采集卡完全够用。
2.4 两种方案怎么选
我的建议是:如果你的设备都能通过 USB 或局域网 ADB 稳定连接服务器,优先选方案 A,省事、延迟低、控制能力强。如果手上有特殊设备,比如部分平板不方便开 ADB,或者采集的画质要求更高,再考虑方案 B。做云真机出租平台,大多数场景下方案 A 就够用了,后面讲到多分屏和远程控制,也都是基于方案 A 来展开。
3. 环境准备与基础组件
3.1 服务端环境与目录结构
本文示例以 Linux 服务器为例,Android 设备通过 USB 接入服务器。你需要提前安装好:
- Python 3.8+
- Android SDK Platform Tools(包含 adb)
- scrcpy
- FFmpeg(可选,采集卡方案使用)
- Docker(可选,用来跑 SRS)
建议的项目目录结构如下:
code复制cloud-device/
├── agent/ # 设备端注册与心跳
│ └── register.py
├── server/ # 业务 API
│ ├── app.py
│ ├── control.py
│ └── rental.py
├── scripts/
│ ├── start_scrcpy.sh
│ └── start_stream.sh
└── web/
├── index.html
└── player.js
目录拆分的原则是:agent 负责管设备,server 负责管业务,web 负责面向用户。这样后续扩展模块时,互相之间不需要大规模改动。
3.2 Android 设备准备
手机端要做两步准备工作。
第一步:开启开发者选项和 USB 调试。进入“设置 → 关于手机”,连续点击版本号 7 次开启开发者模式,再进入“开发者选项”打开 USB 调试。
第二步:连接服务器并信任调试授权。
bash复制adb devices
首次连接时手机会弹出“允许 USB 调试吗?”,勾选“始终允许”,然后确认。跑通后,服务器就能读取设备信息并执行指令了。
3.3 流媒体服务快速搭建
云真机平台可以没有传统意义上的流媒体服务器吗?可以,但有了它会方便很多。SRS 是一个开源流媒体服务器,支持 RTMP、HTTP-FLV、WebRTC 等多种协议,非常适合做低延迟直播链路。
先介绍方案 B 会用到的 SRS。官方提供了 Docker 镜像,启动命令如下:
bash复制docker run -d --name srs \
-p 1935:1935 \
-p 1985:1985 \
-p 8080:8080 \
--restart=always \
ossrs/srs:5
端口说明:
- 1935:RTMP 推流和拉流端口。
- 1985:HTTP API 端口。
- 8080:HTTP 流端口,HTTP-FLV、HLS 都从这里访问。
启动之后,SRS 默认配置就能接收 RTMP 流并转成 HTTP-FLV。理论上你只需要往 rtmp://服务器IP:1935/live/设备ID 推流,用户端就能从 http://服务器IP:8080/live/设备ID.flv 播放。
如果你打算后续用 WebRTC 进一步降低延迟,还需要在 SRS 配置中开启 WebRTC 并开放对应的 UDP 端口。这一块可以等基础链路跑通后再研究,本文先以 RTMP/HTTP-FLV 为主线。
4. 穿云投屏核心实现
4.1 设备注册与在线管理
平台不能写死设备列表,设备插上服务器后应该自动注册、自动上报心跳。这样后面加设备、减设备都不需要重启服务。下面用一个 Flask 写的简化版注册接口来演示。
先看 Server 端:
python复制# server/app.py
from flask import Flask, jsonify, request
app = Flask(__name__)
DEVICES = {}
@app.post("/api/device/register")
def register():
payload = request.get_json()
serial = payload.get("serial")
model = payload.get("model")
if not serial:
return jsonify({"code": 1, "message": "serial required"}), 400
DEVICES[serial] = {
"serial": serial,
"model": model,
"status": "online",
"last_heartbeat": time.time()
}
return jsonify({"code": 0, "message": "ok"})
@app.get("/api/device/list")
def device_list():
return jsonify({"code": 0, "devices": list(DEVICES.values())})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
注意,这里为了演示没有加时间字段,生产环境要记录心跳时间,并用定时任务把超时设备标记为离线。
再看设备端 Agent:
python复制# agent/register.py
import subprocess
import time
import requests
SERVER = "http://127.0.0.1:8080"
def get_serial():
return subprocess.check_output(
["adb", "get-serialno"], text=True
).strip()
def get_model():
return subprocess.check_output(
["adb", "shell", "getprop", "ro.product.model"], text=True
).strip()
def heartbeat():
serial = get_serial()
model = get_model()
while True:
try:
requests.post(
SERVER + "/api/device/register",
json={"serial": serial, "model": model},
timeout=5
)
print("[agent] heartbeat ok", serial, model)
except Exception as exc:
print("[agent] heartbeat failed:", exc)
time.sleep(10)
if __name__ == "__main__":
heartbeat()
这段代码做的事情很简单:每隔 10 秒读取一次设备序列号和型号,上报给 Server。Server 以设备序列号为维度维护在线状态。实际项目里,心跳间隔可以调成 30 秒,降低无谓的请求量。
4.2 scrcpy 投屏进程管理
设备注册成功之后,下一步是在服务器上启动 scrcpy 采集屏幕。scrcpy 的原理是:通过 ADB 在手机上执行屏幕采集,通过编码后传回电脑端显示,同时支持把电脑端的鼠标键盘操作回传到手机。这个“回传控制”的能力正好满足云真机需求。
启动单台设备的投屏:
bash复制scrcpy -s <设备序列号> --max-size 1024 --max-fps 15 -b 2M
参数说明:
-s:指定设备序列号,防止多台设备情况下选错。--max-size:限制视频最大尺寸,打游戏、看高清不需要太高的分辨率,1024 足够。--max-fps:限制帧率,15 帧可以保证流畅度并降低带宽。-b:视频码率,2M 表示 2Mbps,画面静态时实际占用会低于这个值。
为了让投屏进程可以在业务 API 里被动态拉起或关闭,可以封装一个 Python 管理脚本:
python复制# server/process_manager.py
import subprocess
import shlex
PROCESS_MAP = {}
def start_scrcpy(serial):
if serial in PROCESS_MAP and PROCESS_MAP[serial].poll() is None:
return False, "already running"
cmd = (
"scrcpy -s {} --max-size 1024 --max-fps 15 -b 2M"
).format(serial)
proc = subprocess.Popen(
shlex.split(cmd),
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
PROCESS_MAP[serial] = proc
return True, proc.pid
def stop_scrcpy(serial):
proc = PROCESS_MAP.pop(serial, None)
if proc and proc.poll() is None:
proc.terminate()
封装之后,业务层启动租用时调用 start_scrcpy(serial),结束时调用 stop_scrcpy(serial),管理逻辑就不会散落在各处。
需要提醒的是:scrcpy 在服务器上运行时会尝试打开一个窗口来显示画面。如果你用的是无图形界面的 Linux 服务器,需要安装轻量的窗口环境或者使用 xvfb 这类虚拟显示工具。这是很多初次搭建的同学踩得最多的地方。
4.3 播放链路:从 scrcpy 到浏览器
理论上,scrcpy 自带窗口,但用户不可能直接登录服务器看窗口,所以要把 scrcpy 的画面再次编码推给远程用户。常见的做法是,在服务器上启动一个虚拟显示环境,scrcpy 输出到虚拟屏幕,再用 FFmpeg 抓取这块虚拟屏幕,推流到 SRS。
流程如下:
code复制Android 手机 → ADB 通道 → scrcpy 进程 → Xvfb 虚拟屏幕 → FFmpeg 抓屏 → SRS → 浏览器播放
为了简化部署,也可以在手机上直接用采集工具输出视频流,但这不是 Android 原生能力,需要定制 ROM 或第三方工具,复杂度高。对于入门级方案,用 Xvfb 加 FFmpeg 的链路最直接。
启动虚拟屏幕:
bash复制Xvfb :99 -screen 0 1024x768x24 &
export DISPLAY=:99
scrcpy -s <设备序列号> --window-title device001 &
然后启动 FFmpeg 抓取虚拟屏幕并推流:
bash复制ffmpeg -f x11grab -video_size 1024x768 -framerate 15 \
-i :99.0 \
-c:v libx264 -preset ultrafast -tune zerolatency \
-pix_fmt yuv420p -f flv rtmp://localhost:1935/live/device001
如果采用方案 B(采集卡),FFmpeg 的命令会有所不同,采集入口从 X11 变成 USB 视频设备:
bash复制ffmpeg -f v4l2 -framerate 15 -video_size 1280x720 \
-i /dev/video0 \
-c:v libx264 -preset ultrafast -tune zerolatency \
-pix_fmt yuv420p -f flv rtmp://localhost:1935/live/device001
这里我以 Linux 下的 /dev/video0 为例。如果你在 Windows 或 macOS 上演示流程,采集参数需要分别改为 dshow 和 avfoundation,否则设备名会解析失败。
4.4 远程触控通道
光有画面还不够,用户必须能点按手机屏幕。Android 自带 input 命令,可以模拟点击、滑动、按键等操作。
bash复制adb -s <设备序列号> shell input tap 500 800
adb -s <设备序列号> shell input swipe 100 200 300 400 500
adb -s <设备序列号> shell input keyevent KEYCODE_HOME
这里最关键的是坐标换算。用户在浏览器里看到的画面尺寸可能和手机实际分辨率不一致,比如服务器端 scrcpy 限制的是 1024,而手机实际分辨率是 2400×1080。前端点击时,需要把点击坐标按比例映射回真实分辨率。
假设前端播放区域的宽高是 view_width 和 view_height,投屏画面的模式是“拉伸填满”,那么真实坐标可以用下面的公式换算:
code复制real_x = click_x * phone_width / view_width
real_y = click_y * phone_height / view_height
后端接口可以这样设计:
python复制# server/control.py
from flask import Blueprint, request
import subprocess
control = Blueprint("control", __name__)
def get_phone_size(serial):
out = subprocess.check_output(
["adb", "-s", serial, "shell", "wm", "size"],
text=True
)
# 输出示例:Physical size: 1080x2400
line = out.strip().splitlines()[0]
_, size = line.split(":", 1)
w, h = size.strip().split("x")
return int(w), int(h)
@control.post("/api/device/<serial>/tap")
def tap(serial):
data = request.get_json()
x, y = int(data["x"]), int(data["y"])
subprocess.run(
["adb", "-s", serial, "shell", "input", "tap", str(x), str(y)],
check=False
)
return {"code": 0}
@control.post("/api/device/<serial>/swipe")
def swipe(serial):
data = request.get_json()
x1, y1, x2, y2 = (
int(data["x1"]), int(data["y1"]),
int(data["x2"]), int(data["y2"])
)
duration = int(data.get("duration", 300))
subprocess.run(
["adb", "-s", serial, "shell", "input", "swipe",
str(x1), str(y1), str(x2), str(y2), str(duration)],
check=False
)
return {"code": 0}
前端把用户点击的像素坐标除以播放区域宽高,再乘上手机实际宽高,就能得到真正的设备坐标。这个换算一定不能省,否则会出现“点 A 位置没反应,点偏一点却触发了别的地方”的体验问题。
4.5 租用与计费 API
出租平台的业务核心是“按时计价”。下面给一个非常简化的计费示例,用于说明流程。真实项目里需要把订单、扣费、退款、结算放到数据库里,并且用事务保证一致性。
python复制# server/rental.py
import time
from flask import Blueprint, request
rental = Blueprint("rental", __name__)
RENTAL_RATE = 0.02 # 示例:每分钟 0.02 元
RENTALS = {}
@rental.post("/api/rental/start")
def start_rental():
payload = request.get_json()
serial = payload.get("serial")
user = payload.get("user")
if serial in RENTALS:
return {"code": 1, "message": "设备已被占用"}, 409
rental_id = f"R_{int(time.time())}"
RENTALS[serial] = {
"rental_id": rental_id,
"user": user,
"start": time.time()
}
return {"code": 0, "rental_id": rental_id}
@rental.post("/api/rental/stop")
def stop_rental():
payload = request.get_json()
serial = payload.get("serial")
rental = RENTALS.pop(serial, None)
if not rental:
return {"code": 1, "message": "未找到租用记录"}, 404
minutes = max(1, int((time.time() - rental["start"]) / 60))
fee = round(minutes * RENTAL_RATE, 2)
return {"code": 0, "fee": fee, "minutes": minutes}
这段代码里故意省略了用户校验、并发控制和数据库持久化。做演示可以,但上生产前必须补齐。尤其是并发场景下,两个用户同时请求同一台设备,一定要用数据库唯一索引或分布式锁来防止“一机多租”。
5. 前端页面与多分屏展示
5.1 单设备播放页
浏览器端最省事的播放方式是使用 flv.js,它是开源的 HTTP-FLV 播放器。视频标签加上 flv.js 之后,就可以像播放普通视频一样播放云真机画面。
html复制<!-- web/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>穿云投屏控制台</title>
<style>
video { width: 100%; max-width: 600px; background: #000; }
</style>
</head>
<body>
<h3>设备画面</h3>
<video id="player" controls muted autoplay></video>
<script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.6.2/flv.min.js"></script>
<script src="player.js"></script>
</body>
</html>
播放器初始化:
javascript复制// web/player.js
const video = document.getElementById('player');
if (flvjs.isSupported()) {
const flvPlayer = flvjs.createPlayer({
type: 'flv',
url: 'http://你的服务器IP:8080/live/device001.flv',
isLive: true
});
flvPlayer.attachMediaElement(video);
flvPlayer.load();
flvPlayer.play();
}
使用 HTTP-FLV 播放时,页面需要通过 8080 端口访问 SRS 的流地址。如果服务器有防火墙,记得放行 8080 和 1935 端口。
5.2 多设备分屏看板
云真机平台的运营者往往希望在一个页面里同时看多台设备的画面。实现方式很简单:页面里放多个 <video> 标签,每个标签只负责播放一路设备流。
html复制<div id="grid">
<video data-serial="device001" controls muted autoplay></video>
<video data-serial="device002" controls muted autoplay></video>
<video data-serial="device003" controls muted autoplay></video>
</div>
<style>
#grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
#grid video { width: 100%; }
</style>
然后为每个 video 元素创建对应的 flv.js 播放器,代码逻辑可以封装成一个函数:
javascript复制function createFlvPlayer(serial) {
const video = document.querySelector(`video[data-serial="${serial}"]`);
const url = `http://你的服务器IP:8080/live/${serial}.flv`;
const flvPlayer = flvjs.createPlayer({ type: 'flv', url, isLive: true });
flvPlayer.attachMediaElement(video);
flvPlayer.load();
flvPlayer.play();
}
['device001', 'device002', 'device003'].forEach(createFlvPlayer);
多分屏页面的核心瓶颈不在前端逻辑,而在带宽和播放器实例数。每路 2Mbps 的画质,同时播放 10 路就需要 20Mbps 的下行带宽,服务器上行带宽也要对应足够。
5.3 多分屏投屏的参数思路
最近很多平板支持多分屏投屏功能,比如一些安卓平板可以把屏幕分成多个区域,分别显示不同应用。如果你打算把平板设备用于云真机出租,需要注意以下几点:
- 多分屏是设备端功能,云端只需要把整个屏幕画面传出,和平常投屏没有本质区别。
- 分屏状态下整体画面的分辨率会变化,建议采集时降低分辨率到 720p 甚至更低,否则文字小、画质差。
- 多分屏画面里的文字交互对码率更敏感,建议把码率从 2M 提高到 3M,帧率保持 15 即可。
- 不同品牌平板的多分屏开启路径不同,有的在系统设置里的“显示与亮度”,有的在最近任务界面长按应用图标。遇到具体设备时,以现场设备的系统界面为准,不要凭经验盲点。
6. 常见问题排查
搭建和运行过程中,几乎所有人都会碰到下面几个问题。我按现象、原因、解决方案整理成清单,方便按顺序排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| Type-C 转 HDMI 投屏无信号 | Type-C 接口不支持 DP Alt Mode | 换支持视频输出的接口或设备 |
| Type-C 转 HDMI 无信号 | 转接头是单向传输 | 换双向转接头,并查看接口方向标识 |
| Type-C 转 HDMI 无信号 | 采集卡要求外接供电 | 使用带供电的扩展坞或 OTG 转接 |
| Miracast 投屏卡顿 | WiFi 信道干扰严重 | 切换 5GHz 频段,关闭蓝牙 |
| Miracast 画面黑屏 | HDCP 加密保护不兼容 | 更换投屏接收端或关闭 HDCP |
| scrcpy 启动后闪退 | 服务器缺少图形环境 | 安装 Xvfb 或改用虚拟显示 |
| 设备明明在线但 adb 找不到 | USB 供电不足 | 使用带独立供电的 USB Hub |
| 播放页黑屏 | 流没有成功发布到 SRS | 用 FFmpeg 测试推流,看 SRS 日志 |
| 点击坐标不准确 | 浏览器坐标未换算 | 按前后端坐标换算公式重新计算 |
6.1 Type-C 转 HDMI 投屏无信号怎么解决
这个问题在采集卡方案里出现率最高。先说结论:Type-C 转 HDMI 无信号,九成是接口协议不支持,而不是线材坏了。
Android 手机或平板的 Type-C 接口要输出 HDMI 信号,硬件上必须支持 DP Alt Mode。不是所有 Type-C 接口都支持这个模式,很多入门机和平板只支持充电和数据传输。排查顺序建议如下:
- 查设备参数,确认 Type-C 口支持 DP Alt Mode。
- 换一根已知正常的转接线,排除线材故障。
- 如果线材要供电,接上电源再试。
- 降低手机端的显示分辨率,有些采集设备对高分辨率支持不好。
- 用另一台手机测试,确认采集卡本身没问题。
如果设备明确不支持 DP Alt Mode,那就不要纠结转换线了,改用软件投屏方案,在设备上安装投屏接收端,或者直接走 ADB+scrcpy 方案,这是真正一劳永逸的做法。
6.2 Miracast 投屏延迟、卡顿与黑屏
云真机出租平台不推荐以 Miracast 为最终链路,但很多同学前期测试时会把手机画面直接 Miracast 到采集盒上,然后采集盒输出 HDMI 给服务器。这种链路一旦卡顿,排查方向如下:
- 网络:Miracast 走 WiFi Direct,对信道非常敏感。如果路由器开了 AP 隔离,或者周围 WiFi 设备太多,画面就会不断重传,表现为掉帧、卡顿。
- 功耗:长时间热插拔投屏,手机发热会自动降频,画面帧率跟着下降。
- HDCP:部分商业内容带了 HDCP 加密,采集端不支持时会直接黑屏。测试时用普通桌面画面拉流,不要播放受版权保护的视频。
- 帧率:Miracast 默认帧率可能是 30 或 60,对接收端要求高。我建议在手机端投屏设置里选择“流畅优先”或固定 30 帧。
总体建议是:Miracast 适合临时演示,不适合做成稳定服务。真要做云真机出租,能用有线就尽量用有线。
6.3 多分屏投屏的画面设置
多分屏场景调整设置时,要把“设备端多分屏”和“云端多路播放”两件事分开看。
设备端多分屏,影响的是采集画面的内容布局;云端多路播放,影响的是带宽和播放器性能。常见调整如下:
- 如果多分屏画面在播放端看起来模糊,优先调高推流码率。
- 如果多个分屏画面播放时互相抢带宽,给每一路单独设置一个 SRS 流名,不要共用一路。
- 如果平板自带的多分屏投屏功能不稳定,直接关闭设备端多分屏,改为云端开多个页面分别推送不同应用画面。
6.4 设备掉线与 ADB 权限问题
USB 连接的设备经常会出现“设备状态变成 unauthorized”或者“adb 找不到设备”的情况。这时按下面顺序处理:
bash复制# 1. 查看 adb 设备状态
adb devices
# 2. 如果有 unauthorized,说明手机端授权过期
adb kill-server
adb start-server
# 3. 重新插拔 USB 线,在手机上确认授权弹窗
如果多台设备频繁掉线,大概率是 USB Hub 供电不足。解决办法是换带独立电源的 Hub,并且尽量让每台设备独占一个 USB 控制器通道。必要时可以用无线 ADB 来减轻端口压力,但需要保证 WiFi 环境稳定,否则延迟会明显上升。
7. 工程化与安全建议
7.1 认证与设备授权
云真机平台直接暴露 ADB 是极度危险的事情。用户只要能访问到服务器的 ADB 端口,就等于拿到了所有设备的控制权。生产环境至少要满足以下几点:
- 所有业务 API 都需要登录态校验,用户身份用 Token 或 Session 保持。
- 用户只能操作自己“已租用”的设备,不能直接请求设备 ADB 接口。
- 服务器上的 adb 端口只允许本机访问,不要绑定公网 IP。
- 用户退出租用后,服务端执行一次设备重置逻辑:杀掉 scrcpy、清除应用数据、恢复初始桌面。
- 涉及远程控制、日志、账号处理,必须确认拥有设备和数据的使用授权,绝不能越权操作。
7.2 延迟与画质调优
延迟是云真机体验的核心指标。如果目前链路延迟稳定在 200ms 以内,用户基本无感知。如果延迟偏高,按这几个方向调:
| 优化项 | 调整建议 |
|---|---|
| 编码 | 使用 ultrafast + zerolatency,牺牲一点压缩率换延迟 |
| 分辨率 | 优先 720P,而不是 1080P,移动端测试场景下观感差异不大 |
| 帧率 | 15fps 起步,动态画面多再升到 30fps |
| 协议 | HTTP-FLV 延迟比 HLS 低很多,WebRTC 又比 HTTP-FLV 更低 |
| 网络 | 服务器带宽要够,注意上行带宽和下行带宽不要搞混 |
7.3 日志、监控与部署
设备层要记录每台设备的注册、心跳、断线时间;业务层要记录每次租用开始、结束、费用。部署时用 systemd 管理 Python 服务,SRS 用 Docker 的 restart 策略保证崩溃后自动恢复。
下面是一个 systemd 服务示例,用来守护设备 Agent:
ini复制[Unit]
Description=Cloud Device Agent
After=network.target
[Service]
WorkingDirectory=/opt/cloud-device/agent
ExecStart=/usr/bin/python3 /opt/cloud-device/agent/register.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
放到 /etc/systemd/system/cloud-device-agent.service 后,执行 systemctl enable --now cloud-device-agent 即可开机自启。
我建议上线前做一次压力测试:同时开 10 路流,持续播放 1 小时,观察服务器 CPU、内存、带宽和 adb 进程数量。如果 scrcpy 进程异常退出,需要有监控脚本自动重启,不然用户会看到设备在线但一直黑屏。
8. 写在最后
这套“穿云投屏”方案其实并不复杂:注册设备,启动 scrcpy 采集画面,用流媒体服务把画面分发给用户,再把用户的点击坐标回传给手机,最后套一层计费 API。核心难点不在单个技术点,而在于把这条链路稳定地串起来。
文章里给出的代码是教学级别的骨架,帮你理解每个模块长什么样。如果要做正式出租服务,还需要补齐用户体系、订单数据库、设备状态机、运维监控这些工程能力。下一步可以从三个方向继续深入:一是学习 WebRTC 的低延迟推流替换 HTTP-FLV;二是给 Android 设备做批量自动化管理;三是研究设备场景隔离,让每台设备在用户使用前后都能安全恢复。先跑通最小闭环,再逐步迭代,这套平台的价值就会慢慢体现出来。
