[webRTC for ios vol4] 複数台接続

iOSで複数台接続やります。

- iOS 1:1はこちら → [webRTC for ios vol3 tricke ICE](http://qiita.com/nakadoribooks/items/a0f19dd96b0763c35ed2)
- Web複数台はこちら → [webRTC for web vol2 複数人での接続](http://qiita.com/nakadoribooks/items/d8a55811fe0b51b8401f)

# 成果物

<img src="https://j.gifs.com/KOPl3z.gif" height="500px" />

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

# 流れ

流れはweb版と同じ。
roomKeyはとりあえず固定で。

参考

- https://html5experts.jp/mganeko/20112/
- https://html5experts.jp/mganeko/20273/

<img src="https://qiita-image-store.s3.amazonaws.com/0/159266/44d9f952-34fd-9460-07c4-0175a263a3ac.png" />

# 実装

こちらも[web版](http://qiita.com/nakadoribooks/items/d8a55811fe0b51b8401f)とほぼ同じ。

## Wamp

簡易wampサーバーはこちら → [webRTC for ios vol2](http://qiita.com/nakadoribooks/items/9f1b1cbe32f2fc6dec70#wamp%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6heroku%E3%81%AB%E7%BD%AE%E3%81%8F)

connectしてsubscribeする。
あとはsessionの提供。

```swift:Wamp.swift
import UIKit
import Swamp


enum WampTopic:String{
    
    case callme = "com.nakadoribook.webrtc.[roomId].callme"
    , close = "com.nakadoribook.webrtc.[roomId].close"
    , answer = "com.nakadoribook.webrtc.[roomId].[id].answer"
    , offer = "com.nakadoribook.webrtc.[roomId].[id].offer"
    , candidate = "com.nakadoribook.webrtc.[roomId].[id].candidate"

}

typealias WampOnOpenHandler = (()->())
typealias WampReceiveAnswerHandler = ((_ targetId:String, _ sdp:NSDictionary)->())
typealias WampReceiveOfferHandler = ((_ targetId:String, _ sdp:NSDictionary)->())
typealias WampReceiveCandidateHandler = ((_ targetId:String, _ candidate:NSDictionary)->())
typealias WampReceiveCallmeHandler = ((_ targetId:String)->())
typealias WampOncloseConnectionHandler = ((_ targetId:String)->())

typealias WampCallbacks = (onOpen:WampOnOpenHandler
    , onReceiveAnswer:WampReceiveAnswerHandler
    , onReceiveOffer:WampReceiveOfferHandler
    , onReceiveCallme:WampReceiveCallmeHandler
    , onCloseConnection:WampOncloseConnectionHandler)

class Wamp: NSObject, SwampSessionDelegate {

    static let sharedInstance = Wamp()
    private var _session:SwampSession!
    
    private override init() {
        super.init()        
    }
    
    var session:SwampSession{
        get{
            return _session
        }
    }
    
    func connect(){
        let swampTransport = WebSocketSwampTransport(wsEndpoint:  URL(string: "wss://nakadoribooks-webrtc.herokuapp.com")!)
        let swampSession = SwampSession(realm: "realm1", transport: swampTransport)
        swampSession.delegate = self
        swampSession.connect()
    }
        
    private func resultToSdp(results:[Any]?)->NSDictionary?{
        
        if let sdp = results?.first as? NSDictionary{
            return sdp;
        }
        
        return nil;
    }
    
    // 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){
        self._session = session
        
        // subscribe
        session.subscribe(endpointAnswer(targetId: userId), onSuccess: { (subscription) in
        }, onError: { (details, error) in
        }) { (details, args, kwArgs) in
            guard let args = args, let targetId = args[0] as? String, let sdpString = args[1] as? String else{
                print("no args answer")
                return
            }
            
            let sdp = try! JSONSerialization.jsonObject(with: sdpString.data(using: .utf8)!, options: .allowFragments) as! NSDictionary
            self.callbacks.onReceiveAnswer(targetId, sdp)
        }
        
        session.subscribe(endpointOffer(targetId: userId), onSuccess: { (subscription) in
        }, onError: { (details, error) in
        }) { (details, args, kwArgs) in
            guard let args = args, let targetId = args[0] as? String, let sdpString = args[1] as? String else{
                print("no args offer")
                return
            }
            
            let sdp = try! JSONSerialization.jsonObject(with: sdpString.data(using: .utf8)!, options: .allowFragments) as! NSDictionary
            self.callbacks.onReceiveOffer(targetId, sdp)
        }
        
        session.subscribe(endpointCallme(), onSuccess: { (subscription) in
        }, onError: { (details, error) in
        }) { (details, args, kwArgs) in
            guard let args = args, let targetId = args[0] as? String else{
                print("no args callMe")
                return
            }
            
            if targetId == self.userId{
                return;
            }
            self.callbacks.onReceiveCallme(targetId)
        }
        
        session.subscribe(endpointClose(), onSuccess: { (subscription) in
        }, onError: { (details, error) in
        }) { (details, args, kwArgs) in
            guard let args = args, let targetId = args[0] as? String else{
                print("no args close")
                return
            }
            
            if targetId == self.userId{
                return;
            }
            
            self.callbacks.onCloseConnection(targetId)
        }
                
        self.callbacks.onOpen()
    }
    
    func swampSessionEnded(_ reason: String){
        print("swampSessionEnded: \(reason)")
    }
    
    private var roomKey:String!
    private var userId:String!
    private var callbacks:WampCallbacks!
    
    func setup(roomKey:String, userId:String, callbacks:WampCallbacks){
        self.roomKey = roomKey
        self.userId = userId
        self.callbacks = callbacks
    }
    
    static let HandshakeEndpint = "wss://nakadoribooks-webrtc.herokuapp.com"
    
    private func roomTopic(base:String)->String{
        return base.replacingOccurrences(of: "[roomId]", with: self.roomKey)
    }
    
    func endpointAnswer(targetId:String)->String{
        return self.roomTopic(base: WampTopic.answer.rawValue.replacingOccurrences(of: "[id]", with: targetId))
    }
    
    func endpointOffer(targetId:String)->String{
        return self.roomTopic(base: WampTopic.offer.rawValue.replacingOccurrences(of: "[id]", with: targetId))
    }
    
    func endpointCandidate(targetId:String)->String{
        return self.roomTopic(base: WampTopic.candidate.rawValue.replacingOccurrences(of: "[id]", with: targetId))
    }
    
    func endpointCallme()->String{
        return self.roomTopic(base: WampTopic.callme.rawValue)
    }
    
    func endpointClose()->String{
        return self.roomTopic(base: WampTopic.close.rawValue)
    }
   
}
```


## RTCPeerConnectionまわり

static でlocalStreamを管理
接続の数だけインスタンスを作って相手のStreamを管理

```swift:WebRTC.swift
import UIKit

typealias WebRTCOnIceCandidateHandler = (_ candidate:NSDictionary) -> ()
typealias WebRTCOnAddedStream = (_ stream:RTCMediaStream, _ view:UIView) -> ()
typealias WebRTCOnRemoveStream = (_ stream:RTCMediaStream) -> ()
typealias WebRTCCallbacks = (onIceCandidate:WebRTCOnIceCandidateHandler, onAddedStream:WebRTCOnAddedStream, onRemoveStream:WebRTCOnRemoveStream)

class WebRTC: NSObject, RTCPeerConnectionDelegate, RTCEAGLVideoViewDelegate {
    
    // ▼ inner class -----
    class LocalViewDelegate:NSObject, RTCEAGLVideoViewDelegate{
        
        private let localRenderView:RTCEAGLVideoView
        
        init(localRenderView:RTCEAGLVideoView){
            self.localRenderView = localRenderView
            super.init()
            
            localRenderView.delegate = self
        }
        
        // MARK: RTCEAGLVideoViewDelegate
        
        func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
            print("---- didChangeVideoSize -----")
            
            let ratio:CGFloat = size.width / size.height
            
            if ratio > 1.0{
                // 横長
                let height:CGFloat = WebRTC.ViewSize
                let width:CGFloat = height * ratio
                let x:CGFloat = (WebRTC.ViewSize - width) / 2.0
                localRenderView.frame = CGRect(x: x, y: 0, width: width, height: height)
            }else{
                // 縦長
                let width:CGFloat = WebRTC.ViewSize
                let height:CGFloat = width / max(ratio, 0.1)
                let y:CGFloat = (WebRTC.ViewSize - height) / 2.0
                localRenderView.frame = CGRect(x: 0, y: y, width: width, height: height)
            }
        }
    }
    
    // ▼ static -----
    
    private static let IceServerUrls = ["stun:stun.l.google.com:19302"]
    private static let factory = RTCPeerConnectionFactory()
    private static var localStream:RTCMediaStream?
    private static var localRenderView = RTCEAGLVideoView()
    private static let _localView = UIView(frame:CGRect(x:10, y:10, width:WebRTC.ViewSize, height:WebRTC.ViewSize))
    private static var localViewDelegate:LocalViewDelegate!
    
    private static let Margin:CGFloat = 20.0
    static let ViewSize:CGFloat = (windowWidth() - (WebRTC.Margin * 3)) / 2.0
    
    static func setup(){
        let streamId = WebRTCUtil.idWithPrefix(prefix: "stream")
        localStream = factory.mediaStream(withStreamId: streamId)
        
        localViewDelegate = LocalViewDelegate(localRenderView: localRenderView)
        _localView.backgroundColor = UIColor.white
        _localView.clipsToBounds = true
        _localView.addSubview(localRenderView)
        
        setupLocalVideoTrack()
        setupLocalAudioTrack()
    }
    
    static func disableVideo(){
        localStream?.videoTracks.first?.isEnabled = false
    }
    
    static func enableVideo(){
        localStream?.videoTracks.first?.isEnabled = true
    }
    
    static var localView:UIView{
        get{
            return _localView
        }
    }
    
    private static 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 static func setupLocalAudioTrack(){
        let localAudioTrack = factory.audioTrack(withTrackId: WebRTCUtil.idWithPrefix(prefix: "audio"))
        localStream?.addAudioTrack(localAudioTrack)
    }
    
    // ▼ instance ------
    
    private var remoteStream:RTCMediaStream?
    private var remoteRenderView = RTCEAGLVideoView()
    private let _remoteView = UIView(frame: CGRect(x: 0, y: 0, width: WebRTC.ViewSize, height: WebRTC.ViewSize))
    
    private var peerConnection:RTCPeerConnection?
    private let callbacks:WebRTCCallbacks
    
    init(callbacks:WebRTCCallbacks){
        self.callbacks = callbacks
        super.init()
        
        setupPeerConnection()
        
        remoteView.clipsToBounds = true
        remoteRenderView.delegate = self
        remoteView.addSubview(remoteRenderView)
    }
    
    private func setupPeerConnection(){
        let configuration = RTCConfiguration()
        configuration.iceServers = [RTCIceServer(urlStrings: WebRTC.IceServerUrls)]
        peerConnection = WebRTC.factory.peerConnection(with: configuration, constraints: WebRTCUtil.peerConnectionConstraints(), delegate: self)
        peerConnection?.add(WebRTC.localStream!)
    }
    
    var remoteView:UIView{
        get{
            return _remoteView
        }
    }
    
    func receiveCandidate(candidate:NSDictionary){
        guard let candidate = candidate as? [AnyHashable:Any]
            , let rtcCandidate = RTCIceCandidate(fromJSONDictionary: candidate) else{
            print("invalid candiate")
            return
        }
        
        self.peerConnection?.add(rtcCandidate)
    }
    
    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

        })
    }
    
    func receiveOffer(remoteSdp:NSDictionary, callback:@escaping (_ answerSdp: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
                    
                    // 3. answer を送る
                    guard let localDescription = WebRTCUtil.jsonFromDescription(description: self.peerConnection?.localDescription) else{
                        print("no localDescription")
                        return ;
                    }
                    
                    callback(localDescription)
                })
                
            })
        })
    }
    
    func createOffer(callback:@escaping (_ offerSdp:NSDictionary)->()){
        
        // 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. offer を送る
                guard let localDescription = WebRTCUtil.jsonFromDescription(description: self.peerConnection?.localDescription) else{
                    print("no localDescription")
                    return ;
                }
                
                callback(localDescription)
            })
        })
    }
    
    func close(){
        if let localStream = WebRTC.localStream{
            self.peerConnection?.remove(localStream)
        }
        
        self.peerConnection?.close()
        self.peerConnection = nil
    }
    
    // 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, didRemove candidates: [RTCIceCandidate]){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState){}
    public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState){}
    
    // for Trickle ice
    public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate){
        
        if let candidateJson = WebRTCUtil.jsonFromCandidate(candidate: candidate){
            self.callbacks.onIceCandidate(candidateJson)
        }
    }
    
    public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream){
        
        self.remoteStream = stream
        if let remoteVideoTrack =  stream.videoTracks.first {
            remoteVideoTrack.add(remoteRenderView)
        }
        
        DispatchQueue.main.async {
            self.callbacks.onAddedStream(stream, self.remoteView)
        }
    }
    
    // MARK: RTCEAGLVideoViewDelegate
    
    func videoView(_ videoView: RTCEAGLVideoView, didChangeVideoSize size: CGSize) {
        
        let ratio:CGFloat = size.width / size.height
        
        if ratio > 1.0{
            // 横長
            let height:CGFloat = WebRTC.ViewSize
            let width:CGFloat = height * ratio
            let x:CGFloat = (WebRTC.ViewSize - width) / 2.0
            remoteRenderView.frame = CGRect(x: x, y: 0, width: width, height: height)
        }else{
            // 縦長
            let width:CGFloat = WebRTC.ViewSize
            let height:CGFloat = width / max(ratio, 0.1)
            let y:CGFloat = (WebRTC.ViewSize - height) / 2.0
            remoteRenderView.frame = CGRect(x: 0, y: y, width: width, height: height)
        }
        
    }
    
}
```

## 一個分のコネクション管理

これも、一人の相手ごとに一個インスタンス作る

```swift:Connection.swift
import UIKit

typealias ConnectionOnAddedStream = (_ streamWrapper:StreamWrapper)->()

class StreamWrapper:NSObject{
    
    let stream:RTCMediaStream
    let targetId:String
    let view:UIView
    
    init(stream:RTCMediaStream, targetId:String, view:UIView){
        self.stream = stream
        self.targetId = targetId
        self.view = view
        super.init()
        
        let overlay = UIView()
        overlay.frame.size = view.frame.size
        view.addSubview(overlay)
        
        let labelBg = UIView()
        labelBg.frame = CGRect(x: 0, y: view.frame.size.height - 30, width: view.frame.size.width, height: 30)
        labelBg.backgroundColor = UIColor.white
        labelBg.alpha = 0.8
        overlay.addSubview(labelBg)
        
        let label = UILabel()
        label.frame = CGRect(x: 0, y: view.frame.size.height - 30, width: view.frame.size.width, height: 30)
        label.textAlignment = .center
        label.text = targetId
        label.textColor = UIColor.black
        overlay.addSubview(label)
    }
}

class Connection: NSObject {

    private let onAddedStream:ConnectionOnAddedStream
    private var webRtc:WebRTC!
    private let myId:String
    let targetId:String
    
    init(myId:String, targetId:String, onAddedStream:@escaping ConnectionOnAddedStream){
        self.myId = myId
        self.targetId = targetId
        self.onAddedStream = onAddedStream
        
        super.init()
        
        webRtc = WebRTC(callbacks: (
            onIceCandidate: {(iceCandidate:NSDictionary) -> Void in
                
                let jsonData = try! JSONSerialization.data(withJSONObject: iceCandidate, options: [])
                let jsonStr = String(bytes: jsonData, encoding: .utf8)!
                
                let wamp = Wamp.sharedInstance
                let topic = wamp.endpointCandidate(targetId: targetId)
                wamp.session.publish(topic, options: [:], args: [jsonStr], kwargs: [:])
            }
            , onAddedStream: {(stream:RTCMediaStream, view:UIView) -> Void in
                let streamWrapper = StreamWrapper(stream: stream, targetId: targetId, view:view)
                self.onAddedStream(streamWrapper)
            }
            , onRemoveStream: {(stream:RTCMediaStream) -> Void in
                
            }
        ))
        
        // for tricke ice
        subscribeCandidate()
    }
    
    private func subscribeCandidate(){
        let wamp = Wamp.sharedInstance
        let candidateTopic = wamp.endpointCandidate(targetId: myId)
        
        Wamp.sharedInstance.session.subscribe(candidateTopic, onSuccess: { (subscription) in
        }, onError: { (results, error) in
        }) { (results, args, kwArgs) in
            
            guard let candidateStr = args?.first as? String else{
                print(args?.first)
                print("no candidate")
                return
            }
            
            let data = candidateStr.data(using: String.Encoding.utf8)!
            let candidate = try! JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! NSDictionary
                        
            self.webRtc.receiveCandidate(candidate: candidate)
        }
    }
    
    deinit {
        print("connection deinit")
    }
    
    func publishAnswer(offerSdp:NSDictionary){
        webRtc.receiveOffer(remoteSdp: offerSdp) { (answerSdp) in
            let wamp = Wamp.sharedInstance
            let topic = wamp.endpointAnswer(targetId: self.targetId)
            
            let jsonData = try! JSONSerialization.data(withJSONObject: answerSdp, options: [])
            let jsonStr = String(bytes: jsonData, encoding: .utf8)!
            
            Wamp.sharedInstance.session.publish(topic, options: [:], args: [self.myId, jsonStr], kwargs: [:])
        }
    }
    
    func publishOffer(){
        webRtc.createOffer { (offerSdp) in
            let wamp = Wamp.sharedInstance
            let topic = wamp.endpointOffer(targetId: self.targetId)
            
            let jsonData = try! JSONSerialization.data(withJSONObject: offerSdp, options: [])
            let jsonStr = String(bytes: jsonData, encoding: .utf8)!
            
            wamp.session.publish(topic, options: [:], args: [self.myId, jsonStr], kwargs: [:])
        }
    }
    
    func receiveAnswer(sdp:NSDictionary){
        webRtc.receiveAnswer(remoteSdp: sdp)
    }
    
    func receiveCnadidate(candidate:NSDictionary){
        
    }
    
    func close(){
        print("close connection")
        webRtc.close()
    }
    
}
```

## アプリケーション

roomKey はとりあえず固定

```swift:ViewController.swift
import UIKit
import FirebaseDatabase

import UIKit

extension String {
    static func getRandomStringWithLength(length: Int) -> String {
        
        let alphabet = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        let upperBound = UInt32(alphabet.characters.count)
        
        return String((0..<length).map { _ -> Character in
            return alphabet[alphabet.index(alphabet.startIndex, offsetBy: Int(arc4random_uniform(upperBound)))]
        })
    }
}

class ViewController: UIViewController {

    private let wamp = Wamp.sharedInstance
    private var connectionList:[Connection] = []
    private var streamWrapperList:[StreamWrapper] = []
    
    private let remoteLayer = UIView(frame: windowFrame())
    private let localLayer = UIView(frame: CGRect(x: 0, y: windowHeight()-WebRTC.ViewSize - 20, width: windowWidth(), height: WebRTC.ViewSize + 20))
    private var roomKey:String!
    private let userId = String.getRandomStringWithLength(length: 8)
    private let toggleButton = UIButton()
    
    deinit {
        print("ViewController deinit")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupRoom()
        setupWamp()
        setupStream()
        
        Wamp.sharedInstance.connect()
        
        view.addSubview(remoteLayer)
        view.addSubview(localLayer)
        
        localLayer.backgroundColor = UIColor.gray
        localLayer.addSubview(WebRTC.localView)
        
        let labelX = WebRTC.ViewSize + 20.0
        let labelWidth = windowWidth() - labelX - 10
        let label = UILabel(frame: CGRect(x: labelX, y: 10, width: labelWidth, height: 50))
        label.numberOfLines = 2
        label.font = UIFont.systemFont(ofSize: 20.0)
        label.textColor = UIColor.white
        label.text = "You\n(\(self.userId))"
        label.sizeToFit()
        localLayer.addSubview(label)
        
        let y:CGFloat = label.frame.size.height + 20
        
        toggleButton.frame = CGRect(x: labelX, y: y, width: labelWidth, height: 44)
        toggleButton.backgroundColor = UIColor.white
        toggleButton.layer.cornerRadius = 5
        toggleButton.clipsToBounds = true
        toggleButton.setTitleColor(UIColor.gray, for: .normal)
        toggleButton.setTitle("Stop", for: .normal)
        toggleButton.setTitle("Play", for: .selected)
        toggleButton.addTarget(self, action: #selector(ViewController.tapToggle), for: .touchUpInside)
        localLayer.addSubview(toggleButton)
    }
    
    private dynamic func tapToggle(){
        
        toggleButton.isSelected = !toggleButton.isSelected
        
        let active = !toggleButton.isSelected
        if active{
            WebRTC.enableVideo()
        }else{
            WebRTC.disableVideo()
        }
    }
    
    private func setupStream(){
        WebRTC.setup()
    }
    
    private func setupRoom(){
        let roomKey:String? = "-Kr-JqhdoZ1YtdeO0-9r"
        
        if let roomKey = roomKey{
            self.roomKey = roomKey
            return
        }
        
        let ref = Database.database().reference().child("rooms")
        let roomRef = ref.childByAutoId()
        self.roomKey = roomRef.key
        
        print("roomKey:\(roomRef.key)")
    }
    
    private func setupWamp(){
        
        let wamp = Wamp.sharedInstance
        Wamp.sharedInstance.setup(roomKey: roomKey, userId: userId
        , callbacks: (
            onOpen:{() -> Void in
                print("onOpen")
                
                let topic = wamp.endpointCallme()
                wamp.session.publish(topic, options: [:], args: [self.userId], kwargs: [:])
            }
            , onReceiveOffer:{(targetId:String, sdp:NSDictionary) -> Void in
                print("onReceiveOffer")
                let connection = self.createConnection(targetId: targetId)
                connection.publishAnswer(offerSdp: sdp)
            }
            , onReceiveAnswer:{(targetId:String, sdp:NSDictionary) -> Void in
                print("onReceiveAnswer")
                guard let connection = self.findConnection(targetId: targetId) else{
                    return
                }
                connection.receiveAnswer(sdp: sdp)
            }
            
            , onReceiveCallme:{(targetId:String) -> Void in
                print("onReceivCallme")
                let connection = self.createConnection(targetId:targetId)
                connection.publishOffer()
            }
            , onCloseConnection:{(targetId:String) -> Void in
                print("onCloseConnection")
                
                // removeConnection
                guard let removeIndex = self.connectionList.index(where: { (row) -> Bool in
                    return row.targetId == targetId
                }) else{
                    return
                }
                
                let connection = self.connectionList.remove(at: removeIndex)
                connection.close()
                
                // removeView
                guard let streamIndex = self.streamWrapperList.index(where: { (row) -> Bool in
                    return row.targetId == targetId
                }) else{
                    return;
                }
                
                let streamWrapper = self.streamWrapperList.remove(at: streamIndex)
                streamWrapper.view.removeFromSuperview()
                
                self.calcRemoteViewPosition()
            }
       ))
    }
    
    private func createConnection(targetId:String)->Connection{
        let connection = Connection(myId: userId, targetId: targetId) { (streamWrapper) in
            print("onAeedStream")
            
            self.remoteLayer.addSubview(streamWrapper.view)
            self.streamWrapperList.append(streamWrapper)
            
            self.calcRemoteViewPosition()
            
        }
        connectionList.append(connection)
        return connection
    }
    
    private func findConnection(targetId:String)->Connection?{
        for i in 0..<connectionList.count{
            let connection = connectionList[i]
            if(connection.targetId == targetId){
                return connection
            }
        }
        
        print("not found connection")
        return nil
    }
    
    private func calcRemoteViewPosition(){
        var y:CGFloat = 20.0
        var x:CGFloat = 20
        
        for i in 0..<streamWrapperList.count{
            let view = streamWrapperList[i].view
            view.frame.origin = CGPoint(x: x, y: y)
            
            if x + view.frame.size.width > windowWidth(){
                x = 20
                y = y + view.frame.size.height
                view.frame.origin = CGPoint(x: x, y: y)
            }
            x = x + view.frame.size.width + 20
        }
    }

}
```

## 次
- Android 複数接続
- 接続切れた時が取れない