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

vivo網際網路技術發表於2021-05-07

一、前言

隨著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

相關文章