FPlayer FF Service

FPlayer FF Service

一份流媒体服务端的技术全貌教程
每个你不认识的名词,这里都会从头讲清楚

流媒体服务Go + ElectronZLMediaKit跨平台三端协同
01

项目概览

这个项目做了什么

FPlayer FF Service 是一个可以独立运行在电脑上的流媒体服务程序

先解释几个词:

  • 程序:你双击图标或在终端里敲一行命令后运行起来的东西。它能持续运行,接收输入、做出处理、给出输出。微信、浏览器、音乐播放器都是程序。
  • 服务程序:有一种程序不需要你去操作它。它启动后在后台安静等着,等别的程序通过网络找它要数据。网页后端、API 接口都属于这一类。FPlayer FF Service 就是这样——它启动后等着 Desktop 和 Mobile 来找它。
  • 流媒体:就是"象流水一样持续传输的音视频数据",而不是先录成文件再传。后面第 02 节会展开讲。

这个程序做的事情很简单:

  1. 接收视频数据—— 采集端(桌面客户端 Desktop)通过网络把正在录制的画面和声音持续发给它
  2. 转换播放格式—— 同一份视频数据自动变成多种不同的"包装方式",让不同类型的播放端都能用
  3. 管理播放地址—— 告诉推流端往哪推、告诉播放端从哪拉。地址怎么拼、端口是多少全部由它处理

不采集画面(那是 desktop 的事),也不显示画面(那是 mobile 或播放器的事)。它只做中间环节:收 → 转 → 发。

📹 采集端 桌面客户端 · 摄像头 RTMP 推流 ⚙️ FPlayer FF Service ① 接收推流 ② 自动转封装为多种格式 ③ 生成播放地址 HTTP-FLV / HLS 拉流 📱 手机 🖥 电脑 🌐 网页 Service 内部的四个组件: 📡 ZLMediaKit 媒体引擎 · C++ 🚦 Gateway API 网关 · Go 🖥️ Electron UI 管理界面 · Electron ⌨️ Kernel Console 命令行启动器 · Go

FPlayer FF Service 的整体定位:接收推流 → 协议转换 → 分发播放。内部有四个组件各司其职。

它和谁配合工作

  • fplayer-ff-desktop:桌面客户端程序,采集屏幕或摄像头画面推送给 Service
  • fplayer-ff-service:本项目——接收、转换、分发
  • fplayer-ff-mobile:手机 App,从 Service 获取地址后拉流播放

典型场景:同一局域网下,一台电脑采集推流 → Service 中转 → 其他设备拉流观看。

02

流媒体基础概念

这一节不涉及一行代码,但它讲清楚的东西会让你读后面的内容时毫无障碍。

看视频的两种方式:文件 vs 流

你看视频有两种方式。第一种:下载一个 .mp4 到电脑上双击打开。播放器把整个文件读一遍,一边读一边还原画面和声音。如果文件只下载了一半就打开——播到一半会卡住。

第二种:打开一个直播页面,画面立刻出来了。你不需要等"完整文件"——因为根本不存在完整文件。画面是此时此刻正在发生的,采集端不断产生新画面帧,立刻压缩、立刻发送、接收端立刻播放。数据像打开的水龙头一样持续流过来,而不是像一桶水先装满再喝。

第二种方式就叫"流"(stream)。这个项目处理的就是"流"。

数据怎么从一台电脑传到另一台:TCP 和 UDP

在讲具体视频传输方式之前,必须先讲一个更底层的东西。网络上两台电脑之间传数据,有两种最基本的"运送规则"。

  • TCP(传输控制协议):每发一包数据都要等对方回复"收到了"。没收到就重发。保证数据不丢、不乱、不少。代价是需要往返确认,速度会稍慢。就像挂号信——必须签收。
  • UDP(用户数据报协议):发了就不管了,不关心对方是否收到。速度快但可能丢数据。就像往信箱里塞传单——塞进去就不管了。

