session-ios/Session/Calls/Call Management/SessionCall.swift

358 lines
11 KiB
Swift
Raw Normal View History

2021-10-28 08:02:41 +02:00
import Foundation
import WebRTC
import SessionMessagingKit
2021-11-07 23:12:18 +01:00
import PromiseKit
import CallKit
2021-10-28 08:02:41 +02:00
2021-11-03 05:31:50 +01:00
public final class SessionCall: NSObject, WebRTCSessionDelegate {
2021-12-14 03:57:25 +01:00
@objc static let isEnabled = true
2021-10-28 08:02:41 +02:00
// MARK: Metadata Properties
let uuid: String
let callID: UUID // This is for CallKit
2021-10-28 08:02:41 +02:00
let sessionID: String
let mode: Mode
2021-11-15 02:22:31 +01:00
var audioMode: AudioMode
2021-10-28 08:02:41 +02:00
let webRTCSession: WebRTCSession
2021-11-09 06:05:23 +01:00
let isOutgoing: Bool
2021-11-03 05:31:50 +01:00
var remoteSDP: RTCSessionDescription? = nil
2021-11-29 02:10:33 +01:00
var callMessageID: String?
var answerCallAction: CXAnswerCallAction? = nil
2021-10-28 08:02:41 +02:00
var contactName: String {
let contact = Storage.shared.getContact(with: self.sessionID)
2021-12-01 05:57:23 +01:00
return contact?.displayName(for: Contact.Context.regular) ?? "\(self.sessionID.prefix(4))...\(self.sessionID.suffix(4))"
2021-10-28 08:02:41 +02:00
}
var profilePicture: UIImage {
if let result = OWSProfileManager.shared().profileAvatar(forRecipientId: sessionID) {
return result
} else {
return Identicon.generatePlaceholderIcon(seed: sessionID, text: contactName, size: 300)
}
}
2021-11-03 05:31:50 +01:00
// 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()
}
}
}
2021-10-28 08:02:41 +02:00
// MARK: Mode
enum Mode {
case offer
2021-11-03 05:31:50 +01:00
case answer
2021-10-28 08:02:41 +02:00
}
2021-11-09 06:05:23 +01:00
// MARK: End call mode
enum EndCallMode {
case local
case remote
2021-11-11 06:51:54 +01:00
case unanswered
2021-11-19 01:50:14 +01:00
case answeredElsewhere
2021-11-09 06:05:23 +01:00
}
2021-11-15 02:22:31 +01:00
// MARK: Audio I/O mode
enum AudioMode {
case earpiece
case speaker
case headphone
case bluetooth
}
2021-10-28 08:02:41 +02:00
// 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)?
2021-11-03 05:31:50 +01:00
var remoteVideoStateDidChange: ((Bool) -> Void)?
var hasStartedReconnecting: (() -> Void)?
var hasReconnected: (() -> Void)?
2021-10-28 08:02:41 +02:00
// 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 }
}
2021-11-11 06:51:54 +01:00
2022-03-24 05:05:00 +01:00
var timeOutTimer: Timer? = nil
2021-11-11 06:51:54 +01:00
var didTimeout = false
2021-10-28 08:02:41 +02:00
var duration: TimeInterval {
guard let connectedDate = connectedDate else {
return 0
}
2021-11-09 06:05:23 +01:00
if let endDate = endDate {
return endDate.timeIntervalSince(connectedDate)
}
2021-10-28 08:02:41 +02:00
return Date().timeIntervalSince(connectedDate)
}
var reconnectTimer: Timer? = nil
2021-10-28 08:02:41 +02:00
// MARK: Initialization
2021-11-09 06:05:23 +01:00
init(for sessionID: String, uuid: String, mode: Mode, outgoing: Bool = false) {
2021-10-28 08:02:41 +02:00
self.sessionID = sessionID
self.uuid = uuid
self.callID = UUID()
2021-10-28 08:02:41 +02:00
self.mode = mode
2021-11-15 02:22:31 +01:00
self.audioMode = .earpiece
2021-10-28 08:02:41 +02:00
self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: uuid)
2021-11-09 06:05:23 +01:00
self.isOutgoing = outgoing
2021-11-08 05:09:45 +01:00
WebRTCSession.current = self.webRTCSession
2021-10-28 08:02:41 +02:00
super.init()
2021-11-03 05:31:50 +01:00
self.webRTCSession.delegate = self
2021-11-09 01:53:38 +01:00
if AppEnvironment.shared.callManager.currentCall == nil {
AppEnvironment.shared.callManager.currentCall = self
} else {
SNLog("[Calls] A call is ongoing.")
}
2021-10-28 08:02:41 +02:00
}
2021-11-03 05:31:50 +01:00
func reportIncomingCallIfNeeded(completion: @escaping (Error?) -> Void) {
guard case .answer = mode else { return }
setupTimeoutTimer()
2021-10-28 08:02:41 +02:00
AppEnvironment.shared.callManager.reportIncomingCall(self, callerName: contactName) { error in
2021-11-03 05:31:50 +01:00
completion(error)
}
}
func didReceiveRemoteSDP(sdp: RTCSessionDescription) {
2022-04-05 08:35:09 +02:00
SNLog("[Calls] Did receive remote sdp.")
2021-11-03 05:31:50 +01:00
remoteSDP = sdp
if hasStartedConnecting {
2021-11-03 05:31:50 +01:00
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally
2021-10-28 08:02:41 +02:00
}
}
// MARK: Actions
2021-11-09 01:53:38 +01:00
func startSessionCall() {
2021-10-28 08:02:41 +02:00
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>!
2021-11-07 23:12:18 +01:00
Storage.write(with: { transaction in
promise = self.webRTCSession.sendPreOffer(message, in: thread, using: transaction)
2021-11-07 23:12:18 +01:00
}, completion: { [weak self] in
let _ = promise.done {
2021-11-07 23:12:18 +01:00
Storage.shared.write { transaction in
2021-11-10 04:31:02 +01:00
self?.webRTCSession.sendOffer(to: self!.sessionID, using: transaction as! YapDatabaseReadWriteTransaction).retainUntilComplete()
2021-11-07 23:12:18 +01:00
}
self?.setupTimeoutTimer()
2021-11-07 23:12:18 +01:00
}
})
2021-10-28 08:02:41 +02:00
}
2021-11-10 04:31:02 +01:00
func answerSessionCall() {
2021-11-03 05:31:50 +01:00
guard case .answer = mode else { return }
2021-10-28 08:02:41 +02:00
hasStartedConnecting = true
2021-11-03 05:31:50 +01:00
if let sdp = remoteSDP {
webRTCSession.handleRemoteSDP(sdp, from: sessionID) // This sends an answer message internally
}
2021-10-28 08:02:41 +02:00
}
2021-11-10 04:31:02 +01:00
func answerSessionCallInBackground(action: CXAnswerCallAction) {
answerCallAction = action
self.answerSessionCall()
}
2021-10-28 08:02:41 +02:00
func endSessionCall() {
guard !hasEnded else { return }
2021-11-10 04:31:02 +01:00
webRTCSession.hangUp()
2021-10-28 08:02:41 +02:00
Storage.write { transaction in
self.webRTCSession.endCall(with: self.sessionID, using: transaction)
}
hasEnded = true
}
2021-11-03 05:31:50 +01:00
2021-11-09 06:05:23 +01:00
// MARK: Update call message
func updateCallMessage(mode: EndCallMode) {
2021-11-29 02:10:33 +01:00
guard let callMessageID = callMessageID else { return }
2021-11-09 06:05:23 +01:00
Storage.write { transaction in
2021-11-29 02:10:33 +01:00
let infoMessage = TSInfoMessage.fetch(uniqueId: callMessageID, transaction: transaction)
if let messageToUpdate = infoMessage {
2021-11-09 06:05:23 +01:00
var shouldMarkAsRead = false
if self.duration > 0 {
shouldMarkAsRead = true
} else if self.hasStartedConnecting {
shouldMarkAsRead = true
2021-11-09 06:05:23 +01:00
} 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
2021-11-09 06:05:23 +01:00
}
}
2021-11-29 02:10:33 +01:00
if shouldMarkAsRead {
2022-03-02 04:46:23 +01:00
messageToUpdate.markAsRead(atTimestamp: NSDate.ows_millisecondTimeStamp(), trySendReadReceipt: false, transaction: transaction)
2021-11-09 06:05:23 +01:00
}
}
}
}
2021-11-03 05:31:50 +01:00
// MARK: Renderer
func attachRemoteVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.attachRemoteRenderer(renderer)
}
2021-11-09 06:05:23 +01:00
func removeRemoteVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.removeRemoteRenderer(renderer)
}
2021-11-03 05:31:50 +01:00
func attachLocalVideoRenderer(_ renderer: RTCVideoRenderer) {
webRTCSession.attachLocalRenderer(renderer)
}
// MARK: Delegate
public func webRTCIsConnected() {
self.invalidateTimeoutTimer()
2022-03-25 06:29:52 +01:00
self.reconnectTimer?.invalidate()
guard !self.hasConnected else {
hasReconnected?()
return
}
2021-11-03 05:31:50 +01:00
self.hasConnected = true
self.answerCallAction?.fulfill()
2021-11-03 05:31:50 +01:00
}
public func isRemoteVideoDidChange(isEnabled: Bool) {
isRemoteVideoEnabled = isEnabled
}
2021-11-10 04:31:02 +01:00
public func didReceiveHangUpSignal() {
self.hasEnded = true
2021-11-10 04:31:02 +01:00
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)
}
}
2021-11-03 05:31:50 +01:00
public func dataChannelDidOpen() {
// Send initial video status
if (isVideoEnabled) {
webRTCSession.turnOnVideo()
} else {
webRTCSession.turnOffVideo()
}
}
2022-03-24 05:05:00 +01:00
public func reconnectIfNeeded() {
setupTimeoutTimer()
hasStartedReconnecting?()
guard isOutgoing else { return }
tryToReconnect()
}
private func tryToReconnect() {
reconnectTimer?.invalidate()
if SSKEnvironment.shared.reachabilityManager.isReachable {
Storage.write { transaction in
self.webRTCSession.sendOffer(to: self.sessionID, using: transaction, isRestartingICEConnection: true).retainUntilComplete()
}
} else {
reconnectTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: 5, repeats: false) { _ in
self.tryToReconnect()
}
}
}
2022-03-24 05:05:00 +01:00
// MARK: Timeout
public func setupTimeoutTimer() {
invalidateTimeoutTimer()
let timeInterval: TimeInterval = hasConnected ? 60 : 30
timeOutTimer = Timer.scheduledTimerOnMainThread(withTimeInterval: timeInterval, repeats: false) { _ in
2022-03-24 05:05:00 +01:00
self.didTimeout = true
AppEnvironment.shared.callManager.endCall(self) { error in
self.timeOutTimer = nil
}
}
}
public func invalidateTimeoutTimer() {
timeOutTimer?.invalidate()
timeOutTimer = nil
}
2021-10-28 08:02:41 +02:00
}