Skip to content

Instantly share code, notes, and snippets.

@saroar
Last active April 25, 2019 16:59
Show Gist options
  • Save saroar/0e7aa0e0e6122786ebbecb617e78d0bf to your computer and use it in GitHub Desktop.
Save saroar/0e7aa0e0e6122786ebbecb617e78d0bf to your computer and use it in GitHub Desktop.
CallManager
//
// CallManager.swift
// AddaIOS
//
// Created by Alif on 30/1/19.
// Copyright © 2019 Alif. All rights reserved.
//
import UIKit
import CallKit
import Starscream
import WebRTC
@available(iOS 10.0, *)
class CallManager {
var uuid = ""
var callerID = ""
var receiverID = ""
var receiverName = ""
var mobileNumber = ""
var receiverAvatar = PerfectLocalAuth.avatarUrl
var videoTrack = false
var isCallActive = false
var isOutGoingCall = false
static let unansweredTimeoutInterval: TimeInterval = 60
private var unansweredTimer: Timer?
private var socket = WebSocketTracker()
private var webRTCClient: WebRTCClient!
struct Static {
fileprivate static var instance: CallManager?
}
class var sharedInstance: CallManager {
if Static.instance == nil {
Static.instance = CallManager()
}
return Static.instance!
}
func dispose() {
log.info(#line, "Disposed Singleton instance")
endCall()
webRTCClient.disconnect()
// isMuted = false
// usesSpeaker = false
delay(1.0) { self.socket.disconnect() }
CallManager.Static.instance = nil
}
private let queue = DispatchQueue(label: "me.adda.call-manager")
private let ringtonePlayer = try? AVAudioPlayer(
contentsOf: Bundle.main.url(forResource: "incoming", withExtension: "wav")!
)
var callsChangedHandler: (() -> Void)?
private(set) var calls = [Call]()
private let callController = CXCallController()
// var isMuted = false {
// didSet {
// guard webRTCClient.iceConnectionState == .connected else {
// return
// }
// webRTCClient.isMuted = isMuted
// }
// }
//
// var usesSpeaker = false {
// didSet {
// guard webRTCClient.iceConnectionState == .connected else {
// return
// }
//
// queue.async {
// try? AVAudioSession.sharedInstance().overrideOutputAudioPort(self.portOverride)
// }
// }
// }
init() {
self.webRTCClient = WebRTCClient()
self.webRTCClient.delegate = self
NotificationCenter.default.addObserver(
self,
selector: #selector(audioSessionRouteChange(_:)),
name: AVAudioSession.routeChangeNotification,
object: nil
)
socket.addListener(self)
if socket.isConnected() {
socket.disconnect()
}
_ = socket.connect(link: "call", protocolName: "call")
_ = CallerAndReceiverID.instent.callerIDAndReceiverID(
isOutGoingCall: isOutGoingCall,
callerID: callerID,
receiverID: receiverID
)
}
deinit {
log.info(#line, "Deinit from CallManager")
NotificationCenter.default.removeObserver(self)
}
lazy var storyBoard: UIStoryboard = UIStoryboard(name: "Contacts", bundle: nil)
// swiftlint:disable force_cast
lazy var callVC: CallVC = {
return storyBoard.instantiateViewController(withIdentifier: "CallVC") as! CallVC
}()
func moveToCallingScreen() {
if let window = AppDelegate.shared.window,
let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentedController = currentController.presentedViewController {
currentController = presentedController
}
delay(1.5) {
UIApplication.shared.endIgnoringInteractionEvents()
currentController.modalTransitionStyle = .flipHorizontal
currentController.showDetailViewController(self.callVC, sender: Any.self)
//currentController.present(self.callVC, animated: true, completion: nil)
self.invalidateUnansweredTimeoutTimerAndSetNil()
}
}
}
func endCall() {
guard let uuid = UUID(uuidString: uuid) else { log.info(#line, "missing uuid"); return }
let call = Call(uuid: uuid, callerID: "", receiverID: "", mobileNumber: "", receiverAvatar: "", firstname: "", hasVideo: false, isOutgoing: false)
end(call)
guard let addaCallVC = storyBoard.instantiateViewController(withIdentifier: "AddaCallVC") as? AddaCallVC else {
log.error(#line, "AddaCallVC cant find fatalError")
fatalError("AddaCallVC cant find")
}
guard let callVC = storyBoard.instantiateViewController(withIdentifier: "CallVC") as? CallVC else {
log.error(#line, "CallVC cant find fatalError")
fatalError("CallVC cant find")
}
if let window = AppDelegate.shared.window,
let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentedController = currentController.presentedViewController {
currentController = presentedController
}
delay(0.5) {
UIApplication.shared.endIgnoringInteractionEvents()
if currentController.nibName == addaCallVC.nibName {
currentController.dismiss(animated: true, completion: nil)
}
if currentController.nibName == callVC.nibName {
currentController.dismiss(animated: true, completion: nil)
}
}
}
}
// MARK: Actions
func answerCall() {
self.webRTCClient.answer { (localSdp) in
let sessionDescription = RTCSessionDescription(
type: .answer,
sdp: localSdp.sdp
)
let sdp = SDP.init(sdp: sessionDescription.sdp)
let receiver = CallSignaling(callerID: CallerAndReceiverID.instent.callerID, receiverID: CallerAndReceiverID.instent.receiverID, isOutgoing: true, calling: true, type: SignalType.answer.rawValue, sessionDescription: sdp)
self.socket.send(receiver.jsonString!)
}
}
func hangupCall() {
let callSignalMsg = CallSignaling(
uuid: uuid,
userName: PerfectLocalAuth.username,
callerID: CallerAndReceiverID.instent.callerID,
receiverID: CallerAndReceiverID.instent.receiverID,
receiverAvatar: receiverAvatar,
type: SignalType.leave.rawValue)
self.socket.send(callSignalMsg.jsonString!)
// if webRTCClient.isConnected {
// webRTCClient.disconnect()
// }
}
@objc private func audioSessionRouteChange(_ notification: Notification) {
// guard call != nil else {
// return
// }
// try? AVAudioSession.sharedInstance().overrideOutputAudioPort(portOverride)
}
}
extension CallManager {
// MARK: Call Managment
static let CallsChangeNotification = Notification.Name("CallManagerCallsChangedNotification")
func setupTimeoutTimer() {
let timer = Timer(
timeInterval: CallManager.unansweredTimeoutInterval,
target: self,
selector: #selector(self.unansweredTimeout),
userInfo: nil,
repeats: false)
RunLoop.main.add(timer, forMode: .default)
self.unansweredTimer = timer
}
private func invalidateUnansweredTimeoutTimerAndSetNil() {
unansweredTimer?.invalidate()
unansweredTimer = nil
}
@objc private func unansweredTimeout() {
// guard let call = call, !call.hasReceivedRemoteAnswer else {
// return
// }
endCall()
webRTCClient.disconnect()
//isMuted = false
queue.async {
self.ringtonePlayer?.stop()
let call = Call.init(
uuid: UUID(uuidString: self.uuid)!,
callerID: CallerAndReceiverID.instent.callerID,
receiverID: CallerAndReceiverID.instent.receiverID,
mobileNumber: "",
receiverAvatar: self.receiverAvatar,
firstname: PerfectLocalAuth.realname(),
hasVideo: self.videoTrack,
isOutgoing: self.isOutGoingCall)
call.end()
self.end(call)
}
}
private func clean() {
webRTCClient.disconnect()
// call = nil
// isMuted = false
// usesSpeaker = false
invalidateUnansweredTimeoutTimerAndSetNil()
// performSynchronouslyOnMainThread {
// view.dismiss()
// }
}
func startCall(uuid: String, handle: String, videoEnabled: Bool) {
// pre-heat the AVAudioSession
configureAudioSession()
let handle = CXHandle(type: .phoneNumber, value: handle)
let startCallAction = CXStartCallAction(call: UUID(uuidString: uuid)!, handle: handle)
startCallAction.isVideo = videoEnabled
let transaction = CXTransaction()
transaction.addAction(startCallAction)
requestTransaction(transaction)
}
func setHeld(call: Call, onHold: Bool) {
let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
let transaction = CXTransaction()
transaction.addAction(setHeldCallAction)
requestTransaction(transaction)
}
func requestTransaction(_ transaction: CXTransaction) {
callController.request(transaction) { error in
if let error = error {
log.error("Error requesting transaction: \(error.localizedDescription)")
} else {
log.info("Requested transaction successfully")
}
}
}
func callWithUUID(uuid: UUID) -> Call? {
guard let index = calls.index(where: { $0.uuid == uuid }) else {
return nil
}
return calls[index]
}
func add(_ call: Call) {
log.info(#line, "add \(call.uuid)")
calls.append(call)
call.stateDidChange = { [weak self] in
guard let strongSelf = self else { return }
strongSelf.callsChangedHandler?()
strongSelf.postCallChangeNotification()
}
callsChangedHandler?()
}
func end(_ call: Call) {
log.info(#line, "end \(call.uuid)")
let endCallAction = CXEndCallAction(call: call.uuid)
let transaction = CXTransaction(action: endCallAction)
requestTransaction(transaction)
postCallChangeNotification()
webRTCClient.disconnect()
}
func remove(_ call: Call) {
guard let index = calls.index(where: { $0 === call}) else {
return
}
calls.remove(at: index)
callsChangedHandler?()
postCallChangeNotification()
webRTCClient.disconnect()
}
func removeAllCalls() {
calls.removeAll()
callsChangedHandler?()
postCallChangeNotification()
}
private func postCallChangeNotification() {
NotificationCenter.default.post(
name: type(of: self).CallsChangeNotification, object: self
)
}
func playRingtone(usesSpeaker: Bool) {
if usesSpeaker {
try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [])
} else {
try? AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .voiceChat, options: [])
}
ringtonePlayer?.play()
}
}
extension CallManager: WebSocketEventsListener {
func webSocketEventsListenerDidConnect(_ webSocketTrackerService: WebSocketTracker) {
_ = CallerAndReceiverID.instent.callerIDAndReceiverID(
isOutGoingCall: isOutGoingCall,
callerID: callerID,
receiverID: receiverID
)
let signalingMessage = CallSignaling(
uuid: uuid,
userName: PerfectLocalAuth.username,
callerID: CallerAndReceiverID.instent.callerID,
receiverID: CallerAndReceiverID.instent.receiverID,
receiverAvatar: receiverAvatar,
mobileNumber: mobileNumber,
hasVideoAndDataChannel: videoTrack,
calling: false,
isReceiverReadyToPickUpCall: false,
type: SignalType.connect.rawValue)
socket.sendData(signalingMessage.jsonData!)
print(#line, "after connet send all information to caller \(signalingMessage.jsonString!)")
}
func webSocketEventsListenerDidDisconnect(_ webSocketTrackerService: WebSocketTracker, didDisconnectError: Error?) {
log.info("DidDisconnect", context: didDisconnectError.debugDescription)
guard let uuid = UUID(uuidString: uuid) else { log.info(#line, "missing uuid"); return }
let call = Call.init(
uuid: uuid,
callerID: CallerAndReceiverID.instent.callerID,
receiverID: CallerAndReceiverID.instent.receiverID,
mobileNumber: mobileNumber,
receiverAvatar: receiverAvatar,
firstname: PerfectLocalAuth.realname(),
hasVideo: videoTrack,
isOutgoing: false)
call.end()
self.end(call)
}
func webSocketEventsListenerDidRecieveError(_ webSocketTrackerService: WebSocketTracker, didRecieveError: Error) {
log.info("DidRecieveError", context: didRecieveError.localizedDescription)
}
func webSocketEventsListenerDidReceiveMessage(_ webSocketTrackerService: WebSocketTracker, didRecieveMessage text: String) {
print(#line, "read text: \(text)")
guard let data = text.data(using: .utf8) else {
print(#line, "Wrong encoding for received message")
return
}
do {
let signalingMessage = try JSONDecoder().decode(CallSignaling.self, from: data)
switch signalingMessage.type {
case SignalType.connect.rawValue:
print(#line, "connect")
let signalingMessage = CallSignaling(
uuid: uuid,
userName: PerfectLocalAuth.username,
callerID: CallerAndReceiverID.instent.callerID,
receiverID: CallerAndReceiverID.instent.receiverID,
receiverAvatar: receiverAvatar,
mobileNumber: mobileNumber,
hasVideoAndDataChannel: videoTrack,
calling: false,
isReceiverReadyToPickUpCall: true,
type: SignalType.call.rawValue)
socket.send(signalingMessage.jsonString!)
case SignalType.call.rawValue:
print(#line, "calling to \(receiverName)")
case SignalType.offer.rawValue:
let offerSDP = RTCSessionDescription(
type: .offer,
sdp: signalingMessage.sessionDescription!.sdp
)
self.webRTCClient.set(remoteSdp: offerSDP) { (error) in
debugPrint(#line, error?.localizedDescription ?? "")
}
case SignalType.answer.rawValue:
let answerSDP = RTCSessionDescription(
type: .answer,
sdp: signalingMessage.sessionDescription!.sdp
)
self.webRTCClient.set(remoteSdp: answerSDP) { (error) in
debugPrint(#line, error?.localizedDescription ?? "")
}
case SignalType.candidate.rawValue:
let iceCandidate = signalingMessage.iceCandidate!
let candidate = RTCIceCandidate(
sdp: iceCandidate.sdp,
sdpMLineIndex: iceCandidate.sdpMLineIndex,
sdpMid: iceCandidate.sdpMid
)
self.webRTCClient.set(remoteCandidate: candidate)
print(#line, "iceCandidate")
case SignalType.leave.rawValue:
print(#line, "leave")
default:
debugPrint("Error in connection")
}
} catch {
debugPrint(#line, " decoded \(error.localizedDescription) ")
}
}
func webSocketEventsListenerDidReceiveData(_ webSocketTrackerService: WebSocketTracker, didRecieveMessage data: Data) {
log.info("didRecieveMessage", context: data)
}
func sendMessage(_ message: String) { }
func register(_ response: String) { }
}
extension CallManager: WebRTCClientDelegate {
func webRTCClient(_ client: WebRTCClient, didDiscoverLocalCandidate candidate: RTCIceCandidate) {
print("discovered local candidate")
let candidate = CallCandidate(from: candidate)
let candidateMsg = CallSignaling(
callerID: CallerAndReceiverID.instent.callerID,
receiverID: CallerAndReceiverID.instent.receiverID,
type: SignalType.candidate.rawValue,
iceCandidate: candidate)
self.socket.send(candidateMsg.jsonString!)
}
func webRTCClient(_ client: WebRTCClient, didChangeConnectionState state: RTCIceConnectionState) {
let textColor: UIColor
switch state {
case .connected, .completed:
textColor = .green
case .disconnected:
textColor = .orange
if let callVC = UIStoryboard(name: "Contacts", bundle: nil)
.instantiateViewController(withIdentifier: "CallVC") as? CallVC {
if let window = AppDelegate.shared.window, let rootViewController = window.rootViewController {
var currentController = rootViewController
while let presentedController = currentController.presentedViewController {
currentController = presentedController
}
delay(0.5) {
UIApplication.shared.endIgnoringInteractionEvents()
if currentController.nibName == callVC.nibName {
currentController.dismiss(animated: true, completion: nil)
}
}
}
}
case .failed, .closed:
textColor = .red
case .new, .checking, .count:
textColor = .black
@unknown default:
textColor = .black
}
}
func webRTCClient(_ client: WebRTCClient, didReceiveData data: Data) {
// DispatchQueue.main.async {
// let message = String(data: data, encoding: .utf8) ?? "(Binary: \(data.count) bytes)"
// let alert = UIAlertController(title: "Message from WebRTC", message: message, preferredStyle: .alert)
// alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
// self.present(alert, animated: true, completion: nil)
// }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment