共享Android手机屏幕的几种方式(ADB Server + Scrcpy, Sonic)
· 13 min read
使用 Remote ADB Server 共享 USB设备
安装 ADB
准备一台 Linux 系统的 PC 机, 推荐 Ubuntu 24.04 LTS, 安装 Android Debug Bridge, 推荐下载最新版https://developer.android.com/tools/releases/platform-tools:
curl -OL https://dl.google.com/android/repository/platform-tools-latest-linux.zip
unzip platform-tools-latest-linux.zip -d ./
mv platform-tools /usr/lib/android-sdk/
ln /usr/lib/android-sdk/platform-tools/adb /usr/bin/adb
启动 ADB Server
以 systemd 形式启动 ADB Server 监听在所有网络接口
cat > /etc/systemd/system/adb-server.service <<'EOF'
[Unit]
Description=ADB Server
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/adb -a -P 5037 nodaemon server
ExecStartPre=/usr/bin/adb kill-server
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now adb-server
通过 USB 数据线连接手机到 ADB Server 所在的 Linux PC, 并开启手机的 USB Debug
在其它 PC 上通过 ADB 指令使用 ADB Server 上的设备
- 方式1
export ADB_SERVER_SOCKET=tcp:<ADB Server IP>:5037
adb devices
- 方式2
adb -H <ADB Server IP> -P 5037 devices
通过 scrcpy 投屏到电脑
- 下载最新的 scrcpy 3.3.3: https://github.com/Genymobile/scrcpy/releases
Linux 也可以从源码编译安装, 参考: https://github.com/Genymobile/scrcpy/blob/master/doc/linux.md
# for Debian/Ubuntu
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
gcc git pkg-config meson ninja-build libsdl2-dev \
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
git clone https://github.com/Genymobile/scrcpy
cd scrcpy
./install_release.sh
- 执行 scrcpy 并把 --tunnel-host 参数指向 ADB Server, 确保能访问 ADB Server 上的 27183 到 27199 端口, Android 手机的投屏将会显示在 scrcpy 运行的PC上
scrcpy -s <设备 ID>--tunnel-host <ADB Server IP> --tunnel-port=27183
- 参数说明
- -s 参数在只有一台设备时可以省略
- --tunnel-host 参数指向连接手机的 ADB Server
- --tunnel-port 参数可以省略, 默认值是 27183
- scrcpy 启动时,将会:
- 向设备 /data/local/tmp 推送 scrcpy-server. 等同指令:
adb -s <设备 ID> push scrcpy-server /data/local/tmp/scrcpy-server.jar
- 在设备上启动 scrcpy-server, 等同指令:
adb -s <设备 ID> shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 3.3.3 scid=23d1ac65 log_level=info tunnel_forward=true
- scrcpy-server 会在设备中创建unix socket, 用于 scrcpy-server与 adbd 之间的 音频/视频/控制 数据的转发:
$ cat /proc/net/unix | grep scrcpy
0000000000000000: 00000003 00000000 00000000 0001 03 1750017 @scrcpy_0801e049
0000000000000000: 00000003 00000000 00000000 0001 03 1748184 @scrcpy_0801e049
0000000000000000: 00000003 00000000 00000000 0001 03 1748188 @scrcpy_0801e049
- 启动 scrcpy 连接 ADB 转发隧道, 获取视频流并展示在 scrcpy 客户端 PC, 并接收用户指令转发给 adb, 经 adbd 转发给设备
基于 Linux gnome 创建一个手机屏幕监控
写脚本
#!/usr/bin/env bash
################################################################################
# auto_scrcpy.sh - Android设备自动投屏监控脚本
#
# 功能说明:
# - 自动检测ADB设备插拔,动态启动/停止scrcpy投屏
# - 多设备横向排列显示,避免窗口重叠
# - 投屏窗口意外关闭时自动重启
# - Ctrl+C退出时自动清理所有进程,无残留
#
# 使用方法:
# chmod +x auto_scrcpy.sh
# ./auto_scrcpy.sh
# (按 Ctrl+C 停止)
#
# 环境要求:
# - 已安装 adb 和 scrcpy
# - Android设备开启USB调试并连接
#
# 配置参数:
# X_OFFSET=40 # 窗口水平间距(px)
# Y_OFFSET=60 # 窗口垂直起始位置(px)
# WINDOW_WIDTH=428 # 窗口宽度(px)
# RECHECK_INTERVAL=2 # 设备检测间隔(秒)
#
################################################################################
X_OFFSET=40
Y_OFFSET=60
WINDOW_WIDTH=428
RECHECK_INTERVAL=2
declare -A MON_PID # 监控器进程 PID: MON_PID[$serial]=pid
calc_x() {
local idx=$1
echo $(( X_OFFSET*(idx+1) + WINDOW_WIDTH*idx ))
}
# 检查进程是否存活
is_process_alive() {
local pid=$1
kill -0 "$pid" 2>/dev/null
return $?
}
# 获取设备在当前列表中的索引
get_device_index() {
local serial=$1
local -n arr=$2
for i in "${!arr[@]}"; do
if [ "${arr[$i]}" == "$serial" ]; then
echo $i
return
fi
done
echo -1
}
# 启动单个设备的监控器(后台)
start_monitor_for() {
local serial=$1
local idx=$2
# 检查是否已经有存活的监控器
if [ -n "${MON_PID[$serial]}" ]; then
if is_process_alive "${MON_PID[$serial]}"; then
echo "[MAIN] monitor for $serial already running (pid ${MON_PID[$serial]})"
return
else
echo "[MAIN] cleaning stale monitor pid ${MON_PID[$serial]} for $serial"
unset MON_PID["$serial"]
fi
fi
# 先计算好X坐标
local x_pos=$(calc_x $idx)
# 监控器脚本体
(
setsid bash -c -- "
while true; do
X=$x_pos
Y=$Y_OFFSET
echo \"[MONITOR][$serial] start scrcpy at \$X,\$Y (index=$idx)\"
scrcpy -s \"$serial\" --window-x=\$X --window-y=\$Y &
child=\$!
echo \$child > /tmp/auto_scrcpy_${serial}.pid
wait \$child
echo \"[MONITOR][$serial] scrcpy (\$child) exited, restart after 1s\"
rm -f /tmp/auto_scrcpy_${serial}.pid
sleep 1
done
"
) &
local mpid=$!
MON_PID["$serial"]=$mpid
echo "[MAIN] started monitor for $serial (pid $mpid, X=$x_pos)"
}
# 停止单个设备监控
stop_monitor_for() {
local serial=$1
local mpid=${MON_PID[$serial]}
if [ -z "$mpid" ]; then
return
fi
echo "[MAIN] stopping monitor for $serial (monitor pid $mpid)"
# 1) 杀 scrcpy 子进程
local pidfile="/tmp/auto_scrcpy_${serial}.pid"
if [ -f "$pidfile" ]; then
childpid=$(cat "$pidfile" 2>/dev/null)
if [ -n "$childpid" ] && is_process_alive "$childpid"; then
echo "[MAIN] killing scrcpy pid $childpid for $serial"
kill $childpid 2>/dev/null || true
sleep 0.2
kill -9 $childpid 2>/dev/null || true
fi
rm -f "$pidfile"
fi
# 2) 杀 monitor 的所有子进程(包括孤儿进程)
pchildren=$(pgrep -P $mpid 2>/dev/null || true)
if [ -n "$pchildren" ]; then
echo "[MAIN] killing child processes of monitor $mpid: $pchildren"
for c in $pchildren; do
kill $c 2>/dev/null || true
sleep 0.05
kill -9 $c 2>/dev/null || true
done
fi
# 3) 杀 monitor 本身(使用进程组杀死,确保 setsid 创建的会话也被清理)
if is_process_alive "$mpid"; then
# 获取进程组ID并杀死整个组
pgid=$(ps -o pgid= -p $mpid 2>/dev/null | tr -d ' ')
if [ -n "$pgid" ]; then
echo "[MAIN] killing process group $pgid for monitor $mpid"
kill -- -$pgid 2>/dev/null || true
sleep 0.2
kill -9 -- -$pgid 2>/dev/null || true
fi
# 再单独杀 monitor 进程
kill $mpid 2>/dev/null || true
sleep 0.1
kill -9 $mpid 2>/dev/null || true
fi
unset MON_PID["$serial"]
rm -f /tmp/auto_scrcpy_${serial}.pid
}
# 彻底停止并清理所有
stop_all() {
echo "[MAIN] stopping all monitors..."
for s in "${!MON_PID[@]}"; do
stop_monitor_for "$s"
done
# 额外清理:查找所有可能残留的 scrcpy 进程
echo "[MAIN] cleaning up any remaining scrcpy processes..."
pkill -f "scrcpy -s" 2>/dev/null || true
# 清理所有临时文件
rm -f /tmp/auto_scrcpy_*.pid
MON_PID=()
}
# 读取设备列表并倒序
get_device_array() {
local -n out=$1
mapfile -t out < <(adb devices | awk 'NR>1 && $2=="device"{print $1}' | tac)
}
# 捕获退出信号,确保清理所有进程
cleanup_on_exit() {
echo ""
echo "[MAIN] received exit signal, cleaning up..."
stop_all
echo "[MAIN] cleanup complete, exiting"
exit 0
}
# 注册信号处理
trap cleanup_on_exit SIGINT SIGTERM EXIT
# 主循环
declare -a last_devices=()
echo "[MAIN] auto_scrcpy monitor started"
echo "[MAIN] press Ctrl+C to stop"
while true; do
sleep $RECHECK_INTERVAL
declare -a cur_devices=()
get_device_array cur_devices
# 构建设备集合
declare -A set_last=()
declare -A set_cur=()
for d in "${last_devices[@]}"; do set_last["$d"]=1; done
for d in "${cur_devices[@]}"; do set_cur["$d"]=1; done
# 处理移除的设备
for d in "${last_devices[@]}"; do
if [ -z "${set_cur[$d]}" ]; then
echo "[EVENT] device removed: $d"
stop_monitor_for "$d"
fi
done
# 处理新增的设备
for d in "${cur_devices[@]}"; do
if [ -z "${set_last[$d]}" ]; then
echo "[EVENT] device added: $d"
idx=$(get_device_index "$d" cur_devices)
if [ $idx -ge 0 ]; then
start_monitor_for "$d" "$idx"
sleep 0.2
fi
fi
done
# 更新设备列表
last_devices=("${cur_devices[@]}")
done
chmod 755 /opt/screen-manager/auto_scrcpy.sh
创建桌面登录后启动项
cat > ~/.config/autostart/scrcpy-manager.desktop <<EOF
[Desktop Entry]
Type=Application
Name=SCRCPY Manager
Exec=/opt/screen-manager/auto_scrcpy.sh
X-GNOME-Autostart-enabled=true
EOF
基于 SSH 的加密端口转发共享屏幕
参考: https://github.com/Genymobile/scrcpy/blob/master/doc/tunnels.md
先来了解一下 scrcpy 基于 adb 的数据转发方式:
======================== CONTROL FLOW ========================
PC (adb client)
| TCP connect to PC:5037
v
PC (adb server :5037)
| USB ADB protocol
v
手机 (adbd)
| ADB internal socket
v
手机 (scrcpy-server)
==============================================================
======================== VIDEO FLOW ==========================
手机 (scrcpy-server)
| localabstract:scrcpy (video source)
v
手机 (adbd)
| via USB (adb reverse)
v
PC (adb server listens on :27183)
| TCP forwarding (your network)
v
PC (scrcpy client: connects to 27183)
==============================================================
环境
- PC1 环境: 启动了 adb server:
adb start-server, USB连接了手机 - PC2 环境: ADB client, scrcpy
步骤
隧道方式1:
- 在 PC2 上执行 ssh, 建立到 PC1的 隧道:
ssh -CN -L5037:localhost:5037 -R27183:localhost:27183 root@<PC1 IP>
作用:
- 在本地打开一个 5037 → 远程 5037 的正向转发
- 在远程打开一个 27183 → 本地 27183 的反向转发
- 在 PC2 上启动客户端, 可直接投屏呈现手机屏幕:
scrcpy
隧道方式2:
- 在 PC2 上执行
ssh -CN -L5037:localhost:5037 -L27183:localhost:27183 root@<PC1 IP>
作用:
- 在本地打开一个 5037 → 远程 5037 的正向转发
- 在本地打开一个 27183 → 远程 27183 的正向转发
- 在 PC2 上 添加 --force-adb-forward 参数启动 scrcpy 客户端, 可直接投屏呈现手机屏幕:
scrcpy --force-adb-forward
通过 Sonic Server 管理手机和 Web 投屏
开发编译环境搭建
Sonic Server
Install JDK and Maven
sudo apt install openjdk-17-jdk -y
java --version
sudo apt install maven -y
mvn --version
调试运行
下载源码, 进入源码根目录
git clone [email protected]:ylhyh/sonic-server.git
cd sonic-server
开发环境启动 eureka
cd sonic-server-eureka
mvn spring-boot:run -DSONIC_VERSION=2.7.3
开发环境启动 controller
cd sonic-server-controller
# 配置数据库
export MYSQL_HOST=172.16.126.26
export MYSQL_DATABASE=sonic
export MYSQL_PORT=3306
export MYSQL_USERNAME=sonic
export MYSQL_PASSWORD=xxxxxx
mvn spring-boot:run -DSONIC_VERSION=2.7.3
开发环境启动 gateway
cd sonic-server-gateway
mvn spring-boot:run -DSONIC_VERSION=2.7.3
编译打包安装
mvn clean install -Dplatform=linux-x86_64 -DskipTests -DSONIC_VERSION=2.7.3
构建 Docker 镜像
cd sonic-server
docker build -t ylhyh/sonic-server-folder:v2.7.3 -f sonic-server-folder/src/main/docker/Dockerfile .
docker build -t ylhyh/sonic-server-gateway:v2.7.3 -f sonic-server-gateway/src/main/docker/Dockerfile .
docker build -t ylhyh/sonic-server-eureka:v2.7.3 -f sonic-server-eureka/src/main/docker/Dockerfile .
docker build -t ylhyh/sonic-server-controller:v2.7.3 -f sonic-server-controller/src/main/docker/Dockerfile .
Sonic Web
安装 nvm npm
安装 npm 18
nvm install 18
调试运行
git clone [email protected]:ylhyh/sonic-client-web.git
cd sonic-client-web
nvm use 18
nvm alias default 18
npm install
npm run dev
编译打包
cd sonic-client-web
npm run build
构建 Docker 镜像
cd sonic-client-web
docker build -t ylhyh/sonic-client-web:v2.7.3 .
Sonic Agent
调试运行
git clone [email protected]:ylhyh/sonic-agent.git
cd sonic-agent
mvn spring-boot:run -DSONIC_VERSION=2.7.3
编译 jar 包并运行
mvn clean package -Dplatform=linux-x86_64 -DskipTests -DSONIC_VERSION=2.7.3
java -Dfile.encoding=utf-8 -jar sonic-agent-linux-x86_64.jar
部署
服务端部署
参考: https://github.com/ylhyh/sonic-server/blob/main/k8s-all-in-one.md
Agent 部署
- 配置 sonic-agent service
cat > /etc/systemd/system/sonic-agent.service <<'EOF'
[Unit]
Description=Sonic Agent Service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
# 使用 root 用户(设备管理需要)
User=root
Group=root
WorkingDirectory=/opt/sonic-agent
ExecStart=/usr/bin/java -Dfile.encoding=utf-8 -Xms512m -Xmx1024m -jar /opt/sonic-agent/sonic-agent-linux-x86_64.jar
# 重启策略
Restart=on-failure
RestartSec=10
StartLimitInterval=300
StartLimitBurst=5
# 资源限制
LimitNOFILE=65536
# 超时
TimeoutStartSec=90
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now sonic-agent
Android 设备 -> 在 Agent 上 配置 ADB Server
iOS 设备 -> 在 Agent 上配置 usbmuxd
- 安装 usbmuxd
sudo apt install usbmuxd libimobiledevice-utils -y
- 在 Agent 所在 PC 上通过 USB 连接 iOS 手机, 在手机弹窗中选择信任该电脑
- 运行 agent 包中的 sonic-ios-bridge (sib) 查看 USB 中的 iOS 设备
/opt/sonic-agent/plugins/sonic-ios-bridge devices
iOS手机的投屏需要通过 macOS + 开发者账号在手机中安装 WebDriverAgent
其它投屏玩法: 通过 X11 转发手机屏幕
前提: 有 ADB Server 的 ssh 权限
# 在客户端上通过 SSH X11转发连接
ssh -X [email protected]
# SSH到 ADB Server上执行
scrcpy
附录
Scrcpy 使用小技巧
- 打开手机摄像头录制视频到文件
scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --record=file.mp4
- 打开手机前置摄像头录制视频
scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --camera-facing=front
- 使用前置摄像头把实时手机画面写入 V4L2 虚拟摄像头用于直播、会议、OBS 推流等 Webcam 场景:
scrcpy --video-source=camera --camera-size=1920x1080 --camera-facing=front --v4l2-sink=/dev/video2 --no-playback
使用前提要先通过 v4l2loopback 创建虚拟摄像头, 类似 sudo modprobe v4l2loopback video_nr=2
通过 USBIP 远程 attach USB 设备
USBIP 远程 attach USB 设备适用于 摄像头、U盘、USB键盘、鼠标等通用设备, 不适用于 ADB
- 安装新版的 USBIP
usbip 是 linux内核自带的 lib, 也可以从源码编译安装最新版
sudo apt update
sudo apt install -y build-essential git linux-headers-$(uname -r) libudev-dev libwrap0-dev libtool automake autoconf pkg-config
git clone https://github.com/torvalds/linux.git
cd linux/tools/usb/usbip
./autogen.sh
./configure
make
make install
ldconfig
- 以 systemd 模式运行 USBIPD
cat > /etc/systemd/system/usbipd.service <<'EOF'
[Unit]
Description=USB/IP daemon
After=network.target
[Service]
Type=simple
ExecStartPre=/sbin/modprobe usbip_core
ExecStartPre=/sbin/modprobe usbip_host
ExecStartPre=/sbin/modprobe vhci_hcd
ExecStart=/usr/sbin/usbipd -D
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now usbipd
- 在 USBIPD 中绑定 USB 设备
# 绑定指定的 busid 到 USBIPD
/usr/local/sbin/usbip bind -b 1-13
- 在 客户端 PC 上 attach USBIPD export 出来的 usb 设备
usbip attach -r <HostPC_IP> -b 1-3