文件下载、网页浏览——用 TCP(数据一点不能错)。在线游戏、视频通话——用 UDP(丢几帧无所谓但延迟必须低)。视频推流通常用 TCP——画面完整性比零点几秒延迟更重要。

TCP — 可靠传输 发送方 → 数据包 接收方 "收到了!" ← 确认 ← "继续发" UDP — 尽力传输 发送方 → 数据包×3 → → → 可能丢包 ✗ 不确认 · 不重传 · 只求快

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 服务器切成 .ts 片段 → [ts1] [ts2] [ts3] [ts4] → 播放端逐个下载片段播放 延迟: 5~10秒 · 兼容性: 最好

HTTP-FLV 靠持续追加数据获得低延迟(1-3秒);HLS 靠切片获得广泛兼容性,但延迟更高(5-10秒)。项目中两种都提供,播放端按需选择。

拉流方式二:HLS

HLS(HTTP 实时流传输)是苹果公司设计的,思路完全不同:服务器把视频流切成一个个 2 秒的 .ts 小文件(TS 是一种视频片段格式),同时维护一个 .m3u8 目录文件列出所有片段的地址和顺序。播放端先下载目录,再按顺序逐个下载并播放片段。

  • 兼容性最好—— iPhone、安卓、所有浏览器都原生支持,不需装任何东西
  • 延迟较高(5-10 秒)——因为必须等一个片段切完封好,播放端才能下载到
  • 容错性好——某片下载慢了不影响已缓冲的后续片段

在这套系统中,播放端可以按需选择:低延迟用 HTTP-FLV,兼容性用 HLS。

HLS 切片原理 原始视频流(持续不断的数据) ← 2秒 → segment-001.ts segment-002.ts segment-003.ts segment-004.ts (正在生成...) 📄 playlist.m3u8(目录文件):"当前有 001.ts, 002.ts, 003.ts,按这个顺序播"

HLS 把持续的视频流切成 2 秒的 .ts 片段,.m3u8 目录列出所有片段地址。播放端先下载目录,再逐个下载片段按序播放。每个片段必须先切完才能被下载——这就是延迟的来源。

从采集到播放:一条视频流的完整旅程 📹 摄像头 采集画面 产生原始数据 (海量,未压缩) 🔧 编码器 压缩视频 H.264 / H.265 (数据量缩到1%) 📦 RTMP 封装 打包数据 通过 TCP 发送 (可靠运输) 📡 ZLMediaKit 接收 · 转封装 生成 HTTP-FLV 生成 HLS 片段 编码不变 📱 播放端 下载 .ts / FLV 解码还原 (用户看到画面) 🖥 屏幕 显示画面

从摄像头到屏幕的完整链路:采集 → 编码压缩 → RTMP 封装发送 → ZLMediaKit 转封装 → 播放端下载解码 → 用户看到画面。每一步都不可或缺。

本节概念速查

视频流
持续传输、不需等完整文件的音视频数据。直播必须用流。
TCP
发一包确认一包的传输规则。数据不丢但稍慢。RTMP 和 HTTP 基于它。
UDP
发了不管的传输规则。速度快但可能丢数据。
RTMP
推流协议。客户端连服务器,持续推视频。默认端口 1935。
HTTP
浏览器看网页用的协议。基于 TCP,可靠。
HTTP-FLV
拉流方式。HTTP 持续传 FLV 格式视频。延迟 1-3 秒。iPhone 兼容性一般。
HLS
拉流方式。视频切成 2 秒小片段,.m3u8 目录串起来。兼容性最好,延迟 5-10 秒。
端口号
操作系统用来区分不同网络程序的数字。一个端口同时只能被一个程序用。
03

ZLMediaKit 媒体引擎

它是什么

