玩转直播系列之从 0 到 1 构建简单直播系统(1)

时间:2024-02-16 14:55:45

一、前言

随着5G时代的到来,音视频行业也可能迎来一个行业的春天,直播则是新视频行业一直以来的一个重要的产品形态,从最初的秀场直播,游戏直播,到今年由于疫情,目前比较火的在线教育直播,带货直播等,各类新的直播形式则是越来越多的展示在大众面前。

作为技术开发的我们,今天我们一起简单的了解一下,如何快速搭建一套最简单的直播系统,简单地了解一下主流直播的架构模型。

二、推拉流模型

首先我们先看一张完整的直播推拉流的模型图,我们可以很清楚地看到直播宏观上的架构模型图。

2.1 直播三个主要模块

推流模块

推流模块主要分为音视频数据的采集,如果是秀场类直播,可以做美颜滤镜相关功能,用来提升直播的画面品质和用户体验,最后通过编码压缩,降低音视频数据的体积,最后通过流媒体传输协议将数据按照固定格式传递到RTMP服务器,这样整个推流端的工作就完成了。

RTMP服务端模块

传统意义上的RTMP服务器其实可能就只有转码的功能,将推流端传递过来的数据,转成flv等网络格式的数据文件,方便播放端的观看,不过目前云商都提供了一整套的解决方案,例如清晰度转码,内容健康检查,直播封面的生成,数据统计,录制回放等功能,这也是在RTMP服务器的基础上,进行的业务封装,这样才能提供一整套的解决方案。

播放端模块

播放端的逻辑就相对比较简单,简而言之就是获取拉流地址,进行音视频的播放,不过在实际开发的过程中,播放端的业务工作量和技术优化点都是最多的,如上图所示的首屏秒开,解码优化,切换直播间等功能,都是需要花费大量的精力,根据业务不断地去演进优化的。

三、搭建步骤

本入门直播简单教程主要分为如下几个模块:

搭建直播服务器;

使用OBS进行推流;

直播流如何观看;

直播间消息的实现。

3.1 搭建直播服务器

直播服务器实时地将推流端上传的视频流进行解析和编解码,以用于支持rtmp、hls或httpflv等直播协议的观看端进行观看。

当前市面上有很多开源的直播服务器解决方案,如 livego、srs 和 nginx-rtmp ,亦或者是目前比较主流的云解决方案,目前阿里云,七牛云,腾讯云等都提供了标准的成熟的解决方案,本篇文章旨在快速地搭建一个简单的直播,所以我们可以采用livego这个开放源代码的方式去搭建推拉流服务器,livego 使用纯 go 语言编写,性能高且跨平台,安装和使用非常简单,支持常用的传输协议、文件格式和编码格式,或者安装上文所示,直接在云商开播直播服务。

安装 livego 主要有三种方式:1)直接下载二进制可运行文件;2)从Docker启动;3)从源码编译。

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文件来进行循环推流,因此来源选择“媒体源”,名称用默认的就行,点击“确定”后,设置要播放的视频文件,然后点击“确定”即可。

然后,要解决的就是“往哪推”的问题,也就是需要有一个可用的推流地址才行。

前面我们已经搭建好了livego直播服务器,它提供了一个默认推流地址:rtmp://localhost:1935/live,一个标准的RTMP服务器的推流URL类似这种格式:rtmp://domain/AppName/StreamName,但是要想使用该推流地址,需要有授权的 channelkey 才行。

通过访问 http://localhost:8090/control/get?room=movie 就可以获取用于推流的 channelkey,如下所示,其中 data 字段就是此次获取到的 channelkey。

{
    "status": 200,
    "data": "rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk"
}

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

然后,选择“推流”选项。服务选择“自定义”,服务器设置为:rtmp://localhost:1935/live,串流密钥设置为前面获取到的 channelkey:rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk 。设置好后,点击“控件”的“开始推流”按钮,就可以进行推流了。

