diff --git a/Session/Calls/CallVC.swift b/Session/Calls/CallVC.swift index ed837b7bd..d54af1878 100644 --- a/Session/Calls/CallVC.swift +++ b/Session/Calls/CallVC.swift @@ -165,7 +165,7 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { init(for sessionID: String, mode: Mode) { self.sessionID = sessionID self.mode = mode - self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID) + self.webRTCSession = WebRTCSession.current ?? WebRTCSession(for: sessionID, with: UUID().uuidString) super.init(nibName: nil, bundle: nil) self.webRTCSession.delegate = self } @@ -187,7 +187,9 @@ final class CallVC : UIViewController, WebRTCSessionDelegate { if case .offer = mode { callInfoLabel.text = "Ringing..." Storage.write { transaction in - self.webRTCSession.sendOffer(to: self.sessionID, using: transaction).retainUntilComplete() + self.webRTCSession.sendPreOffer(to: self.sessionID, using: transaction).done { + self.webRTCSession.sendOffer(to: self.sessionID, using: transaction).retainUntilComplete() + } } answerButton.isHidden = true } diff --git a/SessionMessagingKit/Calls/WebRTCSession.swift b/SessionMessagingKit/Calls/WebRTCSession.swift index 27996b1b4..b801dd825 100644 --- a/SessionMessagingKit/Calls/WebRTCSession.swift +++ b/SessionMessagingKit/Calls/WebRTCSession.swift @@ -11,6 +11,7 @@ public protocol WebRTCSessionDelegate : AnyObject { public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { public weak var delegate: WebRTCSessionDelegate? private let contactSessionID: String + private let uuid: String private var queuedICECandidates: [RTCIceCandidate] = [] private var iceCandidateSendTimer: Timer? @@ -88,8 +89,9 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { // MARK: Initialization public static var current: WebRTCSession? - public init(for contactSessionID: String) { + public init(for contactSessionID: String, with uuid: String) { self.contactSessionID = contactSessionID + self.uuid = uuid super.init() let mediaStreamTrackIDS = ["ARDAMS"] createDataChannel() @@ -100,6 +102,24 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } // MARK: Signaling + public func sendPreOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + print("[Calls] Sending pre-offer message.") + guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) } + let (promise, seal) = Promise.pending() + DispatchQueue.main.async { + let message = CallMessage() + message.uuid = self.uuid + message.kind = .preOffer + MessageSender.sendNonDurably(message, in: thread, using: transaction).done2 { + print("[Calls] Pre-offer message has been sent.") + seal.fulfill(()) + }.catch2 { error in + seal.reject(error) + } + } + return promise + } + public func sendOffer(to sessionID: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { print("[Calls] Sending offer message.") guard let thread = TSContactThread.fetch(for: sessionID, using: transaction) else { return Promise(error: Error.noThread) } @@ -117,6 +137,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate { } DispatchQueue.main.async { let message = CallMessage() + message.uuid = self.uuid message.kind = .offer message.sdps = [ sdp.sdp ] let tsMessage = TSOutgoingMessage.from(message, associatedWith: thread) diff --git a/SessionMessagingKit/Messages/Control Messages/CallMessage.swift b/SessionMessagingKit/Messages/Control Messages/CallMessage.swift index 19455a24f..cd3609991 100644 --- a/SessionMessagingKit/Messages/Control Messages/CallMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/CallMessage.swift @@ -3,6 +3,7 @@ import WebRTC /// See https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription for more information. @objc(SNCallMessage) public final class CallMessage : ControlMessage { + public var uuid: String? public var kind: Kind? /// See https://developer.mozilla.org/en-US/docs/Glossary/SDP for more information. public var sdps: [String]? @@ -13,6 +14,7 @@ public final class CallMessage : ControlMessage { // MARK: Kind public enum Kind : Codable, CustomStringConvertible { + case preOffer case offer case answer case provisionalAnswer @@ -21,6 +23,7 @@ public final class CallMessage : ControlMessage { public var description: String { switch self { + case .preOffer: return "preOffer" case .offer: return "offer" case .answer: return "answer" case .provisionalAnswer: return "provisionalAnswer" @@ -33,8 +36,9 @@ public final class CallMessage : ControlMessage { // MARK: Initialization public override init() { super.init() } - internal init(kind: Kind, sdps: [String]) { + internal init(uuid: String, kind: Kind, sdps: [String]) { super.init() + self.uuid = uuid self.kind = kind self.sdps = sdps } @@ -42,7 +46,7 @@ public final class CallMessage : ControlMessage { // MARK: Validation public override var isValid: Bool { guard super.isValid else { return false } - return kind != nil + return kind != nil && uuid != nil } // MARK: Coding @@ -50,6 +54,7 @@ public final class CallMessage : ControlMessage { super.init(coder: coder) guard let rawKind = coder.decodeObject(forKey: "kind") as! String? else { return nil } switch rawKind { + case "preOffer": kind = .preOffer case "offer": kind = .offer case "answer": kind = .answer case "provisionalAnswer": kind = .provisionalAnswer @@ -61,11 +66,13 @@ public final class CallMessage : ControlMessage { default: preconditionFailure() } if let sdps = coder.decodeObject(forKey: "sdps") as! [String]? { self.sdps = sdps } + if let uuid = coder.decodeObject(forKey: "uuid") as! String? { self.uuid = uuid } } public override func encode(with coder: NSCoder) { super.encode(with: coder) switch kind { + case .preOffer: coder.encode("preOffer", forKey: "kind") case .offer: coder.encode("offer", forKey: "kind") case .answer: coder.encode("answer", forKey: "kind") case .provisionalAnswer: coder.encode("provisionalAnswer", forKey: "kind") @@ -77,6 +84,7 @@ public final class CallMessage : ControlMessage { default: preconditionFailure() } coder.encode(sdps, forKey: "sdps") + coder.encode(uuid, forKey: "uuid") } // MARK: Proto Conversion @@ -84,6 +92,7 @@ public final class CallMessage : ControlMessage { guard let callMessageProto = proto.callMessage else { return nil } let kind: Kind switch callMessageProto.type { + case .preOffer: kind = .preOffer case .offer: kind = .offer case .answer: kind = .answer case .provisionalAnswer: kind = .provisionalAnswer @@ -94,23 +103,25 @@ public final class CallMessage : ControlMessage { case .endCall: kind = .endCall } let sdps = callMessageProto.sdps - return CallMessage(kind: kind, sdps: sdps) + let uuid = callMessageProto.uuid + return CallMessage(uuid: uuid, kind: kind, sdps: sdps) } public override func toProto(using transaction: YapDatabaseReadWriteTransaction) -> SNProtoContent? { - guard let kind = kind else { + guard let kind = kind, let uuid = uuid else { SNLog("Couldn't construct call message proto from: \(self).") return nil } let type: SNProtoCallMessage.SNProtoCallMessageType switch kind { + case .preOffer: type = .preOffer case .offer: type = .offer case .answer: type = .answer case .provisionalAnswer: type = .provisionalAnswer case .iceCandidates(_, _): type = .iceCandidates case .endCall: type = .endCall } - let callMessageProto = SNProtoCallMessage.builder(type: type) + let callMessageProto = SNProtoCallMessage.builder(type: type, uuid: uuid) if let sdps = sdps, !sdps.isEmpty { callMessageProto.setSdps(sdps) } @@ -132,6 +143,7 @@ public final class CallMessage : ControlMessage { public override var description: String { """ CallMessage( + uuid: \(uuid ?? "null"), kind: \(kind?.description ?? "null"), sdps: \(sdps?.description ?? "null") ) diff --git a/SessionMessagingKit/Protos/Generated/SNProto.swift b/SessionMessagingKit/Protos/Generated/SNProto.swift index dae62350a..e4263501a 100644 --- a/SessionMessagingKit/Protos/Generated/SNProto.swift +++ b/SessionMessagingKit/Protos/Generated/SNProto.swift @@ -658,6 +658,7 @@ extension SNProtoContent.SNProtoContentBuilder { case provisionalAnswer = 3 case iceCandidates = 4 case endCall = 5 + case preOffer = 6 } private class func SNProtoCallMessageTypeWrap(_ value: SessionProtos_CallMessage.TypeEnum) -> SNProtoCallMessageType { @@ -667,6 +668,7 @@ extension SNProtoContent.SNProtoContentBuilder { case .provisionalAnswer: return .provisionalAnswer case .iceCandidates: return .iceCandidates case .endCall: return .endCall + case .preOffer: return .preOffer } } @@ -677,18 +679,19 @@ extension SNProtoContent.SNProtoContentBuilder { case .provisionalAnswer: return .provisionalAnswer case .iceCandidates: return .iceCandidates case .endCall: return .endCall + case .preOffer: return .preOffer } } // MARK: - SNProtoCallMessageBuilder - @objc public class func builder(type: SNProtoCallMessageType) -> SNProtoCallMessageBuilder { - return SNProtoCallMessageBuilder(type: type) + @objc public class func builder(type: SNProtoCallMessageType, uuid: String) -> SNProtoCallMessageBuilder { + return SNProtoCallMessageBuilder(type: type, uuid: uuid) } // asBuilder() constructs a builder that reflects the proto's contents. @objc public func asBuilder() -> SNProtoCallMessageBuilder { - let builder = SNProtoCallMessageBuilder(type: type) + let builder = SNProtoCallMessageBuilder(type: type, uuid: uuid) builder.setSdps(sdps) builder.setSdpMlineIndexes(sdpMlineIndexes) builder.setSdpMids(sdpMids) @@ -701,10 +704,11 @@ extension SNProtoContent.SNProtoContentBuilder { @objc fileprivate override init() {} - @objc fileprivate init(type: SNProtoCallMessageType) { + @objc fileprivate init(type: SNProtoCallMessageType, uuid: String) { super.init() setType(type) + setUuid(uuid) } @objc public func setType(_ valueParam: SNProtoCallMessageType) { @@ -741,6 +745,10 @@ extension SNProtoContent.SNProtoContentBuilder { proto.sdpMids = wrappedItems } + @objc public func setUuid(_ valueParam: String) { + proto.uuid = valueParam + } + @objc public func build() throws -> SNProtoCallMessage { return try SNProtoCallMessage.parseProto(proto) } @@ -754,6 +762,8 @@ extension SNProtoContent.SNProtoContentBuilder { @objc public let type: SNProtoCallMessageType + @objc public let uuid: String + @objc public var sdps: [String] { return proto.sdps } @@ -767,9 +777,11 @@ extension SNProtoContent.SNProtoContentBuilder { } private init(proto: SessionProtos_CallMessage, - type: SNProtoCallMessageType) { + type: SNProtoCallMessageType, + uuid: String) { self.proto = proto self.type = type + self.uuid = uuid } @objc @@ -788,12 +800,18 @@ extension SNProtoContent.SNProtoContentBuilder { } let type = SNProtoCallMessageTypeWrap(proto.type) + guard proto.hasUuid else { + throw SNProtoError.invalidProtobuf(description: "\(logTag) missing required field: uuid") + } + let uuid = proto.uuid + // MARK: - Begin Validation Logic for SNProtoCallMessage - // MARK: - End Validation Logic for SNProtoCallMessage - let result = SNProtoCallMessage(proto: proto, - type: type) + type: type, + uuid: uuid) return result } diff --git a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift index 7b4894f60..b3b69166d 100644 --- a/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift +++ b/SessionMessagingKit/Protos/Generated/SessionProtos.pb.swift @@ -311,7 +311,7 @@ struct SessionProtos_CallMessage { /// @required var type: SessionProtos_CallMessage.TypeEnum { - get {return _type ?? .offer} + get {return _type ?? .preOffer} set {_type = newValue} } /// Returns true if `type` has been explicitly set. @@ -325,10 +325,21 @@ struct SessionProtos_CallMessage { var sdpMids: [String] = [] + /// @required + var uuid: String { + get {return _uuid ?? String()} + set {_uuid = newValue} + } + /// Returns true if `uuid` has been explicitly set. + var hasUuid: Bool {return self._uuid != nil} + /// Clears the value of `uuid`. Subsequent reads from it will return its default value. + mutating func clearUuid() {self._uuid = nil} + var unknownFields = SwiftProtobuf.UnknownStorage() enum TypeEnum: SwiftProtobuf.Enum { typealias RawValue = Int + case preOffer // = 6 case offer // = 1 case answer // = 2 case provisionalAnswer // = 3 @@ -336,7 +347,7 @@ struct SessionProtos_CallMessage { case endCall // = 5 init() { - self = .offer + self = .preOffer } init?(rawValue: Int) { @@ -346,6 +357,7 @@ struct SessionProtos_CallMessage { case 3: self = .provisionalAnswer case 4: self = .iceCandidates case 5: self = .endCall + case 6: self = .preOffer default: return nil } } @@ -357,6 +369,7 @@ struct SessionProtos_CallMessage { case .provisionalAnswer: return 3 case .iceCandidates: return 4 case .endCall: return 5 + case .preOffer: return 6 } } @@ -365,6 +378,7 @@ struct SessionProtos_CallMessage { init() {} fileprivate var _type: SessionProtos_CallMessage.TypeEnum? = nil + fileprivate var _uuid: String? = nil } #if swift(>=4.2) @@ -1798,10 +1812,12 @@ extension SessionProtos_CallMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa 2: .same(proto: "sdps"), 3: .same(proto: "sdpMLineIndexes"), 4: .same(proto: "sdpMids"), + 5: .same(proto: "uuid"), ] public var isInitialized: Bool { if self._type == nil {return false} + if self._uuid == nil {return false} return true } @@ -1815,6 +1831,7 @@ extension SessionProtos_CallMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa case 2: try { try decoder.decodeRepeatedStringField(value: &self.sdps) }() case 3: try { try decoder.decodeRepeatedUInt32Field(value: &self.sdpMlineIndexes) }() case 4: try { try decoder.decodeRepeatedStringField(value: &self.sdpMids) }() + case 5: try { try decoder.decodeSingularStringField(value: &self._uuid) }() default: break } } @@ -1833,6 +1850,9 @@ extension SessionProtos_CallMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if !self.sdpMids.isEmpty { try visitor.visitRepeatedStringField(value: self.sdpMids, fieldNumber: 4) } + if let v = self._uuid { + try visitor.visitSingularStringField(value: v, fieldNumber: 5) + } try unknownFields.traverse(visitor: &visitor) } @@ -1841,6 +1861,7 @@ extension SessionProtos_CallMessage: SwiftProtobuf.Message, SwiftProtobuf._Messa if lhs.sdps != rhs.sdps {return false} if lhs.sdpMlineIndexes != rhs.sdpMlineIndexes {return false} if lhs.sdpMids != rhs.sdpMids {return false} + if lhs._uuid != rhs._uuid {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } @@ -1853,6 +1874,7 @@ extension SessionProtos_CallMessage.TypeEnum: SwiftProtobuf._ProtoNameProviding 3: .same(proto: "PROVISIONAL_ANSWER"), 4: .same(proto: "ICE_CANDIDATES"), 5: .same(proto: "END_CALL"), + 6: .same(proto: "PRE_OFFER"), ] } diff --git a/SessionMessagingKit/Protos/SessionProtos.proto b/SessionMessagingKit/Protos/SessionProtos.proto index 4193d202c..052ae7e5f 100644 --- a/SessionMessagingKit/Protos/SessionProtos.proto +++ b/SessionMessagingKit/Protos/SessionProtos.proto @@ -54,6 +54,7 @@ message Content { message CallMessage { enum Type { + PRE_OFFER = 6; OFFER = 1; ANSWER = 2; PROVISIONAL_ANSWER = 3; @@ -68,6 +69,8 @@ message CallMessage { repeated string sdps = 2; repeated uint32 sdpMLineIndexes = 3; repeated string sdpMids = 4; + // @required + required string uuid = 5; } message KeyPair { diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 94196ac00..bf5263d50 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -269,17 +269,17 @@ extension MessageReceiver { if let current = WebRTCSession.current { result = current } else { - WebRTCSession.current = WebRTCSession(for: message.sender!) + WebRTCSession.current = WebRTCSession(for: message.sender!, with: message.uuid!) result = WebRTCSession.current! } return result } switch message.kind! { + case .preOffer: + print("[Calls] Received pre-offer message.") + // TODO: Notify incoming call case .offer: print("[Calls] Received offer message.") - // Delegate to the main app, which is expected to show a dialog confirming - // that the user wants to pick up the call. When they do, the SDP contained - // in the offer message will be passed to WebRTCSession.handleRemoteSDP(_:from:). let storage = SNMessagingKitConfiguration.shared.storage let transaction = transaction as! YapDatabaseReadWriteTransaction if let threadID = storage.getOrCreateThread(for: message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: nil, using: transaction), @@ -287,6 +287,9 @@ extension MessageReceiver { let tsMessage = TSIncomingMessage.from(message, associatedWith: thread) tsMessage.save(with: transaction) } + // Delegate to the main app, which is expected to show a dialog confirming + // that the user wants to pick up the call. When they do, the SDP contained + // in the offer message will be passed to WebRTCSession.handleRemoteSDP(_:from:). handleOfferCallMessage?(message) case .answer: print("[Calls] Received answer message.")