一、前言
随着5G时代的到来,音视频行业也可能迎来一个行业的春天,直播则是新视频行业一直以来的一个重要的产品形态,从最初的秀场直播,游戏直播,到今年由于疫情,目前比较火的在线教育直播,带货直播等,各类新的直播形式则是越来越多的展示在大众面前。作为技术开发的我们,今天我们一起简单的了解一下,如何快速搭建一套最简单的直播系统,简单地了解一下主流直播的架构模型。二、推拉流模型
首先我们先看一张完整的直播推拉流的模型图,我们可以很清楚地看到直播宏观上的架构模型图
三、搭建步骤
本入门直播简单教程主要分为如下几个模块搭建直播服务器使用OBS进行推流直播流如何观看直播间消息的实现3.1 搭建直播服务器直播服务器实时地将推流端上传的视频流进行解析和编解码,以用于支持rtmp、hls或httpflv等直播协议的观看端进行观看。

docker run -p 1935:1935 -p 7001:7001 -p 7002:7002 -p 8090:8090 -d gwuhaolin/livego
其中,各个端口的含义如下:8090:HTTP 管理访问监听地址
1935:RTMP 服务监听地址
7001:HTTP-FLV 服务监听地址
7002:HLS 服务监听地址3.2 使用OBS推流OBS(Open Broadcaster Software)是一款开源免费的提供视频录制和直播功能的软件,去OBS官网下载对应平台的软件进行安装即可。要想推流,首先要解决的是“推什么”的问题,也就是要明确流的来源。打开OBS,点击新建“来源”按钮,如下图中第1步所示,可以看到OBS支持的来源比较丰富,有媒体源、显示器采集、浏览器和窗口采集等等。此处用现有的mp4文件来进行循环推流,因此来源选择“媒体源”,名称用默认的就行,点击“确定”后,设置要播放的视频文件,然后点击“确定”即可。

到现在,推流地址和 channelkey 都有了,只需要在OBS里面进行相关设置就可以进行推流。首先点击“控件”的“设置”按钮,进入设置面板。{
"status": 200,
"data": "rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"
}



CBR(Constant Bitrate)恒定码率,一定时间范围内比特率基本保持恒定。使用该模式时,在视频动态画面较多的场景下,图像质量会变差,而在静态画面较多的场景下,图像质量又会变好。
VBR(Variable Bitrate)可变码率,其码率可以随着图像的复杂程度的不同而变化。使用该模式时,在图像内容比较简单的场景下,分配较少的码率,而在图像内容复杂的场景下,则分配较多的码率。这样既保证了质量,又兼顾到带宽限制,优先考虑到图像质量。
ABR(Average Bitrate)平均比特率,是VBR的一种插值参数。简单场景分配较低码率,复杂场景分配足够码率,这一点类似VBR。同时,一定时间内平均码率又接近设置的目标码率,这一点又类似CBR。可以认为ABR是CBR和VBR的折中方案。
CRF(Constant Rate Factor)恒定码率系数。CRF值可以理解为对视频的清晰度和流畅度期望的一个固定输出值,即无论是在复杂场景还是在简单场景下,都希望有一个稳定的主观视频质量。关键帧间隔(Group of Pictures,GOP)指的是一组由一个I帧、多个P帧和B帧组成的一个帧序列。一帧就是视频中的一个画面,其中:
I帧(intra coded picture):最完整的画面,自带全部信息,无需参考其他帧即可解码,每个GOP都是以I帧开始;P帧(predictive coded picture):帧间预测编码帧,需要参考前面的I帧或P帧,才能进行解码,压缩率较高;B帧(bipredictive coded picture):双向预测编码帧,以前帧后帧作为参考帧,压缩率最高。对于普通视频,加大GOP长度有利于减小视频体积,但是在直播场景下,GOP过大会导致客户端的首屏播放时间变长。GOP越小图片质量越高,建议设为2秒,最长不要超过4秒。3.3 直播流观看我们刚刚已经搭建完成了RTMP服务器,并且使用目前比较成熟,功能比较丰富的推流工具OBS进行推流,接下来我们就要解决如何在用户终端进行观看了的问题。FLV(Flash Video)是一种网络视频格式,是一种流媒体格式,目前主流的一些直播网络使用的流媒体格式比较多的都是flv,它能够不需要安装任何插件即可进行播放。3.3.1 小试牛刀:使用VLC工具观看VLC 是一款音视频播放器,可以播放本地媒体,也可以播放网络上的媒体,到官网 https://static1.haohuo.net/uploads/images/2184827/2184827_index.zh.html 下载对应的安装包安装即可。点击“媒体”tab下的“打开网络串流”选项,然后网络地址设置为:rtmp://localhost:1935/live/movie ,点击“确定”后就可以看到OBS推流的视频啦。





RTMP
HLS
HTTP-FLVHLS全称是 HTTP Live Streaming。这是 Apple 提出的直播流协议。目前,IOS 和 高版本 Android 都支持 HLS,HLS 主要的两块内容是 .m3u8 文件和 .ts 播放文件。接收服务器会将接收到的视频流进行缓存,然后缓存到一定程度后,会将这些视频流进行编码格式化,同时会生成一份 .m3u8 文件和其它很多的 .ts 文件,HLS的优点是跨平台性比较好,HTML5可以直接打开播放,移动端兼容性良好,缺点也是比较明显,就是时延比较高,如果有些直播,例如互动性不高的直播,可以使用该协议,HLS网络传输格式是非常适合用于点播的场景。RTMP全称 Real Time Messaging Protocol,即实时消息传送协议,对于开发者来说,我们先明确RTMP是应用层协议,底层是使用的TCP传输协议,这边我们知道RTMP是音视频相关领域的协议,所以这块使用TCP作为主要的传输层协议也给后续RTMP关于网络的各种各样的演进,留下了很多的空间,在直播行业,特别是在推流端,RTMP协议是名副其实的霸主,基本上所有主流的直播网站都是支持rtmp协议进行推流的,关于RTMP的具体协议细节,后续文章有具体的分析。FLV(Flash Video)是 Adobe 公司推出的另一种视频格式,是一种在网络上传输的流媒体数据存储容器格式。其格式相对简单轻量,不需要很大的媒体头部信息。整个 FLV 由 The FLV Header, The FLV Body 以及其它 Tag 组成。因此加载速度极快。采用 FLV 格式封装的文件后缀为 .flv。