一般情况下,默认的输出配置就足以应付大多数场景了,但是要想获得更适合自己想要的的直播效果的话,可以在“输出”选项里设置“高级”输出模式,对此无需求的话可以直接跳过本部分。如下图所示,在高级输出设置界面,可以对串流、录像、音频和回放缓存进行配置,其中,最重要的就是对串流的设置。编码器软件可以选择 x264 和 QuickSync H.264,使用强大的 x264就可以。“重新缩放输出”可以设置输出的分辨率,默认使用原视频的分辨率。

比特率(码率)的含义是视频经过压缩编码后每秒的数据量的大小,单位是 Kbps,此处 K=1000。该值越大,每秒推送的视频数据流就越大,视频质量也越高,但是占用的带宽也更多,可以根据需要进行调整,一般秀场直播常用2000~2500Kbps就可,游戏直播可能对码率的要求比较高一点,可以做对应的调整。

直播推流时,可以使用多种码率控制方式,主要有CBR、ABR、VBR和CRF。

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://www.videolan.org/index.zh.html 下载对应的安装包安装即可。

点击“媒体”tab下的“打开网络串流”选项,然后网络地址设置为:rtmp://localhost:1935/live/movie ,点击“确定”后就可以看到OBS推流的视频啦。

使用VLC主要是方便开发同学进行观看测试,例如观看卡顿的问题,分辨率查看,时延问题的定位,VLC算是一个比较专业的工具,能够方便我们去定位问题和解决问题的

3.3.2 使用flv.js进行浏览器端的观看

flv.js是目标最为流行的html5的纯的javascript,也是目前国内比较主流的浏览器终端播放flv格式的解决方案,本小节我们就使用flv.js进行简单的播放,打开如下的网址:http://bilibili.github.io/flv.js/demo/


可以看到如图所示的,将如下streamURL的输入框输入http://127.0.0.1:7001/live/movie.flv 后,点击switch to MediaDataSource后Load即可播放如下的画面。

3.3.3 直播协议的简单介绍

到目前为止,我们已经成功的搭建了RTMP小框架,了解了整个推拉流的完整过程,接下来我们就需要对与RTMP协议几个强相关的直播网络传输协议有一个入门的了解。

国内常见的直播协议有几个:

RTMP

HLS

HTTP-FLV

HLS全称是 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。

流媒体协议 RTMP, HTTP-FLV, HLS 简单对比:

3.3.4 直播中的消息

在秀场直播系统中,如果说音视频功能的实现,是给直播装扮上了华丽的新装外表的话,那么直播系统中消息系统的实现,则是整个直播华丽新装下的灵魂,如何搭建高可用的直播间消息系统,也是每一个直播系统必须要解决的问题。

在设计秀场直播的消息系统之前,我们需要简单地梳理一下直播间的消息类型。

通知类消息例如送礼、弹幕、进场、榜单变化、等级变化等等消息。他们的特征是通知用户直播间的事件,营造直播间氛围,提升用户观看直播的体验。

功能类消息例如踢人、反垃圾审核、红包、PK消息等等。这类消息的特征是辅助直播业务开展,在流程上串联开播端、观看端、服务端三个角色。

我们可以从业务角度中,分析出直播间的各类消息虽然因为业务形态各式各样,最终呈现的形式也是多彩绚丽,但是我们可以从各类的消息展现形式可以分析出,消息从开发的角度,有如下几个特性,我们按照消息是否可丢弃,和实时性划分,我们可以把所有的业务消息归为如下几类:


在直播系统中,秀场直播,带货直播的直播间消息信令通信是比较偏多的,主要是因为业务性质所决定的,秀场直播和带货直播这两类直播的互动性相对比较强,玩法也比较多样,按照我们上图的分类,每一个业务的消息的可丢弃性和实时性要求都不一样,所以在开发消息系统的时候,也需要对消息进行优先级排序,对消息分发的实时性也要有业务性能考量。

