[webRTC for ios vol2] 繋げてみる

※ 2017/4/6 編集 Answer部分

webRTC.framework を作ってプロジェクトに入れるまでは前回
http://qiita.com/nakadoribooks/items/3af18222760c9b036ec6

Trickle ICE は次回
http://qiita.com/nakadoribooks/items/a0f19dd96b0763c35ed2

実際につなげてみる。
iOS - iOS

シグナリングは[wamp](http://wamp-proto.org/)でやる。
websocket上でアレするプロトコル。

## 環境
- xcode 8.2.1
- iOS 10.2.1

## 成果物

github
https://github.com/nakadoribooks/webrtc-ios/releases/tag/v0.0.2

<img src="https://j.gifs.com/2Rl1NK.gif" />

# 流れ

- wampサーバーを作って、Heroku に置く
- ios側を実装を作る 

# wampサーバーを作って、herokuに置く

wamp.rt を使う。
https://github.com/Orange-OpenSource/wamp.rt

コードはこちら。index.js と package.json の二つだけ
https://github.com/nakadoribooks/webrtc-server

## 設定

```json:package.json
{
  "name": "server",
  "version": "1.0.0",
  "description": "webrtc-web",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "wamp.rt": "^0.2.0"
  },
  "dependencies": {
    "wamp.rt": "^0.2.0"
  }
}
```

## 実装

```js:index.js
WAMPRT_TRACE = true;

var Router = require('wamp.rt');
var program = require('commander');

program
    .option('-p, --port <port>', 'Server IP port', parseInt,process.env.PORT || 8000);


var app = new Router(
    { port: program.port}
);
```

## デプロイ

http://qiita.com/yoh-nak/items/80e51197410c7f956ccd

```sh:
git push heroku master
```

以上。

# ios側を実装を作る 

## 設定

### ライブラリ取り込み

wampライブラリ は [swamp](https://github.com/iscriptology/swamp)
名前がかっこいい。

```Podfile:Podfile
platform :ios,'9.0'
use_frameworks!
target 'webrtcExample' do
    pod 'Swamp', '0.2.0'
end
```


### info.plist

パーミッション追加

```xml:info.plist
<key>NSCameraUsageDescription</key>
<string>カメラ使います</string>
<key>NSMicrophoneUsageDescription</key>
<string>マイク使います</string>
```

## まずは大枠。

- ViewController.swift (アプリケーション)
- WebRTC.swift 
- Wamp.swift

アプリケーションから WebRTC と Wamp を使って作っていきます。

### WebRTC のインタフェース

```swift:WebRTC.swift
func setup(){}
func localView()->UIView
func remoteView()->UIView
func connect(iceServerUrlList:[String], onCreatedLocalSdp:@escaping ((_ localSdp:NSDictionary)->()), didReceiveRemoteStream:@escaping (()->()))
// Answer の受け取り
func receiveAnswer(remoteSdp:NSDictionary)
// Offerの受け取り
func receiveOffer(remoteSdp:NSDictionary)
// Offerを作る
func createOffer()
```

### Wamp のインタフェース

```swift:Wamp.swift
// wampサーバーへ接続してイベント監視
func connect(onConnected:@escaping (()->()), onReceiveAnswer:@escaping ((_ sdp:NSDictionary)->()), onReceiveOffer:@escaping ((_ sdp:NSDictionary)->()))

// Offerの送信
func publishOffer(sdp:NSDictionary)

// Answerの送信
func publishAnswer(sdp:NSDictionary)
```

### アプリケーションから使う

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    
    private dynamic func tapOffer(){
        typeOffer = true
        webRTC.createOffer()
    }
    
    private func connect(){
        
        webRTC.connect(iceServerUrlList: ["stun:stun.l.google.com:19302"], onCreatedLocalSdp: { (localSdp) in
            
            if self.typeOffer{
                self.wamp.publishOffer(sdp: localSdp)
            }else{
                self.wamp.publishAnswer(sdp: localSdp)
            }
            
        }, didReceiveRemoteStream: { () in
            self.stateWebrtcConnected()
        })
        
        wamp.connect(onConnected: {
            
            self.stateConnected()
            
        }, onReceiveAnswer: { (answerSdp) in
                       
            self.webRTC.receiveAnswer(remoteSdp: answerSdp)
            
        }, onReceiveOffer: { (offerSdp) in
            
            if self.typeOffer{
                return;
            }
                        
            self.stateReceivedOffer()
            self.webRTC.receiveOffer(remoteSdp: offerSdp)
            
        })

    }

}

```

## 実装詳細

### WebRTC

```swift:WebRTC.swift
import UIKit

class WebRTC: NSObject, RTCPeerConnectionDelegate, RTCEAGLVideoViewDelegate {

    private var didReceiveRemoteStream:(()->())?
    private var onCreatedLocalSdp:((_ localSdp:NSDictionary)->())?
    
    private let factory = RTCPeerConnectionFactory()
    
    private var localStream:RTCMediaStream?
    private var localRenderView = RTCEAGLVideoView()
    private let _localView = UIView(frame:CGRect(x:0, y:0, width:windowWidth()/3, height:windowWidth()/3))
    
    private var remoteStream:RTCMediaStream?
    private var remoteRenderView = RTCEAGLVideoView()
    private let _remoteView = UIView(frame: CGRect(x: 0, y: 0, width: windowWidth(), height: windowWidth()))
    
    private var peerConnection:RTCPeerConnection?
    
    static let sharedInstance = WebRTC()
    
    private override init() {
        super.init()
    }
    
    // MARK: inerface
    
    func localView()->UIView{
        return _localView
    }
    
    func remoteView()->UIView{
        return _remoteView
    }
    
    func setup(){
        setupLocalStream()
    }
    
    func connect(iceServerUrlList:[String], onCreatedLocalSdp:@escaping ((_ localSdp:NSDictionary)->()), didReceiveRemoteStream:@escaping (()->())){
        self.onCreatedLocalSdp = onCreatedLocalSdp
        self.didReceiveRemoteStream = didReceiveRemoteStream
        
        let configuration = RTCConfiguration()
        configuration.iceServers = [RTCIceServer(urlStrings: iceServerUrlList)]
        peerConnection = factory.peerConnection(with: configuration, constraints: WebRTCUtil.peerConnectionConstraints(), delegate: self)
        peerConnection?.add(localStream!)
    }
    
    // Answer の受け取り
    func receiveAnswer(remoteSdp:NSDictionary){
        _receiveAnswer(remoteSdp: remoteSdp)
    }
    
    // Offerの受け取り
    func receiveOffer(remoteSdp:NSDictionary){
        _receiveOffer(remoteSdp: remoteSdp)
    }
    
    // Offerを作る
    func createOffer(){
        _createOffer()
    }
    
    // MARK: implements
    
    private func _receiveAnswer(remoteSdp:NSDictionary){
        
        guard let sdpContents = remoteSdp.object(forKey: "sdp") as? String else{
            print("noSDp")
            return;
        }
        
        let sdp = RTCSessionDescription(type: .answer, sdp: sdpContents)
        
        // 1. remote SDP を登録
        peerConnection?.setRemoteDescription(sdp, completionHandler: { (error) in
 
        })
    }
    
    private func _receiveOffer(remoteSdp:NSDictionary){
        
        guard let sdpContents = remoteSdp.object(forKey: "sdp") as? String else{
            print("noSDp")
            return;
        }
        
        // 1. remote SDP を登録
        let remoteSdp = RTCSessionDescription(type: .offer, sdp: sdpContents)
        peerConnection?.setRemoteDescription(remoteSdp, completionHandler: { (error) in

            // 2. answerを作る
            self.peerConnection?.answer(for: WebRTCUtil.answerConstraints(), completionHandler: { (sdp, error) in
                
                guard let sdp = sdp else{
                    print("can not create sdp")
                    return;
                }
                
                // 3.ローカルにSDPを登録
                self.peerConnection?.setLocalDescription(sdp, completionHandler: { (error) in
                    
                })
                
                // 4. peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState)
                //    で complete になったら Answerを送る
            })
        })
    }
    
    private func _createOffer(){
        
        // 1. offerを作る
        peerConnection?.offer(for: WebRTCUtil.mediaStreamConstraints(), completionHandler: { (description, error) in
            
            guard let description = description else{
                print("----- no description ----")
                return;
            }
            
            // 2.ローカルにSDPを登録
            self.peerConnection?.setLocalDescription(description, completionHandler: { (error) in
                
            })
            
            // 3. peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) 
            //    で complete になったら offerを送る
        })
    }
    
    private func setupLocalStream(){
        
        let streamId = WebRTCUtil.idWithPrefix(prefix: "stream")
        localStream = factory.mediaStream(withStreamId: streamId)
        
        setupView()
        
        setupLocalVideoTrack()
        setupLocalAudioTrack()
    }
    
    private func setupView(){
        
        localRenderView.delegate = self
        _localView.backgroundColor = UIColor.white
        _localView.frame.origin = CGPoint(x: 20, y: _remoteView.frame.size.height - (_localView.frame.size.height / 2))
        _localView.addSubview(localRenderView)
        
        remoteRenderView.delegate = self
        _remoteView.backgroundColor = UIColor.lightGray
        _remoteView.addSubview(remoteRenderView)
    }
    
    private func setupLocalVideoTrack(){
        let localVideoSource = factory.avFoundationVideoSource(with: WebRTCUtil.mediaStreamConstraints())
        let localVideoTrack = factory.videoTrack(with: localVideoSource, trackId: WebRTCUtil.idWithPrefix(prefix: "video"))
        
        if let avSource = localVideoTrack.source as? RTCAVFoundationVideoSource{
            avSource.useBackCamera = true
        }
        
        localVideoTrack.add(localRenderView)
        localStream?.addVideoTrack(localVideoTrack)
    }
    
    private func setupLocalAudioTrack(){
        let localAudioTrack = factory.audioTrack(withTrackId: WebRTCUtil.idWithPrefix(prefix: "audio"))
        localStream?.addAudioTrack(localAudioTrack)
    }
    
    // MARK: RTCPeerConnectionDelegate
    
    // いったんスルー
    public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate){ }
    public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState){}
    
    public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream){
        print("peerConnection didAdd stream:")
        
        if stream == localStream{
            return;
        }
        
        self.remoteStream = stream
        
        if let remoteVideoTrack =  stream.videoTracks.first {
            remoteVideoTrack.add(remoteRenderView)
        }
        
        if let callback = self.didReceiveRemoteStream{
            DispatchQueue.main.async {
                callback()
            }
            self.didReceiveRemoteStream = nil
        }
    }
    
    public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState){
        print("peerConnection didChange newState: RTCIceGatheringState, \(newState)")
        
        if newState != .complete{
            return;
        }
        
        guard let callback = self.onCreatedLocalSdp, let localDescription = WebRTCUtil.jsonFromDescription(description: self.peerConnection?.localDescription) else{
            print("no localDescription")
            return ;
        }
        
        callback(localDescription)
        self.onCreatedLocalSdp = nil
    }
    
    // MARK: RTCEAGLVideoViewDelegate
    
    func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
        print("---- didChangeVideoSize -----")
        
        let ratio:CGFloat = size.width / size.height
        
        if videoView == localRenderView{
            let parentWidth = _localView.frame.size.width
            let width = parentWidth * ratio
            localRenderView.frame = CGRect(x: (parentWidth - width) / 2, y: 2, width: width, height: _localView.frame.size.height-4)
        }else if videoView == remoteRenderView{
            let parentWidth = _remoteView.frame.size.width
            let width = parentWidth * ratio
            remoteRenderView.frame = CGRect(x: (parentWidth - width) / 2, y: 0, width: width, height: _remoteView.frame.size.height)
        }
    }
    
}
```

Util

```swift:WebRTCUtil.swift
class WebRTCUtil: NSObject {

