[筆記] 中秋連假小實驗

Reading time ~7 minutes

最近直播蠻火紅的, 直播服務也不少, 連Facebook都做了直播功能, 最近也跟人聊了不少這方面的東西, 所以想說趁中秋連假來自己研究看看, 只是中秋節連假雖然多, 做的事情還真是不少, 看電影, 打球, 打電動, 看球的, 又碰上颱風天, 不過還是先搞個簡單的雛形唄

相關應用

市面上有不少關於直播的應用, 應該說, 簡直是氾濫, 而且每一種人氣似乎都是很旺, 還不是很了解在旺啥的, 現在可能也不少人認為當個網紅就可以一炮而紅之類的

  • Live.me
  • 小米直播
  • 17
  • 麥卡貝

這邊其實可以看出, 根本大同小異, 大多是賣肉…喔, 不是, 賣網紅, 後面故意舉了一個麥卡貝當例子, 它是稍微不一樣, 以賣直播節目像是運動比賽的轉播(嗚~~金鋒退休了). 而不是一般的UGC(User generated), 當然這邊也沒舉一般很流行的遊戲直播像是Twitch, 不過這類早已為大家所熟知了

不管是哪一種, 大部分的設計都是大同小異, 都是以單向直播為主, 輔以文字聊天室, 可以送個愛心或禮物, Facebook稍稍進階點, 會將直播過程錄製下來, 不只錄製節目, 還有過程中的互動, 不過大家都是蠻近似的

基本原理

如果照以上的功能設計, 簡單的可以畫成兩個部分, 一個是直播串流(Video Stream)的部分, 一個則是聊天室的部分, 大致上的後端可以以這兩個為核心

關於直播串流的技術部分, Facebook 曾分享了一篇關於他們做直播串流經驗的文章:

Under the hood: Broadcasting live video to millions

從這邊可以了解到, 串流需要能夠支撐到非常多人同時觀賞, 網紅可能數百到數千, 像是蘋果的發表會, 或是Google I/O, WWDC 這種會議則可能是數萬到數十萬, 所以服務的高並發, 高流量是可以預期的, 架構上也要能夠承受這樣的強度, 簡化的畫起來應該像是這樣:

graph LR; C[Client]--publish-->M[Media Server]; M--forward-->E(Edge Server); M--forward-->E2(Edge Server); M--forward-->E3(Edge Server); E-->V(Viewer); E-->V2(Viewer); E2-->V3(Viewer); E2-->V4(Viewer); E3-->CDN; CDN-->V5(Viewer); CDN-->V6(Viewer); CDN-->V7(Viewer);

Viewer不直接從Media Server取串流內容是考量到Media server通常要接收多個Client發佈的串流, 一個假設性的想法是, 對於UGC(User generated content), 主播應該遠少於觀眾, 假設就算一個服務可以吸引到百萬級別的觀眾, 同時線上的主播應該了不起是幾千個而已, 即便如此, Media server本身從Client接收發布的串流資料後, 可能還需要做轉碼(transcoding), 和轉送的動作, 尤其是轉碼是較為耗CPU資源的工作, 如果把主播跟觀眾放在同一個伺服器上, 除了影響品質外, 也會不方便擴充, 因此減少Media server上的"觀眾"(讓觀眾只是其他少數的edge servers), 便可以在觀眾增加時相對好擴充容量(增加edge的數量)

但大部分的狀況來說, 每個直播的觀眾不一定是非常大量, 在Facebook那篇文章內也有提到, 在小量觀眾的狀況下, 分流到多個edge的效率應該就沒那麼的好了, 反而這時候放在同一台server減少延遲會是更好的選擇

串流的通訊協定有不少, 像是RTP, RTSP, RTMP, HLS, WebRTC等等, Facebook那篇文章主要提到的是RTMP, HLS, 查了一下資料, 似乎這兩個也是目前比較主流做直播用的, 雖然WebRTC被討論的也蠻多的, 但似乎比較沒被應用在大量的直播, RTMP跟HLS都是可以透過HTTP來做傳輸(RTMP需要做封裝 - RTMPT), 讓他們具有穿越防火牆的優勢, 而HLS是以檔案為基礎的, 所以適合用一般的CDN來做快取, 在做大量的直播優勢較大, 缺點是延遲太長了, 但這兩者其實也是可以合併使用的, 在小群體時用RTMP, 等觀眾成長到一定數量實再導流到HLS去