ZLMediaKit 是一个用 C++(一种以执行速度快著称的编程语言)编写的高性能流媒体服务器程序。源代码开源在 GitHub。本项目使用别人已经编译好的可执行文件(Windows 上叫 MediaServer.exe,Linux 上叫 MediaServer),由构建脚本自动下载部署。编译就是把人类可读的代码翻译成电脑能直接执行的机器指令。

ZLMediaKit 支持很多协议,这个项目主要用到三种:RTMP 收推流,HTTP-FLVHLS 对外分发。

"协议转换"到底做了什么——编码 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 三套输出。

RTMP 推流 (H.264 编码数据) 用 RTMP 包装运输 📡 ZLMediaKit 拆包 → 取出 H.264 编码数据 → 重新打包 (编码本身不变,只换运输包装) 这个过程叫"转封装" 不叫"转码"——转码要重新压缩,计算量大得多 → RTMP 输出 → HTTP-FLV 输出 → HLS 输出 (m3u8 + ts)

ZLMediaKit 收到一份 RTMP 推流(H.264 编码),拆包取出编码数据,重新打包成三种输出。编码不变,只换包装。

一个重要的配置:关掉"按需"

ZLMediaKit 里有一个选项叫"按需拉流"(on-demand)。开启时:没有播放端请求就不生成输出,有人请求才临时开始。省 CPU(中央处理器,电脑的计算核心)和内存。

本项目选择全部关掉——推流一到达,所有协议输出立刻同时生成。代价是始终消耗 CPU 做转封装。好处是第一个播放端打开时几乎秒播——数据已就绪,不用等转换启动。HLS 切片也做了优化:每片 2 秒(常规 6-10 秒),保留最近 5 片。

# ZLMediaKit 配置文件 config.ini 中的关键设置 [protocol] enable_hls=1 # 开启 HLS 协议支持 enable_rtmp=1 # 开启 RTMP 协议支持 hls_demand=0 # 0 = 关闭按需 → 推流即转 [hls] segDur=2 # 每个 .ts 片段 2 秒(越短首屏越快) segNum=3 # .m3u8 里保留 3 个片段的索引 segRetain=5 # 磁盘保留 5 个片段
04

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 调用过程(以"创建流"为例) Desktop(调用方) 发 HTTP POST 请求 URL: /api/v1/streams/start Body: {"app":"live",...} Request Gateway 处理 幂等检查 → 生成 ID → 拼接地址 → 存入 map → 返回 JSON 响应 全程只用 Go 标准库 Response JSON 响应 publishRtmp: ... playHttpFlv: ... playHls: ...

API 调用过程:调用方发 HTTP 请求(带 JSON 参数)→ Gateway 内部处理 → 返回 JSON 响应。全程通过网络完成,双方不需要在同一程序里。

三种模式 / 数据存哪 / 幂等

创建流时有三种 serviceModedirect(仅生成 RTMP 地址)、broadcast(RTMP+HTTP-FLV+HLS 全生成)、httpflv(和 broadcast 一样,历史名称)。

流数据存在程序内存里——用一个叫 map 的结构(可以理解为一本字典:给定流 ID 立刻查到对应的流信息,不用从第一条开始翻)。用读写锁RWMutex)保护——读操作可同时进行互不干扰,写操作必须独占。因为 Gateway 可能同时处理 Desktop 的创建请求和 Mobile 的查询请求,这就是并发——多个操作在同一时刻发生。

读写锁 (RWMutex) 如何保护并发安全 Desktop → 创建流 需要「写锁」(独占) 写入时其他人不能读也不能写 Mobile A → 查询流 只需要「读锁」(共享) 读锁可多个同时持有 Mobile B → 查询流 只需要「读锁」 可以和 A 同时读

读写锁规则:多个读操作可以同时进行(互不干扰),但写操作必须独占(写的时候其他人不能读也不能写)。这保证了任何时刻数据都是完整正确的。

幂等:同一个操作执行一次和十次结果相同。Gateway 的创建流接口是幂等的——第二次用相同名调用不会报错也不会创建重复流,直接返回第一次的结果。调用方不用自己判断"我是不是创建过了"。

