[{"data":1,"prerenderedAt":269},["ShallowReactive",2],{"news-item-\u002Fvi\u002Fnews\u002Fvideochat-skyway":3},{"id":4,"title":5,"body":6,"category":255,"created by":258,"date":259,"description":17,"extension":260,"meta":261,"navigation":262,"path":263,"sections":264,"seo":265,"stem":266,"thumbnail":267,"__hash__":268},"content_vi\u002Fvi\u002Fnews\u002Fvideochat-skyway.md","Tạo ứng dụng gọi thoại cho iOS sử dụng skyway SD",{"type":7,"value":8,"toc":249},"minimark",[9,14,18,29,33,42,51,61,64,68,72,75,79,85,88,98,101,107,111,114,120,126,132,135,148,158,176,186,203,210,215,218,224,227,230,235,243],[10,11,13],"h2",{"id":12},"lời-giới-thiệu","Lời giới thiệu",[15,16,17],"p",{},"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.",[15,19,20,21,28],{},"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 ",[22,23,27],"a",{"href":24,"rel":25},"https:\u002F\u002Fwebrtc.ecl.ntt.com\u002Fen\u002F",[26],"nofollow","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.",[10,30,32],{"id":31},"tạo-apikey-trên-trang-skyway","Tạo ApiKey trên trang Skyway",[15,34,35,36,41],{},"Đăng ký và login vào ",[22,37,40],{"href":38,"rel":39},"https:\u002F\u002Fconsole-webrtc-free.ecl.ntt.com\u002Fusers\u002Flogin",[26],"Skyway console",".",[15,43,44,45,50],{},"Tại trang chủ của ",[22,46,49],{"href":47,"rel":48},"https:\u002F\u002Fconsole-webrtc-free.ecl.ntt.com\u002F",[26],"console"," tạo 1 ứng dụng mới tên là \"DemoSkywayApp\"",[52,53],"img",{"className":54,"alt":58,"src":59,"style":60},[55,56,57],"block","mx-auto","mb-4","","https:\u002F\u002Fhomepage-media.s3.ap-southeast-1.amazonaws.com\u002Fwp-content\u002Fuploads\u002F2020\u002F07\u002F16110849\u002Fcreate_app-1024x889.png","width: 100%;",[15,62,63],{},"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.",[52,65],{"className":66,"alt":58,"src":67,"style":60},[55,56,57],"https:\u002F\u002Fhomepage-media.s3.ap-southeast-1.amazonaws.com\u002Fwp-content\u002Fuploads\u002F2020\u002F07\u002F16111503\u002FScreen-Shot-2020-07-16-at-11.14.44-1024x520.png",[10,69,71],{"id":70},"xây-dựng-dự-án-ios","Xây dựng dự án iOS",[15,73,74],{},"Tạo 1 dự án mới tên là \"DemoChatApp\"",[52,76],{"className":77,"alt":58,"src":78,"style":60},[55,56,57],"https:\u002F\u002Fhomepage-media.s3.ap-southeast-1.amazonaws.com\u002Fwp-content\u002Fuploads\u002F2020\u002F07\u002F16104243\u002Fcreate_ios_prj-1024x426.png",[15,80,81],{},[82,83,84],"strong",{},"Cấu hình  CocoaPods",[15,86,87],{},"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",[89,90,95],"pre",{"className":91,"code":93,"language":94},[92],"language-text","platform :ios,'9.0'\nuse_frameworks!\ndef install_pods\n  pod 'SkyWay'\n  pod 'MBProgressHUD' #for toast message\nend\n\ntarget 'DemoChatApp' do\n  install_pods\nend\n","text",[96,97,93],"code",{"__ignoreMap":58},[15,99,100],{},"Sau đó mở ứng dụng Terminal và chạy lệnh sau ở thư mục root của dự án để cài đặt Skyway",[89,102,105],{"className":103,"code":104,"language":94},[92],"pod install\n",[96,106,104],{"__ignoreMap":58},[52,108],{"className":109,"alt":58,"src":110,"style":60},[55,56,57],"https:\u002F\u002Fhomepage-media.s3.ap-southeast-1.amazonaws.com\u002Fwp-content\u002Fuploads\u002F2020\u002F07\u002F16105409\u002Fpod_install-1024x239.png",[15,112,113],{},"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.",[89,115,118],{"className":116,"code":117,"language":94},[92],"NSMicrophoneUsageDescription\nThis sample uses a mic device for voice chatap\n",[96,119,117],{"__ignoreMap":58},[15,121,122,125],{},[82,123,124],{},"Á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.",[89,127,130],{"className":128,"code":129,"language":94},[92],"import UIKit\nimport SkyWay\nimport MBProgressHUD\n\nlet SKYWAY_APIKEY = \"\"\u002F\u002Fapi key từ trang skyway console\nlet SKYWAY_DOMAIN = \"\"\u002F\u002Fdomain từ trang skyway console\n\nlet kSkywayController = SkywayController.shared\npublic enum RoomStatus{\n    case Opened\n    case Joined\n    case Left\n    case OtherJoined\n    case OtherLeft\n    case Disconnected\n    case Closed\n    case Error\n}\n\npublic typealias RoomStatusUpdatedHandler = (RoomStatus) -> (Void)\npublic typealias ConnectionHandler = (Bool) -> (Void)\n\nclass SkywayController: NSObject {\n    static let shared = SkywayController()\n    \n    public var currentPeer:SKWPeer? = nil\n    public var localStream:SKWMediaStream? = nil\n    public var currentRoom:SKWMeshRoom? = nil\n    \n    private var connected = false\n    \n    \u002F\u002FKhởi tạo kết nối tới PeerServer\n    func connectToPeerServer(handler:ConnectionHandler? = nil) {\n        let options = SKWPeerOption()\n        options.key = SKYWAY_APIKEY;\n        options.domain = SKYWAY_DOMAIN;\n        currentPeer = SKWPeer.init(options: options)\n        \n        \u002F\u002FXử lý sự kiện khi kết nối tới PeerServer đã được thiết lập\n        currentPeer?.on(.PEER_EVENT_OPEN, callback: { (obj) in\n            print(\"PEER_EVENT_OPEN \\(obj!)\")\n            \u002F\u002Finit data stream from local to PeerServer\n            self.connected = true\n            self.initLocalStream()\n            handler?(true)\n        })\n                \n        \u002F\u002FXử 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\n        currentPeer?.on(.PEER_EVENT_CLOSE, callback: { (obj) in\n            print(\"PEER_EVENT_CLOSE \\(obj!)\")\n            \u002F\u002FHuỷ stream data từ thiết bị tới PeerServer\n            self.destroyLocalStream()\n            handler?(false)\n        })\n        \n        \u002F\u002FXử lý sự kiện khi bị mất kết nối tới PeerServer, có thể do user tự huỷ\n        currentPeer?.on(.PEER_EVENT_DISCONNECTED, callback: { (obj) in\n            print(\"PEER_EVENT_DISCONNECTED \\(obj!)\")\n            self.connected = false\n            self.destroyLocalStream()\n            handler?(false)\n        })\n        \n        \u002F\u002FXử lý sự kiện khi không thể kết nối tới PeerServer, có thể do đường truyền có vấn đề\n        currentPeer?.on(.PEER_EVENT_ERROR, callback: { (obj) in\n            print(\"PEER_EVENT_ERROR \\(obj!)\")\n            self.connected = false\n            self.destroyLocalStream()\n            handler?(false)\n        })\n        \n    }\n    \n    \u002F\u002F Tham gia vào 1 room cụ thể\n    func joinRoom(roomName:String, handler:@escaping RoomStatusUpdatedHandler) {\n        \u002F\u002FNếu chưa kết nối tới PeerServer, hãy kết nối trước\n        if(!connected){\n            self.connectToPeerServer { (flag) -> (Void) in\n                if(flag){\n                    self.joinRoom(roomName: roomName, handler: handler)\n                }\n            }\n            return\n        }\n        \u002F\u002FCấu hình room\n        let option = SKWRoomOption()\n        option.mode = .ROOM_MODE_MESH\n        option.stream = localStream\n        \n        \u002F\u002FKhởi tạo room\n        currentRoom = currentPeer?.joinRoom(withName: roomName, options: option) as? SKWMeshRoom\n        \u002F\u002FXử lý các sự kiện\n        \n        \u002F\u002FSự kiện khi chúng ta tham gia vào room thành công\n        currentRoom?.on(.ROOM_EVENT_OPEN, callback: { (obj) in\n            print(\"room \\(roomName) opened\")\n            handler(.Opened)\n        })\n        \n        \u002F\u002FSự kiện khi chúng ta rời khỏi room\n        currentRoom?.on(.ROOM_EVENT_CLOSE, callback: { (obj) in\n            self.currentRoom?.offAll()\n            self.currentRoom = nil\n            handler(.Closed)\n        })\n                \n        \u002F\u002FSự kiện khi 1 người khác tham gia vào room\n        currentRoom?.on(.ROOM_EVENT_PEER_JOIN, callback: { (obj) in\n            let message = \"peerId \\(obj as! String) joined call\"\n            self.toastMessage(message: message)\n            handler(.OtherJoined)\n        })\n        \n        \u002F\u002FSự kiện khi 1 người khác rời khỏi room\n        currentRoom?.on(.ROOM_EVENT_PEER_LEAVE, callback: { (obj) in\n            let message = \"peerId \\(obj as! String) left call\"\n            self.toastMessage(message: message)\n            handler(.OtherLeft)\n        })\n        \u002F\u002FSự kiện khi không thể thiết lập kết nối tới room\n        currentRoom?.on(.ROOM_EVENT_ERROR, callback: { (obj) in\n            print(\"ROOM_EVENT_ERROR \\(obj)\")\n            handler(.Error)\n        })\n    }\n    \n    func leaveRoom()  {\n        self.currentRoom?.close()\n        self.currentRoom?.offAll()\n    }\n    \u002F\u002F MARK: - Private methods\n    private func initLocalStream(){\n        let constraint = SKWMediaConstraints()\n        constraint.audioFlag = true\n        constraint.videoFlag = false\u002F\u002Ftrong bài viết này chúng ta chỉ sử dụng voice chat nên hãy set videoFlag thành false\n        SKWNavigator.initialize(currentPeer!)\n        localStream = SKWNavigator.getUserMedia(constraint)\n    }\n    \n    private func destroyLocalStream(){\n        SKWNavigator.terminate()\n        localStream = nil\n        leaveRoom()\n    }\n    \n    private func toastMessage(message:String){\n        print(\"toastMessage \\(message)\")\n        DispatchQueue.main.asyncAfter(deadline: .now() ) {\n            let hud = MBProgressHUD.showAdded(to: UIApplication.shared.keyWindow!, animated: true)\n            hud.mode = .text\n            hud.label.text = message\n            hud.removeFromSuperViewOnHide = true\n            hud.hide(animated: true, afterDelay: 2)\n        }\n    }        \n}\n",[96,131,129],{"__ignoreMap":58},[15,133,134],{},"Chúng ta có 3 hàm chính:",[136,137,138,142,145],"ul",{},[139,140,141],"li",{},"connectToPeerServer()",[139,143,144],{},"joinRoom()",[139,146,147],{},"leaveRoom()",[15,149,150,153,154,157],{},[82,151,152],{},"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à ",[82,155,156],{},"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:",[136,159,160,167,170,173],{},[139,161,162,163,166],{},"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 ",[82,164,165],{},"videoFlag"," (mặc định là YES) .",[139,168,169],{},"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ó.",[139,171,172],{},"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.",[139,174,175],{},"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.",[15,177,178,181,182,185],{},[82,179,180],{},"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 ",[82,183,184],{},"connectToPeerServer(),"," bạn cần xử lý thêm các sự kiện sau:",[136,187,188,191,194,197,200],{},[139,189,190],{},"ROOM_EVENT_OPEN:  Khi chúng ta tham gia room",[139,192,193],{},"ROOM_EVENT_CLOSE: Khi chúng ta rời room",[139,195,196],{},"ROOM_EVENT_PEER_JOIN: Khi  người khác tham gia room",[139,198,199],{},"ROOM_EVENT_PEER_LEAVE: Khi  người khác rời room",[139,201,202],{},"ROOM_EVENT_ERROR: Khi kết nối tới room bị lỗi(do mạng hay timeout,...)",[15,204,205,206,209],{},"Sau đó chúng tạo 1 view controller tên là ",[82,207,208],{},"CallViewController"," có giao diện đơn giản như sau:",[52,211],{"className":212,"alt":58,"src":213,"style":214},[55,56,57],"https:\u002F\u002Fhomepage-media.s3.ap-southeast-1.amazonaws.com\u002Fwp-content\u002Fuploads\u002F2020\u002F07\u002F16165958\u002FIMG_9227-576x1024.png","width: 50%;",[15,216,217],{},"Trong file CallViewController.swift , hãy dán đoạn code sau vào",[89,219,222],{"className":220,"code":221,"language":94},[92],"import UIKit\nimport SkyWay\n\nclass CallViewController: UIViewController {\n    \n    @IBOutlet weak var joinButton: UIButton!\n    @IBOutlet weak var roomTxt: UITextField!\n        \n    \u002F\u002FKhi trạng thái của room thay đổi, cập nhật tiêu đề của button\n    private var joined = false{\n        didSet{\n            if joined {\n                joinButton.setTitle(\"End call\", for: .normal)\n            }else{\n                joinButton.setTitle(\"Join\", for: .normal)\n            }\n        }\n    }\n    override func viewDidLoad() {\n        super.viewDidLoad()\n        kSkywayController.connectToPeerServer()\n        \n    }\n    \n    @IBAction func joinRoom(_ sender: Any) {\n        if !joined{\n            kSkywayController.joinRoom(roomName: roomTxt.text!) { (status) -> (Void) in\n                if status == .Opened {\n                    self.joined = true\n                }\n                if status == .Closed || status == .Disconnected || status == .Error || status == .OtherLeft{\n                    self.joined = false\n                }\n            }\n        }else{\n            kSkywayController.leaveRoom()\n        }\n        \n    }\n        \n}\n",[96,223,221],{"__ignoreMap":58},[15,225,226],{},"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.",[15,228,229],{},"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é.",[15,231,232],{},[82,233,234],{},"Tham khảo:",[15,236,237,242],{},[22,238,241],{"href":239,"rel":240},"https:\u002F\u002Fgithub.com\u002Fskyway\u002Fskyway-ios-sdk",[26],"Skyway iOS SDK"," ",[15,244,245],{},[22,246,248],{"href":24,"rel":247},[26],"Skyway Homepage",{"title":58,"searchDepth":250,"depth":250,"links":251},2,[252,253,254],{"id":12,"depth":250,"text":13},{"id":31,"depth":250,"text":32},{"id":70,"depth":250,"text":71},[256,257],"tech talk","mobile","Briswell Vietnam Co Ltd","2020-08-10","md",{},true,"\u002Fvi\u002Fnews\u002Fvideochat-skyway",null,{"title":5,"description":17},"vi\u002Fnews\u002Fvideochat-skyway","https:\u002F\u002Fhomepage-media.s3.ap-southeast-1.amazonaws.com\u002Fwp-content\u002Fuploads\u002F2020\u002F08\u002F10091008\u002Fskyway-log.png","Kl34B59PCaSvxxoBiJuYvk7TT2woBGuSwmoRcNsmSEo",1785320528229]