    static func idWithPrefix(prefix:String)->String{
        return "\(prefix)_\(randomStringWithLength(len: 20))"
    }
    
    static func randomStringWithLength (len : Int) -> NSString {
        let letters : NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        let randomString : NSMutableString = NSMutableString(capacity: len)
        
        for _ in 0..<len {
            let length = UInt32 (letters.length)
            let rand = arc4random_uniform(length)
            randomString.appendFormat("%C", letters.character(at: Int(rand)))
        }
        
        return randomString
    }
    
    static func jsonFromDescription(description:RTCSessionDescription?)->NSDictionary?{
        guard let description = description else{
            print("description is nil")
            return nil
        }
        
        return jsonFromData(data: description.jsonData())
    }
    
    static func jsonFromCandidate(candidate:RTCIceCandidate)->NSDictionary?{
        return jsonFromData(data: candidate.jsonData())
    }
    
    static func jsonFromData(data:Data)->NSDictionary?{
        let json = try! JSONSerialization.jsonObject(with: data, options: [])
        
        guard let result = json as? NSDictionary else{
            return nil
        }
        
        return result
    }
    
    static func answerConstraints()->RTCMediaConstraints{
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: ["OfferToReceiveVideo": kRTCMediaConstraintsValueTrue,
                                   "OfferToReceiveAudio": kRTCMediaConstraintsValueTrue],
            optionalConstraints: nil)
        
