天天看點

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

本文主要介紹如何從0到1建構一個簡單的直播系統,簡單地了解一下主流直播的架構模型,幫助大家對直播系統有一個基礎的認識。

一、前言

随着5G時代的到來,音視訊行業也可能迎來一個行業的春天,直播則是新視訊行業一直以來的一個重要的産品形态,從最初的秀場直播,遊戲直播,到今年由于疫情,目前比較火的線上教育直播,帶貨直播等,各類新的直播形式則是越來越多的展示在大衆面前。

作為技術開發的我們,今天我們一起簡單的了解一下,如何快速搭建一套最簡單的直播系統,簡單地了解一下主流直播的架構模型。

二、推拉流模型

首先我們先看一張完整的直播推拉流的模型圖,我們可以很清楚地看到直播宏觀上的架構模型圖。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

2.1 直播三個主要子產品

推流子產品

推流子產品主要分為音視訊資料的采集,如果是秀場類直播,可以做美顔濾鏡相關功能,用來提升直播的畫面品質和使用者體驗,最後通過編碼壓縮,降低音視訊資料的體積,最後通過流媒體傳輸協定将資料按照固定格式傳遞到RTMP伺服器,這樣整個推流端的工作就完成了。

RTMP服務端子產品

傳統意義上的RTMP伺服器其實可能就隻有轉碼的功能,将推流端傳遞過來的資料,轉成flv等網絡格式的資料檔案,友善播放端的觀看,不過目前雲商都提供了一整套的解決方案,例如清晰度轉碼,内容健康檢查,直播封面的生成,資料統計,錄制回放等功能,這也是在RTMP伺服器的基礎上,進行的業務封裝,這樣才能提供一整套的解決方案。

播放端子產品

播放端的邏輯就相對比較簡單,簡而言之就是擷取拉流位址,進行音視訊的播放,不過在實際開發的過程中,播放端的業務工作量和技術優化點都是最多的,如上圖所示的首屏秒開,解碼優化,切換直播間等功能,都是需要花費大量的精力,根據業務不斷地去演進優化的。

三、搭建步驟

本入門直播簡單教程主要分為如下幾個子產品:

搭建直播伺服器;

使用OBS進行推流;

直播流如何觀看;

直播間消息的實作。

3.1 搭建直播伺服器

直播伺服器實時地将推流端上傳的視訊流進行解析和編解碼,以用于支援rtmp、hls或httpflv等直播協定的觀看端進行觀看。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

目前市面上有很多開源的直播伺服器解決方案,如 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檔案來進行循環推流,是以來源選擇“媒體源”,名稱用預設的就行,點選“确定”後,設定要播放的視訊檔案,然後點選“确定”即可。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

然後,要解決的就是“往哪推”的問題,也就是需要有一個可用的推流位址才行。

前面我們已經搭建好了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裡面進行相關設定就可以進行推流。首先點選“控件”的“設定”按鈕,進入設定面闆。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

然後,選擇“推流”選項。服務選擇“自定義”,伺服器設定為:rtmp://localhost:1935/live,串流密鑰設定為前面擷取到的 channelkey:rfBd56ti2SMtYvSgD5xAV0YU99zampta7Z7S575KLkIZ9PYk 。設定好後,點選“控件”的“開始推流”按鈕,就可以進行推流了。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

一般情況下,預設的輸出配置就足以應付大多數場景了,但是要想獲得更适合自己想要的的直播效果的話,可以在“輸出”選項裡設定“進階”輸出模式,對此無需求的話可以直接跳過本部分。如下圖所示,在進階輸出設定界面,可以對串流、錄像、音頻和回放緩存進行配置,其中,最重要的就是對串流的設定。編碼器軟體可以選擇 x264 和 QuickSync H.264,使用強大的 x264就可以。“重新縮放輸出”可以設定輸出的分辨率,預設使用原視訊的分辨率。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

比特率(碼率)的含義是視訊經過壓縮編碼後每秒的資料量的大小,機關是 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推流的視訊啦。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)
玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

使用VLC主要是友善開發同學進行觀看測試,例如觀看卡頓的問題,分辨率檢視,時延問題的定位,VLC算是一個比較專業的工具,能夠友善我們去定位問題和解決問題的

3.3.2 使用flv.js進行浏覽器端的觀看

flv.js是目标最為流行的html5的純的javascript,也是目前國内比較主流的浏覽器終端播放flv格式的解決方案,本小節我們就使用flv.js進行簡單的播放,打開如下的網址:http://bilibili.github.io/flv.js/demo/。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

可以看到如圖所示的,将如下streamURL的輸入框輸入http://127.0.0.1:7001/live/movie.flv 後,點選switch to MediaDataSource後Load即可播放如下的畫面。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

3.3.3 直播協定的簡單介紹

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

到目前為止,我們已經成功的搭建了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。

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

流媒體協定 RTMP, HTTP-FLV, HLS 簡單對比:

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

3.3.4 直播中的消息

在秀場直播系統中,如果說音視訊功能的實作,是給直播裝扮上了華麗的新裝外表的話,那麼直播系統中消息系統的實作,則是整個直播華麗新裝下的靈魂,如何搭建高可用的直播間消息系統,也是每一個直播系統必須要解決的問題。

在設計秀場直播的消息系統之前,我們需要簡單地梳理一下直播間的消息類型。

通知類消息例如送禮、彈幕、進場、榜單變化、等級變化等等消息。他們的特征是通知使用者直播間的事件,營造直播間氛圍,提升使用者觀看直播的體驗。

功能類消息例如踢人、反垃圾稽核、紅包、PK消息等等。這類消息的特征是輔助直播業務開展,在流程上串聯開播端、觀看端、服務端三個角色。

我們可以從業務角度中,分析出直播間的各類消息雖然因為業務形态各式各樣,最終呈現的形式也是多彩絢麗,但是我們可以從各類的消息展現形式可以分析出,消息從開發的角度,有如下幾個特性,我們按照消息是否可丢棄,和實時性劃分,我們可以把所有的業務消息歸為如下幾類:

玩轉直播系列之從 0 到 1 建構簡單直播系統(1)

在直播系統中,秀場直播,帶貨直播的直播間消息信令通信是比較偏多的,主要是因為業務性質所決定的,秀場直播和帶貨直播這兩類直播的互動性相對比較強,玩法也比較多樣,按照我們上圖的分類,每一個業務的消息的可丢棄性和實時性要求都不一樣,是以在開發消息系統的時候,也需要對消息進行優先級排序,對消息分發的實時性也要有業務性能考量。

剛剛針對直播間消息實時性和不可丢棄性這兩個屬性做了業務上相關的闡述,不過對于直播消息而言,第一要素是穩定性,消息如何準确穩定地分發到指定的直播間,也是我們需要考慮的問題之一,直播消息的分發實作,從總體上說可以分為兩種實作方式,第一是依靠直播間的實時通訊(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

分享 vivo 網際網路技術幹貨與沙龍活動,推薦最新行業動态與熱門會議。

繼續閱讀