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.
325 lines
11 KiB
Swift
325 lines
11 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalRingRTC
|
|
import SignalCoreKit
|
|
|
|
// All Observer methods will be invoked from the main thread.
|
|
public protocol CallObserver: AnyObject {
|
|
func individualCallStateDidChange(_ call: SignalCall, state: CallState)
|
|
func individualCallLocalVideoMuteDidChange(_ call: SignalCall, isVideoMuted: Bool)
|
|
func individualCallLocalAudioMuteDidChange(_ call: SignalCall, isAudioMuted: Bool)
|
|
func individualCallRemoteVideoMuteDidChange(_ call: SignalCall, isVideoMuted: Bool)
|
|
func individualCallRemoteSharingScreenDidChange(_ call: SignalCall, isRemoteSharingScreen: Bool)
|
|
func individualCallHoldDidChange(_ call: SignalCall, isOnHold: Bool)
|
|
|
|
func groupCallLocalDeviceStateChanged(_ call: SignalCall)
|
|
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall)
|
|
func groupCallPeekChanged(_ call: SignalCall)
|
|
func groupCallRequestMembershipProof(_ call: SignalCall)
|
|
func groupCallRequestGroupMembers(_ call: SignalCall)
|
|
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason)
|
|
|
|
/// Invoked if a call message failed to send because of a safety number change
|
|
/// UI observing call state may choose to alert the user (e.g. presenting a SafetyNumberConfirmationSheet)
|
|
func callMessageSendFailedUntrustedIdentity(_ call: SignalCall)
|
|
}
|
|
|
|
public extension CallObserver {
|
|
func individualCallStateDidChange(_ call: SignalCall, state: CallState) {}
|
|
func individualCallLocalVideoMuteDidChange(_ call: SignalCall, isVideoMuted: Bool) {}
|
|
func individualCallLocalAudioMuteDidChange(_ call: SignalCall, isAudioMuted: Bool) {}
|
|
func individualCallRemoteVideoMuteDidChange(_ call: SignalCall, isVideoMuted: Bool) {}
|
|
func individualCallRemoteSharingScreenDidChange(_ call: SignalCall, isRemoteSharingScreen: Bool) {}
|
|
func individualCallHoldDidChange(_ call: SignalCall, isOnHold: Bool) {}
|
|
|
|
func groupCallLocalDeviceStateChanged(_ call: SignalCall) {}
|
|
func groupCallRemoteDeviceStatesChanged(_ call: SignalCall) {}
|
|
func groupCallPeekChanged(_ call: SignalCall) {}
|
|
func groupCallRequestMembershipProof(_ call: SignalCall) {}
|
|
func groupCallRequestGroupMembers(_ call: SignalCall) {}
|
|
func groupCallEnded(_ call: SignalCall, reason: GroupCallEndReason) {}
|
|
|
|
func callMessageSendFailedUntrustedIdentity(_ call: SignalCall) {}
|
|
}
|
|
|
|
@objc
|
|
public class SignalCall: NSObject, CallManagerCallReference {
|
|
public let mode: Mode
|
|
public enum Mode {
|
|
case individual(IndividualCall)
|
|
case group(GroupCall)
|
|
}
|
|
|
|
public let audioActivity: AudioActivity
|
|
|
|
@objc
|
|
var isGroupCall: Bool {
|
|
switch mode {
|
|
case .group: return true
|
|
case .individual: return false
|
|
}
|
|
}
|
|
|
|
var groupCall: GroupCall! {
|
|
owsAssertDebug(isGroupCall)
|
|
guard case .group(let call) = mode else {
|
|
owsFailDebug("Missing group call")
|
|
return nil
|
|
}
|
|
return call
|
|
}
|
|
|
|
@objc
|
|
var isIndividualCall: Bool {
|
|
switch mode {
|
|
case .group: return false
|
|
case .individual: return true
|
|
}
|
|
}
|
|
|
|
@objc
|
|
var individualCall: IndividualCall! {
|
|
owsAssertDebug(isIndividualCall)
|
|
guard case .individual(let call) = mode else {
|
|
owsFailDebug("Missing individual call")
|
|
return nil
|
|
}
|
|
return call
|
|
}
|
|
|
|
private(set) lazy var videoCaptureController = VideoCaptureController()
|
|
|
|
// Should be used only on the main thread
|
|
public var connectedDate: Date? {
|
|
didSet { AssertIsOnMainThread() }
|
|
}
|
|
|
|
@objc
|
|
public let thread: TSThread
|
|
|
|
public var error: CallError?
|
|
public enum CallError: Error {
|
|
case providerReset
|
|
case disconnected
|
|
case externalError(underlyingError: Error)
|
|
case timeout(description: String)
|
|
case messageSendFailure(underlyingError: Error)
|
|
}
|
|
|
|
var participantAddresses: [String] {
|
|
switch mode {
|
|
case .group(let call):
|
|
return call.remoteDeviceStates.values.map { $0.address }
|
|
case .individual(let call):
|
|
return [call.publicKey]
|
|
}
|
|
}
|
|
|
|
init(groupCall: GroupCall, groupThread: TSGroupThread) {
|
|
mode = .group(groupCall)
|
|
audioActivity = AudioActivity(
|
|
audioDescription: "[SignalCall] with group \(groupThread.groupModel.groupId)",
|
|
behavior: .call
|
|
)
|
|
thread = groupThread
|
|
super.init()
|
|
groupCall.delegate = self
|
|
}
|
|
|
|
init(individualCall: IndividualCall) {
|
|
mode = .individual(individualCall)
|
|
audioActivity = AudioActivity(
|
|
audioDescription: "[SignalCall] with individual \(individualCall.thread.contactSessionID())",
|
|
behavior: .call
|
|
)
|
|
thread = individualCall.thread
|
|
super.init()
|
|
individualCall.delegate = self
|
|
}
|
|
|
|
public class func groupCall(thread: TSGroupThread) -> SignalCall? {
|
|
let videoCaptureController = VideoCaptureController()
|
|
let sfuURL = TSConstants.sfuURL
|
|
|
|
guard let groupCall = Self.callService.callManager.createGroupCall(
|
|
groupId: thread.groupModel.groupId,
|
|
sfuUrl: sfuURL,
|
|
videoCaptureController: videoCaptureController
|
|
) else {
|
|
owsFailDebug("Failed to create group call")
|
|
return nil
|
|
}
|
|
|
|
let call = SignalCall(groupCall: groupCall, groupThread: thread)
|
|
call.videoCaptureController = videoCaptureController
|
|
return call
|
|
}
|
|
|
|
public class func outgoingIndividualCall(localId: UUID, publicKey: String) -> SignalCall {
|
|
let individualCall = IndividualCall(
|
|
direction: .outgoing,
|
|
localId: localId,
|
|
state: .dialing,
|
|
publicKey: publicKey,
|
|
sentAtTimestamp: NSDate.ows_millisecondTimeStamp(),
|
|
callAdapterType: .default
|
|
)
|
|
return SignalCall(individualCall: individualCall)
|
|
}
|
|
|
|
public class func incomingIndividualCall(
|
|
localId: UUID,
|
|
publicKey: String,
|
|
sentAtTimestamp: UInt64,
|
|
offerMediaType: TSRecentCallOfferType
|
|
) -> SignalCall {
|
|
// If this is a video call, we want to use in the in app call screen
|
|
// because CallKit has poor support for video calls. On iOS 14+ we
|
|
// always use CallKit, because as of iOS 14 AVAudioPlayer is no longer
|
|
// able to start playing sounds in the background.
|
|
let callAdapterType: CallAdapterType
|
|
if #available(iOS 14, *) {
|
|
callAdapterType = .default
|
|
} else if offerMediaType == .video {
|
|
callAdapterType = .nonCallKit
|
|
} else {
|
|
callAdapterType = .default
|
|
}
|
|
|
|
let individualCall = IndividualCall(
|
|
direction: .incoming,
|
|
localId: localId,
|
|
state: .answering,
|
|
publicKey: publicKey,
|
|
sentAtTimestamp: sentAtTimestamp,
|
|
callAdapterType: callAdapterType
|
|
)
|
|
individualCall.offerMediaType = offerMediaType
|
|
return SignalCall(individualCall: individualCall)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private var observers: WeakArray<CallObserver> = []
|
|
|
|
public func addObserverAndSyncState(observer: CallObserver) {
|
|
AssertIsOnMainThread()
|
|
|
|
observers.append(observer)
|
|
|
|
// Synchronize observer with current call state
|
|
switch mode {
|
|
case .individual(let individualCall):
|
|
observer.individualCallStateDidChange(self, state: individualCall.state)
|
|
case .group:
|
|
observer.groupCallLocalDeviceStateChanged(self)
|
|
observer.groupCallRemoteDeviceStatesChanged(self)
|
|
}
|
|
}
|
|
|
|
public func removeObserver(_ observer: CallObserver) {
|
|
AssertIsOnMainThread()
|
|
|
|
observers.removeAll { $0 === observer }
|
|
}
|
|
|
|
public func removeAllObservers() {
|
|
AssertIsOnMainThread()
|
|
|
|
observers = []
|
|
}
|
|
|
|
public func publishSendFailureUntrustedParticipantIdentity() {
|
|
observers.elements.forEach { $0.callMessageSendFailedUntrustedIdentity(self) }
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
// This method should only be called when the call state is "connected".
|
|
public func connectionDuration() -> TimeInterval {
|
|
guard let connectedDate = connectedDate else {
|
|
owsFailDebug("Called connectionDuration before connected.")
|
|
return 0
|
|
}
|
|
return -connectedDate.timeIntervalSinceNow
|
|
}
|
|
}
|
|
|
|
extension SignalCall: GroupCallDelegate {
|
|
public func groupCall(onLocalDeviceStateChanged groupCall: GroupCall) {
|
|
if groupCall.localDeviceState.joinState == .joined, connectedDate == nil {
|
|
connectedDate = Date()
|
|
|
|
// make sure we don't terminate audio session during call
|
|
audioSession.isRTCAudioEnabled = true
|
|
owsAssertDebug(audioSession.startAudioActivity(audioActivity))
|
|
}
|
|
|
|
observers.elements.forEach { $0.groupCallLocalDeviceStateChanged(self) }
|
|
}
|
|
|
|
public func groupCall(onRemoteDeviceStatesChanged groupCall: GroupCall) {
|
|
observers.elements.forEach { $0.groupCallRemoteDeviceStatesChanged(self) }
|
|
}
|
|
|
|
public func groupCall(onPeekChanged groupCall: GroupCall) {
|
|
observers.elements.forEach { $0.groupCallPeekChanged(self) }
|
|
}
|
|
|
|
public func groupCall(requestMembershipProof groupCall: GroupCall) {
|
|
observers.elements.forEach { $0.groupCallRequestMembershipProof(self) }
|
|
}
|
|
|
|
public func groupCall(requestGroupMembers groupCall: GroupCall) {
|
|
observers.elements.forEach { $0.groupCallRequestGroupMembers(self) }
|
|
}
|
|
|
|
public func groupCall(onEnded groupCall: GroupCall, reason: GroupCallEndReason) {
|
|
observers.elements.forEach { $0.groupCallEnded(self, reason: reason) }
|
|
}
|
|
}
|
|
|
|
extension SignalCall: IndividualCallDelegate {
|
|
public func individualCallStateDidChange(_ call: IndividualCall, state: CallState) {
|
|
if case .connected = state, connectedDate == nil {
|
|
connectedDate = Date()
|
|
}
|
|
|
|
observers.elements.forEach { $0.individualCallStateDidChange(self, state: state) }
|
|
}
|
|
|
|
public func individualCallLocalVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
|
|
observers.elements.forEach { $0.individualCallLocalVideoMuteDidChange(self, isVideoMuted: isVideoMuted) }
|
|
}
|
|
|
|
public func individualCallLocalAudioMuteDidChange(_ call: IndividualCall, isAudioMuted: Bool) {
|
|
observers.elements.forEach { $0.individualCallLocalAudioMuteDidChange(self, isAudioMuted: isAudioMuted) }
|
|
}
|
|
|
|
public func individualCallHoldDidChange(_ call: IndividualCall, isOnHold: Bool) {
|
|
observers.elements.forEach { $0.individualCallHoldDidChange(self, isOnHold: isOnHold) }
|
|
}
|
|
|
|
public func individualCallRemoteVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
|
|
observers.elements.forEach { $0.individualCallRemoteVideoMuteDidChange(self, isVideoMuted: isVideoMuted) }
|
|
}
|
|
|
|
public func individualCallRemoteSharingScreenDidChange(_ call: IndividualCall, isRemoteSharingScreen: Bool) {
|
|
observers.elements.forEach { $0.individualCallRemoteSharingScreenDidChange(self, isRemoteSharingScreen: isRemoteSharingScreen) }
|
|
}
|
|
}
|
|
|
|
extension GroupCall {
|
|
public var isFull: Bool {
|
|
guard let peekInfo = peekInfo, let maxDevices = peekInfo.maxDevices else { return false }
|
|
return peekInfo.deviceCount >= maxDevices
|
|
}
|
|
public var maxDevices: UInt32? {
|
|
guard let peekInfo = peekInfo, let maxDevices = peekInfo.maxDevices else { return nil }
|
|
return maxDevices
|
|
}
|
|
}
|