Tạo ứng dụng gọi thoại cho iOS sử dụng skyway SDK

Tạo ứng dụng gọi thoại cho iOS sử dụng skyway SDK

Lời giới thiệu

Khi phát triển 1 ứng dụng chat voice cho điện thoại, chúng ta phải xây dựng rất nhiều thứ, từ server, viết API kết nối các thiết bị,….May mắn thay, gần đây chúng ta đã có 1 số framework làm hầu hết công việc đó như là Skyway, Twilio. Hôm nay chúng ta sẽ tập trung vào Skyway và sẽ nói về Twillo trong 1 bài viết khác.

Tổng quan, Skyway là 1 SDK đa nền tảng và đầy đủ dịch vụ API quản lý khiến việc xây dựng tính năng chat video hay voice chat thật đơn giản, bạn có thể tìm hiểu thêm trên trang chủ của Skyway SDK  .Trong bài viết này, chúng tôi sẽ hướng dẫn các bạn làm thế nào để xây dựng 1 ứng dụng voice chat trên iOS.

Tạo ApiKey trên trang Skyway

Đăng ký và login vào Skyway console.

Tại trang chủ của console tạo 1 ứng dụng mới tên là “DemoSkywayApp”

Sau khi tạo xong hãy lưu api key và domain bạn chọn vào 1 nơi nào đó để có thể sử dụng cho phía iOS.

Xây dựng dự án iOS

Tạo 1 dự án mới tên là “DemoChatApp”

Cấu hình  CocoaPods

Tạo 1 file tên là PodFile và đặt trong thư mục root của dự án và dán đoạn code sau vào

platform :ios,'9.0'
use_frameworks!
def install_pods
  pod 'SkyWay'
  pod 'MBProgressHUD' #for toast message
end

target 'DemoChatApp' do
  install_pods
end

 

Sau đó mở ứng dụng Terminal và chạy lệnh sau ở thư mục root của dự án để cài đặt Skyway

pod install

Bởi vì ứng dụng sẽ sử dụng micro của thiết bị nên chúng ta cần thêm quyền truy cập micro vào file info.plist của dự án.

NSMicrophoneUsageDescription
This sample uses a mic device for voice chatap

Áp dụng Skyway
Trước tiên tạo 1 controller tên là để xử lý các việc như thiết lập kết nối tới PeerServer, gia nhập phòng hay rời phòng.

import UIKit
import SkyWay
import MBProgressHUD

let SKYWAY_APIKEY = ""//api key từ trang skyway console
let SKYWAY_DOMAIN = ""//domain từ trang 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
    
    //Khởi tạo kết nối tới PeerServer
    func connectToPeerServer(handler:ConnectionHandler? = nil) {
        let options = SKWPeerOption()
        options.key = SKYWAY_APIKEY;
        options.domain = SKYWAY_DOMAIN;
        currentPeer = SKWPeer.init(options: options)
        
        //Xử lý sự kiện khi kết nối tới PeerServer đã được thiết lập
        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)
        })
                
        //Xử lý sự kiện khi kết nối tới PeerServer bị huỷ và không còn kết nối được nữa
        currentPeer?.on(.PEER_EVENT_CLOSE, callback: { (obj) in
            print("PEER_EVENT_CLOSE \(obj!)")
            //Huỷ stream data từ thiết bị tới PeerServer
            self.destroyLocalStream()
            handler?(false)
        })
        
        //Xử lý sự kiện khi bị mất kết nối tới PeerServer, có thể do user tự huỷ
        currentPeer?.on(.PEER_EVENT_DISCONNECTED, callback: { (obj) in
            print("PEER_EVENT_DISCONNECTED \(obj!)")
            self.connected = false
            self.destroyLocalStream()
            handler?(false)
        })
        
        //Xử lý sự kiện khi không thể kết nối tới PeerServer, có thể do đường truyền có vấn đề
        currentPeer?.on(.PEER_EVENT_ERROR, callback: { (obj) in
            print("PEER_EVENT_ERROR \(obj!)")
            self.connected = false
            self.destroyLocalStream()
            handler?(false)
        })
        
    }
    
    // Tham gia vào 1 room cụ thể
    func joinRoom(roomName:String, handler:@escaping RoomStatusUpdatedHandler) {
        //Nếu chưa kết nối tới PeerServer, hãy kết nối trước
        if(!connected){
            self.connectToPeerServer { (flag) -> (Void) in
                if(flag){
                    self.joinRoom(roomName: roomName, handler: handler)
                }
            }
            return
        }
        //Cấu hình room
        let option = SKWRoomOption()
        option.mode = .ROOM_MODE_MESH
        option.stream = localStream
        
        //Khởi tạo room
        currentRoom = currentPeer?.joinRoom(withName: roomName, options: option) as? SKWMeshRoom
        //Xử lý các sự kiện
        
        //Sự kiện khi chúng ta tham gia vào room thành công
        currentRoom?.on(.ROOM_EVENT_OPEN, callback: { (obj) in
            print("room \(roomName) opened")
            handler(.Opened)
        })
        
        //Sự kiện khi chúng ta rời khỏi room
        currentRoom?.on(.ROOM_EVENT_CLOSE, callback: { (obj) in
            self.currentRoom?.offAll()
            self.currentRoom = nil
            handler(.Closed)
        })
                
        //Sự kiện khi 1 người khác tham gia vào room
        currentRoom?.on(.ROOM_EVENT_PEER_JOIN, callback: { (obj) in
            let message = "peerId \(obj as! String) joined call"
            self.toastMessage(message: message)
            handler(.OtherJoined)
        })
        
        //Sự kiện khi 1 người khác rời khỏi room
        currentRoom?.on(.ROOM_EVENT_PEER_LEAVE, callback: { (obj) in
            let message = "peerId \(obj as! String) left call"
            self.toastMessage(message: message)
            handler(.OtherLeft)
        })
        //Sự kiện khi không thể thiết lập kết nối tới room
        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//trong bài viết này chúng ta chỉ sử dụng voice chat nên hãy set videoFlag thành false
        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)
        }
    }        
}

Chúng ta có 3 hàm chính:

  • connectToPeerServer()
  • joinRoom()
  • leaveRoom()

connectToPeerServer() cần được gọi đầu tiên để thiết lập kết nối tới PeerServer của Skyway, giá trị nhận về là peerId của chính chúng ta, đó là 1 chuỗi đại loại như kZoHgbFfTtgBeQhp. Trong hàm này chúng ta cần áp dụng các xử lý sự kiện sau:

  • PEER_EVENT_OPEN: Khi nối tới PeerServer thành công, tạo 1 kết nối stream tới PeerServer để truyền dữ liệu(âm thanh) lên đó, ở bài viết này chỉ tập trung vào voice chat vì vậy hãy disable biến videoFlag (mặc định là YES) .
  • PEER_EVENT_CLOSE: Khi user đóng kết nối hãy huỷ  kết nối stream và kết nối tới room nếu có.
  • PEER_EVENT_DISCONNECTED: Khi user mất kết nối, hãy làm thao tác tương tự như event trên.
  • PEER_EVENT_ERROR:  Khi kết nối bị lỗi, hãy làm thao tác tương tự như event trên.

joinRoom() được gọi khi bạn tham gia 1 room, tham số là tên room(có thể sử dụng tuỳ ý bất kỳ tên nào mà bạn muốn, nhưng bạn nên chọn 1 tên duy nhất để không ai có thể tham gia vào room ngoài người bạn mong muốn, việc này nên xử lý ở phía server để tạo 1 chuỗi duy nhất). Và tương tự như hàm connectToPeerServer(), bạn cần xử lý thêm các sự kiện sau:

  • ROOM_EVENT_OPEN:  Khi chúng ta tham gia room
  • ROOM_EVENT_CLOSE: Khi chúng ta rời room
  • ROOM_EVENT_PEER_JOIN: Khi  người khác tham gia room
  • ROOM_EVENT_PEER_LEAVE: Khi  người khác rời room
  • ROOM_EVENT_ERROR: Khi kết nối tới room bị lỗi(do mạng hay timeout,…)

Sau đó chúng tạo 1 view controller tên là CallViewController có giao diện đơn giản như sau:

Trong file CallViewController.swift , hãy dán đoạn code sau vào

import UIKit
import SkyWay

class CallViewController: UIViewController {
    
    @IBOutlet weak var joinButton: UIButton!
    @IBOutlet weak var roomTxt: UITextField!
        
    //Khi trạng thái của room thay đổi, cập nhật tiêu đề của button
    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()
        }
        
    }
        
}

 

Chúng ta đơn giản có 1 textfield để user nhập tên room mong muốn tham gia, với 1 button để tham gia hay rời room.

Giờ hãy chạy project trên 2 thiết bị iOS (ứng dụng này không hỗ trợ simulator), cùng kết nối 1 room và xem nó hoạt động như thế nào nhé.

Tham khảo:

Skyway iOS SDK 

Skyway Homepage