diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index f1b67cb55..9cc6c46ff 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ 456F6E231E24133500FD2210 /* Platform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2041E0D74AC003D14BE /* Platform.swift */; }; 456F6E241E24133E00FD2210 /* CallKitCallUIAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F659721E1BD99C00444429 /* CallKitCallUIAdaptee.swift */; }; 456F6E251E24216100FD2210 /* DataChannelMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45464DBB1DFA041F001D3FD6 /* DataChannelMessage.swift */; }; + 456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */; }; 4574A5D61DD6704700C6B692 /* CallService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4574A5D51DD6704700C6B692 /* CallService.swift */; }; 45794E861E00620000066731 /* CallUIAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45794E851E00620000066731 /* CallUIAdapter.swift */; }; 45843D1F1D2236B30013E85A /* OWSContactsSearcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 45843D1E1D2236B30013E85A /* OWSContactsSearcher.m */; }; @@ -634,6 +635,7 @@ 45666F7A1D9C0533008FE134 /* OWSDatabaseMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDatabaseMigration.m; path = Migrations/OWSDatabaseMigration.m; sourceTree = ""; }; 45666F7C1D9C0814008FE134 /* OWSDatabaseMigrationRunner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSDatabaseMigrationRunner.h; path = Migrations/OWSDatabaseMigrationRunner.h; sourceTree = ""; }; 45666F7D1D9C0814008FE134 /* OWSDatabaseMigrationRunner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDatabaseMigrationRunner.m; path = Migrations/OWSDatabaseMigrationRunner.m; sourceTree = ""; }; + 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerConnectionClientTest.swift; sourceTree = ""; }; 4574A5D51DD6704700C6B692 /* CallService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallService.swift; sourceTree = ""; }; 45794E851E00620000066731 /* CallUIAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CallUIAdapter.swift; path = UserInterface/CallUIAdapter.swift; sourceTree = ""; }; 45843D1D1D2236B30013E85A /* OWSContactsSearcher.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsSearcher.h; sourceTree = ""; }; @@ -2146,6 +2148,7 @@ B660F6731C29867F00687D6E /* call */ = { isa = PBXGroup; children = ( + 456F6E2E1E261D1000FD2210 /* PeerConnectionClientTest.swift */, B660F6741C29867F00687D6E /* RecentCallTest.m */, ); path = call; @@ -3240,6 +3243,7 @@ B660F70D1C29988E00687D6E /* AnonymousAudioCallbackHandler.m in Sources */, B660F70E1C29988E00687D6E /* RemoteIOAudio.m in Sources */, B660F70F1C29988E00687D6E /* RemoteIOBufferListWrapper.m in Sources */, + 456F6E2F1E261D1000FD2210 /* PeerConnectionClientTest.swift in Sources */, B660F7101C29988E00687D6E /* SpeexCodec.m in Sources */, B660F7111C29988E00687D6E /* SoundBoard.m in Sources */, B660F7121C29988E00687D6E /* SoundInstance.m in Sources */, diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 92c2c3f96..e09a9d24f 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -75,7 +75,7 @@ enum CallError: Error { // FIXME TODO do we need to timeout? fileprivate let timeoutSeconds = 60 -@objc class CallService: NSObject, RTCDataChannelDelegate, RTCPeerConnectionDelegate { +@objc class CallService: NSObject, PeerConnectionClientDelegate, RTCDataChannelDelegate { // MARK: - Properties @@ -167,7 +167,7 @@ fileprivate let timeoutSeconds = 60 return getIceServers().then(on: CallService.signalingQueue) { iceServers -> Promise in Logger.debug("\(self.TAG) got ice servers:\(iceServers)") - let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, peerConnectionDelegate: self) + let peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self) self.peerConnectionClient = peerConnectionClient // When calling, it's our responsibility to create the DataChannel. Receivers will not have to do this explicitly. @@ -315,7 +315,7 @@ fileprivate let timeoutSeconds = 60 }.then(on: CallService.signalingQueue) { (iceServers: [RTCIceServer]) -> Promise in // FIXME for first time call recipients I think we'll see mic/camera permission requests here, // even though, from the users perspective, no incoming call is yet visible. - self.peerConnectionClient = PeerConnectionClient(iceServers: iceServers, peerConnectionDelegate: self) + self.peerConnectionClient = PeerConnectionClient(iceServers: iceServers, delegate: self) let offerSessionDescription = RTCSessionDescription(type: .offer, sdp: callerSessionDescription) let constraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) @@ -323,7 +323,6 @@ fileprivate let timeoutSeconds = 60 // Find a sessionDescription compatible with my constraints and the remote sessionDescription return self.peerConnectionClient!.negotiateSessionDescription(remoteDescription: offerSessionDescription, constraints: constraints) }.then(on: CallService.signalingQueue) { (negotiatedSessionDescription: HardenedRTCSessionDescription) in - // TODO? WebRtcCallService.this.lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); Logger.debug("\(self.TAG) set the remote description") let answerMessage = OWSCallAnswerMessage(callId: newCall.signalingId, sessionDescription: negotiatedSessionDescription.sdp) @@ -784,6 +783,37 @@ fileprivate let timeoutSeconds = 60 } } + // MARK: - PeerConnectionClientDelegate + + /** + * The connection has been established. The clients can now communicate. + */ + internal func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient) { + CallService.signalingQueue.async { + self.handleIceConnected() + } + } + + /** + * The connection failed to establish. The clients will not be able to communicate. + */ + internal func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient) { + CallService.signalingQueue.async { + self.handleFailedCall(error: CallError.disconnected) + } + } + + /** + * During the Signaling process each client generates IceCandidates locally, which contain information about how to + * reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client + * out of band, as part of establishing a connection over WebRTC. + */ + internal func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) { + CallService.signalingQueue.async { + self.handleLocalAddedIceCandidate(iceCandidate) + } + } + // MARK: Helpers /** @@ -799,7 +829,8 @@ fileprivate let timeoutSeconds = 60 } /** - * + * RTCIceServers are used when attempting to establish an optimal connection to the other party. SignalService supplies + * a list of servers, plus we have fallback servers hardcoded in the app. */ private func getIceServers() -> Promise<[RTCIceServer]> { return firstly { @@ -836,38 +867,15 @@ fileprivate let timeoutSeconds = 60 terminateCall() } + /** + * Clean up any existing call state and get ready to receive a new call. + */ private func terminateCall() { assertOnSignalingQueue() - -// lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING); -// NotificationBarManager.setCallEnded(this); -// -// incomingRinger.stop(); -// outgoingRinger.stop(); -// outgoingRinger.playDisconnected(); -// -// if (peerConnection != null) { -// peerConnection.dispose(); -// peerConnection = null; -// } -// -// if (eglBase != null && localRenderer != null && remoteRenderer != null) { -// localRenderer.release(); -// remoteRenderer.release(); -// eglBase.release(); -// } -// -// shutdownAudio(); -// -// this.callState = CallState.STATE_IDLE; -// this.recipient = null; -// this.callId = null; -// this.audioEnabled = false; -// this.videoEnabled = false; -// this.pendingIceUpdates = null; -// lockManager.updatePhoneState(LockManager.PhoneState.IDLE); + Logger.debug("\(TAG) in \(#function)") peerConnectionClient?.terminate() + peerConnectionClient = nil call = nil thread = nil @@ -877,11 +885,11 @@ fileprivate let timeoutSeconds = 60 } // MARK: - RTCDataChannelDelegate + // TODO move `RTCDataChannelDelegate` stuff into peerConnectionClient and add a method to peerConnectionClientDelegate `receiveDataChannelMssage(_ message:OWSWebRTCProtos) /** The data channel state changed. */ public func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { Logger.debug("\(TAG) dataChannelDidChangeState: \(dataChannel)") - // SignalingQueue.dispatch.async {} } /** The data channel successfully received a data buffer. */ @@ -903,138 +911,6 @@ fileprivate let timeoutSeconds = 60 public func dataChannel(_ dataChannel: RTCDataChannel, didChangeBufferedAmount amount: UInt64) { Logger.debug("\(TAG) didChangeBufferedAmount: \(amount)") } - - // MARK: - RTCPeerConnectionDelegate - - /** Called when the SignalingState changed. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { - Logger.debug("\(TAG) didChange signalingState:\(stateChanged.debugDescription)") - } - - /** Called when media is received on a new stream from remote peer. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { - Logger.debug("\(TAG) didAdd stream:\(stream)") - } - - /** Called when a remote peer closes a stream. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { - Logger.debug("\(TAG) didRemove Stream:\(stream)") - } - - /** Called when negotiation is needed, for example ICE has restarted. */ - public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { - Logger.debug("\(TAG) shouldNegotiate") - } - - /** Called any time the IceConnectionState changes. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { - Logger.debug("\(TAG) didChange IceConnectionState:\(newState.debugDescription)") - - CallService.signalingQueue.async { - switch newState { - case .connected, .completed: - self.handleIceConnected() - case .failed: - Logger.warn("\(self.TAG) RTCIceConnection failed.") - guard self.thread != nil else { - Logger.error("\(self.TAG) refusing to hangup for failed IceConnection because there is no current thread") - return - } - self.handleFailedCall(error: CallError.disconnected) - default: - Logger.debug("\(self.TAG) ignoring change IceConnectionState:\(newState.debugDescription)") - } - } - } - - /** Called any time the IceGatheringState changes. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { - Logger.debug("\(TAG) didChange IceGatheringState:\(newState.debugDescription)") - } - - /** New ice candidate has been found. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { - Logger.debug("\(TAG) didGenerate IceCandidate:\(candidate.sdp)") - CallService.signalingQueue.async { - self.handleLocalAddedIceCandidate(candidate) - } - } - - /** Called when a group of local Ice candidates have been removed. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { - Logger.debug("\(TAG) didRemove IceCandidates:\(candidates)") - } - - /** New data channel has been opened. */ - public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { - Logger.debug("\(TAG) didOpen dataChannel:\(dataChannel)") - CallService.signalingQueue.async { - guard let peerConnectionClient = self.peerConnectionClient else { - Logger.error("\(self.TAG) surprised to find nil peerConnectionClient in \(#function)") - return - } - - Logger.debug("\(self.TAG) set dataChannel") - peerConnectionClient.dataChannel = dataChannel - } - } -} - -// Mark: Pretty Print Objc enums. - -fileprivate extension RTCSignalingState { - var debugDescription: String { - switch self { - case .stable: - return "stable" - case .haveLocalOffer: - return "haveLocalOffer" - case .haveLocalPrAnswer: - return "haveLocalPrAnswer" - case .haveRemoteOffer: - return "haveRemoteOffer" - case .haveRemotePrAnswer: - return "haveRemotePrAnswer" - case .closed: - return "closed" - } - } -} - -fileprivate extension RTCIceGatheringState { - var debugDescription: String { - switch self { - case .new: - return "new" - case .gathering: - return "gathering" - case .complete: - return "complete" - } - } -} - -fileprivate extension RTCIceConnectionState { - var debugDescription: String { - switch self { - case .new: - return "new" - case .checking: - return "checking" - case .connected: - return "connected" - case .completed: - return "completed" - case .failed: - return "failed" - case .disconnected: - return "disconnected" - case .closed: - return "closed" - case .count: - return "count" - } - } } fileprivate extension MessageSender { diff --git a/Signal/src/call/PeerConnectionClient.swift b/Signal/src/call/PeerConnectionClient.swift index e4c8914bd..00e2e283d 100644 --- a/Signal/src/call/PeerConnectionClient.swift +++ b/Signal/src/call/PeerConnectionClient.swift @@ -8,14 +8,36 @@ import WebRTC let kAudioTrackType = kRTCMediaStreamTrackKindAudio let kVideoTrackType = kRTCMediaStreamTrackKindVideo +/** + * The PeerConnectionClient notifies it's delegate (the CallService) of key events in the call signaling life cycle + */ +protocol PeerConnectionClientDelegate: class { + + /** + * The connection has been established. The clients can now communicate. + */ + func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient) + + /** + * The connection failed to establish. The clients will not be able to communicate. + */ + func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient) + + /** + * During the Signaling process each client generates IceCandidates locally, which contain information about how to + * reach the local client via the internet. The delegate must shuttle these IceCandates to the other (remote) client + * out of band, as part of establishing a connection over WebRTC. + */ + func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) +} + /** * `PeerConnectionClient` is our interface to WebRTC. * * It is primarily a wrapper around `RTCPeerConnection`, which is responsible for sending and receiving our call data - * including audio, video, and some signaling - though the bulk of the signaling is *establishing* the connection, - * meaning we can't use the connection to transmit yet. + * including audio, video, and some post-connected signaling (hangup, add video) */ -class PeerConnectionClient: NSObject { +class PeerConnectionClient: NSObject, RTCPeerConnectionDelegate { let TAG = "[PeerConnectionClient]" enum Identifiers: String { @@ -25,9 +47,12 @@ class PeerConnectionClient: NSObject { dataChannelSignaling = "signaling" } + // Delegate is notified of key events in the call lifecycle. + private weak var delegate: PeerConnectionClientDelegate! + // Connection - private let peerConnection: RTCPeerConnection + internal var peerConnection: RTCPeerConnection! private let iceServers: [RTCIceServer] private let connectionConstraints: RTCMediaConstraints private let configuration: RTCConfiguration @@ -51,8 +76,9 @@ class PeerConnectionClient: NSObject { private var videoTrack: RTCVideoTrack? private var cameraConstraints: RTCMediaConstraints - init(iceServers: [RTCIceServer], peerConnectionDelegate: RTCPeerConnectionDelegate) { + init(iceServers: [RTCIceServer], delegate: PeerConnectionClientDelegate) { self.iceServers = iceServers + self.delegate = delegate configuration = RTCConfiguration() configuration.iceServers = iceServers @@ -61,14 +87,15 @@ class PeerConnectionClient: NSObject { let connectionConstraintsDict = ["DtlsSrtpKeyAgreement": "true"] connectionConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: connectionConstraintsDict) - peerConnection = factory.peerConnection(with: configuration, - constraints: connectionConstraints, - delegate: peerConnectionDelegate) audioConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints:nil) cameraConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) + super.init() + peerConnection = factory.peerConnection(with: configuration, + constraints: connectionConstraints, + delegate: self) createAudioSender() createVideoSender() } @@ -255,12 +282,14 @@ class PeerConnectionClient: NSObject { // audioTrack is a strong property because we need access to it to mute/unmute, but I was seeing it // become nil when it was only a weak property. So we retain it and manually nil the reference here, because // we are likely to crash if we retain any peer connection properties when the peerconnection is released + Logger.debug("\(TAG) in \(#function)") audioTrack = nil videoTrack = nil dataChannel = nil audioSender = nil videoSender = nil + peerConnection.delegate = nil peerConnection.close() } @@ -275,6 +304,67 @@ class PeerConnectionClient: NSObject { let buffer = RTCDataBuffer(data: data, isBinary: false) return dataChannel.sendData(buffer) } + + // MARK: - RTCPeerConnectionDelegate + + /** Called when the SignalingState changed. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) { + Logger.debug("\(TAG) didChange signalingState:\(stateChanged.debugDescription)") + } + + /** Called when media is received on a new stream from remote peer. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) { + Logger.debug("\(TAG) didAdd stream:\(stream)") + } + + /** Called when a remote peer closes a stream. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) { + Logger.debug("\(TAG) didRemove Stream:\(stream)") + } + + /** Called when negotiation is needed, for example ICE has restarted. */ + public func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) { + Logger.debug("\(TAG) shouldNegotiate") + } + + /** Called any time the IceConnectionState changes. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { + Logger.debug("\(TAG) didChange IceConnectionState:\(newState.debugDescription)") + switch newState { + case .connected, .completed: + self.delegate.peerConnectionClientIceConnected(self) + case .failed: + Logger.warn("\(self.TAG) RTCIceConnection failed.") + self.delegate.peerConnectionClientIceFailed(self) + default: + Logger.debug("\(self.TAG) ignoring change IceConnectionState:\(newState.debugDescription)") + } + } + + /** Called any time the IceGatheringState changes. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) { + Logger.debug("\(TAG) didChange IceGatheringState:\(newState.debugDescription)") + } + + /** New ice candidate has been found. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { + Logger.debug("\(TAG) didGenerate IceCandidate:\(candidate.sdp)") + self.delegate.peerConnectionClient(self, addedLocalIceCandidate: candidate) + } + + /** Called when a group of local Ice candidates have been removed. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) { + Logger.debug("\(TAG) didRemove IceCandidates:\(candidates)") + } + + /** New data channel has been opened. */ + public func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { + Logger.debug("\(TAG) didOpen dataChannel:\(dataChannel)") + CallService.signalingQueue.async { + Logger.debug("\(self.TAG) set dataChannel") + self.dataChannel = dataChannel + } + } } /** @@ -304,3 +394,60 @@ class HardenedRTCSessionDescription { return RTCSessionDescription.init(type: rtcSessionDescription.type, sdp: description) } } + +// Mark: Pretty Print Objc enums. + +fileprivate extension RTCSignalingState { + var debugDescription: String { + switch self { + case .stable: + return "stable" + case .haveLocalOffer: + return "haveLocalOffer" + case .haveLocalPrAnswer: + return "haveLocalPrAnswer" + case .haveRemoteOffer: + return "haveRemoteOffer" + case .haveRemotePrAnswer: + return "haveRemotePrAnswer" + case .closed: + return "closed" + } + } +} + +fileprivate extension RTCIceGatheringState { + var debugDescription: String { + switch self { + case .new: + return "new" + case .gathering: + return "gathering" + case .complete: + return "complete" + } + } +} + +fileprivate extension RTCIceConnectionState { + var debugDescription: String { + switch self { + case .new: + return "new" + case .checking: + return "checking" + case .connected: + return "connected" + case .completed: + return "completed" + case .failed: + return "failed" + case .disconnected: + return "disconnected" + case .closed: + return "closed" + case .count: + return "count" + } + } +} diff --git a/Signal/src/call/Speakerbox/CallKitCallManager.swift b/Signal/src/call/Speakerbox/CallKitCallManager.swift index a3efc1734..ee8f9165c 100644 --- a/Signal/src/call/Speakerbox/CallKitCallManager.swift +++ b/Signal/src/call/Speakerbox/CallKitCallManager.swift @@ -98,7 +98,6 @@ final class CallKitCallManager: NSObject { } } - fileprivate extension Array { mutating func removeFirst(where predicate: (Element) throws -> Bool) rethrows { @@ -108,5 +107,4 @@ fileprivate extension Array { remove(at: index) } - } diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index d5549c2c3..e6bb67535 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -42,7 +42,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { return providerConfiguration } - init(callService: CallService, notificationsAdapter: CallNotificationsAdapter) { + init(callService: CallService, notificationsAdapter: CallNotificationsAdapter) { self.callManager = CallKitCallManager() self.callService = callService self.notificationsAdapter = notificationsAdapter diff --git a/Signal/test/call/PeerConnectionClientTest.swift b/Signal/test/call/PeerConnectionClientTest.swift new file mode 100644 index 000000000..d60d7d520 --- /dev/null +++ b/Signal/test/call/PeerConnectionClientTest.swift @@ -0,0 +1,80 @@ +// Created by Michael Kirk on 1/11/17. +// Copyright © 2017 Open Whisper Systems. All rights reserved. + +import XCTest +import WebRTC + +/** + * Playing the role of the call service. + */ +class FakePeerConnectionClientDelegate: PeerConnectionClientDelegate { + + enum ConnectionState { + case connected, failed + } + + var connectionState: ConnectionState? + var localIceCandidates = [RTCIceCandidate]() + + internal func peerConnectionClientIceConnected(_ peerconnectionClient: PeerConnectionClient) { + connectionState = .connected + } + + internal func peerConnectionClientIceFailed(_ peerconnectionClient: PeerConnectionClient) { + connectionState = .failed + } + + internal func peerConnectionClient(_ peerconnectionClient: PeerConnectionClient, addedLocalIceCandidate iceCandidate: RTCIceCandidate) { + localIceCandidates.append(iceCandidate) + } +} + +class PeerConnectionClientTest: XCTestCase { + + var client: PeerConnectionClient! + var clientDelegate: FakePeerConnectionClientDelegate! + var peerConnection: RTCPeerConnection! + + override func setUp() { + super.setUp() + + let iceServers = [RTCIceServer]() + clientDelegate = FakePeerConnectionClientDelegate() + client = PeerConnectionClient(iceServers: iceServers, delegate: clientDelegate) + peerConnection = client.peerConnection + } + + override func tearDown() { + client.terminate() + + super.tearDown() + } + + func testIceConnectionStateChange() { + XCTAssertNil(clientDelegate.connectionState) + + client.peerConnection(peerConnection, didChange: RTCIceConnectionState.connected) + XCTAssertEqual(FakePeerConnectionClientDelegate.ConnectionState.connected, clientDelegate.connectionState) + + client.peerConnection(peerConnection, didChange: RTCIceConnectionState.completed) + XCTAssertEqual(FakePeerConnectionClientDelegate.ConnectionState.connected, clientDelegate.connectionState) + + client.peerConnection(peerConnection, didChange: RTCIceConnectionState.failed) + XCTAssertEqual(FakePeerConnectionClientDelegate.ConnectionState.failed, clientDelegate.connectionState) + } + + func testIceCandidateAdded() { + XCTAssertEqual(0, clientDelegate.localIceCandidates.count) + + let candidate1 = RTCIceCandidate(sdp: "sdp-1", sdpMLineIndex: 0, sdpMid: "sdpMid-1") + let candidate2 = RTCIceCandidate(sdp: "sdp-2", sdpMLineIndex: 0, sdpMid: "sdpMid-2") + let candidate3 = RTCIceCandidate(sdp: "sdp-3", sdpMLineIndex: 0, sdpMid: "sdpMid-3") + + client.peerConnection(peerConnection, didGenerate: candidate1) + client.peerConnection(peerConnection, didGenerate: candidate2) + client.peerConnection(peerConnection, didGenerate: candidate3) + + XCTAssertEqual(3, clientDelegate.localIceCandidates.count) + } + +}