mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			309 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			309 lines
		
	
	
		
			9.5 KiB
		
	
	
	
		
			Swift
		
	
| import Foundation
 | |
| import WebRTC
 | |
| import SessionMessagingKit
 | |
| import PromiseKit
 | |
| import CallKit
 | |
| 
 | |
| public final class SessionCall: NSObject, WebRTCSessionDelegate {
 | |
|     
 | |
|     @objc static let isEnabled = true
 | |
|     
 | |
|     // MARK: Metadata Properties
 | |
|     let uuid: String
 | |
|     let callID: UUID // This is for CallKit
 | |
|     let sessionID: String
 | |
|     let mode: Mode
 | |
|     var audioMode: AudioMode
 | |
|     let webRTCSession: WebRTCSession
 | |
|     let isOutgoing: Bool
 | |
|     var remoteSDP: RTCSessionDescription? = nil
 | |
|     var callMessageID: String?
 | |
|     var answerCallAction: CXAnswerCallAction? = nil
 | |
|     var contactName: String {
 | |
|         let contact = Storage.shared.getContact(with: self.sessionID)
 | |
|         return contact?.displayName(for: Contact.Context.regular) ?? "\(self.sessionID.prefix(4))...\(self.sessionID.suffix(4))"
 | |
|     }
 | |
|     var profilePicture: UIImage {
 | |
|         if let result = OWSProfileManager.shared().profileAvatar(forRecipientId: sessionID) {
 | |
|             return result
 | |
|         } else {
 | |
|             return Identicon.generatePlaceholderIcon(seed: sessionID, text: contactName, size: 300)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: Control
 | |
|     lazy public var videoCapturer: RTCVideoCapturer = {
 | |
|         return RTCCameraVideoCapturer(delegate: webRTCSession.localVideoSource)
 | |
|     }()
 | |
|     
 | |
|     var isRemoteVideoEnabled = false {
 | |
|         didSet {
 | |
|             remoteVideoStateDidChange?(isRemoteVideoEnabled)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     var isMuted = false {
 | |
|         willSet {
 | |
|             if newValue {
 | |
|                 webRTCSession.mute()
 | |
|             } else {
 | |
|                 webRTCSession.unmute()
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     var isVideoEnabled = false {
 | |
|         willSet {
 | |
|             if newValue {
 | |
|                 webRTCSession.turnOnVideo()
 | |
|             } else {
 | |
|                 webRTCSession.turnOffVideo()
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: Mode
 | |
|     enum Mode {
 | |
|         case offer
 | |
|         case answer
 | |
|     }
 | |
|     
 | |
|     // MARK: End call mode
 | |
|     enum EndCallMode {
 | |
|         case local
 | |
|         case remote
 | |
|         case unanswered
 | |
|         case answeredElsewhere
 | |
|     }
 | |
|     
 | |
|     // MARK: Audio I/O mode
 | |
|     enum AudioMode {
 | |
|         case earpiece
 | |
|         case speaker
 | |
|         case headphone
 | |
|         case bluetooth
 | |
|     }
 | |
|     
 | |
|     // MARK: Call State Properties
 | |
|     var connectingDate: Date? {
 | |
|         didSet {
 | |
|             stateDidChange?()
 | |
|             hasStartedConnectingDidChange?()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     var connectedDate: Date? {
 | |
|         didSet {
 | |
|             stateDidChange?()
 | |
|             hasConnectedDidChange?()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     var endDate: Date? {
 | |
|         didSet {
 | |
|             stateDidChange?()
 | |
|             hasEndedDidChange?()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Not yet implemented
 | |
|     var isOnHold = false {
 | |
|         didSet {
 | |
|             stateDidChange?()
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // MARK: State Change Callbacks
 | |
|     var stateDidChange: (() -> Void)?
 | |
|     var hasStartedConnectingDidChange: (() -> Void)?
 | |
|     var hasConnectedDidChange: (() -> Void)?
 | |
|     var hasEndedDidChange: (() -> Void)?
 | |
|     var remoteVideoStateDidChange: ((Bool) -> Void)?
 | |
|     
 | |
|     // MARK: Derived Properties
 | |
|     var hasStartedConnecting: Bool {
 | |
|         get { return connectingDate != nil }
 | |
|         set { connectingDate = newValue ? Date() : nil }
 | |
|     }
 | |
| 
 | |
|     var hasConnected: Bool {
 | |
|         get { return connectedDate != nil }
 | |
|         set { connectedDate = newValue ? Date() : nil }
 | |
|     }
 | |
| 
 | |
|     var hasEnded: Bool {
 | |
|         get { return endDate != nil }
 | |
|         set { endDate = newValue ? Date() : nil }
 | |
|     }
 | |
|     
 | |
|     var didTimeout = false
 | |
| 
 | |
|     var duration: TimeInterval {
 | |
|         guard let connectedDate = connectedDate else {
 | |
|             return 0
 | |
|         }
 | |
|         if let endDate = endDate {
 | |
|             return endDate.timeIntervalSince(connectedDate)
 | |
|         }
 | |
| 
 | |
|         return Date().timeIntervalSince(connectedDate)
 | |
|     }
 | |
|     
 | |
|     // MARK: Initialization
 | |
|     init(for sessionID: String, uuid: String, mode: Mode, outgoing: Bool = false) {
 | |
|         self.sessionID = sessionID
 | |
|         self.uuid = uuid
 | |
|         self.callID = UUID()
 | |
|         self.mode = mode
 | |
|         self.audioMode = .earpiece
 | |
|         self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid)
 | |
|         self.isOutgoing = outgoing
 | |
|         WebRTCSession.current = self.webRTCSession
 | |
|         super.init()
 | |
|         self.webRTCSession.delegate = self
 | |
|         if AppEnvironment.shared.callManager.currentCall == nil {
 | |
|             AppEnvironment.shared.callManager.currentCall = self
 | |
|         } else {
 | |
|             SNLog("[Calls] A call is ongoing.")
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) {
 | |
|         guard case .answer = mode else { return }
 | |
|         AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in
 | |
|             completion(error)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     func didReceiveRemoteSDP(sdp: RTCSessionDescription) {
 | |
|         print("[Calls] Did receive remote sdp.")
 | |
|         remoteSDP = sdp
 | |
|         if hasStartedConnecting {
 | |
|             webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: Actions
 | |
|     func startSessionCall() {
 | |
|         guard case .offer = mode else { return }
 | |
|         guard let thread = TSContactThread.fetch(uniqueId: TSContactThread.threadID(fromContactSessionID: sessionID)) else { return }
 | |
|         
 | |
|         let message = CallMessage()
 | |
|         message.sender = getUserHexEncodedPublicKey()
 | |
|         message.sentTimestamp = NSDate.millisecondTimestamp()
 | |
|         message.uuid = self.uuid
 | |
|         message.kind = .preOffer
 | |
|         let infoMessage = TSInfoMessage.from(message, associatedWith: thread)
 | |
|         infoMessage.save()
 | |
|         self.callMessageID = infoMessage.uniqueId
 | |
|         
 | |
|         var promise: Promise<Void>!
 | |
|         Storage.write(with: { transaction in
 | |
|             promise = self.webRTCSession.sendPreOffer(message, in: thread, using: transaction)
 | |
|         }, completion: { [weak self] in
 | |
|             let _ = promise.done {
 | |
|                 Storage.shared.write { transaction in
 | |
|                     self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).retainUntilComplete()
 | |
|                 }
 | |
|             }
 | |
|         })
 | |
|     }
 | |
|     
 | |
|     func answerSessionCall() {
 | |
|         guard case .answer = mode else { return }
 | |
|         hasStartedConnecting = true
 | |
|         if let sdp = remoteSDP {
 | |
|             webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     func answerSessionCallInBackground(action: CXAnswerCallAction) {
 | |
|         answerCallAction = action
 | |
|         self.answerSessionCall()
 | |
|     }
 | |
|     
 | |
|     func endSessionCall() {
 | |
|         guard !hasEnded else { return }
 | |
|         webRTCSession.hangUp()
 | |
|         Storage.write { transaction in
 | |
|             self.webRTCSession.endCall(with: self.sessionID, using: transaction)
 | |
|         }
 | |
|         hasEnded = true
 | |
|     }
 | |
|     
 | |
|     // MARK: Update call message
 | |
|     func updateCallMessage(mode: EndCallMode) {
 | |
|         guard let callMessageID = callMessageID else { return }
 | |
|         Storage.write { transaction in
 | |
|             let infoMessage = TSInfoMessage.fetch(uniqueId: callMessageID, transaction: transaction)
 | |
|             if let messageToUpdate = infoMessage {
 | |
|                 var shouldMarkAsRead = false
 | |
|                 if self.duration > 0 {
 | |
|                     shouldMarkAsRead = true
 | |
|                 } else if self.hasStartedConnecting {
 | |
|                     shouldMarkAsRead = true
 | |
|                 } else {
 | |
|                     switch mode {
 | |
|                     case .local:
 | |
|                         shouldMarkAsRead = true
 | |
|                         fallthrough
 | |
|                     case .remote:
 | |
|                         fallthrough
 | |
|                     case .unanswered:
 | |
|                         if messageToUpdate.callState == .incoming {
 | |
|                             messageToUpdate.updateCallInfoMessage(.missed, using: transaction)
 | |
|                         }
 | |
|                     case .answeredElsewhere:
 | |
|                         shouldMarkAsRead = true
 | |
|                     }
 | |
|                 }
 | |
|                 if shouldMarkAsRead {
 | |
|                     messageToUpdate.markAsRead(atTimestamp: NSDate.ows_millisecondTimeStamp(), sendReadReceipt: false, transaction: transaction)
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: Renderer
 | |
|     func attachRemoteVideoRenderer(_ renderer: RTCVideoRenderer) {
 | |
|         webRTCSession.attachRemoteRenderer(renderer)
 | |
|     }
 | |
|     
 | |
|     func removeRemoteVideoRenderer(_ renderer: RTCVideoRenderer) {
 | |
|         webRTCSession.removeRemoteRenderer(renderer)
 | |
|     }
 | |
|     
 | |
|     func attachLocalVideoRenderer(_ renderer: RTCVideoRenderer) {
 | |
|         webRTCSession.attachLocalRenderer(renderer)
 | |
|     }
 | |
|     
 | |
|     // MARK: Delegate
 | |
|     public func webRTCIsConnected() {
 | |
|         guard !self.hasConnected else { return }
 | |
|         self.hasConnected = true
 | |
|         self.answerCallAction?.fulfill()
 | |
|     }
 | |
|     
 | |
|     public func isRemoteVideoDidChange(isEnabled: Bool) {
 | |
|         isRemoteVideoEnabled = isEnabled
 | |
|     }
 | |
|     
 | |
|     public func didReceiveHangUpSignal() {
 | |
|         self.hasEnded = true
 | |
|         DispatchQueue.main.async {
 | |
|             if let currentBanner = IncomingCallBanner.current { currentBanner.dismiss() }
 | |
|             if let callVC = CurrentAppContext().frontmostViewController() as? CallVC { callVC.handleEndCallMessage() }
 | |
|             if let miniCallView = MiniCallView.current { miniCallView.dismiss() }
 | |
|             AppEnvironment.shared.callManager.reportCurrentCallEnded(reason: .remoteEnded)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     public func dataChannelDidOpen() {
 | |
|         // Send initial video status
 | |
|         if (isVideoEnabled) {
 | |
|             webRTCSession.turnOnVideo()
 | |
|         } else {
 | |
|             webRTCSession.turnOffVideo()
 | |
|         }
 | |
|     }
 | |
| }
 |