はじめに

みなさんこんにちは。
今回は、自分と他人をつなぐコミュニケーションという勝手なテーマで、WebRTCを実装したいと思います。

WebRTCとは、wikipediaによると

World Wide Web Consortium (W3C)が提唱するリアルタイムコミュニケーション用のAPIの定義で、プラグイン無しでウェブブラウザ間のボイスチャット、ビデオチャット、ファイル共有ができる。

的な感じに説明されています。

え、WebRTCってことはWebで?
みたいな勝手なイメージがあるのは私だけでしょうか。
今回はアプリで実装してみたいと思います。
ちなみに、今回はビデオチャットの実装をしてみます。

本記事では、実装にあたり、手軽に使えるサービスを発見したので、ご紹介とともに実際に利用してみます。

SkyWay

NTTコミュニケーションさんが運用している、構築・運用サーバーを構築する必要もなくWebRTCを利用できるというサービスです。
ユーザー登録するだけで、自前のサーバーを用意することなく、ビデオ・音声通話・データ通信が手軽にできます。
手軽に使い始めることができますね。

SkyWay

お高いのかな−と公式を覗いてみると、

Community Editionは一切、料金がかかりません。 Enterprise Editionは商用サービスや大規模サービスに安心してご利用いただけます。

なんと無料で利用できる!
これはやるっきゃないということで使ってみました。
今回はiOSアプリにSkayWayを利用したWebRTCを実装してみようと思います。

実装

事前準備

SkyWayはユーザー登録が必須なので、各自ご登録をお願いします。
登録後、ダッシュボードにアクセスします。
アプリケーションを作成し、APIKey・ドメインを確認します。
コード内に設定する必要があるので、覚えておきましょう。

また、SkyWayを利用するにはSDKの導入が必要なので、cocoapodsで導入していきます。

pod 'SkyWay', '~> 1.0.1'

旧SkyWayのライブラリ'SkyWay-iOS-SDK'も存在しますが、そちらでは動作しません。ご注意ください

待ち画面

まずは、通話する前の待ち画面を作成します。
この画面で実装することは以下のとおりです。

  • サーバーへの接続
  • PeerIdの取得
  • 発信処理

サーバーへの接続

まずは必要となるオブジェクト等を用意します。
ダッシュボードで確認した、ApiKeyとドメインを利用しますので宣言しておきましょう。

let apiKey: String = //ApiKey
let domain: String = //domain

また、SkyWayで用意されている、以下のオブジェクトを利用するので宣言しておきましょう。必要となるオブジェクトは以下の通りです。

var peer: SKWPeer? //Peerオブジェクト
var remoteStream: SKWMediaStream? //相手のMediaStreamオブジェクト
var localStream: SKWMediaStream? //自分自身のMediaStreamオブジェクト
var mediaConnection: SKWMediaConnection? //MediaConnectionオブジェクト

では次に、Peerオブジェクトを作成します。
ApiKey・ドメインをoptionとして設定し作成します。

let option: SKWPeerOption = SKWPeerOption.init()
option.key = apiKey
option.domain = domain
option.debug = SKWDebugLevelEnum.DEBUG_LEVEL_ALL_LOGS

peer = SKWPeer(id: nil, options: option)

あとは、必要となるイベントコールバックを記述します。イベントコールバックは以下の通りです。
(5つのコールバックがありますが、今回は3つだけ利用しています。)

peer?.on(.PEER_EVENT_OPEN) { (obj: NSObject?) in
    //初期接続時
}
peer?.on(.PEER_EVENT_CALL) { (obj: NSObject?) in
    //発信したとき
}
peer?.on(.PEER_EVENT_DISCONNECTED) { (obj: NSObject?) in
    //サーバーと接続が切れたとき
}
peer?.on(.PEER_EVENT_CLOSE) { (obj: NSObject?) in
    //Peerと接続が切れたとき
}
peer?.on(.PEER_EVENT_ERROR) { (obj: NSObject?) in
    //エラーが発生したとき
}

重要なのはOPENイベントです。
サーバーに接続できないとどうしようもないので、OPENイベントが実行されているかを最初にご確認ください。
OPENイベントでは、localStreamへのレンダリングを行ったりします。サンプルは以下の通り。

peer?.on(.PEER_EVENT_OPEN) { (obj: NSObject?) in
    self.peerId = obj as! String

    let mediaConstraints = SKWMediaConstraints()
    mediaConstraints.maxWidth = 960
    mediaConstraints.maxHeight = 540
    mediaConstraints.cameraPosition = .CAMERA_POSITION_FRONT

    SKWNavigator.initialize(self.peer!)
    self.localStream = SKWNavigator.getUserMedia(mediaConstraints)
    self.localStream?.addVideoRenderer(self.localVideo!, track: 0)
}

