三端架构 + 控制面/数据面分离
桌面端 (C++) ──RTMP推流──→ ZLMediaKit ──HTTP-FLV/HLS──→ 移动端 (Flutter)
│ ↑ │
└─── REST API ──→ Gateway (Go) ←─── REST API ────────────┘
控制面 (会话管理/地址编排)
关键话术:
- "控制面用 Go 写了一个 Gateway,管理流会话、做 IP 可达性探测、地址编排"
- "数据面用 ZLMediaKit 做协议转换,RTMP 推进去自动出 HLS/HTTP-FLV/RTSP,一端推流全协议分发"
- "分离设计的好处:Gateway 挂了不影响正在推的流,ZLM 照常分发"
- Go 编译为单二进制,部署简单 — 零外部依赖,仅用 net/http 标准库
- 内存会话管理(sync.RWMutex),比 Node.js 的 GC 可控
- 配合 kernel-console 做无头部署,一个 exe 搞定
app/ → 入口 (main.cpp) ~140 行 widget/ → UI 层 (CaptureWindow) ~8,800 行 service/ → 业务编排层 (工厂模式) ~1,800 行 runtime/ → 运行时工厂 (后端切换) ~190 行 backend/ → 后端实现 (FFmpeg/Qt6/DXGI) ~13,800 行 api/ → 抽象接口层 (纯虚类) ~340 行 common/ → 公用工具 (OpenGL渲染器) ~1,200 行
"依赖方向是 app → widget → service → runtime → backend → api,上层不依赖具体实现,通过 api/ 的纯虚接口解耦。"
📍 CMakeLists.txt 子目录依赖顺序三线程架构:
- 解复用线程 (demux):av_read_frame() 读包 → 按 stream_index 分别 push 到视频/音频 packet queue
- 解码线程 (decode):从 packet queue 取包 → avcodec_send_packet / avcodec_receive_frame → YUV frame → push 到 frame queue
- 音频线程 (audio):从解码后的音频 frame queue 取 → swr_convert 重采样 → Qt QAudioSink 输出
关键细节:音视频同步以音频时钟为主时钟;非 YUV420P 用 sws_scale 转换;Seek 用 avformat_seek_file + 刷新编解码器缓冲区;EOF 循环自动重播。
📍 backend/media_ffmpeg/src/playerffmpeg.cpp (~1265行)- 音频线程每输出一帧,更新 audio_clock(基于 PTS + 已播放样本数/采样率)
- 视频帧解码后读取其 PTS,与 audio_clock 比较:视频超前 → sleep(精确到毫秒);视频落后 → 立即渲染(丢帧策略防止延迟累积)
- 使用音频为主时钟的原因:人耳对音频不连续更敏感
- OpenGL 渲染需要统一格式,YUV420P 是三平面格式(Y/U/V 分离),纹理上传效率高
- YUV420P → RGB 转换在 GPU 侧 shader 完成,比 CPU 转省带宽
- OpenGL 纹理可以用 3 个 sampler2D 分别绑定 Y/U/V 平面
| 编码器 | 厂商 | FFmpeg 名称 |
|---|---|---|
| NVENC | NVIDIA | h264_nvenc / hevc_nvenc |
| AMF | AMD | h264_amf / hevc_amf |
| QSV | Intel | h264_qsv / hevc_qsv |
| MediaFoundation | Windows 通用 | h264_mf |
| 软编码回退 | — | libx264 |
选择策略:先检测 GPU 设备 → 优先同厂商硬编 → 无 GPU 回退软编 → 码率基于 BPP 模型自动估算
📍 backend/stream_ffmpeg/src/streamffmpeg.cpp两种方案,可切换:
- FFmpeg gdigrab:兼容性好,性能一般
- DXGI Desktop Duplication:Windows 原生,低延迟,直接从 GPU 显存获取桌面帧,无需 CPU 拷贝,延迟比 gdigrab 低 80%+
| 协议 | 延迟 | 兼容性 | 适用场景 |
|---|---|---|---|
| RTMP | 1-3s | Flash 时代,H5 不支持 | 推流端(OBS/ffmpeg 生态好) |
| HTTP-FLV | 1-3s | H5 需 MSE/flv.js | Web 低延迟播放 |
| HLS | 5-15s | 全平台原生支持 | 移动端/Web 通用播放 |
| RTSP | 1-3s | 监控行业 | IPC 摄像头接入 |
| SRT | 1-3s | 新兴协议 | 公网不稳定网络推流 |
设计逻辑:RTMP 做推流入口,ZLMediaKit 自动转封装 → HTTP-FLV(低延迟 Web 播放)+ HLS(iOS/Safari/微信内置浏览器),五种输出共存,客户端按能力选。
- 端到端延迟:局域网 1-3s(HTTP-FLV),公网 2-5s
- HLS 延迟高:分片策略(2s/段 × 3 段 = 最少 6s 缓冲),可调但牺牲稳定性
- 瓶颈:编码延迟 > 网络传输 > 播放缓冲
- 编码:硬件编码器(NVENC ~10ms/frame)vs 软编码(x264 ultrafast ~30ms/frame)
- 传输:局域网 RTMP over TCP 几乎无延迟
- 缓冲:播放器的 jitter buffer / GOP 大小决定起播延迟
- 低延迟方案:WebRTC(<500ms),CMake 已预留编译开关,待后续版本实现
问题:服务在内网(192.168.x.x),外网客户端需要公网 IP
方案:Gateway 接受 publicHost 参数 → 地址编排时替换 URL 中的 host → FRP 做端口映射
外网客户端 → frp公网IP:1935 → FRP隧道 → 内网ZLM:1935
↑ ↑
publicHost覆盖 实际服务在内网
📍 gateway/main.go — publicHostOverride / publishHostForResponse()
"CaptureWindow 确实偏长,因为它承担了四种模式(摄像头/屏幕/文件/合成)的所有 UI 逻辑 + 推流拉流控制 + AI 对话框 + 截图库侧栏。当时为了快速迭代,先在一个文件里实现完整功能。如果要重构,会拆成独立 Controller + 组合模式。实际上 Service 层已经用工厂模式解耦了业务逻辑,UI 层的臃肿是时间问题不是设计问题。"
📍 widget/src/capturewindow.cpp (~6730行)- 自定义
add_standard_module宏:统一处理 AUTOMOC/AUTOUIC/AUTORCC + src/include/private/uis/res 目录结构 + 导出符号 - FFmpeg:预编译库,根据编译器(MSVC/MinGW/GCC)自动选择 lib 路径
- Logger/yaml-tool:通过 CPM.cmake 从 Git 拉取
- Qt6:通过 CMake find_package
- 构建选项:FPLAYER_BUILD_MEDIA_FFMPEG / FPLAYER_BUILD_STREAM_FFMPEG 等按需裁剪
- CPack 打包:NSIS (Win) + DEB/TGZ (Linux) + 自动下载 VC++ 可再发行组件
| 组件 | Windows | Linux |
|---|---|---|
| 桌面端 | MSVC/MinGW + Qt6 | GCC + Qt6 |
| 服务端 | PowerShell 脚本 + .bat | Bash 脚本 + systemd |
| 屏幕捕获 | DXGI / gdigrab | X11 / xdg-portal |
| 公网主机选择 | 直接选本机 IP | IP 可达性探测(UDP + 子网 + TCP) |
- 端口冲突处理:动态端口探测 — 优先常用端口,被占用则随机 — ZLM INI 动态补丁
- 健康检查重试:Gateway 启动后轮询 /healthz,最多重试 8 次
- 优雅关闭:退出时按顺序终止子进程(UI→Gateway→ZLM),1.5 秒超时后强制 kill
- 播放器 EOF 处理:文件播放到末尾自动从头循环
- API 幂等:同 app+stream 名称重复调用 start 不创建重复会话
- 单页播放器
lib/main.dart1,431 行 - 双模式输入:服务模式(Gateway API 解析 URL)+ 直连模式(手动输入 URL)
- media_kit(libmpv 封装)支持 HLS/HTTP-FLV/RTMP
- 平台自适应:Web/Android 默认 HLS,Windows 默认 HTTP-FLV
- 设置持久化(SharedPreferences,7 个键值)
- 日志系统(200 条上限,按级别着色)
- Material 3 深/浅色主题
- video_player 不支持 RTMP 和 HTTP-FLV(只支持 HLS/DASH/MP4)
- media_kit 底层是 libmpv,协议支持全(HLS/FLV/RTMP/RTSP 都行)
- libmpv 解码性能好(硬件解码),适合直播场景
- 代价:包体积增大 ~15MB(libmpv native libs)
诚实 + 改进方向(面试官喜欢能自我批判的候选人):
- Gateway 无鉴权 → 应该加 JWT/Basic Auth
- 流会话仅内存 → 重启丢失,应该加持久化或至少 Redis
- 无性能测试数据 → 应该做压测(并发路数、内存/CPU 曲线)
- 移动端代码单文件 → 应该拆组件 + 状态管理(Provider/Riverpod)
- 无 CI/CD → 应该加 GitHub Actions 自动构建 + 测试
- 无单元测试覆盖 → 桌面端应该加 FFmpeg 后端 mock 测试
- Gateway 内存:每个 streamRecord ~1KB,1000 路 = 1MB — 不是问题
- ZLM 性能:瓶颈在编解码,单实例通常支持几十路,1000 路需要集群 + 转码节点分离
- 带宽:1080p@30fps 约 4Mbps/路 → 1000 路 = 4Gbps 上行
- 改进方向:ZLM 集群 + 负载均衡 + Gateway 无状态化(可用 Nginx 反向代理多实例)
"我做了一个跨端流媒体播放系统 fplayer,三端架构:桌面端用 C++/Qt/FFmpeg 实现本地采集编码推流,服务端用 Go+ZLMediaKit 做流媒体分发,移动端用 Flutter 做拉流播放。
核心亮点是控制面与数据面分离——Go Gateway 负责会话管理和地址编排,ZLMediaKit 专注媒体分发,RTMP 推入自动转 HLS/HTTP-FLV/RTSP 多协议输出。桌面端有约 2.6 万行 C++ 代码,包括自研的 FFmpeg 三线程解码器、硬件编码推流引擎、OpenGL YUV 渲染器,支持摄像头/屏幕/文件/合成四种采集模式。
整个项目从 CMake 构建到跨平台打包脚本都是我独立完成,支持公网/局域网双模式部署。目前在持续迭代中。"
| 面试官问 | 你答什么 | 代码在哪 |
|---|---|---|
| "线程模型" | 三线程:解复用→解码→音频,PTS 同步 | playerffmpeg.cpp |
| "硬编码" | NVENC/AMF/QSV/MF 四种 | streamffmpeg.cpp |
| "音视频同步" | 音频主时钟,PTS 对比,视频 sleep/drop | playerffmpeg.cpp |
| "OpenGL 渲染" | YUV420P 三平面纹理,双缓冲 PBO | fglwidget.cpp |
| "屏幕捕获" | gdigrab + DXGI Desktop Duplication | screencaptureffmpeg.cpp + DXGI |
| "协议转换" | ZLM 自动 RTMP→HLS/FLV/RTSP | config.ini |
| "公网穿透" | publicHost 覆盖 + FRP 端口映射 | gateway/main.go publicHostOverride |
| "端口分配" | 动态探测,优先 1935/8080/9000 | kernel-console/main.go (fplayer-ff-service) |
| "架构分层" | api→backend→runtime→service→widget | CMakeLists.txt 子目录依赖 |
| "工厂模式" | RunTime 工厂按 BackendType 实例化 | runtime/ |
| "跨平台" | CMake 自动检测编译器+平台 | cmake/3rd.cmake |
| "流会话管理" | sync.RWMutex + map,幂等,无持久化 | gateway/main.go |
| "推流协议" | RTMP/RTSP/SRT 三种 | streamffmpeg.cpp |
| "拉流能力" | 桌面端也支持拉流 + 录制 | capturewindow.cpp PullPreviewDialog |
| "组合模式" | QMdiArea + 独立 Service 实例 + YUV 合成推流 | capturewindow.cpp · streamffmpeg.cpp |