# 流媒体工程：本地最小实验

配套六篇文章：https://blog.traceof.me/topics/streaming

下载全部源码与图解：https://blog.traceof.me/examples/streaming-lab.zip

这是本地教学实验，不提供可对公网开放的媒体平台。博客仅分发文件，不在服务器上运行编码器、Java 或实验 HTTP 服务。

## 已验证环境

- macOS arm64；FFmpeg/ffprobe 7.1（包含 libx264、AAC）。
- MediaMTX 1.21.1：从 https://github.com/bluenviron/mediamtx/releases/tag/v1.21.1 下载对应平台二进制。macOS arm64 归档 SHA-256：`25e20ed41611f1f3103b8359585210b29b11b69fa0d9e11bd11b92f7bbcb42ef`。校验值只适用于该平台归档。
- hls.js 1.7.3：本目录包含官方 `dist/hls.min.js`，许可证见 `HLS-LICENSE`。
- JDK 21.0.1；Node 24；Python 3。Docker 不是依赖。
- 使用 Chromium 内核浏览器验证，不声称覆盖 Safari、iOS、Android、真实 CDN 或公网网络条件。

## 文件

- `generate.sh`：六十秒合成素材、转封装文件和三档 HLS。
- `verify_media.py`：45 个分片的首帧、分辨率、时间线和完整解码验证。
- `mediamtx.yml`：普通 HLS 的回环媒体服务配置。
- `server.mjs`、`player.html`、`hls.min.js`：静态播放与逐请求网络故障模拟。
- `TranscodeRunner.java`、`StreamState.java`、`LabTest.java`：Java 21 进程管理与状态模型，共 21 项检查。
- `01-media-layers.svg` 至 `06-reliability.svg`：文章技术图；`results.json`：本轮实际观察的摘要。

## 1. 生成与校验

在解压后的目录执行；输出目录请使用专门的实验目录。脚本会覆盖该目录中自己的同名输出，不要指向用户素材目录。

```bash
ffmpeg -version
ffprobe -version
bash generate.sh "$PWD/output"
python3 verify_media.py "$PWD/output"
node server.mjs "$PWD/output"
```

浏览器打开 http://127.0.0.1:18890/player.html ，点击开始实验。视频默认静音，可手动开启。目标码率为视频 600/1200/2400 kbps，加每档 AAC 96 kbps。三档为 640×360、960×540、1280×720，30 fps，两秒 GOP，四秒分片目标。输入为测试图与 440 Hz 正弦音，不含用户媒体。

`verify_media.py` 会对每个分片实际解码，再比较多档起点；还会比较 MP4 与 MKV 解码视频摘要。此校验针对固定素材，不替代任意输入校验或终端兼容性测试。

## 2. RTMP → HLS 直播

将官方 MediaMTX 二进制放在当前目录，三个终端分别运行媒体服务、推流和前述 Node 播放器服务。配置显式关闭 RTSP、WebRTC、SRT 和 MoQ，只开放回环 RTMP/HLS 监听。

```bash
./mediamtx mediamtx.yml
```

```bash
ffmpeg -nostdin -hide_banner -re -stream_loop -1 -i output/source.mp4 \
  -c copy -f flv rtmp://127.0.0.1:19350/live
```

在播放器输入 `http://127.0.0.1:18888/live/index.m3u8`。主清单中的媒体清单地址可能带会话参数，请跟随实际返回内容，不自行猜测分片文件名。清单可读、分片持续推进和浏览器显示画面要分别验证。

推流终端按 Ctrl-C 可模拟断流；记录服务端离线日志和播放器行为。重新执行推流命令，再点击开始实验确认恢复。这里允许重新加载，不承诺无缝自动重连。所有实验结束后在对应终端 Ctrl-C 停止自己启动的进程。

## 3. LL-HLS 对照