05

流会话管理

什么是"会话"

会话(session)在计算机里指"从开始做一件事到结束之间的整段过程和所有信息"。你登录网站 → 服务器创建会话记住你是谁 → 你浏览下单留言都属于这个会话 → 退出登录会话结束。Gateway 里一个"流会话"就是从创建到停止的整段过程——谁创建的、叫什么、什么模式、推流播放地址分别是什么、当前状态。

1 创建会话 Desktop → POST /api/v1/streams/start → Gateway 生成 ID + 地址 → 返回 JSON 2 推流 Desktop → RTMP 推流到 ZLMediaKit → ZLM 自动转封装 (HTTP-FLV + HLS) 3 查询 + 拉流 Mobile → GET /resolve?app=...&stream=... → Gateway 返回地址 → Mobile 拉流播放 4 停止 POST /streams/{id}/stop → 状态标记 "stopped" → ZLM 独立管理流生命周期 Gateway 管"控制面":创建、查询、停止 ZLMediaKit 管"数据面":收流、转换、分发 通过环境变量传递端口信息 控制面 (Gateway) 和数据面 (ZLM) 分离:两者独立运行,通过端口号耦合

一个流会话的四个环节。Gateway 管理控制面(创建/查询/停止),ZLMediaKit 管理数据面(实际媒体处理)。两者通过端口号信息耦合,彼此独立运行。

06

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(进程间通信——两个正在运行的程序之间互相传消息的机制)协作。

Chromium 渲染 HTML/CSS 执行 JavaScript 显示窗口界面 Node.js 读写本地文件 启动子进程 TCP 网络操作 ↕ IPC 进程间消息传递 Electron = Chromium + 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(自包含应用格式)。

07

Kernel Console 启动器

它是什么

Kernel Console 是一个 命令行程序——在终端(黑底白字或白底黑字的文字窗口)里运行,没有图形按钮,全通过文字输入输出交互。用 Go 编写约 650 行,编译为独立可执行文件。适用场景:远程 Linux 服务器(通过 SSH 远程连接过去只能看到终端)、NAS、树莓派等没有图形界面的环境。

① 自动探测目录 ZLM 可执行文件 ② 动态分配端口 1935→8080→9000... ③ 生成配置文件 写端口号到 ini ④ 启动 ZLMediaKit 子进程 · 重定向日志 ⑤ 验证 ZLM 就绪 TCP 连 RTMP 端口 ⑥ 启动 Gateway 注入环境变量(端口) ⑦ 健康检查轮询 /healthz · 5s超时·重试8次 ⑧ 写入 runtime.json + 自动创建默认流 ⑨ 前台驻留 · 监听 Ctrl+C · 收到后优雅停止

Kernel Console 启动的完整流程:从探测目录到前台驻留,共 9 步。

08

服务启动与生命周期

三套启动方式——同一件事实现三遍

启动 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 端口分配与重试机制 尝试端口 9000 启动 Gateway → /healthz 超时 ✗ 换端口 62341 重新启动 Gateway → /healthz 超时 ✗ 再换端口 17029 重新启动 Gateway → /healthz OK ✓ 运行在端口 17029 写入 runtime.json 最多重试 8 次 · 每次等待 /healthz 最多 5 秒 · 全部失败则启动报错

端口被占用时 Gateway 自动换端口重试。不是只试一次——最多试 8 个不同的端口,任一次 /healthz 返回成功即停止重试。

优雅停止:先发 SIGINT 信号(操作系统标准通知"请退出",等同于你在终端按 Ctrl+C)+ 给 1.5 秒窗口让程序清理收尾 + 超时还不退就 SIGKILL(操作系统强制终止)。顺序:先停 Gateway,再停 ZLM——因为 Gateway 依赖 ZLM 的端口信息,反过来没有。

09

网络与地址编排

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)。

