背景

因工作室的培训项目需要一个类似学校机房的内网直播系统,在我查阅了网络上的现成方案后发现了以下问题:

  • 娱乐直播平台:需繁杂的实名认证,直播延迟过高,无法控制观看范围
  • 腾讯会议,钉钉等直播:收费,直播清晰度较低,延迟较高

于是尝试自行搭建直播系统,目标

  • 在局域网内流畅(20fps),低延迟(1s以内),高清晰度(1080p)
  • 承载100人观看
  • 便于自定义推流内容,例如某个程序,水印等
  • 安全控制,杜绝未经授权的推流
  • 录制流,便于后期观看

在查阅资料后找到了以下常用协议:

  • rtmp:娱乐直播平台常见直播协议,便于分发,但延迟较高
  • rtsp:网络摄像机常见直播协议,基于udp或tcp,延迟较低
  • HLS:常用于播放各种节目,可回放,延迟很高
  • webrtc:低延迟的直播协议,但是资料很少,配套推流设施不完善

本文使用rtsp方案

服务器的搭建

在查找了GitHub上的各种开源rtsp服务端之后,本人发现大部分服务器方案都比较简单,好在我们的使用情景不需要很多复杂功能,于是使用rtsp-simple-server

配置

该软件使用go编写,可以方便地在Linux和Windows平台上运行

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# supported stream protocols (the handshake is always performed with TCP).
protocols: [tcp] # 这个地方只能使用tcp,udp会丢包
# port of the TCP RTSP listener.
rtspPort: 8554
# port of the UDP RTP listener (used only if udp is in protocols).
rtpPort: 8000
# port of the UDP RTCP listener (used only if udp is in protocols).
rtcpPort: 8001

# timeout of read operations.
readTimeout: 10s
# timeout of write operations.
writeTimeout: 10s

# supported authentication methods (both are insecure, use RTSP inside a VPN
# to enforce security).
authMethods: [basic, digest]

# enable Prometheus-compatible metrics on port 9998.
metrics: no
# enable pprof on port 9999 to monitor performances.
pprof: no

# destinations of log messages; available values are "stdout", "file" and "syslog".
logDestinations: [stdout]
# if "file" is in logDestinations, this is the file which will receive the logs.
logFile: rtsp-simple-server.log

# command to run when a client connects to the server.
# this is terminated with SIGINT when a client disconnects from the server.
# the server port is available in the RTSP_PORT variable.
# the restart parameter allows to restart the command if it exits suddenly.
runOnConnect:
runOnConnectRestart: no

# these settings are path-dependent.
# it's possible to use regular expressions by using a tilde as prefix.
# for example, "~^(test1|test2)$" will match both "test1" and "test2".
# for example, "~^prefix" will match all paths that start with "prefix".
# the settings under the path "all" are applied to all paths that do not match
# another entry.
paths:
all:
# source of the stream - this can be:
# * record -> the stream is provided by a RTSP client
# * rtsp://existing-url -> the stream is pulled from another RTSP server
# * rtmp://existing-url -> the stream is pulled from a RTMP server
# * redirect -> the stream is provided by another path or server
source: record

# if the source is an RTSP url, this is the protocol that will be used to
# pull the stream.
sourceProtocol: udp # 使用udp拉流可以防止丢包引起的花屏
# if the source is an RTSP or RTMP url, it will be pulled only when at least
# one reader is connected, saving bandwidth.
sourceOnDemand: no
# if sourceOnDemand is "yes", readers will be put on hold until the source is
# ready or until this amount of time has passed.
sourceOnDemandStartTimeout: 10s
# if sourceOnDemand is "yes", the source will be closed when there are no
# readers connected and this amount of time has passed.
sourceOnDemandCloseAfter: 10s

# if the source is "redirect", this is the RTSP url which clients will be
# redirected to.
sourceRedirect:

# fallback url to redirect clients to when nobody is publishing to this path.
fallback:

# username required to publish.
publishUser: # 在这里设置推流用户名密码
# password required to publish.
publishPass:
# ips or networks (x.x.x.x/24) allowed to publish.
publishIps: []

# username required to read.
readUser:
# password required to read.
readPass:
# ips or networks (x.x.x.x/24) allowed to read.
readIps: []

