はじめに

こんにちは、インキュベーション事業部所属 iOSエンジニアの葛山です。

タイトル通り、WebRTCを用いた生放送iOSアプリを開発し本日リリースしましたので、使った技術や苦労した箇所などをプロジェクト振り返り的な感じでまとめられればと思います。
WebRTCを触ってみた的な紹介記事は多いと思うのですがiOSアプリでがっつり採用した事例は少ないと思うのでそちらも踏み込んで書ければなと。


先日リリースした101 LIVEです!

また、これからiOSアプリを新規開発しようとする人たちにとっても参考になる記事になれば良いなと思います。

WebRTCとは?

WebRTCはWeb RealTime Communicationの略で、ブラウザが直接通信し、映像・音声といったデータをリアルタイムにやりとりすることを可能にした技術です。
通信の特徴としてP2P方式でユーザー間で直接通信を行うためレスポンス性が高い通信を実現することができます。
(通常はブラウザとサーバーの間で通信)

このようにリアルタイム性のある特徴を備えているため、ビデオチャットなどといったWebアプリケーションで良く採用されます。
AppRTCで実際にどんなものか体験できます。

WebRTCの公式ページですがこちらはGoogleによって運営されています。

対応ブラウザ・プラットフォーム

  • iOS
  • Android
  • Chrome
  • Firefox
  • Opera

日々開発が進み、対応ブラウザなどが増えている状況です。

101 LiVE!の特徴

なぜこのアプリでWebRTCを用いることにしたか説明するために当アプリの特徴を簡単にまとめます。

  • 1:1のビデオ通話の様子を配信
  • アプリ登録時にTwitter連携が必須で、放送中にツイートやいいね、RTなどができる
  • 視聴者はその様子に対してコメントやアイテム(有料)を送れる

といった機能を備えていることから極力遅延なくリアルタイムでユーザーが交流できる仕組みが必要と考え、WebRTCの採用を決めました。

多くの生放送サービスだと配信者と視聴者との間にラグが5〜30秒程度あるものが多いと思いますが、101 LIVE!だと1秒未満で生放送が実現できています。

一般的な生放送サービスに遅延が発生しているのはHLS、RTMPといったストリーミングのプロトコルを採用しているためです。
大体はクライアントからの動画をファイルにし、CDNを通して視聴者に対して配信を行っています。

もちろんこちらを採用する強みは視聴者数を気にせず配信できるなど数多くあるのですが、
101 LIVE!はあくまでも二人の配信者が双方向でビデオ通話(やりとり)している様子を視聴者に見せるサービスなので、二人の配信者間で遅延が生じるのはかなりきついなと考えました。

そうしたこともありWebRTCを採用しました。

WebRTCの技術ですが自分たちで自前でサーバーやら何やら全てを実装することも検討したのですが、サーバーエンジニアとも相談し時間的コスト安定性など様々なことから一旦、内側を支える仕組みとしてはどこかのベンダーを利用しようということになり今回のサービスを実現できそうなベンダーを調査しました。

各ベンダーのサンプルコードなどを動かし調査を進めていった結果、とあるベンダーを採用することに決めました。こちらはまたサーバー編の記事で。

利用した技術・フレームワーク

101 LIVE!のクライアントを開発するにあたり利用した技術・フレームワークなどを紹介したいと思います。
いずれも有名で使いやすいものが多く、どのプロジェクトでも採用されやすいものです。

まず言語ですがSwift4で書きました。
当初はSwift3で書いていたのですが、プロジェクトの進行中にSwift4が公開されたのでどうせならと、Xcode9・Swift4でiOS11対応をしっかりする形になりました。

UI周り

  • IGListKit

Instagramが公開したオープンソースで、フィードをリファクタリングする過程で社内のエンジニアによって作られたものです。
101 LIVE!ではUICollectionViewを使っている全フィードでこちらを用いています。

Instagramのフィードを書き直したことの知見
にあるように早くて、クラッシュがなく、アニメーションの付いた更新をもつフィードが欲しければ採用して良いと思います。
コードの見通しも良くなります。

  • SnapKit

AutoLayoutをコードで簡単に扱えるライブラリです。普段はPureLayoutを使っていたのですが、今回こちらを採用してみました。
NSLayoutConstraintで記述するのは複雑になりがちなので、コードでAutoLayoutを書くときはライブラリの採用がおすすめです。

UIの変更などの調整を入れる際も楽にできます。

  • RxSwift

全体的なアプリの設計をRxSwiftで書いているのではなくAVPlayerやUIKeyboardNotification、UITextViewといったUI周りで部分的に採用しています。


extension Reactive where Base: AVAsset {
    var playable: Observable {
        let keys = ["playable"]
        return Observable.create { observer in
            self.base.loadValuesAsynchronously(forKeys: keys, completionHandler: {
                observer.onNext(self.base.isPlayable)
            })
            return Disposables.create()
        }
    }
}

