Create voice chat app using Skyway SDK in iOS
Introduction
When developing a chat app in mobile, we have to setup many things from set-up server, write API to set up connections between users…Fortunately, we have some frameworks support we do that like Skyway, Twilio,.. This post will focus on Skyway, we will talk about Twilio in a different post.
Overview, SkyWay is a multi-platform SDK & fully managed API service that makes it easy to implement video and voice calling functionality in your applications as described in home page . It supports for building chat applications in iOS, Android, Web platforms. In this tutorial, we will show you how to implement on iOS.
Create ApiKey in Skyway site
Signup and login to Skyway console.
In console page, create a new application named “DemoSkywayApp”
After app created, save api key and your domain in somewhere so that you can use in client side.
Setup iOS project
Create a project called “DemoChatApp”
Configure CocoaPods
Create a file named PodFile put in root project’s directory and paste following commands:
platform :ios,'9.0' use_frameworks! def install_pods pod 'SkyWay' pod 'MBProgressHUD' #for toast message end target 'DemoChatApp' do install_pods end
Open Terminal app then run following command:
pod install
at root project’s directory to install Skyway
Because our application need to use micro for voice chat so we need to add micro permission to info.plist
NSMicrophoneUsageDescription This sample uses a mic device for voice chat
Implement Skyway
First we need a controller to handle setup connect to peer server, join room,.. named SkywayController.swift
import UIKit import SkyWay import MBProgressHUD let SKYWAY_APIKEY = ""//api key from skyway console let SKYWAY_DOMAIN = ""//domain from skyway console let kSkywayController = SkywayController.shared public enum RoomStatus{ case Opened case Joined case Left case OtherJoined case OtherLeft case Disconnected case Closed case Error } public typealias RoomStatusUpdatedHandler = (RoomStatus) -> (Void) public typealias ConnectionHandler = (Bool) -> (Void) class SkywayController: NSObject { static let shared = SkywayController() public var currentPeer:SKWPeer? = nil public var localStream:SKWMediaStream? = nil public var currentRoom:SKWMeshRoom? = nil private var connected = false //Initalize to connect to PeerServer func connectToPeerServer(handler:ConnectionHandler? = nil) { let options = SKWPeerOption() options.key = SKYWAY_APIKEY; options.domain = SKYWAY_DOMAIN; currentPeer = SKWPeer.init(options: options) //handle event when connection to the PeerServer is established currentPeer?.on(.PEER_EVENT_OPEN, callback: { (obj) in print("PEER_EVENT_OPEN \(obj!)") //init data stream from local to PeerServer self.connected = true self.initLocalStream() handler?(true) }) //handle event when the peer is destroyed and can no longer accept or create any new connections currentPeer?.on(.PEER_EVENT_CLOSE, callback: { (obj) in print("PEER_EVENT_CLOSE \(obj!)") //destroy data stream from local to PeerServer self.destroyLocalStream() handler?(false) }) //handle event when the peer is disconnected from the signalling server, either manually or because the connection to the signalling server was lost. currentPeer?.on(.PEER_EVENT_DISCONNECTED, callback: { (obj) in print("PEER_EVENT_DISCONNECTED \(obj!)") self.connected = false self.destroyLocalStream() handler?(false) }) //handle event when errors on the peer are almost always fatal and will destroy the peer. currentPeer?.on(.PEER_EVENT_ERROR, callback: { (obj) in print("PEER_EVENT_ERROR \(obj!)") self.connected = false self.destroyLocalStream() handler?(false) }) } //join to a specific room func joinRoom(roomName:String, handler:@escaping RoomStatusUpdatedHandler) { //if not connected, try re-connect to connectToPeerServer if(!connected){ self.connectToPeerServer { (flag) -> (Void) in if(flag){ self.joinRoom(roomName: roomName, handler: handler) } } return } //set room's options let option = SKWRoomOption() option.mode = .ROOM_MODE_MESH option.stream = localStream //init room with roomName as parameter currentRoom = currentPeer?.joinRoom(withName: roomName, options: option) as? SKWMeshRoom //setup event handlers //event when we join the room currentRoom?.on(.ROOM_EVENT_OPEN, callback: { (obj) in print("room \(roomName) opened") handler(.Opened) }) //event when we left the room currentRoom?.on(.ROOM_EVENT_CLOSE, callback: { (obj) in self.currentRoom?.offAll() self.currentRoom = nil handler(.Closed) }) //event when other member joined the room currentRoom?.on(.ROOM_EVENT_PEER_JOIN, callback: { (obj) in let message = "peerId \(obj as! String) joined call" self.toastMessage(message: message) handler(.OtherJoined) }) //event when other member left the room currentRoom?.on(.ROOM_EVENT_PEER_LEAVE, callback: { (obj) in let message = "peerId \(obj as! String) left call" self.toastMessage(message: message) handler(.OtherLeft) }) currentRoom?.on(.ROOM_EVENT_ERROR, callback: { (obj) in print("ROOM_EVENT_ERROR \(obj)") handler(.Error) }) } func leaveRoom() { self.currentRoom?.close() self.currentRoom?.offAll() } // MARK: - Private methods private func initLocalStream(){ let constraint = SKWMediaConstraints() constraint.audioFlag = true constraint.videoFlag = false//in this tutorial we just use voice chat, so we skip videoFlag SKWNavigator.initialize(currentPeer!) localStream = SKWNavigator.getUserMedia(constraint) } private func destroyLocalStream(){ SKWNavigator.terminate() localStream = nil leaveRoom() } private func toastMessage(message:String){ print("toastMessage \(message)") DispatchQueue.main.asyncAfter(deadline: .now() ) { let hud = MBProgressHUD.showAdded(to: UIApplication.shared.keyWindow!, animated: true) hud.mode = .text hud.label.text = message hud.removeFromSuperViewOnHide = true hud.hide(animated: true, afterDelay: 2) } } }
We have 3 main functions:
- connectToPeerServer()
- joinRoom()
- leaveRoom()
connectToPeerServer() need to be called first to establish connection with PeerServer of Skyway, it will return a string called peerId, which a string like this kZoHgbFfTtgBeQhp, then you need to implement call back events:
- PEER_EVENT_OPEN: When user connection established successfully, set a stream connection from our device to PeerServer to send data(voice data) to PeerServer, because in this tutorial we just implement voice chat so we disable videoFlag (default is YES).
- PEER_EVENT_CLOSE: When user lost connection, destroy local data stream and room connection also if connected a room.
- PEER_EVENT_DISCONNECTED: When user disconnected, handle same like above event.
- PEER_EVENT_ERROR: When connection got error, handle same like above event.
joinRoom() is called when you expect to join a room, parameter is room’s name (you can use any name as you want, but you should choose a unique name so no one else can join, this step should be handled in server to render unique string). And like connectToPeerServer(), you need to handle call back events:
- ROOM_EVENT_OPEN: When we joined room.
- ROOM_EVENT_CLOSE: When we left room.
- ROOM_EVENT_PEER_JOIN: When other users joined room.
- ROOM_EVENT_PEER_LEAVE: When other users left room.
- ROOM_EVENT_ERROR: When connection to room got error (disconnected or timeout problem)
Then we create a sample view controller named CallViewController like following
in CallViewController.swift we include following code
import UIKit import SkyWay class CallViewController: UIViewController { @IBOutlet weak var joinButton: UIButton! @IBOutlet weak var roomTxt: UITextField! //when joined status update, update button title also private var joined = false{ didSet{ if joined { joinButton.setTitle("End call", for: .normal) }else{ joinButton.setTitle("Join", for: .normal) } } } override func viewDidLoad() { super.viewDidLoad() kSkywayController.connectToPeerServer() } @IBAction func joinRoom(_ sender: Any) { if !joined{ kSkywayController.joinRoom(roomName: roomTxt.text!) { (status) -> (Void) in if status == .Opened { self.joined = true } if status == .Closed || status == .Disconnected || status == .Error || status == .OtherLeft{ self.joined = false } } }else{ kSkywayController.leaveRoom() } } }
Simply, we have a textfield so that user can input room’s name, with a button to Join or End Call (we detect the room’s status has changed to update button’s title and call function joinRoom() or leaveRoom() properly)
Now you run project in 2 device (simulator not supported) connected with the same room name to check how it works.
Refs: