Further implement CallVC

pull/560/head
Niels Andriesse 3 years ago
parent eb2cba7410
commit f1f48ec865

@ -253,6 +253,8 @@
B8B558F926C4CE6800693325 /* CallVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558F826C4CE6800693325 /* CallVC.swift */; };
B8B558FB26C4D25C00693325 /* MockWebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558FA26C4D25C00693325 /* MockWebSocket.swift */; };
B8B558FD26C4D35400693325 /* MockCallServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558FC26C4D35400693325 /* MockCallServer.swift */; };
B8B558FF26C4E05E00693325 /* CallManager+Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B558FE26C4E05E00693325 /* CallManager+Messages.swift */; };
B8B5590126C4E2A400693325 /* SignalingMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B5590026C4E2A400693325 /* SignalingMessage.swift */; };
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BB82A4238F627000BA5194 /* HomeVC.swift */; };
B8BC00C0257D90E30032E807 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC00BF257D90E30032E807 /* General.swift */; };
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; };
@ -1227,6 +1229,8 @@
B8B558F826C4CE6800693325 /* CallVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVC.swift; sourceTree = "<group>"; };
B8B558FA26C4D25C00693325 /* MockWebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebSocket.swift; sourceTree = "<group>"; };
B8B558FC26C4D35400693325 /* MockCallServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCallServer.swift; sourceTree = "<group>"; };
B8B558FE26C4E05E00693325 /* CallManager+Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CallManager+Messages.swift"; sourceTree = "<group>"; };
B8B5590026C4E2A400693325 /* SignalingMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalingMessage.swift; sourceTree = "<group>"; };
B8B5BCEB2394D869003823C9 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
B8BAC75B2695645400EA1759 /* hr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hr; path = hr.lproj/Localizable.strings; sourceTree = "<group>"; };
B8BAC75C2695648500EA1759 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = "<group>"; };
@ -2365,10 +2369,12 @@
children = (
B8DE1FB326C22F2F0079C9CE /* CallManager.swift */,
B806ECA026C4A7E4008BDA44 /* CallManager+UI.swift */,
B8B558FE26C4E05E00693325 /* CallManager+Messages.swift */,
B806ECA226C4A8C6008BDA44 /* MockTURNServer.swift */,
B8B558F226C4CA4600693325 /* MockCallConfig.swift */,
B8B558FA26C4D25C00693325 /* MockWebSocket.swift */,
B8B558FC26C4D35400693325 /* MockCallServer.swift */,
B8B5590026C4E2A400693325 /* SignalingMessage.swift */,
);
path = Calls;
sourceTree = "<group>";
@ -4682,6 +4688,7 @@
C352A32F2557549C00338F3E /* NotifyPNServerJob.swift in Sources */,
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */,
C300A5F22554B09800555489 /* MessageSender.swift in Sources */,
B8B558FF26C4E05E00693325 /* CallManager+Messages.swift in Sources */,
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
@ -4803,6 +4810,7 @@
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
B8B5590126C4E2A400693325 /* SignalingMessage.swift in Sources */,
B806ECA126C4A7E4008BDA44 /* CallManager+UI.swift in Sources */,
C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */,
C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */,

