Nginx RTMP 视频推流

有时候需要提供将影视资源 影片点播 这种类似的功能, 方便将服务器的影片直接在线播放;
虽然 mp4 文件直接挂个 nginx 服务播放, 但是影片资源不止有 mp4, 还有其他主流比如 flv 等格式.

大部分资源只是按照通用格式采用 mp4, 还有其他类似 .avi/mov/wmv/mkv/flv 等, 浏览器仅能支持 mp4

所以我们需要处理的就是将资源利用 ffmpeg 推流, 可以类似于直播一样播放我们服务器的影片资源

注意: 不止播放家庭影视资源, 还能涉及到家里监控推到自己内网服务器保存, 还有其他很多种玩法

需要明确在推流过程当中的定义:

  • 要实现全天推流需要提供 推送端(推) 和 发布端(拉), 本质上就是要构建视频流推/拉流
  • 需要搭建支持 rtmp 等协议的 hls 流媒体网络服务(充当转发平台)
  • 需要 ffmpeg(服务端)|OBS(桌面端) 做流推送到流转发平台

默认的源安装 Nginx 不支持 nginx-rtmp-module 相关模块, 手动编译就感觉没什么必要,
不过在 debian 系的 Linux 发行版的源追加第三方扩展方便直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 直接用这种方式源安装 nginx 会附带一些官方扩展和 rtmp 支持
# 可以避免需要自己去手动编译处理, nginx 非官方的第三方扩展都是以 libnginx-mod-* 来发布
# ffmpeg 则是作为推送工具使用, 如果要推流和拉流分开就需要将 nginx 和 ffmpeg 分开部署
sudo apt install nginx nginx-extras libnginx-mod-rtmp ffmpeg

# 确认是否扩展已经存在, 如果有输出代表已经安装完毕
ls -l /usr/lib/nginx/modules |grep rtmp

# 验证安装
# 注意: 做推送流对于服务器的带宽/内存/CPU要求很高, 如果服务器性能过于低下会导致推流卡顿
nginx -V
ffmpeg -version

# 之后创建专用的 conf.d 目录处理, 这里有 live/hls 两种模式, 后面会说明差别
sudo mkdir /etc/nginx/rtmp.d
sudo touch /etc/nginx/rtmp.d/live.conf # 创建备用的 rtmp 服务配置

# 准备修改主配置
sudo vim /etc/nginx/nginx.conf

注意: rtmp 扩展不能放置在 http{ ... } 内部处理, 而是单独另外声明 rtmp{ ... } 块.

/etc/nginx/nginx.conf 建议采用 http{} 块来声明加载多配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 系统事件配置
events {
# 略
}

http {
# 略
}


# 开启自动推送流, 只允许被放置在根配置
# 默认情况下, 若 FFmpeg/OBS 等推流端因网络波动、进程异常、服务器重启等原因断开与 Nginx RTMP 服务的连接
# 如果设置 rtmp_auto_push 之后会主动等待推流端重新建立连接, 恢复推流后自动接续分发
# rtmp_auto_push_reconnect 则是设定自动开启之后每次重连的间隔
rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;