# command to run when this path is initialized.
# this can be used to publish a stream and keep it always opened.
# this is terminated with SIGINT when the program closes.
# the path name is available in the RTSP_PATH variable.
# the server port is available in the RTSP_PORT variable.
# the restart parameter allows to restart the command if it exits suddenly.
runOnInit:
runOnInitRestart: no

# command to run when this path is requested.
# this can be used to publish a stream on demand.
# this is terminated with SIGINT when the path is not requested anymore.
# the path name is available in the RTSP_PATH variable.
# the server port is available in the RTSP_PORT variable.
# the restart parameter allows to restart the command if it exits suddenly.
runOnDemand:
runOnDemandRestart: no
# readers will be put on hold until the runOnDemand command starts publishing
# or until this amount of time has passed.
runOnDemandStartTimeout: 10s
# the runOnDemand command will be closed when there are no
# readers connected and this amount of time has passed.
runOnDemandCloseAfter: 10s

# command to run when a client starts publishing.
# this is terminated with SIGINT when a client stops publishing.
# the path name is available in the RTSP_PATH variable.
# the server port is available in the RTSP_PORT variable.
# the restart parameter allows to restart the command if it exits suddenly.
runOnPublish:
runOnPublishRestart: no

# command to run when a clients starts reading.
# this is terminated with SIGINT when a client stops reading.
# the path name is available in the RTSP_PATH variable.
# the server port is available in the RTSP_PORT variable.
# the restart parameter allows to restart the command if it exits suddenly.
runOnRead:
runOnReadRestart: no

按照上面的配置文件进行配置即可

推流

推流使用的是OBS,因为OBS的推流选项无法直接推送rtsp流,所以使用录制功能完成推流

配置

路径

路径的填写方式是这样的

1
2
3
rtsp://用户名:密码@服务器地址:端口/流id
例如
rtsp://aaa:xxxxx@10.0.0.1:8554/lalalal

容器格式

我们使用rtsp协议推流,自然选择rtsp

视频比特率

由于我们直播的内容基本都是写代码,所以并不需要太高比特率,3000kbps足够(也许还能再小一些)

关键帧间隔

关键帧(i帧)是视频中图像的完整帧,后续帧(增量帧)仅包含已更改的信息。关键帧间隔增大可以节约网络带宽。

由于写代码场景背景几乎不变,预测帧能很好地工作,所以可以将间隔增大来节约带宽且不对画质产生较大影响

视频编码器

一开始我尝试了h264(h264和显卡编码),效果虽然还行但是并不是最佳的解决方案。考虑到客户端由我们进行分发,可以使用h265编码

我的显卡是10系显卡,可以使用显卡h265编码,所以选择(nvenc_hevc),一般来说显卡编码速度比CPU编码快

update: 使用显卡h264编码速度会更快一些(大概50ms左右),所以也可以使用显卡h264编码

视频编码器设置

1
2
3

-preset:v llhq -tune:v zerolatency -profile veryfast

-preset:v llhq: 使用高质量低延迟预设(仅限显卡编码)

-tune:v zerolatency: 使用低延迟模式

-profile veryfast: 启用veryfast预设

图像大小和帧数

较低的帧数可以保证推流质量,在我们的应用场景中不需要过高的帧数,设置15-20即可

客户端

最容易找到的可方便自定义的客户端应该就是ffmpeg内自带的ffpaly了

1
ffplay -fflags nobuffer -flags low_delay -analyzeduration 1000000 -i rtsp://xxxxx

-fflags nobuffer 禁止缓存,减少播放时产生的延迟,但在网络较差的时候会导致丢包

-flags low_delay 低延迟模式

-analyzeduration 1 减少分析时间

移动设备目前我没有找到低延迟的解决方案,但是vlc至少可以播放rtsp流,于是暂时先使用vlc

录制

录制方案其实很简单:要么在推流端直接录制,要么使用ffmpeg或者是vlc客户端完成录制

测试

该方案可以基本达到最开始的要求,实测延迟400ms左右,1080p清晰画质,单个用户消耗带宽20-400kb/s。但是未在不支持显卡h265编码的设备上进行测试

但仍然出现了一些问题:在网络较差的情况下会出现严重花屏,丢包等问题

改善花屏

之前出现花屏的原因是因为拉流使用了tcp协议,而在buffer较小的时候使用tcp会导致雪崩式丢包,造成无法恢复的花屏,使用udp拉流可以改善该问题