会话管理、设备控制、群控管理、系统监控、竞争记录接口文档
填写设备信息,点击「创建会话」即可获取播放地址,并直接在页面内预览投屏画面。
| 字段 | 类型 | 说明 | |
|---|---|---|---|
| device_ip | string | 必填 | 设备 IP 地址 |
| device_tcp_port | int | 必填 | 设备 TCP 端口 (scrcpy) |
| device_udp_port | int | 必填 | 设备 UDP 端口 (WebRTC) |
| quality | int | 可选 | 画质等级(见下方说明),默认 720 |
| codec | string | 可选 | 编码格式:"h264"(默认)或 "h265" |
| 值 | 分辨率 | 推流尺寸 (sw x sh) | 说明 |
|---|---|---|---|
| 480 | 480p | 854 x 480 | 低画质,节省带宽,适合弱网环境 |
| 720 | 720p(默认) | 1280 x 720 | 标清,画质与流畅度平衡,推荐日常使用 |
| 1080 | 1080p | 1920 x 1080 | 高清,画面清晰但带宽消耗大 |
{
"device_ip": "192.168.1.10",
"device_tcp_port": 30007,
"device_udp_port": 30008,
"quality": 720,
"codec": "h264"
}
{
"code": 0,
"data": {
"session_id": "abc123",
"proxy_tcp_port": 3478,
"proxy_udp_port": 3478,
"play_url": "/player.html?session_id=s_abc123&token=t_xxx",
"orientation": "portrait",
"created_at": "2026-04-07T12:00:00Z",
"preempted_session": null
}
}
{
"session_id": "你的会话ID"
}
{
"code": 0,
"message": "session closed"
}
| 字段 | 类型 | 说明 | |
|---|---|---|---|
| id | string | 必填 | 会话 ID |
{
"code": 0,
"data": {
"session_id": "abc123",
"device_ip": "192.168.1.10",
"status": "active",
"idle_seconds": 12,
"heartbeat_status": "healthy",
...
}
}
| 字段 | 类型 | 说明 | |
|---|---|---|---|
| status | string | 可选 | 状态过滤:"active"、"closed" |
| page | int | 可选 | 页码,默认 1 |
| page_size | int | 可选 | 每页条数,默认 50(最大 200) |
{
"code": 0,
"data": {
"total": 5,
"sessions": [ ... ]
}
}
{
"session_id": "你的会话ID",
"orientation": "portrait" // 或 "landscape"
}
{
"code": 0,
"message": "orientation updated",
"data": { "session_id": "...", "orientation": "portrait" }
}
{
"session_id": "你的会话ID",
"command": "goHome"
}
| 指令 | 说明 |
|---|---|
| volUp | 音量加 |
| volDown | 音量减 |
| goHome | 回到桌面 |
| goBack | 返回上一级 |
| VK_RETURN | 回车键 |
| goClean | 清除最近任务 |
| 字段 | 类型 | 说明 | |
|---|---|---|---|
| session_id | string | 必填 | 要轮询指令的会话 ID |
{
"code": 0,
"data": { "commands": ["goHome", "volUp"] }
}
{
"session_id": "你的会话ID",
"quality": 720 // 可选值: 480, 720, 1080
}
{
"code": 0,
"data": {
"uptime": "2h30m", // 运行时长
"goroutines": 42, // 协程数
"memory": { // 内存使用
"alloc_mb": "12.34",
"total_alloc_mb": "56.78",
"sys_mb": "30.00",
"gc_count": 15
},
"port_pool": { // 端口池
"total": 20,
"used": 3,
"available": 17,
"usage_percent": "15.00"
},
"devices": { // 设备统计
"active_devices": 3,
"proxy_connections": 6,
"stale_devices": 0
},
"traffic": { // 流量统计
"active_forwarded_bytes": 1048576,
"throughput_bps": "524288.00"
},
"startup": { // 启动失败
"recent_failures": 0,
"last_failure_time": null,
"last_failure_reason": ""
},
"contention": { // 竞争统计
"total_count": 0,
"last_minute_count": 0,
"frequent_devices": []
}
}
}
| 字段 | 类型 | 说明 | |
|---|---|---|---|
| page | int | 可选 | 页码,默认 1 |
| page_size | int | 可选 | 每页条数,默认 50(最大 200) |
{
"code": 0,
"message": "contentions cleared"
}
对等群控:群组内所有设备角色平等,任意设备上的操作(触摸、滑动、按钮)会自动同步到其余所有设备。通过 WebSocket 实时广播,发送者自身不会收到回弹。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| session_ids | string[] | 必填 | 初始成员的 session_id 数组,至少一个 |
{
"session_ids": ["s_abc123", "s_def456"]
}
{
"code": 0,
"data": {
"group_id": "g_98b16c56bf523c7e",
"members": ["s_abc123", "s_def456"],
"created_at": "2026-04-07T12:00:00Z"
}
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| group_id | string | 必填 | 目标群组 ID |
| session_id | string | 必填 | 要加入的会话 ID |
{
"group_id": "g_98b16c56bf523c7e",
"session_id": "s_newmember"
}
{
"code": 0,
"message": "joined group"
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| session_id | string | 必填 | 要离开的会话 ID |
{
"code": 0,
"message": "left group"
}
最后一个成员离开时群组自动解散。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| group_id | string | 必填 | 要解散的群组 ID |
{
"code": 0,
"message": "group disbanded"
}
{
"code": 0,
"data": {
"group_id": "g_98b16c56bf523c7e",
"members": ["s_abc123", "s_def456", "s_ghi789"],
"member_count": 3,
"created_at": "2026-04-07T12:00:00Z"
}
}
{
"code": 0,
"data": {
"total": 1,
"groups": [{
"group_id": "g_98b16c56bf523c7e",
"members": ["s_abc123", "s_def456"],
"member_count": 2,
"created_at": "2026-04-07T12:00:00Z"
}]
}
}
WebSocket 连接,用于实时同步群组内的操作事件。每个成员建立一个连接,既发送又接收。
ws://host/api/v1/group/ws?session_id=s_abc123
| 方向 | 说明 |
|---|---|
| 发送 | 本端操作事件(触摸/滑动/按钮)JSON 发给服务器 |
| 接收 | 服务器广播其他成员的操作事件(自动排除发送者) |
// SDK 触摸事件(由 fun_send_data 发出)
{"t":1, "d":{"mx":360,"my":640,"sw":720,"sh":1280,...}}
// 控制按钮(home/back/volUp 等)
{"_groupCmd":true, "command":"goHome"}
/api/v1/group/create 创建群组并传入所有 session_idstart-group-peer 消息启动 peer 模式/api/v1/group/join 后同样启动 peer 模式Go 壳播放器通常会自动调用这个接口。它根据 session_id 和 token 返回固定端口播放所需的运行时参数,前端一般不需要手动拼接 shost、rtc_i、sport、rtc_p。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| session_id | string | 是 | 会话 ID |
| token | string | 是 | 播放鉴权 token |
{
"code": 0,
"data": {
"session_id": "s_abc123",
"token": "t_xxx",
"shost": "relay.example.com",
"sport": 3478,
"rtc_i": "relay.example.com",
"rtc_p": 3478,
"q": 2,
"v": "h264",
"sw": 1280,
"sh": 720,
"tshow": 0
}
}
调用创建会话 API 后,将返回的 play_url 转换为播放器地址,嵌入 iframe 即可。生产环境里,返回的 shost / rtc_i 必须是客户端可访问的公网 IP 或域名,不能是 127.0.0.1。
// 1. 调用 API 创建会话
const res = await fetch('/api/v1/session/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device_ip: '192.168.1.10',
device_tcp_port: 30007,
device_udp_port: 30008,
quality: 720,
codec: 'h264'
})
});
const { data } = await res.json();
// 2. play_url 已经是 Go 壳播放器地址
const embedUrl = data.play_url;
// 3. 嵌入 iframe
document.getElementById('myPlayer').src = embedUrl;
// 也可以直接用 play_url 在新窗口打开
// window.open(data.play_url);
<!-- HTML: 9:16 竖屏容器 -->
<div style="width:360px; position:relative;">
<div style="padding-top:177.78%; position:relative;">
<iframe id="myPlayer"
style="position:absolute;top:0;left:0;width:100%;height:100%;border:none;">
</iframe>
</div>
</div>
最简单的方式,直接用 play_url 打开新窗口。
// 创建会话后直接打开
window.open(data.play_url);
// 或者完整地址(跨域时需要用完整地址)
window.open('https://relay.example.com' + data.play_url);
嵌入 iframe 后,还可以通过 postMessage 向播放器发送指令。
const frame = document.getElementById('myPlayer');
// 发送设备按键指令(直接通过播放器,无需再调 API)
frame.contentWindow.postMessage({
type: 'control-command',
command: 'goHome' // goBack, volUp, volDown, VK_RETURN, goClean
}, '*');
// 切换屏幕方向
frame.contentWindow.postMessage({
type: 'set-orientation',
orientation: 'landscape' // 或 'portrait'
}, '*');
// 截图
frame.contentWindow.postMessage({ type: 'take-screenshot' }, '*');
window.addEventListener('message', function(e) {
if (e.data.type === 'screenshot-result') {
// e.data.dataUrl 就是 base64 PNG 图片
document.getElementById('screenshotImg').src = e.data.dataUrl;
}
});
| 地址 | 说明 | 适用场景 |
|---|---|---|
| /player.html?... | Go 壳播放器,内含方向控制与固定端口参数 | iframe 嵌入、直接打开、新窗口播放 |
const BASE = ''; // 同源,留空即可
// 1. 创建会话
const res = await fetch(BASE + '/api/v1/session/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device_ip: '192.168.1.10',
device_tcp_port: 30007,
device_udp_port: 30008,
quality: 720,
codec: 'h264'
})
});
const { data } = await res.json();
const sessionId = data.session_id;
// 2. 嵌入播放器 iframe
document.getElementById('myPlayer').src = data.play_url;
// 3. 发送设备指令
await fetch(BASE + '/api/v1/session/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: sessionId,
command: 'goHome'
})
});
// 4. 查询系统状态
const stats = await fetch(BASE + '/api/v1/stats').then(r => r.json());
console.log(stats.data);
// 5. 关闭会话
await fetch(BASE + '/api/v1/session/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
});
# 创建会话(返回 play_url)
curl -X POST https://relay.example.com/api/v1/session/create \
-H "Content-Type: application/json" \
-d '{"device_ip":"192.168.1.10","device_tcp_port":30007,"device_udp_port":30008,"quality":720,"codec":"h264"}'
# 拿到 play_url 后在浏览器打开即可播放,例如:
# https://relay.example.com/player.html?session_id=s_abc123&token=t_xxx
# 获取会话列表
curl https://relay.example.com/api/v1/sessions
# 发送设备指令
curl -X POST https://relay.example.com/api/v1/session/command \
-H "Content-Type: application/json" \
-d '{"session_id":"SESSION_ID","command":"goHome"}'
# 查询系统状态
curl https://relay.example.com/api/v1/stats
# 关闭会话
curl -X POST https://relay.example.com/api/v1/session/close \
-H "Content-Type: application/json" \
-d '{"session_id":"SESSION_ID"}'
import requests
BASE = "https://relay.example.com"
# 创建会话
r = requests.post(f"{BASE}/api/v1/session/create", json={
"device_ip": "192.168.1.10",
"device_tcp_port": 30007,
"device_udp_port": 30008,
"quality": 720,
"codec": "h264"
})
data = r.json()["data"]
session_id = data["session_id"]
play_url = data["play_url"]
# play_url 就是播放地址,拼接完整 URL 后可在浏览器打开
full_url = f"{BASE}{play_url}"
print(f"播放地址: {full_url}")
# 发送设备指令
requests.post(f"{BASE}/api/v1/session/command", json={
"session_id": session_id,
"command": "goHome"
})
# 查询系统状态
stats = requests.get(f"{BASE}/api/v1/stats").json()
# 关闭会话
requests.post(f"{BASE}/api/v1/session/close", json={
"session_id": session_id
})
import java.net.http.*;
import java.net.URI;
import com.google.gson.*;
String BASE = "https://relay.example.com";
HttpClient client = HttpClient.newHttpClient();
Gson gson = new Gson();
// 1. 创建会话
String body = gson.toJson(Map.of(
"device_ip", "192.168.1.10",
"device_tcp_port", 30007,
"device_udp_port", 30008,
"quality", 720,
"codec", "h264"
));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/api/v1/session/create"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
JsonObject data = gson.fromJson(res.body(), JsonObject.class)
.getAsJsonObject("data");
String sessionId = data.get("session_id").getAsString();
String playUrl = data.get("play_url").getAsString();
// 2. 拼接完整播放地址,传给前端
String fullPlayUrl = BASE + playUrl;
// 前端拿到这个 URL 放进 iframe 或 window.open() 即可播放
// 3. 关闭会话
String closeBody = gson.toJson(Map.of("session_id", sessionId));
HttpRequest closeReq = HttpRequest.newBuilder()
.uri(URI.create(BASE + "/api/v1/session/close"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(closeBody))
.build();
client.send(closeReq, HttpResponse.BodyHandlers.ofString());