Interview Prep

FPlayer 技术面试准备

针对简历中"跨端流媒体播放系统"项目的技术深挖清单。
每条标注代码对应位置,面试被问到能直接指出代码行。

系统架构总览 · 三端 + 控制面/数据面分离
flowchart TD
    subgraph clients["📱 客户端层"]
        desktop["fplayer-ff-desktop
C++17 / Qt6 / FFmpeg 8.1
采集·编码·推流·拉流
~26,000 行"] mobile["fplayer-ff-mobile
Flutter / media_kit
HLS·FLV·RTMP 播放
~2,200 行"] end subgraph service["🖥️ 服务端层"] gw["Gateway (Go, :9000)
HTTP API · 会话管理
地址编排 · 公网代理
859 行 · 零外部依赖"] zlm["ZLMediaKit (C++)
RTMP接收 → HLS/FLV/RTSP
协议自动转封装
动态端口分配"] ui["Electron UI
管理面板"] kc["Kernel Console (Go)
无头模式 · CLI
647 行"] end subgraph protocols["🌐 协议与数据面"] rtmp["RTMP 推流"] flv["HTTP-FLV 低延迟"] hls["HLS 通用兼容"] rtsp["RTSP/SRT"] end desktop -->|"REST: POST /api/v1/streams/start"| gw mobile -->|"REST: GET /api/v1/streams/resolve"| gw gw -.->|"端口配置"| zlm desktop -->|"③ RTMP 推流 (数据面)"| zlm zlm -->|"④ HTTP-FLV/HLS/RTMP"| mobile zlm --> rtmp zlm --> flv zlm --> hls zlm --> rtsp
Section 01
高频必问:架构设计
"这个项目的整体架构是什么?讲一下数据流"

三端架构 + 控制面/数据面分离

桌面端 (C++) ──RTMP推流──→ ZLMediaKit ──HTTP-FLV/HLS──→ 移动端 (Flutter)
    │                            ↑                            │
    └─── REST API ──→ Gateway (Go) ←─── REST API ────────────┘
                      控制面 (会话管理/地址编排)

关键话术:

  • "控制面用 Go 写了一个 Gateway,管理流会话、做 IP 可达性探测、地址编排"
  • "数据面用 ZLMediaKit 做协议转换,RTMP 推进去自动出 HLS/HTTP-FLV/RTSP,一端推流全协议分发"
  • "分离设计的好处:Gateway 挂了不影响正在推的流,ZLM 照常分发"
📍 gateway/main.go (859行) · kernel-console/main.go (647行)
"为什么用 Go 写 Gateway,不用 Node.js?"
  • Go 编译为单二进制,部署简单 — 零外部依赖,仅用 net/http 标准库
  • 内存会话管理(sync.RWMutex),比 Node.js 的 GC 可控
  • 配合 kernel-console 做无头部署,一个 exe 搞定
📍 gateway/main.go · kernel-console/main.go
"你这个项目的分层设计是怎么想的?"
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 子目录依赖顺序
Section 02
C++ 音视频核心
"FFmpeg 播放器的线程模型是怎样的?"

三线程架构:

  1. 解复用线程 (demux):av_read_frame() 读包 → 按 stream_index 分别 push 到视频/音频 packet queue
  2. 解码线程 (decode):从 packet queue 取包 → avcodec_send_packet / avcodec_receive_frame → YUV frame → push 到 frame queue
  3. 音频线程 (audio):从解码后的音频 frame queue 取 → swr_convert 重采样 → Qt QAudioSink 输出

关键细节:音视频同步以音频时钟为主时钟;非 YUV420P 用 sws_scale 转换;Seek 用 avformat_seek_file + 刷新编解码器缓冲区;EOF 循环自动重播。

📍 backend/media_ffmpeg/src/playerffmpeg.cpp (~1265行)
"怎么处理音视频同步的?"
  1. 音频线程每输出一帧,更新 audio_clock(基于 PTS + 已播放样本数/采样率)
  2. 视频帧解码后读取其 PTS,与 audio_clock 比较:视频超前 → sleep(精确到毫秒);视频落后 → 立即渲染(丢帧策略防止延迟累积)
  3. 使用音频为主时钟的原因:人耳对音频不连续更敏感
📍 playerffmpeg.cpp — PTS 同步逻辑
"为什么要把非 YUV420P 的帧转成 YUV420P?"
  • OpenGL 渲染需要统一格式,YUV420P 是三平面格式(Y/U/V 分离),纹理上传效率高
  • YUV420P → RGB 转换在 GPU 侧 shader 完成,比 CPU 转省带宽
  • OpenGL 纹理可以用 3 个 sampler2D 分别绑定 Y/U/V 平面
📍 common/src/fglwidget.cpp — OpenGL YUV 渲染器,双缓冲 PBO
"硬编码是怎么支持的?"
编码器厂商FFmpeg 名称
NVENCNVIDIAh264_nvenc / hevc_nvenc
AMFAMDh264_amf / hevc_amf
QSVIntelh264_qsv / hevc_qsv
MediaFoundationWindows 通用h264_mf
软编码回退libx264

选择策略:先检测 GPU 设备 → 优先同厂商硬编 → 无 GPU 回退软编 → 码率基于 BPP 模型自动估算

📍 backend/stream_ffmpeg/src/streamffmpeg.cpp
"屏幕捕获怎么实现的?"

两种方案,可切换:

  1. FFmpeg gdigrab:兼容性好,性能一般
  2. DXGI Desktop Duplication:Windows 原生,低延迟,直接从 GPU 显存获取桌面帧,无需 CPU 拷贝,延迟比 gdigrab 低 80%+
📍 backend/media_ffmpeg/src/screencaptureffmpeg.cpp · backend/desktopcapture_dxgi/
Section 03
流媒体协议
"RTMP、HTTP-FLV、HLS 的区别?为什么三种都支持?"
协议延迟兼容性适用场景
RTMP1-3sFlash 时代,H5 不支持推流端(OBS/ffmpeg 生态好)
HTTP-FLV1-3sH5 需 MSE/flv.jsWeb 低延迟播放
HLS5-15s全平台原生支持移动端/Web 通用播放
RTSP1-3s监控行业IPC 摄像头接入
SRT1-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 已预留编译开关,待后续版本实现
📍 fplayer-ff-service 项目 — ZLM HLS 分片配置 (segDur=2, segNum=3)
"你的公网模式是怎么实现的?"

问题:服务在内网(192.168.x.x),外网客户端需要公网 IP

方案:Gateway 接受 publicHost 参数 → 地址编排时替换 URL 中的 host → FRP 做端口映射

外网客户端 → frp公网IP:1935 → FRP隧道 → 内网ZLM:1935
              ↑                      ↑
           publicHost覆盖        实际服务在内网
📍 gateway/main.go — publicHostOverride / publishHostForResponse()
Section 04
工程能力
"为什么 CaptureWindow 有 6,746 行?不觉得太长了吗?"

"CaptureWindow 确实偏长,因为它承担了四种模式(摄像头/屏幕/文件/合成)的所有 UI 逻辑 + 推流拉流控制 + AI 对话框 + 截图库侧栏。当时为了快速迭代,先在一个文件里实现完整功能。如果要重构,会拆成独立 Controller + 组合模式。实际上 Service 层已经用工厂模式解耦了业务逻辑,UI 层的臃肿是时间问题不是设计问题。"

📍 widget/src/capturewindow.cpp (~6730行)
"CMake 工程的结构是怎么组织的?怎么管理第三方依赖?"
  • 自定义 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++ 可再发行组件
📍 CMakeLists.txt · cmake/3rd.cmake · cmake/utils.cmake
"你是怎么做多平台支持的?"
组件WindowsLinux
桌面端MSVC/MinGW + Qt6GCC + Qt6
服务端PowerShell 脚本 + .batBash 脚本 + systemd
屏幕捕获DXGI / gdigrabX11 / xdg-portal
公网主机选择直接选本机 IPIP 可达性探测(UDP + 子网 + TCP)
"你做了哪些错误处理和容错?"
  1. 端口冲突处理:动态端口探测 — 优先常用端口,被占用则随机 — ZLM INI 动态补丁
  2. 健康检查重试:Gateway 启动后轮询 /healthz,最多重试 8 次
  3. 优雅关闭:退出时按顺序终止子进程(UI→Gateway→ZLM),1.5 秒超时后强制 kill
  4. 播放器 EOF 处理:文件播放到末尾自动从头循环
  5. API 幂等:同 app+stream 名称重复调用 start 不创建重复会话
📍 gateway/main.go · kernel-console/main.go (fplayer-ff-service)
Section 05
移动端 (Flutter)
"Flutter 移动端主要做了什么?"
  • 单页播放器 lib/main.dart 1,431 行
  • 双模式输入:服务模式(Gateway API 解析 URL)+ 直连模式(手动输入 URL)
  • media_kit(libmpv 封装)支持 HLS/HTTP-FLV/RTMP
  • 平台自适应:Web/Android 默认 HLS,Windows 默认 HTTP-FLV
  • 设置持久化(SharedPreferences,7 个键值)
  • 日志系统(200 条上限,按级别着色)
  • Material 3 深/浅色主题
📍 lib/main.dart (1431行) · pubspec.yaml
"为什么不直接用 video_player 官方插件?"
  • video_player 不支持 RTMP 和 HTTP-FLV(只支持 HLS/DASH/MP4)
  • media_kit 底层是 libmpv,协议支持全(HLS/FLV/RTMP/RTSP 都行)
  • libmpv 解码性能好(硬件解码),适合直播场景
  • 代价:包体积增大 ~15MB(libmpv native libs)
Section 06
可能的追问陷阱
"这个项目有什么不足?如果重来会怎么做?"

诚实 + 改进方向(面试官喜欢能自我批判的候选人):

  1. Gateway 无鉴权 → 应该加 JWT/Basic Auth
  2. 流会话仅内存 → 重启丢失,应该加持久化或至少 Redis
  3. 无性能测试数据 → 应该做压测(并发路数、内存/CPU 曲线)
  4. 移动端代码单文件 → 应该拆组件 + 状态管理(Provider/Riverpod)
  5. 无 CI/CD → 应该加 GitHub Actions 自动构建 + 测试
  6. 无单元测试覆盖 → 桌面端应该加 FFmpeg 后端 mock 测试
"如果有 1000 路并发推流,你的系统会出什么问题?"
  1. Gateway 内存:每个 streamRecord ~1KB,1000 路 = 1MB — 不是问题
  2. ZLM 性能:瓶颈在编解码,单实例通常支持几十路,1000 路需要集群 + 转码节点分离
  3. 带宽:1080p@30fps 约 4Mbps/路 → 1000 路 = 4Gbps 上行
  4. 改进方向:ZLM 集群 + 负载均衡 + Gateway 无状态化(可用 Nginx 反向代理多实例)
Section 07
1 分钟电梯演讲

"我做了一个跨端流媒体播放系统 fplayer,三端架构:桌面端用 C++/Qt/FFmpeg 实现本地采集编码推流,服务端用 Go+ZLMediaKit 做流媒体分发,移动端用 Flutter 做拉流播放。

核心亮点是控制面与数据面分离——Go Gateway 负责会话管理和地址编排,ZLMediaKit 专注媒体分发,RTMP 推入自动转 HLS/HTTP-FLV/RTSP 多协议输出。桌面端有约 2.6 万行 C++ 代码,包括自研的 FFmpeg 三线程解码器、硬件编码推流引擎、OpenGL YUV 渲染器,支持摄像头/屏幕/文件/合成四种采集模式。

整个项目从 CMake 构建到跨平台打包脚本都是我独立完成,支持公网/局域网双模式部署。目前在持续迭代中。"

Section 08
面试速查表(按关键字)
面试官问你答什么代码在哪
"线程模型"三线程:解复用→解码→音频,PTS 同步playerffmpeg.cpp
"硬编码"NVENC/AMF/QSV/MF 四种streamffmpeg.cpp
"音视频同步"音频主时钟,PTS 对比,视频 sleep/dropplayerffmpeg.cpp
"OpenGL 渲染"YUV420P 三平面纹理,双缓冲 PBOfglwidget.cpp
"屏幕捕获"gdigrab + DXGI Desktop Duplicationscreencaptureffmpeg.cpp + DXGI
"协议转换"ZLM 自动 RTMP→HLS/FLV/RTSPconfig.ini
"公网穿透"publicHost 覆盖 + FRP 端口映射gateway/main.go publicHostOverride
"端口分配"动态探测,优先 1935/8080/9000kernel-console/main.go (fplayer-ff-service)
"架构分层"api→backend→runtime→service→widgetCMakeLists.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