复制配置为 `mediamtx-ll.yml`，仅将 RTMP 端口改为 19351、HLS 端口改为 18889，并把 `hlsVariant` 改为 `lowLatency`。启动第二个 MediaMTX；向 19351 推相同文件，在播放器加载 `http://127.0.0.1:18889/live/index.m3u8`。

普通模式观察 `.ts`、`EXTINF`、媒体序号；低延迟模式观察初始化段、`EXT-X-PART`、`EXT-X-SERVER-CONTROL`、`EXT-X-PRELOAD-HINT`，以及播放器带 `_HLS_msn`、`_HLS_part` 的更新请求。部分分片时长以实际清单为准，不把配置值当作测量值。

本次循环输入的服务日志曾提示部分分片时长变化可能影响 iOS；因此该配置没有获得 iOS 可用性验收。不要把它直接作为移动端生产配置。

本地 HTTP + Chromium 实验只验证该组合；目标平台关于 HTTPS、播放能力和代理的要求需要另测。未进行 WebRTC 与 HLS 的公平性能基准，不提供端到端延迟排名。

## 4. 网络与缓存实验

使用默认 `/master.m3u8`；在开发者工具 Network 中禁用缓存后切换情景并开始。稳定、450 kbps/请求、额外 1.8 秒分片等待、接下来两次分片返回 503，四种情景均由 `server.mjs` 提供。先正常播放，再切到限速可观察带宽骤降；已有响应继续采用开始请求时的条件。

本地限速是每个响应独立发送，不是共享链路，不模拟丢包或完整拥塞控制。首帧从按钮操作到首次画面回调，`waiting` 仅在首帧后计入。手动暂停、seek、切后台和媒体结束不适合作为此最小统计器的质量基准；正式埋点应处理完整生命周期。

页面展示当前指标，`window.labResults` 提供当前实验采样和事件，便于在开发者工具复制 JSON。普通实验需禁用缓存；HTTP 缓存实验另做，切回稳定情景后请求任意分片，记录 `ETag`，再带 `If-None-Match` 请求得到 304。分片使用短期缓存，清单使用 `no-store`。这是 HTTP 协商模拟，不是 CDN 命中率测量。

## 5. Java 示例

确保 `java`、`javac` 均为 21。macOS 可使用已安装 JDK：

```bash
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
export PATH="$JAVA_HOME/bin:$PATH"
javac -encoding UTF-8 -d out TranscodeRunner.java StreamState.java LabTest.java
java -cp out LabTest
java -cp out TranscodeRunner output/source.mp4 output/java-360.mp4
ffprobe -v error -show_streams -of json output/java-360.mp4
ffmpeg -v error -xerror -i output/java-360.mp4 -map 0 -f null -
```

执行器不经过 Shell，固定处理模板，日志重定向文件，超时先终止后等待退出，非零退出明确失败，`-n` 拒绝覆盖已有输出。示例不是任意用户输入的沙箱；服务化需补充路径/协议限制、配额、日志轮转和可信产物验证。

状态模型只保存内存，`connect()` 分配 generation；普通事件不能创建新世代。任务同键异参拒绝，claim 受容量上限限制，fence 阻止旧执行者完成。`expire()` 由测试显式调用，不实现分布式租约。状态清理不会真实停止外部进程；输出隔离、数据库条件提交和对账由生产系统负责。

测试覆盖：连接不是可播放、首次与重复提交、同键异参、重复 claim、容量不足、未校验输出、执行权过期、旧执行者、当前结果、重复回执、旧断流、未来世代、旧世代结果、普通回调越权、当前断流、断流后提交；以及实际 FFmpeg 成功、坏输入、超时和非零退出。

## 实验结论边界

结构验证通过不证明主观画质更好；本机首帧不等于直播采集到显示的端到端延迟；本地缓存不等于 CDN；内存状态测试不等于分布式一致性。具体观察窗口和结果见 `results.json`，不同情景不使用不一致的窗口计算性能提升比例。