可以看到消息存储,如果使用redis的sortedSet进行存储还是比较方便的,接下来我们需要处理就是redis中过期消息的删除,因为无效的过期消息是没有价值的(所有的消息可以做持久化存储),redis中如果单一的key存储的消息过多,也会导致消息的慢查,和内存的使用量不断增大,这是我们不想看到的,这边因为是示例代码,所以简单地处理一下删除逻辑。long time = new Date().getTime();
try {
// redis中插入消息数据
jedisTemplate.zadd(V_UNIQUE_ROOM_ID, time, JSON.toJSONString(roomMessage));
// 按照概率性的去删除redis中过期的消息数据
if (probability()) {
deleteOverTimeCache(V_UNIQUE_ROOM_ID);
}
} catch (Exception e) {
log.error("message save error", e);
}
上面的伪代码probability()首先先做一个概率性的判断,例如我们做百分之一的随机判断,判断该次请求是否要进行消息的删除(请注意我们删除的逻辑是放在插入的逻辑之中的。如果每一次插入都需要判断是否要删除过期数据,会影响插入的性能)。如果通过概率性判断后,我们就优先判断某一个直播间的消息个数,如果消息个数还是比较少的话,则退出删除逻辑,如果超过消息阀值,则按照时间倒序删除已经过期的消息。说完了http短轮询消息的存储后,我们最后再简单地说一下客户端消息查询实现逻辑。客户端通过直播间id和时间戳两个字段来请求服务端以查询直播间消息,其中"时间戳"是每一次服务端返回的,这个时间戳是渐进式的,当下一次客户端来请求服务端的数据的时候,都会带来上次服务端返回的时间戳,伪代码如下:private void deleteOverTimeCache(String roomId) {
Long totalCount = jedisTemplate.zcard(roomId);
log.info("deleteOldTimeCache size is {}", totalCount);
if (totalCount < 600) {
return;
}
// 倒序删除过期数据
Set<Tuple> tuples = jedisTemplate.zrangeWithScores(roomId, -601, -1);
if (CollectionUtils.isNotEmpty(tuples)) {
for (Tuple tuple : tuples) {
// 这是第一个-600条的那个score
double score = tuple.getScore();
jedisTemplate.zremrangeByScore(roomId, 0d, score);
break;
}
}
}
上述三段比较完整地代码主要陈述了一个依赖http短轮询这种方式快速实现的直播间的能力,这种方式是比较粗糙的,不过却是一个很好的实现思路,目前我们线上部分业务也是根据这个轮询的思想进行部分模块的实现。这样实现的思路也有一个小坑,如果有采用该思路去实现的,可以尝试去规避。如果Android客户端断网的情况下,轮询的线程是不会停止的,例如是晚上8点整断网的,8点01分恢复网络的,当网络恢复的时候,第一次轮询就会导致服务端返回大量的消息,这边是需要进行处理的,否则会返回过多的消息,服务端也会出现慢查,客户端因为渲染过期的消息也会出现部分消息展示区间出现闪跳。例如公屏区可能会"发疯"般的出现各类消息,这些可以通过客户端和服务端的双方约定进行规避,例如客户端当出现网络问题的时候,在超过5秒以上,可以把时间戳置为0,要求服务端返回最新的直播间消息即可,中间丢失掉的消息,可以在业务返回内的进行丢弃。@Override
public RoomMessage queryRoomMessages(MessageMessageReq messageMessageReq) {
RoomMessage result = new RoomMessage();
long timestamp = messageMessageReq.getTimestamp();
Set<Tuple> tuples = ;
if (timestamp == 0) {
// 如果传递是0,说明这个客户端终端是第一次来轮询,我们只要返回一个最近最新的消息返回即可
tuples = jedisTemplate.zrevrangeWithScores(UNIQUE_ROOM_ID, 0, 0);
} else
// 加上一毫秒,返回后续的消息,每次返回5个,防止客户端因为低端手机原因,过多的消息渲染不出来
tuples = jedisTemplate.zrangeByScoreWithScores(UNIQUE_ROOM_ID, timestamp + 1, System.currentTimeMillis(), 0, 5);
}
List<EachRoomMessage> eachRoomMessages = new ArrayList<>();
long lastTimestamp = 0L;
if (!CollectionUtils.isEmpty(tuples)) {
for (Tuple tuple : tuples) {
//最后一次循环后,会把最后一条消息产生的时间戳,返回给客户端,这样下次客户端就可以拿着这个时间戳来进行查询
lastTimestamp = new Double(tuple.getScore()).longValue();
eachRoomMessages.add(JSON.parseObject(tuple.getElement(), EachRoomMessage.class));
}
}
result.setTimestamp(lastTimestamp);
result.setEachRoomMessages(eachRoomMessages);
return result;
}
四、小结
本文主要是想让大家对直播有一个初步的了解,了解直播基本的概念模型,一些基础的概念,后续我们会深入直播具体的模块的学习,进一步去了解直播的原理,也能够帮助我们更好的做好直播的业务。