LoginSignup
10
8

More than 5 years have passed since last update.

[webRTC for ios vol2] 繋げてみる

Last updated at Posted at 2017-04-02

※ 2017/4/6 編集 Answer部分

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

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

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

シグナリングはwampでやる。
websocket上でアレするプロトコル。

環境

  • xcode 8.2.1
  • iOS 10.2.1

成果物

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

流れ

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

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

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

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

設定

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"
  }
}

実装

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}
);

デプロイ

git push heroku master

以上。

ios側を実装を作る

設定

ライブラリ取り込み

wampライブラリ は swamp
名前がかっこいい。

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

info.plist

パーミッション追加

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

まずは大枠。

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

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

WebRTC のインタフェース

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 のインタフェース

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)

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

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

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

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

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

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 からコピペ。

スクリーンショット 2017-04-02 16.16.26.png

参考

10
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
8