※ 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
設定
{
"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"
}
}
実装
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
名前がかっこいい。
platform :ios,'9.0'
use_frameworks!
target 'webrtcExample' do
pod 'Swamp', '0.2.0'
end
info.plist
パーミッション追加
<key>NSCameraUsageDescription</key>
<string>カメラ使います</string>
<key>NSMicrophoneUsageDescription</key>
<string>マイク使います</string>
まずは大枠。
- ViewController.swift (アプリケーション)
- WebRTC.swift
- Wamp.swift
アプリケーションから WebRTC と Wamp を使って作っていきます。
WebRTC のインタフェース
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サーバーへ接続してイベント監視
func connect(onConnected:@escaping (()->()), onReceiveAnswer:@escaping ((_ sdp:NSDictionary)->()), onReceiveOffer:@escaping ((_ sdp:NSDictionary)->()))
// Offerの送信
func publishOffer(sdp:NSDictionary)
// Answerの送信
func publishAnswer(sdp:NSDictionary)
アプリケーションから使う
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
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
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
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
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 からコピペ。