extension Reactive where Base: AVPlayerLayer {
    var ready: Observable {
        let key = "readyForDisplay"
        return observe(AnyObject.self, key, options: [], retainSelf: false)
            .map { _ in
                Void()
            }
            .filter {
                self.base.isReadyForDisplay == true
        }
    }
}

ネットワーク

  • APIKit

リクエストとレスポンスを一箇所に定義できるため可読性が高いです。
昔はAlamofireを使っていたのですが最近はAPIクライアントを実装するときはこちらのライブラリを必ず採用しています。

作成者がメンテナンスなども素早く行っているため最新のSwiftバージョンに追随できない・・などといったこともないです。

画像キャッシュ

  • KingFisher

ピュアSwiftで書かれた画像通信・キャッシュライブラリです。
SDWebImageを参考にして書かれているライブラリのため、WebPなど一部の画像フォーマットに対応しているか否かを除いて機能的に大きな差異はありません。
個人的にSwiftでアプリ開発をしていて特に理由などがなければKingFisherの採用で良いかなと考えています。


extension UIImageView {
    func download(image url: String) {
        guard let imageURL = URL(string: url) else {
            return
        }
        self.kf.setImage(with: imageURL, placeholder: nil, options: [.transition(.fade(0.3))])
    }
    func download(authorImage url: String) {
        guard let imageURL = URL(string: url) else {
            return
        }
        self.kf.setImage(with: imageURL, placeholder: nil, options: [.transition(.fade(0.3))])
    }
}

当アプリではこのようなExtensionを作り使いやすくしています。

データ管理

  • Realm

今までのアプリではCoreDataをずっと使っていたのですが、心機一転Realmに乗り換えました。
学習コストがあまり高くなくすんなりプロジェクトに導入できたので、これからアプリ開発をする方もRealmはオススメです。

キーチェーン

  • KeychainAccess

キーチェーンのAPIを非常にシンプルに使うことができます。

Twitter

  • TwitterCore
  • TwitterKit

当アプリはTwitterログインが必須なので上記のフレームワークを採用しています。
フォロー、いいね、RTといった各APIの定義に関してはAPIKitのインターフェイスを参考に実装しどのプロジェクトでも今後使いまわせるように作りました。


extension TwitterAPI {
    struct LikeTweet: TwitterTweetRequestType {
        var tweetID: String!
        
        init(tweetID: String) {
            self.tweetID = tweetID
        }
        var path: String {
            return "favorites/create.json"
        }
        var method: String {
            return "POST"
        }
        var parameters: [String: Any] {
            return ["id" : tweetID]
        }
        func processTweet(_ tweet: TweetObject) {
        }
    }
    struct UnLikeTweet: TwitterTweetRequestType {
        var tweetID: String!
        
        init(tweetID: String) {
            self.tweetID = tweetID
        }
        var path: String {
            return "favorites/destroy.json"
        }
        var method: String {
            return "POST"
        }
        var parameters: [String: Any] {
            return ["id" : tweetID]
        }
        func processTweet(_ tweet: Tweet) {
        }
    }
}

Twitter APIについてはこちら
Twitter API Reference

ただ今回がっつりTwitter APIを使って感じたのが、使いにくい・・・!の一言です。
API limitに気をつけて実装するのも一つなのですが、いいねやアンリツイートをした後に返ってくるレスポンスのcountが更新されてなかったり、色々難儀でした。笑

リソース管理

  • R.swift

Storyboard, Nib, ReuseIdentifier, UIImageといったリソース周りをプロパティとしてアクセスすることで記述できます。
最近良く流行っている気がします。弊社の他事業部でもよく活用されています。

  • オンデマンドリソース

こちら今回始めて利用したのですがかなり便利でした。Appleが公式に用意している仕組みです。
On-Demand Resources Guide
アプリケーションが必要なタイミングでAppStoreからリソースをダウンロードすることでアプリケーションのインストールサイズを抑えることができます。

今回放送中にアイテムをプレゼントすることができるのですが、一つ一つのアイテムアニメーションがリッチなためサイズが大きいです。
フォーマットは上記で既に書きましたがAnimation PNGを採用しています。

普通にXcodeプロジェクトに入れると計100MBサイズ以上になり流石にユーザーのAppStoreでのインストール率が下がると判断しました。
とは言えこれをDLするためのAPIを新規で作ってもらうのもどうかなと考えていたところ、こちらの仕組みにありつきました。

以下のコードで、

  • リソースがデバイスにない状態にデバイスにリソースをDLする

を実現しています。

仕組み上、オペレーションシステムがリソースのダウンロードを開始するのは該当するリソースがデバイス上にない場合だけですので、不用意なDLを毎回することはありません。


final class ResourceManager: NSObject {
    static let sharedInstance = ResourceManager()
    static let giftResourceTag = "Gift"
    fileprivate var resourceAvailable = false
    fileprivate var currentResourceRequest: NSBundleResourceRequest?
    
