项目概览
这个项目做了什么
FPlayer FF Service 是一个可以独立运行在电脑上的流媒体服务程序。
先解释几个词:
- 程序:你双击图标或在终端里敲一行命令后运行起来的东西。它能持续运行,接收输入、做出处理、给出输出。微信、浏览器、音乐播放器都是程序。
- 服务程序:有一种程序不需要你去操作它。它启动后在后台安静等着,等别的程序通过网络找它要数据。网页后端、API 接口都属于这一类。FPlayer FF Service 就是这样——它启动后等着 Desktop 和 Mobile 来找它。
- 流媒体:就是"象流水一样持续传输的音视频数据",而不是先录成文件再传。后面第 02 节会展开讲。
这个程序做的事情很简单:
- 接收视频数据—— 采集端(桌面客户端 Desktop)通过网络把正在录制的画面和声音持续发给它
- 转换播放格式—— 同一份视频数据自动变成多种不同的"包装方式",让不同类型的播放端都能用
- 管理播放地址—— 告诉推流端往哪推、告诉播放端从哪拉。地址怎么拼、端口是多少全部由它处理
它不采集画面(那是 desktop 的事),也不显示画面(那是 mobile 或播放器的事)。它只做中间环节:收 → 转 → 发。
FPlayer FF Service 的整体定位:接收推流 → 协议转换 → 分发播放。内部有四个组件各司其职。
它和谁配合工作
- fplayer-ff-desktop:桌面客户端程序,采集屏幕或摄像头画面推送给 Service
- fplayer-ff-service:本项目——接收、转换、分发
- fplayer-ff-mobile:手机 App,从 Service 获取地址后拉流播放
典型场景:同一局域网下,一台电脑采集推流 → Service 中转 → 其他设备拉流观看。
流媒体基础概念
这一节不涉及一行代码,但它讲清楚的东西会让你读后面的内容时毫无障碍。
看视频的两种方式:文件 vs 流
你看视频有两种方式。第一种:下载一个 .mp4 到电脑上双击打开。播放器把整个文件读一遍,一边读一边还原画面和声音。如果文件只下载了一半就打开——播到一半会卡住。
第二种:打开一个直播页面,画面立刻出来了。你不需要等"完整文件"——因为根本不存在完整文件。画面是此时此刻正在发生的,采集端不断产生新画面帧,立刻压缩、立刻发送、接收端立刻播放。数据像打开的水龙头一样持续流过来,而不是像一桶水先装满再喝。
第二种方式就叫"流"(stream)。这个项目处理的就是"流"。
数据怎么从一台电脑传到另一台:TCP 和 UDP
在讲具体视频传输方式之前,必须先讲一个更底层的东西。网络上两台电脑之间传数据,有两种最基本的"运送规则"。
- TCP(传输控制协议):每发一包数据都要等对方回复"收到了"。没收到就重发。保证数据不丢、不乱、不少。代价是需要往返确认,速度会稍慢。就像挂号信——必须签收。
- UDP(用户数据报协议):发了就不管了,不关心对方是否收到。速度快但可能丢数据。就像往信箱里塞传单——塞进去就不管了。
文件下载、网页浏览——用 TCP(数据一点不能错)。在线游戏、视频通话——用 UDP(丢几帧无所谓但延迟必须低)。视频推流通常用 TCP——画面完整性比零点几秒延迟更重要。
TCP 每包确认;UDP 发了不管。视频推流用 TCP——画面完整性优先。
推流:RTMP
现在你知道了 TCP。RTMP(实时消息传输协议)就是建立在 TCP 之上的一种推流方式。它由 Adobe 公司设计,客户端主动连到服务器,建立一条 TCP 长连接,然后在这条连接上持续推送音视频数据。默认端口号 1935(端口号就是操作系统用来区分不同网络程序的数字标识——一个端口同一时刻只能被一个程序用)。
拉流方式一:HTTP-FLV
先解释 HTTP:你浏览器输入网址访问网页用的协议。浏览器向服务器发一个 HTTP 请求("请把首页给我"),服务器返回 HTTP 响应("好的,这是页面内容")。HTTP 基于 TCP,数据可靠。
FLV(Flash Video)是一种视频的"包装格式"——规定了画面和声音数据怎么排列。同一段视频可以包装成不同格式。
HTTP-FLV 就是通过 HTTP 来传输 FLV 格式的视频流。播放端发一个 HTTP 请求,服务器不结束这个请求——而是持续往响应里追加 FLV 数据。播放端收到一段播一段。延迟 1-3 秒。缺点是 iPhone 自带的 Safari 浏览器对 FLV 支持不太好。
HTTP-FLV 靠持续追加数据获得低延迟(1-3秒);HLS 靠切片获得广泛兼容性,但延迟更高(5-10秒)。项目中两种都提供,播放端按需选择。
拉流方式二:HLS
HLS(HTTP 实时流传输)是苹果公司设计的,思路完全不同:服务器把视频流切成一个个 2 秒的 .ts 小文件(TS 是一种视频片段格式),同时维护一个 .m3u8 目录文件列出所有片段的地址和顺序。播放端先下载目录,再按顺序逐个下载并播放片段。
- 兼容性最好—— iPhone、安卓、所有浏览器都原生支持,不需装任何东西
- 延迟较高(5-10 秒)——因为必须等一个片段切完封好,播放端才能下载到
- 容错性好——某片下载慢了不影响已缓冲的后续片段
在这套系统中,播放端可以按需选择:低延迟用 HTTP-FLV,兼容性用 HLS。
HLS 把持续的视频流切成 2 秒的 .ts 片段,.m3u8 目录列出所有片段地址。播放端先下载目录,再逐个下载片段按序播放。每个片段必须先切完才能被下载——这就是延迟的来源。
从摄像头到屏幕的完整链路:采集 → 编码压缩 → RTMP 封装发送 → ZLMediaKit 转封装 → 播放端下载解码 → 用户看到画面。每一步都不可或缺。
本节概念速查
- 视频流
- 持续传输、不需等完整文件的音视频数据。直播必须用流。
- TCP
- 发一包确认一包的传输规则。数据不丢但稍慢。RTMP 和 HTTP 基于它。
- UDP
- 发了不管的传输规则。速度快但可能丢数据。
- RTMP
- 推流协议。客户端连服务器,持续推视频。默认端口 1935。
- HTTP
- 浏览器看网页用的协议。基于 TCP,可靠。
- HTTP-FLV
- 拉流方式。HTTP 持续传 FLV 格式视频。延迟 1-3 秒。iPhone 兼容性一般。
- HLS
- 拉流方式。视频切成 2 秒小片段,.m3u8 目录串起来。兼容性最好,延迟 5-10 秒。
- 端口号
- 操作系统用来区分不同网络程序的数字。一个端口同时只能被一个程序用。
ZLMediaKit 媒体引擎
它是什么
ZLMediaKit 是一个用 C++(一种以执行速度快著称的编程语言)编写的高性能流媒体服务器程序。源代码开源在 GitHub。本项目使用别人已经编译好的可执行文件(Windows 上叫 MediaServer.exe,Linux 上叫 MediaServer),由构建脚本自动下载部署。编译就是把人类可读的代码翻译成电脑能直接执行的机器指令。
ZLMediaKit 支持很多协议,这个项目主要用到三种:RTMP 收推流,HTTP-FLV 和 HLS 对外分发。
"协议转换"到底做了什么——编码 vs 传输的区分
这里有一个很重要的区分:
- 视频编码(如 H.264、H.265):决定画面怎么被压缩成 0 和 1。摄像机拍到的原片数据量极大(一秒未压缩高清视频有几百 MB),必须压缩才能传。编码器负责压缩,解码器负责还原。
- 传输协议(如 RTMP、HLS):决定压缩后的 0 和 1 怎么打包、走什么路线到目的地。不关心包里装的是什么编码,只管怎么运。
同一份 H.264 编码的视频数据,可以装在 RTMP 包里运输,也可以切成 HLS 的 .ts 片段运输,也可以装进 FLV 格式通过 HTTP 运输。内容没变,只是运输方式变了。
"协议转换"更准确的名字是"转封装"——把通过 A 协议收到的包裹拆开,取出里面的视频编码数据,然后用 B 协议的包装方式重新打包发出去。编码不改,只换包装和运输方式。ZLMediaKit 在收到 RTMP 推流后会自动实时生成 RTMP、HTTP-FLV、HLS 三套输出。
ZLMediaKit 收到一份 RTMP 推流(H.264 编码),拆包取出编码数据,重新打包成三种输出。编码不变,只换包装。
一个重要的配置:关掉"按需"
ZLMediaKit 里有一个选项叫"按需拉流"(on-demand)。开启时:没有播放端请求就不生成输出,有人请求才临时开始。省 CPU(中央处理器,电脑的计算核心)和内存。
本项目选择全部关掉——推流一到达,所有协议输出立刻同时生成。代价是始终消耗 CPU 做转封装。好处是第一个播放端打开时几乎秒播——数据已就绪,不用等转换启动。HLS 切片也做了优化:每片 2 秒(常规 6-10 秒),保留最近 5 片。
Gateway API 网关
它是什么
Gateway 是一个用 Go 语言(Google 设计的编程语言,特点是编译出来的程序是一个独立文件,不需要在目标电脑上装 Go 环境就能运行)编写的 HTTP 服务程序。HTTP 服务就是一段在某个端口上监听、收到 HTTP 请求后根据 URL 路径执行对应逻辑、然后返回 HTTP 响应(通常是 JSON 格式)的程序。JSON 是一种用纯文本表示结构化数据的方式——比如 {"name":"张三","age":30},花括号包着键值对,人和程序都能读懂。
Gateway 的特点:全部代码在一个文件里(约 860 行);不依赖任何第三方 Go 库,只用 Go 自带的标准库(语言出厂自带的工具集);编译后拷贝到任何同类型电脑就能跑。
什么叫"API"
API(应用程序编程接口)。这个词听起来很技术,但意思很简单:两个程序之间约定好的一套对话方式。Desktop 想创建一个流,它不能用人话对 Gateway 说——它必须按 Gateway 规定的格式:往某个 URL 发 POST 请求,请求体里放一段 JSON,写上 app、stream、serviceMode 这几个字段。Gateway 收到后按约定返回一段 JSON,里面包含推流地址和播放地址。
Gateway 一共提供 8 个这样的"约定"(API 端点)。GET 意思是"我要获取数据",POST 意思是"我要提交数据让你做一件事":
| 方法 | URL 路径 | 用来做什么 |
|---|---|---|
| GET | /healthz | 健康检查——问"你活着吗",答"活着"。返回 {"status":"ok"} |
| POST | /api/v1/streams/start | 创建流——传 app、stream、mode,返回推流和播放地址。同参数重复调不报错 |
| GET | /api/v1/streams/resolve | 按名称查地址——只传 app+stream。播放端不需要知道流 ID |
| GET | /api/v1/streams/{id}/status | 查某个流的状态和播放地址(URL 里带流 ID) |
| POST | /api/v1/streams/{id}/stop | 停止流——标记为 "stopped" |
| GET | /api/v1/streams | 列出当前所有流 |
| GET | /api/v1/debug/logs | 查看各组件的运行日志 |
| GET | /api/v1/debug/network | 查看当前网络状态:IP、端口号等 |
API 调用过程:调用方发 HTTP 请求(带 JSON 参数)→ Gateway 内部处理 → 返回 JSON 响应。全程通过网络完成,双方不需要在同一程序里。
三种模式 / 数据存哪 / 幂等
创建流时有三种 serviceMode:direct(仅生成 RTMP 地址)、broadcast(RTMP+HTTP-FLV+HLS 全生成)、httpflv(和 broadcast 一样,历史名称)。
流数据存在程序内存里——用一个叫 map 的结构(可以理解为一本字典:给定流 ID 立刻查到对应的流信息,不用从第一条开始翻)。用读写锁(RWMutex)保护——读操作可同时进行互不干扰,写操作必须独占。因为 Gateway 可能同时处理 Desktop 的创建请求和 Mobile 的查询请求,这就是并发——多个操作在同一时刻发生。
读写锁规则:多个读操作可以同时进行(互不干扰),但写操作必须独占(写的时候其他人不能读也不能写)。这保证了任何时刻数据都是完整正确的。
幂等:同一个操作执行一次和十次结果相同。Gateway 的创建流接口是幂等的——第二次用相同名调用不会报错也不会创建重复流,直接返回第一次的结果。调用方不用自己判断"我是不是创建过了"。
流会话管理
什么是"会话"
会话(session)在计算机里指"从开始做一件事到结束之间的整段过程和所有信息"。你登录网站 → 服务器创建会话记住你是谁 → 你浏览下单留言都属于这个会话 → 退出登录会话结束。Gateway 里一个"流会话"就是从创建到停止的整段过程——谁创建的、叫什么、什么模式、推流播放地址分别是什么、当前状态。
一个流会话的四个环节。Gateway 管理控制面(创建/查询/停止),ZLMediaKit 管理数据面(实际媒体处理)。两者通过端口号信息耦合,彼此独立运行。
Electron 管理界面
Electron 是什么
要理解 Electron,先认识它两个"原材料":
- Chromium:Google Chrome 浏览器的核心引擎——负责把 HTML 和 CSS 代码变成你看到的网页画面。开源,谁都能用。
- Node.js:让 JavaScript(网页用的编程语言)脱离浏览器直接在操作系统层面运行的程序。普通浏览器里的 JavaScript 不能读本地文件、不能启动其他程序(安全限制),Node.js 里的 JavaScript 可以——它能读写文件、启子进程、访问网络。
Electron = Chromium + Node.js 嵌在一起,打包成一个桌面可执行文件。界面用 HTML/CSS/JS 写(简单跨平台),系统操作(启动 ZLM、写文件)用 Node.js 做。两者通过 IPC(进程间通信——两个正在运行的程序之间互相传消息的机制)协作。
Electron 把浏览器引擎和系统运行时合并。界面用网页技术,系统操作用 Node.js,两者通过 IPC 对话。
UI 功能 / 两种模式
UI 提供流管理面板(填参数→创建→显示地址→复制)、日志查看器(每 3 秒刷新)、按需启动(开窗口不自动启动,点按钮才启动)、主题切换。
- 托管模式(发布版):Electron 自己启动和管理 ZLMediaKit + Gateway。整套启动逻辑用 JavaScript 在 main.js 里实现。
- 脚本模式(开发版):依赖外部 start-all.ps1(PowerShell)或 start-all.sh(Bash)脚本。
打包时通过 electron-builder(把 Electron 项目打包成可分发程序的工具)把 ZLMediaKit 二进制、Gateway 编译产物、启动脚本全部打进去。最终输出:Windows 上一个 .exe(portable 模式,不需安装),Linux 上一个 AppImage(自包含应用格式)。
Kernel Console 启动器
它是什么
Kernel Console 是一个 命令行程序——在终端(黑底白字或白底黑字的文字窗口)里运行,没有图形按钮,全通过文字输入输出交互。用 Go 编写约 650 行,编译为独立可执行文件。适用场景:远程 Linux 服务器(通过 SSH 远程连接过去只能看到终端)、NAS、树莓派等没有图形界面的环境。
Kernel Console 启动的完整流程:从探测目录到前台驻留,共 9 步。
服务启动与生命周期
三套启动方式——同一件事实现三遍
启动 ZLMediaKit + Gateway 这套流程在 PowerShell/Bash 脚本里实现了一遍,在 Electron 的 main.js 里又实现了一遍,在 Go Kernel Console 里又实现了一遍。三套代码做的事完全相同,但不共享任何代码。
为什么?为了避免跨语言依赖链。如果只有一套启动逻辑写在 PowerShell 脚本里,那 Electron 和 Kernel Console 都得依赖这个脚本——脚本改了任何一个参数名,两头都可能莫名其妙挂掉。各自独立实现虽然代码有重复,但每条路径完全自包含。一条出问题不影响另两条。这是工程上的权衡。
PowerShell/Bash 脚本
双击 .bat 或执行脚本一键启动。开发调试用。
开发Electron 主进程
main.js 里用 JavaScript 实现相同逻辑。用户点按钮启动。
发布版Go Kernel Console
纯命令行。无图形界面的服务器环境用。
服务器启动七步 / 端口不够用 / 优雅停止
启动:探测空闲端口 → 生成 ZLM 配置文件 → 启动 ZLM(子进程)→ 启动 Gateway(Electron 模式下会先 TCP 验证 ZLM RTMP 端口就绪,Kernel Console 模式下直接启动 Gateway)(子进程,注入环境变量)→ 每 0.2 秒轮询 /healthz(5 秒超时重试最多 8 次)→ 写入 runtime.json。
端口不够:一台电脑 65536 个端口,一个端口只能一个程序占。默认端口被占了就请操作系统随机给个空闲的。Gateway 更顽强——healthz 不成功就换端口重启,最多 8 次。
端口被占用时 Gateway 自动换端口重试。不是只试一次——最多试 8 个不同的端口,任一次 /healthz 返回成功即停止重试。
优雅停止:先发 SIGINT 信号(操作系统标准通知"请退出",等同于你在终端按 Ctrl+C)+ 给 1.5 秒窗口让程序清理收尾 + 超时还不退就 SIGKILL(操作系统强制终止)。顺序:先停 Gateway,再停 ZLM——因为 Gateway 依赖 ZLM 的端口信息,反过来没有。
网络与地址编排
IP 地址怎么选
Gateway 生成的播放地址里必须包含一个能被播放端访问到的 IP。但一台电脑可能有多个 IP——有线网卡一个、WiFi 一个、VPN 虚拟网卡一个、Docker 虚拟网卡一个、127.0.0.1 回环地址(只能本机访问)。选错了播放端就连不上。
Gateway 的优先级:① 用户显式设置的公网地址 → ② 启动时探测到的第一个可达 IP → ③(仅 Linux)请求到达时的动态判断:路由优选(UDP 探测找到客户端可达的网卡)+ 同 /24 子网优先(IP 前三段相同优先选)+ 轮询 TCP 可达性(300ms 超时)→ ④ 从 HTTP 请求头推断 → ⑤ 回退到 localIPv4()(遍历网卡找非回环地址,找不到才用 127.0.0.1)。
IP 选择的多级回退链。每一步失败才进入下一步。目标是找到播放端真正能访问到的那个 IP。
CORS——为什么需要它
Gateway 里有个 corsMiddleware。要理解它,先理解浏览器的同源策略:浏览器默认禁止 A 网站的 JavaScript 向 B 网站发 HTTP 请求。"源"= 协议 + 域名 + 端口号,三者有一个不同就算不同源。这个限制是为了安全——防止恶意网站冒充你的身份去访问你的银行。
但前后端分离(界面在一个域名,API 在另一个域名)是合法需求——两套服务都是同一家公司部署的,只是域名不同。CORS(跨域资源共享)就是解决这个的:服务器在 HTTP 响应头里加标记,告诉浏览器"这个跨域请求我允许"。
Gateway 设为允许任意来源(Access-Control-Allow-Origin: *)——因为当前版本没有任何身份认证,API 本来就是公开的,不存在"谁的请求应该被拒绝"。
三端协同
- Desktop 采集 → 调 Gateway 创建流 → 拿到 RTMP 推流地址 → 把视频推到 ZLMediaKit
- Service ZLMediaKit 自动转封装出 HTTP-FLV 和 HLS;Gateway 维护会话和地址
- Mobile(或另一个 Desktop)调 Gateway resolve 接口(只传 app+stream)→ 拿到播放地址 → 连 ZLMediaKit 拉流
Mobile 不需要知道流 ID、端口号、协议细节——它只需要 app + stream 两个名称。所有复杂逻辑在 Service 端完成。这就是"地址编排"的价值。
构建与分发
Go 编译 / Electron 打包 / 跨平台
Go 程序编译:Go 源代码 → Go 编译器 → 独立二进制可执行文件。包含 Go 运行时和所有依赖,拷贝到同架构机器就能跑。
编译就是把人类可读的代码变成电脑能直接执行的机器指令。Go 的编译产物自带运行时,不需要在目标机器上装 Go。
命令:go build -o gateway/bin/gateway.exe gateway/。
Electron 打包:electron-builder 把 HTML/CSS/JS + Chromium + Node.js + extraResources(ZLMediaKit 二进制、Gateway 编译产物、启动脚本)全部塞进一个 .exe(Windows portable)或 AppImage(Linux)。
最终两个分发包:portable-ui(图形界面版)和 portable-kernel(命令行版)——底层 ZLMediaKit 和 Gateway 完全一样,只差上层入口。构建脚本做完整性校验防漏文件。
跨平台:Kernel Console 通过 runtime.GOOS(Go 提供的常量,告诉你当前在哪个操作系统)判断平台,选正确的文件名后缀、ZLM 目录、端口策略。Linux 下额外开启 RTSP 端口(默认用 8554 避免需要 root 权限绑定标准端口 554)。
设计决策
不做持久化数据库
Gateway 的流数据全在进程内存里,重启就没了。这不是能力不够,是有意为之:流本就是临时的(直播结束就没用了)、消除数据库依赖(不需要装 MySQL/Redis,Gateway 一个文件拷贝即部署)、如需历史记录可在 Gateway 外层再包一个服务(调 API 拿到数据后存自己的数据库)。
不做身份认证
当前任何人知道 Gateway 地址都能调 API。原因:使用场景是同一局域网内可信环境(网络边界就是防线)、公网部署可在网络层用防火墙/VPN 解决。未来可加推流令牌+观看令牌,但当前不是必需的。
不做转码
ZLMediaKit 只换包装不换压缩方式。转码(解码再重新编码)需要的计算量是转封装的几十上百倍。不做转码意味着推流端用什么编码播放端就得支持什么编码——但服务端 CPU 负载极低。
Gateway 为什么是单文件 + 零依赖
860 行 Go 全在一个 main.go 里。功能范围明确(创建/查询/停止 + 健康检查),拆文件反而增加跳转成本。不用第三方框架(Gin、Echo 等流行 Go HTTP 框架)意味着没有框架升级引发的兼容问题——Go 标准库的 net/http 十年后还能编译。