SkyWay ~WebRTCアプリ開発~

昨今、リモートワークの需要が高まっています。
SkypeやZoom等のビデオ会議システムを利用する機会も多くなってきているのではないでしょうか。
そういったビデオ会議を実現するための技術として「WebRTC」という規格があります。

WebRTCとは

WebRTC(Web Real-Time Communication)とは、ブラウザやモバイルアプリケーションにシンプルなAPI経由でリアルタイム通信を提供する規格です。クライアント間で直接P2P通信することで、プラグイン等をインストールしないブラウザ間のビデオ通話、チャット、ファイル共有等が可能になります。

ただ、実際にWebRTCでP2P通信を行うとなると、お互いの情報を交換するためのシグナリングやネットワークを超えてデータをやりとりするためのSTUN/TURNなどのサーバを構築する必要がでてきます。

SkyWay – Enterprise Cloud WebRTC Platform

SkyWayは、NTT Communications社が提供するWebRTCアプリ開発者向けプラットフォームです。SkyWayを利用すると、WebRTCに必要なサーバ構築・運用をすることなく、手軽にビデオ通話や音声通話、データの共有が実現できます。

Webブラウザ、iOS、Android向けのSDKがそれぞれ提供されていますが、今回はiOSアプリの実装について紹介します。

開発前の準備

SkyWayアカウント取得・APIキー発行

こちらでSkyWayのアカウントを取得します。
アカウント取得後、コンソールでアプリケーションを作成し、APIキーを発行します。

プロジェクトの作成

今回は以下の環境でiOSプロジェクトを作成します。
・Xcode 10.3
・Swift 5

SDKをプロジェクトに追加

CocoaPodsでSkyWay SDKを追加します。

platform :ios,'9.0'
use_frameworks!

def install_pods
  pod 'SkyWay'
end

target 'SkyWayDemo' do
  install_pods
end

端末のカメラとマイクの利用を許可

info.plistにNSCameraUsageDescriptionNSMicrophoneUsageDescriptionを追加します。

実装

レイアウト

以下のコンポーネントを配置します。
・Remote Steam View … 相手の映像を表示するビュー (緑のエリア)
・Local Stream View … 自分の映像を表示するビュー (赤のエリア)
・Call Button … 通話開始・終了ボタン

Peerオブジェクトの生成

