はじめに
知人のお手伝いで通話機能を利用したアプリを作成することになり、NTTコミュニケーションが無料で提供しているSkywayを利用したいとのことだったので、調査のためにアプリを実装したので簡単にやったことをまとめておきたいと思います。
Skywayについて
- 基本無料。オプションは有料?
- JavaScript / Android / iOSでのAPI互換により、簡単にマルチデバイス対応が可能
- クライアントサイドの開発だけでOK、サーバサイドは準備不要
- 多人数参加のグループビデオチャットに対応している
WebRTCとは?
- Webブラウザで簡単にボイスチャット、ビデオチャット、ファイル共有等ができる仕組みのこと
環境
Xcode 8.1
swift 3.0
OS 10.12.1
導入準備
開発者登録
SkyWay SDKを使用するために開発者登録を行います。
2.ドメインを入力する
※確定していない場合は、とりあえずlocalhostでいいみたいです
但し、リリースするときは、セキュリティーを考えると削除しておいた方が良いです
3.利用規約に同意して登録ボタンを選択すると仮登録メールが届くので本登録を行う
サンプルを動かしてみる
1.cd に移動して、pod insallする。
2.workspaceを起動する
3.入手したSkyWay.frameworkを追加する
4.API Keyのドメインを指定する
// Enter your APIkey and Domain
// Please check this page. >> https://skyway.io/ds/
static NSString *const kAPIkey = @"yourAPIKEY";
static NSString *const kDomain = @"yourDomain";
// Enter your APIkey and Domain
// Please check this page. >> https://skyway.io/ds/
static NSString *const kAPIkey = @"yourAPIKEY";
static NSString *const kDomain = @"yourDomain";
5.ビルドしようとしたら、Linker Errorが出るので、Edit SchemeでPods-SkyWay-iOS-Sampleを追加して、DerivedDataを削除してビルドしなおしたらビルドが通りました。
7.MediaConnectionボタンをタップして、テレビ電話の画面に遷移すると、以下のような画面が表示されます。
簡単なアプリを作成する
1.サンプルプロジェクトを作成する
2.入手したSkyWay.frameworkを追加する
3.objのFWなので、swiftで利用できるようにBridgingHeaderを追加する
- Bridging-Header.hを追加する
- Bridging-Header.hに#import を追加する
- Build Settings -> Swift Compiler -> Objective-C Bridging HeaderにBridging-Header.hを設定する
4.必要なライブラリを追加する
- AudioToolbox.framework
- AVFoundation.framework
- CoreMedia.framework
- CoreVideo.framework
- VideoToolbox.framework
- CoreGraphoics.framework
- Foundation.framework
- GLKit.framework
- SystemConfiguration.framework
- libc++.tbd
- libstdc++.6.0.9.tbd
- libsqlite3.tbd
- libicucore.tbd
5.Build Settingを変更する
- Build Settings -> Build Options -> Enable BitcodeをNoに設定する
- Build Settings -> Linking -> Other Linker Flagsを-ObjCに設定する
6.objective-cのサンプルを参考に管理クラスを作成する。
セッションスタートや、相手から接続要求が来た時などにcallbackでイベントなどが通知されるので、
delegateなどでController側に通知して、UIを更新する
import UIKit
import AVFoundation
private var instance:SkywayManager? = nil
class SkywayManager: NSObject {
//API Key
static let apiKey:String = "<あなたのID>"
//Domain
static let domain:String = "<あなたの指定したdomain>"
//Peer
private var peer:SKWpeer? = nil
private var localStream:SKWMediaStream? = nil
private var remoteStream:SKWMediaStream? = nil
private var mediaConnection:SKWMediaConnection? = nil
private var conected:Bool = false
private var peerId: String!
private var connectStart:Bool = false;
private var sessionDelegate:SkywaySessionDelegate?
class func sharedManager() -> SkywayManager{
if (instance == nil) {
instance = SkywayManager()
instance!.configInit()
}
return instance!
}
// MARK: Session
func sessionStart(delegate:SkywaySessionDelegate) {
sessionDelegate = nil
sessionDelegate = delegate
//Initialize SkyWay Peer
let option:SKWPeerOption = SKWPeerOption.init()
option.key = SkywayManager.apiKey
option.domain = SkywayManager.domain
//Peer初期化
peer = SKWPeer(options: option)
setCallbacks()
}
func getPeerId()->String {
return peerId
}
// MARK: callbacks
private func setCallbacks(){
peer!.on(.PEER_EVENT_OPEN, callback: {obj in
if obj is NSString{
print("peer onopen")
self.peerId = obj as! String!
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.sessionStart()
}
})
//PeerServerへの接続が確立すると発生
peer!.on(.PEER_EVENT_CALL, callback: {obj in
print("peer call")
if let connection = obj as? SKWMediaConnection{
self.mediaConnection = connection
self.setMediaCallbacks(media: self.mediaConnection)
self.mediaConnection!.answer(self.localStream!)
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.connectSucces()
}
})
peer!.on(.PEER_EVENT_CLOSE, callback: {obj in
print("peer close")
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.connectEnd()
})
peer!.on(.PEER_EVENT_DISCONNECTED, callback: {obj in
print("peer disconnected")
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.connectDisconnect()
})
peer!.on(.PEER_EVENT_ERROR, callback: {obj in
print("peer error")
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.connectError()
})
}
//接続リストを取得する
public func getPeerList(delegate:SkywaySessionDelegate) {
peer!.listAllPeers({obj in
if let array:Array<String> = obj as? Array<String> {
let peerList:NSMutableArray = NSMutableArray()
for id in array {
if (id == self.peerId) {
continue;
}
peerList.add(id)
}
delegate.getPeerList(newPeerList:peerList)
}
})
}
public func setWaitLocal(localView:SKWVideo, delegate:SkywaySessionDelegate) {
sessionDelegate = nil
sessionDelegate = delegate
//初期化
SKWNavigator.initialize(peer)
let bounds = UIScreen.main.nativeBounds
let constraints = SKWMediaConstraints()
constraints.maxFrameRate = 10
constraints.cameraMode = .CAMERA_MODE_ADJUSTABLE
constraints.cameraPosition = .CAMERA_POSITION_FRONT
constraints.minWidth = UInt(bounds.width)
constraints.minHeight = UInt(bounds.height / 2)
self.localStream = SKWNavigator.getUserMedia(constraints)
//localViewに設定する
localView.addSrc(self.localStream, track: 0)
localView.setNeedsDisplay()
}
//通話開始要求
public func connectStart(connectPeerId:String, delegate:SkywaySessionDelegate) {
connectStart = true
sessionDelegate = delegate
mediaConnection = peer!.call(withId: connectPeerId, stream: localStream)
self.setMediaCallbacks(media: self.mediaConnection)
}
public func closeMedia(localView:SKWVideo, remoteView:SKWVideo) {
conected = false
connectStart = false
if mediaConnection != nil{
mediaConnection!.close()
mediaConnection!.on(.MEDIACONNECTION_EVENT_STREAM, callback: nil)
mediaConnection!.on(.MEDIACONNECTION_EVENT_CLOSE, callback: nil)
mediaConnection!.on(.MEDIACONNECTION_EVENT_ERROR, callback: nil)
mediaConnection = nil
}
if remoteStream != nil {
remoteView.removeSrc(remoteStream, track: 0)
remoteStream!.close()
remoteStream = nil
}
if localStream != nil {
localView.removeSrc(localStream, track: 0)
localStream!.close()
localStream = nil
}
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.connectEnd()
}
public func sessionClose() {
peer!.on(.PEER_EVENT_OPEN, callback: nil)
peer!.on(.PEER_EVENT_CLOSE, callback: nil)
peer!.on(.PEER_EVENT_CALL, callback: nil)
peer!.on(.PEER_EVENT_DISCONNECTED, callback: nil)
peer!.on(.PEER_EVENT_ERROR, callback: nil)
SKWNavigator.terminate()
peer!.destroy()
peer = nil
}
func setRemoteView(remoteView:SKWVideo) {
conected = true
remoteView.addSrc(self.remoteStream, track: 0)
}
private func setMediaCallbacks(media: SKWMediaConnection?){
guard let connection = media else {
return
}
connection.on(.MEDIACONNECTION_EVENT_STREAM, callback: {obj in
print("media stream")
if let stream = obj as? SKWMediaStream{
self.dispatch_async_global {
self.remoteStream = stream
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.remoteConnectSucces()
}
}
})
connection.on(.MEDIACONNECTION_EVENT_CLOSE, callback: {obj in
print("media close")
guard let delegate:SkywaySessionDelegate = self.sessionDelegate else {
return
}
delegate.connectDisconnect()
})
connection.on(.MEDIACONNECTION_EVENT_ERROR, callback: {obj in
print("media error")
})
}
}
7.着信を受け付ける画面を実装する。
動画のチャットアプリなどを実現する場合は、相手のPeerIDがわからないので
以下のような実装を行う必要があるが今回の調査では、待受画面のみを実装しました。
1.通話元で通話ボタンをタップする
2.接続要求APIで一度自分のIDをサーバーに渡す。
3.サーバーから指定のユーザーにPUSH等で通知して、IDを通知して、
UI等で通信が来ているような形にして、接続を許可してから、相手とConnect状態にする。
4.Remote/localの動画を表示する
かける部分については、今回は説明しません。サンプルアプリからかけてみてください
class ConnectViewController: UIViewController {
//相手のVideoView
@IBOutlet weak var connectRemoteVideo: SKWVideo!
//jibunnnoVideoView
@IBOutlet weak var connectLocalVideo: SKWVideo!
//着信中のイメージ
@IBOutlet weak var connectingView: UIView!
fileprivate var connectWaitFlag:Bool = true
override func viewDidLoad() {
super.viewDidLoad()
//NavigationBarは非表示にする
self.navigationController?.setNavigationBarHidden(true, animated: false)
//セッションを開始する
SkywayManager.sharedManager().sessionStart(delegate: self)
}
@IBAction func tapConnectEnd(_ sender: Any) {
//通話中の場合は通話を終了する
if (!connectWaitFlag) {
SkywayManager.sharedManager().closeMedia(localView: connectLocalVideo, remoteView: connectRemoteVideo)
}
}
}
extension ConnectViewController:SkywaySessionDelegate {
func sessionStart() {
//Skywayとの接続が完了した場合に何か処理したい場合
}
func connectSucces() {
//着信中のUIを更新する
}
func remoteConnectSucces() {
//相手と接続できたのでRemoteのViewを更新する
SkywayManager.sharedManager().setRemoteView(remoteView: connectRemoteVideo)
}
func connectEnd() {
//skywayとのConnectを切断する
SkywayManager.sharedManager().sessionClose()
}
func connectDisconnect() {
//切断された場合のUI更新
//通話が終了しましたなど
}
func connectError() {
}
func getPeerList(newPeerList: NSMutableArray) {
//処理なし
}
}
問題点
Videoが画面全体もしくは、2分割で相手/自分と縦並びで表示しようとしたが、指定したサイズで表示できない
let bounds = UIScreen.main.nativeBounds
let constraints = SKWMediaConstraints()
constraints.maxFrameRate = 10
constraints.cameraMode = .CAMERA_MODE_ADJUSTABLE
constraints.cameraPosition = .CAMERA_POSITION_FRONT
constraints.minWidth = UInt(bounds.width)
constraints.minHeight = UInt(bounds.height / 2)
self.localStream = SKWNavigator.getUserMedia(constraints)
上記のように画面サイズで指定してみましたが、適用されないですね。以下のドキュメントにも
https://nttcom.github.io/skyway/docs/#iOS
max (Width x Height): 640x480, 352x288
min (Width x Height): 192x144, 352x288, 512x384, 640x480
とあるのでダメそう。改善してほしいな
音声を内蔵スピーカーで出力したい
Skywayの提供するAPIにて音声出力周りのものがなかったのでどうしようと思っていたが、結論から言うとAudioSessionを利用すればいけました
MEDIACONNECTION_EVENT_STREAMを受け取ったあとにdelayを置いてから
以下のようにすることで内蔵スピーカーから音が出ました。切断した場合は、setActiveをfalseにすれば良いかと思います
AVAudioSession.sharedInstance().setCategory(AVAudioSessionModeVoiceChat)
AVAudioSession.sharedInstance().overrideOutputAudioPort(AVAudioSessionPortOverride.speaker)
AVAudioSession.sharedInstance().setActive(true)
所感
サンプルを参考にswiftで組んでみましたが、簡単に無料通話アプリが実現できました。
サービスに適用できるかはまだ検討中ですが、他のサービスも検証して最終的に導入するかは判断したいと思います
参考サイト
https://nttcom.github.io/skyway/documentation.html
https://github.com/nttcom/SkyWay-iOS-Sample