Heroku蠻好用的, 也用了好幾年了, 拿來做prototype真是方便, 不過慚愧的是, 我還沒付過錢給他(真惡劣) 最近chatbot玩得比較多, 不想花錢租server, 所以就比較頻繁的用它, 說到這裡, 照例, 先來廣告一下:

新的Line叭寇(Barcode)小幫手 (轉含有條碼的圖片給它, 它會幫你解讀):

好了, 回歸正題, Heroku 雖然是一個Paas的服務, 但它彈性非常大, 透過不同的buildpack也可以支援不同的語言跟框架, 不像Google的GAE, 支援的平台就比較有限

Heroku本身也是跑在Linux上, 因此, 如果你需要額外的套件, 其實也是沒問題的, 舉個例子, 雖然我這個叭寇小幫手是用Go來寫的 但卻會用到ZBar這個讀取條碼的C程式庫, Heroku上當然沒裝, 所以建置Go時, 會因為找不到 程式庫而失敗

在Linux上, 我們可以用apt-get去安裝套件, 以zbar這例子是apt-get install libzbar-dev, 但在Heroku上又該怎麼裝呢?

還是要透過buildpack, 在command line下執行下面指令:

heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-apt

apt這個build pack是放在官方的Github上, 因為我們希望該需要的, 軟體在一開始就把它準備好了

除了加build pack外, 還是不夠的, 你的套件還是沒被安裝, 因此繼續加一個叫Aptfile的檔案, 內容是你需要安裝的套件

當你push你的程式到heroku上後, 它就會根據Aptfile去安裝相對應的套件了

不知不覺的突然就多出了兩天颱風假, 這颱風實在很威, 乒乒乓乓的, 不過, 也沒做什麼, 時間就快過完了, 現在才想到, 還是來寫點什麼, 嚴格說來這些東西並不完全是颱風假時弄的, 只是拖得有點久

起因是, 之前(很久…追朔到去年)想寫個App, 需要用到中華職棒賽程的資料, 拖了很久一直沒真的去做, 斷斷續續的, 最近才先把資料這部分補齊, 首先需求是:

  1. 當月之後的賽程資料, 但中華職棒並沒有API, 只有(很爛)的網頁, 因此資料勢必得從網頁去解析
  2. 由Client app直接去解析html, 會比較麻煩(如果網站更新了, 就要更新App), 不是那麼可行
  3. 不想花錢(或不想花太多錢)弄一個server, 更不用說還要考慮Scaling

而賽程表這樣的資料的特性則是:

  1. 球季是3~10月
  2. 資料內容除週一(休賽)外, 幾乎每天都會變, 但不會一兩個小時或幾分鐘就變一次
  3. 變動的內容可能是: 4. 比賽結束, 比數有更新 5. 延賽或停賽
  4. 一個月才幾十場比賽而已, 基本上不太需要有search或query的功能, 依據月份分類也就足夠了

因此我採用的做法是:

  1. 利用AWS lambda定時解析中華職棒網站的資料
  2. 資料以json格式存在github (使用Github api)
  3. Client透過CDN去要這些json的raw content

賽程解析

這部分我是用Go + Goquery來寫的, source code在這邊: cpblschedule, 這code沒啥整理過, 光解析這堆亂七八糟的html就夠頭痛囉, 就沒啥整理

我做成了一個package, 因此要使用可用下列指令先安裝:

go get -u github.com/julianshen/cpblschedule

裡面也很簡單就一個function而已, 因此要使用可以參考:

import "github.com/julianshen/cpblschedule"

func main() {
	matches, err := cpblschedule.ParseCPBLSchedule(year, month)
    ....
}

AWS Lambda

這邊就不介紹這東西是什麼了, 網路上文章一大堆, 基本上他是AWS一個severless的解決方案(這算廣告詞吧), 會使用這個的原因有二:

  1. 依我的用量應該是免費(事實證明, 其實還是要花點錢, 我忘了算網路傳輸的費用了, 不過這不多)
  2. 可以用Cloud watch排程觸發

不過有個小問題

**他不支援Go!!!!!**

而我上面那個解析的東東是go寫的, 那不就寫心酸的

所幸還有別的辦法,就是把程式編譯成執行檔, 然後用nodejs去包裝它, 不過這有點煩瑣, 所幸還有工具

apex, 這是讓你更簡單的去建立lambda function的工具, 而且他正好也可以幫你簡單做好上面所說的包裝

安裝及使用就看文件吧, 不特別說了, 但要如何用go寫一個lambda function handle呢?以下是範例:

import (
	"encoding/json"
	"github.com/apex/go-apex"
)
func main() {
	apex.HandleFunc(func(event json.RawMessage, ctx *apex.Context) (interface{}, error) {
		dosomething()
		return nil, nil
	})
}

Github as API source

資料既然變動不頻繁, 就用lambda定期產生然後把結果放到Github上就可了

Github API的Go的實做是Google放出來的go-github, 文件還蠻眼花撩亂的, 不過在這應用需要的API不多:

  1. client.Repositories.GetContents - 取得內容
  2. client.Repositories.CreateFile - 創立一個新檔
  3. client.Repositories.UpdateFile - 更新某個檔

之所以需要1的原因是要確認檔案是不是已經在repository裡面了, 如果沒有就用create, 如果有就拿SHA hash去更新內容

GetContents會把檔案內容一併給抓回來, 這可以用來在更新檔案前先比較, 如果不比較, 就算沒更動, API也會新增一個新的commit, 為了避免不要太誇張, 還是先比較一下好了

那之後client怎樣存取這些資料? 找到檔案, 選取raw就可以知道raw的url了, client每次就抓這個URL就好, 但為了避免過量地request湧到github, 因此透過一個CDN來存取可能會好一點

這時候就可以用RawGit, 這邊透過MaxCDN, 讓你可以去存取Github上的raw content, 而你的檔案的網址會是像這樣:

https://cdn.rawgit.com/user/repo/tag/file

大致上就這樣

最近直播蠻火紅的, 直播服務也不少, 連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來說比較簡單易用