刚刚针对直播间消息实时性和不可丢弃性这两个属性做了业务上相关的阐述,不过对于直播消息而言,第一要素是稳定性,消息如何准确稳定地分发到指定的直播间,也是我们需要考虑的问题之一,直播消息的分发实现,从总体上说可以分为两种实现方式,第一是依靠直播间的实时通讯(Instant Messaging),也就是我们常说的IM消息系统,第二个是依靠http短轮询,例如客户端每隔1秒来请求一次服务器,服务器返回这一秒内发生的增量消息信息,客户端获取到这些增量信息,再根据具体的消息业务类型,再进行相对业务的页面UI渲染,这样就可以了,从技术上说,一个是“推”模型,一个是“拉”模型,今天我们因为搭建一个简单的直播间消息系统,我们先用一个简单的"拉"模型进行简单的实现。

基本实现思路:客户端每隔一个极短的时间,例如1秒亦或者更短的时间,根据直播间的id来调用服务端的接口,轮询该直播间发生的消息,服务端这边我们使用redis的SortedSet的数据结构来存储消息,其中key是直播间的房间id,score是服务器接收到该消息事件生成的时间戳,value可以简单地直接存储该消息序列化后的字符串,这样可以按照时间顺序地去存储消息,并且配置过期消息的删除逻辑,整个消息的存储就可以简单地搭建起来。

消息存储用java的伪代码所示:

 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);
 }

可以看到消息存储,如果使用redis的sortedSet进行存储还是比较方便的,接下来我们需要处理就是redis中过期消息的删除,因为无效的过期消息是没有价值的(所有的消息可以做持久化存储),redis中如果单一的key存储的消息过多,也会导致消息的慢查,和内存的使用量不断增大,这是我们不想看到的,这边因为是示例代码,所以简单地处理一下删除逻辑。

    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;
            }
        }
    }

上面的伪代码probability()首先先做一个概率性的判断,例如我们做百分之一的随机判断,判断该次请求是否要进行消息的删除(请注意我们删除的逻辑是放在插入的逻辑之中的。如果每一次插入都需要判断是否要删除过期数据,会影响插入的性能)。如果通过概率性判断后,我们就优先判断某一个直播间的消息个数,如果消息个数还是比较少的话,则退出删除逻辑,如果超过消息阀值,则按照时间倒序删除已经过期的消息。

说完了http短轮询消息的存储后,我们最后再简单地说一下客户端消息查询实现逻辑。客户端通过直播间id和时间戳两个字段来请求服务端以查询直播间消息,其中"时间戳"是每一次服务端返回的,这个时间戳是渐进式的,当下一次客户端来请求服务端的数据的时候,都会带来上次服务端返回的时间戳,伪代码如下:

   @Override
 public RoomMessage queryRoomMessages(MessageMessageReq messageMessageReq) {
 
        RoomMessage result = new RoomMessage();
 
        long timestamp = messageMessageReq.getTimestamp();
 
        Set<Tuple> tuples = null;
        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;
    }

上述三段比较完整地代码主要陈述了一个依赖http短轮询这种方式快速实现的直播间的能力,这种方式是比较粗糙的,不过却是一个很好的实现思路,目前我们线上部分业务也是根据这个轮询的思想进行部分模块的实现。

这样实现的思路也有一个小坑,如果有采用该思路去实现的,可以尝试去规避。如果Android客户端断网的情况下,轮询的线程是不会停止的,例如是晚上8点整断网的,8点01分恢复网络的,当网络恢复的时候,第一次轮询就会导致服务端返回大量的消息,这边是需要进行处理的,否则会返回过多的消息,服务端也会出现慢查,客户端因为渲染过期的消息也会出现部分消息展示区间出现闪跳。例如公屏区可能会"发疯"般的出现各类消息,这些可以通过客户端和服务端的双方约定进行规避,例如客户端当出现网络问题的时候,在超过5秒以上,可以把时间戳置为0,要求服务端返回最新的直播间消息即可,中间丢失掉的消息,可以在业务返回内的进行丢弃。

四、小结

本文主要是想让大家对直播有一个初步的了解,了解直播基本的概念模型,一些基础的概念,后续我们会深入直播具体的模块的学习,进一步去了解直播的原理,也能够帮助我们更好的做好直播的业务。

作者:vivo 互联网服务器团队-Li Guolin