@ -2,7 +2,16 @@ import UIKit
import AVFoundation
import WebRTC
final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate, CallManagerDelegate {
final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate, CallManagerDelegate, MockWebSocketDelegate {
private let videoCallVC = VideoCallVC()
private var messageQueue: [String] = []
private var isConnected = false {
didSet {
let title = isConnected ? "Leave" : "Join"
joinOrLeaveButton.setTitle(title, for: UIControl.State.normal)
}
}
private var currentRoomInfo: RoomInfo?
// MARK: UI Components
private lazy var previewView: UIImageView = {
@ -13,7 +22,7 @@ final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate
return UIView()
}()
private lazy var joinButton: UIButton = {
private lazy var joinOrLeaveButton: UIButton = {
let result = UIButton()
result.setTitle("Join", for: UIControl.State.normal)
return result
@ -31,6 +40,7 @@ final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate
override func viewDidLoad() {
super.viewDidLoad()
setUpCamera()
embedVideoCallVC()
}
private func setUpCamera() {
@ -38,6 +48,13 @@ final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate
CameraManager.shared.prepare()
}
private func embedVideoCallVC() {
addChild(videoCallVC)
containerView.addSubview(videoCallVC.view)
videoCallVC.view.translatesAutoresizingMaskIntoConstraints = false
videoCallVC.view.pin(to: containerView)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
CameraManager.shared.start()
@ -48,7 +65,94 @@ final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate
CameraManager.shared.stop()
}
// MARK: Interaction
@objc private func joinOrLeave() {
guard let roomID = roomNumberTextField.text, !roomID.isEmpty else { return }
if isConnected {
disconnect()
} else {
isConnected = true
MockCallServer.join(roomID: roomID).done2 { [weak self] info in
guard let self = self else { return }
self.log("Successfully joined room.")
self.currentRoomInfo = info
if let messages = info.messages {
self.handle(messages)
}
MockWebSocket.shared.delegate = self
MockWebSocket.shared.connect(url: URL(string: info.wssURL)!)
}.catch2 { [weak self] error in
guard let self = self else { return }
self.isConnected = false
self.log("Couldn't join room due to error: \(error).")
SNLog("Couldn't join room due to error: \(error).")
}
roomNumberTextField.resignFirstResponder()
}
}
private func disconnect() {
guard let info = currentRoomInfo else { return }
MockCallServer.leave(roomID: info.roomID, userID: info.clientID).done2 { [weak self] in
guard let self = self else { return }
self.log("Disconnected.")
}
let message = [ "type": "bye" ]
guard let data = try? JSONSerialization.data(withJSONObject: message, options: [.prettyPrinted]) else { return }
MockWebSocket.shared.send(data)
MockWebSocket.shared.delegate = nil
currentRoomInfo = nil
isConnected = false
CallManager.shared.endCall()
}
// MARK: Message Handling
func handle(_ messages: [String]) {
messageQueue.append(contentsOf: messages)
drainMessageQueue()
}
func drainMessageQueue() {
guard isConnected else { return }
for message in messageQueue {
handle(message)
}
messageQueue.removeAll()
CallManager.shared.drainMessageQueue()
}
func handle(_ message: String) {
let signalingMessage = SignalingMessage.from(message: message)
switch signalingMessage {
case .candidate(let candidate):
CallManager.shared.handleCandidateMessage(candidate)
log("Candidate received.")
case .answer(let answer):
CallManager.shared.handleRemoteDescription(answer)
log("Answer received.")
case .offer(let offer):
CallManager.shared.handleRemoteDescription(offer)
log("Offer received.")
case .bye:
disconnect()
default:
break
}
}
// MARK: Streaming
func webSocketDidConnect(_ webSocket: MockWebSocket) {
}
func webSocket(_ webSocket: MockWebSocket, didReceive data: String) {
}
func webSocketDidDisconnect(_ webSocket: MockWebSocket) {
}
func callManager(_ callManager: CallManager, sendData data: Data) {
// TODO: Implement
}
@ -64,7 +168,7 @@ final class MainChatRoomViewController : UIViewController, CameraCaptureDelegate
}
// MARK: Logging
private func log(string: String) {
private func log(_ string: String) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.infoTextView.text = self.infoTextView.text + "\n" + string

@ -0,0 +1,28 @@
import WebRTC
extension CallManager {
public func handleCandidateMessage(_ candidate: RTCIceCandidate) {
candidateQueue.append(candidate)
}
public func handleRemoteDescription(_ sdp: RTCSessionDescription) {
peerConnection.setRemoteDescription(sdp, completionHandler: { [weak self] error in
if let error = error {
SNLog("Couldn't set SDP due to error: \(error).")
} else {
guard let self = self else { return }
if sdp.type == .offer, self.peerConnection.localDescription == nil {
self.acceptCall()
}
}
})
}
public func drainMessageQueue() {
for candidate in candidateQueue {
peerConnection.add(candidate)
}
candidateQueue.removeAll()
}
}

@ -9,6 +9,7 @@ public protocol CallManagerDelegate : AnyObject {
/// See https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription for more information.
public final class CallManager : NSObject, RTCPeerConnectionDelegate {
public weak var delegate: CallManagerDelegate?
internal var candidateQueue: [RTCIceCandidate] = []
internal lazy var factory: RTCPeerConnectionFactory = {
RTCInitializeSSL()
@ -21,7 +22,7 @@ public final class CallManager : NSObject, RTCPeerConnectionDelegate {
/// remote peer, maintain and monitor the connection, and close the connection once it's no longer needed.
internal lazy var peerConnection: RTCPeerConnection = {
let configuration = RTCConfiguration()
// TODO: Configure
configuration.iceServers = [ RTCIceServer(urlStrings: MockCallConfig.default.webRTCICEServers) ]
// TODO: Do these constraints make sense?
let constraints = RTCMediaConstraints(mandatoryConstraints: [:], optionalConstraints: [ "DtlsSrtpKeyAgreement" : "true" ])
return factory.peerConnection(with: configuration, constraints: constraints, delegate: self)
@ -105,8 +106,10 @@ public final class CallManager : NSObject, RTCPeerConnectionDelegate {
public static let shared = CallManager()
// MARK: Call Management
public func initiateCall(with publicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
public func initiateCall() -> Promise<Void> {
/*
guard let thread = TSContactThread.fetch(for: publicKey, using: transaction) else { return Promise(error: Error.noThread) }
*/
let (promise, seal) = Promise<Void>.pending()
peerConnection.offer(for: constraints) { [weak self] sdp, error in
if let error = error {
@ -119,18 +122,22 @@ public final class CallManager : NSObject, RTCPeerConnectionDelegate {
return seal.reject(error)
}
}
/*
let message = CallMessage()
message.type = .offer
message.sdp = sdp.sdp
MessageSender.send(message, in: thread, using: transaction)
*/
seal.fulfill(())
}
}
return promise
}
public func acceptCall(with publicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise<Void> {
public func acceptCall() -> Promise<Void> {
/*
guard let thread = TSContactThread.fetch(for: publicKey, using: transaction) else { return Promise(error: Error.noThread) }
*/
let (promise, seal) = Promise<Void>.pending()
peerConnection.answer(for: constraints) { [weak self] sdp, error in
if let error = error {
@ -143,10 +150,12 @@ public final class CallManager : NSObject, RTCPeerConnectionDelegate {
return seal.reject(error)
}
}
/*
let message = CallMessage()
message.type = .answer
message.sdp = sdp.sdp
MessageSender.send(message, in: thread, using: transaction)
*/
seal.fulfill(())
}
}

@ -7,7 +7,7 @@ public struct RoomInfo {
public let wssPostURL: String
public let clientID: String
public let isInitiator: String
public let messages: [String]
public let messages: [String]?
}
public enum MockCallServer {
@ -34,8 +34,8 @@ public enum MockCallServer {
let wssURL = info["wss_url"] as? String,
let wssPostURL = info["wss_post_url"] as? String,
let clientID = info["client_id"] as? String,
let isInitiator = info["is_initiator"] as? String,
let messages = info["messages"] as? [String] else { throw HTTP.Error.invalidJSON }
let isInitiator = info["is_initiator"] as? String else { throw HTTP.Error.invalidJSON }
let messages = info["messages"] as? [String]
return RoomInfo(roomID: roomID, wssURL: wssURL, wssPostURL: wssPostURL,
clientID: clientID, isInitiator: isInitiator, messages: messages)
}

@ -1,34 +1,38 @@
import Foundation
import SocketRocket
protocol MockWebSocketDelegate : AnyObject {
public protocol MockWebSocketDelegate : AnyObject {
func webSocketDidConnect(_ webSocket: MockWebSocket)
func webSocketDidDisconnect(_ webSocket: MockWebSocket)
func webSocket(_ webSocket: MockWebSocket, didReceive data: String)
}
final class MockWebSocket : NSObject {
weak var delegate: MockWebSocketDelegate?
var socket: SRWebSocket?
public final class MockWebSocket : NSObject {
public weak var delegate: MockWebSocketDelegate?
private var socket: SRWebSocket?
var isConnected: Bool {
public var isConnected: Bool {
return socket != nil
}
func connect(url: URL) {
private override init() { }
public static let shared = MockWebSocket()
public func connect(url: URL) {
socket = SRWebSocket(url: url)
socket?.delegate = self
socket?.open()
}
func disconnect() {
public func disconnect() {
socket?.close()
socket = nil
delegate?.webSocketDidDisconnect(self)
}
func send(data: Data) {
public func send(_ data: Data) {
guard let socket = socket else { return }
socket.send(data)
}
@ -36,21 +40,21 @@ final class MockWebSocket : NSObject {
extension MockWebSocket : SRWebSocketDelegate {
func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {
public func webSocket(_ webSocket: SRWebSocket!, didReceiveMessage message: Any!) {
guard let message = message as? String else { return }
delegate?.webSocket(self, didReceive: message)
}
func webSocketDidOpen(_ webSocket: SRWebSocket!) {
public func webSocketDidOpen(_ webSocket: SRWebSocket!) {
delegate?.webSocketDidConnect(self)
}
func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) {
public func webSocket(_ webSocket: SRWebSocket!, didFailWithError error: Error!) {
SNLog("Web socket failed with error: \(error?.localizedDescription ?? "nil").")
self.disconnect()
}
func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {
public func webSocket(_ webSocket: SRWebSocket!, didCloseWithCode code: Int, reason: String!, wasClean: Bool) {
SNLog("Web socket closed.")
self.disconnect()
}

@ -0,0 +1,59 @@
import Foundation
import WebRTC
public enum SignalingMessage {
case none
case candidate(_ message: RTCIceCandidate)
case answer(_ message: RTCSessionDescription)
case offer(_ message: RTCSessionDescription)
case bye
public static func from(message: String) -> SignalingMessage {
guard let data = message.data(using: String.Encoding.utf8) else { return .none }
guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? JSON else { return .none }
let messageAsJSON: JSON
if let foo = json["msg"] as? String {
guard let data = foo.data(using: String.Encoding.utf8) else { return .none }
guard let bar = try? JSONSerialization.jsonObject(with: data, options: []) as? JSON else { return .none }
messageAsJSON = bar
} else {
messageAsJSON = json
}
guard let type = messageAsJSON["type"] as? String else { return .none }
switch type {
case "candidate":
guard let candidate = RTCIceCandidate.candidate(from: messageAsJSON) else { return .none }
return .candidate(candidate)
case "answer":
guard let sdp = messageAsJSON["sdp"] as? String else { return .none }
return .answer(RTCSessionDescription(type: .answer, sdp: sdp))
case "offer":
guard let sdp = messageAsJSON["sdp"] as? String else { return .none }
return .offer(RTCSessionDescription(type: .offer, sdp: sdp))
case "bye":
return .bye
default: return .none
}
}
}
extension RTCIceCandidate {
public func serialize() -> Data? {
let json = [
"type": "candidate",
"label": "\(sdpMLineIndex)",
"id": sdpMid,
"candidate": sdp
]
return try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted])
}
static func candidate(from json: JSON) -> RTCIceCandidate? {
let sdp = json["candidate"] as? String
let sdpMid = json["id"] as? String
let labelStr = json["label"] as? String
let label = (json["label"] as? Int32) ?? 0
return RTCIceCandidate(sdp: sdp ?? "", sdpMLineIndex: Int32(labelStr ?? "") ?? label, sdpMid: sdpMid)
}
}
Loading…
Cancel
Save