ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从零搭建慢直播频道:FFmpeg+SRS+HLS全链路实践

从零搭建慢直播频道:FFmpeg+SRS+HLS全链路实践 LunaTV 这个名字最初只是我相册里一个随手建的文件夹用来存放每晚拍的月亮延时素材。后来文件夹越来越大素材越来越多我冒出一个念头与其让这些素材躺在硬盘里吃灰不如把它们做成一个 7x24 小时自动播出的慢直播频道让任何人打开网页就能看到一片属于自己的夜空。这个念头最终落地成了一个完整的个人流媒体项目从采集、推流、分发到播放跑通全链路用了我大概一个周末。这篇内容就是把 LunaTV 从零搭建的完整过程拆开来讲包括流媒体服务器怎么选、FFmpeg 循环推流命令怎么写、HLS 分片怎么调、以及连续运行一周会遇到哪些坑。无论你是想做一个类似的慢直播频道还是单纯想搞懂直播推流背后的工程细节这篇文章的思路都可以直接复用。1. LunaTV 的项目定位一个慢直播频道背后的真实需求很多人第一次听到慢直播会觉得这是个伪需求无非是把摄像机架在那里拍风景。但 LunaTV 的定位不太一样它要解决的核心问题有三个。第一个是素材价值的再利用。我手里有大量按月相拍摄的延时视频单个素材几分钟到几十分钟不等如果只是存在网盘里基本不会再被打开。慢直播等于给这些素材提供了一个永不下线的展映厅。第二个是技术验证。直播链路里涉及推流、转码、切片、分发、播放每一个环节都有不少参数需要调一个长期运行的个人项目是最好的试验场。第三个更纯粹——我确实想要一个随时打开都能看到月亮的页面这在深夜加班时挺解压的。明白了需求再来看 LunaTV 的整体架构就清晰了。它本质上是一条单向数据管道从素材文件开始最终到用户浏览器播放器结束中间经过三大块模块职责核心组件采集推流端读素材、解码、编码、推送FFmpeg流媒体服务端接收 RTMP、转封装、切片SRS 5.0播放分发端HTTP 服务、HLS 拉流、播放Nginx hls.js这三个模块之间用标准协议通信采集端把视频通过 RTMP 协议推到服务器服务器收到后转封装成 HLS 分片客户端再通过 HTTP 拉取这些分片并连续播放。这里要顺带解释一个新手容易混淆的概念直播和点播的最大区别在于直播没有完整的文件数据是流过来的。所以服务器必须一边收数据一边把它切成一段段几秒钟的小文件播放器按顺序拉取这些小文件看起来就是连续的直播画面。这套机制是 Apple 提出的 HLS 协议的核心思想也是目前网页端直播兼容性最好的方案。LunaTV 的链路选择也是围绕这个机制展开的FFmpeg 负责把素材变成持续输出的 RTMP 流SRS 负责把 RTMP 流转成 HLS 切片Nginx 负责把切片文件提供给网页播放器。链路不复杂但每一环都有不少值得细究的地方。2. 流媒体服务器选型为什么最终选了 SRS 而不是 Nginx-RTMP在做 LunaTV 之前我简单调研过主流的开源流媒体服务器最后进入决赛圈的是三款Nginx-RTMP、SRS、MediaMTX。它们的核心能力都能满足接收 RTMP 推流并输出 HLS这个需求但实际跑下来差异相当大。2.1 三款流媒体服务器的横向对比先看 Nginx-RTMP。它的优势是 Nginx 生态成熟模块安装简单很多教程都用它。但它的问题也很明显项目已经很久没有大的功能迭代了HLS 切片功能比较基础不支持按需转码对异常断流的容错也一般。如果只是临时用一下没问题长期 7x24 跑会有不少小毛病。MediaMTX 是另一条路线它主打轻量单二进制文件支持 RTSP、RTMP、HLS、WebRTC 等多种协议互转配置非常简洁。但问题在于它的定位更偏向协议网关而不是直播服务端在高并发拉流、切片缓存等场景下功能不够细。SRSSimple Realtime Server是三者中功能最完整的它专门为直播场景设计天然支持 RTMP 推流、HLS 切片、HTTP-FLV、WebRTC 拉流还内置了集群、转码、回调等企业级能力。对 LunaTV 这个项目来说SRS 的 HLS 切片质量是三者里最可控的分片时长、窗口大小、切片文件名规则都能调整这正是慢直播最需要的东西。2.2 部署 SRS 时用到的关键配置SRS 的部署很简单官方提供了 Docker 镜像一条命令就能拉起来。但要注意默认配置只开了 HTTP-FLV 和 WebRTCHLS 需要手动开启。我的 srs.conf 核心配置是这样写的listen 1935; max_connections 1000; daemon off; srs_log_tank console; vhost __defaultVhost__ { hls { enabled on; hls_path /data/lunatv/hls; hls_fragment 4; hls_window 60; hls_cleanup on; hls_dispose 5; } }重点解释几个参数。hls_fragment是单个分片的时长我设置的是 4 秒这个值直接影响直播延迟和播放流畅度之间的平衡。hls_window是 HLS 播放窗口的总时长60 秒意味着播放器最多可以回看最近 60 秒的内容超出部分的分片会被自动清理。hls_cleanup开启后SRS 会定期删除过期分片避免磁盘被撑爆。这里有一个很关键的细节SRS 容器里的时区默认是 UTC而 HLS 分片文件名默认带时间戳如果不调整时区早上排查日志的时候会发现分片文件名和本地时间对不上容易误导人。我在 docker run 的时候用-e TZAsia/Shanghai解决了这个问题。还有一个容易踩的坑是hls_path的权限。SRS 容器通常以 root 运行但如果用非 root 用户启动容器hls_path 指向的目录必须提前创建好并赋予写权限否则 SRS 会报 create dir failed 然后直接退出。这个错误在日志里藏得比较深不仔细看还以为是端口冲突。3. FFmpeg 循环推流命令慢直播最核心的部分也是最容易翻车的地方流媒体服务器就绪后剩下的问题只有一个怎么让 FFmpeg 不知道疲惫地循环播放素材并且保证推流不中断、不花屏、不音画不同步。这一章是 LunaTV 项目里我花时间最多的部分命令本身不长但背后的参数陷阱不少。3.1 素材循环的两种主流写法LunaTV 的素材是几十个视频文件我需要让它们排成队列依次播放播完一轮再从头开始。FFmpeg 实现循环的主流写法有两种。第一种是用-stream_loop -1循环单个文件ffmpeg -stream_loop -1 -re -i moon_001.mp4 \ -c:v libx264 -preset veryfast -tune zerolatency \ -c:a aac -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/lunatv这种写法适合只有一个视频文件无限循环的场景优点是简单缺点是单个文件在循环边界处会有明显的画面断层而且不支持按素材列表顺序播放。第二种是用 concat demuxer 做素材列表循环# playlist.txt file moon_001.mp4 file moon_002.mp4 file moon_003.mp4配合命令ffmpeg -stream_loop -1 -re -f concat -safe 0 -i playlist.txt \ -c:v libx264 -preset veryfast -tune zerolatency \ -c:a aac -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/lunatv这是 LunaTV 最终采用的方案。concat demuxer 的好处是按列表顺序播放不同素材而且列表可以随时修改新增素材只需要向 playlist.txt 里加一行FFmpeg 会在下次循环时自动读到。缺点是它对素材的编码参数要求比较高如果列表里混入了分辨率、帧率、音频采样率不一致的文件拼接时可能出现音画不同步甚至推流中断。从实际使用来看素材在进列表之前最好统一做一次预处理把所有视频转成同样的编码参数。这个预处理可以单独跑一条 FFmpeg 命令也可以用脚本批量完成关键是保证分辨率、帧率、音频格式一致。我的做法是用 1920x1080、25fps、AAC 128kbps 作为统一标准。3.2 -re 参数的两个方向性作用-re这个参数值得单独拿出来讲。它的作用是让 FFmpeg 以实时速度读取输入文件而不是以尽可能快的速度读完。对于直播推流来说-re是必须的否则 FFmpeg 会在几秒内把整个素材全部读完然后退出推流就断了。但这里有个很多教程没讲透的点-re对 concat demuxer 的处理逻辑和普通文件不太一样。当输入是 concat 列表时-re作用于整个 concat 输入FFmpeg 会尽量让输出时间戳保持实时节奏。问题在于如果某个素材内部存在异常的时间戳跳变-re可能会让 FFmpeg 产生等待或加速的行为表现出来就是画面突然快进或者卡住。所以预处理素材时我会顺手用 ffprobe 检查一遍所有文件的时间戳是否连续这个习惯帮我避免了不少运行时故障。3.3 音频不足时的静音填充技巧LunaTV 最初的素材里有一部分是纯视频文件没有音轨。如果直接用 concat 拼接FFmpeg 在拼接有音轨和无音轨的文件时会出现时间戳错乱轻则播放器频繁缓冲重则推流直接失败。解决方式是在滤镜链里把音频归一化。我采用的是给所有输入都挂上-f lavfi -i anullsrc生成一路静音音轨然后让视频与静音音轨做混合确保输出始终有音频ffmpeg -stream_loop -1 -re -f concat -safe 0 -i playlist.txt \ -f lavfi -t 3600 -i anullsrcchannel_layoutstereo:sample_rate44100 \ -filter_complex [0:v]scale1920:1080,fps25,formatyuv420p[v];[0:a][1:a]amixinputs2:durationfirst[a] \ -map [v] -map [a] \ -c:v libx264 -preset veryfast -tune zerolatency \ -c:a aac -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/lunatv这条命令里有几个细节值得展开说。anullsrc后面的-t 3600表示生成 1 小时的静音源因为 anullsrc 本身是无限长的如果不限制时长FFmpeg 可能因为静音流太长而报错。实际推流是 7x24 小时不间断的但滤镜链里的静音源只要覆盖到每个素材的长度就够了所以 3600 秒的静音源在实际循环中足够用。amixinputs2:durationfirst表示把素材原音轨和静音轨混合时长以第一个输入为准。这样做的好处是即使某个素材没有音轨静音轨也能补齐时间轴保证音视频封装不散。formatyuv420p是因为 HLS 播放兼容性问题。很多专业视频素材是 yuv444 或 10bit 色深的如果不对颜色空间做转换部分浏览器播放器会直接黑屏。转成 yuv420p 是最稳妥的网页播放格式。还有一个细节-tune zerolatency。这个参数是 x264 编码器提供的专门针对直播场景优化目的是降低编码延迟。代价是压缩率会略降码率可能比默认配置高一点点。对 LunaTV 这种慢直播来说画质优先于码率zerolatency 是取舍后的结果。如果你完全不在乎延迟但追求最低带宽可以把-tune去掉换用-crf 23控制画质。3.4 推流卡死的自愈方案FFmpeg 长时间运行后偶尔会出现一种假死状态进程还在日志也不报错但推流的数据已经停了。这种问题在网络抖动、素材文件损坏、服务器磁盘满等场景下都可能出现而且特别隐蔽。LunaTV 的做法是给 FFmpeg 加一个看门狗脚本核心思路是检测推流数据的实时性。脚本每分钟检查一次 SRS 的 HLS 分片目录如果最新分片文件的修改时间超过 30 秒没有更新就判定推流已死强制杀掉 FFmpeg 进程并重新拉起。这个方案不需要引入额外的监控系统一个 cron 加几行 shell 就能实现。另外FFmpeg 自身的-timeout参数也能在推流阶段提供保护。推流目标是本地 SRS网络超时一般不会发生但加上异常安全网总没坏处。为了不浪费 CPU我用-loglevel warning把 FFmpeg 的日志级别控制在 warning 及以上只有推流异常时才会输出。日志在排查问题时非常重要不能省。系统里最好把 stdout 和 stderr 都重定向到文件并且按天切分否则日志文件会越滚越大。4. HLS 分片策略与网页播放让直播变成打开网页就能看推流端稳定后接下来解决的是观众端体验。LunaTV 的播放器是纯网页实现的不依赖任何客户端软件用户拿手机或电脑浏览器打开一个网址就能看。这里最关键的技术就是 HLS 分片参数和播放器的集成方式。4.1 分片时长与窗口大小怎么定HLS 的分片参数会影响两个体验指标延迟和首屏加载速度。分片越短延迟越低但服务器要处理的切片请求越多播放器缓冲波动也越大。分片越长延迟越高但播放更稳定。我最终选了 4 秒分片、60 秒窗口。原因有三条慢直播对实时性要求不高4 秒分片带来的延迟大约在 8 到 12 秒左右完全可以接受。4 秒分片配合 SRS 默认的切片模式能够在大多数网络环境下流畅播放不会频繁起播。60 秒的窗口足够覆盖播放器缓存和网络抖动同时不会让磁盘上的分片文件堆积过多。SRS 在hls_fragment的基础上还有一个实际切片时长的概念它不是严格按 4 秒一刀切而是会等待关键帧对齐。也就是说如果视频流的 GOP关键帧间隔不是 4 秒的整数倍实际分片会长于或短于 4 秒。所以 FFmpeg 侧的编码参数必须与分片策略联动我在推流命令里用-force_key_frames expr:gte(t,n_forced*4)强制每 4 秒生成一个关键帧这样 SRS 切片就能和关键帧对齐播放器起播时不需要等太久。4.2 hls.js 播放器的接入细节LunaTV 的前端播放器用的是 hls.js。它在支持 Media Source Extensions 的浏览器里能直接播放 HLS 流不需要浏览器原生支持。接入代码精简后大概是这样的video idvideo controls autoplay muted/video script srchttps://cdn.jsdelivr.net/npm/hls.js1/script script const video document.getElementById(video); if (Hls.isSupported()) { const hls new Hls({ lowLatencyMode: false, maxBufferLength: 30, maxMaxBufferLength: 60 }); hls.loadSource(/live/lunatv.m3u8); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, () video.play()); } else if (video.canPlayType(application/vnd.apple.mpegurl)) { video.src /live/lunatv.m3u8; video.addEventListener(loadedmetadata, () video.play()); } /scriptlowLatencyMode: false是一个值得注意的选项。LL-HLS低延迟 HLS虽然能把延迟压到 3 秒左右但它对分片请求的精度要求非常高在很多公共网络上反而容易出现频繁重新缓冲。对于慢直播流畅比低延迟重要得多所以我关闭了低延迟模式。maxBufferLength和maxMaxBufferLength控制播放器的缓冲策略。默认值可能导致 hls.js 在弱网环境下自动提升缓冲到非常大内存占用成倍上涨。我限制在 30 秒和 60 秒既保证抗抖动又不至于让浏览器标签页变成内存怪兽。移动端兼容性这里有一个特殊的处理逻辑iOS Safari 不支持 MSE但原生支持 HLS。所以代码里要先判断Hls.isSupported()不支持时走原生的canPlayType(application/vnd.apple.mpegurl)分支直接给 video 元素赋值 m3u8 地址。Android 端 Chrome 全部走 hls.js 分支。这套双分支逻辑基本覆盖了现代浏览器的所有情况。有一点必须强调HLS 的播放地址必须是可公开访问的 HTTP/HTTPS 地址。如果用 localhost 或局域网 IP 做地址手机浏览器在非同一局域网下是打不开的。LunaTV 的做法是给 Nginx 配了一个子域名并通过 HTTPS 提供服务这样浏览器不会因为权限问题拒绝播放。4.3 Nginx 的反代与静态托管配置SRS 本身不擅长处理高并发的静态文件请求所以我没有直接把 SRS 暴露给用户而是在它前面加了一层 Nginx。Nginx 负责三件事托管前端静态页面、代理 HLS 分片请求、给所有资源加缓存头。Nginx 的关键配置server { listen 443 ssl; server_name lunatv.example.com; ssl_certificate /etc/nginx/ssl/lunatv.crt; ssl_certificate_key /etc/nginx/ssl/lunatv.key; location /live/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; add_header Cache-Control no-cache; } location / { root /var/www/lunatv; index index.html; } }location /live/反代到 SRS 的 HTTP 端口SRS 默认会在 8080 端口提供 HLS 目录访问。分片文件是持续更新的所以这里必须加Cache-Control: no-cache否则部分浏览器会缓存 m3u8 索引文件导致用户一直看到旧画面。静态资源缓存策略是另一回事。页面本身、hls.js 脚本、CSS 这些文件几乎不变可以放心设置长缓存减少重复加载。5. 7x24 稳定性的工程化保障跑一天容易跑一个月才见真功夫LunaTV 这种慢直播项目能撑过一天的推流只是及格线连续跑一周才算是真正稳定。直播进程崩溃、素材文件损坏、磁盘空间不足、网络瞬断任何一个小问题都会让频道停播。这一章聊聊我为稳定性做的几项工程化措施。5.1 用 systemd 托管 FFmpeg 进程LunaTV 的 FFmpeg 推流进程不是用 nohup 启动的而是托管在 systemd 下。这样做的最大好处是进程异常退出后 systemd 会自动拉起而且启动、停止、查看状态都有一致的命令接口。我写的 unit 文件[Unit] DescriptionLunaTV FFmpeg Pusher Afternetwork-online.target docker.service Wantsnetwork-online.target [Service] Userlunatv Grouplunatv ExecStart/usr/local/lunatv/push.sh Restartalways RestartSec10 StartLimitIntervalSec60 StartLimitBurst5 [Install] WantedBymulti-user.targetRestartalways配合RestartSec10是核心进程退出后 10 秒自动重启。StartLimitBurst5做防抖如果 60 秒内崩溃超过 5 次说明存在严重问题systemd 会停止重启循环避免产生疯狂重启的僵尸循环。push.sh 里再嵌套一个内部循环让 FFmpeg 本身即便因临时网络错误退出也能在同一进程内快速重试减少对 systemd 重启的依赖#!/bin/bash while true; do ffmpeg -stream_loop -1 -re -f concat -safe 0 -i playlist.txt \ -f lavfi -t 3600 -i anullsrcchannel_layoutstereo:sample_rate44100 \ -filter_complex [0:v]scale1920:1080,fps25,formatyuv420p[v];[0:a][1:a]amixinputs2:durationfirst[a] \ -map [v] -map [a] \ -c:v libx264 -preset veryfast -tune zerolatency \ -c:a aac -b:a 128k \ -f flv rtmp://127.0.0.1:1935/live/lunatv \ -loglevel warning 2 /var/log/lunatv/push.log echo FFmpeg exited with code $?, restarting in 5 seconds... sleep 5 done这个双层重启机制的好处很明显如果是偶发的网络瞬断FFmpeg 会在 5 秒内自己恢复如果是连续崩溃systemd 会接管并以 10 秒间隔重试如果问题持续超过 5 次systemd 会停止重启并保留现场日志方便人工介入。5.2 素材文件的健康检查慢直播的素材是长期循环播放的文件损坏的可能性比很多人想象中要高。硬盘坏道、误操作覆盖、下载不完整都可能导致某个素材文件中途损坏。而这类损坏最头疼的地方在于FFmpeg 不一定立刻报错它可能正常播放前半段到损坏位置突然退出。LunaTV 做了两步防护。第一步是在素材入库时用 ffprobe 做一次完整检查确认文件可解码、时长正常、编码参数符合预期。第二步是写了一个定时任务每 6 小时对 playlist.txt 里的文件扫描一遍文件大小和修改时间一旦发现文件大小和入库记录不一致就立即从列表中剔除防止它影响整条推流。素材文件的损坏还有一种隐蔽情况文件系统层面没问题但视频流内部有坏帧。这种损坏 ffprobe 不一定能探出来真正的排查手段反而是看 FFmpeg 的运行日志。如果 FFmpeg 在某个固定时间点反复崩大概率就是那个时间点播放的素材坏了。定位方法很简单在 playlist.txt 里做二分查找逐段排除找出坏文件。5.3 日志与磁盘监控7x24 服务的运维离不开监控。LunaTV 的监控方案比较轻量不依赖 Prometheus 那套重型全家桶而是用 shell 脚本加 cron 实现三个核心指标推流进程是否存在。HLS 最新分片是否在最近 60 秒内有更新。磁盘使用率是否超过 90%。前两个都可以在脚本里用简单的文件判断完成第三个用df命令解析。任何一个指标异常脚本都会通过钉钉机器人或邮件发告警。慢直播项目不像商业服务需要秒级响应分钟级告警完全够用。还有一个小细节是hls_dispose参数。SRS 的分片清理机制默认是删除过期分片但有时会碰到分片文件被 Nginx 或播放器占用而无法删除的情况。hls_dispose 5会让 SRS 在删除前重试多次能显著减少磁盘被历史分片占满的风险。5.4 HTTPS 证书的自动续期网页播放器对 HTTPS 的要求在现代浏览器里越来越严格如果站点没有 HTTPS很多浏览器的自动播放策略、摄像头权限、剪贴板权限都会受限。LunaTV 的 Nginx 用了 Lets Encrypt 的免费证书通过 certbot 的 webroot 模式自动续期并把续期任务写进了 cron0 3 * * * certbot renew --quiet --deploy-hook systemctl reload nginx这个 cron 会在每天凌晨 3 点检查证书是否需要续期续期后自动重载 Nginx。对于长期运行的项目证书自动续期不是可选项而是必须项。否则跑着跑着证书过期用户打开网页会看到红色警告流量直接就没了。6. 一周连续运行的实测数据与后续演进思路LunaTV 上线后连续跑了一周我记录了几个关键数据这里直接列出来供参考。硬件环境是一台 2 核 4G 内存的云主机带宽 5Mbps系统盘 40G。FFmpeg 推流 1080p25、码率约 3MbpsSRS 做 HLS 转封装Nginx 提供 Web 访问。整条链路的 CPU 使用率峰值约 40%内存占用稳定在 1.2G 左右磁盘每天新增 HLS 分片约 100M 左右因为带宽有限并发播放峰值我压测到 20 路时无卡顿。指标实测数值CPU 使用率平均 28%峰值 40%内存占用1.1G - 1.3G推流码率约 3Mbps单日磁盘增量约 100MB并发播放上限20 路流畅一周断流次数2 次网络抖动自愈恢复一周里发生了两次断流一次是云主机网络瞬断导致 RTMP 连接断开另一次是素材列表里一个文件的时间戳异常导致 FFmpeg 退出。两次都被双层重启机制自动恢复人为介入为零。这说明整个工程化保障体系是合格的也说明素材预处理环节还需要再加一道校验。后续演进有明确的方向。第一是加动态专辑能力让播放列表可以根据节假日或时段切换不同的主题素材比如农历十五前后多排月亮特写周末排城市夜景。第二是多路频道扩展SRS 天然支持多 vhost一个进程就能跑多个频道未来可以拆成 LunaTV-Moon、LunaTV-City、LunaTV-Stars 三个子频道。第三是加录制回看功能SRS 的 DVR 模块可以在推流的同时把直播录成 mp4 文件这样观众不仅能看直播还能回看昨天晚上的完整月亮轨迹。回到项目本身LunaTV 最让我满意的地方不在于技术多复杂而在于它用一种可维护、可扩展的方式把零散的素材变成了一个持续产出的内容频道。如果你也想做类似的事情建议有一条主线思路先稳定跑通最小链路再逐步加监控、加保护、加新功能。流媒体项目的坑大多藏在细节里只有长期跑起来才能全部暴露出来。
返回列表