Proof of concept 的初步想法與簡單的設計

幾個初步的想法

  1. 直播 = live stream + chat room
  2. 現在直播應用很多, 所以應該不少現成的open source解決方案可以套用, POC可以從這些東西下手, 不用重造輪子
  3. 需求: Client發布直播後, Viewer可以知道現在有誰在直播並觀看, 並可以透過訊息聊天

發布

sequenceDiagram participant Client participant Register participant MediaServer participant Viewer Client->>Register: I want to go live activate Register Register-->>Client: Here is your ID and token deactivate Register Client->>MediaServer: Publish(id,token) activate MediaServer MediaServer->>Register: token valid? activate Register Register-->>MediaServer: Yes deactivate Register MediaServer-->>Client: OK deactivate MediaServer Client->>Register: I am ready to live activate Register Register->>Viewer: Somebody is ready to live activate Viewer deactivate Register Viewer->Viewer: Update UI deactivate Viewer

觀賞直播

這部分就沒什麼特別的了, 當一般的chat room做就好

先看一下成果

這成品有點粗劣, 有點不好意思 :P

我publish client只實作iOS版本, 而Viewer只實作了Android版本(根本只各做一半嘛!!/翻桌), 後端用firebase處理資料的部分, 所以即時通知新的直播, 和聊天沒啥問題(但我聊天的UI還是沒刻 >"<)

相關解決方案

既然沒有重新做輪子, 自然用了不少Open source的解決方案來達成, 從Server到Android, iOS要找到相關可用的實在不難, 可以說這部份實在是太成熟了, 研究完後覺得Facebook那篇文章也只是一般般而已

Steaming server

RTMP相關的解決方案還算不少, 這邊列幾個

  1. Nginx RTMP Module - 架構在Nginx之上, 也算老牌了, 支援RTMP和HLS, 但看code base, 實在也沒啥在更新
  2. Mona server - 支援RTMP, HTTP(非HLS), Web socket等等
  3. Red5 Media Server - 支援RTMP, HLS, WebSocket, RTSP, 好像是要錢
  4. Simpe RTMP Server - 這是由中國的觀止雲這家開源出來的, 講"Simple"其實一點都不Simple, 輕量, 穩定(至少我試直播一晚上都還蠻順利的), 好擴展(支援forward to edge), 可RTMP轉HLS, 因此我最後選擇這個方案
SRS (Simple RTMP Server)

硬體

我沒看到文件有寫硬體需求, 但我用Digital Ocean 1GB RAM, 30GB SSD的Droplet跑, 單一個直播, 直播好幾個鐘頭, CPU都不超過5%, 所以應該足夠

安裝

安裝上相當簡單

  1. 從git上抓下來: git clone https://github.com/ossrs/srs.git
  2. 切換到相對應版本的branch(我是用2.0release) - git checkout 2.0release
  3. cd srs/trunk; ./configure ; make ;

建置好的執行檔會在srs/trunk/objs目錄下, 可以直接執行

設定

conf目錄下有很多不同的設定檔可以參考, 因為我要試RTMP, HLS所以我用的設定檔如下:

listen              1935;
max_connections     1000;
srs_log_tank        file;
srs_log_file        ./objs/srs.log;
http_api {
    enabled         on;
    listen          1985;
}
http_server {
    enabled         on;
    listen          8080;
    dir             ./objs/nginx/html;
}
stats {
    network         0;
    disk            sda sdb xvda xvdb;
}
vhost __defaultVhost__ {
    hls {
        enabled         on;
		hls_fragment    10;
        hls_window      60;
        hls_path        ./objs/nginx/html;
        hls_m3u8_file   [app]/[stream].m3u8;
        hls_ts_file     [app]/[stream]-[seq].ts;
    }
}

相對應的設定可以參考文件

執行

很簡單: srs -c my.conf 即可

iOS RTMP Publish

找到iOS支援RTMP publish的解決方案有幾種:

  1. VideoCore - 這個我還沒去試過, 不知道好不好用, 但似乎有支援Filter和Watermark, 感覺蠻威的
  2. lf.swift - 簡單, Swift做的, 這兩個是優點, 對我這個只看得懂Swift看不懂ObjC的, debug是比較方便, 但除此之外好像也沒啥特色了
  3. LiveVideoCoreSDK - 這文件不多, 我暫時就沒試了, 也支援濾鏡, 而且似乎這個作者也提供了Android版本, 只是好像沒支援Cocoapods或Cathage, 有空再來玩玩
  4. LFLiveKit - 我最後是選用這個, 簡單, 且"自帶美顏"(現在騙人是很基本的)
lf.swift

雖然文件上有提供cocoapods跟Carhtage的安裝方式, 但絕對不要用Carthage的那個, 第一原因是它Carthage支援似乎尚未搞定, 就算改點東西解決了它, 是可以安裝成功沒錯, 但會崩潰在XCGLogger, 似乎用framework含進來的方式會導致XCGLogger == nil, 這害我花了好多時間, 畢竟我是Carthage的愛用者, 後來轉用Cocoapods就沒事了

幾個需要加的部分:

AppDelegate.swift

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

        XCGLogger.defaultInstance().outputLogLevel = .Info
        XCGLogger.defaultInstance().xcodeColorsEnabled = true
        
        return true
    }