        return constraints
    }
    
    static func offerConstraints()->RTCMediaConstraints{
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: ["OfferToReceiveVideo": kRTCMediaConstraintsValueTrue,
                                   "OfferToReceiveAudio": kRTCMediaConstraintsValueTrue],
            optionalConstraints: nil)
        
        return constraints
    }
    
    static func mediaStreamConstraints()->RTCMediaConstraints{
        
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: nil,
            optionalConstraints: nil)
        
        return constraints
    }
    
    static func peerConnectionConstraints()->RTCMediaConstraints {
        
        let constraints = RTCMediaConstraints(
            mandatoryConstraints: ["OfferToReceiveVideo": kRTCMediaConstraintsValueTrue,
                                   "OfferToReceiveAudio": kRTCMediaConstraintsValueTrue],
            optionalConstraints: nil)
        
        return constraints
    }

}
```

### Wamp

```swift:Wamp.swift
import Swamp

class Wamp: NSObject, SwampSessionDelegate {

    static let sharedInstance = Wamp()
    
    private static let AnswerTopic = "com.nakadoribook.webrtc.answer"
    private static let OfferTopic = "com.nakadoribook.webrtc.offer"
    
    private var swampSession:SwampSession?
    private var onConnected:(()->())?
    private var onReceiveAnswer:((_ sdp:NSDictionary)->())?
    private var onReceiveOffer:((_ sdp:NSDictionary)->())?
    
