diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 8823383d9..da45f9d5e 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -266,7 +266,7 @@ B8D64FCB25BA78A90029CFC0 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; }; B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84EA225DF745A005A043E /* LinkPreviewState.swift */; }; B8D84ECF25E3108A005A043E /* ExpandingAttachmentsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D84ECE25E3108A005A043E /* ExpandingAttachmentsButton.swift */; }; - B8DE1FB426C22F2F0079C9CE /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE1FB326C22F2F0079C9CE /* File.swift */; }; + B8DE1FB426C22F2F0079C9CE /* CallManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE1FB326C22F2F0079C9CE /* CallManager.swift */; }; B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */; }; B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */; }; B8EB20F02640F7F000773E52 /* OpenGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8EB20EF2640F7F000773E52 /* OpenGroupInvitationView.swift */; }; @@ -1248,7 +1248,7 @@ B8D8F1BC25661C6F0092EF10 /* Storage+OnionRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OnionRequests.swift"; sourceTree = ""; }; B8D8F1EF256621180092EF10 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "MessageSender+Convenience.swift"; path = "../../SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift"; sourceTree = ""; }; B8DE1FAF26C228780079C9CE /* SignalRingRTC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SignalRingRTC.framework; path = Dependencies/SignalRingRTC.framework; sourceTree = ""; }; - B8DE1FB326C22F2F0079C9CE /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = ""; }; + B8DE1FB326C22F2F0079C9CE /* CallManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallManager.swift; sourceTree = ""; }; B8DE1FB526C22FCB0079C9CE /* CallMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallMessage.swift; sourceTree = ""; }; B8EB20E6263F7E4B00773E52 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = ""; }; B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+OpenGroupInvitation.swift"; sourceTree = ""; }; @@ -2334,17 +2334,10 @@ path = Shared; sourceTree = ""; }; - B8DE1FB126C22AF20079C9CE /* Calls */ = { - isa = PBXGroup; - children = ( - ); - path = Calls; - sourceTree = ""; - }; B8DE1FB226C22F1F0079C9CE /* Calls */ = { isa = PBXGroup; children = ( - B8DE1FB326C22F2F0079C9CE /* File.swift */, + B8DE1FB326C22F2F0079C9CE /* CallManager.swift */, ); path = Calls; sourceTree = ""; @@ -3534,7 +3527,6 @@ children = ( C3F0A58F255C8E3D007BE2A3 /* Meta */, C36096BC25AD1C3E008B62B2 /* Backups */, - B8DE1FB126C22AF20079C9CE /* Calls */, C360969C25AD18BA008B62B2 /* Closed Groups */, B835246C25C38AA20089A44F /* Conversations */, C32B405424A961E1001117B5 /* Dependencies */, @@ -4693,7 +4685,7 @@ B8856ECE256F1E58001CE70E /* OWSPreferences.m in Sources */, C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */, C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */, - B8DE1FB426C22F2F0079C9CE /* File.swift in Sources */, + B8DE1FB426C22F2F0079C9CE /* CallManager.swift in Sources */, C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */, C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */, B8DE1FB626C22FCB0079C9CE /* CallMessage.swift in Sources */, diff --git a/SessionMessagingKit/Calls/CallManager.swift b/SessionMessagingKit/Calls/CallManager.swift new file mode 100644 index 000000000..1660e9a67 --- /dev/null +++ b/SessionMessagingKit/Calls/CallManager.swift @@ -0,0 +1,190 @@ +import PromiseKit +import WebRTC + +/// See https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription for more information. +public final class CallManager : NSObject, RTCPeerConnectionDelegate { + + private lazy var factory: RTCPeerConnectionFactory = { + RTCInitializeSSL() + let videoEncoderFactory = RTCVideoEncoderFactoryH264() + let videoDecoderFactory = RTCVideoDecoderFactoryH264() + return RTCPeerConnectionFactory(encoderFactory: videoEncoderFactory, decoderFactory: videoDecoderFactory) + }() + + /// Represents a WebRTC connection between the user and a remote peer. Provides methods to connect to a + /// remote peer, maintain and monitor the connection, and close the connection once it's no longer needed. + private lazy var peerConnection: RTCPeerConnection = { + let configuration = RTCConfiguration() + // TODO: Configure + // TODO: Do these constraints make sense? + let constraints = RTCMediaConstraints(mandatoryConstraints: [:], optionalConstraints: [ "DtlsSrtpKeyAgreement" : "true" ]) + return factory.peerConnection(with: configuration, constraints: constraints, delegate: self) + }() + + private lazy var constraints: RTCMediaConstraints = { + let mandatory: [String:String] = [ + kRTCMediaConstraintsOfferToReceiveAudio : kRTCMediaConstraintsValueTrue, + kRTCMediaConstraintsOfferToReceiveVideo : kRTCMediaConstraintsValueTrue + ] + let optional: [String:String] = [:] + // TODO: Do these constraints make sense? + return RTCMediaConstraints(mandatoryConstraints: mandatory, optionalConstraints: optional) + }() + + // Audio + private lazy var audioSource: RTCAudioSource = { + // TODO: Do these constraints make sense? + let constraints = RTCMediaConstraints(mandatoryConstraints: [:], optionalConstraints: [:]) + return factory.audioSource(with: constraints) + }() + + private lazy var audioTrack: RTCAudioTrack = { + return factory.audioTrack(with: audioSource, trackId: "ARDAMSa0") + }() + + // Video + private lazy var localVideoSource: RTCVideoSource = { + return factory.videoSource() + }() + + private lazy var localVideoTrack: RTCVideoTrack = { + return factory.videoTrack(with: localVideoSource, trackId: "ARDAMSv0") + }() + + private lazy var videoCapturer: RTCVideoCapturer = { + return RTCCameraVideoCapturer(delegate: localVideoSource) + }() + + private lazy var remoteVideoTrack: RTCVideoTrack? = { + return peerConnection.receivers.first { $0.track.kind == "video" }?.track as? RTCVideoTrack + }() + + // Stream + private lazy var stream: RTCMediaStream = { + let result = factory.mediaStream(withStreamId: "ARDAMS") + result.addAudioTrack(audioTrack) + result.addVideoTrack(localVideoTrack) + return result + }() + + // MARK: Error + public enum Error : LocalizedError { + case noThread + + public var errorDescription: String? { + switch self { + case .noThread: return "Couldn't find thread for contact." + } + } + } + + // MARK: Initialization + private override init() { + super.init() + peerConnection.add(stream) + // Configure audio session + let audioSession = RTCAudioSession.sharedInstance() + audioSession.lockForConfiguration() + do { + try audioSession.setCategory(AVAudioSession.Category.playAndRecord.rawValue) + try audioSession.setMode(AVAudioSession.Mode.voiceChat.rawValue) + try audioSession.overrideOutputAudioPort(.speaker) + try audioSession.setActive(true) + } catch let error { + SNLog("Couldn't set up WebRTC audio session due to error: \(error)") + } + audioSession.unlockForConfiguration() + } + + public static let shared = CallManager() + + // MARK: Call Management + public func initiateCall(with publicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + guard let thread = TSContactThread.fetch(for: publicKey, using: transaction) else { return Promise(error: Error.noThread) } + let (promise, seal) = Promise.pending() + peerConnection.offer(for: constraints) { [weak self] sdp, error in + if let error = error { + seal.reject(error) + } else { + guard let self = self, let sdp = sdp else { preconditionFailure() } + self.peerConnection.setLocalDescription(sdp) { error in + if let error = error { + print("Couldn't initiate call due to error: \(error).") + return seal.reject(error) + } + } + let message = CallMessage() + message.type = .offer + message.sdp = sdp.sdp + MessageSender.send(message, in: thread, using: transaction) + seal.fulfill(()) + } + } + return promise + } + + public func acceptCall(with publicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + guard let thread = TSContactThread.fetch(for: publicKey, using: transaction) else { return Promise(error: Error.noThread) } + let (promise, seal) = Promise.pending() + peerConnection.answer(for: constraints) { [weak self] sdp, error in + if let error = error { + seal.reject(error) + } else { + guard let self = self, let sdp = sdp else { preconditionFailure() } + self.peerConnection.setLocalDescription(sdp) { error in + if let error = error { + print("Couldn't accept call due to error: \(error).") + return seal.reject(error) + } + } + let message = CallMessage() + message.type = .answer + message.sdp = sdp.sdp + MessageSender.send(message, in: thread, using: transaction) + seal.fulfill(()) + } + } + return promise + } + + public func endCall() { + peerConnection.close() + } + + // MARK: Delegate + public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCSignalingState) { + SNLog("Signaling state changed to: \(state).") + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { + // Do nothing + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { + // Do nothing + } + + public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { + // Do nothing + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceConnectionState) { + SNLog("ICE connection state changed to: \(state).") + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didChange state: RTCIceGatheringState) { + SNLog("ICE gathering state changed to: \(state).") + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { + SNLog("ICE candidate generated.") + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { + SNLog("\(candidates.count) ICE candidate(s) removed.") + } + + public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { + SNLog("Data channel opened.") + } +} diff --git a/SessionMessagingKit/Calls/File.swift b/SessionMessagingKit/Calls/File.swift deleted file mode 100644 index 16c7be30c..000000000 --- a/SessionMessagingKit/Calls/File.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -import WebRTC - -//The RTCSessionDescription interface describes one end of a connection—or potential connection—and how it's configured. Each RTCSessionDescription consists of a description type indicating which part of the offer/answer negotiation process it describes and of the SDP descriptor of the session. - -enum Foo { - - func bar() { - - - RTCSessionDescription(type: .answer, sdp: "") - } -} diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index e06c502b9..7bf42c26e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -52,7 +52,7 @@ extension MessageReceiver { public static func showTypingIndicatorIfNeeded(for senderPublicKey: String) { var threadOrNil: TSContactThread? Storage.read { transaction in - threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction) + threadOrNil = TSContactThread.fetch(for: senderPublicKey, using: transaction) } guard let thread = threadOrNil else { return } func showTypingIndicatorsIfNeeded() { @@ -70,7 +70,7 @@ extension MessageReceiver { public static func hideTypingIndicatorIfNeeded(for senderPublicKey: String) { var threadOrNil: TSContactThread? Storage.read { transaction in - threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction) + threadOrNil = TSContactThread.fetch(for: senderPublicKey, using: transaction) } guard let thread = threadOrNil else { return } func hideTypingIndicatorsIfNeeded() { @@ -88,7 +88,7 @@ extension MessageReceiver { public static func cancelTypingIndicatorsIfNeeded(for senderPublicKey: String) { var threadOrNil: TSContactThread? Storage.read { transaction in - threadOrNil = TSContactThread.getWithContactSessionID(senderPublicKey, transaction: transaction) + threadOrNil = TSContactThread.fetch(for: senderPublicKey, using: transaction) } guard let thread = threadOrNil else { return } func cancelTypingIndicatorsIfNeeded() { @@ -110,7 +110,7 @@ extension MessageReceiver { private static func handleDataExtractionNotification(_ message: DataExtractionNotification, using transaction: Any) { let transaction = transaction as! YapDatabaseReadWriteTransaction guard message.groupPublicKey == nil, - let thread = TSContactThread.getWithContactSessionID(message.sender!, transaction: transaction) else { return } + let thread = TSContactThread.fetch(for: message.sender!, using: transaction) else { return } let type: TSInfoMessageType switch message.kind! { case .screenshot: type = .screenshotNotification @@ -140,7 +140,7 @@ extension MessageReceiver { let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) threadOrNil = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) } else { - threadOrNil = TSContactThread.getWithContactSessionID(syncTarget ?? senderPublicKey, transaction: transaction) + threadOrNil = TSContactThread.fetch(for: syncTarget ?? senderPublicKey, using: transaction) } guard let thread = threadOrNil else { return } let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: true, durationSeconds: duration) @@ -160,7 +160,7 @@ extension MessageReceiver { let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey) threadOrNil = TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupID), transaction: transaction) } else { - threadOrNil = TSContactThread.getWithContactSessionID(syncTarget ?? senderPublicKey, transaction: transaction) + threadOrNil = TSContactThread.fetch(for: syncTarget ?? senderPublicKey, using: transaction) } guard let thread = threadOrNil else { return } let configuration = OWSDisappearingMessagesConfiguration(threadId: thread.uniqueId!, enabled: false, durationSeconds: 24 * 60 * 60) diff --git a/SessionMessagingKit/Threads/TSContactThread.h b/SessionMessagingKit/Threads/TSContactThread.h index f40a7b98c..f3e6aa085 100644 --- a/SessionMessagingKit/Threads/TSContactThread.h +++ b/SessionMessagingKit/Threads/TSContactThread.h @@ -18,7 +18,7 @@ extern NSString *const TSContactThreadPrefix; transaction:(YapDatabaseReadWriteTransaction *)transaction; // Unlike getOrCreateThreadWithContactSessionID, this will _NOT_ create a thread if one does not already exist. -+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction; ++ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction NS_SWIFT_NAME(fetch(for:using:)); - (NSString *)contactSessionID;