ppmmx standalone 自建节点功能测试报告¶
部署 → 推流 → 拉流 → 按需录像 → 回放地址 → 过期清理
| 属性 | 值 |
|---|---|
| 文档类型 | 功能测试报告 |
| 测试对象 | ppmmx standalone 自建节点(ppmmxDocker 部署):推流接入、WHEP 拉流、按需录像(POST /api/record/split)、回放地址、过期录像清理 |
| 测试时间 | 2026-10-23(北京时间) |
| 被测版本 | ppmmx v1.19.1rc080(linux/amd64,含 standalone 按需录像与本地回放能力) |
| 测试环境 | 1 台 Ubuntu 云主机 + Docker(network_mode: host),公网 IP 与 Let's Encrypt https 域名 mmx-xxxxxxxx.rent.pp-cdn.org 均已脱敏;PPCDN 生产 ppcenter(api.pp-cdn.org);一个专用测试 App(appId 记为 app_xxx,appSecret 未出现在本文) |
| 测试流 | 测试端持续 SRT 双流推流:app_xxx/B01-frontView/h264(H264,三档 simulcast:1080p / 720p / 360p)与 app_xxx/B01-frontView/hevc(HEVC,三档),音频 Opus;1080p H264 约 4 Mbps |
| 结论 | 通过:六项功能全部符合预期;录像文件保留在节点本机,round-end 响应直接返回回放地址,地址无需任何凭证即可播放;过期清理见 §7 |
1. 测试目标¶
按「自建一个节点并用起来」的真实顺序,逐项验证:
- 如何部署:一键脚本部署 standalone 节点,节点注册到 ppcenter 并显示运行中;
- 如何推流:向 ppcenter 取到的推流地址确实指向自己的节点,并能在节点上看到在线的流;
- 如何拉流观看:浏览器直接观看、标准 WHEP 地址、经 ppcenter 取播放地址三种方式;
- 如何确定录像 API 地址并按需录像:API 主机必须是自己的节点地址,一场次
round-start→round-end; - 如何获得回放地址:
round-end响应里直接带回放地址,能下载、能在浏览器里播放; - 如何清理过期录像:按
recordDeleteAfter(10 天)自动清理,清理后回放地址失效。
2. 如何部署¶
2.1 创建节点并拿到 License Code¶
- 登录 PPCDN 控制台 → ppmmx 节点 标签页 → 新增 ppmmx 节点,类型选
standalone; - 创建成功后得到 License Code(形如
lic_xxx,随时可在节点列表复制)。
2.2 在自己的服务器上一键部署¶
脚本由你自己在目标服务器上执行,只需要 License Code(节点类型会凭它自动查回来):
curl -fsSL https://raw.githubusercontent.com/ppcdn-org/ppmmxDocker/main/deploy.sh -o deploy.sh
chmod +x deploy.sh
./deploy.sh --license-code lic_xxx
脚本会自动完成:安装 Docker(如缺)、探测公网 IP、向 ppcenter 申请一个固定的 https 域名并配好 Caddy 自动证书、生成 .env、
docker compose up -d --build 拉起节点。Windows 用 deploy.ps1 -LicenseCode lic_xxx。
2.3 放行端口¶
| 端口 | 协议 | 用途 |
|---|---|---|
| 8888 | TCP | WHIP/WHEP 信令;同时是录像 API 与回放地址的服务端口 |
| 8188 | UDP | WebRTC 媒体(ICE) |
| 7890 | UDP | SRT 推流入口(standalone 默认开启) |
| 80、443 | TCP | 自动 HTTPS(证书签发 + https 入口) |
| 8080 | TCP | 管理界面,只对运维开放,不要对公网放开 |
2.4 验证部署成功¶
docker ps # ppmmx、ppmmx-caddy 均为 Up
docker logs ppmmx 2>&1 | grep -E "mmx v1|MMX control connected"
预期看到 mmx v1.19.1rc0xx, linux, amd64 与 MMX control connected to wss://api.pp-cdn.org/ws/mmx,
控制台节点列表里该节点状态变为「运行中」。
实测:新版本启动后 5 秒内 MMX control connected,两路推流在约 40 秒内自动重连。
关于本次测试的版本
standalone 的按需录像能力是新增功能:测试时 ppmmxDocker 发布的 ppmmx 还是旧版本,因此本次测试是在一台已按 §2.2
部署好的节点上手工替换二进制后重建镜像(做法见附录 A)。现在 ppmmxDocker 已改为在构建镜像时从它的 GitHub Release
下载 ppmmx 二进制(仓库里不再存放二进制,Release 只保留最新版本);维护者发布新版本后,新部署的节点开箱即有录像能力,
已部署的节点执行 git pull && docker compose build --no-cache ppmmx && docker compose up -d ppmmx 即可升级。
3. 如何推流¶
3.1 取得推流地址:就是你自己的节点¶
standalone 节点注册后归入你自己的资源池,ppcenter 给你的 App 分配推流入口时只会选你自己的节点。实测向 ppcenter 请求 SRT 推流地址:
curl -s -X POST https://api.pp-cdn.org/v1/publish/requests \
-H "Content-Type: application/json" \
-d '{"appId":"app_xxx","appSecret":"<你的 appSecret>","streamName":"B01-frontView","protocol":"srt","requestRegion":"cn"}'
返回(节选,token 已省略):
{
"originNodeId": 11,
"ppcdnPublishEndpoint": "<节点公网IP>:7890",
"publishToken": "v1.whip-1.…",
"publishTokens": { "h264": "v1.whip-1.…", "hevc": "v1.whip-1.…" }
}
ppcdnPublishEndpoint 正是自建节点的 公网IP:7890,originNodeId 是这个节点在 ppcenter 里的编号。
3.2 推流¶
- ppobs(推荐):设置 → 直播 → 服务选
PPCDN,填appId/appSecret,入库协议选 SRT 或 WHIP,开始推流。地址与 token 由 ppobs 自动换取,断线自动重连。详见 ppobs 使用指南。 - 自己拼 SRT 地址:
srt://{ppcdnPublishEndpoint}?streamid=publish:{appId}/{streamName}/{codec}:{token},其中codec为h264或hevc,token用上面publishTokens里对应 codec 的那个(token 绑定精确路径)。 - 流名约束:
streamName必须是{tableId}-{view}两段式(本次为B01-frontView,即tableId=B01、view=frontView),否则推流会被节点拒绝,后面的按需录像也找不到这路流。
3.3 在节点上确认流已在线¶
curl -s localhost:8080/v3/paths/list # 在节点本机执行(管理端口不对公网开放)
docker logs ppmmx 2>&1 | grep recv-stats | tail -3
实测:两条路径 app_xxx/B01-frontView/h264、app_xxx/B01-frontView/hevc 均 ready: true;
h264 路径含三条 H264 轨(1920×1080 / 1280×720 / 640×360)与一条 Opus 音频轨,hevc 路径含三条 H265 轨与 Opus;
日志每 10 秒输出一次 [SRT] … recv-stats(码率、丢包、RTT 等)。
4. 如何拉流观看¶
三种方式,都落到你自己的节点上。
4.1 浏览器直接打开(最快)¶
节点自带观看页:
https://mmx-xxxxxxxx.rent.pp-cdn.org/<appId>/<streamName>/h264
实测:用浏览器打开 …/app_xxx/B01-frontView/h264,约 4 秒后出现实时画面(叠加的时间戳与当时墙钟时间一致)。HEVC 观众把末尾换成 /hevc,需浏览器支持 HEVC 解码。
4.2 标准 WHEP 地址¶
任何 WHEP 播放器都可以用:
https://mmx-xxxxxxxx.rent.pp-cdn.org/<appId>/<streamName>/h264/whep # 或 /hevc/whep
standalone 默认不开启播放鉴权(webrtcPlaybackAuthEnable: false),地址可直接使用。实测:对该地址 OPTIONS 返回 204,浏览器通过它完成 WebRTC 建连并出画。
4.3 经 ppcenter 取播放地址(接入自己的播放页)¶
自己的网页/播放器用 pplayer,或者由业务后端向 ppcenter 取播放地址:
# 1) 换取 viewer token(后端调用,appSecret 不要放到前端)
curl -s -X POST https://api.pp-cdn.org/auth/generate -H "Content-Type: application/json" \
-d '{"appId":"app_xxx","appSecret":"<你的 appSecret>","stream":"B01-frontView"}'
# 2) 用 Authorization: Bearer {appId}:{txTime}:{txSecret} 取播放地址
curl -s -X POST https://api.pp-cdn.org/v1/play/requests \
-H "Authorization: Bearer app_xxx:<txTime>:<txSecret>" -H "Content-Type: application/json" \
-d '{"streamName":"B01-frontView","clientId":"c1","requestRegion":"cn","capabilities":["whep"]}'
实测返回(节选):
{
"mode": "edge-only",
"edgeStreamUrl": "https://mmx-xxxxxxxx.rent.pp-cdn.org/app_xxx/B01-frontView?txTime=…&txSecret=…",
"edge": { "nodeId": 11, "region": "self-hosted" }
}
edge.nodeId 与 §3.1 的 originNodeId 是同一个节点:观众被调度到你自己的 standalone 节点。播放端给 edgeStreamUrl 追加 /h264/whep 或 /hevc/whep 即可。
5. 如何确定录像 API 地址并按需录像¶
5.1 API 地址:必须是自己节点的地址¶
录像发生在接收这路流的节点上,所以录像 API 要调用你自己的节点:
POST {你的节点地址}/api/record/split
{你的节点地址} 就是节点 .env 里的 MMX_WEBRTC_BASE_URL(自动 HTTPS 时是 https://mmx-xxxxxxxx.rent.pp-cdn.org,否则是 http://<公网IP>:8888):
grep ^MMX_WEBRTC_BASE_URL= ~/ppmmxDocker/.env
不要使用 api.pp-cdn.org
api.pp-cdn.org 是 PPCDN 托管的录像节点,只录推到托管节点上的流,看不到自建节点上的流。
官方示例客户端 record_api.py 会直接拒绝这个地址。
5.2 一场次 = 一次开始 + 一次结束¶
tableId 对应流名 {tableId}-{view} 的前缀(本次 B01);gameId 是你的业务场次标识;gameRound 为空表示开始,非空表示结束。签名用该 appId 自己的 appSecret(md5(appSecret + time + appId + tableId + [gameRound] + gameId)),完整协议见 场次录像 API。
用官方示例客户端(Python 标准库即可)走一遍完整场次:
cd ppmmxDocker/example/recordApi
export RECORD_API_BASE_URL=https://mmx-xxxxxxxx.rent.pp-cdn.org # 你自己的节点
export RECORD_API_APP_ID=app_xxx
export RECORD_API_APP_SECRET=<你的 appSecret>
python record_api.py auto --table-id B01 --game-id my-game-001 --game-round gc001 --wait 30
实测(--wait 30,即录 30 秒):
[round-start] HTTP 200
response: {"code":200,"msg":"OK","data":null}
... round in progress, waiting 30s ...
[round-end (gameRound=gc…)] HTTP 200
response: {"code":200,"msg":"OK","data":{"playbackUrls":[{"view":"frontView","url":"https://mmx-xxxxxxxx.rent.pp-cdn.org/api/record/play/app_xxx/splitrec_29c0….mp4","fileName":"B01-frontView-gc….mp4","sizeBytes":7892613,"durationSeconds":30}]}}
要点:
- 自建节点的
round-start响应data为null,round-end的data.playbackUrls才带回放地址; - 同一
(appId, tableId)同一时刻只能被一个gameId持有,另一个gameId的round-start会被拒绝; - 该
tableId下当前没有任何在线流时,round-start返回code=50001; - 开始后不要立刻结束(短于约 1 秒的分段时长会因「没有可收尾的内容」失败),正常场次远长于此。
6. 如何获得回放地址¶
6.1 地址从哪来¶
round-end 响应里的 data.playbackUrls[].url 就是回放地址,每个在线机位一项(view)。格式:
{节点地址}/api/record/play/<appId>/<记录id>.mp4
- 不鉴权、不设过期:文件还在节点上,任何拿到地址的人都能播放;记录 id 随机、无法猜测,但仍不要把地址发给不该看的人;
- 地址只能打开属于该
appId的记录,换成别的appId访问同一个记录 id 返回 404; - 文件由节点本机提供:
data/recordings/<日期>/<appId>/<tableId>-<view>/<tableId>-<view>-<gameRound>.mp4(docker 卷mmx-data)。
6.2 实测验证¶
| 验证项 | 方法 | 结果 |
|---|---|---|
| 免凭证下载 | curl -o rec.mp4 <url> |
HTTP 200,Content-Type: video/mp4,7,892,613 字节,与响应里的 sizeBytes 一致 |
| Range / 拖动进度条 | curl -H "Range: bytes=0-99" <url> |
HTTP 206,Content-Range: bytes 0-99/7892613 |
| 文件结构 | 解析顶层 box | ftyp → moov → mdat(普通 mp4,moov 在前,可边下边播) |
| 媒体信息 | ffprobe |
H264 1920×1080 约 30 fps + Opus 音频,时长 30.05 秒 |
| 完整解码 | ffmpeg -i rec.mp4 -f null - |
无任何报错 |
| 浏览器播放 | 浏览器直接打开地址,读取 <video> 状态 |
duration=30.05、1920×1080、paused=false、error=null,正常播放 |
| 跨 App 访问 | 把地址里的 appId 换成别的 |
HTTP 404 recording not found |
7. 如何清理过期录像¶
7.1 机制¶
mmx 自带录像清理,按文件最后修改时间(≈ 场次结束时间)清理超过保留期的录像:
- 保留期由
conf/standalone.yml中paths.all.recordDeleteAfter决定,模板默认240h(10 天); - 清理在节点启动时执行一次,之后每
min(30 分钟, 保留期/2)执行一次(默认每 30 分钟); - 文件被删除后,其所在的空目录逐层清理(最底层随即清理,上层空目录在后续周期清理);原回放地址返回
404; - 该清理同时覆盖原始录像分段与已完成场次的文件;只在
recordLocalPlayback: true的节点上清理已完成场次的文件(standalone / 自建 record 模板默认开启)。
7.2 调整保留期¶
paths:
all:
recordDeleteAfter: 240h # 改成想要的保留期,如 72h(3 天)、720h(30 天)
修改 conf/standalone.yml(或按 README「自定义配置」复制为 mmx.yml 后修改),然后 docker compose up -d --build ppmmx 生效。
磁盘容量请按「每天录像量 × 保留天数」规划;recordMinFreeSpace(模板 8G)触发的强制回收只删除原始分段,不会删除已完成场次的文件。
7.3 手工清理¶
不想等周期,或想提前删除某些文件:
docker exec ppmmx find /app/data/recordings -name '*.mp4' -mtime +10 -delete # 删除 10 天前的
7.4 实测验证¶
为避免等待 10 天,把 §5 产生的测试录像的修改时间改到 11 天前,等待节点的下一轮清理:
docker exec ppmmx touch -d "11 days ago" /app/data/recordings/<日期>/app_xxx/B01-frontView/B01-frontView-gc….mp4
实测结果:节点启动后的周期点(节点时间 16:26:40)过后,该文件在 16:26:41 被删除:
find /app/data/recordings -name '*.mp4'不再有结果;- 原回放地址返回 HTTP
404,正文recording file is no longer available; - 文件所在的最底层空目录(
B01-frontView)随即被清理,上层的日期、appId空目录需要在后续周期里逐层清理(观察时它们仍在,不占空间)。
8. 测试结果汇总¶
| # | 功能 | 预期 | 实测 | 结果 |
|---|---|---|---|---|
| 1 | 一键部署并注册 | 节点注册到 ppcenter,状态运行中 | MMX control connected,5 秒内完成 |
通过 |
| 2 | 推流入口指向自己的节点 | /v1/publish/requests 返回自建节点的 SRT 地址 |
返回 <节点公网IP>:7890,originNodeId=11 |
通过 |
| 3 | 推流在线 | 节点上出现 h264 / hevc 路径,含 simulcast 与音频 | 两路 ready,各三档视频 + Opus |
通过 |
| 4 | 浏览器直接观看 | 打开观看页出画 | 约 4 秒出现实时画面 | 通过 |
| 5 | WHEP 地址 | 标准 WHEP 可建连 | OPTIONS 204,WebRTC 建连成功 |
通过 |
| 6 | 经 ppcenter 取播放地址 | 调度到自己的节点 | edge.nodeId 与推流节点一致 |
通过 |
| 7 | 录像 API 走自己的节点 | round-start/round-end 均 200 |
均 200 | 通过 |
| 8 | round-end 返回回放地址 |
data.playbackUrls 含地址、大小、时长 |
与实际文件一致 | 通过 |
| 9 | 回放地址免凭证、可 Range、浏览器可播 | 200 / 206 / 正常播放 | 200 / 206 / 正常播放,解码无报错 | 通过 |
| 10 | 回放地址隔离 | 其他 appId 访问 404 | 404 | 通过 |
| 11 | 过期录像清理 | 超过保留期的文件被删,地址 404 | 把测试录像的修改时间改为 11 天前后,在下一个清理周期点(16:26:40)被删,原地址返回 404 | 通过 |
9. 发现与限制¶
- 录像只录 H264 路径的最高一档:测试流有三档 simulcast(1080p / 720p / 360p),录像取
…/h264路径的第一条视频轨(1080p)和音频;日志里会出现skipping track 2 / 3 (H264)的警告,属预期。…/hevc路径不录像。 - 回放地址不鉴权:这是设计取舍(文件在你自己的节点上、记录 id 不可猜测);需要权限控制时,请由你的业务后端代理或自行在节点前加访问控制。
- 回放地址与 API 同端口:
8888(或 https 域名)既是推流/拉流入口,也是录像 API 与回放地址的入口,防火墙需对业务后端与观看者放行。 - 清理按周期执行:默认每 30 分钟一轮,过期文件最多会多留一个周期;需要更及时可缩短
recordDeleteAfter(周期取其一半与 30 分钟中较小者)。 - 磁盘规划:已完成场次的文件在保留期内一直占用磁盘,容量要按「每天录像量 × 保留天数」自行规划。
- 启动日志里的一条 warn:节点重启后第一秒会出现一次
[publish-whitelist] app sync fetch failed,随后与 ppcenter 连接建立并正常同步,此后带签名的录像 API 调用正常,不影响使用。 - 未覆盖:
advance鉴权模式、appEnv前缀、同时配置S3_*上传、多机位同时录像、长时间(小时级)录像与并发场次;本次只验证单机位、30 秒场次。
附录 A:测试预发布版本的步骤¶
适用于「节点已用 §2.2 部署,需要在正式发布前验证一个新构建」。ppmmxDocker 的镜像构建从 MMX_BASE_URL 下载二进制
(默认是 GitHub Release),所以测试时把它指向自己的临时文件服务即可,不需要改仓库里的任何文件:
# 在开发机交叉编译,并生成校验文件,放进一个临时目录
cd ppmmx && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /tmp/pre/mmx-linux-amd64 .
cd /tmp/pre && sha256sum mmx-linux-amd64 > mmx-linux-amd64.sha256 && python3 -m http.server 8000 &
# 在节点上先备份当前镜像,再用临时地址重建并只重启 ppmmx(Caddy 不动)
ssh root@<节点> 'cd ~/ppmmxDocker && docker tag ppmmx:latest ppmmx:backup-$(date +%Y%m%d%H%M%S) \
&& docker compose build --build-arg MMX_BASE_URL=http://<开发机地址>:8000 ppmmx && docker compose up -d ppmmx'
重启会让推流断开几秒,推流端自动重连。回滚:把 ppmmx:backup-<时间戳> 重新打回 ppmmx:latest,再 docker compose up -d ppmmx。
正式发布由维护者在 ppmmxDocker 里运行 ./build.sh --release(见该仓库 README「维护者」一节)。
附录 B:相关文档¶
- 场次录像 API:协议、签名、状态码与回放地址字段;
- ppobs 使用指南、pplayer 集成指南;
- ppmmxDocker 的 README 与
example/recordApi/(部署、环境变量、示例客户端); - ppmmx Standalone 压测报告:同一类节点的并发承载能力。