    private override init() {
        super.init()        
    }
    
    func connect(onConnected:@escaping (()->()), onReceiveAnswer:@escaping ((_ sdp:NSDictionary)->()), onReceiveOffer:@escaping ((_ sdp:NSDictionary)->())){
        self.onConnected = onConnected
        self.onReceiveAnswer = onReceiveAnswer
        self.onReceiveOffer = onReceiveOffer

//        let swampTransport = WebSocketSwampTransport(wsEndpoint:  URL(string: "wss://nakadoribooks-webrtc.herokuapp.com")!)
        let swampTransport = WebSocketSwampTransport(wsEndpoint:  URL(string: "ws://192.168.1.2:8000")!)
        let swampSession = SwampSession(realm: "realm1", transport: swampTransport)
        swampSession.delegate = self
        swampSession.connect()
        
        self.swampSession = swampSession
    }
    
    func publishOffer(sdp:NSDictionary){
        swampSession?.publish(Wamp.OfferTopic, options: [:], args: [sdp], kwargs: [:])
    }
    
    func publishAnswer(sdp:NSDictionary){
        swampSession?.publish(Wamp.AnswerTopic, options: [:], args: [sdp], kwargs: [:])
    }
    
    // MARK: private
    
    private func subscribe(){
        _subscribeOffer()
        _subscribeAnswer()
    }
    
    private func _subscribeOffer(){
        swampSession!.subscribe(Wamp.OfferTopic, onSuccess: { subscription in
        }, onError: { details, error in
            print("onError: \(error)")
        }, onEvent: { details, results, kwResults in
            
            guard let sdp = results?.first as? NSDictionary else{
                print("no args")
                return;
            }
            
            if let callback = self.onReceiveOffer{
                callback(sdp)
            }
        })
    }
    
    private func _subscribeAnswer(){
        swampSession!.subscribe(Wamp.AnswerTopic, onSuccess: { subscription in
            
        }, onError: { details, error in
            print("onError: \(error)")
        }, onEvent: { details, results, kwResults in
            
            guard let sdp = results?.first as? NSDictionary else{
                print("no args")
                return;
            }
            
            if let callback = self.onReceiveAnswer{
                callback(sdp)
            }
        })
    }
    
    // MARK: SwampSessionDelegate
    
    func swampSessionHandleChallenge(_ authMethod: String, extra: [String: Any]) -> String{
        return SwampCraAuthHelper.sign("secret123", challenge: extra["challenge"] as! String)
    }
    
    func swampSessionConnected(_ session: SwampSession, sessionId: Int){
        print("swampSessionConnected: \(sessionId)")
        
        if let callback = self.onConnected{
            callback()
        }
        
        subscribe()
    }
    
    func swampSessionEnded(_ reason: String){
        print("swampSessionEnded: \(reason)")
    }
    
}

```

### Application

```swift:ViewController.swift
class ViewController: UIViewController {

    private let webRTC = WebRTC.sharedInstance
    private let wamp = Wamp.sharedInstance
    