そして、OPENイベントが実行されたことにより、他のPeerIdを取得できるようになります。
今回はボタン押下時に、以下のような処理で通話相手となるPeerIdを保存します。
今回のサンプルでは、特定の一人となるようなPeerIdの抽出処理としています

SKWPeer.listAllPeers(peer!)() { peerIds in
    let peers = peerIds!.map { $0 as! String }.filter { $0 != self.peerId! }
    guard !peers.isEmpty else {
        return
    }
    self.remotePeerId = peers.first!
}

これで、通話相手となるPeerIdを特定しました。
あとはCallするだけですね。
以下メソッドを実行します。
引数には、相手のPeerIdOPENイベントで取得した、localStreamです。
このlocalStreamを設定することで、相手にこちらのビデオデータが送信されます。

mediaConnection = self.peer!.call(withId: remotePeerId!, stream: localStream)

この画面の処理として、架電側はここまでの処理で良いですが、受電する処理を実装しなければいけませんね。
ですので、必要となるPEER_EVENT_CALLを設定しましょう。
今回はサンプルとして、通話画面に必要なる情報を渡し、そちらでレンダリングするようにしてみます。
必要となるのは以下のとおりです。

  • peerオブジェクト
  • 引数で受け取ったSKWMediaConnectionオブジェクト
  • (必要であればlocalStream)
peer?.on(.PEER_EVENT_CALL) { (obj: NSObject?) in
    let storyBoard = UIStoryboard(name: "RenderingVideo", bundle: nil)
    let viewController = storyBoard.instantiateInitialViewController() as! RenderingVideoViewController
    viewController.mediaConnection = obj as! SKWMediaConnection
    viewController.peer = self.peer
    viewController.localStream = self.localStream
    viewController.delegate = self
    self.present(viewController, animated: true) {
        viewController.mediaConnection?.answer(self.localStream)
    }
}

ストーリーボードはこんな感じにしました
スクリーンショット 2017-12-03 0.22.13.png
アウトレット接続はこんな感じ

@IBOutlet weak var localVideo: SKWVideo!
@IBOutlet weak var labRemotePeerId: UILabel!

通話画面

架電・受電したときに表示する画面の実装になります。
こちらでは、SKWMediaConnectionオブジェクトのイベントコールバックを実装する必要があります。
今回は使うのは以下の3つです。
(接続時の処理は省略するので、メインでつかうのは1つです)

connection.on(.MEDIACONNECTION_EVENT_STREAM) { (obj: NSObject?) in
    //相手のカメラ・マイク情報を受信したとき
}
connection.on(.MEDIACONNECTION_EVENT_CLOSE) { (obj: NSObject?) in
    //切断されたとき
}
connection.on(.MEDIACONNECTION_EVENT_ERROR) { (obj: NSObject?) in
    //エラーが発生したとき
}

重要なのは、MEDIACONNECTION_EVENT_STREAMになります。
相手のカメラ・マイク情報をイベントコールバックの引数で受け取ることができるので、それをレンダリングしてあげるだけです。
以下の処理でOKです。

self.remoteStream = obj as! SKWMediaStream
self.remoteStream?.addVideoRenderer(self.remoteVideo!, track: 0)

これまでの処理でビデオ通話ができると思います。
必要であれば、カメラの切り替えも簡単にできるのでやってみてください。

@IBAction func actionChangeCameraPosition(_ sender: Any) {
    let position = localStream?.getCameraPosition()
    if position == SKWCameraPositionEnum.CAMERA_POSITION_BACK {
        localStream?.setCameraPosition(.CAMERA_POSITION_FRONT)
    } else {
        localStream?.setCameraPosition(.CAMERA_POSITION_BACK)
    }
}

ストーリーボードはこんな感じにしました。
スクリーンショット 2017-12-03 0.22.27.png
アウトレット接続はこんな感じ

@IBOutlet weak var remoteVideo: SKWVideo!
@IBOutlet weak var localVideo: SKWVideo!
@IBOutlet weak var viewRemoteVideoIndicater: UIView!
@IBOutlet weak var viewLocalVideoIndicater: UIView!

おわりに

基本的には上記処理を抑えておけば実装できると思いますが、いかがだったでしょうか?
今回はpeerIdが複数存在しない場合を前提としてしまいましたが、複数人でも利用できるように調整すれば実用的なアプリになると思います。

また、ざっと紹介しましたが、公式チュートリアルで大抵のことがわかるので、そちらも合わせて御覧ください。
サンプルコードもあります。

これでWebRTC入門完了です。
みなさんもやってみてください。