公网配置覆盖 SERVICE_PUBLIC_HOST 启动阶段探测 绑定可达性测试 请求时动态选择 路由优选·同子网·轮询 请求头回退 Host / X-Forwarded 127.0.0.1

IP 选择的多级回退链。每一步失败才进入下一步。目标是找到播放端真正能访问到的那个 IP。

CORS——为什么需要它

Gateway 里有个 corsMiddleware。要理解它,先理解浏览器的同源策略:浏览器默认禁止 A 网站的 JavaScript 向 B 网站发 HTTP 请求。"源"= 协议 + 域名 + 端口号,三者有一个不同就算不同源。这个限制是为了安全——防止恶意网站冒充你的身份去访问你的银行。

但前后端分离(界面在一个域名,API 在另一个域名)是合法需求——两套服务都是同一家公司部署的,只是域名不同。CORS(跨域资源共享)就是解决这个的:服务器在 HTTP 响应头里加标记,告诉浏览器"这个跨域请求我允许"。

Gateway 设为允许任意来源Access-Control-Allow-Origin: *)——因为当前版本没有任何身份认证,API 本来就是公开的,不存在"谁的请求应该被拒绝"。

10

三端协同

fplayer-ff-desktop 采集画面 · 推流 · 拉流 ① 创建流 + RTMP 推流 fplayer-ff-service 收流转发 · 地址编排 ② 查地址 + 拉流 fplayer-ff-mobile 查地址 · 拉流播放 同一局域网:Desktop 推 → Service 转 → Mobile 播
  1. Desktop 采集 → 调 Gateway 创建流 → 拿到 RTMP 推流地址 → 把视频推到 ZLMediaKit
  2. Service ZLMediaKit 自动转封装出 HTTP-FLV 和 HLS;Gateway 维护会话和地址
  3. Mobile(或另一个 Desktop)调 Gateway resolve 接口(只传 app+stream)→ 拿到播放地址 → 连 ZLMediaKit 拉流

Mobile 不需要知道流 ID、端口号、协议细节——它只需要 app + stream 两个名称。所有复杂逻辑在 Service 端完成。这就是"地址编排"的价值。

11

构建与分发

Go 编译 / Electron 打包 / 跨平台

Go 程序编译:Go 源代码 → Go 编译器 → 独立二进制可执行文件。包含 Go 运行时和所有依赖,拷贝到同架构机器就能跑。

从源代码到可执行文件(以 Gateway 为例) 📄 main.go 860 行 Go 源代码 🔧 Go 编译器 翻译 + 打包运行时 gateway.exe 独立可执行文件 运行! (无需装 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)。

12

设计决策

不做持久化数据库

Gateway 的流数据全在进程内存里,重启就没了。这不是能力不够,是有意为之:流本就是临时的(直播结束就没用了)、消除数据库依赖(不需要装 MySQL/Redis,Gateway 一个文件拷贝即部署)、如需历史记录可在 Gateway 外层再包一个服务(调 API 拿到数据后存自己的数据库)。

不做身份认证

当前任何人知道 Gateway 地址都能调 API。原因:使用场景是同一局域网内可信环境(网络边界就是防线)、公网部署可在网络层用防火墙/VPN 解决。未来可加推流令牌+观看令牌,但当前不是必需的。

不做转码

ZLMediaKit 只换包装不换压缩方式。转码(解码再重新编码)需要的计算量是转封装的几十上百倍。不做转码意味着推流端用什么编码播放端就得支持什么编码——但服务端 CPU 负载极低。

Gateway 为什么是单文件 + 零依赖

860 行 Go 全在一个 main.go 里。功能范围明确(创建/查询/停止 + 健康检查),拆文件反而增加跳转成本。不用第三方框架(Gin、Echo 等流行 Go HTTP 框架)意味着没有框架升级引发的兼容问题——Go 标准库的 net/http 十年后还能编译。

13

延伸阅读

📡 📚 🔵 📦 ⚛️ 🌐 🎬 🍎