コンソールで取得したAPIキーとドメインでPeerオブジェクトを初期化します。

    override func viewDidLoad() {
        super.viewDidLoad()

        let option: SKWPeerOption = SKWPeerOption.init();
        option.key = "YOUR_API_KEY"
        option.domain = "YOUR_DOMAIN"

        if let peer = SKWPeer(options: option) {
            SKWNavigator.initialize(peer);
            self.setPeerCallbacks(peer)
            self.peer = peer
        } else {
            print("failed to initialize peer")
        }

Peerイベントコールバックの実装

Peerオブジェクトに必要なイベントのコールバックを実装します。

    func setPeerCallbacks(_ peer: SKWPeer) {
        // シグナリングサーバとの接続が確立されたときのイベント
        peer.on(SKWPeerEventEnum.PEER_EVENT_OPEN, callback: { (obj) -> Void in
            if let peerId = obj as? String {
                print("my peer id : \(peerId)")
            }
        })
        // リモートピアからのメディア接続が発生したときのイベント
        peer.on(SKWPeerEventEnum.PEER_EVENT_CALL, callback: { (obj) -> Void in
            if let connection = obj as? SKWMediaConnection {
                self.setMediaConnectionCallbacks(connection)
                self.mediaConnection = connection
                self.updateCallState()
                connection.answer(self.localStream)
            }
        })
        // エラーが発生したときのイベント
        peer.on(SKWPeerEventEnum.PEER_EVENT_ERROR, callback: { (obj) -> Void in
            if let error = obj as? SKWPeerError {
                print("\(error)")
            }
        })
    }

通話開始・終了アクション

通話開始/終了ボタンが押下されたときのアクションです。
通話中でない場合は、待機中のピア一覧から接続先を選択して通話開始、通話中の場合は、通話終了します。

    @IBAction func tapCall() {
        guard let peer = self.peer else {
            // no peer..
            return
        }

        if (!self.isCalling()) {
            peer.listAllPeers({ (peers) -> Void in
                guard let allPeerIds = peers as? [String] else {
                    // no peer ids..
                    return
                }
                let targetPeerIds = allPeerIds.filter({ (peerId) -> Bool in
                    return peerId != peer.identity
                })
                if targetPeerIds.count {
                    let alert = UIAlertController.init(title: nil, message: "接続先を選択してください。", preferredStyle: .alert)
                    for targetPeerId in targetPeerIds {
                        let action = UIAlertAction(title: targetPeerId, style: .default, handler: { (alert) in
                            self.call(targetPeerId)
                        })
                        alert.addAction(action)
                    }
                    alert.addAction(UIAlertAction(title: "キャンセル", style: .cancel, handler: nil))
                    self.present(alert, animated: true, completion: nil)
                } else {
                    let alert = UIAlertController.init(title: nil, message: "接続先が見つかりません。", preferredStyle: .alert)
                    alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
                    self.present(alert, animated: true, completion: nil)
                }
            })
        } else {
            self.endCall()
        }
    }

通話開始

    func call(_ targetPeerId: String) {
        let option = SKWCallOption()
        if let mediaConnection = self.peer?.call(withId: targetPeerId, stream: self.localStream, options: option) {
            self.setMediaConnectionCallbacks(mediaConnection)
            self.mediaConnection = mediaConnection
            self.updateCallState()
        } else {
            print("failed to call :\(targetPeerId)")
        }
    }

通話終了

    func endCall() {
        self.mediaConnection?.close()
    }

通話中かどうか

    func isCalling() -> Bool {
        return self.mediaConnection != nil
    }

通話開始・終了ボタンの更新

   func updateCallState() {
        if (!self.isCalling()) {
            self.callButton.setTitle("開始", for: .normal)
            self.callButton.setTitleColor(.green, for: .normal)
        } else {
            self.callButton.setTitle("終了", for: .normal)
            self.callButton.setTitleColor(.red, for: .normal)
        }
    }

MediaConnectionイベントコールバックの実装

通話開始時に取得したMediaConnectionに必要なイベントのコールバックを実装します。

    func setMediaConnectionCallbacks(_ mediaConnection: SKWMediaConnection) {
        // リモートメディアストリームを追加された時のイベント
        mediaConnection.on(SKWMediaConnectionEventEnum.MEDIACONNECTION_EVENT_STREAM, callback: { (obj) -> Void in
            if let mediaStream = obj as? SKWMediaStream {
                self.remoteStream = mediaStream
                DispatchQueue.main.async {
                    self.remoteStream?.addVideoRenderer(self.remoteStreamView, track: 0)
                }
            }
        })
        // メディアコネクションが閉じられた時のイベント
        mediaConnection.on(SKWMediaConnectionEventEnum.MEDIACONNECTION_EVENT_CLOSE, callback: { (obj) -> Void in
            if let _ = obj as? SKWMediaConnection {
                DispatchQueue.main.async {
                    self.remoteStream?.removeVideoRenderer(self.remoteStreamView, track: 0)
                    self.remoteStream = nil
                    self.mediaConnection = nil
                    self.updateCallState()
                }
            }
        })
        // エラーが発生したときのイベント
        mediaConnection.on(SKWMediaConnectionEventEnum.MEDIACONNECTION_EVENT_ERROR) { (obj) in
            if let error = obj as? SKWPeerError {
                print("\(error)")
            }
        }
    }

動作確認

こんな感じになりました。

iPhone 7
iPhone XS