    private let videoLayer = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 375))
    private let controlButton = UIButton()
    private let infomationLabel = UILabel()
    private var typeOffer:Bool = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        webRTC.setup()
        
        view.addSubview(videoLayer)
        videoLayer.addSubview(webRTC.remoteView())
        videoLayer.addSubview(webRTC.localView())
        
        view.addSubview(controlButton)
        view.addSubview(infomationLabel)
        
        stateInitial()
    }
    
    private func connect(){
        
        webRTC.connect(iceServerUrlList: ["stun:stun.l.google.com:19302"], onCreatedLocalSdp: { (localSdp) in
            
            if self.typeOffer{
                self.wamp.publishOffer(sdp: localSdp)
            }else{
                self.wamp.publishAnswer(sdp: localSdp)
            }
            
        }, didReceiveRemoteStream: { () in
            self.stateWebrtcConnected()
        })
        
        wamp.connect(onConnected: {
            
            self.stateConnected()
            
        }, onReceiveAnswer: { (answerSdp) in
            
            print("onReceiveAnswer")
            
            self.webRTC.receiveAnswer(remoteSdp: answerSdp)
            
        }, onReceiveOffer: { (offerSdp) in
            
            if self.typeOffer{
                return;
            }
            
            print("onReceiveOffer")
            
            self.stateReceivedOffer()
            self.webRTC.receiveOffer(remoteSdp: offerSdp)
            
        })

    }
    
    private func changeButton(title:String, color:UIColor, enabled:Bool){
        controlButton.layer.borderColor = color.cgColor
        controlButton.setTitleColor(color, for: .normal)
        controlButton.setTitle(title, for: .normal)
        controlButton.isEnabled = enabled
        
        controlButton.removeTarget(self, action: nil, for: .allEvents)
    }
    
    private func changeInfomation(text:String, color:UIColor=UIColor.gray){
        infomationLabel.text = text
        infomationLabel.textColor = color
    }
    
    private func buttonAnimation(){
        controlButton.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
        
        UIView.animate(withDuration: 0.2) { 
            self.controlButton.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
        }
    }
    
    // MARK: UIEvents
    
    private dynamic func tapOffer(){
        typeOffer = true
        
        buttonAnimation()
        
        stateOffering()
        
        
        webRTC.createOffer()
    }
    
    private dynamic func tapConnect(){
        buttonAnimation()
        
        stateConnecting()
        
        connect()
    }
    
    // MARK: states
    
    private func stateInitial(){
        controlButton.frame = CGRect(x: 20, y: windowHeight()-64-20, width: windowWidth()-40, height: 64)
        
        controlButton.layer.cornerRadius = 5;
        controlButton.layer.borderWidth = 2
        controlButton.titleLabel?.font = UIFont.systemFont(ofSize: 22)
        changeButton(title: "Connect", color: UIColor.orange, enabled: true)
        controlButton.addTarget(self, action: #selector(ViewController.tapConnect), for: .touchUpInside)
        
        infomationLabel.frame = CGRect(x: 20, y: controlButton.frame.origin.y - 30 - 20, width: windowWidth()-40, height: 30)
        infomationLabel.font = UIFont.systemFont(ofSize: 20)
        infomationLabel.textAlignment = .center
    }
    
    private func stateConnected(){
        changeInfomation(text: "Connected!", color: UIColor.green)
        
        changeButton(title: "Send Offer", color: UIColor.blue, enabled: true)
        controlButton.addTarget(self, action: #selector(ViewController.tapOffer), for: .touchUpInside)
    }
    
    private func stateConnecting(){
        changeButton(title: "Connecting...", color: UIColor.orange, enabled: false)
        changeInfomation(text: "Connecting...")
    }
    
    private func stateOffering(){
        changeButton(title: "Offered", color: UIColor.gray, enabled: false)
        changeInfomation(text: "Offering", color: UIColor.blue)
    }

    private func stateReceivedOffer(){
        changeButton(title: "ReceivedOffer", color: UIColor.gray, enabled: false)
        changeInfomation(text: "CreatingAnswer...", color: UIColor.blue)
    }
    
    private func stateWebrtcConnected(){
        changeButton(title: "OK!", color: UIColor.gray, enabled: false)
        changeInfomation(text: "OK!", color: UIColor.green)
    }
    
}
```

# その他

Extention 系を [libjingle](https://cocoapods.org/pods/libjingle_peerconnection) からコピペ。

![スクリーンショット 2017-04-02 16.16.26.png](https://qiita-image-store.s3.amazonaws.com/0/159266/2c8c6491-d35b-dcb6-b081-d15b8e0a6fa7.png)

# 参考

- http://wamp-proto.org/
- https://mike-neko.github.io/blog/webrtc-ios/