session-ios/Session/Calls/Individual/IndividualCall.swift

237 lines
6.7 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalServiceKit
import SignalRingRTC
public enum CallState: String {
case idle
case dialing
case answering
case remoteRinging
case localRinging
case connected
case reconnecting
case localFailure // terminal
case localHangup // terminal
case remoteHangup // terminal
case remoteHangupNeedPermission // terminal
case remoteBusy // terminal
case answeredElsewhere // terminal
case declinedElsewhere // terminal
case busyElsewhere // terminal
}
public enum CallAdapterType {
case `default`, nonCallKit
}
public enum CallDirection {
case outgoing, incoming
}
public protocol IndividualCallDelegate: AnyObject {
func individualCallStateDidChange(_ call: IndividualCall, state: CallState)
func individualCallLocalVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool)
func individualCallLocalAudioMuteDidChange(_ call: IndividualCall, isAudioMuted: Bool)
func individualCallHoldDidChange(_ call: IndividualCall, isOnHold: Bool)
func individualCallRemoteVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool)
func individualCallRemoteSharingScreenDidChange(_ call: IndividualCall, isRemoteSharingScreen: Bool)
}
/**
* Data model for a WebRTC backed voice/video call.
*
* This class' state should only be accessed on the main queue.
*/
@objc
public class IndividualCall: NSObject, IndividualCallNotificationInfo {
// Mark -
var backgroundTask: OWSBackgroundTask? {
didSet {
AssertIsOnMainThread()
Logger.info("")
}
}
var callId: UInt64? {
didSet {
AssertIsOnMainThread()
Logger.info("")
}
}
let callAdapterType: CallAdapterType
weak var remoteVideoTrack: RTCVideoTrack? {
didSet {
AssertIsOnMainThread()
Logger.info("")
delegate?.individualCallRemoteVideoMuteDidChange(self, isVideoMuted: !isRemoteVideoEnabled)
}
}
var isRemoteVideoEnabled = false {
didSet {
AssertIsOnMainThread()
Logger.info("\(isRemoteVideoEnabled)")
delegate?.individualCallRemoteVideoMuteDidChange(self, isVideoMuted: !isRemoteVideoEnabled)
}
}
var isRemoteSharingScreen = false {
didSet {
AssertIsOnMainThread()
Logger.info("\(isRemoteSharingScreen)")
delegate?.individualCallRemoteSharingScreenDidChange(self, isRemoteSharingScreen: isRemoteSharingScreen)
}
}
// MARK: -
// tracking cleanup
var wasReportedToSystem = false
var wasRemovedFromSystem = false
@objc
public let remoteAddress: SignalServiceAddress
public var isEnded: Bool {
switch state {
case .localFailure, .localHangup, .remoteHangup, .remoteHangupNeedPermission, .remoteBusy, .answeredElsewhere, .declinedElsewhere, .busyElsewhere:
return true
case .idle, .dialing, .answering, .remoteRinging, .localRinging, .connected, .reconnecting:
return false
}
}
public let direction: CallDirection
// Distinguishes between calls locally, e.g. in CallKit
@objc
public let localId: UUID
public let thread: TSContactThread
public let sentAtTimestamp: UInt64
public var callRecord: TSCall? {
didSet {
AssertIsOnMainThread()
assert(oldValue == nil)
updateCallRecordType()
}
}
public lazy var hasLocalVideo = offerMediaType == .video {
didSet {
AssertIsOnMainThread()
delegate?.individualCallLocalVideoMuteDidChange(self, isVideoMuted: !hasLocalVideo)
}
}
public var state: CallState {
didSet {
AssertIsOnMainThread()
Logger.debug("state changed: \(oldValue) -> \(self.state) for call: \(self)")
updateCallRecordType()
delegate?.individualCallStateDidChange(self, state: state)
}
}
public var offerMediaType: TSRecentCallOfferType = .audio
// We start out muted if the record permission isn't granted. This should generally
// only happen for incoming calls, because we proactively ask about it before you
// can make an outgoing call.
public var isMuted = AVAudioSession.sharedInstance().recordPermission != .granted {
didSet {
AssertIsOnMainThread()
Logger.debug("muted changed: \(oldValue) -> \(self.isMuted)")
delegate?.individualCallLocalAudioMuteDidChange(self, isAudioMuted: isMuted)
}
}
public var isOnHold = false {
didSet {
AssertIsOnMainThread()
Logger.debug("isOnHold changed: \(oldValue) -> \(self.isOnHold)")
delegate?.individualCallHoldDidChange(self, isOnHold: isOnHold)
}
}
public weak var delegate: IndividualCallDelegate?
// MARK: Initializers and Factory Methods
init(direction: CallDirection, localId: UUID, state: CallState, remoteAddress: SignalServiceAddress, sentAtTimestamp: UInt64, callAdapterType: CallAdapterType) {
self.direction = direction
self.localId = localId
self.state = state
self.remoteAddress = remoteAddress
self.thread = TSContactThread.getOrCreateThread(contactAddress: remoteAddress)
self.sentAtTimestamp = sentAtTimestamp
self.callAdapterType = callAdapterType
}
deinit {
Logger.debug("")
if !isEnded {
owsFailDebug("isEnded was unexpectedly false")
}
if wasReportedToSystem {
if !wasRemovedFromSystem {
owsFailDebug("wasRemovedFromSystem was unexpectedly false")
}
} else {
if wasRemovedFromSystem {
owsFailDebug("wasRemovedFromSystem was unexpectedly true")
}
}
}
override public var description: String {
return "IndividualCall: {\(remoteAddress), localId: \(localId), signalingId: \(callId as Optional)))}"
}
private func updateCallRecordType() {
AssertIsOnMainThread()
guard let callRecord = self.callRecord else {
return
}
// Mark incomplete calls as completed if call has connected.
if state == .connected &&
callRecord.callType == .outgoingIncomplete {
callRecord.updateCallType(.outgoing)
}
if state == .connected &&
callRecord.callType == .incomingIncomplete {
callRecord.updateCallType(.incoming)
}
}
// MARK: Equatable
static func == (lhs: IndividualCall, rhs: IndividualCall) -> Bool {
return lhs.localId == rhs.localId
}
}