    fileprivate func resourceRequest()-> NSBundleResourceRequest {
        let resourceTag = NSSet(array: [ResourceManager.giftResourceTag])
        let resourceRequest = NSBundleResourceRequest(tags: resourceTag as! Set)
        resourceRequest.loadingPriority = loadingPriority
        currentResourceRequest = resourceRequest
        return resourceRequest
    }
    
    override init() {
        super.init()
    }
    func downloadResourceIfNecessary() {
        if !resourceAvailable {
            let req = resourceRequest()
            req.conditionallyBeginAccessingResources { (resourceAvailable) in
                if resourceAvailable {
                    self.resourceAvailable = true
                } else {
                    req.beginAccessingResources(completionHandler: { (error) in
                        if error == nil {
                            self.resourceAvailable = true
                        }
                    })
                }
            }
        }
    }
}

非常に簡単に実現できるので、毎回は使わないチュートリアルのリソースだったり、アプリケーションサイズに影響をあたえるような重いリソースはこちらの機能を使うことを検討してみてもいいかもしれません。サーバーエンジニアのリソース0で実現できます。

ライブラリ管理

  • Carthage
  • CocoaPods

こちらに関しては特に説明は要らないですね。
基本的にCarthageでライブラリを管理しており対応していないものに関してCocoaPodsを使うようにしています。

CI

  • Bitrise

iOSをメインターゲットにしたモバイル向けのCIサービスです。
Web上の管理画面が非常に使いやすくとりあえずCI入門したい!って方にもおすすめのサービスです。

Analytics

  • Google Analytics

こちらに関しては弊社の小菅が執筆した記事が非常に参考になるので是非ご一読ください!
iOSアプリでもGoogleAnalyticsでがっつり計測する実装方法【総集編】

デザイナーとの連携

  • Invision
  • Sketch
  • Abstract

Invisionで基本的な画面遷移を把握しつつ、Sketchを見ながら画面の作り込みをしていきました。
Abstractはデザイナー版GitHubのようなもので、developとmasterで使い分けることで、どれが確定したデザインなのかが一目でわかるためスムーズに実装することができます。

サーバーエンジニアとの連携

  • GitHub

APIのバグやレスポンスの調整など細かいやり取りはGitHubのIssue上で行いました。

  • Swagger

APIドキュメントジェネレーターですね。
同様の機能を備えたものとしてはAPI BluePrintなどが有名なのですが、Open API Initiativeで基準APIフォーマットとして採用されたことが強みの一つです。
サーバーエンジニアの方に提案されて今回初めて使用したのですが非常にAPI開発がスムーズにできました。


定義が見やすく各API毎のレスポンスが整理されています。クライアントエンジニアからしたら非常に便利で開発していて気持ちよく使えたのですが多少の学習コストが必要なため、導入するかどうかは担当サーバーエンジニアの方と相談しましょう。

苦労した話

そもそもWebRTCとは何かをがっつり理解するのに最初苦労しました。
なんとなくは知っていたりサンプルを動かしたりしたことはあったものの、専門用語や仕組みなどの理解に少し時間を使いました。

またアプリケーションでの話だと、このアプリは一人の配信者(MC)がもう一人の配信者(Guest)を招待リンクを発行することで招待し、その二人のやりとりの様子をその他大勢が視聴するという構図なのですが、二人の配信者の状態管理が非常に大変でした。
MCがWebSocketから切断された場合は、放送終了をリアルタイムでその他に伝えるなど、この辺りのデバッグも端末が複数台必要なため手間でした。

後はアイテムアニメーションの同期性を担保することです。
一つのアイテムアニメーションが画面に表示されている間、別のユーザーが送ったアイテムアニメーションは表示せず待機というのを延々と繰り返すのですが、あえてAPIを経由せず、クライアントのみで内部的にMessageを送り状態を管理することで全ユーザーの同期性がほぼ担保されました。

終わりに

振り返ってみると、今回は今まで触れてこなかった生放送という領域での開発に加えて、開発技術や開発手法も新しいものを試したプロジェクトだったのかなと思います。

さて101 LIVE!ですが、現在はまだiOSアプリのみの提供になっています。
ですがせっかくWebRTCという技術を採用している以上、Android・Web版もリリースし様々なプラットフォームで配信でき、一つの場所に集うことができる世界を目指しています。

さらにiOS11でsafari対応ということもあり、ますますWebRTCの分野は盛り上がってきています。
是非、WebRTCや生放送という分野に興味のある方は一度話だけでも聞きに来てみてください。

また今回ブログに書いた101 LIVE!の話を今月弊社で行うSwift勉強会で話させていただく予定なので、こちらも興味ある方はよかったら是非いらしてください!
Nagisa.swift

今回の記事執筆にあたり参考にしたもの