Skyway をswiftで使ってみた

# はじめに

![スクリーンショット 2016-12-01 0.48.32.png](https://qiita-image-store.s3.amazonaws.com/0/96091/0adbfa79-2819-208b-1b48-acc8d448e569.png)

知人のお手伝いで通話機能を利用したアプリを作成することになり、NTTコミュニケーションが無料で提供しているSkywayを利用したいとのことだったので、調査のためにアプリを実装したので簡単にやったことをまとめておきたいと思います。

# Skywayについて
https://nttcom.github.io/skyway/info-mobile.html

- 基本無料。オプションは有料？
- JavaScript / Android / iOSでのAPI互換により、簡単にマルチデバイス対応が可能
- クライアントサイドの開発だけでOK、サーバサイドは準備不要
- 多人数参加のグループビデオチャットに対応している

# WebRTCとは？
- Webブラウザで簡単にボイスチャット、ビデオチャット、ファイル共有等ができる仕組みのこと

# 環境
Xcode 8.1
swift 3.0
OS 10.12.1

# 導入準備
## 開発者登録
SkyWay SDKを使用するために開発者登録を行います。

https://skyway.io/ds/registration/

1.必要な情報を入れる
![スクリーンショット 2016-11-30 0.54.11.png](https://qiita-image-store.s3.amazonaws.com/0/96091/56851233-edab-fcce-8b2c-810a6c7394f3.png)

2.ドメインを入力する
※確定していない場合は、とりあえずlocalhostでいいみたいです
但し、リリースするときは、セキュリティーを考えると削除しておいた方が良いです

![スクリーンショット 2016-11-30 0.57.10.png](https://qiita-image-store.s3.amazonaws.com/0/96091/900191e2-d09a-7ec5-db06-d4ca512b40d5.png)

3.利用規約に同意して登録ボタンを選択すると仮登録メールが届くので本登録を行う

4.APIキーが払い出されます。
![スクリーンショット 2016-12-01 0.44.23.png](https://qiita-image-store.s3.amazonaws.com/0/96091/a256a6ec-82ed-9ee4-671e-e1f4a31b2fe3.png)

## サンプルを動かしてみる
https://github.com/nttcom/SkyWay-iOS-Sample

1.cd <Projet>に移動して、pod insallする。
2.workspaceを起動する
3.入手したSkyWay.frameworkを追加する
4.API Keyのドメインを指定する

```DataConnectionViewController.m
// Enter your APIkey and Domain
// Please check this page. >> https://skyway.io/ds/
static NSString *const kAPIkey = @"yourAPIKEY";
static NSString *const kDomain = @"yourDomain";
```

```MediaConnectionViewController.m
// Enter your APIkey and Domain
// Please check this page. >> https://skyway.io/ds/
static NSString *const kAPIkey = @"yourAPIKEY";
static NSString *const kDomain = @"yourDomain";
```

5.ビルドしようとしたら、Linker Errorが出るので、Edit SchemeでPods-SkyWay-iOS-Sampleを追加して、DerivedDataを削除してビルドしなおしたらビルドが通りました。

6.起動すると以下の画面が表示される
![スクリーンショット 2016-11-30 23.52.45.png](https://qiita-image-store.s3.amazonaws.com/0/96091/f2090e24-cd89-19c8-5a31-dfee3d7ce8b1.png)

7.MediaConnectionボタンをタップして、テレビ電話の画面に遷移すると、以下のような画面が表示されます。

![スクリーンショット 2016-12-01 0.32.23.png](https://qiita-image-store.s3.amazonaws.com/0/96091/fb7252e2-ade7-18d9-5074-954ee8edbff8.png)

# 簡単なアプリを作成する













1.サンプルプロジェクトを作成する
2.入手したSkyWay.frameworkを追加する
3.objのFWなので、swiftで利用できるようにBridgingHeaderを追加する

- Bridging-Header.hを追加する
- Bridging-Header.hに#import <SkyWay/SKWPeer.h>を追加する
- Build Settings -> Swift Compiler -> Objective-C Bridging HeaderにBridging-Header.hを設定する

4.必要なライブラリを追加する

- AudioToolbox.framework
- AVFoundation.framework
- CoreMedia.framework
- CoreVideo.framework
- VideoToolbox.framework
- CoreGraphoics.framework
- Foundation.framework
- GLKit.framework
- SystemConfiguration.framework
- libc++.tbd
- libstdc++.6.0.9.tbd
- libsqlite3.tbd
- libicucore.tbd

5.Build Settingを変更する
- Build Settings -> Build Options -> Enable BitcodeをNoに設定する
- Build Settings -> Linking -> Other Linker Flagsを-ObjCに設定する

6.objective-cのサンプルを参考に管理クラスを作成する。
セッションスタートや、相手から接続要求が来た時などにcallbackでイベントなどが通知されるので、
delegateなどでController側に通知して、UIを更新する

```SkywayManager.swift
import UIKit
import AVFoundation

private var instance:SkywayManager? = nil

class SkywayManager: NSObject {

    //API Key
    static let apiKey:String = "<あなたのID>"
    
    //Domain
    static let domain:String = "<あなたの指定したdomain>"

    //Peer
    private var peer:SKWpeer? = nil
    private var localStream:SKWMediaStream? = nil
    
    private var remoteStream:SKWMediaStream? = nil
    private var mediaConnection:SKWMediaConnection? = nil
    private var conected:Bool = false
    private var peerId: String!
    private var connectStart:Bool = false;
    private var sessionDelegate:SkywaySessionDelegate?

    class func sharedManager() -> SkywayManager{
        
        if (instance == nil) {
            instance = SkywayManager()
            instance!.configInit()
        }
        
        return instance!
    }
    
    // MARK: Session
    func sessionStart(delegate:SkywaySessionDelegate) {
        sessionDelegate = nil
        sessionDelegate = delegate
        
        //Initialize SkyWay Peer
        let option:SKWPeerOption = SKWPeerOption.init()
        option.key = SkywayManager.apiKey
        option.domain = SkywayManager.domain

        //Peer初期化
        peer = SKWPeer(options: option)
        setCallbacks()
    }
    
    func getPeerId()->String {
        return peerId
    }

    // MARK: callbacks
    private func setCallbacks(){
        
        peer!.on(.PEER_EVENT_OPEN, callback: {obj in
            if obj is NSString{
                print("peer onopen")
                self.peerId = obj as! String!
                
                guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                    return
                }
                
                delegate.sessionStart()
            }
        })
        
        //PeerServerへの接続が確立すると発生
        peer!.on(.PEER_EVENT_CALL, callback: {obj in
            print("peer call")

            if let connection = obj as? SKWMediaConnection{
                self.mediaConnection = connection
                self.setMediaCallbacks(media: self.mediaConnection)
                self.mediaConnection!.answer(self.localStream!)
                
                
                guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                    return
                }
                
                delegate.connectSucces()
            }
        })
        
        peer!.on(.PEER_EVENT_CLOSE, callback: {obj in
            print("peer close")
            
            guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                return
            }

            delegate.connectEnd()
        })
        
        peer!.on(.PEER_EVENT_DISCONNECTED, callback: {obj in
            print("peer disconnected")
            
            guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                return
            }
            
            delegate.connectDisconnect()
        })
        
        peer!.on(.PEER_EVENT_ERROR, callback: {obj in
            print("peer error")
            
            guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                return
            }
            
            delegate.connectError()
        })
    }

    //接続リストを取得する
    public func getPeerList(delegate:SkywaySessionDelegate) {
        
        peer!.listAllPeers({obj in
            
            if let array:Array<String> = obj as? Array<String> {
                let peerList:NSMutableArray = NSMutableArray()
                
                for id in array {
                    if (id == self.peerId) {
                        continue;
                    }
                    
                    peerList.add(id)
                }
                
                delegate.getPeerList(newPeerList:peerList)
            }
        })
    }
    
    
    public func setWaitLocal(localView:SKWVideo, delegate:SkywaySessionDelegate) {
        sessionDelegate = nil
        sessionDelegate = delegate
        
        //初期化
        SKWNavigator.initialize(peer)
        let bounds = UIScreen.main.nativeBounds
        let constraints = SKWMediaConstraints()
        constraints.maxFrameRate = 10
        constraints.cameraMode = .CAMERA_MODE_ADJUSTABLE
        constraints.cameraPosition = .CAMERA_POSITION_FRONT
        constraints.minWidth = UInt(bounds.width)
        constraints.minHeight = UInt(bounds.height / 2)

        self.localStream = SKWNavigator.getUserMedia(constraints)

        
        //localViewに設定する
        localView.addSrc(self.localStream, track: 0)
        localView.setNeedsDisplay()
    }
    
    
    //通話開始要求
    public func connectStart(connectPeerId:String, delegate:SkywaySessionDelegate) {
        
        connectStart = true
        sessionDelegate = delegate
        mediaConnection = peer!.call(withId: connectPeerId, stream: localStream)

        self.setMediaCallbacks(media: self.mediaConnection)
    }
    
    public func closeMedia(localView:SKWVideo, remoteView:SKWVideo) {
        conected = false
        connectStart = false
        
        if mediaConnection != nil{
            mediaConnection!.close()
            mediaConnection!.on(.MEDIACONNECTION_EVENT_STREAM, callback: nil)
            mediaConnection!.on(.MEDIACONNECTION_EVENT_CLOSE, callback: nil)
            mediaConnection!.on(.MEDIACONNECTION_EVENT_ERROR, callback: nil)
            mediaConnection = nil
        }

        if remoteStream != nil {
            remoteView.removeSrc(remoteStream, track: 0)
            remoteStream!.close()
            remoteStream = nil
        }

        if localStream != nil {
            localView.removeSrc(localStream, track: 0)
            localStream!.close()
            localStream = nil
        }
        

        
        guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
            return
        }
        
        
        delegate.connectEnd()
    }
    

    public func sessionClose() {
        
        peer!.on(.PEER_EVENT_OPEN, callback: nil)
        peer!.on(.PEER_EVENT_CLOSE, callback: nil)
        peer!.on(.PEER_EVENT_CALL, callback: nil)
        peer!.on(.PEER_EVENT_DISCONNECTED, callback: nil)
        peer!.on(.PEER_EVENT_ERROR, callback: nil)
        
        SKWNavigator.terminate()
        
        peer!.destroy()
        peer = nil
    }

    func setRemoteView(remoteView:SKWVideo) {
        conected = true
        remoteView.addSrc(self.remoteStream, track: 0)
    }
     
    private func setMediaCallbacks(media: SKWMediaConnection?){
        
        guard let connection = media else {
            return
        }
        
        connection.on(.MEDIACONNECTION_EVENT_STREAM, callback: {obj in
            print("media stream")
            if let stream = obj as? SKWMediaStream{
                self.dispatch_async_global {
                    self.remoteStream = stream
                    
                    guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                        return
                    }
                    
                    delegate.remoteConnectSucces()
                }
            }
        })
        
        connection.on(.MEDIACONNECTION_EVENT_CLOSE, callback: {obj in
            print("media close")
            
            guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
                return
            }
            
            delegate.connectDisconnect()
        })
        
        connection.on(.MEDIACONNECTION_EVENT_ERROR, callback: {obj in
            print("media error")
        })
    }
}
```


7.着信を受け付ける画面を実装する。

動画のチャットアプリなどを実現する場合は、相手のPeerIDがわからないので
以下のような実装を行う必要があるが今回の調査では、待受画面のみを実装しました。

1.通話元で通話ボタンをタップする
2.接続要求APIで一度自分のIDをサーバーに渡す。
3.サーバーから指定のユーザーにPUSH等で通知して、IDを通知して、
UI等で通信が来ているような形にして、接続を許可してから、相手とConnect状態にする。
4.Remote/localの動画を表示する

かける部分については、今回は説明しません。サンプルアプリからかけてみてください

```ConnectViewController.swift
class ConnectViewController: UIViewController {

    //相手のVideoView
    @IBOutlet weak var connectRemoteVideo: SKWVideo!

    //jibunnnoVideoView
    @IBOutlet weak var connectLocalVideo: SKWVideo!

    //着信中のイメージ
    @IBOutlet weak var connectingView: UIView!

    fileprivate var connectWaitFlag:Bool = true
     
    override func viewDidLoad() {
        super.viewDidLoad()

        //NavigationBarは非表示にする
        self.navigationController?.setNavigationBarHidden(true, animated: false)

        //セッションを開始する
        SkywayManager.sharedManager().sessionStart(delegate: self)
    }

    @IBAction func tapConnectEnd(_ sender: Any) {
        
        //通話中の場合は通話を終了する
        if (!connectWaitFlag) {
        　　SkywayManager.sharedManager().closeMedia(localView: connectLocalVideo, remoteView: connectRemoteVideo)
        }
    }
}

extension ConnectViewController:SkywaySessionDelegate {
    
    func sessionStart() {
        //Skywayとの接続が完了した場合に何か処理したい場合
    }

    func connectSucces() {
    	 //着信中のUIを更新する
    }

    func remoteConnectSucces() {
        //相手と接続できたのでRemoteのViewを更新する
        SkywayManager.sharedManager().setRemoteView(remoteView: connectRemoteVideo)
    }

    func connectEnd() {
        //skywayとのConnectを切断する
        SkywayManager.sharedManager().sessionClose()
    }

    func connectDisconnect() {
		//切断された場合のUI更新
		//通話が終了しましたなど
    }
    
    func connectError() {
    
    }
    
    func getPeerList(newPeerList: NSMutableArray) {
        //処理なし
    }
}

```
# 問題点
## Videoが画面全体もしくは、２分割で相手/自分と縦並びで表示しようとしたが、指定したサイズで表示できない

```
let bounds = UIScreen.main.nativeBounds
let constraints = SKWMediaConstraints()
constraints.maxFrameRate = 10
constraints.cameraMode = .CAMERA_MODE_ADJUSTABLE
constraints.cameraPosition = .CAMERA_POSITION_FRONT
constraints.minWidth = UInt(bounds.width)
constraints.minHeight = UInt(bounds.height / 2)
self.localStream = SKWNavigator.getUserMedia(constraints)
```

上記のように画面サイズで指定してみましたが、適用されないですね。以下のドキュメントにも
https://nttcom.github.io/skyway/docs/#iOS

max (Width x Height): 640x480, 352x288
min (Width x Height): 192x144, 352x288, 512x384, 640x480

とあるのでダメそう。改善してほしいな

## 音声を内蔵スピーカーで出力したい
Skywayの提供するAPIにて音声出力周りのものがなかったのでどうしようと思っていたが、結論から言うとAudioSessionを利用すればいけました

MEDIACONNECTION_EVENT_STREAMを受け取ったあとにdelayを置いてから
以下のようにすることで内蔵スピーカーから音が出ました。切断した場合は、setActiveをfalseにすれば良いかと思います

```
AVAudioSession.sharedInstance().setCategory(AVAudioSessionModeVoiceChat)
AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSessionPortOverride.speaker)
AVAudioSession.sharedInstance().setActive(true)
```

# 所感
サンプルを参考にswiftで組んでみましたが、簡単に無料通話アプリが実現できました。
サービスに適用できるかはまだ検討中ですが、他のサービスも検証して最終的に導入するかは判断したいと思います

# 参考サイト
https://nttcom.github.io/skyway/documentation.html
https://github.com/nttcom/SkyWay-iOS-Sample