# RTMP 模块通用配置
rtmp {
# 日志配置
access_log /var/log/nginx/rtmp_access.log;

# 具体配置可以查看 https://github.com/arut/nginx-rtmp-module
# 引用加载其他服务目录的文件配置
include /etc/nginx/rtmp.d/*.conf;
}

主类的配置差不多就是这样, 但是其实目前还有个问题, 就是我们要选用什么推送方式, 目前流推送有以下方式:

  • rtmp: 以类似直播方式来远程投递数据流, 这种是实时的流推送, 如果推送端稳定性不好可能会导致断流(直播流)
  • hls: 以本地目录方式将视频流切片放置在目录推送, 他会将数据流自动切片放置本地, nginx 就是将目录切片数据转发(切片化)

这里就是主流的推流送方式, 具体两者都有利弊:

  • rtmp: 是直接通过网络将流转发过来, 不会在本地产生太多资源冗余, 但是对推送端的稳定性要求高(需要上行流量稳定)
  • hls: 在通过接收的网络流分片切分然后转发合并的数据流, 虽然会占用本地空间但分发数据流效率更高(需要满足空间占用)

rtmp 适合那些需要低延迟的场景, hls 适合网页|移动端需要更大兼容性的场景, hls 流大部分采用 *.m3u8 结尾

这里就是区分选型的地方, 如果你是准备打算本地/内网视频文件推流且推送用户比较少的情况, 可以直接采用 rtmp:

  • 因为视频文件本身处于本地/内网, 也就代表本身网络延迟不是那么高, 哪怕放到其他服务器里走内网转发效率都很高
  • 用户量不多甚至基本上只有内网用户观看, 代表了主要网络下发瓶颈可能不这么大
  • 并且 hls 还有的切片保存可能会爆硬盘问题, 重复切片保存本地/内网就有视频感觉就很呆

但是如果你场景是户外/家庭直播的情况, 那么可能 rtmp 会无限扩大直播推流的问题, 以下场景更加适合 hls 推流:

  • 一般分配给户外/家庭的运营商会限制上行流量, 导致了上行在某些用过头的时间段直接被降低(除非购买专线宽带)
  • 直播一般都不是文件形式推送, 而是以推送流切片提交上来保存, 如果硬盘空间足够可以保存在本地出问题就溯源
  • 日常需要设置监控推送流保存在本地情况, 也是比较推荐 hls 保存本地留底方便后续查看监控录像

这里就分开采用两种方式来监听处理, 可以按照自己需求来选择, 我是需要看本地电影的资源推流就用 rtmp, 监控推流则是用 hls.

其实还可以两者结合起来, 比如变通过 rtmp 直播推流保存切片, 而用户查看的就是 hls 的资源流, 这个后续会说明

可以先查看官方的具体例子:

RTMP 推流

首先就是创建我们简单的 rtmp 流配置(/etc/nginx/rtmp.d/live.conf):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
server {
listen 1935; # RTMP 默认端口, 可自定义
chunk_size 4096; # 数据块大小, 优化小文件推流
max_message 1M; # 单包最大尺寸, 防止恶意数据
timeout 60s; # 延长连接超时,避免短连接导致数据中断
ping 30s; # 每30s发送心跳包,维持推/拉流连接
ping_timeout 10s; # 心跳超时时间,避免假死连接

# 核心直播应用(推/拉流核心标识)
# 这里其实和 http-location 一样, 用来声明访问路径, 也就是访问的是 rtmp://{服务地址}:{listen}/{application声明路径}/{自定义推送名称:也就是推流码}
# 最后访问的就是类似 127.0.0.1:1935/live/{自定义推送名称:推流码}
# 这里其实有 BUG, 也就是不知道是不是 Ngnix 实现有问题, {推流码} 其实是无效的, 推流和拉流最后只能通过 rtmp://{服务地址}:{listen}/{application} 处理
# 后面会说明这个 BUG, 我也是网上找资料之后才发现这个问题
application live {
live on; # 开启直播模式
record off; # 不需要做流录制
meta copy; # 保留推流的元数据(必须加, 否则播放端解析不到流信息)
interleave on; # 交织音频/视频包(RTMP 标准要求)

# 允许推流, 可限制IP
# 因为我们的流其实就是本地视频文件, 所以推流的地址也就是我们本地
allow publish 127.0.0.1;

# 如果我们需要多个推送来源可以不断附加
# 支持范用的匹配机制支持内网IP访问
allow publish 192.168.1.0/24;


# 拒绝所有未在白名单的IP推流必须放在最后
# 建议不相关的地址全都禁止, 避免被其他嗅探到
deny publish all;

# 如果 20s 内没有 push 行为就断开连接
drop_idle_publisher 20s;

# 允许拉流, 允许所有来源的地址进行拉取流播放
# 如果要封禁指定操作也是如下
# deny play 111.111.111.111
allow play all;
}

# 如果是本地文件需要将自动转为 rtmp 可以直接配置目录
# 这里声明 files, 则是直接访问 rtmp://{服务地址}:{listen}/{application声明路径}/{文件名}
# 但是需要注意: 这种方式只能处理 mp4 格式文件, 无法处理其他格式文件
application files {
# 这里我路径有 /data/media/anime-1.mp4 文件, 可以直接映射到内部转化为流
# 也就是最后播放地址是 rtmp://127.0.0.1:1935/files/anime-1.mp4
# 如果单独的单文件播放的话, 这种方式就足够支持了, 但是没办法做到多个视频轮播这种功能
play /data/media;
}

# 另外还有个小技巧, 就是网络文件的的流转发
# 如果远程服务器有个 http://localhost:8080/files/video.mp4 文件
# 那么依靠这种方式可以读取并自动将影片解码成 rtmp 协议
# 依靠这种方法可以播放远程 rtmp://127.0.0.1:1935/remote/video.mp4 资源
# 这种适合本地内网本身有另外服务器专门保存的情况, 直接转发给内网挂载在 Web 上的资源就行了
#application remote {
# play http://localhost:8080/files/;
#}
}

这样就处理完成 nginxrtmp 直播流配置, 可以重启下 nginx 确认是否有问题:

1
2
sudo systemctl restart nginx # 重启 nginx
sudo lsof -i :1935 # 确认是否挂起监听

如果局域网有桌面端可以直接用 OBS 设置流推送地址为 rtmp://127.0.0.1:1935/live/movie 来做推送流:

  • live 对应 Nginx 配置的 application
  • movie 就是自己对于这个流分配的自定义标识名称, 只要保持唯一可以随便设置, 有的视频网站直播会生成 MD5 推送 TOKEN

我先采用桌面端的 OBS 来测试推送, ffmpeg 命令行推送比较复杂, 先确保推送端和播放端能够正确跑通:

  1. 打开软件在菜单栏执行 文件 -> 设置 -> 直播
  2. 服务 选中 自定义, 一般在最上面
  3. 服务器 输入 rtmp://127.0.0.1:1935/live, 注意不是 /live/movie
  4. 推送码 输入 movie, 这里就是主要推送唯一码, 之后应用就可以
  5. 之后就是设置视频采集源, 在 来源 之中点击加号创建 屏幕采集 或者 窗口采集 就行
  6. 最后点击 开始直播, 这里就开始会把流推送到 nginx 之中

这里可以在随便支持流读取的本地播放当中输入 rtmp://127.0.0.1:1935/live/movie 来播放, 我这里采用 VLC;
直接打开 VLC -> '媒体' 菜单栏 -> 打开网络串流 操作输入地址就能看到了.

之后就是命令行来通过 ffmpeg 推送到服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 我这里的视频文件在 /data/media/anime-1.mp4
# 还有个需要注意的点, 不要把视频放置在太多层级目录下, 有的视频名称长度太长会出问题

# 这里直接简单的单个文件 ffmpeg 投递推送
ffmpeg -re -stream_loop -1 -i /data/media/anime-1.mp4 -c copy -f flv \
rtmp://127.0.0.1:1935/live/movie

# 如果要对视频原生重新转化编码可以配合以下指令
ffmpeg -re -stream_loop -1 -i /data/media/anime-1.mp4 \
-c:v libx264 -preset medium -b:v 2000k -c:a aac -b:a 128k \
-f flv rtmp://127.0.0.1:1935/live/movie

# 如果想检测是不是特殊编码采用以下命令
ffprobe /data/media/anime-1.mp4 # 输出中需显示 Video: h264, Audio: aac

# 这里启动 ffmpeg 自带的播放确定是否有值跳动
# 如果出现 ffmpeg 命令跳动而 ffplay 没有跳动, 那么就一定是 Nginx/ffmpeg 内部配置出问题
ffplay -fflags +igndts rtmp://127.0.0.1:1935/live/movie # 模拟播放器
ffprobe -v debug rtmp://127.0.0.1:1935/live/movie

这里需要说明我们用到的 ffmpeg 参数:

  • -re: 按实际帧率读取视频(模拟实时流, 也就是相当于录播形式)
  • -stream_loop -1: 无限循环播放视频
  • -i:指定本地视频文件路径
  • -c:v libx264: 视频编码为 H.264(兼容性最好, 也可以考虑采用 av1)
  • -preset medium: H.264 视频编码器的预设参数, 通用场景的参数, 兼顾编码速度和压缩效率
  • -b:v 2000k:视频码率 2000kbps(可根据网络调整, 但是需要注意码率越高对服务器性能负载越大)
  • -c:a aac: 音频编码为 AAC
  • -f flv: 输出格式为 FLV(RTMP 标准格式)
  • -c copy: 直接拷贝视频和音频的编码, 不重新编码节省系统资源

注意这里有个巨坑的问题, 如果设置推送码推流的话, VLC 可能无法正常获取到直播流行:

1
2
3
4
5
6
7
8
# 我们上面说过的 NGINX 可能算是 BUG 的问题
# 比如设置 /live/movie 推流
rtmp://127.0.0.1:1935/live/movie
# 采用这种方式是没办法接收到流

# 而是必须要采用无推送码推送
# 只有这样才能被正确推流和拉流
rtmp://127.0.0.1:1935/live

这里后面更改以下指令就推送正常了, 也不知道是什么原理:

1
2
3
# 这样反而能够接收到流
ffmpeg -re -stream_loop -1 -i /data/media/anime-1.mp4 -c copy -f flv \
rtmp://127.0.0.1:1935/live

目前已经实现资源实时推送流播放功能, 其实这里有点大费周章, 因为最开始上面有说过可以直接转发内网文件流:

1
2
3
4
5
# 这样来转发内网的资源, 只需要内网服务器开个 nginx 把资源暴露出来
# 但是需要注意: 这种方式只能处理 mp4 格式文件
application remote {
play http://localhost:8080/files/;
}

这种方式直接硬件资源加载其实效率更好点, 可以避免另外运行 ffmpeg 占用系统资源,
不过影视资源不止有 mp4 格式, 其他复杂的影片格式最后还是要动态利用 ffmpeg 来推流播放.

还有利用其他额外扩展 ngx_http_flv_module 来实现多格式的视频转发,
但是这个模块主要问题对于源视频要求 H.264 视频编码 + AAC/MP3 音频编码,
除此之外的其他格式都不支持, 这样如果有格式稍微不一致的影片都需要手动转化成对应的格式也是很不方便.

HLS 切片

官方讲解 HLS(HTTP Live Streaming) 是苹果推出的基于 HTTP 的流媒体传输协议, 核心是将视频流切分成若干 .ts 格式的小切片,
从而生成 .m3u8 索引文件, 而播放器通过请求索引文件和切片文件实现播放, 兼容性(尤其是网页|移动端)远优于直接 RTMP 实时推送.

而由于这种是调用存盘落地的文件播放, 所以在大并发点播的情况其实更像是直接访问资源, 所以并发效率比实时推送更好点.

注意还是需要 ffmpeg 推送, 只是推送的时候是 nginx 在帮助生成本地的切片文件, 而多个用户访问的其实就是这些切片文件

1
2
3
4
5
6
7
8
9
10
11
12
13
# 需要先创建切片保存目录
# 建议挂载外部硬盘, 放置系统硬盘爆满
sudo mkdir /data/frames # 创建切片根目录
sudo mkdir /data/frames/hls # 这里为什么要单独创建个 hls 目录, 后面会说明

# 之后赋予 www-data 权限
sudo chown -R www-data:www-data /data/frames
sudo chmod -R 755 /data/frames
sudo chmod g+s /data/frames # 后续文件归属于 www-data

# 这里就是直接在 http 配置目录块创建和编写
sudo touch /etc/nginx/conf.d/hls.conf # 创建对外 http 的 hls 切片访问服务
sudo touch /etc/nginx/rtmp.d/hls.conf # 创建内部推流的 rtmp 推流服务

/etc/nginx/conf.d/hls.conf 文件内容如下, 主要用于处理外部访问视频切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server {
listen 11936; # 这里随便取个端口, 我是按照 rtmp 推流配置的端口签名加个值
server_name _; # 匹配所有域名/IP

# 分发HLS切片文件
# 这里就是之前说的, 为什么要在 /data/frames 创建 hls 目录
# 因为 location 路径生成的目录对应的是是内部 {root}{location}
# 所以我们一般访问的 http(s)://{server_name}:{listen}{location}
# 其实主要推流创建的地方是 {root}{location}
# 主要这里最好对应 rtmp 推流配置的 application 名称, 方便直接映射
location /hls/ {

# 切片目录设置
# 这里就是推流切片的本地保存根目录
root /data/frames/;
autoindex off; # 关闭目录索引,防止泄露文件

# 访问设置
add_header Access-Control-Allow-Origin *; # 允许跨域(网页播放必备)
add_header Cache-Control "no-cache"; # 禁用缓存(实时切片需最新)
types {
application/vnd.apple.mpegurl m3u8; # 识别.m3u8索引文件
video/mp2t ts; # 识别.ts切片文件
}
}
}

/etc/nginx/rtmp.d/hls.conf 文件内容如下, 主要用于 ffmpeg 推流并转化成切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
server {
listen 1936; # 这里为了规避原来的 live 配置该了端口
chunk_size 4096; # 数据块大小, 优化小文件推流
max_message 1M; # 单包最大尺寸, 防止恶意数据
timeout 60s; # 延长连接超时,避免短连接导致数据中断
ping 30s; # 每30s发送心跳包,维持推/拉流连接
ping_timeout 10s; # 心跳超时时间,避免假死连接

# HLS 核心应用(切片存储+分发)
application hls {
live on; # 开启直播模式
record off; # 不需要做流录制
meta copy; # 保留推流的元数据(必须加, 否则播放端解析不到流信息)
interleave on; # 交织音频/视频包(RTMP 标准要求)

# 允许推流的IP(本地/内网)
allow publish 127.0.0.1;
allow publish 192.168.1.0/24;
deny publish all;
allow play all;

# HLS 切片核心配置
hls on; # 开启HLS切片
hls_path /data/frames/hls/; # 切片文件存储目录(需提前创建并赋权), 这里就是 http-hls 访问的目录
hls_fragment 5s; # 切片时长(建议3-10s,越小延迟越低)
hls_playlist_length 30s; # 播放列表保留的切片时长(至少大于1个切片)
hls_continuous on; # 断流后重新推流时,继续追加切片(而非新建)
hls_cleanup on; # 自动清理过期切片(防止磁盘占满), 如果想持久化保存流则需要直接关闭
hls_nested on; # 按流名称创建子目录存储切片(避免冲突)
hls_type live; # 直播模式(区别于点播)
}

# HLS 点播应用(直接播放本地切片文件)
application hls_play {
play /data/frames/hls/; # 直接读取已生成的.m3u8和.ts文件
}
}

之后重启服务确认是否挂起监听服务:

1
2
3
4
5
6
7
8
9
10
11
sudo systemctl restart nginx # 重启服务
sudo lsof -i :1936 # 确认 rtmp 推流接口
sudo lsof -i :11936 # 确认 hls 推流接口


# 现在就是可以直接推流过去
# movie为流名称,对应切片目录 /data/frames/hls/movie/
ffmpeg -re -stream_loop -1 -i /data/media/anime-1.mp4 -c copy -f flv \
rtmp://127.0.0.1:1936/hls/movie
# 可以另开命令行窗口确认 /data/frames/hls/movie/ 是否生成
# 内部是否已经构建 *.ts 和 index.m3u8 文件

这里也就是很奇葩的地方, /hls/movie 推流现在成功, 不需要剔除最后推流码

如果要播放 HLS 流, 可以采用以下方式查看:

  • 本地播放器(VLC): http(s)://{服务器IP}:{hls-http端口}/hls/movie/index.m3u8
  • 网页播放(H5): 使用 video.js/hls.js 等库渲染网页

hls.js 渲染的网页访问页面, 这里编写成文件直接打开都可以自动播放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/hls.js"></script>
</head>
<body>
<video id="player" controls width="800"></video>
<script>
const video = document.getElementById('player');
if (Hls.isSupported()) {
const hls = new Hls();
// hls.loadSource('http://服务器IP/hls/movie/index.m3u8');
// 这里替换成我们之前服务器地址
hls.loadSource('http://127.0.0.1:11936/hls/movie/index.m3u8');
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
//video.src = 'http://服务器IP/hls/movie/index.m3u8';
// 这里替换成我们之前服务器地址
video.src = 'http://127.0.0.1:11936/hls/movie/index.m3u8';
}
</script>
</body>
</html>

这里就是交叉 ffmpeg + hls 的推流处理方式, 基本上没什么太多的问题点, 后续就是额外有趣的推流配置.

其实还有很多细节需要处理, 比如 ffmpeg 注册系统单元和摄像头监控推流等, 但是这些篇幅可能更加复杂, 后续有时间再处理.