這兩行是設定logger要記錄的東西, 方便debug用

ViewController

override func viewDidLoad() {
	rtmpStream = RTMPStream(rtmpConnection: rtmpConnection)
    rtmpStream.syncOrientation = true
    rtmpStream.attachAudio(AVCaptureDevice.defaultDeviceWithMediaType(AVMediaTypeAudio))
        rtmpStream.attachCamera(DeviceUtil.deviceWithPosition(.Back))
    rtmpStream.addObserver(self, forKeyPath: "currentFPS", options: NSKeyValueObservingOptions.New, context: nil)
        
    rtmpStream.captureSettings = [
            "sessionPreset": AVCaptureSessionPreset1280x720,
            "continuousAutofocus": true,
            "continuousExposure": true,
        ]

    rtmpStream.videoSettings = [
            "width": 1280,
            "height": 720,
        ]
    lfView.attachStream(rtmpStream)

    view.addSubview(lfView)
}

直播的部分會跟他的LFView綁一起

LFLiveKit

後來選用LFLiveKit的原因不是因為他自帶美顏 :D, 他寫法跟lf.swift一樣簡單, 而且不一定要把preview加到UI裡面, 而且他的preview不用用特定的class,只要是UIView就可

範例直接貼他文件裡的就很清楚了:

// import LFLiveKit in [ProjectName]-Bridging-Header.h
import <LFLiveKit.h> 

//MARK: - Getters and Setters
lazy var session: LFLiveSession = {
    let audioConfiguration = LFLiveAudioConfiguration.defaultConfiguration()
    let videoConfiguration = LFLiveVideoConfiguration.defaultConfigurationForQuality(LFLiveVideoQuality.Low3, landscape: false)
    let session = LFLiveSession(audioConfiguration: audioConfiguration, videoConfiguration: videoConfiguration)

    session?.delegate = self
    session?.preView = self.view
    return session!
}()

//MARK: - Event
func startLive() -> Void { 
    let stream = LFLiveStreamInfo()
    stream.url = "your server rtmp url";
    session.startLive(stream)
}

func stopLive() -> Void {
    session.stopLive()
}

//MARK: - Callback
func liveSession(session: LFLiveSession?, debugInfo: LFLiveDebug?) 
func liveSession(session: LFLiveSession?, errorCode: LFLiveSocketErrorCode)
func liveSession(session: LFLiveSession?, liveStateDidChange state: LFLiveState)

Android stream player

寫到後面有點累了, 也有點懶了, 還剩下Android這塊沒寫, 這邊就只列出方案不寫細節了, 主要我測過:

  1. ExoPlayer: Google開源的Media player, 之前在做另一個東西時我有用過, 所以第一時間就想起這個, 不過, 原生的完全不支援RTMP, 不過可以參考這邊, 但我實際上用, RTMP完全沒成功過, 一直出現FLV parse的問題, 倒是HLS沒問題
  2. Ijkplayer - 這同時有Android和iOS版本, 是Bilibili開源的, 有點強大, 但基於ffmpeg, 不知道在license上會不會有風險, 使用起來還有點複雜, 但HLS, RTMP都是沒問題
  3. PLDroidPlayer - 七牛雲針對ijkplayer的再製品, 比較方便的是, 它有封裝出一個video view可以直接使用, 相較於ijkplayer來說比較簡單易用