Create voice chat app using Skyway SDK in iOS

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:

Skyway iOS SDK 

Skyway Homepage